diff --git a/PRIVACY.md b/PRIVACY.md index 3b4ba2ba8a..66dff0e807 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,5 +1,6 @@ --- layout: layouts/privacy.html +permalink: /privacy/index.html --- # SimpleX Chat Privacy Policy and Conditions of Use @@ -10,7 +11,7 @@ SimpleX Chat communication protocol is the first protocol that has no user profi Double ratchet algorithm has such important properties as [forward secrecy](/docs/GLOSSARY.md#forward-secrecy), sender [repudiation](/docs/GLOSSARY.md#) and break-in recovery (also known as [post-compromise security](/docs/GLOSSARY.md#post-compromise-security)). -If you believe that any part of this document is not aligned with our mission or values, please raise it with us via [email](chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion). +If you believe that any part of this document is not aligned with our mission or values, please raise it with us via [email](mailto:chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion). ## Privacy Policy @@ -78,7 +79,7 @@ When you choose to use instant push notifications in SimpleX iOS app, because th Preset notification server cannot observe the actual addresses of these queues, as a separate address is used to subscribe to the notifications. It also cannot observe who sends messages to you. Apple push notifications servers can only observe how many notifications are sent to you, but not from how many contacts, or from which messaging relays, as notifications are delivered to your device end-to-end encrypted by one of the preset notification servers - these notifications only contain end-to-end encrypted metadata, not even encrypted message content, and they look completely random to Apple push notification servers. -You can read more about the design of iOS push notifications [here](https://simplex.chat/blog/20220404-simplex-chat-instant-notifications.html#our-ios-approach-has-one-trade-off). +You can read more about the design of iOS push notifications [here](./blog/20220404-simplex-chat-instant-notifications.md#our-ios-approach-has-one-trade-off). #### Another information stored on the servers @@ -115,7 +116,7 @@ We will update this Privacy Policy as needed so that it is current, accurate, an Please also read our Conditions of Use of Software and Infrastructure below. -If you have questions about our Privacy Policy please contact us via [email](chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion). +If you have questions about our Privacy Policy please contact us via [email](mailto:chat@simplex.chat) or [chat](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%253D%26srv%3Dbylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion). ## Conditions of Use of Software and Infrastructure diff --git a/README.md b/README.md index 697f5ca08e..1a500187c0 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ 2. ↔️ [Connect to the team](#connect-to-the-team), [join user groups](#join-user-groups) and [follow our updates](#follow-our-updates). 3. 🤝 [Make a private connection](#make-a-private-connection) with a friend. 4. 🔤 [Help translating SimpleX Chat](#help-translating-simplex-chat). -5. ⚡️ [Contribute](#contribute) and [help us with donations](#help-us-with-donations). +5. ⚡️ [Contribute](#contribute) and [support us with donations](#please-support-us-with-your-donations). [Learn more about SimpleX Chat](#contents). @@ -150,7 +150,7 @@ We would love to have you join the development! You can help us with: - contributing to SimpleX Chat knowledge-base. - developing features - please connect to us via chat so we can help you get started. -## Help us with donations +## Please support us with your donations Huge thank you to everybody who donated to SimpleX Chat! @@ -158,20 +158,19 @@ We are prioritizing users privacy and security - it would be impossible without Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure. -Your donations help us raise more funds – any amount, even the price of the cup of coffee, would make a big difference for us. +Your donations help us raise more funds - any amount, even the price of the cup of coffee, would make a big difference for us. It is possible to donate via: -- [GitHub](https://github.com/sponsors/simplex-chat) - it is commission-free for us. -- [OpenCollective](https://opencollective.com/simplex-chat) - it charges a commission, and also accepts donations in crypto-currencies. +- [GitHub](https://github.com/sponsors/simplex-chat) (commission-free) or [OpenCollective](https://opencollective.com/simplex-chat) (~10% commission). +- Bitcoin: bc1qd74rc032ek2knhhr3yjq2ajzc5enz3h4qwnxad - Monero: 8568eeVjaJ1RQ65ZUn9PRQ8ENtqeX9VVhcCYYhnVLxhV4JtBqw42so2VEUDQZNkFfsH5sXCuV7FN8VhRQ21DkNibTZP57Qt -- Bitcoin: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG -- BCH: 1bpefFkzuRoMY3ZuBbZNZxycbg7NYPYTG +- BCH: bitcoincash:qq6c8vfvxqrk6rhdysgvkhqc24sggkfsx5nqvdlqcg +- Ethereum: 0xD9ee7Db0AD0dc1Dfa7eD53290199ED06beA04692 - USDT: - - BNB Smart Chain: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2 - - Tron: TNnTrKLBmdy2Wn3cAQR98dAVvWhLskQGfW -- Ethereum: 0x83fd788f7241a2be61780ea9dc72d2151e6843e2 -- Solana: 43tWFWDczgAcn4Rzwkpqg2mqwnQETSiTwznmCgA2tf1L + - Ethereum: 0xD9ee7Db0AD0dc1Dfa7eD53290199ED06beA04692 +- Solana: 7JCf5m3TiHmYKZVr6jCu1KeZVtb9Y1jRMQDU69p5ARnu +- please ask if you want to donate any other coins. Thank you, @@ -234,6 +233,10 @@ You can use SimpleX with your own servers and still communicate with people usin Recent and important updates: +[Aug 14, 2024. SimpleX network: the investment from Jack Dorsey and Asymmetric, v6.0 released with the new user experience and private message routing](./blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md) + +[Jun 4, 2024. SimpleX network: private message routing, v5.8 released with IP address protection and chat themes](./blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md) + [Apr 26, 2024. SimpleX network: legally binding transparency, v5.7 released with better calls and messages.](./blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md) [Mar 23, 2024. SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration.](./blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md) @@ -382,10 +385,10 @@ Please also join [#simplex-devs](https://simplex.chat/contact#/?v=1-2&smp=smp%3A - ✅ Private notes. - ✅ Improve sending videos (including encryption of locally stored videos). - ✅ Post-quantum resistant key exchange in double ratchet protocol. +- ✅ Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic). - 🏗 Improve stability and reduce battery usage. - 🏗 Improve experience for the new users. - 🏗 Large groups, communities and public channels. -- 🏗 Message delivery relay for senders (to conceal IP address from the recipients' servers and to reduce the traffic). - Privacy & security slider - a simple way to set all settings at once. - SMP queue redundancy and rotation (manual is supported). - Include optional message into connection request sent via contact address. diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index 7204625ad4..dd91d4af68 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -9,35 +9,18 @@ import Foundation import UIKit import SimpleXChat +import SwiftUI class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { logger.debug("AppDelegate: didFinishLaunchingWithOptions") application.registerForRemoteNotifications() - if #available(iOS 17.0, *) { trackKeyboard() } NotificationCenter.default.addObserver(self, selector: #selector(pasteboardChanged), name: UIPasteboard.changedNotification, object: nil) removePasscodesIfReinstalled() + prepareForLaunch() return true } - @available(iOS 17.0, *) - private func trackKeyboard() { - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) - } - - @available(iOS 17.0, *) - @objc func keyboardWillShow(_ notification: Notification) { - if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue { - ChatModel.shared.keyboardHeight = keyboardFrame.cgRectValue.height - } - } - - @available(iOS 17.0, *) - @objc func keyboardWillHide(_ notification: Notification) { - ChatModel.shared.keyboardHeight = 0 - } - @objc func pasteboardChanged() { ChatModel.shared.pasteboardHasStrings = UIPasteboard.general.hasStrings } @@ -141,6 +124,10 @@ class AppDelegate: NSObject, UIApplicationDelegate { } } + private func prepareForLaunch() { + try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true) + } + static func keepScreenOn(_ on: Bool) { UIApplication.shared.isIdleTimerDisabled = on } @@ -148,13 +135,79 @@ class AppDelegate: NSObject, UIApplicationDelegate { class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate { var window: UIWindow? + static var windowStatic: UIWindow? var windowScene: UIWindowScene? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + UITableView.appearance().backgroundColor = .clear guard let windowScene = scene as? UIWindowScene else { return } self.windowScene = windowScene window = windowScene.keyWindow - window?.tintColor = UIColor(cgColor: getUIAccentColorDefault()) - window?.overrideUserInterfaceStyle = getUserInterfaceStyleDefault() + SceneDelegate.windowStatic = windowScene.keyWindow + migrateAccentColorAndTheme() + ThemeManager.applyTheme(currentThemeDefault.get()) + ThemeManager.adjustWindowStyle() + } + + private func migrateAccentColorAndTheme() { + let defs = UserDefaults.standard + /// For checking migration +// themeOverridesDefault.set([]) +// currentThemeDefault.set(DefaultTheme.SYSTEM_THEME_NAME) +// defs.set(0.5, forKey: DEFAULT_ACCENT_COLOR_RED) +// defs.set(0.3, forKey: DEFAULT_ACCENT_COLOR_GREEN) +// defs.set(0.8, forKey: DEFAULT_ACCENT_COLOR_BLUE) + + let userInterfaceStyle = getUserInterfaceStyleDefault() + if defs.double(forKey: DEFAULT_ACCENT_COLOR_GREEN) == 0 && userInterfaceStyle == .unspecified { + // No migration needed or already migrated + return + } + + let defaultAccentColor = Color(cgColor: CGColor(red: 0.000, green: 0.533, blue: 1.000, alpha: 1)) + let accentColor = Color(cgColor: getUIAccentColorDefault()) + if accentColor != defaultAccentColor { + let colors = ThemeColors(primary: accentColor.toReadableHex()) + var overrides = themeOverridesDefault.get() + var themeIds = currentThemeIdsDefault.get() + switch userInterfaceStyle { + case .light: + let light = ThemeOverrides(base: DefaultTheme.LIGHT, colors: colors, wallpaper: ThemeWallpaper(preset: PresetWallpaper.school.filename)) + overrides.append(light) + themeOverridesDefault.set(overrides) + themeIds[DefaultTheme.LIGHT.themeName] = light.themeId + currentThemeIdsDefault.set(themeIds) + ThemeManager.applyTheme(DefaultTheme.LIGHT.themeName) + case .dark: + let dark = ThemeOverrides(base: DefaultTheme.DARK, colors: colors, wallpaper: ThemeWallpaper(preset: PresetWallpaper.school.filename)) + overrides.append(dark) + themeOverridesDefault.set(overrides) + themeIds[DefaultTheme.DARK.themeName] = dark.themeId + currentThemeIdsDefault.set(themeIds) + ThemeManager.applyTheme(DefaultTheme.DARK.themeName) + case .unspecified: + let light = ThemeOverrides(base: DefaultTheme.LIGHT, colors: colors, wallpaper: ThemeWallpaper(preset: PresetWallpaper.school.filename)) + let dark = ThemeOverrides(base: DefaultTheme.DARK, colors: colors, wallpaper: ThemeWallpaper(preset: PresetWallpaper.school.filename)) + overrides.append(light) + overrides.append(dark) + themeOverridesDefault.set(overrides) + themeIds[DefaultTheme.LIGHT.themeName] = light.themeId + themeIds[DefaultTheme.DARK.themeName] = dark.themeId + currentThemeIdsDefault.set(themeIds) + ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME) + @unknown default: () + } + } else if userInterfaceStyle != .unspecified { + let themeName = switch userInterfaceStyle { + case .light: DefaultTheme.LIGHT.themeName + case .dark: DefaultTheme.DARK.themeName + default: DefaultTheme.SYSTEM_THEME_NAME + } + ThemeManager.applyTheme(themeName) + } + defs.removeObject(forKey: DEFAULT_ACCENT_COLOR_RED) + defs.removeObject(forKey: DEFAULT_ACCENT_COLOR_GREEN) + defs.removeObject(forKey: DEFAULT_ACCENT_COLOR_BLUE) + defs.removeObject(forKey: DEFAULT_USER_INTERFACE_STYLE) } } diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_cats.imageset/Contents.json b/apps/ios/Shared/Assets.xcassets/wallpaper_cats.imageset/Contents.json new file mode 100644 index 0000000000..a1747ab6ba --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/wallpaper_cats.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "wallpaper_cats@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "wallpaper_cats@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "wallpaper_cats@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_cats.imageset/wallpaper_cats@1x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_cats.imageset/wallpaper_cats@1x.png new file mode 100644 index 0000000000..7d4624c3f9 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_cats.imageset/wallpaper_cats@1x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_cats.imageset/wallpaper_cats@2x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_cats.imageset/wallpaper_cats@2x.png new file mode 100644 index 0000000000..1015139393 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_cats.imageset/wallpaper_cats@2x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_cats.imageset/wallpaper_cats@3x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_cats.imageset/wallpaper_cats@3x.png new file mode 100644 index 0000000000..9bff3eb3d0 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_cats.imageset/wallpaper_cats@3x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_flowers.imageset/Contents.json b/apps/ios/Shared/Assets.xcassets/wallpaper_flowers.imageset/Contents.json new file mode 100644 index 0000000000..c6bc439be2 --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/wallpaper_flowers.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "wallpaper_flowers@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "wallpaper_flowers@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "wallpaper_flowers@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_flowers.imageset/wallpaper_flowers@1x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_flowers.imageset/wallpaper_flowers@1x.png new file mode 100644 index 0000000000..965f552599 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_flowers.imageset/wallpaper_flowers@1x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_flowers.imageset/wallpaper_flowers@2x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_flowers.imageset/wallpaper_flowers@2x.png new file mode 100644 index 0000000000..0cb219acd3 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_flowers.imageset/wallpaper_flowers@2x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_flowers.imageset/wallpaper_flowers@3x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_flowers.imageset/wallpaper_flowers@3x.png new file mode 100644 index 0000000000..59246eb50d Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_flowers.imageset/wallpaper_flowers@3x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_hearts.imageset/Contents.json b/apps/ios/Shared/Assets.xcassets/wallpaper_hearts.imageset/Contents.json new file mode 100644 index 0000000000..556d01a6f2 --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/wallpaper_hearts.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "wallpaper_hearts@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "wallpaper_hearts@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "wallpaper_hearts@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_hearts.imageset/wallpaper_hearts@1x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_hearts.imageset/wallpaper_hearts@1x.png new file mode 100644 index 0000000000..780ff13513 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_hearts.imageset/wallpaper_hearts@1x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_hearts.imageset/wallpaper_hearts@2x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_hearts.imageset/wallpaper_hearts@2x.png new file mode 100644 index 0000000000..cee89e57d9 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_hearts.imageset/wallpaper_hearts@2x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_hearts.imageset/wallpaper_hearts@3x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_hearts.imageset/wallpaper_hearts@3x.png new file mode 100644 index 0000000000..35da7c7aed Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_hearts.imageset/wallpaper_hearts@3x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_kids.imageset/Contents.json b/apps/ios/Shared/Assets.xcassets/wallpaper_kids.imageset/Contents.json new file mode 100644 index 0000000000..aba5903ec0 --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/wallpaper_kids.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "wallpaper_kids@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "wallpaper_kids@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "wallpaper_kids@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_kids.imageset/wallpaper_kids@1x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_kids.imageset/wallpaper_kids@1x.png new file mode 100644 index 0000000000..83e48b4f78 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_kids.imageset/wallpaper_kids@1x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_kids.imageset/wallpaper_kids@2x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_kids.imageset/wallpaper_kids@2x.png new file mode 100644 index 0000000000..1927c2fe2a Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_kids.imageset/wallpaper_kids@2x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_kids.imageset/wallpaper_kids@3x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_kids.imageset/wallpaper_kids@3x.png new file mode 100644 index 0000000000..f5f15d3643 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_kids.imageset/wallpaper_kids@3x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_school.imageset/Contents.json b/apps/ios/Shared/Assets.xcassets/wallpaper_school.imageset/Contents.json new file mode 100644 index 0000000000..59c209b134 --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/wallpaper_school.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "wallpaper_school@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "wallpaper_school@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "wallpaper_school@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_school.imageset/wallpaper_school@1x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_school.imageset/wallpaper_school@1x.png new file mode 100644 index 0000000000..c95ac60b6e Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_school.imageset/wallpaper_school@1x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_school.imageset/wallpaper_school@2x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_school.imageset/wallpaper_school@2x.png new file mode 100644 index 0000000000..81a3a3d94d Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_school.imageset/wallpaper_school@2x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_school.imageset/wallpaper_school@3x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_school.imageset/wallpaper_school@3x.png new file mode 100644 index 0000000000..f6e1cce383 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_school.imageset/wallpaper_school@3x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_travel.imageset/Contents.json b/apps/ios/Shared/Assets.xcassets/wallpaper_travel.imageset/Contents.json new file mode 100644 index 0000000000..4e56988263 --- /dev/null +++ b/apps/ios/Shared/Assets.xcassets/wallpaper_travel.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "wallpaper_travel@1x.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "wallpaper_travel@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "wallpaper_travel@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_travel.imageset/wallpaper_travel@1x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_travel.imageset/wallpaper_travel@1x.png new file mode 100644 index 0000000000..c1d825b86e Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_travel.imageset/wallpaper_travel@1x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_travel.imageset/wallpaper_travel@2x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_travel.imageset/wallpaper_travel@2x.png new file mode 100644 index 0000000000..d640f10c7c Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_travel.imageset/wallpaper_travel@2x.png differ diff --git a/apps/ios/Shared/Assets.xcassets/wallpaper_travel.imageset/wallpaper_travel@3x.png b/apps/ios/Shared/Assets.xcassets/wallpaper_travel.imageset/wallpaper_travel@3x.png new file mode 100644 index 0000000000..64ec137331 Binary files /dev/null and b/apps/ios/Shared/Assets.xcassets/wallpaper_travel.imageset/wallpaper_travel@3x.png differ diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index acea38e69e..1f41e7c31d 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -14,6 +14,8 @@ struct ContentView: View { @ObservedObject var alertManager = AlertManager.shared @ObservedObject var callController = CallController.shared @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme + @EnvironmentObject var sceneDelegate: SceneDelegate var contentAccessAuthenticationExtended: Bool @@ -34,7 +36,7 @@ struct ContentView: View { @State private var waitingForOrPassedAuth = true @State private var chatListActionSheet: ChatListActionSheet? = nil - private let callTopPadding: CGFloat = 50 + private let callTopPadding: CGFloat = 40 private enum ChatListActionSheet: Identifiable { case planAndConnectSheet(sheet: PlanAndConnectActionSheet) @@ -51,6 +53,16 @@ struct ContentView: View { } var body: some View { + if #available(iOS 16.0, *) { + allViews() + .scrollContentBackground(.hidden) + } else { + // on iOS 15 scroll view background disabled in SceneDelegate + allViews() + } + } + + @ViewBuilder func allViews() -> some View { ZStack { let showCallArea = chatModel.activeCall != nil && chatModel.activeCall?.callState != .waitCapabilities && chatModel.activeCall?.callState != .invitationAccepted // contentView() has to be in a single branch, so that enabling authentication doesn't trigger re-rendering and close settings. @@ -138,6 +150,17 @@ struct ContentView: View { break } } + .onAppear { + reactOnDarkThemeChanges(systemInDarkThemeCurrently) + } + .onChange(of: colorScheme) { scheme in + // It's needed to update UI colors when iOS wants to make screenshot after going to background, + // so when a user changes his global theme from dark to light or back, the app will adapt to it + reactOnDarkThemeChanges(scheme == .dark) + } + .onChange(of: theme.name) { _ in + ThemeManager.adjustWindowStyle() + } } @ViewBuilder private func contentView() -> some View { @@ -184,7 +207,7 @@ struct ContentView: View { CallDuration(call: call) } .padding(.horizontal) - .frame(height: callTopPadding - 10) + .frame(height: callTopPadding) .background(Color(uiColor: UIColor(red: 47/255, green: 208/255, blue: 88/255, alpha: 1))) .onTapGesture { chatModel.activeCallViewIsCollapsed = false @@ -224,8 +247,8 @@ struct ContentView: View { .frame(maxWidth: .infinity, maxHeight: .infinity ) .background( Rectangle() - .fill(.background) - ) + .fill(theme.colors.background) + ) } private func mainView() -> some View { diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index a4651e1d42..0ac1a9cacb 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -43,11 +43,95 @@ private func addTermItem(_ items: inout [TerminalItem], _ item: TerminalItem) { items.append(item) } +class ItemsModel: ObservableObject { + static let shared = ItemsModel() + private let publisher = ObservableObjectPublisher() + private var bag = Set() + var reversedChatItems: [ChatItem] = [] { + willSet { publisher.send() } + } + var itemAdded = false { + willSet { publisher.send() } + } + + // Publishes directly to `objectWillChange` publisher, + // this will cause reversedChatItems to be rendered without throttling + @Published var isLoading = false + @Published var showLoadingProgress = false + + init() { + publisher + .throttle(for: 0.25, scheduler: DispatchQueue.main, latest: true) + .sink { self.objectWillChange.send() } + .store(in: &bag) + } + + func loadOpenChat(_ chatId: ChatId, willNavigate: @escaping () -> Void = {}) { + let navigationTimeout = Task { + do { + try await Task.sleep(nanoseconds: 250_000000) + await MainActor.run { + willNavigate() + ChatModel.shared.chatId = chatId + } + } catch {} + } + let progressTimeout = Task { + do { + try await Task.sleep(nanoseconds: 1500_000000) + await MainActor.run { showLoadingProgress = true } + } catch {} + } + Task { + if let chat = ChatModel.shared.getChat(chatId) { + await MainActor.run { self.isLoading = true } +// try? await Task.sleep(nanoseconds: 5000_000000) + await loadChat(chat: chat) + navigationTimeout.cancel() + progressTimeout.cancel() + await MainActor.run { + self.isLoading = false + self.showLoadingProgress = false + willNavigate() + ChatModel.shared.chatId = chatId + } + } + } + } +} + +class NetworkModel: ObservableObject { + // map of connections network statuses, key is agent connection id + @Published var networkStatuses: Dictionary = [:] + + static let shared = NetworkModel() + + private init() { } + + func setContactNetworkStatus(_ contact: Contact, _ status: NetworkStatus) { + if let conn = contact.activeConn { + networkStatuses[conn.agentConnId] = status + } + } + + func contactNetworkStatus(_ contact: Contact) -> NetworkStatus { + if let conn = contact.activeConn { + networkStatuses[conn.agentConnId] ?? .unknown + } else { + .unknown + } + } +} + final class ChatModel: ObservableObject { @Published var onboardingStage: OnboardingStage? @Published var setDeliveryReceipts = false @Published var v3DBMigration: V3DBMigrationState = v3DBMigrationDefault.get() - @Published var currentUser: User? + @Published var currentUser: User? { + didSet { + ThemeManager.applyTheme(currentThemeDefault.get()) + } + } @Published var users: [UserInfo] = [] @Published var chatInitialized = false @Published var chatRunning: Bool? @@ -59,16 +143,15 @@ final class ChatModel: ObservableObject { @Published var contentViewAccessAuthenticated: Bool = false @Published var laRequest: LocalAuthRequest? // list of chat "previews" - @Published var chats: [Chat] = [] + @Published private(set) var chats: [Chat] = [] @Published var deletedChats: Set = [] - // map of connections network statuses, key is agent connection id - @Published var networkStatuses: Dictionary = [:] // current chat @Published var chatId: String? - @Published var reversedChatItems: [ChatItem] = [] var chatItemStatuses: Dictionary = [:] @Published var chatToTop: String? @Published var groupMembers: [GMember] = [] + @Published var groupMembersIndexes: Dictionary = [:] // groupMemberId to index in groupMembers list + @Published var membersLoaded = false // items in the terminal view @Published var showingTerminal = false @Published var terminalItems: [TerminalItem] = [] @@ -100,8 +183,6 @@ final class ChatModel: ObservableObject { @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 @Published var networkInfo = UserNetworkInfo(networkType: .other, online: true) @@ -111,6 +192,8 @@ final class ChatModel: ObservableObject { static let shared = ChatModel() + let im = ItemsModel.shared + static var ok: Bool { ChatModel.shared.chatDbStatus == .ok } let ntfEnableLocal = true @@ -176,18 +259,47 @@ final class ChatModel: ObservableObject { } } + func populateGroupMembersIndexes() { + groupMembersIndexes.removeAll() + for (i, member) in groupMembers.enumerated() { + groupMembersIndexes[member.groupMemberId] = i + } + } + func getGroupMember(_ groupMemberId: Int64) -> GMember? { - groupMembers.first { $0.groupMemberId == groupMemberId } + if let i = groupMembersIndexes[groupMemberId] { + return groupMembers[i] + } + return nil + } + + func loadGroupMembers(_ groupInfo: GroupInfo, updateView: @escaping () -> Void = {}) async { + let groupMembers = await apiListMembers(groupInfo.groupId) + await MainActor.run { + if chatId == groupInfo.id { + self.groupMembers = groupMembers.map { GMember.init($0) } + self.populateGroupMembersIndexes() + self.membersLoaded = true + updateView() + } + } } private func getChatIndex(_ id: String) -> Int? { chats.firstIndex(where: { $0.id == id }) } - func addChat(_ chat: Chat, at position: Int = 0) { - withAnimation { - chats.insert(chat, at: position) + func addChat(_ chat: Chat) { + if chatId == nil { + withAnimation { addChat_(chat, at: 0) } + } else { + addChat_(chat, at: 0) } + popChatCollector.throttlePopChat(chat.chatInfo.id, currentPosition: 0) + } + + func addChat_(_ chat: Chat, at position: Int = 0) { + chats.insert(chat, at: position) } func updateChatInfo(_ cInfo: ChatInfo) { @@ -245,26 +357,10 @@ final class ChatModel: ObservableObject { } } - func updateChats(with newChats: [ChatData]) { - for i in 0.. 0 { - if chatId == nil { - withAnimation { popChat_(i) } - } else if chatId == cInfo.id { - chatToTop = cInfo.id - } else { - popChat_(i) - } + unreadCollector.changeUnreadCounter(cInfo.id, by: 1) } + popChatCollector.throttlePopChat(cInfo.id, currentPosition: i) } else { addChat(Chat(chatInfo: cInfo, chatItems: [cItem])) } @@ -315,7 +408,7 @@ final class ChatModel: ObservableObject { var res: Bool if let chat = getChat(cInfo.id) { if let pItem = chat.chatItems.last { - if pItem.id == cItem.id || (chatId == cInfo.id && reversedChatItems.first(where: { $0.id == cItem.id }) == nil) { + if pItem.id == cItem.id || (chatId == cInfo.id && im.reversedChatItems.first(where: { $0.id == cItem.id }) == nil) { chat.chatItems = [cItem] } } else { @@ -326,23 +419,27 @@ final class ChatModel: ObservableObject { addChat(Chat(chatInfo: cInfo, chatItems: [cItem])) res = true } + if cItem.isDeletedContent || cItem.meta.itemDeleted != nil { + VoiceItemState.stopVoiceInChatView(cInfo, cItem) + } // update current chat return chatId == cInfo.id ? _upsertChatItem(cInfo, cItem) : res } private func _upsertChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) -> Bool { if let i = getChatItemIndex(cItem) { - withAnimation { + withConditionalAnimation { _updateChatItem(at: i, with: cItem) } return false } else { - withAnimation(itemAnimation()) { + withConditionalAnimation(itemAnimation()) { var ci = cItem if let status = chatItemStatuses.removeValue(forKey: ci.id), case .sndNew = ci.meta.itemStatus { ci.meta.itemStatus = status } - reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0) + im.reversedChatItems.insert(ci, at: hasLiveDummy ? 1 : 0) + im.itemAdded = true } return true } @@ -357,7 +454,7 @@ final class ChatModel: ObservableObject { func updateChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem, status: CIStatus? = nil) { if chatId == cInfo.id, let i = getChatItemIndex(cItem) { - withAnimation { + withConditionalAnimation { _updateChatItem(at: i, with: cItem) } } else if let status = status { @@ -366,17 +463,17 @@ final class ChatModel: ObservableObject { } private func _updateChatItem(at i: Int, with cItem: ChatItem) { - reversedChatItems[i] = cItem - reversedChatItems[i].viewTimestamp = .now + im.reversedChatItems[i] = cItem + im.reversedChatItems[i].viewTimestamp = .now } func getChatItemIndex(_ cItem: ChatItem) -> Int? { - reversedChatItems.firstIndex(where: { $0.id == cItem.id }) + im.reversedChatItems.firstIndex(where: { $0.id == cItem.id }) } func removeChatItem(_ cInfo: ChatInfo, _ cItem: ChatItem) { if cItem.isRcvNew { - decreaseUnreadCounter(cInfo) + unreadCollector.changeUnreadCounter(cInfo.id, by: -1) } // update previews if let chat = getChat(cInfo.id) { @@ -388,23 +485,24 @@ final class ChatModel: ObservableObject { if chatId == cInfo.id { if let i = getChatItemIndex(cItem) { _ = withAnimation { - self.reversedChatItems.remove(at: i) + im.reversedChatItems.remove(at: i) } } } + VoiceItemState.stopVoiceInChatView(cInfo, cItem) } func nextChatItemData(_ chatItemId: Int64, previous: Bool, map: @escaping (ChatItem) -> T?) -> T? { - guard var i = reversedChatItems.firstIndex(where: { $0.id == chatItemId }) else { return nil } + guard var i = im.reversedChatItems.firstIndex(where: { $0.id == chatItemId }) else { return nil } if previous { - while i < reversedChatItems.count - 1 { + while i < im.reversedChatItems.count - 1 { i += 1 - if let res = map(reversedChatItems[i]) { return res } + if let res = map(im.reversedChatItems[i]) { return res } } } else { while i > 0 { i -= 1 - if let res = map(reversedChatItems[i]) { return res } + if let res = map(im.reversedChatItems[i]) { return res } } } return nil @@ -422,10 +520,21 @@ final class ChatModel: ObservableObject { } } + func updateCurrentUserUiThemes(uiThemes: ThemeModeOverrides?) { + guard var current = currentUser else { return } + current.uiThemes = uiThemes + let i = users.firstIndex(where: { $0.user.userId == current.userId }) + if let i { + users[i].user = current + } + currentUser = current + } + func addLiveDummy(_ chatInfo: ChatInfo) -> ChatItem { let cItem = ChatItem.liveDummy(chatInfo.chatType) withAnimation { - reversedChatItems.insert(cItem, at: 0) + im.reversedChatItems.insert(cItem, at: 0) + im.itemAdded = true } return cItem } @@ -433,15 +542,15 @@ final class ChatModel: ObservableObject { func removeLiveDummy(animated: Bool = true) { if hasLiveDummy { if animated { - withAnimation { _ = reversedChatItems.removeFirst() } + withAnimation { _ = im.reversedChatItems.removeFirst() } } else { - _ = reversedChatItems.removeFirst() + _ = im.reversedChatItems.removeFirst() } } } private var hasLiveDummy: Bool { - reversedChatItems.first?.isLiveDummy == true + im.reversedChatItems.first?.isLiveDummy == true } func markChatItemsRead(_ cInfo: ChatInfo) { @@ -458,7 +567,7 @@ final class ChatModel: ObservableObject { private func markCurrentChatRead(fromIndex i: Int = 0) { var j = i - while j < reversedChatItems.count { + while j < im.reversedChatItems.count { markChatItemRead_(j) j += 1 } @@ -472,7 +581,7 @@ final class ChatModel: ObservableObject { var unreadBelow = 0 var j = i - 1 while j >= 0 { - if case .rcvNew = self.reversedChatItems[j].meta.itemStatus { + if case .rcvNew = self.im.reversedChatItems[j].meta.itemStatus { unreadBelow += 1 } j -= 1 @@ -507,51 +616,146 @@ final class ChatModel: ObservableObject { // clear current chat if chatId == cInfo.id { chatItemStatuses = [:] - reversedChatItems = [] + im.reversedChatItems = [] } } - func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) { - // update preview - decreaseUnreadCounter(cInfo) - // update current chat - if chatId == cInfo.id, let i = getChatItemIndex(cItem) { - markChatItemRead_(i) - } - } - - private func markChatItemRead_(_ i: Int) { - let meta = reversedChatItems[i].meta - if case .rcvNew = meta.itemStatus { - reversedChatItems[i].meta.itemStatus = .rcvRead - reversedChatItems[i].viewTimestamp = .now - if meta.itemLive != true, let ttl = meta.itemTimed?.ttl { - reversedChatItems[i].meta.itemTimed?.deleteAt = .now + TimeInterval(ttl) + func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async { + if chatId == cInfo.id, + let itemIndex = getChatItemIndex(cItem), + im.reversedChatItems[itemIndex].isRcvNew { + await MainActor.run { + withTransaction(Transaction()) { + // update current chat + markChatItemRead_(itemIndex) + // update preview + unreadCollector.changeUnreadCounter(cInfo.id, by: -1) + } } } } - func decreaseUnreadCounter(_ cInfo: ChatInfo) { - if let i = getChatIndex(cInfo.id) { - chats[i].chatStats.unreadCount = chats[i].chatStats.unreadCount - 1 - decreaseUnreadCounter(user: currentUser!) + private let unreadCollector = UnreadCollector() + + class UnreadCollector { + private let subject = PassthroughSubject() + private var bag = Set() + private var unreadCounts: [ChatId: Int] = [:] + + init() { + subject + .debounce(for: 1, scheduler: DispatchQueue.main) + .sink { + let m = ChatModel.shared + for (chatId, count) in self.unreadCounts { + if let i = m.getChatIndex(chatId) { + m.changeUnreadCounter(i, by: count) + } + } + self.unreadCounts = [:] + } + .store(in: &bag) } + + func changeUnreadCounter(_ chatId: ChatId, by count: Int) { + DispatchQueue.main.async { + self.unreadCounts[chatId] = (self.unreadCounts[chatId] ?? 0) + count + } + subject.send() + } + } + + let popChatCollector = PopChatCollector() + + class PopChatCollector { + private let subject = PassthroughSubject() + private var bag = Set() + private var chatsToPop: [ChatId: Date] = [:] + private let popTsComparator = KeyPathComparator(\.popTs, order: .reverse) + + init() { + subject + .throttle(for: 2, scheduler: DispatchQueue.main, latest: true) + .sink { self.popCollectedChats() } + .store(in: &bag) + } + + func throttlePopChat(_ chatId: ChatId, currentPosition: Int) { + let m = ChatModel.shared + if currentPosition > 0 && m.chatId == chatId { + m.chatToTop = chatId + } + if currentPosition > 0 || !chatsToPop.isEmpty { + chatsToPop[chatId] = Date.now + subject.send() + } + } + + func clear() { + chatsToPop = [:] + } + + func popCollectedChats() { + let m = ChatModel.shared + var ixs: IndexSet = [] + var chs: [Chat] = [] + // collect chats that received updates + for (chatId, popTs) in self.chatsToPop { + // Currently opened chat is excluded, removing it from the list would navigate out of it + // It will be popped to top later when user exits from the list. + if m.chatId != chatId, let i = m.getChatIndex(chatId) { + ixs.insert(i) + let ch = m.chats[i] + ch.popTs = popTs + chs.append(ch) + } + } + + let removeInsert = { + m.chats.remove(atOffsets: ixs) + // sort chats by pop timestamp in descending order + m.chats.insert(contentsOf: chs.sorted(using: self.popTsComparator), at: 0) + } + + if m.chatId == nil { + withAnimation { removeInsert() } + } else { + removeInsert() + } + + self.chatsToPop = [:] + } + } + + private func markChatItemRead_(_ i: Int) { + let meta = im.reversedChatItems[i].meta + if case .rcvNew = meta.itemStatus { + im.reversedChatItems[i].meta.itemStatus = .rcvRead + im.reversedChatItems[i].viewTimestamp = .now + if meta.itemLive != true, let ttl = meta.itemTimed?.ttl { + im.reversedChatItems[i].meta.itemTimed?.deleteAt = .now + TimeInterval(ttl) + } + } + } + + func changeUnreadCounter(_ chatIndex: Int, by count: Int) { + chats[chatIndex].chatStats.unreadCount = chats[chatIndex].chatStats.unreadCount + count + changeUnreadCounter(user: currentUser!, by: count) } func increaseUnreadCounter(user: any UserLike) { changeUnreadCounter(user: user, by: 1) - NtfManager.shared.incNtfBadgeCount() } func decreaseUnreadCounter(user: any UserLike, by: Int = 1) { changeUnreadCounter(user: user, by: -by) - NtfManager.shared.decNtfBadgeCount(by: by) } private func changeUnreadCounter(user: any UserLike, by: Int) { if let i = users.firstIndex(where: { $0.user.userId == user.userId }) { users[i].unreadCount += by } + NtfManager.shared.changeNtfBadgeCount(by: by) } func totalUnreadCountForAllUsers() -> Int { @@ -565,8 +769,8 @@ final class ChatModel: ObservableObject { var ns: [String] = [] if let ciCategory = chatItem.mergeCategory, var i = getChatItemIndex(chatItem) { - while i < reversedChatItems.count { - let ci = reversedChatItems[i] + while i < im.reversedChatItems.count { + let ci = im.reversedChatItems[i] if ci.mergeCategory != ciCategory { break } if let m = ci.memberConnected { ns.append(m.displayName) @@ -581,7 +785,7 @@ final class ChatModel: ObservableObject { // returns the index of the passed item and the next item (it has smaller index) func getNextChatItem(_ ci: ChatItem) -> (Int?, ChatItem?) { if let i = getChatItemIndex(ci) { - (i, i > 0 ? reversedChatItems[i - 1] : nil) + (i, i > 0 ? im.reversedChatItems[i - 1] : nil) } else { (nil, nil) } @@ -591,10 +795,10 @@ final class ChatModel: ObservableObject { // and the previous visible item with another merge category func getPrevShownChatItem(_ ciIndex: Int?, _ ciCategory: CIMergeCategory?) -> (Int?, ChatItem?) { guard var i = ciIndex else { return (nil, nil) } - let fst = reversedChatItems.count - 1 + let fst = im.reversedChatItems.count - 1 while i < fst { i = i + 1 - let ci = reversedChatItems[i] + let ci = im.reversedChatItems[i] if ciCategory == nil || ciCategory != ci.mergeCategory { return (i - 1, ci) } @@ -607,7 +811,7 @@ final class ChatModel: ObservableObject { var prevMember: GroupMember? = nil var memberIds: Set = [] for i in range { - if case let .groupRcv(m) = reversedChatItems[i].chatDir { + if case let .groupRcv(m) = im.reversedChatItems[i].chatDir { if prevMember == nil && m.groupMemberId != member.groupMemberId { prevMember = m } memberIds.insert(m.groupMemberId) } @@ -617,6 +821,7 @@ final class ChatModel: ObservableObject { func popChat(_ id: String) { if let i = getChatIndex(id) { + // no animation here, for it not to look like it just moved when leaving the chat popChat_(i) } } @@ -651,14 +856,17 @@ final class ChatModel: ObservableObject { } // update current chat if chatId == groupInfo.id { - if let i = groupMembers.firstIndex(where: { $0.groupMemberId == member.groupMemberId }) { + if let i = groupMembersIndexes[member.groupMemberId] { withAnimation(.default) { self.groupMembers[i].wrapped = member self.groupMembers[i].created = Date.now } return false } else { - withAnimation { groupMembers.append(GMember(member)) } + withAnimation { + groupMembers.append(GMember(member)) + groupMembersIndexes[member.groupMemberId] = groupMembers.count - 1 + } return true } } else { @@ -679,37 +887,29 @@ final class ChatModel: ObservableObject { var i = 0 var totalBelow = 0 var unreadBelow = 0 - while i < reversedChatItems.count - 1 && !itemsInView.contains(reversedChatItems[i].viewId) { + while i < im.reversedChatItems.count - 1 && !itemsInView.contains(im.reversedChatItems[i].viewId) { totalBelow += 1 - if reversedChatItems[i].isRcvNew { + if im.reversedChatItems[i].isRcvNew { unreadBelow += 1 } i += 1 } - return UnreadChatItemCounts(totalBelow: totalBelow, unreadBelow: unreadBelow) + return UnreadChatItemCounts( + // TODO these thresholds account for the fact that items are still "visible" while + // covered by compose area, they should be replaced with the actual height in pixels below the screen. + isNearBottom: totalBelow < 15, + isReallyNearBottom: totalBelow < 2, + unreadBelow: unreadBelow + ) } func topItemInView(itemsInView: Set) -> ChatItem? { - let maxIx = reversedChatItems.count - 1 + let maxIx = im.reversedChatItems.count - 1 var i = 0 - let inView = { itemsInView.contains(self.reversedChatItems[$0].viewId) } + let inView = { itemsInView.contains(self.im.reversedChatItems[$0].viewId) } while i < maxIx && !inView(i) { i += 1 } while i < maxIx && inView(i) { i += 1 } - return reversedChatItems[min(i - 1, maxIx)] - } - - func setContactNetworkStatus(_ contact: Contact, _ status: NetworkStatus) { - if let conn = contact.activeConn { - networkStatuses[conn.agentConnId] = status - } - } - - func contactNetworkStatus(_ contact: Contact) -> NetworkStatus { - if let conn = contact.activeConn { - networkStatuses[conn.agentConnId] ?? .unknown - } else { - .unknown - } + return im.reversedChatItems[min(i - 1, maxIx)] } } @@ -723,16 +923,18 @@ struct NTFContactRequest { var chatId: String } -struct UnreadChatItemCounts { - var totalBelow: Int +struct UnreadChatItemCounts: Equatable { + var isNearBottom: Bool + var isReallyNearBottom: Bool var unreadBelow: Int } -final class Chat: ObservableObject, Identifiable { +final class Chat: ObservableObject, Identifiable, ChatLike { @Published var chatInfo: ChatInfo @Published var chatItems: [ChatItem] @Published var chatStats: ChatStats var created = Date.now + fileprivate var popTs: Date? init(_ cData: ChatData) { self.chatInfo = cData.chatInfo @@ -779,24 +981,6 @@ final class Chat: ObservableObject, Identifiable { var viewId: String { get { "\(chatInfo.id) \(created.timeIntervalSince1970)" } } - func groupFeatureEnabled(_ feature: GroupFeature) -> Bool { - if case let .group(groupInfo) = self.chatInfo { - let p = groupInfo.fullGroupPreferences - return switch feature { - case .timedMessages: p.timedMessages.on - case .directMessages: p.directMessages.on(for: groupInfo.membership) - case .fullDelete: p.fullDelete.on - case .reactions: p.reactions.on - case .voice: p.voice.on(for: groupInfo.membership) - case .files: p.files.on(for: groupInfo.membership) - case .simplexLinks: p.simplexLinks.on(for: groupInfo.membership) - case .history: p.history.on - } - } else { - return true - } - } - public static var sampleData: Chat = Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []) } diff --git a/apps/ios/Shared/Model/NtfManager.swift b/apps/ios/Shared/Model/NtfManager.swift index f1fdcc018e..95063845f1 100644 --- a/apps/ios/Shared/Model/NtfManager.swift +++ b/apps/ios/Shared/Model/NtfManager.swift @@ -57,7 +57,9 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { chatModel.ntfCallInvitationAction = (chatId, ntfAction) } } else { - chatModel.chatId = content.targetContentIdentifier + if let chatId = content.targetContentIdentifier { + ItemsModel.shared.loadOpenChat(chatId) + } } handler() } @@ -238,12 +240,8 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { ntfBadgeCountGroupDefault.set(count) } - func decNtfBadgeCount(by count: Int = 1) { - setNtfBadgeCount(max(0, UIApplication.shared.applicationIconBadgeNumber - count)) - } - - func incNtfBadgeCount(by count: Int = 1) { - setNtfBadgeCount(UIApplication.shared.applicationIconBadgeNumber + count) + func changeNtfBadgeCount(by count: Int = 1) { + setNtfBadgeCount(max(0, UIApplication.shared.applicationIconBadgeNumber + count)) } private func addNotification(_ content: UNMutableNotificationContent) { diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index b4c9a48d5d..ebc58c6a05 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -92,25 +92,29 @@ private func withBGTask(bgDelay: Double? = nil, f: @escaping () -> T) -> T { return r } -func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, _ ctrl: chat_ctrl? = nil) -> ChatResponse { - logger.debug("chatSendCmd \(cmd.cmdType)") +func chatSendCmdSync(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, _ ctrl: chat_ctrl? = nil, log: Bool = true) -> ChatResponse { + if log { + logger.debug("chatSendCmd \(cmd.cmdType)") + } let start = Date.now let resp = bgTask ? withBGTask(bgDelay: bgDelay) { sendSimpleXCmd(cmd, ctrl) } : sendSimpleXCmd(cmd, ctrl) - logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)") - if case let .response(_, json) = resp { - logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)") - } - Task { - await TerminalItems.shared.addCommand(start, cmd.obfuscated, resp) + if log { + logger.debug("chatSendCmd \(cmd.cmdType): \(resp.responseType)") + if case let .response(_, json) = resp { + logger.debug("chatSendCmd \(cmd.cmdType) response: \(json)") + } + Task { + await TerminalItems.shared.addCommand(start, cmd.obfuscated, resp) + } } return resp } -func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, _ ctrl: chat_ctrl? = nil) async -> ChatResponse { +func chatSendCmd(_ cmd: ChatCommand, bgTask: Bool = true, bgDelay: Double? = nil, _ ctrl: chat_ctrl? = nil, log: Bool = true) async -> ChatResponse { await withCheckedContinuation { cont in - cont.resume(returning: chatSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay, ctrl)) + cont.resume(returning: chatSendCmdSync(cmd, bgTask: bgTask, bgDelay: bgDelay, ctrl, log: log)) } } @@ -133,8 +137,8 @@ func apiGetActiveUser(ctrl: chat_ctrl? = nil) throws -> User? { } } -func apiCreateActiveUser(_ p: Profile?, sameServers: Bool = false, pastTimestamp: Bool = false, ctrl: chat_ctrl? = nil) throws -> User { - let r = chatSendCmdSync(.createActiveUser(profile: p, sameServers: sameServers, pastTimestamp: pastTimestamp), ctrl) +func apiCreateActiveUser(_ p: Profile?, pastTimestamp: Bool = false, ctrl: chat_ctrl? = nil) throws -> User { + let r = chatSendCmdSync(.createActiveUser(profile: p, pastTimestamp: pastTimestamp), ctrl) if case let .activeUser(user) = r { return user } throw r } @@ -213,7 +217,7 @@ func apiDeleteUser(_ userId: Int64, _ delSMPQueues: Bool, viewPwd: String?) asyn } func apiStartChat(ctrl: chat_ctrl? = nil) throws -> Bool { - let r = chatSendCmdSync(.startChat(mainApp: true), ctrl) + let r = chatSendCmdSync(.startChat(mainApp: true, enableSndFiles: true), ctrl) switch r { case .chatStarted: return true case .chatRunning: return false @@ -221,6 +225,15 @@ func apiStartChat(ctrl: chat_ctrl? = nil) throws -> Bool { } } +func apiCheckChatRunning() throws -> Bool { + let r = chatSendCmdSync(.checkChatRunning) + switch r { + case .chatRunning: return true + case .chatStopped: return false + default: throw r + } +} + func apiStopChat() async throws { let r = await chatSendCmd(.apiStopChat) switch r { @@ -242,14 +255,8 @@ func apiSuspendChat(timeoutMicroseconds: Int) { logger.error("apiSuspendChat error: \(String(describing: r))") } -func apiSetTempFolder(tempFolder: String, ctrl: chat_ctrl? = nil) throws { - let r = chatSendCmdSync(.setTempFolder(tempFolder: tempFolder), ctrl) - if case .cmdOk = r { return } - throw r -} - -func apiSetFilesFolder(filesFolder: String, ctrl: chat_ctrl? = nil) throws { - let r = chatSendCmdSync(.setFilesFolder(filesFolder: filesFolder), ctrl) +func apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String, ctrl: chat_ctrl? = nil) throws { + let r = chatSendCmdSync(.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder), ctrl) if case .cmdOk = r { return } throw r } @@ -272,8 +279,10 @@ func apiGetAppSettings(settings: AppSettings) throws -> AppSettings { throw r } -func apiExportArchive(config: ArchiveConfig) async throws { - try await sendCommandOkResp(.apiExportArchive(config: config)) +func apiExportArchive(config: ArchiveConfig) async throws -> [ArchiveError] { + let r = await chatSendCmd(.apiExportArchive(config: config)) + if case let .archiveExported(archiveErrors) = r { return archiveErrors } + throw r } func apiImportArchive(config: ArchiveConfig) async throws -> [ArchiveError] { @@ -309,8 +318,10 @@ private func apiChatsResponse(_ r: ChatResponse) throws -> [ChatData] { throw r } -func apiGetChat(type: ChatType, id: Int64, search: String = "") throws -> Chat { - let r = chatSendCmdSync(.apiGetChat(type: type, id: id, pagination: .last(count: 50), search: search)) +let loadItemsPerPage = 50 + +func apiGetChat(type: ChatType, id: Int64, search: String = "") async throws -> Chat { + let r = await chatSendCmd(.apiGetChat(type: type, id: id, pagination: .last(count: loadItemsPerPage), search: search)) if case let .apiChat(_, chat) = r { return Chat.init(chat) } throw r } @@ -321,15 +332,20 @@ func apiGetChatItems(type: ChatType, id: Int64, pagination: ChatPagination, sear throw r } -func loadChat(chat: Chat, search: String = "") { +func loadChat(chat: Chat, search: String = "", clearItems: Bool = true) async { do { let cInfo = chat.chatInfo let m = ChatModel.shared + let im = ItemsModel.shared m.chatItemStatuses = [:] - m.reversedChatItems = [] - let chat = try apiGetChat(type: cInfo.chatType, id: cInfo.apiId, search: search) - m.updateChatInfo(chat.chatInfo) - m.reversedChatItems = chat.chatItems.reversed() + if clearItems { + await MainActor.run { im.reversedChatItems = [] } + } + let chat = try await apiGetChat(type: cInfo.chatType, id: cInfo.apiId, search: search) + await MainActor.run { + im.reversedChatItems = chat.chatItems.reversed() + m.updateChatInfo(chat.chatInfo) + } } catch let error { logger.error("loadChat error: \(responseError(error))") } @@ -341,8 +357,8 @@ func apiGetChatItemInfo(type: ChatType, id: Int64, itemId: Int64) async throws - throw r } -func apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64) async -> ChatItem? { - let cmd: ChatCommand = .apiForwardChatItem(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemId: itemId) +func apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64, ttl: Int?) async -> ChatItem? { + let cmd: ChatCommand = .apiForwardChatItem(toChatType: toChatType, toChatId: toChatId, fromChatType: fromChatType, fromChatId: fromChatId, itemId: itemId, ttl: ttl) return await processSendMessageCmd(toChatType: toChatType, cmd: cmd) } @@ -397,7 +413,7 @@ private func sendMessageErrorAlert(_ r: ChatResponse) { logger.error("send message error: \(String(describing: r))") AlertManager.shared.showAlertMsg( title: "Error sending message", - message: "Error: \(String(describing: r))" + message: "Error: \(responseError(r))" ) } @@ -405,7 +421,7 @@ private func createChatItemErrorAlert(_ r: ChatResponse) { logger.error("apiCreateChatItem error: \(String(describing: r))") AlertManager.shared.showAlertMsg( title: "Error creating message", - message: "Error: \(String(describing: r))" + message: "Error: \(responseError(r))" ) } @@ -421,15 +437,15 @@ func apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, re throw r } -func apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) async throws -> (ChatItem, ChatItem?) { - let r = await chatSendCmd(.apiDeleteChatItem(type: type, id: id, itemId: itemId, mode: mode), bgDelay: msgDelay) - if case let .chatItemDeleted(_, deletedChatItem, toChatItem, _) = r { return (deletedChatItem.chatItem, toChatItem?.chatItem) } +func apiDeleteChatItems(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode) async throws -> [ChatItemDeletion] { + let r = await chatSendCmd(.apiDeleteChatItem(type: type, id: id, itemIds: itemIds, mode: mode), bgDelay: msgDelay) + if case let .chatItemsDeleted(_, items, _) = r { return items } throw r } -func apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64) async throws -> (ChatItem, ChatItem?) { - let r = await chatSendCmd(.apiDeleteMemberChatItem(groupId: groupId, groupMemberId: groupMemberId, itemId: itemId), bgDelay: msgDelay) - if case let .chatItemDeleted(_, deletedChatItem, toChatItem, _) = r { return (deletedChatItem.chatItem, toChatItem?.chatItem) } +func apiDeleteMemberChatItems(groupId: Int64, itemIds: [Int64]) async throws -> [ChatItemDeletion] { + let r = await chatSendCmd(.apiDeleteMemberChatItem(groupId: groupId, itemIds: itemIds), bgDelay: msgDelay) + if case let .chatItemsDeleted(_, items, _) = r { return items } throw r } @@ -541,6 +557,11 @@ func reconnectAllServers() async throws { try await sendCommandOkResp(.reconnectAllServers) } +func reconnectServer(smpServer: String) async throws { + let userId = try currentUserId("reconnectServer") + try await sendCommandOkResp(.reconnectServer(userId: userId, smpServer: smpServer)) +} + func apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) async throws { try await sendCommandOkResp(.apiSetChatSettings(type: type, id: id, chatSettings: chatSettings)) } @@ -561,6 +582,18 @@ func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) throws -> (Gro throw r } +func apiContactQueueInfo(_ contactId: Int64) async throws -> (RcvMsgInfo?, QueueInfo) { + let r = await chatSendCmd(.apiContactQueueInfo(contactId: contactId)) + if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) } + throw r +} + +func apiGroupMemberQueueInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws -> (RcvMsgInfo?, QueueInfo) { + let r = await chatSendCmd(.apiGroupMemberQueueInfo(groupId: groupId, groupMemberId: groupMemberId)) + if case let .queueInfo(_, rcvMsgInfo, queueInfo) = r { return (rcvMsgInfo, queueInfo) } + throw r +} + func apiSwitchContact(contactId: Int64) throws -> ConnectionStats { let r = chatSendCmdSync(.apiSwitchContact(contactId: contactId)) if case let .contactSwitchStarted(_, _, connectionStats) = r { return connectionStats } @@ -672,7 +705,7 @@ func apiConnect_(incognito: Bool, connReq: String) async -> ((ConnReqType, Pendi return ((.contact, connection), nil) case let .contactAlreadyExists(_, contact): if let c = m.getContactChat(contact.contactId) { - await MainActor.run { m.chatId = c.id } + ItemsModel.shared.loadOpenChat(c.id) } let alert = contactAlreadyExistsAlert(contact) return (nil, alert) @@ -682,7 +715,7 @@ func apiConnect_(incognito: Bool, connReq: String) async -> ((ConnReqType, Pendi message: "Please check that you used the correct link or ask your contact to send you another one." ) return (nil, alert) - case .chatCmdError(_, .errorAgent(.SMP(.AUTH))): + case .chatCmdError(_, .errorAgent(.SMP(_, .AUTH))): let alert = mkAlert( title: "Connection error (AUTH)", message: "Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." @@ -715,7 +748,7 @@ private func connectionErrorAlert(_ r: ChatResponse) -> Alert { } else { return mkAlert( title: "Connection error", - message: "Error: \(String(describing: r))" + message: "Error: \(responseError(r))" ) } } @@ -732,22 +765,38 @@ func apiConnectContactViaAddress(incognito: Bool, contactId: Int64) async -> (Co return (nil, alert) } -func apiDeleteChat(type: ChatType, id: Int64, notify: Bool? = nil) async throws { +func apiDeleteChat(type: ChatType, id: Int64, chatDeleteMode: ChatDeleteMode = .full(notify: true)) async throws { let chatId = type.rawValue + id.description DispatchQueue.main.async { ChatModel.shared.deletedChats.insert(chatId) } defer { DispatchQueue.main.async { ChatModel.shared.deletedChats.remove(chatId) } } - let r = await chatSendCmd(.apiDeleteChat(type: type, id: id, notify: notify), bgTask: false) + let r = await chatSendCmd(.apiDeleteChat(type: type, id: id, chatDeleteMode: chatDeleteMode), bgTask: false) if case .direct = type, case .contactDeleted = r { return } if case .contactConnection = type, case .contactConnectionDeleted = r { return } if case .group = type, case .groupDeletedUser = r { return } throw r } -func deleteChat(_ chat: Chat, notify: Bool? = nil) async { +func apiDeleteContact(id: Int64, chatDeleteMode: ChatDeleteMode = .full(notify: true)) async throws -> Contact { + let type: ChatType = .direct + let chatId = type.rawValue + id.description + if case .full = chatDeleteMode { + DispatchQueue.main.async { ChatModel.shared.deletedChats.insert(chatId) } + } + defer { + if case .full = chatDeleteMode { + DispatchQueue.main.async { ChatModel.shared.deletedChats.remove(chatId) } + } + } + let r = await chatSendCmd(.apiDeleteChat(type: type, id: id, chatDeleteMode: chatDeleteMode), bgTask: false) + if case let .contactDeleted(_, contact) = r { return contact } + throw r +} + +func deleteChat(_ chat: Chat, chatDeleteMode: ChatDeleteMode = .full(notify: true)) async { do { let cInfo = chat.chatInfo - try await apiDeleteChat(type: cInfo.chatType, id: cInfo.apiId, notify: notify) - DispatchQueue.main.async { ChatModel.shared.removeChat(cInfo.id) } + try await apiDeleteChat(type: cInfo.chatType, id: cInfo.apiId, chatDeleteMode: chatDeleteMode) + await MainActor.run { ChatModel.shared.removeChat(cInfo.id) } } catch let error { logger.error("deleteChat apiDeleteChat error: \(responseError(error))") AlertManager.shared.showAlertMsg( @@ -757,6 +806,39 @@ func deleteChat(_ chat: Chat, notify: Bool? = nil) async { } } +func deleteContactChat(_ chat: Chat, chatDeleteMode: ChatDeleteMode = .full(notify: true)) async -> Alert? { + do { + let cInfo = chat.chatInfo + let ct = try await apiDeleteContact(id: cInfo.apiId, chatDeleteMode: chatDeleteMode) + await MainActor.run { + switch chatDeleteMode { + case .full: + ChatModel.shared.removeChat(cInfo.id) + case .entity: + ChatModel.shared.removeChat(cInfo.id) + ChatModel.shared.addChat(Chat( + chatInfo: .direct(contact: ct), + chatItems: chat.chatItems + )) + case .messages: + ChatModel.shared.removeChat(cInfo.id) + ChatModel.shared.addChat(Chat( + chatInfo: .direct(contact: ct), + chatItems: [] + )) + } + } + } catch let error { + logger.error("deleteContactChat apiDeleteContact error: \(responseError(error))") + return mkAlert( + title: "Error deleting chat!", + message: "Error: \(responseError(error))" + ) + } + return nil +} + + func apiClearChat(type: ChatType, id: Int64) async throws -> ChatInfo { let r = await chatSendCmd(.apiClearChat(type: type, id: id), bgTask: false) if case let .chatCleared(_, updatedChatInfo) = r { return updatedChatInfo } @@ -819,6 +901,21 @@ func apiSetConnectionAlias(connId: Int64, localAlias: String) async throws -> Pe throw r } +func apiSetUserUIThemes(userId: Int64, themes: ThemeModeOverrides?) async -> Bool { + let r = await chatSendCmd(.apiSetUserUIThemes(userId: userId, themes: themes)) + if case .cmdOk = r { return true } + logger.error("apiSetUserUIThemes bad response: \(String(describing: r))") + return false +} + +func apiSetChatUIThemes(chatId: ChatId, themes: ThemeModeOverrides?) async -> Bool { + let r = await chatSendCmd(.apiSetChatUIThemes(chatId: chatId, themes: themes)) + if case .cmdOk = r { return true } + logger.error("apiSetChatUIThemes bad response: \(String(describing: r))") + return false +} + + func apiCreateUserAddress() async throws -> String { let userId = try currentUserId("apiCreateUserAddress") let r = await chatSendCmd(.apiCreateMyAddress(userId: userId)) @@ -866,7 +963,7 @@ func apiAcceptContactRequest(incognito: Bool, contactReqId: Int64) async -> Cont let am = AlertManager.shared if case let .acceptingContactRequest(_, contact) = r { return contact } - if case .chatCmdError(_, .errorAgent(.SMP(.AUTH))) = r { + if case .chatCmdError(_, .errorAgent(.SMP(_, .AUTH))) = r { am.showAlertMsg( title: "Connection error (AUTH)", message: "Sender may have deleted the connection request." @@ -877,7 +974,7 @@ func apiAcceptContactRequest(incognito: Bool, contactReqId: Int64) async -> Cont logger.error("apiAcceptContactRequest error: \(String(describing: r))") am.showAlertMsg( title: "Error accepting contact request", - message: "Error: \(String(describing: r))" + message: "Error: \(responseError(r))" ) } return nil @@ -903,7 +1000,7 @@ func uploadStandaloneFile(user: any UserLike, file: CryptoFile, ctrl: chat_ctrl? return (fileTransferMeta, nil) } else { logger.error("uploadStandaloneFile error: \(String(describing: r))") - return (nil, String(describing: r)) + return (nil, responseError(r)) } } @@ -913,7 +1010,7 @@ func downloadStandaloneFile(user: any UserLike, url: String, file: CryptoFile, c return (rcvFileTransfer, nil) } else { logger.error("downloadStandaloneFile error: \(String(describing: r))") - return (nil, String(describing: r)) + return (nil, responseError(r)) } } @@ -927,14 +1024,19 @@ func standaloneFileInfo(url: String, ctrl: chat_ctrl? = nil) async -> MigrationF } } -func receiveFile(user: any UserLike, fileId: Int64, auto: Bool = false) async { - if let chatItem = await apiReceiveFile(fileId: fileId, encrypted: privacyEncryptLocalFilesGroupDefault.get(), auto: auto) { +func receiveFile(user: any UserLike, fileId: Int64, userApprovedRelays: Bool = false, auto: Bool = false) async { + if let chatItem = await apiReceiveFile( + fileId: fileId, + userApprovedRelays: userApprovedRelays || !privacyAskToApproveRelaysGroupDefault.get(), + encrypted: privacyEncryptLocalFilesGroupDefault.get(), + auto: auto + ) { await chatItemSimpleUpdate(user, chatItem) } } -func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil, auto: Bool = false) async -> AChatItem? { - let r = await chatSendCmd(.receiveFile(fileId: fileId, encrypted: encrypted, inline: inline)) +func apiReceiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool, inline: Bool? = nil, auto: Bool = false) async -> AChatItem? { + let r = await chatSendCmd(.receiveFile(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted, inline: inline)) let am = AlertManager.shared if case let .rcvFileAccepted(_, chatItem) = r { return chatItem } if case .rcvFileAcceptedSndCancelled = r { @@ -947,19 +1049,50 @@ func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil, auto: B } } else if let networkErrorAlert = networkErrorAlert(r) { logger.error("apiReceiveFile network error: \(String(describing: r))") - am.showAlert(networkErrorAlert) + if !auto { + am.showAlert(networkErrorAlert) + } } else { switch chatError(r) { case .fileCancelled: logger.debug("apiReceiveFile ignoring fileCancelled error") case .fileAlreadyReceiving: logger.debug("apiReceiveFile ignoring fileAlreadyReceiving error") + case let .fileNotApproved(fileId, unknownServers): + logger.debug("apiReceiveFile fileNotApproved error") + if !auto { + let srvs = unknownServers.map { s in + if let srv = parseServerAddress(s), !srv.hostnames.isEmpty { + srv.hostnames[0] + } else { + serverHost(s) + } + } + am.showAlert(Alert( + title: Text("Unknown servers!"), + message: Text("Without Tor or VPN, your IP address will be visible to these XFTP relays: \(srvs.sorted().joined(separator: ", "))."), + primaryButton: .default( + Text("Download"), + action: { + Task { + logger.debug("apiReceiveFile fileNotApproved alert - in Task") + if let user = ChatModel.shared.currentUser { + await receiveFile(user: user, fileId: fileId, userApprovedRelays: true) + } + } + } + ), + secondaryButton: .cancel() + )) + } default: logger.error("apiReceiveFile error: \(String(describing: r))") - am.showAlertMsg( - title: "Error receiving file", - message: "Error: \(String(describing: r))" - ) + if !auto { + am.showAlertMsg( + title: "Error receiving file", + message: "Error: \(responseError(r))" + ) + } } } return nil @@ -1024,18 +1157,9 @@ func deleteRemoteCtrl(_ rcId: Int64) async throws { } func networkErrorAlert(_ r: ChatResponse) -> Alert? { - switch r { - case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TIMEOUT))): - return mkAlert( - title: "Connection timeout", - message: "Please check your network connection with \(serverHostname(addr)) and try again." - ) - case let .chatCmdError(_, .errorAgent(.BROKER(addr, .NETWORK))): - return mkAlert( - title: "Connection error", - message: "Please check your network connection with \(serverHostname(addr)) and try again." - ) - default: + if let alert = getNetworkErrorAlert(r) { + return mkAlert(title: alert.title, message: alert.message) + } else { return nil } } @@ -1043,7 +1167,17 @@ func networkErrorAlert(_ r: ChatResponse) -> Alert? { func acceptContactRequest(incognito: Bool, contactRequest: UserContactRequest) async { if let contact = await apiAcceptContactRequest(incognito: incognito, contactReqId: contactRequest.apiId) { let chat = Chat(chatInfo: ChatInfo.direct(contact: contact), chatItems: []) - DispatchQueue.main.async { ChatModel.shared.replaceChat(contactRequest.id, chat) } + await MainActor.run { + ChatModel.shared.replaceChat(contactRequest.id, chat) + NetworkModel.shared.setContactNetworkStatus(contact, .connected) + } + if contact.sndReady { + DispatchQueue.main.async { + dismissAllSheets(animated: true) { + ItemsModel.shared.loadOpenChat(chat.id) + } + } + } } } @@ -1084,12 +1218,18 @@ func apiEndCall(_ contact: Contact) async throws { try await sendCommandOkResp(.apiEndCall(contact: contact)) } -func apiGetCallInvitations() throws -> [RcvCallInvitation] { +func apiGetCallInvitationsSync() throws -> [RcvCallInvitation] { let r = chatSendCmdSync(.apiGetCallInvitations) if case let .callInvitations(invs) = r { return invs } throw r } +func apiGetCallInvitations() async throws -> [RcvCallInvitation] { + let r = await chatSendCmd(.apiGetCallInvitations) + if case let .callInvitations(invs) = r { return invs } + throw r +} + func apiCallStatus(_ contact: Contact, _ status: String) async throws { if let callStatus = WebRTCCallStatus.init(rawValue: status) { try await sendCommandOkResp(.apiCallStatus(contact: contact, callStatus: callStatus)) @@ -1139,7 +1279,7 @@ func apiMarkChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async { do { logger.debug("apiMarkChatItemRead: \(cItem.id)") try await apiChatRead(type: cInfo.chatType, id: cInfo.apiId, itemRange: (cItem.id, cItem.id)) - await MainActor.run { ChatModel.shared.markChatItemRead(cInfo, cItem) } + await ChatModel.shared.markChatItemRead(cInfo, cItem) } catch { logger.error("apiMarkChatItemRead apiChatRead error: \(responseError(error))") } @@ -1180,7 +1320,7 @@ func apiJoinGroup(_ groupId: Int64) async throws -> JoinGroupResult { let r = await chatSendCmd(.apiJoinGroup(groupId: groupId)) switch r { case let .userAcceptedGroupSent(_, groupInfo, _): return .joined(groupInfo: groupInfo) - case .chatCmdError(_, .errorAgent(.SMP(.AUTH))): return .invitationRemoved + case .chatCmdError(_, .errorAgent(.SMP(_, .AUTH))): return .invitationRemoved case .chatCmdError(_, .errorStore(.groupNotFound)): return .groupNotFound default: throw r } @@ -1229,7 +1369,7 @@ func filterMembersToAdd(_ ms: [GMember]) -> [Contact] { let memberContactIds = ms.compactMap{ m in m.wrapped.memberCurrent ? m.wrapped.memberContactId : nil } return ChatModel.shared.chats .compactMap{ $0.chatInfo.contact } - .filter{ c in c.ready && c.active && !memberContactIds.contains(c.apiId) } + .filter{ c in c.sendMsgEnabled && !c.nextSendGrpInv && !memberContactIds.contains(c.apiId) } .sorted{ $0.displayName.lowercased() < $1.displayName.lowercased() } } @@ -1286,6 +1426,26 @@ func apiGetVersion() throws -> CoreVersionInfo { throw r } +func getAgentSubsTotal() async throws -> (SMPServerSubs, Bool) { + let userId = try currentUserId("getAgentSubsTotal") + let r = await chatSendCmd(.getAgentSubsTotal(userId: userId), log: false) + if case let .agentSubsTotal(_, subsTotal, hasSession) = r { return (subsTotal, hasSession) } + logger.error("getAgentSubsTotal error: \(String(describing: r))") + throw r +} + +func getAgentServersSummary() throws -> PresentedServersSummary { + let userId = try currentUserId("getAgentServersSummary") + let r = chatSendCmdSync(.getAgentServersSummary(userId: userId), log: false) + if case let .agentServersSummary(_, serversSummary) = r { return serversSummary } + logger.error("getAgentServersSummary error: \(String(describing: r))") + throw r +} + +func resetAgentServersStats() async throws { + try await sendCommandOkResp(.resetAgentServersStats) +} + private func currentUserId(_ funcName: String) throws -> Int64 { if let userId = ChatModel.shared.currentUser?.userId { return userId @@ -1305,8 +1465,7 @@ func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = ni if encryptionStartedDefault.get() { encryptionStartedDefault.set(false) } - try apiSetTempFolder(tempFolder: getTempFilesDirectory().path) - try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) + try apiSetAppFilePaths(filesFolder: getAppFilesDirectory().path, tempFolder: getTempFilesDirectory().path, assetsFolder: getWallpaperDirectory().deletingLastPathComponent().path) try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get()) m.chatInitialized = true m.currentUser = try apiGetActiveUser() @@ -1358,15 +1517,16 @@ func startChat(refreshInvitations: Bool = true) throws { logger.debug("startChat") let m = ChatModel.shared try setNetworkConfig(getNetCfg()) - let justStarted = try apiStartChat() + let chatRunning = try apiCheckChatRunning() m.users = try listUsers() - if justStarted { + if !chatRunning { try getUserChatData() NtfManager.shared.setNtfBadgeCount(m.totalUnreadCountForAllUsers()) if (refreshInvitations) { - try refreshCallInvitations() + Task { try await refreshCallInvitations() } } (m.savedToken, m.tokenStatus, m.notificationMode, m.notificationServer) = apiGetNtfToken() + _ = try apiStartChat() // deviceToken is set when AppDelegate.application(didRegisterForRemoteNotificationsWithDeviceToken:) is called, // when it is called before startChat if let token = m.deviceToken { @@ -1391,8 +1551,7 @@ func startChatWithTemporaryDatabase(ctrl: chat_ctrl) throws -> User? { logger.debug("startChatWithTemporaryDatabase") let migrationActiveUser = try? apiGetActiveUser(ctrl: ctrl) ?? apiCreateActiveUser(Profile(displayName: "Temp", fullName: ""), ctrl: ctrl) try setNetworkConfig(getNetCfg(), ctrl: ctrl) - try apiSetTempFolder(tempFolder: getMigrationTempFilesDirectory().path, ctrl: ctrl) - try apiSetFilesFolder(filesFolder: getMigrationTempFilesDirectory().path, ctrl: ctrl) + try apiSetAppFilePaths(filesFolder: getMigrationTempFilesDirectory().path, tempFolder: getMigrationTempFilesDirectory().path, assetsFolder: getWallpaperDirectory().deletingLastPathComponent().path, ctrl: ctrl) _ = try apiStartChat(ctrl: ctrl) return migrationActiveUser } @@ -1438,7 +1597,7 @@ func getUserChatData() throws { m.userAddress = try apiGetUserAddress() m.chatItemTTL = try getChatItemTTL() let chats = try apiGetChats() - m.chats = chats.map { Chat.init($0) } + m.updateChats(chats) } private func getUserChatDataAsync() async throws { @@ -1450,12 +1609,12 @@ private func getUserChatDataAsync() async throws { await MainActor.run { m.userAddress = userAddress m.chatItemTTL = chatItemTTL - m.chats = chats.map { Chat.init($0) } + m.updateChats(chats) } } else { await MainActor.run { m.userAddress = nil - m.chats = [] + m.updateChats([]) } } } @@ -1505,6 +1664,7 @@ func processReceivedMsg(_ res: ChatResponse) async { await TerminalItems.shared.add(.resp(.now, res)) } let m = ChatModel.shared + let n = NetworkModel.shared logger.debug("processReceivedMsg: \(res.responseType)") switch res { case let .contactDeletedByContact(user, contact): @@ -1527,7 +1687,7 @@ func processReceivedMsg(_ res: ChatResponse) async { NtfManager.shared.notifyContactConnected(user, contact) } await MainActor.run { - m.setContactNetworkStatus(contact, .connected) + n.setContactNetworkStatus(contact, .connected) } case let .contactConnecting(user, contact): if active(user) && contact.directOrUsed { @@ -1539,6 +1699,19 @@ func processReceivedMsg(_ res: ChatResponse) async { } } } + case let .contactSndReady(user, contact): + if active(user) && contact.directOrUsed { + await MainActor.run { + m.updateContact(contact) + if let conn = contact.activeConn { + m.dismissConnReqView(conn.id) + m.removeChat(conn.id) + } + } + } + await MainActor.run { + n.setContactNetworkStatus(contact, .connected) + } case let .receivedContactRequest(user, contactRequest): if active(user) { let cInfo = ChatInfo.contactRequest(contactRequest: contactRequest) @@ -1571,7 +1744,7 @@ func processReceivedMsg(_ res: ChatResponse) async { if active(user) && m.hasChat(mergedContact.id) { await MainActor.run { if m.chatId == mergedContact.id { - m.chatId = intoContact.id + ItemsModel.shared.loadOpenChat(mergedContact.id) } m.removeChat(mergedContact.id) } @@ -1579,27 +1752,27 @@ func processReceivedMsg(_ res: ChatResponse) async { case let .networkStatus(status, connections): // dispatch queue to synchronize access networkStatusesLock.sync { - var ns = m.networkStatuses + var ns = n.networkStatuses // slow loop is on the background thread for cId in connections { ns[cId] = status } // fast model update is on the main thread DispatchQueue.main.sync { - m.networkStatuses = ns + n.networkStatuses = ns } } case let .networkStatuses(_, statuses): () // dispatch queue to synchronize access networkStatusesLock.sync { - var ns = m.networkStatuses + var ns = n.networkStatuses // slow loop is on the background thread for s in statuses { ns[s.agentConnId] = s.networkStatus } // fast model update is on the main thread DispatchQueue.main.sync { - m.networkStatuses = ns + n.networkStatuses = ns } } case let .newChatItem(user, aChatItem): @@ -1642,21 +1815,25 @@ func processReceivedMsg(_ res: ChatResponse) async { m.updateChatItem(r.chatInfo, r.chatReaction.chatItem) } } - case let .chatItemDeleted(user, deletedChatItem, toChatItem, _): + case let .chatItemsDeleted(user, items, _): if !active(user) { - if toChatItem == nil && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled { - await MainActor.run { - m.decreaseUnreadCounter(user: user) + for item in items { + if item.toChatItem == nil && item.deletedChatItem.chatItem.isRcvNew && item.deletedChatItem.chatInfo.ntfsEnabled { + await MainActor.run { + m.decreaseUnreadCounter(user: user) + } } } return } await MainActor.run { - if let toChatItem = toChatItem { - _ = m.upsertChatItem(toChatItem.chatInfo, toChatItem.chatItem) - } else { - m.removeChatItem(deletedChatItem.chatInfo, deletedChatItem.chatItem) + for item in items { + if let toChatItem = item.toChatItem { + _ = m.upsertChatItem(toChatItem.chatInfo, toChatItem.chatItem) + } else { + m.removeChatItem(item.deletedChatItem.chatInfo, item.deletedChatItem.chatItem) + } } } case let .receivedGroupInvitation(user, groupInfo, _, _): @@ -1736,7 +1913,7 @@ func processReceivedMsg(_ res: ChatResponse) async { } if let contact = memberContact { await MainActor.run { - m.setContactNetworkStatus(contact, .connected) + n.setContactNetworkStatus(contact, .connected) } } case let .groupUpdated(user, toGroup): @@ -1778,11 +1955,15 @@ func processReceivedMsg(_ res: ChatResponse) async { if let aChatItem = aChatItem { await chatItemSimpleUpdate(user, aChatItem) } - case let .rcvFileError(user, aChatItem, _): + case let .rcvFileError(user, aChatItem, _, _): if let aChatItem = aChatItem { await chatItemSimpleUpdate(user, aChatItem) Task { cleanupFile(aChatItem) } } + case let .rcvFileWarning(user, aChatItem, _, _): + if let aChatItem = aChatItem { + await chatItemSimpleUpdate(user, aChatItem) + } case let .sndFileStart(user, aChatItem, _): await chatItemSimpleUpdate(user, aChatItem) case let .sndFileComplete(user, aChatItem, _): @@ -1799,11 +1980,15 @@ func processReceivedMsg(_ res: ChatResponse) async { } case let .sndFileCompleteXFTP(user, aChatItem, _): await chatItemSimpleUpdate(user, aChatItem) - case let .sndFileError(user, aChatItem, _): + case let .sndFileError(user, aChatItem, _, _): if let aChatItem = aChatItem { await chatItemSimpleUpdate(user, aChatItem) Task { cleanupFile(aChatItem) } } + case let .sndFileWarning(user, aChatItem, _, _): + if let aChatItem = aChatItem { + await chatItemSimpleUpdate(user, aChatItem) + } case let .callInvitation(invitation): await MainActor.run { m.callInvitations[invitation.contact.id] = invitation @@ -1849,21 +2034,35 @@ func processReceivedMsg(_ res: ChatResponse) async { } case .chatSuspended: chatSuspended() - case let .contactSwitch(_, contact, switchProgress): - await MainActor.run { - m.updateContactConnectionStats(contact, switchProgress.connectionStats) + case let .contactSwitch(user, contact, switchProgress): + if active(user) { + await MainActor.run { + m.updateContactConnectionStats(contact, switchProgress.connectionStats) + } } - case let .groupMemberSwitch(_, groupInfo, member, switchProgress): - await MainActor.run { - m.updateGroupMemberConnectionStats(groupInfo, member, switchProgress.connectionStats) + case let .groupMemberSwitch(user, groupInfo, member, switchProgress): + if active(user) { + await MainActor.run { + m.updateGroupMemberConnectionStats(groupInfo, member, switchProgress.connectionStats) + } } - case let .contactRatchetSync(_, contact, ratchetSyncProgress): - await MainActor.run { - m.updateContactConnectionStats(contact, ratchetSyncProgress.connectionStats) + case let .contactRatchetSync(user, contact, ratchetSyncProgress): + if active(user) { + await MainActor.run { + m.updateContactConnectionStats(contact, ratchetSyncProgress.connectionStats) + } } - case let .groupMemberRatchetSync(_, groupInfo, member, ratchetSyncProgress): - await MainActor.run { - m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats) + case let .groupMemberRatchetSync(user, groupInfo, member, ratchetSyncProgress): + if active(user) { + await MainActor.run { + m.updateGroupMemberConnectionStats(groupInfo, member, ratchetSyncProgress.connectionStats) + } + } + case let .contactDisabled(user, contact): + if active(user) { + await MainActor.run { + m.updateContact(contact) + } } case let .remoteCtrlFound(remoteCtrl, ctrlAppInfo_, appVersion, compatible): await MainActor.run { @@ -1887,12 +2086,30 @@ func processReceivedMsg(_ res: ChatResponse) async { let state = UIRemoteCtrlSessionState.connected(remoteCtrl: remoteCtrl, sessionCode: m.remoteCtrlSession?.sessionCode ?? "") m.remoteCtrlSession = m.remoteCtrlSession?.updateState(state) } - case .remoteCtrlStopped: + case let .remoteCtrlStopped(_, rcStopReason): // 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 + dismissAllSheets() { + switch rcStopReason { + case .disconnected: + () + case .connectionFailed(.errorAgent(.RCP(.identity))): + AlertManager.shared.showAlertMsg( + title: "Connection with desktop stopped", + message: "This link was used with another mobile device, please create a new link on the desktop." + ) + default: + AlertManager.shared.showAlert(Alert( + title: Text("Connection with desktop stopped"), + message: Text("Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.\nPlease share any other issues with the developers."), + primaryButton: .default(Text("Ok")), + secondaryButton: .default(Text("Copy error")) { UIPasteboard.general.string = String(describing: rcStopReason) } + )) + } + } } if case .connected = sess.sessionState { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { @@ -1921,12 +2138,13 @@ func processReceivedMsg(_ res: ChatResponse) async { func switchToLocalSession() { let m = ChatModel.shared + let n = NetworkModel.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) + n.networkStatuses = Dictionary(uniqueKeysWithValues: statuses) } catch let error { logger.debug("error updating chat data: \(responseError(error))") } @@ -1949,23 +2167,30 @@ func chatItemSimpleUpdate(_ user: any UserLike, _ aChatItem: AChatItem) async { } } -func refreshCallInvitations() throws { +func refreshCallInvitations() async throws { let m = ChatModel.shared - let callInvitations = try justRefreshCallInvitations() - if let (chatId, ntfAction) = m.ntfCallInvitationAction, - let invitation = m.callInvitations.removeValue(forKey: chatId) { - m.ntfCallInvitationAction = nil - CallController.shared.callAction(invitation: invitation, action: ntfAction) - } else if let invitation = callInvitations.last(where: { $0.user.showNotifications }) { - activateCall(invitation) + let callInvitations = try await apiGetCallInvitations() + await MainActor.run { + m.callInvitations = callsByChat(callInvitations) + if let (chatId, ntfAction) = m.ntfCallInvitationAction, + let invitation = m.callInvitations.removeValue(forKey: chatId) { + m.ntfCallInvitationAction = nil + CallController.shared.callAction(invitation: invitation, action: ntfAction) + } else if let invitation = callInvitations.last(where: { $0.user.showNotifications }) { + activateCall(invitation) + } } } -func justRefreshCallInvitations() throws -> [RcvCallInvitation] { - let m = ChatModel.shared - let callInvitations = try apiGetCallInvitations() - m.callInvitations = callInvitations.reduce(into: [ChatId: RcvCallInvitation]()) { result, inv in result[inv.contact.id] = inv } - return callInvitations +func justRefreshCallInvitations() throws { + let callInvitations = try apiGetCallInvitationsSync() + ChatModel.shared.callInvitations = callsByChat(callInvitations) +} + +private func callsByChat(_ callInvitations: [RcvCallInvitation]) -> [ChatId: RcvCallInvitation] { + callInvitations.reduce(into: [ChatId: RcvCallInvitation]()) { + result, inv in result[inv.contact.id] = inv + } } func activateCall(_ callInvitation: RcvCallInvitation) { diff --git a/apps/ios/Shared/Model/SuspendChat.swift b/apps/ios/Shared/Model/SuspendChat.swift index 4494adc0e8..92bcdcac53 100644 --- a/apps/ios/Shared/Model/SuspendChat.swift +++ b/apps/ios/Shared/Model/SuspendChat.swift @@ -36,6 +36,18 @@ private func _suspendChat(timeout: Int) { } } +let seSubscriber = seMessageSubscriber { + switch $0 { + case let .state(state): + switch state { + case .inactive: + if AppChatState.shared.value.inactive { activateChat() } + case .sendingMessage: + if AppChatState.shared.value.canSuspend { suspendChat() } + } + } +} + func suspendChat() { suspendLockQueue.sync { _suspendChat(timeout: appSuspendTimeout) diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 7d69466c07..7f2c3b5866 100644 --- a/apps/ios/Shared/SimpleXApp.swift +++ b/apps/ios/Shared/SimpleXApp.swift @@ -39,6 +39,7 @@ struct SimpleXApp: App { // so that it's computed by the time view renders, and not on event after rendering ContentView(contentAccessAuthenticationExtended: !authenticationExpired()) .environmentObject(chatModel) + .environmentObject(AppTheme.shared) .onOpenURL { url in logger.debug("ContentView.onOpenURL: \(url)") chatModel.appOpenUrl = url @@ -57,6 +58,7 @@ struct SimpleXApp: App { } .onChange(of: scenePhase) { phase in logger.debug("scenePhase was \(String(describing: scenePhase)), now \(String(describing: phase))") + AppSheetState.shared.scenePhaseActive = phase == .active switch (phase) { case .background: // --- authentication @@ -81,9 +83,11 @@ struct SimpleXApp: App { if appState != .stopped { startChatAndActivate { if appState.inactive && chatModel.chatRunning == true { - updateChats() - if !chatModel.showCallView && !CallController.shared.hasActiveCalls() { - updateCallInvitations() + Task { + await updateChats() + if !chatModel.showCallView && !CallController.shared.hasActiveCalls() { + await updateCallInvitations() + } } } } @@ -128,16 +132,16 @@ struct SimpleXApp: App { } } - private func updateChats() { + private func updateChats() async { do { - let chats = try apiGetChats() - chatModel.updateChats(with: chats) + let chats = try await apiGetChatsAsync() + await MainActor.run { chatModel.updateChats(chats) } if let id = chatModel.chatId, let chat = chatModel.getChat(id) { - loadChat(chat: chat) + Task { await loadChat(chat: chat, clearItems: false) } } if let ncr = chatModel.ntfContactRequest { - chatModel.ntfContactRequest = nil + await MainActor.run { chatModel.ntfContactRequest = nil } if case let .contactRequest(contactRequest) = chatModel.getChat(ncr.chatId)?.chatInfo { Task { await acceptContactRequest(incognito: ncr.incognito, contactRequest: contactRequest) } } @@ -147,9 +151,9 @@ struct SimpleXApp: App { } } - private func updateCallInvitations() { + private func updateCallInvitations() async { do { - try refreshCallInvitations() + try await refreshCallInvitations() } catch let error { logger.error("apiGetCallInvitations: cannot update call invitations \(responseError(error))") } diff --git a/apps/ios/Shared/Theme/Theme.swift b/apps/ios/Shared/Theme/Theme.swift new file mode 100644 index 0000000000..e2641eb8dd --- /dev/null +++ b/apps/ios/Shared/Theme/Theme.swift @@ -0,0 +1,199 @@ +// +// Theme.swift +// SimpleX (iOS) +// +// Created by Avently on 14.06.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import Foundation +import SwiftUI +import SimpleXChat + +var CurrentColors: ThemeManager.ActiveTheme = ThemeManager.currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + +var MenuTextColor: Color { if isInDarkTheme() { AppTheme.shared.colors.onBackground.opacity(0.8) } else { Color.black } } +var NoteFolderIconColor: Color { AppTheme.shared.appColors.primaryVariant2 } + +func isInDarkTheme() -> Bool { !CurrentColors.colors.isLight } + +class AppTheme: ObservableObject, Equatable { + static let shared = AppTheme(name: CurrentColors.name, base: CurrentColors.base, colors: CurrentColors.colors, appColors: CurrentColors.appColors, wallpaper: CurrentColors.wallpaper) + + var name: String + var base: DefaultTheme + @ObservedObject var colors: Colors + @ObservedObject var appColors: AppColors + @ObservedObject var wallpaper: AppWallpaper + + init(name: String, base: DefaultTheme, colors: Colors, appColors: AppColors, wallpaper: AppWallpaper) { + self.name = name + self.base = base + self.colors = colors + self.appColors = appColors + self.wallpaper = wallpaper + } + + static func == (lhs: AppTheme, rhs: AppTheme) -> Bool { + lhs.name == rhs.name && + lhs.colors == rhs.colors && + lhs.appColors == rhs.appColors && + lhs.wallpaper == rhs.wallpaper + } + + func updateFromCurrentColors() { + objectWillChange.send() + name = CurrentColors.name + base = CurrentColors.base + colors.updateColorsFrom(CurrentColors.colors) + appColors.updateColorsFrom(CurrentColors.appColors) + wallpaper.updateWallpaperFrom(CurrentColors.wallpaper) + } +} + +struct ThemedBackground: ViewModifier { + @EnvironmentObject var theme: AppTheme + var grouped: Bool = false + + func body(content: Content) -> some View { + content + .background( + theme.base == DefaultTheme.SIMPLEX + ? LinearGradient( + colors: [ + grouped + ? theme.colors.background.lighter(0.4).asGroupedBackground(theme.base.mode) + : theme.colors.background.lighter(0.4), + grouped + ? theme.colors.background.darker(0.4).asGroupedBackground(theme.base.mode) + : theme.colors.background.darker(0.4) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + : LinearGradient( + colors: [], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .background( + theme.base == DefaultTheme.SIMPLEX + ? Color.clear + : grouped + ? theme.colors.background.asGroupedBackground(theme.base.mode) + : theme.colors.background + ) + } +} + +var systemInDarkThemeCurrently: Bool { + return UITraitCollection.current.userInterfaceStyle == .dark +} + +func reactOnDarkThemeChanges(_ inDarkNow: Bool) { + if currentThemeDefault.get() == DefaultTheme.SYSTEM_THEME_NAME && CurrentColors.colors.isLight == inDarkNow { + // Change active colors from light to dark and back based on system theme + ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME) + } +} + +extension ThemeWallpaper { + public func importFromString() -> ThemeWallpaper { + if preset == nil, let image { + // Need to save image from string and to save its path + if let parsed = UIImage(base64Encoded: image), + let filename = saveWallpaperFile(image: parsed) { + var copy = self + copy.image = nil + copy.imageFile = filename + return copy + } else { + return ThemeWallpaper() + } + } else { + return self + } + } + + func withFilledWallpaperBase64() -> ThemeWallpaper { + let aw = toAppWallpaper() + let type = aw.type + let preset: String? = if case let WallpaperType.preset(filename, _) = type { filename } else { nil } + let scale: Float? = if case let WallpaperType.preset(_, scale) = type { scale } else { if case let WallpaperType.image(_, scale, _) = type { scale } else { 1.0 } } + let scaleType: WallpaperScaleType? = if case let WallpaperType.image(_, _, scaleType) = type { scaleType } else { nil } + let image: String? = if case WallpaperType.image = type, let image = type.uiImage { resizeImageToStrSize(image, maxDataSize: 5_000_000) } else { nil } + return ThemeWallpaper ( + preset: preset, + scale: scale, + scaleType: scaleType, + background: aw.background?.toReadableHex(), + tint: aw.tint?.toReadableHex(), + image: image, + imageFile: nil + ) + } +} + +extension ThemeModeOverride { + func removeSameColors(_ base: DefaultTheme, colorsToCompare tc: ThemeColors) -> ThemeModeOverride { + let wallpaperType = WallpaperType.from(wallpaper) ?? WallpaperType.empty + let w: ThemeWallpaper + switch wallpaperType { + case let WallpaperType.preset(filename, scale): + let p = PresetWallpaper.from(filename) + w = ThemeWallpaper( + preset: filename, + scale: scale ?? wallpaper?.scale, + scaleType: nil, + background: p?.background[base]?.toReadableHex(), + tint: p?.tint[base]?.toReadableHex(), + image: nil, + imageFile: nil + ) + case WallpaperType.image: + w = ThemeWallpaper( + preset: nil, + scale: nil, + scaleType: WallpaperScaleType.fill, + background: Color.clear.toReadableHex(), + tint: Color.clear.toReadableHex(), + image: nil, + imageFile: nil + ) + default: + w = ThemeWallpaper() + } + let wallpaper: ThemeWallpaper? = if let wallpaper { + ThemeWallpaper( + preset: wallpaper.preset, + scale: wallpaper.scale != w.scale ? wallpaper.scale : nil, + scaleType: wallpaper.scaleType != w.scaleType ? wallpaper.scaleType : nil, + background: wallpaper.background != w.background ? wallpaper.background : nil, + tint: wallpaper.tint != w.tint ? wallpaper.tint : nil, + image: wallpaper.image, + imageFile: wallpaper.imageFile + ) + } else { + nil + } + return ThemeModeOverride( + mode: self.mode, + colors: ThemeColors( + primary: colors.primary != tc.primary ? colors.primary : nil, + primaryVariant: colors.primaryVariant != tc.primaryVariant ? colors.primaryVariant : nil, + secondary: colors.secondary != tc.secondary ? colors.secondary : nil, + secondaryVariant: colors.secondaryVariant != tc.secondaryVariant ? colors.secondaryVariant : nil, + background: colors.background != tc.background ? colors.background : nil, + surface: colors.surface != tc.surface ? colors.surface : nil, + title: colors.title != tc.title ? colors.title : nil, + primaryVariant2: colors.primaryVariant2 != tc.primaryVariant2 ? colors.primary : nil, + sentMessage: colors.sentMessage != tc.sentMessage ? colors.sentMessage : nil, + sentQuote: colors.sentQuote != tc.sentQuote ? colors.sentQuote : nil, + receivedMessage: colors.receivedMessage != tc.receivedMessage ? colors.receivedMessage : nil, + receivedQuote: colors.receivedQuote != tc.receivedQuote ? colors.receivedQuote : nil + ), + wallpaper: wallpaper + ) + } +} diff --git a/apps/ios/Shared/Theme/ThemeManager.swift b/apps/ios/Shared/Theme/ThemeManager.swift new file mode 100644 index 0000000000..9d648750d1 --- /dev/null +++ b/apps/ios/Shared/Theme/ThemeManager.swift @@ -0,0 +1,303 @@ +// +// ThemeManager.swift +// SimpleX (iOS) +// +// Created by Avently on 03.06.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import Foundation +import SwiftUI +import SimpleXChat + +class ThemeManager { + struct ActiveTheme: Equatable { + let name: String + let base: DefaultTheme + let colors: Colors + let appColors: AppColors + var wallpaper: AppWallpaper = AppWallpaper(background: nil, tint: nil, type: .empty) + + func toAppTheme() -> AppTheme { + AppTheme(name: name, base: base, colors: colors, appColors: appColors, wallpaper: wallpaper) + } + } + + private static func systemDarkThemeColors() -> (Colors, DefaultTheme) { + switch systemDarkThemeDefault.get() { + case DefaultTheme.DARK.themeName: (DarkColorPalette, DefaultTheme.DARK) + case DefaultTheme.SIMPLEX.themeName: (SimplexColorPalette, DefaultTheme.SIMPLEX) + case DefaultTheme.BLACK.themeName: (BlackColorPalette, DefaultTheme.BLACK) + default: (SimplexColorPalette, DefaultTheme.SIMPLEX) + } + } + + private static func nonSystemThemeName() -> String { + let themeName = currentThemeDefault.get() + return if themeName != DefaultTheme.SYSTEM_THEME_NAME { + themeName + } else { + systemInDarkThemeCurrently ? systemDarkThemeDefault.get() : DefaultTheme.LIGHT.themeName + } + } + + static func defaultActiveTheme(_ appSettingsTheme: [ThemeOverrides]) -> ThemeOverrides? { + let nonSystemThemeName = nonSystemThemeName() + let defaultThemeId = currentThemeIdsDefault.get()[nonSystemThemeName] + return appSettingsTheme.getTheme(defaultThemeId) + } + + static func defaultActiveTheme(_ perUserTheme: ThemeModeOverrides?, _ appSettingsTheme: [ThemeOverrides]) -> ThemeModeOverride { + let perUserTheme = !CurrentColors.colors.isLight ? perUserTheme?.dark : perUserTheme?.light + if let perUserTheme { + return perUserTheme + } + let defaultTheme = defaultActiveTheme(appSettingsTheme) + return ThemeModeOverride(mode: CurrentColors.base.mode, colors: defaultTheme?.colors ?? ThemeColors(), wallpaper: defaultTheme?.wallpaper ?? ThemeWallpaper.from(PresetWallpaper.school.toType(CurrentColors.base), nil, nil)) + } + + static func currentColors(_ themeOverridesForType: WallpaperType?, _ perChatTheme: ThemeModeOverride?, _ perUserTheme: ThemeModeOverrides?, _ appSettingsTheme: [ThemeOverrides]) -> ActiveTheme { + let themeName = currentThemeDefault.get() + let nonSystemThemeName = nonSystemThemeName() + let defaultTheme = defaultActiveTheme(appSettingsTheme) + + let baseTheme = switch nonSystemThemeName { + case DefaultTheme.LIGHT.themeName: ActiveTheme(name: DefaultTheme.LIGHT.themeName, base: DefaultTheme.LIGHT, colors: LightColorPalette.clone(), appColors: LightColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.LIGHT))) + case DefaultTheme.DARK.themeName: ActiveTheme(name: DefaultTheme.DARK.themeName, base: DefaultTheme.DARK, colors: DarkColorPalette.clone(), appColors: DarkColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.DARK))) + case DefaultTheme.SIMPLEX.themeName: ActiveTheme(name: DefaultTheme.SIMPLEX.themeName, base: DefaultTheme.SIMPLEX, colors: SimplexColorPalette.clone(), appColors: SimplexColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.SIMPLEX))) + case DefaultTheme.BLACK.themeName: ActiveTheme(name: DefaultTheme.BLACK.themeName, base: DefaultTheme.BLACK, colors: BlackColorPalette.clone(), appColors: BlackColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.BLACK))) + default: ActiveTheme(name: DefaultTheme.LIGHT.themeName, base: DefaultTheme.LIGHT, colors: LightColorPalette.clone(), appColors: LightColorPaletteApp.clone(), wallpaper: AppWallpaper(background: nil, tint: nil, type: PresetWallpaper.school.toType(DefaultTheme.LIGHT))) + } + + let perUserTheme = baseTheme.colors.isLight ? perUserTheme?.light : perUserTheme?.dark + let theme = appSettingsTheme.sameTheme(themeOverridesForType ?? perChatTheme?.type ?? perUserTheme?.type ?? defaultTheme?.wallpaper?.toAppWallpaper().type, nonSystemThemeName) ?? defaultTheme + + if theme == nil && perUserTheme == nil && perChatTheme == nil && themeOverridesForType == nil { + return ActiveTheme(name: themeName, base: baseTheme.base, colors: baseTheme.colors, appColors: baseTheme.appColors, wallpaper: baseTheme.wallpaper) + } + let presetWallpaperTheme: ThemeColors? = if let themeOverridesForType, case let WallpaperType.preset(filename, _) = themeOverridesForType { + PresetWallpaper.from(filename)?.colors[baseTheme.base] + } else if let wallpaper = perChatTheme?.wallpaper { + if let preset = wallpaper.preset { PresetWallpaper.from(preset)?.colors[baseTheme.base] } else { nil } + } else if let wallpaper = perUserTheme?.wallpaper { + if let preset = wallpaper.preset { PresetWallpaper.from(preset)?.colors[baseTheme.base] } else { nil } + } else { + if let preset = theme?.wallpaper?.preset { PresetWallpaper.from(preset)?.colors[baseTheme.base] } else { nil } + } + + let themeOrEmpty = theme ?? ThemeOverrides(base: baseTheme.base) + let colors = themeOrEmpty.toColors(themeOrEmpty.base, perChatTheme?.colors, perUserTheme?.colors, presetWallpaperTheme) + return ActiveTheme( + name: themeName, + base: baseTheme.base, + colors: colors, + appColors: themeOrEmpty.toAppColors(themeOrEmpty.base, perChatTheme?.colors, perChatTheme?.type, perUserTheme?.colors, perUserTheme?.type, presetWallpaperTheme), + wallpaper: themeOrEmpty.toAppWallpaper(themeOverridesForType, perChatTheme, perUserTheme, colors.background) + ) + } + + static func currentThemeOverridesForExport(_ themeOverridesForType: WallpaperType?, _ perChatTheme: ThemeModeOverride?, _ perUserTheme: ThemeModeOverrides?) -> ThemeOverrides { + let current = currentColors(themeOverridesForType, perChatTheme, perUserTheme, themeOverridesDefault.get()) + let wType = current.wallpaper.type + let wBackground = current.wallpaper.background + let wTint = current.wallpaper.tint + let w: ThemeWallpaper? = if case WallpaperType.empty = wType { + nil + } else { + ThemeWallpaper.from(wType, wBackground?.toReadableHex(), wTint?.toReadableHex()).withFilledWallpaperBase64() + } + return ThemeOverrides( + themeId: "", + base: current.base, + colors: ThemeColors.from(current.colors, current.appColors), + wallpaper: w + ) + } + + static func applyTheme(_ theme: String) { + currentThemeDefault.set(theme) + CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + AppTheme.shared.updateFromCurrentColors() + let tint = UIColor(CurrentColors.colors.primary) + if SceneDelegate.windowStatic?.tintColor != tint { + SceneDelegate.windowStatic?.tintColor = tint + } +// applyNavigationBarColors(CurrentColors.toAppTheme()) + } + + static func adjustWindowStyle() { + let style = switch currentThemeDefault.get() { + case DefaultTheme.LIGHT.themeName: UIUserInterfaceStyle.light + case DefaultTheme.SYSTEM_THEME_NAME: UIUserInterfaceStyle.unspecified + default: UIUserInterfaceStyle.dark + } + if SceneDelegate.windowStatic?.overrideUserInterfaceStyle != style { + SceneDelegate.windowStatic?.overrideUserInterfaceStyle = style + } + } + +// static func applyNavigationBarColors(_ theme: AppTheme) { +// let baseColors = switch theme.base { +// case DefaultTheme.LIGHT: LightColorPaletteApp +// case DefaultTheme.DARK: DarkColorPaletteApp +// case DefaultTheme.SIMPLEX: SimplexColorPaletteApp +// case DefaultTheme.BLACK: BlackColorPaletteApp +// } +// let isDefaultColor = baseColors.title == theme.appColors.title +// +// let title = UIColor(theme.appColors.title) +// if !isDefaultColor && UINavigationBar.appearance().titleTextAttributes?.first as? UIColor != title { +// UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: title] +// UINavigationBar.appearance().largeTitleTextAttributes = [.foregroundColor: title] +// } else { +// UINavigationBar.appearance().titleTextAttributes = nil +// UINavigationBar.appearance().largeTitleTextAttributes = nil +// } +// } + + static func changeDarkTheme(_ theme: String) { + systemDarkThemeDefault.set(theme) + CurrentColors = currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + AppTheme.shared.updateFromCurrentColors() + } + + static func saveAndApplyThemeColor(_ baseTheme: DefaultTheme, _ name: ThemeColor, _ color: Color? = nil, _ pref: CodableDefault<[ThemeOverrides]>? = nil) { + let nonSystemThemeName = baseTheme.themeName + let pref = pref ?? themeOverridesDefault + let overrides = pref.get() + let themeId = currentThemeIdsDefault.get()[nonSystemThemeName] + let prevValue = overrides.getTheme(themeId) ?? ThemeOverrides(base: baseTheme) + pref.set(overrides.replace(prevValue.withUpdatedColor(name, color?.toReadableHex()))) + var themeIds = currentThemeIdsDefault.get() + themeIds[nonSystemThemeName] = prevValue.themeId + currentThemeIdsDefault.set(themeIds) + applyTheme(currentThemeDefault.get()) + } + + static func applyThemeColor(name: ThemeColor, color: Color? = nil, pref: Binding) { + pref.wrappedValue = pref.wrappedValue.withUpdatedColor(name, color?.toReadableHex()) + } + + static func saveAndApplyWallpaper(_ baseTheme: DefaultTheme, _ type: WallpaperType?, _ pref: CodableDefault<[ThemeOverrides]>?) { + let nonSystemThemeName = baseTheme.themeName + let pref = pref ?? themeOverridesDefault + let overrides = pref.get() + let theme = overrides.sameTheme(type, baseTheme.themeName) + var prevValue = theme ?? ThemeOverrides(base: baseTheme) + prevValue.wallpaper = if let type { + if case WallpaperType.empty = type { + nil as ThemeWallpaper? + } else { + ThemeWallpaper.from(type, prevValue.wallpaper?.background, prevValue.wallpaper?.tint) + } + } else { + nil + } + pref.set(overrides.replace(prevValue)) + var themeIds = currentThemeIdsDefault.get() + themeIds[nonSystemThemeName] = prevValue.themeId + currentThemeIdsDefault.set(themeIds) + applyTheme(nonSystemThemeName) + } + + static func copyFromSameThemeOverrides(_ type: WallpaperType?, _ lowerLevelOverride: ThemeModeOverride?, _ pref: Binding) -> Bool { + let overrides = themeOverridesDefault.get() + let sameWallpaper: ThemeWallpaper? = if let wallpaper = lowerLevelOverride?.wallpaper, lowerLevelOverride?.type?.sameType(type) == true { + wallpaper + } else { + overrides.sameTheme(type, CurrentColors.base.themeName)?.wallpaper + } + guard let sameWallpaper else { + if let type { + var w: ThemeWallpaper = ThemeWallpaper.from(type, nil, nil) + w.scale = nil + w.scaleType = nil + w.background = nil + w.tint = nil + pref.wrappedValue = ThemeModeOverride(mode: CurrentColors.base.mode, wallpaper: w) + } else { + // Make an empty wallpaper to override any top level ones + pref.wrappedValue = ThemeModeOverride(mode: CurrentColors.base.mode, wallpaper: ThemeWallpaper()) + } + return true + } + var type = sameWallpaper.toAppWallpaper().type + if case let WallpaperType.image(filename, scale, scaleType) = type, sameWallpaper.imageFile == filename { + // same image file. Needs to be copied first in order to be able to remove the file once it's not needed anymore without affecting main theme override + if let filename = saveWallpaperFile(url: getWallpaperFilePath(filename)) { + type = WallpaperType.image(filename, scale, scaleType) + } else { + logger.error("Error while copying wallpaper from global overrides to chat overrides") + return false + } + } + var prevValue = pref.wrappedValue + var w = ThemeWallpaper.from(type, nil, nil) + w.scale = nil + w.scaleType = nil + w.background = nil + w.tint = nil + prevValue.colors = ThemeColors() + prevValue.wallpaper = w + pref.wrappedValue = prevValue + return true + } + + static func applyWallpaper(_ type: WallpaperType?, _ pref: Binding) { + var prevValue = pref.wrappedValue + prevValue.wallpaper = if let type { + ThemeWallpaper.from(type, prevValue.wallpaper?.background, prevValue.wallpaper?.tint) + } else { + nil + } + pref.wrappedValue = prevValue + } + + static func saveAndApplyThemeOverrides(_ theme: ThemeOverrides, _ pref: CodableDefault<[ThemeOverrides]>? = nil) { + let wallpaper = theme.wallpaper?.importFromString() + let nonSystemThemeName = theme.base.themeName + let pref: CodableDefault<[ThemeOverrides]> = pref ?? themeOverridesDefault + let overrides = pref.get() + var prevValue = overrides.getTheme(nil, wallpaper?.toAppWallpaper().type, theme.base) ?? ThemeOverrides(base: theme.base) + if let imageFile = prevValue.wallpaper?.imageFile { + try? FileManager.default.removeItem(at: getWallpaperFilePath(imageFile)) + } + prevValue.base = theme.base + prevValue.colors = theme.colors + prevValue.wallpaper = wallpaper + pref.set(overrides.replace(prevValue)) + currentThemeDefault.set(nonSystemThemeName) + var currentThemeIds = currentThemeIdsDefault.get() + currentThemeIds[nonSystemThemeName] = prevValue.themeId + currentThemeIdsDefault.set(currentThemeIds) + applyTheme(nonSystemThemeName) + } + + static func resetAllThemeColors(_ pref: CodableDefault<[ThemeOverrides]>? = nil) { + let nonSystemThemeName = nonSystemThemeName() + let pref: CodableDefault<[ThemeOverrides]> = pref ?? themeOverridesDefault + let overrides = pref.get() + guard let themeId = currentThemeIdsDefault.get()[nonSystemThemeName], + var prevValue = overrides.getTheme(themeId) + else { return } + prevValue.colors = ThemeColors() + prevValue.wallpaper?.background = nil + prevValue.wallpaper?.tint = nil + pref.set(overrides.replace(prevValue)) + applyTheme(currentThemeDefault.get()) + } + + static func resetAllThemeColors(_ pref: Binding) { + var prevValue = pref.wrappedValue + prevValue.colors = ThemeColors() + prevValue.wallpaper?.background = nil + prevValue.wallpaper?.tint = nil + pref.wrappedValue = prevValue + } + + static func removeTheme(_ themeId: String?) { + var themes = themeOverridesDefault.get().map { $0 } + themes.removeAll(where: { $0.themeId == themeId }) + themeOverridesDefault.set(themes) + } +} diff --git a/apps/ios/Shared/Views/Call/AudioDevicePicker.swift b/apps/ios/Shared/Views/Call/AudioDevicePicker.swift index 3d846c7b68..be41741ab5 100644 --- a/apps/ios/Shared/Views/Call/AudioDevicePicker.swift +++ b/apps/ios/Shared/Views/Call/AudioDevicePicker.swift @@ -2,7 +2,7 @@ // MPVolumeView.swift // SimpleX (iOS) // -// Created by Stanislav on 24.04.2024. +// Created by Avently on 24.04.2024. // Copyright © 2024 SimpleX Chat. All rights reserved. // diff --git a/apps/ios/Shared/Views/Call/CallController.swift b/apps/ios/Shared/Views/Call/CallController.swift index 64b565e8e6..a8a91057fa 100644 --- a/apps/ios/Shared/Views/Call/CallController.swift +++ b/apps/ios/Shared/Views/Call/CallController.swift @@ -186,7 +186,7 @@ class CallController: NSObject, CXProviderDelegate, PKPushRegistryDelegate, Obse logger.debug("CallController: started chat") self.shouldSuspendChat = true // There are no invitations in the model, as it was processed by NSE - _ = try? justRefreshCallInvitations() + try? justRefreshCallInvitations() logger.debug("CallController: updated call invitations chat") // logger.debug("CallController justRefreshCallInvitations: \(String(describing: m.callInvitations))") // Extract the call information from the push notification payload diff --git a/apps/ios/Shared/Views/Call/IncomingCallView.swift b/apps/ios/Shared/Views/Call/IncomingCallView.swift index 4647995b28..4960281d72 100644 --- a/apps/ios/Shared/Views/Call/IncomingCallView.swift +++ b/apps/ios/Shared/Views/Call/IncomingCallView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct IncomingCallView: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @ObservedObject var cc = CallController.shared var body: some View { @@ -43,7 +44,7 @@ struct IncomingCallView: View { cc.endCall(invitation: invitation) } - callButton("Ignore", "multiply", .accentColor) { + callButton("Ignore", "multiply", .primary) { cc.activeCallInvitation = nil } @@ -63,7 +64,7 @@ struct IncomingCallView: View { .padding(.horizontal, 16) .padding(.vertical, 12) .frame(maxWidth: .infinity) - .background(Color(uiColor: .tertiarySystemGroupedBackground)) + .modifier(ThemedBackground()) .onAppear { dismissAllSheets() } } @@ -76,7 +77,7 @@ struct IncomingCallView: View { .frame(width: 24, height: 24) Text(text) .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } .frame(minWidth: 44) }) diff --git a/apps/ios/Shared/Views/Call/WebRTCClient.swift b/apps/ios/Shared/Views/Call/WebRTCClient.swift index 0b4917c103..79e0dea16c 100644 --- a/apps/ios/Shared/Views/Call/WebRTCClient.swift +++ b/apps/ios/Shared/Views/Call/WebRTCClient.swift @@ -411,6 +411,15 @@ final class WebRTCClient: NSObject, RTCVideoViewDelegate, RTCFrameEncryptorDeleg } func endCall() { + if #available(iOS 16.0, *) { + _endCall() + } else { + // Fixes `connection.close()` getting locked up in iOS15 + DispatchQueue.global(qos: .utility).async { self._endCall() } + } + } + + private func _endCall() { guard let call = activeCall.wrappedValue else { return } logger.debug("WebRTCClient: ending the call") activeCall.wrappedValue = nil diff --git a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift index 140b609902..8c9112a858 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift @@ -9,10 +9,9 @@ import SwiftUI import SimpleXChat -let chatImageColorLight = Color(red: 0.9, green: 0.9, blue: 0.9) -let chatImageColorDark = Color(red: 0.2, green: 0.2, blue: 0.2) struct ChatInfoToolbar: View { @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat var imageSize: CGFloat = 32 @@ -26,29 +25,29 @@ struct ChatInfoToolbar: View { ChatInfoImage( chat: chat, size: imageSize, - color: colorScheme == .dark - ? chatImageColorDark - : chatImageColorLight + color: Color(uiColor: .tertiaryLabel) ) .padding(.trailing, 4) - VStack { - let t = Text(cInfo.displayName).font(.headline) - (cInfo.contact?.verified == true ? contactVerifiedShield + t : t) - .lineLimit(1) - if cInfo.fullName != "" && cInfo.displayName != cInfo.fullName { - Text(cInfo.fullName).font(.subheadline) - .lineLimit(1) + let t = Text(cInfo.displayName).font(.headline) + (cInfo.contact?.verified == true ? contactVerifiedShield + t : t) + .lineLimit(1) + .if (cInfo.fullName != "" && cInfo.displayName != cInfo.fullName) { v in + VStack(spacing: 0) { + v + Text(cInfo.fullName).font(.subheadline) + .lineLimit(1) + .padding(.top, -2) + } } - } } - .foregroundColor(.primary) + .foregroundColor(theme.colors.onBackground) .frame(width: 220) } private var contactVerifiedShield: Text { (Text(Image(systemName: "checkmark.shield")) + Text(" ")) .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .baselineOffset(1) .kerning(-2) } @@ -57,5 +56,6 @@ struct ChatInfoToolbar: View { struct ChatInfoToolbar_Previews: PreviewProvider { static var previews: some View { ChatInfoToolbar(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: [])) + .environmentObject(CurrentColors.toAppTheme()) } } diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 55e84f20d3..ea3b04c2ff 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -36,20 +36,20 @@ func localizedInfoRow(_ title: LocalizedStringKey, _ value: LocalizedStringKey) } } -@ViewBuilder func smpServers(_ title: LocalizedStringKey, _ servers: [String]) -> some View { +@ViewBuilder func smpServers(_ title: LocalizedStringKey, _ servers: [String], _ secondaryColor: Color) -> some View { if servers.count > 0 { HStack { Text(title).frame(width: 120, alignment: .leading) Button(serverHost(servers[0])) { UIPasteboard.general.string = servers.joined(separator: ";") } - .foregroundColor(.secondary) + .foregroundColor(secondaryColor) .lineLimit(1) } } } -private func serverHost(_ s: String) -> String { +func serverHost(_ s: String) -> String { if let i = s.range(of: "@")?.lowerBound { return String(s[i...].dropFirst()) } else { @@ -90,27 +90,34 @@ enum SendReceipts: Identifiable, Hashable { struct ChatInfoView: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss: DismissAction + @ObservedObject var networkModel = NetworkModel.shared @ObservedObject var chat: Chat @State var contact: Contact - @Binding var connectionStats: ConnectionStats? - @Binding var customUserProfile: Profile? @State var localAlias: String - @Binding var connectionCode: String? + var onSearch: () -> Void + @State private var connectionStats: ConnectionStats? = nil + @State private var customUserProfile: Profile? = nil + @State private var connectionCode: String? = nil @FocusState private var aliasTextFieldFocused: Bool @State private var alert: ChatInfoViewAlert? = nil - @State private var showDeleteContactActionSheet = false + @State private var actionSheet: SomeActionSheet? = nil + @State private var sheet: SomeSheet? = nil + @State private var showConnectContactViaAddressDialog = false @State private var sendReceipts = SendReceipts.userDefault(true) @State private var sendReceiptsUserDefault = true @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false - + enum ChatInfoViewAlert: Identifiable { case clearChatAlert case networkStatusAlert case switchAddressAlert case abortSwitchAddressAlert case syncConnectionForceAlert - case error(title: LocalizedStringKey, error: LocalizedStringKey = "") + case queueInfo(info: String) + case someAlert(alert: SomeAlert) + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -119,11 +126,13 @@ struct ChatInfoView: View { case .switchAddressAlert: return "switchAddressAlert" case .abortSwitchAddressAlert: return "abortSwitchAddressAlert" case .syncConnectionForceAlert: return "syncConnectionForceAlert" + case let .queueInfo(info): return "queueInfo \(info)" + case let .someAlert(alert): return "chatInfoSomeAlert \(alert.id)" case let .error(title, _): return "error \(title)" } } } - + var body: some View { NavigationView { List { @@ -133,15 +142,32 @@ struct ChatInfoView: View { .onTapGesture { aliasTextFieldFocused = false } - + Group { localAliasTextEdit() } .listRowBackground(Color.clear) .listRowSeparator(.hidden) + .padding(.bottom, 18) + + GeometryReader { g in + HStack(alignment: .center, spacing: 8) { + let buttonWidth = g.size.width / 4 + searchButton(width: buttonWidth) + AudioCallButton(chat: chat, contact: contact, width: buttonWidth) { alert = .someAlert(alert: $0) } + VideoButton(chat: chat, contact: contact, width: buttonWidth) { alert = .someAlert(alert: $0) } + muteButton(width: buttonWidth) + } + } + .padding(.trailing) + .frame(maxWidth: .infinity) + .frame(height: infoViewActionButtonHeight) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 8)) if let customUserProfile = customUserProfile { - Section("Incognito") { + Section(header: Text("Incognito").foregroundColor(theme.colors.secondary)) { HStack { Text("Your random profile") Spacer() @@ -150,27 +176,38 @@ struct ChatInfoView: View { } } } - + Section { - if let code = connectionCode { verifyCodeButton(code) } - contactPreferencesButton() - sendReceiptsOption() - if let connStats = connectionStats, - connStats.ratchetSyncAllowed { - synchronizeConnectionButton() + Group { + if let code = connectionCode { verifyCodeButton(code) } + contactPreferencesButton() + sendReceiptsOption() + if let connStats = connectionStats, + connStats.ratchetSyncAllowed { + synchronizeConnectionButton() + } + // } else if developerTools { + // synchronizeConnectionButtonForce() + // } } -// } else if developerTools { -// synchronizeConnectionButtonForce() -// } + .disabled(!contact.ready || !contact.active) + NavigationLink { + ChatWallpaperEditorSheet(chat: chat) + } label: { + Label("Chat theme", systemImage: "photo") + } + // } else if developerTools { + // synchronizeConnectionButtonForce() + // } } .disabled(!contact.ready || !contact.active) - + if let conn = contact.activeConn { Section { infoRow(Text(String("E2E encryption")), conn.connPQEnabled ? "Quantum resistant" : "Standard") } } - + if let contactLink = contact.contactLink { Section { SimpleXLinkQRCode(uri: contactLink) @@ -181,13 +218,15 @@ struct ChatInfoView: View { } } header: { Text("Address") + .foregroundColor(theme.colors.secondary) } footer: { Text("You can share this address with your contacts to let them connect with **\(contact.displayName)**.") + .foregroundColor(theme.colors.secondary) } } - + if contact.ready && contact.active { - Section("Servers") { + Section(header: Text("Servers").foregroundColor(theme.colors.secondary)) { networkStatusRow() .onTapGesture { alert = .networkStatusAlert @@ -209,24 +248,37 @@ struct ChatInfoView: View { || connStats.ratchetSyncSendProhibited ) } - smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer }) - smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer }) + smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer }, theme.colors.secondary) + smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer }, theme.colors.secondary) } } } - + Section { clearChatButton() deleteContactButton() } - + if developerTools { - Section(header: Text("For console")) { + Section(header: Text("For console").foregroundColor(theme.colors.secondary)) { infoRow("Local name", chat.chatInfo.localDisplayName) infoRow("Database ID", "\(chat.chatInfo.apiId)") + Button ("Debug delivery") { + Task { + do { + let info = queueInfoText(try await apiContactQueueInfo(chat.chatInfo.apiId)) + await MainActor.run { alert = .queueInfo(info: info) } + } catch let e { + logger.error("apiContactQueueInfo error: \(responseError(e))") + let a = getErrorAlert(e, "Error") + await MainActor.run { alert = .error(title: a.title, error: a.message) } + } + } + } } } } + .modifier(ThemedBackground(grouped: true)) .navigationBarHidden(true) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) @@ -235,6 +287,24 @@ struct ChatInfoView: View { sendReceiptsUserDefault = currentUser.sendRcptsContacts } sendReceipts = SendReceipts.fromBool(contact.chatSettings.sendRcpts, userDefault: sendReceiptsUserDefault) + + + Task { + do { + let (stats, profile) = try await apiContactInfo(chat.chatInfo.apiId) + let (ct, code) = try await apiGetContactCode(chat.chatInfo.apiId) + await MainActor.run { + connectionStats = stats + customUserProfile = profile + connectionCode = code + if contact.activeConn?.connectionCode != ct.activeConn?.connectionCode { + chat.chatInfo = .direct(contact: ct) + } + } + } catch let error { + logger.error("apiContactInfo or apiGetContactCode error: \(responseError(error))") + } + } } .alert(item: $alert) { alertItem in switch(alertItem) { @@ -243,41 +313,31 @@ struct ChatInfoView: View { case .switchAddressAlert: return switchAddressAlert(switchContactAddress) case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchContactAddress) case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncContactConnection(force: true) }) + case let .queueInfo(info): return queueInfoAlert(info) + case let .someAlert(a): return a.alert case let .error(title, error): return mkAlert(title: title, message: error) } } - .actionSheet(isPresented: $showDeleteContactActionSheet) { - if contact.ready && contact.active { - return ActionSheet( - title: Text("Delete contact?\nThis cannot be undone!"), - buttons: [ - .destructive(Text("Delete and notify contact")) { deleteContact(notify: true) }, - .destructive(Text("Delete")) { deleteContact(notify: false) }, - .cancel() - ] - ) + .actionSheet(item: $actionSheet) { $0.actionSheet } + .sheet(item: $sheet) { + if #available(iOS 16.0, *) { + $0.content + .presentationDetents([.fraction(0.4)]) } else { - return ActionSheet( - title: Text("Delete contact?\nThis cannot be undone!"), - buttons: [ - .destructive(Text("Delete")) { deleteContact() }, - .cancel() - ] - ) + $0.content } } } - + private func contactInfoHeader() -> some View { - VStack { + VStack(spacing: 8) { let cInfo = chat.chatInfo ChatInfoImage(chat: chat, size: 192, color: Color(uiColor: .tertiarySystemFill)) - .padding(.top, 12) - .padding() + .padding(.vertical, 12) if contact.verified { ( Text(Image(systemName: "checkmark.shield")) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .font(.title2) + Text(" ") + Text(contact.profile.displayName) @@ -302,7 +362,7 @@ struct ChatInfoView: View { } .frame(maxWidth: .infinity, alignment: .center) } - + private func localAliasTextEdit() -> some View { TextField("Set contact name…", text: $localAlias) .disableAutocorrection(true) @@ -317,9 +377,9 @@ struct ChatInfoView: View { setContactAlias() } .multilineTextAlignment(.center) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } - + private func setContactAlias() { Task { do { @@ -334,6 +394,25 @@ struct ChatInfoView: View { } } + private func searchButton(width: CGFloat) -> some View { + InfoViewButton(image: "magnifyingglass", title: "search", width: width) { + dismiss() + onSearch() + } + .disabled(!contact.ready || chat.chatItems.isEmpty) + } + + private func muteButton(width: CGFloat) -> some View { + InfoViewButton( + image: chat.chatInfo.ntfsEnabled ? "speaker.slash.fill" : "speaker.wave.2.fill", + title: chat.chatInfo.ntfsEnabled ? "mute" : "unmute", + width: width + ) { + toggleNotifications(chat, enableNtfs: !chat.chatInfo.ntfsEnabled) + } + .disabled(!contact.ready || !contact.active) + } + private func verifyCodeButton(_ code: String) -> some View { NavigationLink { VerifyCodeView( @@ -355,6 +434,7 @@ struct ChatInfoView: View { ) .navigationBarTitleDisplayMode(.inline) .navigationTitle("Security code") + .modifier(ThemedBackground(grouped: true)) } label: { Label( contact.verified ? "View security code" : "Verify security code", @@ -362,7 +442,7 @@ struct ChatInfoView: View { ) } } - + private func contactPreferencesButton() -> some View { NavigationLink { ContactPreferencesView( @@ -371,12 +451,13 @@ struct ChatInfoView: View { currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences) ) .navigationBarTitle("Contact preferences") + .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) } label: { Label("Contact preferences", systemImage: "switch.2") } } - + private func sendReceiptsOption() -> some View { Picker(selection: $sendReceipts) { ForEach([.yes, .no, .userDefault(sendReceiptsUserDefault)]) { (opt: SendReceipts) in @@ -390,13 +471,13 @@ struct ChatInfoView: View { setSendReceipts() } } - + private func setSendReceipts() { var chatSettings = chat.chatInfo.chatSettings ?? ChatSettings.defaults chatSettings.sendRcpts = sendReceipts.bool() updateChatSettings(chat, chatSettings: chatSettings) } - + private func synchronizeConnectionButton() -> some View { Button { syncContactConnection(force: false) @@ -405,7 +486,7 @@ struct ChatInfoView: View { .foregroundColor(.orange) } } - + private func synchronizeConnectionButtonForce() -> some View { Button { alert = .syncConnectionForceAlert @@ -414,36 +495,43 @@ struct ChatInfoView: View { .foregroundColor(.red) } } - + private func networkStatusRow() -> some View { HStack { Text("Network status") Image(systemName: "info.circle") - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) .font(.system(size: 14)) Spacer() - Text(chatModel.contactNetworkStatus(contact).statusString) - .foregroundColor(.secondary) + Text(networkModel.contactNetworkStatus(contact).statusString) + .foregroundColor(theme.colors.secondary) serverImage() } } - + private func serverImage() -> some View { - let status = chatModel.contactNetworkStatus(contact) + let status = networkModel.contactNetworkStatus(contact) return Image(systemName: status.imageName) - .foregroundColor(status == .connected ? .green : .secondary) + .foregroundColor(status == .connected ? .green : theme.colors.secondary) .font(.system(size: 12)) } - + private func deleteContactButton() -> some View { Button(role: .destructive) { - showDeleteContactActionSheet = true + deleteContactDialog( + chat, + contact, + dismissToChatList: true, + showAlert: { alert = .someAlert(alert: $0) }, + showActionSheet: { actionSheet = $0 }, + showSheetContent: { sheet = $0 } + ) } label: { - Label("Delete contact", systemImage: "trash") + Label("Delete contact", systemImage: "person.badge.minus") .foregroundColor(Color.red) } } - + private func clearChatButton() -> some View { Button() { alert = .clearChatAlert @@ -452,26 +540,7 @@ struct ChatInfoView: View { .foregroundColor(Color.orange) } } - - private func deleteContact(notify: Bool? = nil) { - Task { - do { - try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, notify: notify) - await MainActor.run { - dismiss() - chatModel.chatId = nil - chatModel.removeChat(chat.chatInfo.id) - } - } catch let error { - logger.error("deleteContactAlert apiDeleteChat error: \(responseError(error))") - let a = getErrorAlert(error, "Error deleting contact") - await MainActor.run { - alert = .error(title: a.title, error: a.message) - } - } - } - } - + private func clearChatAlert() -> Alert { Alert( title: Text("Clear conversation?"), @@ -485,14 +554,14 @@ struct ChatInfoView: View { secondaryButton: .cancel() ) } - + private func networkStatusAlert() -> Alert { Alert( title: Text("Network status"), - message: Text(chatModel.contactNetworkStatus(contact).statusExplanation) + message: Text(networkModel.contactNetworkStatus(contact).statusExplanation) ) } - + private func switchContactAddress() { Task { do { @@ -511,7 +580,7 @@ struct ChatInfoView: View { } } } - + private func abortSwitchContactAddress() { Task { do { @@ -529,7 +598,7 @@ struct ChatInfoView: View { } } } - + private func syncContactConnection(force: Bool) { Task { do { @@ -550,6 +619,302 @@ struct ChatInfoView: View { } } +struct AudioCallButton: View { + var chat: Chat + var contact: Contact + var width: CGFloat + var showAlert: (SomeAlert) -> Void + + var body: some View { + CallButton( + chat: chat, + contact: contact, + image: "phone.fill", + title: "call", + mediaType: .audio, + width: width, + showAlert: showAlert + ) + } +} + +struct VideoButton: View { + var chat: Chat + var contact: Contact + var width: CGFloat + var showAlert: (SomeAlert) -> Void + + var body: some View { + CallButton( + chat: chat, + contact: contact, + image: "video.fill", + title: "video", + mediaType: .video, + width: width, + showAlert: showAlert + ) + } +} + +private struct CallButton: View { + var chat: Chat + var contact: Contact + var image: String + var title: LocalizedStringKey + var mediaType: CallMediaType + var width: CGFloat + var showAlert: (SomeAlert) -> Void + + var body: some View { + let canCall = contact.ready && contact.active && chat.chatInfo.featureEnabled(.calls) && ChatModel.shared.activeCall == nil + + InfoViewButton(image: image, title: title, disabledLook: !canCall, width: width) { + if canCall { + if CallController.useCallKit() { + CallController.shared.startCall(contact, mediaType) + } else { + // When CallKit is not used, colorscheme will be changed and it will be visible if not hiding sheets first + dismissAllSheets(animated: true) { + CallController.shared.startCall(contact, mediaType) + } + } + } else if contact.nextSendGrpInv { + showAlert(SomeAlert( + alert: mkAlert( + title: "Can't call contact", + message: "Send message to enable calls." + ), + id: "can't call contact, send message" + )) + } else if !contact.active { + showAlert(SomeAlert( + alert: mkAlert( + title: "Can't call contact", + message: "Contact is deleted." + ), + id: "can't call contact, contact deleted" + )) + } else if !contact.ready { + showAlert(SomeAlert( + alert: mkAlert( + title: "Can't call contact", + message: "Connecting to contact, please wait or check later!" + ), + id: "can't call contact, contact not ready" + )) + } else if !chat.chatInfo.featureEnabled(.calls) { + switch chat.chatInfo.showEnableCallsAlert { + case .userEnable: + showAlert(SomeAlert( + alert: Alert( + title: Text("Allow calls?"), + message: Text("You need to allow your contact to call to be able to call them."), + primaryButton: .default(Text("Allow")) { + allowFeatureToContact(contact, .calls) + }, + secondaryButton: .cancel() + ), + id: "allow calls" + )) + case .askContact: + showAlert(SomeAlert( + alert: mkAlert( + title: "Calls prohibited!", + message: "Please ask your contact to enable calls." + ), + id: "calls prohibited, ask contact" + )) + case .other: + showAlert(SomeAlert( + alert: mkAlert( + title: "Calls prohibited!", + message: "Please check yours and your contact preferences." + ) + , id: "calls prohibited, other" + )) + } + } else { + showAlert(SomeAlert( + alert: mkAlert(title: "Can't call contact"), + id: "can't call contact" + )) + } + } + .disabled(ChatModel.shared.activeCall != nil) + } +} + +let infoViewActionButtonHeight: CGFloat = 60 + +struct InfoViewButton: View { + var image: String + var title: LocalizedStringKey + var disabledLook: Bool = false + var width: CGFloat + var action: () -> Void + + var body: some View { + VStack(spacing: 4) { + Image(systemName: image) + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + Text(title) + .font(.caption) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .foregroundColor(.accentColor) + .background(Color(.secondarySystemGroupedBackground)) + .cornerRadius(10.0) + .frame(width: width, height: infoViewActionButtonHeight) + .disabled(disabledLook) + .onTapGesture(perform: action) + } +} + +struct ChatWallpaperEditorSheet: View { + @Environment(\.dismiss) var dismiss + @EnvironmentObject var theme: AppTheme + @State private var globalThemeUsed: Bool = false + @State var chat: Chat + @State private var themes: ThemeModeOverrides + + init(chat: Chat) { + self.chat = chat + self.themes = if case let ChatInfo.direct(contact) = chat.chatInfo, let uiThemes = contact.uiThemes { + uiThemes + } else if case let ChatInfo.group(groupInfo) = chat.chatInfo, let uiThemes = groupInfo.uiThemes { + uiThemes + } else { + ThemeModeOverrides() + } + } + + var body: some View { + let preferred = themes.preferredMode(!theme.colors.isLight) + let initialTheme = preferred ?? ThemeManager.defaultActiveTheme(ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + ChatWallpaperEditor( + initialTheme: initialTheme, + themeModeOverride: initialTheme, + applyToMode: themes.light == themes.dark ? nil : initialTheme.mode, + globalThemeUsed: $globalThemeUsed, + save: { applyToMode, newTheme in + await save(applyToMode, newTheme, $chat) + } + ) + .navigationTitle("Chat theme") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.inline) + .onAppear { + globalThemeUsed = preferred == nil + } + .onChange(of: theme.base.mode) { _ in + globalThemeUsed = themesFromChat(chat).preferredMode(!theme.colors.isLight) == nil + } + .onChange(of: ChatModel.shared.chatId) { _ in + dismiss() + } + } + + private func themesFromChat(_ chat: Chat) -> ThemeModeOverrides { + if case let ChatInfo.direct(contact) = chat.chatInfo, let uiThemes = contact.uiThemes { + uiThemes + } else if case let ChatInfo.group(groupInfo) = chat.chatInfo, let uiThemes = groupInfo.uiThemes { + uiThemes + } else { + ThemeModeOverrides() + } + } + + private static var updateBackendTask: Task = Task {} + private func save( + _ applyToMode: DefaultThemeMode?, + _ newTheme: ThemeModeOverride?, + _ chat: Binding + ) async { + let unchangedThemes: ThemeModeOverrides = themesFromChat(chat.wrappedValue) + var wallpaperFiles = Set([unchangedThemes.light?.wallpaper?.imageFile, unchangedThemes.dark?.wallpaper?.imageFile]) + var changedThemes: ThemeModeOverrides? = unchangedThemes + let light: ThemeModeOverride? = if let newTheme { + ThemeModeOverride(mode: DefaultThemeMode.light, colors: newTheme.colors, wallpaper: newTheme.wallpaper?.withFilledWallpaperPath()) + } else { + nil + } + let dark: ThemeModeOverride? = if let newTheme { + ThemeModeOverride(mode: DefaultThemeMode.dark, colors: newTheme.colors, wallpaper: newTheme.wallpaper?.withFilledWallpaperPath()) + } else { + nil + } + + if let applyToMode { + switch applyToMode { + case DefaultThemeMode.light: + changedThemes?.light = light + case DefaultThemeMode.dark: + changedThemes?.dark = dark + } + } else { + changedThemes?.light = light + changedThemes?.dark = dark + } + if changedThemes?.light != nil || changedThemes?.dark != nil { + let light = changedThemes?.light + let dark = changedThemes?.dark + let currentMode = CurrentColors.base.mode + // same image file for both modes, copy image to make them as different files + if var light, var dark, let lightWallpaper = light.wallpaper, let darkWallpaper = dark.wallpaper, let lightImageFile = lightWallpaper.imageFile, let darkImageFile = darkWallpaper.imageFile, lightWallpaper.imageFile == darkWallpaper.imageFile { + let imageFile = if currentMode == DefaultThemeMode.light { + darkImageFile + } else { + lightImageFile + } + let filePath = saveWallpaperFile(url: getWallpaperFilePath(imageFile)) + if currentMode == DefaultThemeMode.light { + dark.wallpaper?.imageFile = filePath + changedThemes = ThemeModeOverrides(light: changedThemes?.light, dark: dark) + } else { + light.wallpaper?.imageFile = filePath + changedThemes = ThemeModeOverrides(light: light, dark: changedThemes?.dark) + } + } + } else { + changedThemes = nil + } + wallpaperFiles.remove(changedThemes?.light?.wallpaper?.imageFile) + wallpaperFiles.remove(changedThemes?.dark?.wallpaper?.imageFile) + wallpaperFiles.forEach(removeWallpaperFile) + + let changedThemesConstant = changedThemes + ChatWallpaperEditorSheet.updateBackendTask.cancel() + ChatWallpaperEditorSheet.updateBackendTask = Task { + do { + try await Task.sleep(nanoseconds: 300_000000) + if await apiSetChatUIThemes(chatId: chat.id, themes: changedThemesConstant) { + if case var ChatInfo.direct(contact) = chat.wrappedValue.chatInfo { + contact.uiThemes = changedThemesConstant + await MainActor.run { + ChatModel.shared.updateChatInfo(ChatInfo.direct(contact: contact)) + chat.wrappedValue = Chat.init(chatInfo: ChatInfo.direct(contact: contact)) + themes = themesFromChat(chat.wrappedValue) + } + } else if case var ChatInfo.group(groupInfo) = chat.wrappedValue.chatInfo { + groupInfo.uiThemes = changedThemesConstant + + await MainActor.run { + ChatModel.shared.updateChatInfo(ChatInfo.group(groupInfo: groupInfo)) + chat.wrappedValue = Chat.init(chatInfo: ChatInfo.group(groupInfo: groupInfo)) + themes = themesFromChat(chat.wrappedValue) + } + } + } + } catch { + // canceled task + } + } + } +} + func switchAddressAlert(_ switchAddress: @escaping () -> Void) -> Alert { Alert( title: Text("Change receiving address?"), @@ -577,15 +942,238 @@ func syncConnectionForceAlert(_ syncConnectionForce: @escaping () -> Void) -> Al ) } +func queueInfoText(_ info: (RcvMsgInfo?, QueueInfo)) -> String { + let (rcvMsgInfo, qInfo) = info + var msgInfo: String + if let rcvMsgInfo { msgInfo = encodeJSON(rcvMsgInfo) } else { msgInfo = "none" } + return String.localizedStringWithFormat(NSLocalizedString("server queue info: %@\n\nlast received msg: %@", comment: "queue info"), encodeJSON(qInfo), msgInfo) +} + +func queueInfoAlert(_ info: String) -> Alert { + Alert( + title: Text("Message queue info"), + message: Text(info), + primaryButton: .default(Text("Ok")), + secondaryButton: .default(Text("Copy")) { UIPasteboard.general.string = info } + ) +} + +func deleteContactDialog( + _ chat: Chat, + _ contact: Contact, + dismissToChatList: Bool, + showAlert: @escaping (SomeAlert) -> Void, + showActionSheet: @escaping (SomeActionSheet) -> Void, + showSheetContent: @escaping (SomeSheet) -> Void +) { + if contact.sndReady && contact.active && !contact.chatDeleted { + deleteContactOrConversationDialog(chat, contact, dismissToChatList, showAlert, showActionSheet, showSheetContent) + } else if contact.sndReady && contact.active && contact.chatDeleted { + deleteContactWithoutConversation(chat, contact, dismissToChatList, showAlert, showActionSheet) + } else { // !(contact.sndReady && contact.active) + deleteNotReadyContact(chat, contact, dismissToChatList, showAlert, showActionSheet) + } +} + +private func deleteContactOrConversationDialog( + _ chat: Chat, + _ contact: Contact, + _ dismissToChatList: Bool, + _ showAlert: @escaping (SomeAlert) -> Void, + _ showActionSheet: @escaping (SomeActionSheet) -> Void, + _ showSheetContent: @escaping (SomeSheet) -> Void +) { + showActionSheet(SomeActionSheet( + actionSheet: ActionSheet( + title: Text("Delete contact?"), + buttons: [ + .destructive(Text("Only delete conversation")) { + deleteContactMaybeErrorAlert(chat, contact, chatDeleteMode: .messages, dismissToChatList, showAlert) + }, + .destructive(Text("Delete contact")) { + showSheetContent(SomeSheet( + content: { AnyView( + DeleteActiveContactDialog( + chat: chat, + contact: contact, + dismissToChatList: dismissToChatList, + showAlert: showAlert + ) + ) }, + id: "DeleteActiveContactDialog" + )) + }, + .cancel() + ] + ), + id: "deleteContactOrConversationDialog" + )) +} + +private func deleteContactMaybeErrorAlert( + _ chat: Chat, + _ contact: Contact, + chatDeleteMode: ChatDeleteMode, + _ dismissToChatList: Bool, + _ showAlert: @escaping (SomeAlert) -> Void +) { + Task { + let alert_ = await deleteContactChat(chat, chatDeleteMode: chatDeleteMode) + if let alert = alert_ { + showAlert(SomeAlert(alert: alert, id: "deleteContactMaybeErrorAlert, error")) + } else { + if dismissToChatList { + await MainActor.run { + ChatModel.shared.chatId = nil + } + DispatchQueue.main.async { + dismissAllSheets(animated: true) { + if case .messages = chatDeleteMode, showDeleteConversationNoticeDefault.get() { + AlertManager.shared.showAlert(deleteConversationNotice(contact)) + } else if chatDeleteMode.isEntity, showDeleteContactNoticeDefault.get() { + AlertManager.shared.showAlert(deleteContactNotice(contact)) + } + } + } + } else { + if case .messages = chatDeleteMode, showDeleteConversationNoticeDefault.get() { + showAlert(SomeAlert(alert: deleteConversationNotice(contact), id: "deleteContactMaybeErrorAlert, deleteConversationNotice")) + } else if chatDeleteMode.isEntity, showDeleteContactNoticeDefault.get() { + showAlert(SomeAlert(alert: deleteContactNotice(contact), id: "deleteContactMaybeErrorAlert, deleteContactNotice")) + } + } + } + } +} + +private func deleteConversationNotice(_ contact: Contact) -> Alert { + return Alert( + title: Text("Conversation deleted!"), + message: Text("You can send messages to \(contact.displayName) from Archived contacts."), + primaryButton: .default(Text("Don't show again")) { + showDeleteConversationNoticeDefault.set(false) + }, + secondaryButton: .default(Text("Ok")) + ) +} + +private func deleteContactNotice(_ contact: Contact) -> Alert { + return Alert( + title: Text("Contact deleted!"), + message: Text("You can still view conversation with \(contact.displayName) in the list of chats."), + primaryButton: .default(Text("Don't show again")) { + showDeleteContactNoticeDefault.set(false) + }, + secondaryButton: .default(Text("Ok")) + ) +} + +enum ContactDeleteMode { + case full + case entity + + public func toChatDeleteMode(notify: Bool) -> ChatDeleteMode { + switch self { + case .full: .full(notify: notify) + case .entity: .entity(notify: notify) + } + } +} + +struct DeleteActiveContactDialog: View { + @Environment(\.dismiss) var dismiss + @EnvironmentObject var theme: AppTheme + var chat: Chat + var contact: Contact + var dismissToChatList: Bool + var showAlert: (SomeAlert) -> Void + @State private var keepConversation = false + + var body: some View { + NavigationView { + List { + Section { + Toggle("Keep conversation", isOn: $keepConversation) + + Button(role: .destructive) { + dismiss() + deleteContactMaybeErrorAlert(chat, contact, chatDeleteMode: contactDeleteMode.toChatDeleteMode(notify: false), dismissToChatList, showAlert) + } label: { + Text("Delete without notification") + } + + Button(role: .destructive) { + dismiss() + deleteContactMaybeErrorAlert(chat, contact, chatDeleteMode: contactDeleteMode.toChatDeleteMode(notify: true), dismissToChatList, showAlert) + } label: { + Text("Delete and notify contact") + } + } footer: { + Text("Contact will be deleted - this cannot be undone!") + .foregroundColor(theme.colors.secondary) + } + } + .modifier(ThemedBackground(grouped: true)) + } + } + + var contactDeleteMode: ContactDeleteMode { + keepConversation ? .entity : .full + } +} + +private func deleteContactWithoutConversation( + _ chat: Chat, + _ contact: Contact, + _ dismissToChatList: Bool, + _ showAlert: @escaping (SomeAlert) -> Void, + _ showActionSheet: @escaping (SomeActionSheet) -> Void +) { + showActionSheet(SomeActionSheet( + actionSheet: ActionSheet( + title: Text("Confirm contact deletion?"), + buttons: [ + .destructive(Text("Delete and notify contact")) { + deleteContactMaybeErrorAlert(chat, contact, chatDeleteMode: .full(notify: true), dismissToChatList, showAlert) + }, + .destructive(Text("Delete without notification")) { + deleteContactMaybeErrorAlert(chat, contact, chatDeleteMode: .full(notify: false), dismissToChatList, showAlert) + }, + .cancel() + ] + ), + id: "deleteContactWithoutConversation" + )) +} + +private func deleteNotReadyContact( + _ chat: Chat, + _ contact: Contact, + _ dismissToChatList: Bool, + _ showAlert: @escaping (SomeAlert) -> Void, + _ showActionSheet: @escaping (SomeActionSheet) -> Void +) { + showActionSheet(SomeActionSheet( + actionSheet: ActionSheet( + title: Text("Confirm contact deletion?"), + buttons: [ + .destructive(Text("Confirm")) { + deleteContactMaybeErrorAlert(chat, contact, chatDeleteMode: .full(notify: false), dismissToChatList, showAlert) + }, + .cancel() + ] + ), + id: "deleteNotReadyContact" + )) +} + struct ChatInfoView_Previews: PreviewProvider { static var previews: some View { ChatInfoView( chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), contact: Contact.sampleData, - connectionStats: Binding.constant(nil), - customUserProfile: Binding.constant(nil), localAlias: "", - connectionCode: Binding.constant(nil) + onSearch: {} ) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/AnimatedImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/AnimatedImageView.swift index bcdeb7fd9c..30f5e7a589 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/AnimatedImageView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/AnimatedImageView.swift @@ -9,6 +9,7 @@ import SwiftUI class AnimatedImageView: UIView { var image: UIImage? = nil var imageView: UIImageView? = nil + var cMode: UIView.ContentMode = .scaleAspectFit override init(frame: CGRect) { super.init(frame: frame) @@ -18,11 +19,12 @@ class AnimatedImageView: UIView { fatalError("Not implemented") } - convenience init(image: UIImage) { + convenience init(image: UIImage, contentMode: UIView.ContentMode) { self.init() self.image = image + self.cMode = contentMode imageView = UIImageView(gifImage: image) - imageView!.contentMode = .scaleAspectFit + imageView!.contentMode = contentMode self.addSubview(imageView!) } @@ -35,7 +37,7 @@ class AnimatedImageView: UIView { if let subview = self.subviews.first as? UIImageView { if image.imageData != subview.gifImage?.imageData { imageView = UIImageView(gifImage: image) - imageView!.contentMode = .scaleAspectFit + imageView!.contentMode = contentMode self.addSubview(imageView!) subview.removeFromSuperview() } @@ -47,13 +49,15 @@ class AnimatedImageView: UIView { struct SwiftyGif: UIViewRepresentable { private let image: UIImage + private let contentMode: UIView.ContentMode - init(image: UIImage) { + init(image: UIImage, contentMode: UIView.ContentMode = .scaleAspectFit) { self.image = image + self.contentMode = contentMode } func makeUIView(context: Context) -> AnimatedImageView { - AnimatedImageView(image: image) + AnimatedImageView(image: image, contentMode: contentMode) } func updateUIView(_ imageView: AnimatedImageView, context: Context) { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift index e3913431f5..3b3e1b3899 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CICallItemView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct CICallItemView: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat var chatItem: ChatItem var status: CICallStatus @@ -22,7 +23,7 @@ struct CICallItemView: View { switch status { case .pending: if sent { - Image(systemName: "phone.arrow.up.right").foregroundColor(.secondary) + Image(systemName: "phone.arrow.up.right").foregroundColor(theme.colors.secondary) } else { acceptCallButton() } @@ -35,7 +36,7 @@ struct CICallItemView: View { case .error: missedCallIcon(sent).foregroundColor(.orange) } - CIMetaView(chat: chat, chatItem: chatItem, showStatus: false, showEdited: false) + CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary, showStatus: false, showEdited: false) .padding(.bottom, 8) .padding(.horizontal, 12) } @@ -52,7 +53,7 @@ struct CICallItemView: View { @ViewBuilder private func endedCallIcon(_ sent: Bool) -> some View { HStack { Image(systemName: "phone.down") - Text(durationText(duration)).foregroundColor(.secondary) + Text(durationText(duration)).foregroundColor(theme.colors.secondary) } } @@ -70,7 +71,7 @@ struct CICallItemView: View { Label("Answer call", systemImage: "phone.arrow.down.left") } } else { - Image(systemName: "phone.arrow.down.left").foregroundColor(.secondary) + Image(systemName: "phone.arrow.down.left").foregroundColor(theme.colors.secondary) } } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift index 5c9ea0f6d8..27d8d9c2de 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift @@ -11,7 +11,9 @@ import SimpleXChat struct CIChatFeatureView: View { @EnvironmentObject var m: ChatModel + @ObservedObject var im = ItemsModel.shared @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme var chatItem: ChatItem @Binding var revealed: Bool var feature: Feature @@ -52,8 +54,8 @@ struct CIChatFeatureView: View { var fs: [FeatureInfo] = [] var icons: Set = [] if var i = m.getChatItemIndex(chatItem) { - while i < m.reversedChatItems.count, - let f = featureInfo(m.reversedChatItems[i]) { + while i < im.reversedChatItems.count, + let f = featureInfo(im.reversedChatItems[i]) { if !icons.contains(f.icon) { fs.insert(f, at: 0) icons.insert(f.icon) @@ -66,10 +68,10 @@ struct CIChatFeatureView: View { private func featureInfo(_ ci: ChatItem) -> FeatureInfo? { switch ci.content { - case let .rcvChatFeature(feature, enabled, param): FeatureInfo(feature, enabled.iconColor, param) - case let .sndChatFeature(feature, enabled, param): FeatureInfo(feature, enabled.iconColor, param) - case let .rcvGroupFeature(feature, preference, param, role): FeatureInfo(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor, param) - case let .sndGroupFeature(feature, preference, param, role): FeatureInfo(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor, param) + case let .rcvChatFeature(feature, enabled, param): FeatureInfo(feature, enabled.iconColor(theme.colors.secondary), param) + case let .sndChatFeature(feature, enabled, param): FeatureInfo(feature, enabled.iconColor(theme.colors.secondary), param) + case let .rcvGroupFeature(feature, preference, param, role): FeatureInfo(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary), param) + case let .sndGroupFeature(feature, preference, param, role): FeatureInfo(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary), param) default: nil } } @@ -81,7 +83,7 @@ struct CIChatFeatureView: View { if let param = f.param { HStack { i - chatEventText(Text(param)).lineLimit(1) + chatEventText(Text(param), theme.colors.secondary).lineLimit(1) } } else { i @@ -93,7 +95,7 @@ struct CIChatFeatureView: View { Image(systemName: icon ?? feature.iconFilled) .foregroundColor(iconColor) .scaleEffect(feature.iconScale) - chatEventText(chatItem) + chatEventText(chatItem, theme.colors.secondary) } .padding(.horizontal, 6) .padding(.vertical, 4) @@ -104,6 +106,6 @@ struct CIChatFeatureView: View { struct CIChatFeatureView_Previews: PreviewProvider { static var previews: some View { let enabled = FeatureEnabled(forUser: false, forContact: false) - CIChatFeatureView(chat: Chat.sampleData, chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), revealed: Binding.constant(true), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor) + CIChatFeatureView(chat: Chat.sampleData, chatItem: ChatItem.getChatFeatureSample(.fullDelete, enabled), revealed: Binding.constant(true), feature: ChatFeature.fullDelete, iconColor: enabled.iconColor(.secondary)) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFeaturePreferenceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFeaturePreferenceView.swift index e52a92a3c6..752f599c8d 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIFeaturePreferenceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFeaturePreferenceView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct CIFeaturePreferenceView: View { @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme var chatItem: ChatItem var feature: ChatFeature var allowed: FeatureAllowed @@ -19,7 +20,7 @@ struct CIFeaturePreferenceView: View { var body: some View { HStack(alignment: .center, spacing: 4) { Image(systemName: feature.icon) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .scaleEffect(feature.iconScale) if let ct = chat.chatInfo.contact, allowed != .no && ct.allowsFeature(feature) && !ct.userAllowsFeature(feature) { @@ -40,17 +41,17 @@ struct CIFeaturePreferenceView: View { private func featurePreferenceView(acceptText: LocalizedStringKey? = nil) -> some View { var r = Text(CIContent.preferenceText(feature, allowed, param) + " ") .fontWeight(.light) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) if let acceptText { r = r + Text(acceptText) .fontWeight(.medium) - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) + Text(" ") } r = r + chatItem.timestampText .fontWeight(.light) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) return r.font(.caption) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift index ae9e09b138..fcb330c321 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift @@ -11,42 +11,48 @@ import SimpleXChat struct CIFileView: View { @EnvironmentObject var m: ChatModel - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme let file: CIFile? let edited: Bool + var smallViewSize: CGFloat? var body: some View { - let metaReserve = edited - ? " " - : " " - Button(action: fileAction) { - HStack(alignment: .bottom, spacing: 6) { - fileIndicator() - .padding(.top, 5) - .padding(.bottom, 3) - if let file = file { - let prettyFileSize = ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary) - VStack(alignment: .leading, spacing: 2) { - Text(file.fileName) - .lineLimit(1) - .multilineTextAlignment(.leading) - .foregroundColor(.primary) - Text(prettyFileSize + metaReserve) - .font(.caption) - .lineLimit(1) - .multilineTextAlignment(.leading) - .foregroundColor(.secondary) + if smallViewSize != nil { + fileIndicator() + .onTapGesture(perform: fileAction) + } else { + let metaReserve = edited + ? " " + : " " + Button(action: fileAction) { + HStack(alignment: .bottom, spacing: 6) { + fileIndicator() + .padding(.top, 5) + .padding(.bottom, 3) + if let file = file { + let prettyFileSize = ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary) + VStack(alignment: .leading, spacing: 2) { + Text(file.fileName) + .lineLimit(1) + .multilineTextAlignment(.leading) + .foregroundColor(theme.colors.onBackground) + Text(prettyFileSize + metaReserve) + .font(.caption) + .lineLimit(1) + .multilineTextAlignment(.leading) + .foregroundColor(theme.colors.secondary) + } + } else { + Text(metaReserve) } - } else { - Text(metaReserve) } + .padding(.top, 4) + .padding(.bottom, 6) + .padding(.leading, 10) + .padding(.trailing, 12) } - .padding(.top, 4) - .padding(.bottom, 6) - .padding(.leading, 10) - .padding(.trailing, 12) + .disabled(!itemInteractive) } - .disabled(!itemInteractive) } private var itemInteractive: Bool { @@ -54,15 +60,18 @@ struct CIFileView: View { switch (file.fileStatus) { case .sndStored: return file.fileProtocol == .local case .sndTransfer: return false - case .sndComplete: return false + case .sndComplete: return true case .sndCancelled: return false - case .sndError: return false + case .sndError: return true + case .sndWarning: return true case .rcvInvitation: return true case .rcvAccepted: return true case .rcvTransfer: return false + case .rcvAborted: return true case .rcvComplete: return true case .rcvCancelled: return false - case .rcvError: return false + case .rcvError: return true + case .rcvWarning: return true case .invalid: return false } } @@ -73,10 +82,10 @@ struct CIFileView: View { logger.debug("CIFileView fileAction") if let file = file { switch (file.fileStatus) { - case .rcvInvitation: + case .rcvInvitation, .rcvAborted: if fileSizeValid(file) { Task { - logger.debug("CIFileView fileAction - in .rcvInvitation, in Task") + logger.debug("CIFileView fileAction - in .rcvInvitation, .rcvAborted, in Task") if let user = m.currentUser { await receiveFile(user: user, fileId: file.fileId) } @@ -107,11 +116,40 @@ struct CIFileView: View { if let fileSource = getLoadedFileSource(file) { saveCryptoFile(fileSource) } + case let .rcvError(rcvFileError): + logger.debug("CIFileView fileAction - in .rcvError") + AlertManager.shared.showAlert(Alert( + title: Text("File error"), + message: Text(rcvFileError.errorInfo) + )) + case let .rcvWarning(rcvFileError): + logger.debug("CIFileView fileAction - in .rcvWarning") + AlertManager.shared.showAlert(Alert( + title: Text("Temporary file error"), + message: Text(rcvFileError.errorInfo) + )) case .sndStored: logger.debug("CIFileView fileAction - in .sndStored") if file.fileProtocol == .local, let fileSource = getLoadedFileSource(file) { saveCryptoFile(fileSource) } + case .sndComplete: + logger.debug("CIFileView fileAction - in .sndComplete") + if let fileSource = getLoadedFileSource(file) { + saveCryptoFile(fileSource) + } + case let .sndError(sndFileError): + logger.debug("CIFileView fileAction - in .sndError") + AlertManager.shared.showAlert(Alert( + title: Text("File error"), + message: Text(sndFileError.errorInfo) + )) + case let .sndWarning(sndFileError): + logger.debug("CIFileView fileAction - in .sndWarning") + AlertManager.shared.showAlert(Alert( + title: Text("Temporary file error"), + message: Text(sndFileError.errorInfo) + )) default: break } } @@ -135,9 +173,10 @@ struct CIFileView: View { case .sndComplete: fileIcon("doc.fill", innerIcon: "checkmark", innerIconSize: 10) case .sndCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10) case .sndError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10) + case .sndWarning: fileIcon("doc.fill", innerIcon: "exclamationmark.triangle.fill", innerIconSize: 10) case .rcvInvitation: if fileSizeValid(file) { - fileIcon("arrow.down.doc.fill", color: .accentColor) + fileIcon("arrow.down.doc.fill", color: theme.colors.primary) } else { fileIcon("doc.fill", color: .orange, innerIcon: "exclamationmark", innerIconSize: 12) } @@ -148,9 +187,12 @@ struct CIFileView: View { } else { progressView() } + case .rcvAborted: + fileIcon("doc.fill", color: theme.colors.primary, innerIcon: "exclamationmark.arrow.circlepath", innerIconSize: 12) case .rcvComplete: fileIcon("doc.fill") case .rcvCancelled: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10) case .rcvError: fileIcon("doc.fill", innerIcon: "xmark", innerIconSize: 10) + case .rcvWarning: fileIcon("doc.fill", innerIcon: "exclamationmark.triangle.fill", innerIconSize: 10) case .invalid: fileIcon("doc.fill", innerIcon: "questionmark", innerIconSize: 10) } } else { @@ -159,21 +201,22 @@ struct CIFileView: View { } private func fileIcon(_ icon: String, color: Color = Color(uiColor: .tertiaryLabel), innerIcon: String? = nil, innerIconSize: CGFloat? = nil) -> some View { - ZStack(alignment: .center) { + let size = smallViewSize ?? 30 + return ZStack(alignment: .center) { Image(systemName: icon) .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 30, height: 30) + .frame(width: size, height: size) .foregroundColor(color) if let innerIcon = innerIcon, - let innerIconSize = innerIconSize { + let innerIconSize = innerIconSize, (smallViewSize == nil || file?.showStatusIconInSmallView == true) { Image(systemName: innerIcon) .resizable() .aspectRatio(contentMode: .fit) .frame(maxHeight: 16) .frame(width: innerIconSize, height: innerIconSize) .foregroundColor(.white) - .padding(.top, 12) + .padding(.top, size / 2.5) } } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift index 1c9df5fcbf..ef0fec5dfe 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift @@ -11,7 +11,7 @@ import SimpleXChat struct CIGroupInvitationView: View { @EnvironmentObject var chatModel: ChatModel - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat var chatItem: ChatItem var groupInvitation: CIGroupInvitation @@ -21,6 +21,8 @@ struct CIGroupInvitationView: View { @State private var inProgress = false @State private var progressByTimeout = false + @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false + var body: some View { let action = !chatItem.chatDir.sent && groupInvitation.status == .pending let v = ZStack(alignment: .bottomTrailing) { @@ -40,10 +42,10 @@ struct CIGroupInvitationView: View { .overlay(DetermineWidth()) ( Text(chatIncognito ? "Tap to join incognito" : "Tap to join") - .foregroundColor(inProgress ? .secondary : chatIncognito ? .indigo : .accentColor) + .foregroundColor(inProgress ? theme.colors.secondary : chatIncognito ? .indigo : theme.colors.primary) .font(.callout) + Text(" ") - + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false) + + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy) ) .overlay(DetermineWidth()) } @@ -51,7 +53,7 @@ struct CIGroupInvitationView: View { ( groupInvitationText() + Text(" ") - + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false) + + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy) ) .overlay(DetermineWidth()) } @@ -63,12 +65,11 @@ struct CIGroupInvitationView: View { } } - CIMetaView(chat: chat, chatItem: chatItem, showStatus: false, showEdited: false) + CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary, showStatus: false, showEdited: false) } .padding(.horizontal, 12) .padding(.vertical, 6) - .background(chatItemFrameColor(chatItem, colorScheme)) - .cornerRadius(18) + .background(chatItemFrameColor(chatItem, theme)) .textSelection(.disabled) .onPreferenceChange(DetermineWidth.Key.self) { frameWidth = $0 } .onChange(of: inProgress) { inProgress in @@ -97,7 +98,7 @@ struct CIGroupInvitationView: View { private func groupInfoView(_ action: Bool) -> some View { var color: Color if action && !inProgress { - color = chatIncognito ? .indigo : .accentColor + color = chatIncognito ? .indigo : theme.colors.primary } else { color = Color(uiColor: .tertiaryLabel) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift index 16974147c8..3966d7e258 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift @@ -11,34 +11,41 @@ import SimpleXChat struct CIImageView: View { @EnvironmentObject var m: ChatModel - @Environment(\.colorScheme) var colorScheme let chatItem: ChatItem - let image: String + var preview: UIImage? let maxWidth: CGFloat - @Binding var imgWidth: CGFloat? - @State var scrollProxy: ScrollViewProxy? - @State var metaColor: Color - @State private var showFullScreenImage = false + var imgWidth: CGFloat? + var smallView: Bool = false + @Binding var showFullScreenImage: Bool + @State private var blurred: Bool = UserDefaults.standard.integer(forKey: DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) > 0 var body: some View { let file = chatItem.file VStack(alignment: .center, spacing: 6) { if let uiImage = getLoadedImage(file) { - imageView(uiImage) + Group { if smallView { smallViewImageView(uiImage) } else { imageView(uiImage) } } .fullScreenCover(isPresented: $showFullScreenImage) { - FullScreenMediaView(chatItem: chatItem, image: uiImage, showView: $showFullScreenImage, scrollProxy: scrollProxy) + FullScreenMediaView(chatItem: chatItem, image: uiImage, showView: $showFullScreenImage) + } + .if(!smallView) { view in + view.modifier(PrivacyBlur(blurred: $blurred)) } .onTapGesture { showFullScreenImage = true } .onChange(of: m.activeCallViewIsCollapsed) { _ in showFullScreenImage = false } - } else if let data = Data(base64Encoded: dropImagePrefix(image)), - let uiImage = UIImage(data: data) { - imageView(uiImage) + } else if let preview { + Group { + if smallView { + smallViewImageView(preview) + } else { + imageView(preview).modifier(PrivacyBlur(blurred: $blurred)) + } + } .onTapGesture { if let file = file { switch file.fileStatus { - case .rcvInvitation: + case .rcvInvitation, .rcvAborted: Task { if let user = m.currentUser { await receiveFile(user: user, fileId: file.fileId) @@ -61,17 +68,39 @@ struct CIImageView: View { case .rcvTransfer: () // ? case .rcvComplete: () // ? case .rcvCancelled: () // TODO + case let .rcvError(rcvFileError): + AlertManager.shared.showAlert(Alert( + title: Text("File error"), + message: Text(rcvFileError.errorInfo) + )) + case let .rcvWarning(rcvFileError): + AlertManager.shared.showAlert(Alert( + title: Text("Temporary file error"), + message: Text(rcvFileError.errorInfo) + )) + case let .sndError(sndFileError): + AlertManager.shared.showAlert(Alert( + title: Text("File error"), + message: Text(sndFileError.errorInfo) + )) + case let .sndWarning(sndFileError): + AlertManager.shared.showAlert(Alert( + title: Text("Temporary file error"), + message: Text(sndFileError.errorInfo) + )) default: () } } } } } + .onDisappear { + showFullScreenImage = false + } } private func imageView(_ img: UIImage) -> some View { let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth - DispatchQueue.main.async { imgWidth = w } return ZStack(alignment: .topTrailing) { if img.imageData == nil { Image(uiImage: img) @@ -83,7 +112,26 @@ struct CIImageView: View { .frame(width: w, height: w * img.size.height / img.size.width) .scaledToFit() } - loadingIndicator() + if !blurred || !showDownloadButton(chatItem.file?.fileStatus) { + loadingIndicator() + } + } + } + + private func smallViewImageView(_ img: UIImage) -> some View { + ZStack(alignment: .topTrailing) { + if img.imageData == nil { + Image(uiImage: img) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: maxWidth, height: maxWidth) + } else { + SwiftyGif(image: img, contentMode: .scaleAspectFill) + .frame(width: maxWidth, height: maxWidth) + } + if chatItem.file?.showStatusIconInSmallView == true { + loadingIndicator() + } } } @@ -100,13 +148,16 @@ struct CIImageView: View { case .sndComplete: fileIcon("checkmark", 10, 13) case .sndCancelled: fileIcon("xmark", 10, 13) case .sndError: fileIcon("xmark", 10, 13) + case .sndWarning: fileIcon("exclamationmark.triangle.fill", 10, 13) case .rcvInvitation: fileIcon("arrow.down", 10, 13) case .rcvAccepted: fileIcon("ellipsis", 14, 11) case .rcvTransfer: progressView() + case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11) + case .rcvComplete: EmptyView() case .rcvCancelled: fileIcon("xmark", 10, 13) case .rcvError: fileIcon("xmark", 10, 13) + case .rcvWarning: fileIcon("exclamationmark.triangle.fill", 10, 13) case .invalid: fileIcon("questionmark", 10, 13) - default: EmptyView() } } } @@ -116,7 +167,7 @@ struct CIImageView: View { .resizable() .aspectRatio(contentMode: .fit) .frame(width: size, height: size) - .foregroundColor(metaColor) + .foregroundColor(.white) .padding(padding) } @@ -127,4 +178,12 @@ struct CIImageView: View { .tint(.white) .padding(8) } + + private func showDownloadButton(_ fileStatus: CIFileStatus?) -> Bool { + switch fileStatus { + case .rcvInvitation: true + case .rcvAborted: true + default: false + } + } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift index 0299a5e6f8..18fd682646 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift @@ -9,6 +9,7 @@ import SwiftUI struct CIInvalidJSONView: View { + @EnvironmentObject var theme: AppTheme var json: String @State private var showJSON = false @@ -21,10 +22,9 @@ struct CIInvalidJSONView: View { .padding(.horizontal, 12) .padding(.vertical, 6) .background(Color(uiColor: .tertiarySystemGroupedBackground)) - .cornerRadius(18) .textSelection(.disabled) .onTapGesture { showJSON = true } - .sheet(isPresented: $showJSON) { + .appSheet(isPresented: $showJSON) { invalidJSONView(json) } } @@ -44,6 +44,7 @@ func invalidJSONView(_ json: String) -> some View { } .frame(maxHeight: .infinity) .padding() + .modifier(ThemedBackground()) } struct CIInvalidJSONView_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift index 4c12c7312a..3c864ab172 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift @@ -10,16 +10,17 @@ import SwiftUI import SimpleXChat struct CILinkView: View { - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme let linkPreview: LinkPreview + @State private var blurred: Bool = UserDefaults.standard.integer(forKey: DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) > 0 var body: some View { VStack(alignment: .center, spacing: 6) { - if let data = Data(base64Encoded: dropImagePrefix(linkPreview.image)), - let uiImage = UIImage(data: data) { + if let uiImage = UIImage(base64Encoded: linkPreview.image) { Image(uiImage: uiImage) .resizable() .scaledToFit() + .modifier(PrivacyBlur(blurred: $blurred)) } VStack(alignment: .leading, spacing: 6) { Text(linkPreview.title) @@ -32,7 +33,7 @@ struct CILinkView: View { Text(linkPreview.uri.absoluteString) .font(.caption) .lineLimit(1) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } .padding(.horizontal, 12) .frame(maxWidth: .infinity, alignment: .leading) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift index da82ed4dd2..463695ddb7 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMemberCreatedContactView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct CIMemberCreatedContactView: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme var chatItem: ChatItem var body: some View { @@ -43,12 +44,12 @@ struct CIMemberCreatedContactView: View { r = r + Text(openText) .fontWeight(.medium) - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) + Text(" ") } r = r + chatItem.timestampText .fontWeight(.light) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) return r.font(.caption) } @@ -56,11 +57,11 @@ struct CIMemberCreatedContactView: View { if let member = chatItem.memberDisplayName { return Text(member + " " + chatItem.content.text + " ") .fontWeight(.light) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } else { return Text(chatItem.content.text + " ") .fontWeight(.light) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift index 17b93930fe..66b810cf2f 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift @@ -11,12 +11,15 @@ import SimpleXChat struct CIMetaView: View { @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme var chatItem: ChatItem - var metaColor = Color.secondary + var metaColor: Color var paleMetaColor = Color(UIColor.tertiaryLabel) var showStatus = true var showEdited = true + @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false + var body: some View { if chatItem.isDeletedContent { chatItem.timestampText.font(.caption).foregroundColor(metaColor) @@ -27,24 +30,24 @@ struct CIMetaView: View { switch meta.itemStatus { case let .sndSent(sndProgress): switch sndProgress { - case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited) - case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited) + case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy) + case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy) } case let .sndRcvd(_, sndProgress): switch sndProgress { case .complete: ZStack { - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited) - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited) + ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy) + ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy) } case .partial: ZStack { - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited) - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited) + ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy) + ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy) } } default: - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, showStatus: showStatus, showEdited: showEdited) + ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy) } } } @@ -61,10 +64,12 @@ func ciMetaText( chatTTL: Int?, encrypted: Bool?, color: Color = .clear, + primaryColor: Color = .accentColor, transparent: Bool = false, sent: SentCheckmark? = nil, showStatus: Bool = true, - showEdited: Bool = true + showEdited: Bool = true, + showViaProxy: Bool ) -> Text { var r = Text("") if showEdited, meta.itemEdited { @@ -78,8 +83,11 @@ func ciMetaText( } r = r + Text(" ") } + if showViaProxy, meta.sentViaProxy == true { + r = r + statusIconText("arrow.forward", color.opacity(0.67)).font(.caption2) + } if showStatus { - if let (icon, statusColor) = meta.statusIcon(color) { + if let (icon, statusColor) = meta.statusIcon(color, primaryColor) { let t = Text(Image(systemName: icon)).font(.caption2) let gap = Text(" ").kerning(-1.25) let t1 = t.foregroundColor(transparent ? .clear : statusColor.opacity(0.67)) @@ -106,15 +114,16 @@ private func statusIconText(_ icon: String, _ color: Color) -> Text { } struct CIMetaView_Previews: PreviewProvider { + static let metaColor = Color.secondary static var previews: some View { Group { - CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete))) - CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .partial))) - CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .complete))) - CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .partial))) - CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .badMsgHash, sndProgress: .complete))) - CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), itemEdited: true)) - CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample()) + CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete)), metaColor: metaColor) + CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .partial)), metaColor: metaColor) + CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .complete)), metaColor: metaColor) + CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .ok, sndProgress: .partial)), metaColor: metaColor) + CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndRcvd(msgRcptStatus: .badMsgHash, sndProgress: .complete)), metaColor: metaColor) + CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), itemEdited: true), metaColor: metaColor) + CIMetaView(chat: Chat.sampleData, chatItem: ChatItem.getDeletedContentSample(), metaColor: metaColor) } .previewLayout(.fixed(width: 360, height: 100)) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift index 3ad45d6987..1f2e16448d 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift @@ -13,18 +13,21 @@ let decryptErrorReason: LocalizedStringKey = "It can happen when you or your con struct CIRcvDecryptionError: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat var msgDecryptError: MsgDecryptError var msgCount: UInt32 var chatItem: ChatItem @State private var alert: CIRcvDecryptionErrorAlert? + @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false + enum CIRcvDecryptionErrorAlert: Identifiable { case syncAllowedAlert(_ syncConnection: () -> Void) case syncNotSupportedContactAlert case syncNotSupportedMemberAlert case decryptionErrorAlert - case error(title: LocalizedStringKey, error: LocalizedStringKey) + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -59,7 +62,7 @@ struct CIRcvDecryptionError: View { case .syncNotSupportedContactAlert: return Alert(title: Text("Fix not supported by contact"), message: message()) case .syncNotSupportedMemberAlert: return Alert(title: Text("Fix not supported by group member"), message: message()) case .decryptionErrorAlert: return Alert(title: Text("Decryption error"), message: message()) - case let .error(title, error): return Alert(title: Text(title), message: Text(error)) + case let .error(title, error): return mkAlert(title: title, message: error) } } } @@ -112,24 +115,23 @@ struct CIRcvDecryptionError: View { } ( Text(Image(systemName: "exclamationmark.arrow.triangle.2.circlepath")) - .foregroundColor(syncSupported ? .accentColor : .secondary) + .foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary) .font(.callout) + Text(" ") + Text("Fix connection") - .foregroundColor(syncSupported ? .accentColor : .secondary) + .foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary) .font(.callout) + Text(" ") - + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true) + + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy) ) } .padding(.horizontal, 12) - CIMetaView(chat: chat, chatItem: chatItem) + CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary) .padding(.horizontal, 12) } .onTapGesture(perform: { onClick() }) .padding(.vertical, 6) .background(Color(uiColor: .tertiarySystemGroupedBackground)) - .cornerRadius(18) .textSelection(.disabled) } @@ -140,16 +142,15 @@ struct CIRcvDecryptionError: View { .foregroundColor(.red) .italic() + Text(" ") - + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true) + + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy) } .padding(.horizontal, 12) - CIMetaView(chat: chat, chatItem: chatItem) + CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary) .padding(.horizontal, 12) } .onTapGesture(perform: { onClick() }) .padding(.vertical, 6) .background(Color(uiColor: .tertiarySystemGroupedBackground)) - .cornerRadius(18) .textSelection(.disabled) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift index b4b190a43a..4670fc685f 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift @@ -13,63 +13,53 @@ import Combine struct CIVideoView: View { @EnvironmentObject var m: ChatModel - @Environment(\.colorScheme) var colorScheme private let chatItem: ChatItem - private let image: String + private let preview: UIImage? @State private var duration: Int @State private var progress: Int = 0 @State private var videoPlaying: Bool = false private let maxWidth: CGFloat - @Binding private var videoWidth: CGFloat? - @State private var scrollProxy: ScrollViewProxy? - @State private var preview: UIImage? = nil + private var videoWidth: CGFloat? + private let smallView: Bool @State private var player: AVPlayer? @State private var fullPlayer: AVPlayer? @State private var url: URL? @State private var urlDecrypted: URL? @State private var decryptionInProgress: Bool = false - @State private var showFullScreenPlayer = false + @Binding private var showFullScreenPlayer: Bool @State private var timeObserver: Any? = nil @State private var fullScreenTimeObserver: Any? = nil @State private var publisher: AnyCancellable? = nil + private var sizeMultiplier: CGFloat { smallView ? 0.38 : 1 } + @State private var blurred: Bool = UserDefaults.standard.integer(forKey: DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) > 0 - init(chatItem: ChatItem, image: String, duration: Int, maxWidth: CGFloat, videoWidth: Binding, scrollProxy: ScrollViewProxy?) { + init(chatItem: ChatItem, preview: UIImage?, duration: Int, maxWidth: CGFloat, videoWidth: CGFloat?, smallView: Bool = false, showFullscreenPlayer: Binding) { self.chatItem = chatItem - self.image = image + self.preview = preview self._duration = State(initialValue: duration) self.maxWidth = maxWidth - self._videoWidth = videoWidth - self.scrollProxy = scrollProxy - if let url = getLoadedVideo(chatItem.file) { - let decrypted = chatItem.file?.fileSource?.cryptoArgs == nil ? url : chatItem.file?.fileSource?.decryptedGet() - self._urlDecrypted = State(initialValue: decrypted) - if let decrypted = decrypted { - self._player = State(initialValue: VideoPlayerView.getOrCreatePlayer(decrypted, false)) - self._fullPlayer = State(initialValue: AVPlayer(url: decrypted)) - } - self._url = State(initialValue: url) - } - if let data = Data(base64Encoded: dropImagePrefix(image)), - let uiImage = UIImage(data: data) { - self._preview = State(initialValue: uiImage) - } + self.videoWidth = videoWidth + self.smallView = smallView + self._showFullScreenPlayer = showFullscreenPlayer } var body: some View { let file = chatItem.file - ZStack { + ZStack(alignment: smallView ? .topLeading : .center) { ZStack(alignment: .topLeading) { - if let file = file, let preview = preview, let player = player, let decrypted = urlDecrypted { + if let file = file, let preview = preview, let decrypted = urlDecrypted, smallView { + smallVideoView(decrypted, file, preview) + } else if let file = file, let preview = preview, let player = player, let decrypted = urlDecrypted { videoView(player, decrypted, file, preview, duration) + } else if let file = file, let defaultPreview = preview, file.loaded && urlDecrypted == nil, smallView { + smallVideoViewEncrypted(file, defaultPreview) } else if let file = file, let defaultPreview = preview, file.loaded && urlDecrypted == nil { videoViewEncrypted(file, defaultPreview, duration) - } else if let data = Data(base64Encoded: dropImagePrefix(image)), - let uiImage = UIImage(data: data) { - imageView(uiImage) - .onTapGesture { - if let file = file { + } else if let preview, let file { + Group { if smallView { smallViewImageView(preview, file) } else { imageView(preview) } } + .onTapGesture { switch file.fileStatus { - case .rcvInvitation: + case .rcvInvitation, .rcvAborted: receiveFileIfValidSize(file: file, receiveFile: receiveFile) case .rcvAccepted: switch file.fileProtocol { @@ -91,18 +81,69 @@ struct CIVideoView: View { default: () } } + } + if !smallView { + durationProgress() + } + } + if !blurred, let file, showDownloadButton(file.fileStatus) { + if !smallView { + Button { + receiveFileIfValidSize(file: file, receiveFile: receiveFile) + } label: { + playPauseIcon("play.fill") } - } - durationProgress() - } - if let file = file, case .rcvInvitation = file.fileStatus { - Button { - receiveFileIfValidSize(file: file, receiveFile: receiveFile) - } label: { + } else if !file.showStatusIconInSmallView { playPauseIcon("play.fill") + .onTapGesture { + receiveFileIfValidSize(file: file, receiveFile: receiveFile) + } } } } + .fullScreenCover(isPresented: $showFullScreenPlayer) { + if let decrypted = urlDecrypted { + fullScreenPlayer(decrypted) + } + } + .onAppear { + setupPlayer(chatItem.file) + } + .onChange(of: chatItem.file) { file in + // ChatItem can be changed in small view on chat list screen + setupPlayer(file) + } + .onDisappear { + showFullScreenPlayer = false + } + } + + private func setupPlayer(_ file: CIFile?) { + let newUrl = getLoadedVideo(file) + if newUrl == url { + return + } + url = nil + urlDecrypted = nil + player = nil + fullPlayer = nil + if let newUrl { + let decrypted = file?.fileSource?.cryptoArgs == nil ? newUrl : file?.fileSource?.decryptedGet() + urlDecrypted = decrypted + if let decrypted = decrypted { + player = VideoPlayerView.getOrCreatePlayer(decrypted, false) + fullPlayer = AVPlayer(url: decrypted) + } + url = newUrl + } + } + + private func showDownloadButton(_ fileStatus: CIFileStatus?) -> Bool { + switch fileStatus { + case .rcvInvitation: true + case .rcvAborted: true + default: false + } } private func videoViewEncrypted(_ file: CIFile, _ defaultPreview: UIImage, _ duration: Int) -> some View { @@ -110,11 +151,6 @@ struct CIVideoView: View { ZStack(alignment: .center) { let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete || (file.fileStatus == .sndStored && file.fileProtocol == .local) imageView(defaultPreview) - .fullScreenCover(isPresented: $showFullScreenPlayer) { - if let decrypted = urlDecrypted { - fullScreenPlayer(decrypted) - } - } .onTapGesture { decrypt(file: file) { showFullScreenPlayer = urlDecrypted != nil @@ -123,20 +159,22 @@ struct CIVideoView: View { .onChange(of: m.activeCallViewIsCollapsed) { _ in showFullScreenPlayer = false } - if !decryptionInProgress { - Button { - decrypt(file: file) { - if urlDecrypted != nil { - videoPlaying = true - player?.play() + if !blurred { + if !decryptionInProgress { + Button { + decrypt(file: file) { + if urlDecrypted != nil { + videoPlaying = true + player?.play() + } } + } label: { + playPauseIcon(canBePlayed ? "play.fill" : "play.slash") } - } label: { - playPauseIcon(canBePlayed ? "play.fill" : "play.slash") + .disabled(!canBePlayed) + } else { + videoDecryptionProgress() } - .disabled(!canBePlayed) - } else { - videoDecryptionProgress() } } } @@ -144,7 +182,6 @@ struct CIVideoView: View { private func videoView(_ player: AVPlayer, _ url: URL, _ file: CIFile, _ preview: UIImage, _ duration: Int) -> some View { let w = preview.size.width <= preview.size.height ? maxWidth * 0.75 : maxWidth - DispatchQueue.main.async { videoWidth = w } return ZStack(alignment: .topTrailing) { ZStack(alignment: .center) { let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete || (file.fileStatus == .sndStored && file.fileProtocol == .local) @@ -156,9 +193,7 @@ struct CIVideoView: View { videoPlaying = false } } - .fullScreenCover(isPresented: $showFullScreenPlayer) { - fullScreenPlayer(url) - } + .modifier(PrivacyBlur(enabled: !videoPlaying, blurred: $blurred)) .onTapGesture { switch player.timeControlStatus { case .playing: @@ -174,7 +209,7 @@ struct CIVideoView: View { .onChange(of: m.activeCallViewIsCollapsed) { _ in showFullScreenPlayer = false } - if !videoPlaying { + if !videoPlaying && !blurred { Button { m.stopPreviousRecPlay = url player.play() @@ -184,7 +219,7 @@ struct CIVideoView: View { .disabled(!canBePlayed) } } - loadingIndicator() + fileStatusIcon() } .onAppear { addObserver(player, url) @@ -196,14 +231,53 @@ struct CIVideoView: View { } } + private func smallVideoViewEncrypted(_ file: CIFile, _ preview: UIImage) -> some View { + return ZStack(alignment: .topLeading) { + let canBePlayed = !chatItem.chatDir.sent || file.fileStatus == CIFileStatus.sndComplete || (file.fileStatus == .sndStored && file.fileProtocol == .local) + smallViewImageView(preview, file) + .onTapGesture { + decrypt(file: file) { + showFullScreenPlayer = urlDecrypted != nil + } + } + .onChange(of: m.activeCallViewIsCollapsed) { _ in + showFullScreenPlayer = false + } + if file.showStatusIconInSmallView { + // Show nothing + } else if !decryptionInProgress { + playPauseIcon(canBePlayed ? "play.fill" : "play.slash") + } else { + videoDecryptionProgress() + } + } + } + + private func smallVideoView(_ url: URL, _ file: CIFile, _ preview: UIImage) -> some View { + return ZStack(alignment: .topLeading) { + smallViewImageView(preview, file) + .onTapGesture { + showFullScreenPlayer = true + } + .onChange(of: m.activeCallViewIsCollapsed) { _ in + showFullScreenPlayer = false + } + + if !file.showStatusIconInSmallView { + playPauseIcon("play.fill") + } + } + } + + private func playPauseIcon(_ image: String, _ color: Color = .white) -> some View { Image(systemName: image) .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 12, height: 12) + .frame(width: smallView ? 12 * sizeMultiplier * 1.6 : 12, height: smallView ? 12 * sizeMultiplier * 1.6 : 12) .foregroundColor(color) - .padding(.leading, 4) - .frame(width: 40, height: 40) + .padding(.leading, smallView ? 0 : 4) + .frame(width: 40 * sizeMultiplier, height: 40 * sizeMultiplier) .background(Color.black.opacity(0.35)) .clipShape(Circle()) } @@ -211,9 +285,9 @@ struct CIVideoView: View { private func videoDecryptionProgress(_ color: Color = .white) -> some View { ProgressView() .progressViewStyle(.circular) - .frame(width: 12, height: 12) + .frame(width: smallView ? 12 * sizeMultiplier : 12, height: smallView ? 12 * sizeMultiplier : 12) .tint(color) - .frame(width: 40, height: 40) + .frame(width: smallView ? 40 * sizeMultiplier * 0.9 : 40, height: smallView ? 40 * sizeMultiplier * 0.9 : 40) .background(Color.black.opacity(0.35)) .clipShape(Circle()) } @@ -244,17 +318,32 @@ struct CIVideoView: View { private func imageView(_ img: UIImage) -> some View { let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth - DispatchQueue.main.async { videoWidth = w } return ZStack(alignment: .topTrailing) { Image(uiImage: img) .resizable() .scaledToFit() .frame(width: w) - loadingIndicator() + .modifier(PrivacyBlur(blurred: $blurred)) + if !blurred || !showDownloadButton(chatItem.file?.fileStatus) { + fileStatusIcon() + } } } - @ViewBuilder private func loadingIndicator() -> some View { + private func smallViewImageView(_ img: UIImage, _ file: CIFile) -> some View { + ZStack(alignment: .center) { + Image(uiImage: img) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: maxWidth, height: maxWidth) + if file.showStatusIconInSmallView { + fileStatusIcon() + .allowsHitTesting(false) + } + } + } + + @ViewBuilder private func fileStatusIcon() -> some View { if let file = chatItem.file { switch file.fileStatus { case .sndStored: @@ -271,7 +360,22 @@ struct CIVideoView: View { } case .sndComplete: fileIcon("checkmark", 10, 13) case .sndCancelled: fileIcon("xmark", 10, 13) - case .sndError: fileIcon("xmark", 10, 13) + case let .sndError(sndFileError): + fileIcon("xmark", 10, 13) + .onTapGesture { + AlertManager.shared.showAlert(Alert( + title: Text("File error"), + message: Text(sndFileError.errorInfo) + )) + } + case let .sndWarning(sndFileError): + fileIcon("exclamationmark.triangle.fill", 10, 13) + .onTapGesture { + AlertManager.shared.showAlert(Alert( + title: Text("Temporary file error"), + message: Text(sndFileError.errorInfo) + )) + } case .rcvInvitation: fileIcon("arrow.down", 10, 13) case .rcvAccepted: fileIcon("ellipsis", 14, 11) case let .rcvTransfer(rcvProgress, rcvTotal): @@ -280,10 +384,26 @@ struct CIVideoView: View { } else { progressView() } + case .rcvAborted: fileIcon("exclamationmark.arrow.circlepath", 14, 11) + case .rcvComplete: EmptyView() case .rcvCancelled: fileIcon("xmark", 10, 13) - case .rcvError: fileIcon("xmark", 10, 13) + case let .rcvError(rcvFileError): + fileIcon("xmark", 10, 13) + .onTapGesture { + AlertManager.shared.showAlert(Alert( + title: Text("File error"), + message: Text(rcvFileError.errorInfo) + )) + } + case let .rcvWarning(rcvFileError): + fileIcon("exclamationmark.triangle.fill", 10, 13) + .onTapGesture { + AlertManager.shared.showAlert(Alert( + title: Text("Temporary file error"), + message: Text(rcvFileError.errorInfo) + )) + } case .invalid: fileIcon("questionmark", 10, 13) - default: EmptyView() } } } @@ -294,7 +414,7 @@ struct CIVideoView: View { .aspectRatio(contentMode: .fit) .frame(width: size, height: size) .foregroundColor(.white) - .padding(padding) + .padding(smallView ? 0 : padding) } private func progressView() -> some View { @@ -302,7 +422,7 @@ struct CIVideoView: View { .progressViewStyle(.circular) .frame(width: 16, height: 16) .tint(.white) - .padding(11) + .padding(smallView ? 0 : 11) } private func progressCircle(_ progress: Int64, _ total: Int64) -> some View { @@ -314,14 +434,14 @@ struct CIVideoView: View { ) .rotationEffect(.degrees(-90)) .frame(width: 16, height: 16) - .padding([.trailing, .top], 11) + .padding([.trailing, .top], smallView ? 0 : 11) } // TODO encrypt: where file size is checked? - private func receiveFileIfValidSize(file: CIFile, receiveFile: @escaping (User, Int64, Bool) async -> Void) { + private func receiveFileIfValidSize(file: CIFile, receiveFile: @escaping (User, Int64, Bool, Bool) async -> Void) { Task { if let user = m.currentUser { - await receiveFile(user, file.fileId, false) + await receiveFile(user, file.fileId, false, false) } } } @@ -354,7 +474,8 @@ struct CIVideoView: View { ) .onAppear { DispatchQueue.main.asyncAfter(deadline: .now()) { - m.stopPreviousRecPlay = url + // Prevent feedback loop - setting `ChatModel`s property causes `onAppear` to be called on iOS17+ + if m.stopPreviousRecPlay != url { m.stopPreviousRecPlay = url } if let player = fullPlayer { player.play() var played = false @@ -391,10 +512,12 @@ struct CIVideoView: View { urlDecrypted = await file.fileSource?.decryptedGetOrCreate(&ChatModel.shared.filesToDelete) await MainActor.run { if let decrypted = urlDecrypted { - player = VideoPlayerView.getOrCreatePlayer(decrypted, false) + if !smallView { + player = VideoPlayerView.getOrCreatePlayer(decrypted, false) + } fullPlayer = AVPlayer(url: decrypted) } - decryptionInProgress = true + decryptionInProgress = false completed?() } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift index 3aecb65ebd..45a20f03bd 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift @@ -11,18 +11,30 @@ import SimpleXChat struct CIVoiceView: View { @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme var chatItem: ChatItem let recordingFile: CIFile? let duration: Int - @Binding var audioPlayer: AudioPlayer? - @Binding var playbackState: VoiceMessagePlaybackState - @Binding var playbackTime: TimeInterval? + @State var audioPlayer: AudioPlayer? = nil + @State var playbackState: VoiceMessagePlaybackState = .noPlayback + @State var playbackTime: TimeInterval? = nil + @Binding var allowMenu: Bool + var smallViewSize: CGFloat? @State private var seek: (TimeInterval) -> Void = { _ in } var body: some View { Group { - if chatItem.chatDir.sent { + if smallViewSize != nil { + HStack(spacing: 10) { + player() + playerTime() + .allowsHitTesting(false) + if .playing == playbackState || (playbackTime ?? 0) > 0 || !allowMenu { + playbackSlider() + } + } + } else if chatItem.chatDir.sent { VStack (alignment: .trailing, spacing: 6) { HStack { if .playing == playbackState || (playbackTime ?? 0) > 0 || !allowMenu { @@ -53,7 +65,13 @@ struct CIVoiceView: View { } private func player() -> some View { - VoiceMessagePlayer( + let sizeMultiplier: CGFloat = if let sz = smallViewSize { + voiceMessageSizeBasedOnSquareSize(sz) / 56 + } else { + 1 + } + return VoiceMessagePlayer( + chat: chat, chatItem: chatItem, recordingFile: recordingFile, recordingTime: TimeInterval(duration), @@ -62,7 +80,8 @@ struct CIVoiceView: View { audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime, - allowMenu: $allowMenu + allowMenu: $allowMenu, + sizeMultiplier: sizeMultiplier ) } @@ -72,7 +91,7 @@ struct CIVoiceView: View { playbackState: $playbackState, playbackTime: $playbackTime ) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } private func playbackSlider() -> some View { @@ -89,10 +108,11 @@ struct CIVoiceView: View { allowMenu = true } } + .tint(theme.colors.primary) } private func metaView() -> some View { - CIMetaView(chat: chat, chatItem: chatItem) + CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary) } } @@ -117,8 +137,9 @@ struct VoiceMessagePlayerTime: View { } struct VoiceMessagePlayer: View { + @ObservedObject var chat: Chat @EnvironmentObject var chatModel: ChatModel - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme var chatItem: ChatItem var recordingFile: CIFile? var recordingTime: TimeInterval @@ -128,23 +149,61 @@ struct VoiceMessagePlayer: View { @Binding var audioPlayer: AudioPlayer? @Binding var playbackState: VoiceMessagePlaybackState @Binding var playbackTime: TimeInterval? + @Binding var allowMenu: Bool + var sizeMultiplier: CGFloat var body: some View { ZStack { if let recordingFile = recordingFile { switch recordingFile.fileStatus { - case .sndStored: playbackButton() - case .sndTransfer: playbackButton() + case .sndStored: + if recordingFile.fileProtocol == .local { + playbackButton() + } else { + loadingIcon() + } + case .sndTransfer: loadingIcon() case .sndComplete: playbackButton() case .sndCancelled: playbackButton() - case .sndError: playbackButton() - case .rcvInvitation: downloadButton(recordingFile) + case let .sndError(sndFileError): + fileStatusIcon("multiply", 14) + .onTapGesture { + AlertManager.shared.showAlert(Alert( + title: Text("File error"), + message: Text(sndFileError.errorInfo) + )) + } + case let .sndWarning(sndFileError): + fileStatusIcon("exclamationmark.triangle.fill", 16) + .onTapGesture { + AlertManager.shared.showAlert(Alert( + title: Text("Temporary file error"), + message: Text(sndFileError.errorInfo) + )) + } + case .rcvInvitation: downloadButton(recordingFile, "play.fill") case .rcvAccepted: loadingIcon() case .rcvTransfer: loadingIcon() + case .rcvAborted: downloadButton(recordingFile, "exclamationmark.arrow.circlepath") case .rcvComplete: playbackButton() case .rcvCancelled: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel)) - case .rcvError: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel)) + case let .rcvError(rcvFileError): + fileStatusIcon("multiply", 14) + .onTapGesture { + AlertManager.shared.showAlert(Alert( + title: Text("File error"), + message: Text(rcvFileError.errorInfo) + )) + } + case let .rcvWarning(rcvFileError): + fileStatusIcon("exclamationmark.triangle.fill", 16) + .onTapGesture { + AlertManager.shared.showAlert(Alert( + title: Text("Temporary file error"), + message: Text(rcvFileError.errorInfo) + )) + } case .invalid: playPauseIcon("play.fill", Color(uiColor: .tertiaryLabel)) } } else { @@ -152,84 +211,170 @@ struct VoiceMessagePlayer: View { } } .onAppear { + if audioPlayer == nil { + let small = sizeMultiplier != 1 + audioPlayer = small ? VoiceItemState.smallView[VoiceItemState.id(chat, chatItem)]?.audioPlayer : VoiceItemState.chatView[VoiceItemState.id(chat, chatItem)]?.audioPlayer + playbackState = (small ? VoiceItemState.smallView[VoiceItemState.id(chat, chatItem)]?.playbackState : VoiceItemState.chatView[VoiceItemState.id(chat, chatItem)]?.playbackState) ?? .noPlayback + playbackTime = small ? VoiceItemState.smallView[VoiceItemState.id(chat, chatItem)]?.playbackTime : VoiceItemState.chatView[VoiceItemState.id(chat, chatItem)]?.playbackTime + } seek = { to in audioPlayer?.seek(to) } - audioPlayer?.onTimer = { playbackTime = $0 } + let audioPath: URL? = if let recordingSource = getLoadedFileSource(recordingFile) { + getAppFilePath(recordingSource.filePath) + } else { + nil + } + let chatId = chatModel.chatId + let userId = chatModel.currentUser?.userId + audioPlayer?.onTimer = { + playbackTime = $0 + notifyStateChange() + // Manual check here is needed because when this view is not visible, SwiftUI don't react on stopPreviousRecPlay, chatId and current user changes and audio keeps playing when it should stop + if (audioPath != nil && chatModel.stopPreviousRecPlay != audioPath) || chatModel.chatId != chatId || chatModel.currentUser?.userId != userId { + stopPlayback() + } + } audioPlayer?.onFinishPlayback = { playbackState = .noPlayback playbackTime = TimeInterval(0) + notifyStateChange() + } + // One voice message was paused, then scrolled far from it, started to play another one, drop to stopped state + if let audioPath, chatModel.stopPreviousRecPlay != audioPath { + stopPlayback() } } .onChange(of: chatModel.stopPreviousRecPlay) { it in if let recordingFileName = getLoadedFileSource(recordingFile)?.filePath, chatModel.stopPreviousRecPlay != getAppFilePath(recordingFileName) { - audioPlayer?.stop() - playbackState = .noPlayback - playbackTime = TimeInterval(0) + stopPlayback() } } .onChange(of: playbackState) { state in allowMenu = state == .paused || state == .noPlayback + // Notify activeContentPreview in ChatPreviewView that playback is finished + if state == .noPlayback, let recordingFileName = getLoadedFileSource(recordingFile)?.filePath, + chatModel.stopPreviousRecPlay == getAppFilePath(recordingFileName) { + chatModel.stopPreviousRecPlay = nil + } + } + .onChange(of: chatModel.chatId) { _ in + stopPlayback() + } + .onDisappear { + if sizeMultiplier == 1 && chatModel.chatId == nil { + stopPlayback() + } } } @ViewBuilder private func playbackButton() -> some View { - switch playbackState { - case .noPlayback: - Button { - if let recordingSource = getLoadedFileSource(recordingFile) { - startPlayback(recordingSource) + if sizeMultiplier != 1 { + switch playbackState { + case .noPlayback: + playPauseIcon("play.fill", theme.colors.primary) + .onTapGesture { + if let recordingSource = getLoadedFileSource(recordingFile) { + startPlayback(recordingSource) + } + } + case .playing: + playPauseIcon("pause.fill", theme.colors.primary) + .onTapGesture { + audioPlayer?.pause() + playbackState = .paused + notifyStateChange() + } + case .paused: + playPauseIcon("play.fill", theme.colors.primary) + .onTapGesture { + audioPlayer?.play() + playbackState = .playing + notifyStateChange() + } + } + } else { + switch playbackState { + case .noPlayback: + Button { + if let recordingSource = getLoadedFileSource(recordingFile) { + startPlayback(recordingSource) + } + } label: { + playPauseIcon("play.fill", theme.colors.primary) + } + case .playing: + Button { + audioPlayer?.pause() + playbackState = .paused + notifyStateChange() + } label: { + playPauseIcon("pause.fill", theme.colors.primary) + } + case .paused: + Button { + audioPlayer?.play() + playbackState = .playing + notifyStateChange() + } label: { + playPauseIcon("play.fill", theme.colors.primary) } - } label: { - playPauseIcon("play.fill") - } - case .playing: - Button { - audioPlayer?.pause() - playbackState = .paused - } label: { - playPauseIcon("pause.fill") - } - case .paused: - Button { - audioPlayer?.play() - playbackState = .playing - } label: { - playPauseIcon("play.fill") } } } - private func playPauseIcon(_ image: String, _ color: Color = .accentColor) -> some View { + private func playPauseIcon(_ image: String, _ color: Color/* = .accentColor*/) -> some View { ZStack { Image(systemName: image) .resizable() .aspectRatio(contentMode: .fit) - .frame(width: 20, height: 20) + .frame(width: 20 * sizeMultiplier, height: 20 * sizeMultiplier) .foregroundColor(color) .padding(.leading, image == "play.fill" ? 4 : 0) - .frame(width: 56, height: 56) - .background(showBackground ? chatItemFrameColor(chatItem, colorScheme) : .clear) + .frame(width: 56 * sizeMultiplier, height: 56 * sizeMultiplier) + .background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear) .clipShape(Circle()) if recordingTime > 0 { ProgressCircle(length: recordingTime, progress: $playbackTime) - .frame(width: 53, height: 53) // this + ProgressCircle lineWidth = background circle diameter + .frame(width: 53 * sizeMultiplier, height: 53 * sizeMultiplier) // this + ProgressCircle lineWidth = background circle diameter } } } - private func downloadButton(_ recordingFile: CIFile) -> some View { - Button { - Task { - if let user = chatModel.currentUser { - await receiveFile(user: user, fileId: recordingFile.fileId) + private func downloadButton(_ recordingFile: CIFile, _ icon: String) -> some View { + Group { + if sizeMultiplier != 1 { + playPauseIcon(icon, theme.colors.primary) + .onTapGesture { + Task { + if let user = chatModel.currentUser { + await receiveFile(user: user, fileId: recordingFile.fileId) + } + } + } + } else { + Button { + Task { + if let user = chatModel.currentUser { + await receiveFile(user: user, fileId: recordingFile.fileId) + } + } + } label: { + playPauseIcon(icon, theme.colors.primary) } } - } label: { - playPauseIcon("play.fill") + } + } + + func notifyStateChange() { + if sizeMultiplier != 1 { + VoiceItemState.smallView[VoiceItemState.id(chat, chatItem)] = VoiceItemState(audioPlayer: audioPlayer, playbackState: playbackState, playbackTime: playbackTime) + } else { + VoiceItemState.chatView[VoiceItemState.id(chat, chatItem)] = VoiceItemState(audioPlayer: audioPlayer, playbackState: playbackState, playbackTime: playbackTime) } } private struct ProgressCircle: View { + @EnvironmentObject var theme: AppTheme var length: TimeInterval @Binding var progress: TimeInterval? @@ -237,7 +382,7 @@ struct VoiceMessagePlayer: View { Circle() .trim(from: 0, to: ((progress ?? TimeInterval(0)) / length)) .stroke( - Color.accentColor, + theme.colors.primary, style: StrokeStyle(lineWidth: 3) ) .rotationEffect(.degrees(-90)) @@ -245,26 +390,100 @@ struct VoiceMessagePlayer: View { } } + private func fileStatusIcon(_ image: String, _ size: CGFloat) -> some View { + Image(systemName: image) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: size * sizeMultiplier, height: size * sizeMultiplier) + .foregroundColor(Color(uiColor: .tertiaryLabel)) + .frame(width: 56 * sizeMultiplier, height: 56 * sizeMultiplier) + .background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear) + .clipShape(Circle()) + } + private func loadingIcon() -> some View { ProgressView() - .frame(width: 30, height: 30) - .frame(width: 56, height: 56) - .background(showBackground ? chatItemFrameColor(chatItem, colorScheme) : .clear) + .frame(width: 30 * sizeMultiplier, height: 30 * sizeMultiplier) + .frame(width: 56 * sizeMultiplier, height: 56 * sizeMultiplier) + .background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear) .clipShape(Circle()) } private func startPlayback(_ recordingSource: CryptoFile) { - chatModel.stopPreviousRecPlay = getAppFilePath(recordingSource.filePath) + let audioPath = getAppFilePath(recordingSource.filePath) + let chatId = chatModel.chatId + let userId = chatModel.currentUser?.userId + chatModel.stopPreviousRecPlay = audioPath audioPlayer = AudioPlayer( - onTimer: { playbackTime = $0 }, + onTimer: { + playbackTime = $0 + notifyStateChange() + // Manual check here is needed because when this view is not visible, SwiftUI don't react on stopPreviousRecPlay, chatId and current user changes and audio keeps playing when it should stop + if chatModel.stopPreviousRecPlay != audioPath || chatModel.chatId != chatId || chatModel.currentUser?.userId != userId { + stopPlayback() + } + }, onFinishPlayback: { playbackState = .noPlayback playbackTime = TimeInterval(0) + notifyStateChange() } ) audioPlayer?.start(fileSource: recordingSource, at: playbackTime) playbackState = .playing + notifyStateChange() } + + private func stopPlayback() { + audioPlayer?.stop() + playbackState = .noPlayback + playbackTime = TimeInterval(0) + notifyStateChange() + } +} + +func voiceMessageSizeBasedOnSquareSize(_ squareSize: CGFloat) -> CGFloat { + let squareToCircleRatio = 0.935 + return squareSize + squareSize * (1 - squareToCircleRatio) +} + +class VoiceItemState { + var audioPlayer: AudioPlayer? + var playbackState: VoiceMessagePlaybackState + var playbackTime: TimeInterval? + + init(audioPlayer: AudioPlayer? = nil, playbackState: VoiceMessagePlaybackState, playbackTime: TimeInterval? = nil) { + self.audioPlayer = audioPlayer + self.playbackState = playbackState + self.playbackTime = playbackTime + } + + static func id(_ chat: Chat, _ chatItem: ChatItem) -> String { + "\(chat.id) \(chatItem.id)" + } + + static func id(_ chatInfo: ChatInfo, _ chatItem: ChatItem) -> String { + "\(chatInfo.id) \(chatItem.id)" + } + + static func stopVoiceInSmallView(_ chatInfo: ChatInfo, _ chatItem: ChatItem) { + let id = id(chatInfo, chatItem) + if let item = smallView[id] { + item.audioPlayer?.stop() + ChatModel.shared.stopPreviousRecPlay = nil + } + } + + static func stopVoiceInChatView(_ chatInfo: ChatInfo, _ chatItem: ChatItem) { + let id = id(chatInfo, chatItem) + if let item = chatView[id] { + item.audioPlayer?.stop() + ChatModel.shared.stopPreviousRecPlay = nil + } + } + + static var smallView: [String: VoiceItemState] = [:] + static var chatView: [String: VoiceItemState] = [:] } struct CIVoiceView_Previews: PreviewProvider { @@ -289,15 +508,12 @@ struct CIVoiceView_Previews: PreviewProvider { chatItem: ChatItem.getVoiceMsgContentSample(), recordingFile: CIFile.getSample(fileName: "voice.m4a", fileSize: 65536, fileStatus: .rcvComplete), duration: 30, - audioPlayer: .constant(nil), - playbackState: .constant(.playing), - playbackTime: .constant(TimeInterval(20)), allowMenu: Binding.constant(true) ) - ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, revealed: Binding.constant(false), allowMenu: .constant(true), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(), revealed: Binding.constant(false), allowMenu: .constant(true), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false), allowMenu: .constant(true), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWtFile, revealed: Binding.constant(false), allowMenu: .constant(true), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + ChatItemView(chat: Chat.sampleData, chatItem: sentVoiceMessage, revealed: Binding.constant(false), allowMenu: .constant(true)) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(), revealed: Binding.constant(false), allowMenu: .constant(true)) + ChatItemView(chat: Chat.sampleData, chatItem: ChatItem.getVoiceMsgContentSample(fileStatus: .rcvTransfer(rcvProgress: 7, rcvTotal: 10)), revealed: Binding.constant(false), allowMenu: .constant(true)) + ChatItemView(chat: Chat.sampleData, chatItem: voiceMessageWtFile, revealed: Binding.constant(false), allowMenu: .constant(true)) } .previewLayout(.fixed(width: 360, height: 360)) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/DeletedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/DeletedItemView.swift index 4763707421..ed2340b6c4 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/DeletedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/DeletedItemView.swift @@ -10,22 +10,21 @@ import SwiftUI import SimpleXChat struct DeletedItemView: View { - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat var chatItem: ChatItem var body: some View { HStack(alignment: .bottom, spacing: 0) { Text(chatItem.content.text) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .italic() - CIMetaView(chat: chat, chatItem: chatItem) + CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary) .padding(.horizontal, 12) } .padding(.leading, 12) .padding(.vertical, 6) - .background(chatItemFrameColor(chatItem, colorScheme)) - .cornerRadius(18) + .background(chatItemFrameColor(chatItem, theme)) .textSelection(.disabled) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift index f57e45fed0..250d9d5636 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/EmojiItemView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct EmojiItemView: View { @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme var chatItem: ChatItem var body: some View { @@ -18,7 +19,7 @@ struct EmojiItemView: View { emojiText(chatItem.content.text) .padding(.top, 8) .padding(.horizontal, 6) - CIMetaView(chat: chat, chatItem: chatItem) + CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary) .padding(.bottom, 8) .padding(.horizontal, 12) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift index af5c917dc8..64a7f29a25 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift @@ -12,21 +12,24 @@ import SwiftUI import SimpleXChat struct FramedCIVoiceView: View { + @EnvironmentObject var theme: AppTheme + @ObservedObject var chat: Chat var chatItem: ChatItem let recordingFile: CIFile? let duration: Int - + + @State var audioPlayer: AudioPlayer? = nil + @State var playbackState: VoiceMessagePlaybackState = .noPlayback + @State var playbackTime: TimeInterval? = nil + @Binding var allowMenu: Bool - - @Binding var audioPlayer: AudioPlayer? - @Binding var playbackState: VoiceMessagePlaybackState - @Binding var playbackTime: TimeInterval? - + @State private var seek: (TimeInterval) -> Void = { _ in } var body: some View { HStack { VoiceMessagePlayer( + chat: chat, chatItem: chatItem, recordingFile: recordingFile, recordingTime: TimeInterval(duration), @@ -35,14 +38,15 @@ struct FramedCIVoiceView: View { audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime, - allowMenu: $allowMenu + allowMenu: $allowMenu, + sizeMultiplier: 1 ) VoiceMessagePlayerTime( recordingTime: TimeInterval(duration), playbackState: $playbackState, playbackTime: $playbackTime ) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .frame(width: 50, alignment: .leading) if .playing == playbackState || (playbackTime ?? 0) > 0 || !allowMenu { playbackSlider() diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index 95c3347f90..313ec0d419 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -9,33 +9,24 @@ import SwiftUI import SimpleXChat -let notesChatColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.21) -let notesChatColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.19) -let sentColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.12) -let sentColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.17) -private let sentQuoteColorLight = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.11) -private let sentQuoteColorDark = Color(.sRGB, red: 0.27, green: 0.72, blue: 1, opacity: 0.09) - struct FramedItemView: View { @EnvironmentObject var m: ChatModel - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme + @EnvironmentObject var scrollModel: ReverseListScrollModel @ObservedObject var chat: Chat var chatItem: ChatItem + var preview: UIImage? @Binding var revealed: Bool var maxWidth: CGFloat = .infinity - @State var scrollProxy: ScrollViewProxy? = nil @State var msgWidth: CGFloat = 0 - @State var imgWidth: CGFloat? = nil - @State var videoWidth: CGFloat? = nil - @State var metaColor = Color.secondary + var imgWidth: CGFloat? = nil + var videoWidth: CGFloat? = nil + @State private var useWhiteMetaColor: Bool = false @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? + @State private var showFullscreenGallery: Bool = false var body: some View { let v = ZStack(alignment: .bottomTrailing) { @@ -58,10 +49,9 @@ struct FramedItemView: View { if let qi = chatItem.quotedItem { ciQuoteView(qi) .onTapGesture { - if let proxy = scrollProxy, - let ci = m.reversedChatItems.first(where: { $0.id == qi.itemId }) { + if let ci = ItemsModel.shared.reversedChatItems.first(where: { $0.id == qi.itemId }) { withAnimation { - proxy.scrollTo(ci.viewId, anchor: .bottom) + scrollModel.scrollToItem(id: ci.id) } } } @@ -73,26 +63,29 @@ struct FramedItemView: View { .padding(chatItem.content.msgContent != nil ? 0 : 4) .overlay(DetermineWidth()) } - .onPreferenceChange(MetaColorPreferenceKey.self) { metaColor = $0 } if chatItem.content.msgContent != nil { - CIMetaView(chat: chat, chatItem: chatItem, metaColor: metaColor) + CIMetaView(chat: chat, chatItem: chatItem, metaColor: useWhiteMetaColor ? Color.white : theme.colors.secondary) .padding(.horizontal, 12) .padding(.bottom, 6) .overlay(DetermineWidth()) .accessibilityLabel("") } } - .background(chatItemFrameColorMaybeImageOrVideo(chatItem, colorScheme)) - .cornerRadius(18) + .background(chatItemFrameColorMaybeImageOrVideo(chatItem, theme)) .onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 } - switch chatItem.meta.itemStatus { - case .sndErrorAuth: - v.onTapGesture { msgDeliveryError("Most likely this contact has deleted the connection with you.") } - case let .sndError(agentError): - v.onTapGesture { msgDeliveryError("Unexpected error: \(agentError)") } - default: v + if let (title, text) = chatItem.meta.itemStatus.statusInfo { + v.onTapGesture { + AlertManager.shared.showAlert( + Alert( + title: Text(title), + message: Text(text) + ) + ) + } + } else { + v } } @@ -109,34 +102,38 @@ struct FramedItemView: View { .padding(.bottom, 2) } else { switch (chatItem.content.msgContent) { - case let .image(text, image): - CIImageView(chatItem: chatItem, image: image, maxWidth: maxWidth, imgWidth: $imgWidth, scrollProxy: scrollProxy, metaColor: metaColor) + case let .image(text, _): + CIImageView(chatItem: chatItem, preview: preview, maxWidth: maxWidth, imgWidth: imgWidth, showFullScreenImage: $showFullscreenGallery) .overlay(DetermineWidth()) if text == "" && !chatItem.meta.isLive { Color.clear .frame(width: 0, height: 0) - .preference( - key: MetaColorPreferenceKey.self, - value: .white - ) + .onAppear { + useWhiteMetaColor = true + } + .onDisappear { + useWhiteMetaColor = false + } } else { ciMsgContentView(chatItem) } - case let .video(text, image, duration): - CIVideoView(chatItem: chatItem, image: image, duration: duration, maxWidth: maxWidth, videoWidth: $videoWidth, scrollProxy: scrollProxy) + case let .video(text, _, duration): + CIVideoView(chatItem: chatItem, preview: preview, duration: duration, maxWidth: maxWidth, videoWidth: videoWidth, showFullscreenPlayer: $showFullscreenGallery) .overlay(DetermineWidth()) if text == "" && !chatItem.meta.isLive { Color.clear .frame(width: 0, height: 0) - .preference( - key: MetaColorPreferenceKey.self, - value: .white - ) + .onAppear { + useWhiteMetaColor = true + } + .onDisappear { + useWhiteMetaColor = false + } } else { ciMsgContentView(chatItem) } case let .voice(text, duration): - FramedCIVoiceView(chatItem: chatItem, recordingFile: chatItem.file, duration: duration, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime) + FramedCIVoiceView(chat: chat, chatItem: chatItem, recordingFile: chatItem.file, duration: duration, allowMenu: $allowMenu) .overlay(DetermineWidth()) if text != "" { ciMsgContentView(chatItem) @@ -157,13 +154,6 @@ struct FramedItemView: View { } } } - - private func msgDeliveryError(_ err: LocalizedStringKey) { - AlertManager.shared.showAlertMsg( - title: "Message delivery error", - message: err - ) - } @ViewBuilder func framedItemHeader(icon: String? = nil, caption: Text, pad: Bool = false) -> some View { let v = HStack(spacing: 6) { @@ -177,13 +167,13 @@ struct FramedItemView: View { .font(.caption) .lineLimit(1) } - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .padding(.horizontal, 12) .padding(.top, 6) .padding(.bottom, pad || (chatItem.quotedItem == nil && chatItem.meta.itemForwarded == nil) ? 6 : 0) .overlay(DetermineWidth()) .frame(minWidth: msgWidth, alignment: .leading) - .background(chatItemFrameContextColor(chatItem, colorScheme)) + .background(chatItemFrameContextColor(chatItem, theme)) if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth { v.frame(maxWidth: mediaWidth, alignment: .leading) } else { @@ -195,8 +185,7 @@ struct FramedItemView: View { let v = ZStack(alignment: .topTrailing) { switch (qi.content) { case let .image(_, image): - if let data = Data(base64Encoded: dropImagePrefix(image)), - let uiImage = UIImage(data: data) { + if let uiImage = UIImage(base64Encoded: image) { ciQuotedMsgView(qi) .padding(.trailing, 70).frame(minWidth: msgWidth, alignment: .leading) Image(uiImage: uiImage) @@ -208,8 +197,7 @@ struct FramedItemView: View { ciQuotedMsgView(qi) } case let .video(_, image, _): - if let data = Data(base64Encoded: dropImagePrefix(image)), - let uiImage = UIImage(data: data) { + if let uiImage = UIImage(base64Encoded: image) { ciQuotedMsgView(qi) .padding(.trailing, 70).frame(minWidth: msgWidth, alignment: .leading) Image(uiImage: uiImage) @@ -235,7 +223,7 @@ struct FramedItemView: View { // if enable this always, size of the framed voice message item will be incorrect after end of playback .overlay { if case .voice = chatItem.content.msgContent {} else { DetermineWidth() } } .frame(minWidth: msgWidth, alignment: .leading) - .background(chatItemFrameContextColor(chatItem, colorScheme)) + .background(chatItemFrameContextColor(chatItem, theme)) if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth { v.frame(maxWidth: mediaWidth, alignment: .leading) @@ -250,7 +238,7 @@ struct FramedItemView: View { VStack(alignment: .leading, spacing: 2) { Text(sender) .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .lineLimit(1) ciQuotedMsgTextView(qi, lines: 2) } @@ -306,7 +294,6 @@ struct FramedItemView: View { .padding(.horizontal, 12) .overlay(DetermineWidth()) .frame(minWidth: 0, alignment: .leading) - .textSelection(.enabled) if let mediaWidth = maxMediaWidth(), mediaWidth < maxWidth { v.frame(maxWidth: mediaWidth, alignment: .leading) @@ -349,13 +336,6 @@ func isRightToLeft(_ s: String) -> Bool { return false } -private struct MetaColorPreferenceKey: PreferenceKey { - static var defaultValue = Color.secondary - static func reduce(value: inout Color, nextValue: () -> Color) { - value = nextValue() - } -} - func onlyImageOrVideo(_ ci: ChatItem) -> Bool { if case let .image(text, _) = ci.content.msgContent { return ci.meta.itemDeleted == nil && !ci.meta.isLive && ci.quotedItem == nil && ci.meta.itemForwarded == nil && text == "" @@ -365,35 +345,35 @@ func onlyImageOrVideo(_ ci: ChatItem) -> Bool { return false } -func chatItemFrameColorMaybeImageOrVideo(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color { +func chatItemFrameColorMaybeImageOrVideo(_ ci: ChatItem, _ theme: AppTheme) -> Color { onlyImageOrVideo(ci) ? Color.clear - : chatItemFrameColor(ci, colorScheme) + : chatItemFrameColor(ci, theme) } -func chatItemFrameColor(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color { +func chatItemFrameColor(_ ci: ChatItem, _ theme: AppTheme) -> Color { ci.chatDir.sent - ? (colorScheme == .light ? sentColorLight : sentColorDark) - : Color(uiColor: .tertiarySystemGroupedBackground) + ? theme.appColors.sentMessage + : theme.appColors.receivedMessage } -func chatItemFrameContextColor(_ ci: ChatItem, _ colorScheme: ColorScheme) -> Color { +func chatItemFrameContextColor(_ ci: ChatItem, _ theme: AppTheme) -> Color { ci.chatDir.sent - ? (colorScheme == .light ? sentQuoteColorLight : sentQuoteColorDark) - : Color(uiColor: .quaternarySystemFill) + ? theme.appColors.sentQuote + : theme.appColors.receivedQuote } struct FramedItemView_Previews: PreviewProvider { static var previews: some View { Group{ - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -"), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line "), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat"), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat"), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello"), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -"), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line "), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat"), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat"), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) } .previewLayout(.fixed(width: 360, height: 200)) } @@ -402,16 +382,16 @@ struct FramedItemView_Previews: PreviewProvider { struct FramedItemView_Edited_Previews: PreviewProvider { static var previews: some View { Group { - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi there hello hello hello ther hello hello", chatDir: .directSnd, image: "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAuKADAAQAAAABAAAAYAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgAYAC4AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQADP/aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKK+CP2vP+ChXwZ/ZPibw7dMfEHi2VAYdGs3G9N33TO/IiU9hgu3ZSOa/NzXNL/4KJ/td6JJ49+NXiq2+Cvw7kG/ZNKbDMLcjKblmfI/57SRqewrwMdxBRo1HQoRdWqt1HaP+KT0j838j7XKOCMXiqEcbjKkcPh5bSne8/wDr3BXlN+is+5+43jb45/Bf4bs0fj/xZpGjSL1jvL2KF/8AvlmDfpXjH/DfH7GQuPsv/CydD35x/wAfIx+fT9a/AO58D/8ABJj4UzvF4v8AFfif4l6mp/evpkfkWzP3w2Isg+omb61X/wCF0/8ABJr/AI9f+FQeJPL6ed9vbzPrj7ZivnavFuIT+KhHyc5Sf3wjY+7w/hlgZQv7PF1P70aUKa+SqTUvwP6afBXx2+CnxIZYvAHi3R9ZkfpHZ3sUz/8AfKsW/SvVq/lItvBf/BJX4rTLF4V8UeJ/hpqTH91JqUfn2yv2y2JcD3MqfUV9OaFon/BRH9krQ4vH3wI8XW3xq+HkY3+XDKb/ABCvJxHuaZMDr5Ergd1ruwvFNVrmq0VOK3lSkp29Y6SS+R5GY+HGGi1DD4qVKo9oYmm6XN5RqK9Nvsro/obor4A/ZC/4KH/Bv9qxV8MLnw54vjU+bo9443SFPvG3k4EoHdcB17rjmvv+vqcHjaGKpKth5qUX1X9aPyZ+b5rlOMy3ESwmOpOFRdH+aezT6NXTCiiiuo84KKKKACiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/Q/v4ooooAKKKKACiiigAr8tf+ChP7cWs/BEWfwD+A8R1P4k+JQkUCQr5rWUc52o+zndNIf9Up4H324wD9x/tDfGjw/wDs9fBnX/i/4jAeHRrZpI4c4M87YWKIe7yFV9gc9q/n6+B3iOb4GfCLxL/wU1+Oypq3jzxndT2nhK2uBwZptyvcBeoQBSq4xthjwPvivluIs0lSthKM+WUk5Sl/JBbtebekfM/R+BOHaeIcszxVL2kISUKdP/n7WlrGL/uxXvT8u6uizc6b8I/+CbmmRePPi9HD8Q/j7rifbktLmTz7bSGm582ZzktITyX++5+5tX5z5L8LPgv+0X/wVH12+8ZfEbxneW/2SRxB9o02eTSosdY4XRlgjYZGV++e5Jr8xvF3i7xN4+8UX/jXxney6jquqTNcXVzMcvJI5ySfQdgBwBgDgV+sP/BPX9jj9oL9oXw9H4tuvG2s+DfAVlM8VsthcyJLdSBsyCBNwREDZ3SEHLcBTgkfmuX4j+0MXHB06LdBXagna/8AenK6u+7el9Ej9+zvA/2Jls81r4uMcY7J1px5lHf93ShaVo9FFJNq8pMyPil/wRs/aj8D6dLq3gq70vxdHECxgtZGtrogf3UmAQn2EmT2r8rPEPh3xB4R1u58M+KrGfTdRsnMdxa3MbRTROOzKwBBr+674VfCnTfhNoI0DTtX1jWFAGZtYvpL2U4934X/AICAK8V/aW/Yf/Z9/areHUvibpkkerWsRhg1KxkMFyqHkBiMrIAeQJFYDJxjJr6bNPD+nOkqmAfLP+WTuvk7XX4/I/PeHvG6tSxDo5zH2lLpUhHll6uN7NelmvPY/iir2T4KftA/GD9njxMvir4Q65caTPkGWFTutrgD+GaE/I4+oyOxB5r2n9tb9jTxj+x18RYvD+pTtqmgaqrS6VqezZ5qpjfHIBwsseRuA4IIYdcD4yr80q0sRgcQ4SvCpB+jT8mvzP6Bw2JwOcYGNany1aFRdVdNdmn22aauno9T9tLO0+D/APwUr02Txd8NI4Ph38ftGT7b5NtIYLXWGh58yJwQVkBGd/8ArEP3i6fMP0R/4J7ftw6/8YZ7z9nb9oGJtN+JPhoPFIJ18p75IPlclegnj/5aKOGHzrxnH8rPhXxT4j8D+JbHxj4QvZdO1TTJkuLW5hba8UqHIIP8x0I4PFfsZ8bPEdx+0N8FvDv/AAUl+CgXSfiJ4EuYLXxZBbDALw4CXO0clMEZznMLlSf3Zr7PJM+nzyxUF+9ir1IrRVILeVtlOO+lrr5n5RxfwbRdKGXVXfDzfLRm9ZUKr+GDlq3RqP3UnfllZfy2/ptorw/9m/43aF+0X8FNA+L+gARpq1uGnhByYLlCUmiP+44IHqMHvXuFfsNGtCrTjVpu8ZJNPyZ/LWKwtXDVp4evG04Nxa7NOzX3hRRRWhzhRRRQBBdf8e0n+6f5Vx1djdf8e0n+6f5Vx1AH/9H+/iiiigAooooAKKKKAPw9/wCCvXiPWviH4q+F/wCyN4XlKT+K9TS6uQvoXFvAT7AvI3/AQe1fnF/wVO+IOnXfxx034AeDj5Xhv4ZaXb6TawKfkE7Ro0rY6bgvlofdT61+h3xNj/4Tv/gtd4Q0W/8Anh8P6THLGp6Ax21xOD/324Nfg3+0T4kufGH7QHjjxRdtukvte1GXJ9PPcKPwAAr8a4pxUpLEz6zq8n/btOK0+cpX9Uf1d4c5bCDy+lbSlh3W/wC38RNq/qoQcV5M8fjiaeRYEOGchR9TxX9svw9+GHijSvgB4I+Gnwr1ceGbGztYY728gijluhbohLLAJVeJZJJCN0jo+0Zwu4gj+JgO8REsf3l+YfUV/bf8DNVm+Mv7KtkNF1CTTZ9Z0d4Ir2D/AFls9zF8sidPmj3hhz1Fel4YyhGtiHpzWjur6e9f9Dw/H9VXQwFvgvUv62hb8Oa3zPoDwfp6aPoiaONXuNaa1Zo3ubp43nLDqrmJEXI/3QfWukmjMsTRBihYEbl6jPcZ7ivxk/4JMf8ABOv9ob9hBvFdr8ZvGOma9Yak22wttLiYGV2kMkl1dzSIkkkzcKisX8tSwDYNfs/X7Bj6NOlXlCjUU4/zJWv8j+ZsNUnOmpThyvtufj/+1Z8Hf2bPi58PviF8Avh/4wl1j4iaBZjXG0m71qfU7i3u4FMqt5VxLL5LzR70Kx7AVfJXAXH8sysGUMOh5r+vzwl+wD+y78KP2wPEX7bGn6xqFv4g8QmWa70+fUFGlrdTRmGS4EGATIY2dRvdlXe+0DPH83Nh+x58bPFev3kljpSaVYPcymGS+kEX7oudp2DL/dx/DX4Z4xZxkmCxGHxdTGRTlG0ueUU7q3S93a7S69Oh/SngTnNSjgcZhMc1CnCSlC70966dr/4U7Lq79T5Kr9MP+CWfxHsNH+P138EPF2JvDfxL0640a9gc/I0vls0Rx6kb4x/v1x3iz9hmHwV4KuPFHiLxlaWkltGzt5sBSAsBkIHL7iT0GFJJ7V8qfAnxLc+D/jd4N8V2bFJdP1vT5wR/szoT+YyK/NeD+Lcvx+Ijisuq88ackpPlklruveSvdX2ufsmavC5zlWKw9CV7xaTs1aSV4tXS1Ukmrdj9/P8Agkfrus/DD4ifFP8AY/8AEkrPJ4Z1F7y1DeiSG3mI9m2wv/wI1+5Ffhd4Ki/4Qf8A4Lb+INM0/wCSHxDpDySqOhL2cMx/8fizX7o1/RnC7ccLPDP/AJdTnBeid1+DP5M8RkqmZUselZ4ijSqv1lG0vvcWwooor6Q+BCiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/S/v4ooooAKKKKACiiigD8LfiNIfBP/BbLwpq9/wDJDr2kJHGTwCZLS4gH/j0eK/Bj9oPw7c+Evj3428M3ilZLHXtRiIPoJ3x+Ywa/fL/grnoWsfDPx98K/wBrzw5EzyeGNSS0uSvokguYQfZtsy/8CFfnB/wVP+HNho/7QFp8bvCeJvDnxK0231mznQfI0vlqsoz6kbJD/v1+M8U4WUViYW1hV5/+3akVr/4FG3qz+r/DnMYTeX1b6VcP7L/t/Dzenq4Tcl5I/M2v6yP+CR3j4eLP2XbLRZZN0uku9sRnp5bMB/45sr+Tev3u/wCCJXj7yNW8T/DyZ+C6XUak9pUw36xD865uAcV7LNFTf24tfd736Hd405d9Y4cddLWlOMvk7wf/AKUvuP6Kq/P/APaa+InjJfF8vge3lez06KONgIyVM+8ZJYjkgHIx045r9AK/Gr/gsB8UPHXwg8N+AvFfgV4oWmv7u3uTJEsiyL5SsiNkZxkMeCDmvU8bsgzPN+Fa+FyrEujUUot6tKcdnBtapO6fny2ejZ/OnAOFWJzqjheVOU+ZK+yaTlfr2t8z85td/b18H6D4n1DQLrw5fSLY3Elv5okRWcxsVJKMAVyR0yTivEPHf7f3jjVFe18BaXb6PGeBPcH7RN9QMBAfqGrFP7UPwj8c3f2/4y/DuzvbxgA93ZNtd8dyGwT+Lmuvh/aP/ZT8IxC58EfD0y3Y5UzwxKAf99mlP5Cv49wvCeBwUoc3D9Sday3qRlTb73c7Wf8Aej8j+rKWVUKLV8vlKf8AiTj/AOlW+9Hw74w8ceNvHl8NX8bajc6jK2SjTsSo/wBxeFUf7orovgf4dufF3xp8H+F7NS0uoa3p8Cgf7c6A/pW98avjx4q+NmoW0mswW9jY2G/7LaWy4WPfjJLHlicD0HoBX13/AMEtPhrZeI/2jH+L3inEPh34cWE+t31w/wBxJFRliBPqPmkH/XOv3fhXCVa/1ahUoRoybV4RacYq/dKK0jq7Ky1s3uezm+PeByeviqkFBxhK0U767RirJattLTqz9H/CMg8af8Futd1DT/ni8P6OySsOxSyiiP8A49Niv3Qr8NP+CS+j6t8V/iv8V/2wdfiZD4i1B7K0LDtLJ9olUf7imFfwr9y6/oLhe88LUxPSrUnNejdl+CP5G8RWqeY0cAnd4ejSpP8AxRjd/c5NBRRRX0h8CFFFFAEF1/x7Sf7p/lXHV2N1/wAe0n+6f5Vx1AH/0/7+KKKKACiiigAooooA8M/aT+B+iftGfBLxB8INcIjGrWxFvORnyLmMh4ZB/uSAE46jI71+AfwU8N3H7SXwL8Qf8E5fjFt0r4kfD65nuvCstycbmhz5ltuPVcE4x1idWHEdf031+UX/AAUL/Yj8T/FG/sv2mP2c5H074keGtkoFufLe+jg5Taennx9Ezw6/Ie2PleI8slUtjKUOZpOM4/zwe6X96L1j5/cfpPAXEMKF8rxNX2cZSU6VR7Uq0dE3/cmvcn5dldn8r/iXw3r/AIN8Q3vhPxXZy6fqemzPb3VtMNskUsZwysPY/n1HFfe3/BL3x/8A8IP+1bptvK+2HVbeSBvdoyso/RWH419SX8fwg/4Kc6QmleIpLfwB8f8ASI/ssiXCGC11kwfLtZSNwkGMbceZH0w6Dj88tM+HvxW/ZK/aO8OQ/FvR7nQ7uw1OElpV/czQs+x2ilGUkUqTypPvivy3DYWWX46hjaT56HOrSXa+ql/LK26fy0P6LzDMYZ3lGMynEx9ni/ZyvTfV2bjKD+3BtJqS9HZn9gnxB/aM+Cvwp8XWXgj4ja/Bo+o6hB9ogW5DrG0ZYoCZNvlr8wI+Zh0r48/4KkfDey+NP7GOqeIPDUsV7L4elh1u0khYOskcOVl2MCQcwu5GDyRXwx/wVBnbVPH3gjxGeVvPDwUt2LxzOW/9Cr87tO8PfFXVdPisbDS9avNImbzLNILa4mtXfo5j2KULZwDjmvqs+4srKvi8rqYfnjays2nqlq9JX3v0P4FwfiDisjzqNanQU3RnGUbNq9rOz0ej207nxZovhrV9enMNhHwpwztwq/U+vt1qrrWlT6JqUumXBDNHj5l6EEZr7U+IHhHxF8JvEUHhL4j2Umiald2sV/Hb3Q8t2hnztbB75BDKfmVgQQCK8e0f4N/E349/FRvBvwh0a41y+YRq/kD91ECPvSyHCRqPVmFfl8aNZ1vYcj59rWd79rbn9T+HPjFnnEPE1WhmmEWEwKw8qkVJNbSppTdSSimmpO1ko2a3aueH+H/D+ueLNds/DHhi0lv9R1CZLe2toV3SSyyHCqoHUk1+yfxl8N3X7Ln7P+h/8E9/hOF1X4nfEm4gufFDWp3FBMR5dqGHRTgLzx5au5wJKtaZZ/B7/gmFpBhsJLbx78fdVi+zwQWyma00UzjbgAfMZDnGMCSToAiElvv/AP4J7fsS+LPh5q15+1H+0q76h8R/Em+ZUuSHksI5/vFj0E8g4YDiNPkH8VfeZJkVTnlhYfxpK02tqUHur7c8trdFfzt9dxdxjQ9lDMKi/wBlpvmpRejxFVfDK26o03713bmla2yv90/sw/ArRv2bvgboHwh0crK2mQZup1GPPu5Tvmk9fmcnGei4HavfKKK/YaFGFGnGlTVoxSSXkj+WMXi6uKr1MTXlec25N923dsKKKK1OcKKKKAILr/j2k/3T/KuOrsbr/j2k/wB0/wAq46gD/9T+/iiiigAooooAKKKKACiiigD87P2wf+Ccnwm/ahmbxvosh8K+NY8NHq1onyzOn3ftEYK7yMcSKVkX1IAFfnT4m8f/ALdv7L+gyfDn9rjwFb/GLwFD8q3ssf2srGOjfaAjspA6GeMMOzV/RTRXz+N4eo1akq+Hm6VR7uNrS/xRekvzPuMo45xOGoQweOpRxFCPwqd1KH/XuorSh8m0uiPwz0L/AIKEf8E3vi6miH4saHd6Xc6B5gs4tWs3vYIPNILAGFpA65UcSLxjgCvtS1/4KT/sLWVlHFZePrCGCJAqRJa3K7VHQBRFxj0xXv8A48/Zc/Zx+J0z3Xj3wPoupzyHLTS2cfnE+8iqH/WvGP8Ah23+w953n/8ACu9PznOPMn2/98+bj9K5oYTOqMpSpyoyb3k4yjJ2015Xqac/BNSbrPD4mlKW6hKlJf8AgUkpP5n5zfta/tof8Ex/jPq+k+IPHelan491HQlljtI7KGWyikWUqSkryNCzJlcgc4JPHNcZ4V+Iv7c37TGgJ8N/2Ovh7bfB7wHN8pvoo/shMZ4LfaSiMxx1MERf/ar9sPAn7LH7N3wxmS68B+BtF02eM5WaOzjMwI9JGBf9a98AAGBWSyDF16kquKrqPN8Xso8rfrN3lY9SXG+WYPDww2W4SdRQ+B4io5xjre6pRtTvfW+up+cv7H//AATg+FX7MdynjzxHMfFnjeTLvqt2vyQO/wB77OjFtpOeZGLSH1AOK/Rqiivo8FgaGEpKjh4KMV/V33fmz4LNs5xuZ4h4rHVXOb6vouyWyS6JJIKKKK6zzAooooAKKKKAILr/AI9pP90/yrjq7G6/49pP90/yrjqAP//Z"), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello there this is a long text", quotedItem: CIQuote.getSample(1, .now, "hi there", chatDir: .directSnd, image: "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAuKADAAQAAAABAAAAYAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgAYAC4AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQADP/aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKK+CP2vP+ChXwZ/ZPibw7dMfEHi2VAYdGs3G9N33TO/IiU9hgu3ZSOa/NzXNL/4KJ/td6JJ49+NXiq2+Cvw7kG/ZNKbDMLcjKblmfI/57SRqewrwMdxBRo1HQoRdWqt1HaP+KT0j838j7XKOCMXiqEcbjKkcPh5bSne8/wDr3BXlN+is+5+43jb45/Bf4bs0fj/xZpGjSL1jvL2KF/8AvlmDfpXjH/DfH7GQuPsv/CydD35x/wAfIx+fT9a/AO58D/8ABJj4UzvF4v8AFfif4l6mp/evpkfkWzP3w2Isg+omb61X/wCF0/8ABJr/AI9f+FQeJPL6ed9vbzPrj7ZivnavFuIT+KhHyc5Sf3wjY+7w/hlgZQv7PF1P70aUKa+SqTUvwP6afBXx2+CnxIZYvAHi3R9ZkfpHZ3sUz/8AfKsW/SvVq/lItvBf/BJX4rTLF4V8UeJ/hpqTH91JqUfn2yv2y2JcD3MqfUV9OaFon/BRH9krQ4vH3wI8XW3xq+HkY3+XDKb/ABCvJxHuaZMDr5Ergd1ruwvFNVrmq0VOK3lSkp29Y6SS+R5GY+HGGi1DD4qVKo9oYmm6XN5RqK9Nvsro/obor4A/ZC/4KH/Bv9qxV8MLnw54vjU+bo9443SFPvG3k4EoHdcB17rjmvv+vqcHjaGKpKth5qUX1X9aPyZ+b5rlOMy3ESwmOpOFRdH+aezT6NXTCiiiuo84KKKKACiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/Q/v4ooooAKKKKACiiigAr8tf+ChP7cWs/BEWfwD+A8R1P4k+JQkUCQr5rWUc52o+zndNIf9Up4H324wD9x/tDfGjw/wDs9fBnX/i/4jAeHRrZpI4c4M87YWKIe7yFV9gc9q/n6+B3iOb4GfCLxL/wU1+Oypq3jzxndT2nhK2uBwZptyvcBeoQBSq4xthjwPvivluIs0lSthKM+WUk5Sl/JBbtebekfM/R+BOHaeIcszxVL2kISUKdP/n7WlrGL/uxXvT8u6uizc6b8I/+CbmmRePPi9HD8Q/j7rifbktLmTz7bSGm582ZzktITyX++5+5tX5z5L8LPgv+0X/wVH12+8ZfEbxneW/2SRxB9o02eTSosdY4XRlgjYZGV++e5Jr8xvF3i7xN4+8UX/jXxney6jquqTNcXVzMcvJI5ySfQdgBwBgDgV+sP/BPX9jj9oL9oXw9H4tuvG2s+DfAVlM8VsthcyJLdSBsyCBNwREDZ3SEHLcBTgkfmuX4j+0MXHB06LdBXagna/8AenK6u+7el9Ej9+zvA/2Jls81r4uMcY7J1px5lHf93ShaVo9FFJNq8pMyPil/wRs/aj8D6dLq3gq70vxdHECxgtZGtrogf3UmAQn2EmT2r8rPEPh3xB4R1u58M+KrGfTdRsnMdxa3MbRTROOzKwBBr+674VfCnTfhNoI0DTtX1jWFAGZtYvpL2U4934X/AICAK8V/aW/Yf/Z9/areHUvibpkkerWsRhg1KxkMFyqHkBiMrIAeQJFYDJxjJr6bNPD+nOkqmAfLP+WTuvk7XX4/I/PeHvG6tSxDo5zH2lLpUhHll6uN7NelmvPY/iir2T4KftA/GD9njxMvir4Q65caTPkGWFTutrgD+GaE/I4+oyOxB5r2n9tb9jTxj+x18RYvD+pTtqmgaqrS6VqezZ5qpjfHIBwsseRuA4IIYdcD4yr80q0sRgcQ4SvCpB+jT8mvzP6Bw2JwOcYGNany1aFRdVdNdmn22aauno9T9tLO0+D/APwUr02Txd8NI4Ph38ftGT7b5NtIYLXWGh58yJwQVkBGd/8ArEP3i6fMP0R/4J7ftw6/8YZ7z9nb9oGJtN+JPhoPFIJ18p75IPlclegnj/5aKOGHzrxnH8rPhXxT4j8D+JbHxj4QvZdO1TTJkuLW5hba8UqHIIP8x0I4PFfsZ8bPEdx+0N8FvDv/AAUl+CgXSfiJ4EuYLXxZBbDALw4CXO0clMEZznMLlSf3Zr7PJM+nzyxUF+9ir1IrRVILeVtlOO+lrr5n5RxfwbRdKGXVXfDzfLRm9ZUKr+GDlq3RqP3UnfllZfy2/ptorw/9m/43aF+0X8FNA+L+gARpq1uGnhByYLlCUmiP+44IHqMHvXuFfsNGtCrTjVpu8ZJNPyZ/LWKwtXDVp4evG04Nxa7NOzX3hRRRWhzhRRRQBBdf8e0n+6f5Vx1djdf8e0n+6f5Vx1AH/9H+/iiiigAooooAKKKKAPw9/wCCvXiPWviH4q+F/wCyN4XlKT+K9TS6uQvoXFvAT7AvI3/AQe1fnF/wVO+IOnXfxx034AeDj5Xhv4ZaXb6TawKfkE7Ro0rY6bgvlofdT61+h3xNj/4Tv/gtd4Q0W/8Anh8P6THLGp6Ax21xOD/324Nfg3+0T4kufGH7QHjjxRdtukvte1GXJ9PPcKPwAAr8a4pxUpLEz6zq8n/btOK0+cpX9Uf1d4c5bCDy+lbSlh3W/wC38RNq/qoQcV5M8fjiaeRYEOGchR9TxX9svw9+GHijSvgB4I+Gnwr1ceGbGztYY728gijluhbohLLAJVeJZJJCN0jo+0Zwu4gj+JgO8REsf3l+YfUV/bf8DNVm+Mv7KtkNF1CTTZ9Z0d4Ir2D/AFls9zF8sidPmj3hhz1Fel4YyhGtiHpzWjur6e9f9Dw/H9VXQwFvgvUv62hb8Oa3zPoDwfp6aPoiaONXuNaa1Zo3ubp43nLDqrmJEXI/3QfWukmjMsTRBihYEbl6jPcZ7ivxk/4JMf8ABOv9ob9hBvFdr8ZvGOma9Yak22wttLiYGV2kMkl1dzSIkkkzcKisX8tSwDYNfs/X7Bj6NOlXlCjUU4/zJWv8j+ZsNUnOmpThyvtufj/+1Z8Hf2bPi58PviF8Avh/4wl1j4iaBZjXG0m71qfU7i3u4FMqt5VxLL5LzR70Kx7AVfJXAXH8sysGUMOh5r+vzwl+wD+y78KP2wPEX7bGn6xqFv4g8QmWa70+fUFGlrdTRmGS4EGATIY2dRvdlXe+0DPH83Nh+x58bPFev3kljpSaVYPcymGS+kEX7oudp2DL/dx/DX4Z4xZxkmCxGHxdTGRTlG0ueUU7q3S93a7S69Oh/SngTnNSjgcZhMc1CnCSlC70966dr/4U7Lq79T5Kr9MP+CWfxHsNH+P138EPF2JvDfxL0640a9gc/I0vls0Rx6kb4x/v1x3iz9hmHwV4KuPFHiLxlaWkltGzt5sBSAsBkIHL7iT0GFJJ7V8qfAnxLc+D/jd4N8V2bFJdP1vT5wR/szoT+YyK/NeD+Lcvx+Ijisuq88ackpPlklruveSvdX2ufsmavC5zlWKw9CV7xaTs1aSV4tXS1Ukmrdj9/P8Agkfrus/DD4ifFP8AY/8AEkrPJ4Z1F7y1DeiSG3mI9m2wv/wI1+5Ffhd4Ki/4Qf8A4Lb+INM0/wCSHxDpDySqOhL2cMx/8fizX7o1/RnC7ccLPDP/AJdTnBeid1+DP5M8RkqmZUselZ4ijSqv1lG0vvcWwooor6Q+BCiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/S/v4ooooAKKKKACiiigD8LfiNIfBP/BbLwpq9/wDJDr2kJHGTwCZLS4gH/j0eK/Bj9oPw7c+Evj3428M3ilZLHXtRiIPoJ3x+Ywa/fL/grnoWsfDPx98K/wBrzw5EzyeGNSS0uSvokguYQfZtsy/8CFfnB/wVP+HNho/7QFp8bvCeJvDnxK0231mznQfI0vlqsoz6kbJD/v1+M8U4WUViYW1hV5/+3akVr/4FG3qz+r/DnMYTeX1b6VcP7L/t/Dzenq4Tcl5I/M2v6yP+CR3j4eLP2XbLRZZN0uku9sRnp5bMB/45sr+Tev3u/wCCJXj7yNW8T/DyZ+C6XUak9pUw36xD865uAcV7LNFTf24tfd736Hd405d9Y4cddLWlOMvk7wf/AKUvuP6Kq/P/APaa+InjJfF8vge3lez06KONgIyVM+8ZJYjkgHIx045r9AK/Gr/gsB8UPHXwg8N+AvFfgV4oWmv7u3uTJEsiyL5SsiNkZxkMeCDmvU8bsgzPN+Fa+FyrEujUUot6tKcdnBtapO6fny2ejZ/OnAOFWJzqjheVOU+ZK+yaTlfr2t8z85td/b18H6D4n1DQLrw5fSLY3Elv5okRWcxsVJKMAVyR0yTivEPHf7f3jjVFe18BaXb6PGeBPcH7RN9QMBAfqGrFP7UPwj8c3f2/4y/DuzvbxgA93ZNtd8dyGwT+Lmuvh/aP/ZT8IxC58EfD0y3Y5UzwxKAf99mlP5Cv49wvCeBwUoc3D9Sday3qRlTb73c7Wf8Aej8j+rKWVUKLV8vlKf8AiTj/AOlW+9Hw74w8ceNvHl8NX8bajc6jK2SjTsSo/wBxeFUf7orovgf4dufF3xp8H+F7NS0uoa3p8Cgf7c6A/pW98avjx4q+NmoW0mswW9jY2G/7LaWy4WPfjJLHlicD0HoBX13/AMEtPhrZeI/2jH+L3inEPh34cWE+t31w/wBxJFRliBPqPmkH/XOv3fhXCVa/1ahUoRoybV4RacYq/dKK0jq7Ky1s3uezm+PeByeviqkFBxhK0U767RirJattLTqz9H/CMg8af8Futd1DT/ni8P6OySsOxSyiiP8A49Niv3Qr8NP+CS+j6t8V/iv8V/2wdfiZD4i1B7K0LDtLJ9olUf7imFfwr9y6/oLhe88LUxPSrUnNejdl+CP5G8RWqeY0cAnd4ejSpP8AxRjd/c5NBRRRX0h8CFFFFAEF1/x7Sf7p/lXHV2N1/wAe0n+6f5Vx1AH/0/7+KKKKACiiigAooooA8M/aT+B+iftGfBLxB8INcIjGrWxFvORnyLmMh4ZB/uSAE46jI71+AfwU8N3H7SXwL8Qf8E5fjFt0r4kfD65nuvCstycbmhz5ltuPVcE4x1idWHEdf031+UX/AAUL/Yj8T/FG/sv2mP2c5H074keGtkoFufLe+jg5Taennx9Ezw6/Ie2PleI8slUtjKUOZpOM4/zwe6X96L1j5/cfpPAXEMKF8rxNX2cZSU6VR7Uq0dE3/cmvcn5dldn8r/iXw3r/AIN8Q3vhPxXZy6fqemzPb3VtMNskUsZwysPY/n1HFfe3/BL3x/8A8IP+1bptvK+2HVbeSBvdoyso/RWH419SX8fwg/4Kc6QmleIpLfwB8f8ASI/ssiXCGC11kwfLtZSNwkGMbceZH0w6Dj88tM+HvxW/ZK/aO8OQ/FvR7nQ7uw1OElpV/czQs+x2ilGUkUqTypPvivy3DYWWX46hjaT56HOrSXa+ql/LK26fy0P6LzDMYZ3lGMynEx9ni/ZyvTfV2bjKD+3BtJqS9HZn9gnxB/aM+Cvwp8XWXgj4ja/Bo+o6hB9ogW5DrG0ZYoCZNvlr8wI+Zh0r48/4KkfDey+NP7GOqeIPDUsV7L4elh1u0khYOskcOVl2MCQcwu5GDyRXwx/wVBnbVPH3gjxGeVvPDwUt2LxzOW/9Cr87tO8PfFXVdPisbDS9avNImbzLNILa4mtXfo5j2KULZwDjmvqs+4srKvi8rqYfnjays2nqlq9JX3v0P4FwfiDisjzqNanQU3RnGUbNq9rOz0ej207nxZovhrV9enMNhHwpwztwq/U+vt1qrrWlT6JqUumXBDNHj5l6EEZr7U+IHhHxF8JvEUHhL4j2Umiald2sV/Hb3Q8t2hnztbB75BDKfmVgQQCK8e0f4N/E349/FRvBvwh0a41y+YRq/kD91ECPvSyHCRqPVmFfl8aNZ1vYcj59rWd79rbn9T+HPjFnnEPE1WhmmEWEwKw8qkVJNbSppTdSSimmpO1ko2a3aueH+H/D+ueLNds/DHhi0lv9R1CZLe2toV3SSyyHCqoHUk1+yfxl8N3X7Ln7P+h/8E9/hOF1X4nfEm4gufFDWp3FBMR5dqGHRTgLzx5au5wJKtaZZ/B7/gmFpBhsJLbx78fdVi+zwQWyma00UzjbgAfMZDnGMCSToAiElvv/AP4J7fsS+LPh5q15+1H+0q76h8R/Em+ZUuSHksI5/vFj0E8g4YDiNPkH8VfeZJkVTnlhYfxpK02tqUHur7c8trdFfzt9dxdxjQ9lDMKi/wBlpvmpRejxFVfDK26o03713bmla2yv90/sw/ArRv2bvgboHwh0crK2mQZup1GPPu5Tvmk9fmcnGei4HavfKKK/YaFGFGnGlTVoxSSXkj+WMXi6uKr1MTXlec25N923dsKKKK1OcKKKKAILr/j2k/3T/KuOrsbr/j2k/wB0/wAq46gD/9T+/iiiigAooooAKKKKACiiigD87P2wf+Ccnwm/ahmbxvosh8K+NY8NHq1onyzOn3ftEYK7yMcSKVkX1IAFfnT4m8f/ALdv7L+gyfDn9rjwFb/GLwFD8q3ssf2srGOjfaAjspA6GeMMOzV/RTRXz+N4eo1akq+Hm6VR7uNrS/xRekvzPuMo45xOGoQweOpRxFCPwqd1KH/XuorSh8m0uiPwz0L/AIKEf8E3vi6miH4saHd6Xc6B5gs4tWs3vYIPNILAGFpA65UcSLxjgCvtS1/4KT/sLWVlHFZePrCGCJAqRJa3K7VHQBRFxj0xXv8A48/Zc/Zx+J0z3Xj3wPoupzyHLTS2cfnE+8iqH/WvGP8Ah23+w953n/8ACu9PznOPMn2/98+bj9K5oYTOqMpSpyoyb3k4yjJ2015Xqac/BNSbrPD4mlKW6hKlJf8AgUkpP5n5zfta/tof8Ex/jPq+k+IPHelan491HQlljtI7KGWyikWUqSkryNCzJlcgc4JPHNcZ4V+Iv7c37TGgJ8N/2Ovh7bfB7wHN8pvoo/shMZ4LfaSiMxx1MERf/ar9sPAn7LH7N3wxmS68B+BtF02eM5WaOzjMwI9JGBf9a98AAGBWSyDF16kquKrqPN8Xso8rfrN3lY9SXG+WYPDww2W4SdRQ+B4io5xjre6pRtTvfW+up+cv7H//AATg+FX7MdynjzxHMfFnjeTLvqt2vyQO/wB77OjFtpOeZGLSH1AOK/Rqiivo8FgaGEpKjh4KMV/V33fmz4LNs5xuZ4h4rHVXOb6vouyWyS6JJIKKKK6zzAooooAKKKKAILr/AI9pP90/yrjq7G6/49pP90/yrjqAP//Z"), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi there hello hello hello ther hello hello", chatDir: .directSnd, image: "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAuKADAAQAAAABAAAAYAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgAYAC4AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQADP/aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKK+CP2vP+ChXwZ/ZPibw7dMfEHi2VAYdGs3G9N33TO/IiU9hgu3ZSOa/NzXNL/4KJ/td6JJ49+NXiq2+Cvw7kG/ZNKbDMLcjKblmfI/57SRqewrwMdxBRo1HQoRdWqt1HaP+KT0j838j7XKOCMXiqEcbjKkcPh5bSne8/wDr3BXlN+is+5+43jb45/Bf4bs0fj/xZpGjSL1jvL2KF/8AvlmDfpXjH/DfH7GQuPsv/CydD35x/wAfIx+fT9a/AO58D/8ABJj4UzvF4v8AFfif4l6mp/evpkfkWzP3w2Isg+omb61X/wCF0/8ABJr/AI9f+FQeJPL6ed9vbzPrj7ZivnavFuIT+KhHyc5Sf3wjY+7w/hlgZQv7PF1P70aUKa+SqTUvwP6afBXx2+CnxIZYvAHi3R9ZkfpHZ3sUz/8AfKsW/SvVq/lItvBf/BJX4rTLF4V8UeJ/hpqTH91JqUfn2yv2y2JcD3MqfUV9OaFon/BRH9krQ4vH3wI8XW3xq+HkY3+XDKb/ABCvJxHuaZMDr5Ergd1ruwvFNVrmq0VOK3lSkp29Y6SS+R5GY+HGGi1DD4qVKo9oYmm6XN5RqK9Nvsro/obor4A/ZC/4KH/Bv9qxV8MLnw54vjU+bo9443SFPvG3k4EoHdcB17rjmvv+vqcHjaGKpKth5qUX1X9aPyZ+b5rlOMy3ESwmOpOFRdH+aezT6NXTCiiiuo84KKKKACiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/Q/v4ooooAKKKKACiiigAr8tf+ChP7cWs/BEWfwD+A8R1P4k+JQkUCQr5rWUc52o+zndNIf9Up4H324wD9x/tDfGjw/wDs9fBnX/i/4jAeHRrZpI4c4M87YWKIe7yFV9gc9q/n6+B3iOb4GfCLxL/wU1+Oypq3jzxndT2nhK2uBwZptyvcBeoQBSq4xthjwPvivluIs0lSthKM+WUk5Sl/JBbtebekfM/R+BOHaeIcszxVL2kISUKdP/n7WlrGL/uxXvT8u6uizc6b8I/+CbmmRePPi9HD8Q/j7rifbktLmTz7bSGm582ZzktITyX++5+5tX5z5L8LPgv+0X/wVH12+8ZfEbxneW/2SRxB9o02eTSosdY4XRlgjYZGV++e5Jr8xvF3i7xN4+8UX/jXxney6jquqTNcXVzMcvJI5ySfQdgBwBgDgV+sP/BPX9jj9oL9oXw9H4tuvG2s+DfAVlM8VsthcyJLdSBsyCBNwREDZ3SEHLcBTgkfmuX4j+0MXHB06LdBXagna/8AenK6u+7el9Ej9+zvA/2Jls81r4uMcY7J1px5lHf93ShaVo9FFJNq8pMyPil/wRs/aj8D6dLq3gq70vxdHECxgtZGtrogf3UmAQn2EmT2r8rPEPh3xB4R1u58M+KrGfTdRsnMdxa3MbRTROOzKwBBr+674VfCnTfhNoI0DTtX1jWFAGZtYvpL2U4934X/AICAK8V/aW/Yf/Z9/areHUvibpkkerWsRhg1KxkMFyqHkBiMrIAeQJFYDJxjJr6bNPD+nOkqmAfLP+WTuvk7XX4/I/PeHvG6tSxDo5zH2lLpUhHll6uN7NelmvPY/iir2T4KftA/GD9njxMvir4Q65caTPkGWFTutrgD+GaE/I4+oyOxB5r2n9tb9jTxj+x18RYvD+pTtqmgaqrS6VqezZ5qpjfHIBwsseRuA4IIYdcD4yr80q0sRgcQ4SvCpB+jT8mvzP6Bw2JwOcYGNany1aFRdVdNdmn22aauno9T9tLO0+D/APwUr02Txd8NI4Ph38ftGT7b5NtIYLXWGh58yJwQVkBGd/8ArEP3i6fMP0R/4J7ftw6/8YZ7z9nb9oGJtN+JPhoPFIJ18p75IPlclegnj/5aKOGHzrxnH8rPhXxT4j8D+JbHxj4QvZdO1TTJkuLW5hba8UqHIIP8x0I4PFfsZ8bPEdx+0N8FvDv/AAUl+CgXSfiJ4EuYLXxZBbDALw4CXO0clMEZznMLlSf3Zr7PJM+nzyxUF+9ir1IrRVILeVtlOO+lrr5n5RxfwbRdKGXVXfDzfLRm9ZUKr+GDlq3RqP3UnfllZfy2/ptorw/9m/43aF+0X8FNA+L+gARpq1uGnhByYLlCUmiP+44IHqMHvXuFfsNGtCrTjVpu8ZJNPyZ/LWKwtXDVp4evG04Nxa7NOzX3hRRRWhzhRRRQBBdf8e0n+6f5Vx1djdf8e0n+6f5Vx1AH/9H+/iiiigAooooAKKKKAPw9/wCCvXiPWviH4q+F/wCyN4XlKT+K9TS6uQvoXFvAT7AvI3/AQe1fnF/wVO+IOnXfxx034AeDj5Xhv4ZaXb6TawKfkE7Ro0rY6bgvlofdT61+h3xNj/4Tv/gtd4Q0W/8Anh8P6THLGp6Ax21xOD/324Nfg3+0T4kufGH7QHjjxRdtukvte1GXJ9PPcKPwAAr8a4pxUpLEz6zq8n/btOK0+cpX9Uf1d4c5bCDy+lbSlh3W/wC38RNq/qoQcV5M8fjiaeRYEOGchR9TxX9svw9+GHijSvgB4I+Gnwr1ceGbGztYY728gijluhbohLLAJVeJZJJCN0jo+0Zwu4gj+JgO8REsf3l+YfUV/bf8DNVm+Mv7KtkNF1CTTZ9Z0d4Ir2D/AFls9zF8sidPmj3hhz1Fel4YyhGtiHpzWjur6e9f9Dw/H9VXQwFvgvUv62hb8Oa3zPoDwfp6aPoiaONXuNaa1Zo3ubp43nLDqrmJEXI/3QfWukmjMsTRBihYEbl6jPcZ7ivxk/4JMf8ABOv9ob9hBvFdr8ZvGOma9Yak22wttLiYGV2kMkl1dzSIkkkzcKisX8tSwDYNfs/X7Bj6NOlXlCjUU4/zJWv8j+ZsNUnOmpThyvtufj/+1Z8Hf2bPi58PviF8Avh/4wl1j4iaBZjXG0m71qfU7i3u4FMqt5VxLL5LzR70Kx7AVfJXAXH8sysGUMOh5r+vzwl+wD+y78KP2wPEX7bGn6xqFv4g8QmWa70+fUFGlrdTRmGS4EGATIY2dRvdlXe+0DPH83Nh+x58bPFev3kljpSaVYPcymGS+kEX7oudp2DL/dx/DX4Z4xZxkmCxGHxdTGRTlG0ueUU7q3S93a7S69Oh/SngTnNSjgcZhMc1CnCSlC70966dr/4U7Lq79T5Kr9MP+CWfxHsNH+P138EPF2JvDfxL0640a9gc/I0vls0Rx6kb4x/v1x3iz9hmHwV4KuPFHiLxlaWkltGzt5sBSAsBkIHL7iT0GFJJ7V8qfAnxLc+D/jd4N8V2bFJdP1vT5wR/szoT+YyK/NeD+Lcvx+Ijisuq88ackpPlklruveSvdX2ufsmavC5zlWKw9CV7xaTs1aSV4tXS1Ukmrdj9/P8Agkfrus/DD4ifFP8AY/8AEkrPJ4Z1F7y1DeiSG3mI9m2wv/wI1+5Ffhd4Ki/4Qf8A4Lb+INM0/wCSHxDpDySqOhL2cMx/8fizX7o1/RnC7ccLPDP/AJdTnBeid1+DP5M8RkqmZUselZ4ijSqv1lG0vvcWwooor6Q+BCiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/S/v4ooooAKKKKACiiigD8LfiNIfBP/BbLwpq9/wDJDr2kJHGTwCZLS4gH/j0eK/Bj9oPw7c+Evj3428M3ilZLHXtRiIPoJ3x+Ywa/fL/grnoWsfDPx98K/wBrzw5EzyeGNSS0uSvokguYQfZtsy/8CFfnB/wVP+HNho/7QFp8bvCeJvDnxK0231mznQfI0vlqsoz6kbJD/v1+M8U4WUViYW1hV5/+3akVr/4FG3qz+r/DnMYTeX1b6VcP7L/t/Dzenq4Tcl5I/M2v6yP+CR3j4eLP2XbLRZZN0uku9sRnp5bMB/45sr+Tev3u/wCCJXj7yNW8T/DyZ+C6XUak9pUw36xD865uAcV7LNFTf24tfd736Hd405d9Y4cddLWlOMvk7wf/AKUvuP6Kq/P/APaa+InjJfF8vge3lez06KONgIyVM+8ZJYjkgHIx045r9AK/Gr/gsB8UPHXwg8N+AvFfgV4oWmv7u3uTJEsiyL5SsiNkZxkMeCDmvU8bsgzPN+Fa+FyrEujUUot6tKcdnBtapO6fny2ejZ/OnAOFWJzqjheVOU+ZK+yaTlfr2t8z85td/b18H6D4n1DQLrw5fSLY3Elv5okRWcxsVJKMAVyR0yTivEPHf7f3jjVFe18BaXb6PGeBPcH7RN9QMBAfqGrFP7UPwj8c3f2/4y/DuzvbxgA93ZNtd8dyGwT+Lmuvh/aP/ZT8IxC58EfD0y3Y5UzwxKAf99mlP5Cv49wvCeBwUoc3D9Sday3qRlTb73c7Wf8Aej8j+rKWVUKLV8vlKf8AiTj/AOlW+9Hw74w8ceNvHl8NX8bajc6jK2SjTsSo/wBxeFUf7orovgf4dufF3xp8H+F7NS0uoa3p8Cgf7c6A/pW98avjx4q+NmoW0mswW9jY2G/7LaWy4WPfjJLHlicD0HoBX13/AMEtPhrZeI/2jH+L3inEPh34cWE+t31w/wBxJFRliBPqPmkH/XOv3fhXCVa/1ahUoRoybV4RacYq/dKK0jq7Ky1s3uezm+PeByeviqkFBxhK0U767RirJattLTqz9H/CMg8af8Futd1DT/ni8P6OySsOxSyiiP8A49Niv3Qr8NP+CS+j6t8V/iv8V/2wdfiZD4i1B7K0LDtLJ9olUf7imFfwr9y6/oLhe88LUxPSrUnNejdl+CP5G8RWqeY0cAnd4ejSpP8AxRjd/c5NBRRRX0h8CFFFFAEF1/x7Sf7p/lXHV2N1/wAe0n+6f5Vx1AH/0/7+KKKKACiiigAooooA8M/aT+B+iftGfBLxB8INcIjGrWxFvORnyLmMh4ZB/uSAE46jI71+AfwU8N3H7SXwL8Qf8E5fjFt0r4kfD65nuvCstycbmhz5ltuPVcE4x1idWHEdf031+UX/AAUL/Yj8T/FG/sv2mP2c5H074keGtkoFufLe+jg5Taennx9Ezw6/Ie2PleI8slUtjKUOZpOM4/zwe6X96L1j5/cfpPAXEMKF8rxNX2cZSU6VR7Uq0dE3/cmvcn5dldn8r/iXw3r/AIN8Q3vhPxXZy6fqemzPb3VtMNskUsZwysPY/n1HFfe3/BL3x/8A8IP+1bptvK+2HVbeSBvdoyso/RWH419SX8fwg/4Kc6QmleIpLfwB8f8ASI/ssiXCGC11kwfLtZSNwkGMbceZH0w6Dj88tM+HvxW/ZK/aO8OQ/FvR7nQ7uw1OElpV/czQs+x2ilGUkUqTypPvivy3DYWWX46hjaT56HOrSXa+ql/LK26fy0P6LzDMYZ3lGMynEx9ni/ZyvTfV2bjKD+3BtJqS9HZn9gnxB/aM+Cvwp8XWXgj4ja/Bo+o6hB9ogW5DrG0ZYoCZNvlr8wI+Zh0r48/4KkfDey+NP7GOqeIPDUsV7L4elh1u0khYOskcOVl2MCQcwu5GDyRXwx/wVBnbVPH3gjxGeVvPDwUt2LxzOW/9Cr87tO8PfFXVdPisbDS9avNImbzLNILa4mtXfo5j2KULZwDjmvqs+4srKvi8rqYfnjays2nqlq9JX3v0P4FwfiDisjzqNanQU3RnGUbNq9rOz0ej207nxZovhrV9enMNhHwpwztwq/U+vt1qrrWlT6JqUumXBDNHj5l6EEZr7U+IHhHxF8JvEUHhL4j2Umiald2sV/Hb3Q8t2hnztbB75BDKfmVgQQCK8e0f4N/E349/FRvBvwh0a41y+YRq/kD91ECPvSyHCRqPVmFfl8aNZ1vYcj59rWd79rbn9T+HPjFnnEPE1WhmmEWEwKw8qkVJNbSppTdSSimmpO1ko2a3aueH+H/D+ueLNds/DHhi0lv9R1CZLe2toV3SSyyHCqoHUk1+yfxl8N3X7Ln7P+h/8E9/hOF1X4nfEm4gufFDWp3FBMR5dqGHRTgLzx5au5wJKtaZZ/B7/gmFpBhsJLbx78fdVi+zwQWyma00UzjbgAfMZDnGMCSToAiElvv/AP4J7fsS+LPh5q15+1H+0q76h8R/Em+ZUuSHksI5/vFj0E8g4YDiNPkH8VfeZJkVTnlhYfxpK02tqUHur7c8trdFfzt9dxdxjQ9lDMKi/wBlpvmpRejxFVfDK26o03713bmla2yv90/sw/ArRv2bvgboHwh0crK2mQZup1GPPu5Tvmk9fmcnGei4HavfKKK/YaFGFGnGlTVoxSSXkj+WMXi6uKr1MTXlec25N923dsKKKK1OcKKKKAILr/j2k/3T/KuOrsbr/j2k/wB0/wAq46gD/9T+/iiiigAooooAKKKKACiiigD87P2wf+Ccnwm/ahmbxvosh8K+NY8NHq1onyzOn3ftEYK7yMcSKVkX1IAFfnT4m8f/ALdv7L+gyfDn9rjwFb/GLwFD8q3ssf2srGOjfaAjspA6GeMMOzV/RTRXz+N4eo1akq+Hm6VR7uNrS/xRekvzPuMo45xOGoQweOpRxFCPwqd1KH/XuorSh8m0uiPwz0L/AIKEf8E3vi6miH4saHd6Xc6B5gs4tWs3vYIPNILAGFpA65UcSLxjgCvtS1/4KT/sLWVlHFZePrCGCJAqRJa3K7VHQBRFxj0xXv8A48/Zc/Zx+J0z3Xj3wPoupzyHLTS2cfnE+8iqH/WvGP8Ah23+w953n/8ACu9PznOPMn2/98+bj9K5oYTOqMpSpyoyb3k4yjJ2015Xqac/BNSbrPD4mlKW6hKlJf8AgUkpP5n5zfta/tof8Ex/jPq+k+IPHelan491HQlljtI7KGWyikWUqSkryNCzJlcgc4JPHNcZ4V+Iv7c37TGgJ8N/2Ovh7bfB7wHN8pvoo/shMZ4LfaSiMxx1MERf/ar9sPAn7LH7N3wxmS68B+BtF02eM5WaOzjMwI9JGBf9a98AAGBWSyDF16kquKrqPN8Xso8rfrN3lY9SXG+WYPDww2W4SdRQ+B4io5xjre6pRtTvfW+up+cv7H//AATg+FX7MdynjzxHMfFnjeTLvqt2vyQO/wB77OjFtpOeZGLSH1AOK/Rqiivo8FgaGEpKjh4KMV/V33fmz4LNs5xuZ4h4rHVXOb6vouyWyS6JJIKKKK6zzAooooAKKKKAILr/AI9pP90/yrjq7G6/49pP90/yrjqAP//Z"), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello there this is a long text", quotedItem: CIQuote.getSample(1, .now, "hi there", chatDir: .directSnd, image: "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAuKADAAQAAAABAAAAYAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgAYAC4AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQADP/aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKK+CP2vP+ChXwZ/ZPibw7dMfEHi2VAYdGs3G9N33TO/IiU9hgu3ZSOa/NzXNL/4KJ/td6JJ49+NXiq2+Cvw7kG/ZNKbDMLcjKblmfI/57SRqewrwMdxBRo1HQoRdWqt1HaP+KT0j838j7XKOCMXiqEcbjKkcPh5bSne8/wDr3BXlN+is+5+43jb45/Bf4bs0fj/xZpGjSL1jvL2KF/8AvlmDfpXjH/DfH7GQuPsv/CydD35x/wAfIx+fT9a/AO58D/8ABJj4UzvF4v8AFfif4l6mp/evpkfkWzP3w2Isg+omb61X/wCF0/8ABJr/AI9f+FQeJPL6ed9vbzPrj7ZivnavFuIT+KhHyc5Sf3wjY+7w/hlgZQv7PF1P70aUKa+SqTUvwP6afBXx2+CnxIZYvAHi3R9ZkfpHZ3sUz/8AfKsW/SvVq/lItvBf/BJX4rTLF4V8UeJ/hpqTH91JqUfn2yv2y2JcD3MqfUV9OaFon/BRH9krQ4vH3wI8XW3xq+HkY3+XDKb/ABCvJxHuaZMDr5Ergd1ruwvFNVrmq0VOK3lSkp29Y6SS+R5GY+HGGi1DD4qVKo9oYmm6XN5RqK9Nvsro/obor4A/ZC/4KH/Bv9qxV8MLnw54vjU+bo9443SFPvG3k4EoHdcB17rjmvv+vqcHjaGKpKth5qUX1X9aPyZ+b5rlOMy3ESwmOpOFRdH+aezT6NXTCiiiuo84KKKKACiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/Q/v4ooooAKKKKACiiigAr8tf+ChP7cWs/BEWfwD+A8R1P4k+JQkUCQr5rWUc52o+zndNIf9Up4H324wD9x/tDfGjw/wDs9fBnX/i/4jAeHRrZpI4c4M87YWKIe7yFV9gc9q/n6+B3iOb4GfCLxL/wU1+Oypq3jzxndT2nhK2uBwZptyvcBeoQBSq4xthjwPvivluIs0lSthKM+WUk5Sl/JBbtebekfM/R+BOHaeIcszxVL2kISUKdP/n7WlrGL/uxXvT8u6uizc6b8I/+CbmmRePPi9HD8Q/j7rifbktLmTz7bSGm582ZzktITyX++5+5tX5z5L8LPgv+0X/wVH12+8ZfEbxneW/2SRxB9o02eTSosdY4XRlgjYZGV++e5Jr8xvF3i7xN4+8UX/jXxney6jquqTNcXVzMcvJI5ySfQdgBwBgDgV+sP/BPX9jj9oL9oXw9H4tuvG2s+DfAVlM8VsthcyJLdSBsyCBNwREDZ3SEHLcBTgkfmuX4j+0MXHB06LdBXagna/8AenK6u+7el9Ej9+zvA/2Jls81r4uMcY7J1px5lHf93ShaVo9FFJNq8pMyPil/wRs/aj8D6dLq3gq70vxdHECxgtZGtrogf3UmAQn2EmT2r8rPEPh3xB4R1u58M+KrGfTdRsnMdxa3MbRTROOzKwBBr+674VfCnTfhNoI0DTtX1jWFAGZtYvpL2U4934X/AICAK8V/aW/Yf/Z9/areHUvibpkkerWsRhg1KxkMFyqHkBiMrIAeQJFYDJxjJr6bNPD+nOkqmAfLP+WTuvk7XX4/I/PeHvG6tSxDo5zH2lLpUhHll6uN7NelmvPY/iir2T4KftA/GD9njxMvir4Q65caTPkGWFTutrgD+GaE/I4+oyOxB5r2n9tb9jTxj+x18RYvD+pTtqmgaqrS6VqezZ5qpjfHIBwsseRuA4IIYdcD4yr80q0sRgcQ4SvCpB+jT8mvzP6Bw2JwOcYGNany1aFRdVdNdmn22aauno9T9tLO0+D/APwUr02Txd8NI4Ph38ftGT7b5NtIYLXWGh58yJwQVkBGd/8ArEP3i6fMP0R/4J7ftw6/8YZ7z9nb9oGJtN+JPhoPFIJ18p75IPlclegnj/5aKOGHzrxnH8rPhXxT4j8D+JbHxj4QvZdO1TTJkuLW5hba8UqHIIP8x0I4PFfsZ8bPEdx+0N8FvDv/AAUl+CgXSfiJ4EuYLXxZBbDALw4CXO0clMEZznMLlSf3Zr7PJM+nzyxUF+9ir1IrRVILeVtlOO+lrr5n5RxfwbRdKGXVXfDzfLRm9ZUKr+GDlq3RqP3UnfllZfy2/ptorw/9m/43aF+0X8FNA+L+gARpq1uGnhByYLlCUmiP+44IHqMHvXuFfsNGtCrTjVpu8ZJNPyZ/LWKwtXDVp4evG04Nxa7NOzX3hRRRWhzhRRRQBBdf8e0n+6f5Vx1djdf8e0n+6f5Vx1AH/9H+/iiiigAooooAKKKKAPw9/wCCvXiPWviH4q+F/wCyN4XlKT+K9TS6uQvoXFvAT7AvI3/AQe1fnF/wVO+IOnXfxx034AeDj5Xhv4ZaXb6TawKfkE7Ro0rY6bgvlofdT61+h3xNj/4Tv/gtd4Q0W/8Anh8P6THLGp6Ax21xOD/324Nfg3+0T4kufGH7QHjjxRdtukvte1GXJ9PPcKPwAAr8a4pxUpLEz6zq8n/btOK0+cpX9Uf1d4c5bCDy+lbSlh3W/wC38RNq/qoQcV5M8fjiaeRYEOGchR9TxX9svw9+GHijSvgB4I+Gnwr1ceGbGztYY728gijluhbohLLAJVeJZJJCN0jo+0Zwu4gj+JgO8REsf3l+YfUV/bf8DNVm+Mv7KtkNF1CTTZ9Z0d4Ir2D/AFls9zF8sidPmj3hhz1Fel4YyhGtiHpzWjur6e9f9Dw/H9VXQwFvgvUv62hb8Oa3zPoDwfp6aPoiaONXuNaa1Zo3ubp43nLDqrmJEXI/3QfWukmjMsTRBihYEbl6jPcZ7ivxk/4JMf8ABOv9ob9hBvFdr8ZvGOma9Yak22wttLiYGV2kMkl1dzSIkkkzcKisX8tSwDYNfs/X7Bj6NOlXlCjUU4/zJWv8j+ZsNUnOmpThyvtufj/+1Z8Hf2bPi58PviF8Avh/4wl1j4iaBZjXG0m71qfU7i3u4FMqt5VxLL5LzR70Kx7AVfJXAXH8sysGUMOh5r+vzwl+wD+y78KP2wPEX7bGn6xqFv4g8QmWa70+fUFGlrdTRmGS4EGATIY2dRvdlXe+0DPH83Nh+x58bPFev3kljpSaVYPcymGS+kEX7oudp2DL/dx/DX4Z4xZxkmCxGHxdTGRTlG0ueUU7q3S93a7S69Oh/SngTnNSjgcZhMc1CnCSlC70966dr/4U7Lq79T5Kr9MP+CWfxHsNH+P138EPF2JvDfxL0640a9gc/I0vls0Rx6kb4x/v1x3iz9hmHwV4KuPFHiLxlaWkltGzt5sBSAsBkIHL7iT0GFJJ7V8qfAnxLc+D/jd4N8V2bFJdP1vT5wR/szoT+YyK/NeD+Lcvx+Ijisuq88ackpPlklruveSvdX2ufsmavC5zlWKw9CV7xaTs1aSV4tXS1Ukmrdj9/P8Agkfrus/DD4ifFP8AY/8AEkrPJ4Z1F7y1DeiSG3mI9m2wv/wI1+5Ffhd4Ki/4Qf8A4Lb+INM0/wCSHxDpDySqOhL2cMx/8fizX7o1/RnC7ccLPDP/AJdTnBeid1+DP5M8RkqmZUselZ4ijSqv1lG0vvcWwooor6Q+BCiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/S/v4ooooAKKKKACiiigD8LfiNIfBP/BbLwpq9/wDJDr2kJHGTwCZLS4gH/j0eK/Bj9oPw7c+Evj3428M3ilZLHXtRiIPoJ3x+Ywa/fL/grnoWsfDPx98K/wBrzw5EzyeGNSS0uSvokguYQfZtsy/8CFfnB/wVP+HNho/7QFp8bvCeJvDnxK0231mznQfI0vlqsoz6kbJD/v1+M8U4WUViYW1hV5/+3akVr/4FG3qz+r/DnMYTeX1b6VcP7L/t/Dzenq4Tcl5I/M2v6yP+CR3j4eLP2XbLRZZN0uku9sRnp5bMB/45sr+Tev3u/wCCJXj7yNW8T/DyZ+C6XUak9pUw36xD865uAcV7LNFTf24tfd736Hd405d9Y4cddLWlOMvk7wf/AKUvuP6Kq/P/APaa+InjJfF8vge3lez06KONgIyVM+8ZJYjkgHIx045r9AK/Gr/gsB8UPHXwg8N+AvFfgV4oWmv7u3uTJEsiyL5SsiNkZxkMeCDmvU8bsgzPN+Fa+FyrEujUUot6tKcdnBtapO6fny2ejZ/OnAOFWJzqjheVOU+ZK+yaTlfr2t8z85td/b18H6D4n1DQLrw5fSLY3Elv5okRWcxsVJKMAVyR0yTivEPHf7f3jjVFe18BaXb6PGeBPcH7RN9QMBAfqGrFP7UPwj8c3f2/4y/DuzvbxgA93ZNtd8dyGwT+Lmuvh/aP/ZT8IxC58EfD0y3Y5UzwxKAf99mlP5Cv49wvCeBwUoc3D9Sday3qRlTb73c7Wf8Aej8j+rKWVUKLV8vlKf8AiTj/AOlW+9Hw74w8ceNvHl8NX8bajc6jK2SjTsSo/wBxeFUf7orovgf4dufF3xp8H+F7NS0uoa3p8Cgf7c6A/pW98avjx4q+NmoW0mswW9jY2G/7LaWy4WPfjJLHlicD0HoBX13/AMEtPhrZeI/2jH+L3inEPh34cWE+t31w/wBxJFRliBPqPmkH/XOv3fhXCVa/1ahUoRoybV4RacYq/dKK0jq7Ky1s3uezm+PeByeviqkFBxhK0U767RirJattLTqz9H/CMg8af8Futd1DT/ni8P6OySsOxSyiiP8A49Niv3Qr8NP+CS+j6t8V/iv8V/2wdfiZD4i1B7K0LDtLJ9olUf7imFfwr9y6/oLhe88LUxPSrUnNejdl+CP5G8RWqeY0cAnd4ejSpP8AxRjd/c5NBRRRX0h8CFFFFAEF1/x7Sf7p/lXHV2N1/wAe0n+6f5Vx1AH/0/7+KKKKACiiigAooooA8M/aT+B+iftGfBLxB8INcIjGrWxFvORnyLmMh4ZB/uSAE46jI71+AfwU8N3H7SXwL8Qf8E5fjFt0r4kfD65nuvCstycbmhz5ltuPVcE4x1idWHEdf031+UX/AAUL/Yj8T/FG/sv2mP2c5H074keGtkoFufLe+jg5Taennx9Ezw6/Ie2PleI8slUtjKUOZpOM4/zwe6X96L1j5/cfpPAXEMKF8rxNX2cZSU6VR7Uq0dE3/cmvcn5dldn8r/iXw3r/AIN8Q3vhPxXZy6fqemzPb3VtMNskUsZwysPY/n1HFfe3/BL3x/8A8IP+1bptvK+2HVbeSBvdoyso/RWH419SX8fwg/4Kc6QmleIpLfwB8f8ASI/ssiXCGC11kwfLtZSNwkGMbceZH0w6Dj88tM+HvxW/ZK/aO8OQ/FvR7nQ7uw1OElpV/czQs+x2ilGUkUqTypPvivy3DYWWX46hjaT56HOrSXa+ql/LK26fy0P6LzDMYZ3lGMynEx9ni/ZyvTfV2bjKD+3BtJqS9HZn9gnxB/aM+Cvwp8XWXgj4ja/Bo+o6hB9ogW5DrG0ZYoCZNvlr8wI+Zh0r48/4KkfDey+NP7GOqeIPDUsV7L4elh1u0khYOskcOVl2MCQcwu5GDyRXwx/wVBnbVPH3gjxGeVvPDwUt2LxzOW/9Cr87tO8PfFXVdPisbDS9avNImbzLNILa4mtXfo5j2KULZwDjmvqs+4srKvi8rqYfnjays2nqlq9JX3v0P4FwfiDisjzqNanQU3RnGUbNq9rOz0ej207nxZovhrV9enMNhHwpwztwq/U+vt1qrrWlT6JqUumXBDNHj5l6EEZr7U+IHhHxF8JvEUHhL4j2Umiald2sV/Hb3Q8t2hnztbB75BDKfmVgQQCK8e0f4N/E349/FRvBvwh0a41y+YRq/kD91ECPvSyHCRqPVmFfl8aNZ1vYcj59rWd79rbn9T+HPjFnnEPE1WhmmEWEwKw8qkVJNbSppTdSSimmpO1ko2a3aueH+H/D+ueLNds/DHhi0lv9R1CZLe2toV3SSyyHCqoHUk1+yfxl8N3X7Ln7P+h/8E9/hOF1X4nfEm4gufFDWp3FBMR5dqGHRTgLzx5au5wJKtaZZ/B7/gmFpBhsJLbx78fdVi+zwQWyma00UzjbgAfMZDnGMCSToAiElvv/AP4J7fsS+LPh5q15+1H+0q76h8R/Em+ZUuSHksI5/vFj0E8g4YDiNPkH8VfeZJkVTnlhYfxpK02tqUHur7c8trdFfzt9dxdxjQ9lDMKi/wBlpvmpRejxFVfDK26o03713bmla2yv90/sw/ArRv2bvgboHwh0crK2mQZup1GPPu5Tvmk9fmcnGei4HavfKKK/YaFGFGnGlTVoxSSXkj+WMXi6uKr1MTXlec25N923dsKKKK1OcKKKKAILr/j2k/3T/KuOrsbr/j2k/wB0/wAq46gD/9T+/iiiigAooooAKKKKACiiigD87P2wf+Ccnwm/ahmbxvosh8K+NY8NHq1onyzOn3ftEYK7yMcSKVkX1IAFfnT4m8f/ALdv7L+gyfDn9rjwFb/GLwFD8q3ssf2srGOjfaAjspA6GeMMOzV/RTRXz+N4eo1akq+Hm6VR7uNrS/xRekvzPuMo45xOGoQweOpRxFCPwqd1KH/XuorSh8m0uiPwz0L/AIKEf8E3vi6miH4saHd6Xc6B5gs4tWs3vYIPNILAGFpA65UcSLxjgCvtS1/4KT/sLWVlHFZePrCGCJAqRJa3K7VHQBRFxj0xXv8A48/Zc/Zx+J0z3Xj3wPoupzyHLTS2cfnE+8iqH/WvGP8Ah23+w953n/8ACu9PznOPMn2/98+bj9K5oYTOqMpSpyoyb3k4yjJ2015Xqac/BNSbrPD4mlKW6hKlJf8AgUkpP5n5zfta/tof8Ex/jPq+k+IPHelan491HQlljtI7KGWyikWUqSkryNCzJlcgc4JPHNcZ4V+Iv7c37TGgJ8N/2Ovh7bfB7wHN8pvoo/shMZ4LfaSiMxx1MERf/ar9sPAn7LH7N3wxmS68B+BtF02eM5WaOzjMwI9JGBf9a98AAGBWSyDF16kquKrqPN8Xso8rfrN3lY9SXG+WYPDww2W4SdRQ+B4io5xjre6pRtTvfW+up+cv7H//AATg+FX7MdynjzxHMfFnjeTLvqt2vyQO/wB77OjFtpOeZGLSH1AOK/Rqiivo8FgaGEpKjh4KMV/V33fmz4LNs5xuZ4h4rHVXOb6vouyWyS6JJIKKKK6zzAooooAKKKKAILr/AI9pP90/yrjq7G6/49pP90/yrjqAP//Z"), itemEdited: true), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) } .previewLayout(.fixed(width: 360, height: 200)) } @@ -420,16 +400,16 @@ struct FramedItemView_Edited_Previews: PreviewProvider { struct FramedItemView_Deleted_Previews: PreviewProvider { static var previews: some View { Group { - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi there hello hello hello ther hello hello", chatDir: .directSnd, image: "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAuKADAAQAAAABAAAAYAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgAYAC4AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQADP/aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKK+CP2vP+ChXwZ/ZPibw7dMfEHi2VAYdGs3G9N33TO/IiU9hgu3ZSOa/NzXNL/4KJ/td6JJ49+NXiq2+Cvw7kG/ZNKbDMLcjKblmfI/57SRqewrwMdxBRo1HQoRdWqt1HaP+KT0j838j7XKOCMXiqEcbjKkcPh5bSne8/wDr3BXlN+is+5+43jb45/Bf4bs0fj/xZpGjSL1jvL2KF/8AvlmDfpXjH/DfH7GQuPsv/CydD35x/wAfIx+fT9a/AO58D/8ABJj4UzvF4v8AFfif4l6mp/evpkfkWzP3w2Isg+omb61X/wCF0/8ABJr/AI9f+FQeJPL6ed9vbzPrj7ZivnavFuIT+KhHyc5Sf3wjY+7w/hlgZQv7PF1P70aUKa+SqTUvwP6afBXx2+CnxIZYvAHi3R9ZkfpHZ3sUz/8AfKsW/SvVq/lItvBf/BJX4rTLF4V8UeJ/hpqTH91JqUfn2yv2y2JcD3MqfUV9OaFon/BRH9krQ4vH3wI8XW3xq+HkY3+XDKb/ABCvJxHuaZMDr5Ergd1ruwvFNVrmq0VOK3lSkp29Y6SS+R5GY+HGGi1DD4qVKo9oYmm6XN5RqK9Nvsro/obor4A/ZC/4KH/Bv9qxV8MLnw54vjU+bo9443SFPvG3k4EoHdcB17rjmvv+vqcHjaGKpKth5qUX1X9aPyZ+b5rlOMy3ESwmOpOFRdH+aezT6NXTCiiiuo84KKKKACiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/Q/v4ooooAKKKKACiiigAr8tf+ChP7cWs/BEWfwD+A8R1P4k+JQkUCQr5rWUc52o+zndNIf9Up4H324wD9x/tDfGjw/wDs9fBnX/i/4jAeHRrZpI4c4M87YWKIe7yFV9gc9q/n6+B3iOb4GfCLxL/wU1+Oypq3jzxndT2nhK2uBwZptyvcBeoQBSq4xthjwPvivluIs0lSthKM+WUk5Sl/JBbtebekfM/R+BOHaeIcszxVL2kISUKdP/n7WlrGL/uxXvT8u6uizc6b8I/+CbmmRePPi9HD8Q/j7rifbktLmTz7bSGm582ZzktITyX++5+5tX5z5L8LPgv+0X/wVH12+8ZfEbxneW/2SRxB9o02eTSosdY4XRlgjYZGV++e5Jr8xvF3i7xN4+8UX/jXxney6jquqTNcXVzMcvJI5ySfQdgBwBgDgV+sP/BPX9jj9oL9oXw9H4tuvG2s+DfAVlM8VsthcyJLdSBsyCBNwREDZ3SEHLcBTgkfmuX4j+0MXHB06LdBXagna/8AenK6u+7el9Ej9+zvA/2Jls81r4uMcY7J1px5lHf93ShaVo9FFJNq8pMyPil/wRs/aj8D6dLq3gq70vxdHECxgtZGtrogf3UmAQn2EmT2r8rPEPh3xB4R1u58M+KrGfTdRsnMdxa3MbRTROOzKwBBr+674VfCnTfhNoI0DTtX1jWFAGZtYvpL2U4934X/AICAK8V/aW/Yf/Z9/areHUvibpkkerWsRhg1KxkMFyqHkBiMrIAeQJFYDJxjJr6bNPD+nOkqmAfLP+WTuvk7XX4/I/PeHvG6tSxDo5zH2lLpUhHll6uN7NelmvPY/iir2T4KftA/GD9njxMvir4Q65caTPkGWFTutrgD+GaE/I4+oyOxB5r2n9tb9jTxj+x18RYvD+pTtqmgaqrS6VqezZ5qpjfHIBwsseRuA4IIYdcD4yr80q0sRgcQ4SvCpB+jT8mvzP6Bw2JwOcYGNany1aFRdVdNdmn22aauno9T9tLO0+D/APwUr02Txd8NI4Ph38ftGT7b5NtIYLXWGh58yJwQVkBGd/8ArEP3i6fMP0R/4J7ftw6/8YZ7z9nb9oGJtN+JPhoPFIJ18p75IPlclegnj/5aKOGHzrxnH8rPhXxT4j8D+JbHxj4QvZdO1TTJkuLW5hba8UqHIIP8x0I4PFfsZ8bPEdx+0N8FvDv/AAUl+CgXSfiJ4EuYLXxZBbDALw4CXO0clMEZznMLlSf3Zr7PJM+nzyxUF+9ir1IrRVILeVtlOO+lrr5n5RxfwbRdKGXVXfDzfLRm9ZUKr+GDlq3RqP3UnfllZfy2/ptorw/9m/43aF+0X8FNA+L+gARpq1uGnhByYLlCUmiP+44IHqMHvXuFfsNGtCrTjVpu8ZJNPyZ/LWKwtXDVp4evG04Nxa7NOzX3hRRRWhzhRRRQBBdf8e0n+6f5Vx1djdf8e0n+6f5Vx1AH/9H+/iiiigAooooAKKKKAPw9/wCCvXiPWviH4q+F/wCyN4XlKT+K9TS6uQvoXFvAT7AvI3/AQe1fnF/wVO+IOnXfxx034AeDj5Xhv4ZaXb6TawKfkE7Ro0rY6bgvlofdT61+h3xNj/4Tv/gtd4Q0W/8Anh8P6THLGp6Ax21xOD/324Nfg3+0T4kufGH7QHjjxRdtukvte1GXJ9PPcKPwAAr8a4pxUpLEz6zq8n/btOK0+cpX9Uf1d4c5bCDy+lbSlh3W/wC38RNq/qoQcV5M8fjiaeRYEOGchR9TxX9svw9+GHijSvgB4I+Gnwr1ceGbGztYY728gijluhbohLLAJVeJZJJCN0jo+0Zwu4gj+JgO8REsf3l+YfUV/bf8DNVm+Mv7KtkNF1CTTZ9Z0d4Ir2D/AFls9zF8sidPmj3hhz1Fel4YyhGtiHpzWjur6e9f9Dw/H9VXQwFvgvUv62hb8Oa3zPoDwfp6aPoiaONXuNaa1Zo3ubp43nLDqrmJEXI/3QfWukmjMsTRBihYEbl6jPcZ7ivxk/4JMf8ABOv9ob9hBvFdr8ZvGOma9Yak22wttLiYGV2kMkl1dzSIkkkzcKisX8tSwDYNfs/X7Bj6NOlXlCjUU4/zJWv8j+ZsNUnOmpThyvtufj/+1Z8Hf2bPi58PviF8Avh/4wl1j4iaBZjXG0m71qfU7i3u4FMqt5VxLL5LzR70Kx7AVfJXAXH8sysGUMOh5r+vzwl+wD+y78KP2wPEX7bGn6xqFv4g8QmWa70+fUFGlrdTRmGS4EGATIY2dRvdlXe+0DPH83Nh+x58bPFev3kljpSaVYPcymGS+kEX7oudp2DL/dx/DX4Z4xZxkmCxGHxdTGRTlG0ueUU7q3S93a7S69Oh/SngTnNSjgcZhMc1CnCSlC70966dr/4U7Lq79T5Kr9MP+CWfxHsNH+P138EPF2JvDfxL0640a9gc/I0vls0Rx6kb4x/v1x3iz9hmHwV4KuPFHiLxlaWkltGzt5sBSAsBkIHL7iT0GFJJ7V8qfAnxLc+D/jd4N8V2bFJdP1vT5wR/szoT+YyK/NeD+Lcvx+Ijisuq88ackpPlklruveSvdX2ufsmavC5zlWKw9CV7xaTs1aSV4tXS1Ukmrdj9/P8Agkfrus/DD4ifFP8AY/8AEkrPJ4Z1F7y1DeiSG3mI9m2wv/wI1+5Ffhd4Ki/4Qf8A4Lb+INM0/wCSHxDpDySqOhL2cMx/8fizX7o1/RnC7ccLPDP/AJdTnBeid1+DP5M8RkqmZUselZ4ijSqv1lG0vvcWwooor6Q+BCiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/S/v4ooooAKKKKACiiigD8LfiNIfBP/BbLwpq9/wDJDr2kJHGTwCZLS4gH/j0eK/Bj9oPw7c+Evj3428M3ilZLHXtRiIPoJ3x+Ywa/fL/grnoWsfDPx98K/wBrzw5EzyeGNSS0uSvokguYQfZtsy/8CFfnB/wVP+HNho/7QFp8bvCeJvDnxK0231mznQfI0vlqsoz6kbJD/v1+M8U4WUViYW1hV5/+3akVr/4FG3qz+r/DnMYTeX1b6VcP7L/t/Dzenq4Tcl5I/M2v6yP+CR3j4eLP2XbLRZZN0uku9sRnp5bMB/45sr+Tev3u/wCCJXj7yNW8T/DyZ+C6XUak9pUw36xD865uAcV7LNFTf24tfd736Hd405d9Y4cddLWlOMvk7wf/AKUvuP6Kq/P/APaa+InjJfF8vge3lez06KONgIyVM+8ZJYjkgHIx045r9AK/Gr/gsB8UPHXwg8N+AvFfgV4oWmv7u3uTJEsiyL5SsiNkZxkMeCDmvU8bsgzPN+Fa+FyrEujUUot6tKcdnBtapO6fny2ejZ/OnAOFWJzqjheVOU+ZK+yaTlfr2t8z85td/b18H6D4n1DQLrw5fSLY3Elv5okRWcxsVJKMAVyR0yTivEPHf7f3jjVFe18BaXb6PGeBPcH7RN9QMBAfqGrFP7UPwj8c3f2/4y/DuzvbxgA93ZNtd8dyGwT+Lmuvh/aP/ZT8IxC58EfD0y3Y5UzwxKAf99mlP5Cv49wvCeBwUoc3D9Sday3qRlTb73c7Wf8Aej8j+rKWVUKLV8vlKf8AiTj/AOlW+9Hw74w8ceNvHl8NX8bajc6jK2SjTsSo/wBxeFUf7orovgf4dufF3xp8H+F7NS0uoa3p8Cgf7c6A/pW98avjx4q+NmoW0mswW9jY2G/7LaWy4WPfjJLHlicD0HoBX13/AMEtPhrZeI/2jH+L3inEPh34cWE+t31w/wBxJFRliBPqPmkH/XOv3fhXCVa/1ahUoRoybV4RacYq/dKK0jq7Ky1s3uezm+PeByeviqkFBxhK0U767RirJattLTqz9H/CMg8af8Futd1DT/ni8P6OySsOxSyiiP8A49Niv3Qr8NP+CS+j6t8V/iv8V/2wdfiZD4i1B7K0LDtLJ9olUf7imFfwr9y6/oLhe88LUxPSrUnNejdl+CP5G8RWqeY0cAnd4ejSpP8AxRjd/c5NBRRRX0h8CFFFFAEF1/x7Sf7p/lXHV2N1/wAe0n+6f5Vx1AH/0/7+KKKKACiiigAooooA8M/aT+B+iftGfBLxB8INcIjGrWxFvORnyLmMh4ZB/uSAE46jI71+AfwU8N3H7SXwL8Qf8E5fjFt0r4kfD65nuvCstycbmhz5ltuPVcE4x1idWHEdf031+UX/AAUL/Yj8T/FG/sv2mP2c5H074keGtkoFufLe+jg5Taennx9Ezw6/Ie2PleI8slUtjKUOZpOM4/zwe6X96L1j5/cfpPAXEMKF8rxNX2cZSU6VR7Uq0dE3/cmvcn5dldn8r/iXw3r/AIN8Q3vhPxXZy6fqemzPb3VtMNskUsZwysPY/n1HFfe3/BL3x/8A8IP+1bptvK+2HVbeSBvdoyso/RWH419SX8fwg/4Kc6QmleIpLfwB8f8ASI/ssiXCGC11kwfLtZSNwkGMbceZH0w6Dj88tM+HvxW/ZK/aO8OQ/FvR7nQ7uw1OElpV/czQs+x2ilGUkUqTypPvivy3DYWWX46hjaT56HOrSXa+ql/LK26fy0P6LzDMYZ3lGMynEx9ni/ZyvTfV2bjKD+3BtJqS9HZn9gnxB/aM+Cvwp8XWXgj4ja/Bo+o6hB9ogW5DrG0ZYoCZNvlr8wI+Zh0r48/4KkfDey+NP7GOqeIPDUsV7L4elh1u0khYOskcOVl2MCQcwu5GDyRXwx/wVBnbVPH3gjxGeVvPDwUt2LxzOW/9Cr87tO8PfFXVdPisbDS9avNImbzLNILa4mtXfo5j2KULZwDjmvqs+4srKvi8rqYfnjays2nqlq9JX3v0P4FwfiDisjzqNanQU3RnGUbNq9rOz0ej207nxZovhrV9enMNhHwpwztwq/U+vt1qrrWlT6JqUumXBDNHj5l6EEZr7U+IHhHxF8JvEUHhL4j2Umiald2sV/Hb3Q8t2hnztbB75BDKfmVgQQCK8e0f4N/E349/FRvBvwh0a41y+YRq/kD91ECPvSyHCRqPVmFfl8aNZ1vYcj59rWd79rbn9T+HPjFnnEPE1WhmmEWEwKw8qkVJNbSppTdSSimmpO1ko2a3aueH+H/D+ueLNds/DHhi0lv9R1CZLe2toV3SSyyHCqoHUk1+yfxl8N3X7Ln7P+h/8E9/hOF1X4nfEm4gufFDWp3FBMR5dqGHRTgLzx5au5wJKtaZZ/B7/gmFpBhsJLbx78fdVi+zwQWyma00UzjbgAfMZDnGMCSToAiElvv/AP4J7fsS+LPh5q15+1H+0q76h8R/Em+ZUuSHksI5/vFj0E8g4YDiNPkH8VfeZJkVTnlhYfxpK02tqUHur7c8trdFfzt9dxdxjQ9lDMKi/wBlpvmpRejxFVfDK26o03713bmla2yv90/sw/ArRv2bvgboHwh0crK2mQZup1GPPu5Tvmk9fmcnGei4HavfKKK/YaFGFGnGlTVoxSSXkj+WMXi6uKr1MTXlec25N923dsKKKK1OcKKKKAILr/j2k/3T/KuOrsbr/j2k/wB0/wAq46gD/9T+/iiiigAooooAKKKKACiiigD87P2wf+Ccnwm/ahmbxvosh8K+NY8NHq1onyzOn3ftEYK7yMcSKVkX1IAFfnT4m8f/ALdv7L+gyfDn9rjwFb/GLwFD8q3ssf2srGOjfaAjspA6GeMMOzV/RTRXz+N4eo1akq+Hm6VR7uNrS/xRekvzPuMo45xOGoQweOpRxFCPwqd1KH/XuorSh8m0uiPwz0L/AIKEf8E3vi6miH4saHd6Xc6B5gs4tWs3vYIPNILAGFpA65UcSLxjgCvtS1/4KT/sLWVlHFZePrCGCJAqRJa3K7VHQBRFxj0xXv8A48/Zc/Zx+J0z3Xj3wPoupzyHLTS2cfnE+8iqH/WvGP8Ah23+w953n/8ACu9PznOPMn2/98+bj9K5oYTOqMpSpyoyb3k4yjJ2015Xqac/BNSbrPD4mlKW6hKlJf8AgUkpP5n5zfta/tof8Ex/jPq+k+IPHelan491HQlljtI7KGWyikWUqSkryNCzJlcgc4JPHNcZ4V+Iv7c37TGgJ8N/2Ovh7bfB7wHN8pvoo/shMZ4LfaSiMxx1MERf/ar9sPAn7LH7N3wxmS68B+BtF02eM5WaOzjMwI9JGBf9a98AAGBWSyDF16kquKrqPN8Xso8rfrN3lY9SXG+WYPDww2W4SdRQ+B4io5xjre6pRtTvfW+up+cv7H//AATg+FX7MdynjzxHMfFnjeTLvqt2vyQO/wB77OjFtpOeZGLSH1AOK/Rqiivo8FgaGEpKjh4KMV/V33fmz4LNs5xuZ4h4rHVXOb6vouyWyS6JJIKKKK6zzAooooAKKKKAILr/AI9pP90/yrjq7G6/49pP90/yrjqAP//Z"), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) - FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello there this is a long text", quotedItem: CIQuote.getSample(1, .now, "hi there", chatDir: .directSnd, image: "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAuKADAAQAAAABAAAAYAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgAYAC4AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQADP/aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKK+CP2vP+ChXwZ/ZPibw7dMfEHi2VAYdGs3G9N33TO/IiU9hgu3ZSOa/NzXNL/4KJ/td6JJ49+NXiq2+Cvw7kG/ZNKbDMLcjKblmfI/57SRqewrwMdxBRo1HQoRdWqt1HaP+KT0j838j7XKOCMXiqEcbjKkcPh5bSne8/wDr3BXlN+is+5+43jb45/Bf4bs0fj/xZpGjSL1jvL2KF/8AvlmDfpXjH/DfH7GQuPsv/CydD35x/wAfIx+fT9a/AO58D/8ABJj4UzvF4v8AFfif4l6mp/evpkfkWzP3w2Isg+omb61X/wCF0/8ABJr/AI9f+FQeJPL6ed9vbzPrj7ZivnavFuIT+KhHyc5Sf3wjY+7w/hlgZQv7PF1P70aUKa+SqTUvwP6afBXx2+CnxIZYvAHi3R9ZkfpHZ3sUz/8AfKsW/SvVq/lItvBf/BJX4rTLF4V8UeJ/hpqTH91JqUfn2yv2y2JcD3MqfUV9OaFon/BRH9krQ4vH3wI8XW3xq+HkY3+XDKb/ABCvJxHuaZMDr5Ergd1ruwvFNVrmq0VOK3lSkp29Y6SS+R5GY+HGGi1DD4qVKo9oYmm6XN5RqK9Nvsro/obor4A/ZC/4KH/Bv9qxV8MLnw54vjU+bo9443SFPvG3k4EoHdcB17rjmvv+vqcHjaGKpKth5qUX1X9aPyZ+b5rlOMy3ESwmOpOFRdH+aezT6NXTCiiiuo84KKKKACiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/Q/v4ooooAKKKKACiiigAr8tf+ChP7cWs/BEWfwD+A8R1P4k+JQkUCQr5rWUc52o+zndNIf9Up4H324wD9x/tDfGjw/wDs9fBnX/i/4jAeHRrZpI4c4M87YWKIe7yFV9gc9q/n6+B3iOb4GfCLxL/wU1+Oypq3jzxndT2nhK2uBwZptyvcBeoQBSq4xthjwPvivluIs0lSthKM+WUk5Sl/JBbtebekfM/R+BOHaeIcszxVL2kISUKdP/n7WlrGL/uxXvT8u6uizc6b8I/+CbmmRePPi9HD8Q/j7rifbktLmTz7bSGm582ZzktITyX++5+5tX5z5L8LPgv+0X/wVH12+8ZfEbxneW/2SRxB9o02eTSosdY4XRlgjYZGV++e5Jr8xvF3i7xN4+8UX/jXxney6jquqTNcXVzMcvJI5ySfQdgBwBgDgV+sP/BPX9jj9oL9oXw9H4tuvG2s+DfAVlM8VsthcyJLdSBsyCBNwREDZ3SEHLcBTgkfmuX4j+0MXHB06LdBXagna/8AenK6u+7el9Ej9+zvA/2Jls81r4uMcY7J1px5lHf93ShaVo9FFJNq8pMyPil/wRs/aj8D6dLq3gq70vxdHECxgtZGtrogf3UmAQn2EmT2r8rPEPh3xB4R1u58M+KrGfTdRsnMdxa3MbRTROOzKwBBr+674VfCnTfhNoI0DTtX1jWFAGZtYvpL2U4934X/AICAK8V/aW/Yf/Z9/areHUvibpkkerWsRhg1KxkMFyqHkBiMrIAeQJFYDJxjJr6bNPD+nOkqmAfLP+WTuvk7XX4/I/PeHvG6tSxDo5zH2lLpUhHll6uN7NelmvPY/iir2T4KftA/GD9njxMvir4Q65caTPkGWFTutrgD+GaE/I4+oyOxB5r2n9tb9jTxj+x18RYvD+pTtqmgaqrS6VqezZ5qpjfHIBwsseRuA4IIYdcD4yr80q0sRgcQ4SvCpB+jT8mvzP6Bw2JwOcYGNany1aFRdVdNdmn22aauno9T9tLO0+D/APwUr02Txd8NI4Ph38ftGT7b5NtIYLXWGh58yJwQVkBGd/8ArEP3i6fMP0R/4J7ftw6/8YZ7z9nb9oGJtN+JPhoPFIJ18p75IPlclegnj/5aKOGHzrxnH8rPhXxT4j8D+JbHxj4QvZdO1TTJkuLW5hba8UqHIIP8x0I4PFfsZ8bPEdx+0N8FvDv/AAUl+CgXSfiJ4EuYLXxZBbDALw4CXO0clMEZznMLlSf3Zr7PJM+nzyxUF+9ir1IrRVILeVtlOO+lrr5n5RxfwbRdKGXVXfDzfLRm9ZUKr+GDlq3RqP3UnfllZfy2/ptorw/9m/43aF+0X8FNA+L+gARpq1uGnhByYLlCUmiP+44IHqMHvXuFfsNGtCrTjVpu8ZJNPyZ/LWKwtXDVp4evG04Nxa7NOzX3hRRRWhzhRRRQBBdf8e0n+6f5Vx1djdf8e0n+6f5Vx1AH/9H+/iiiigAooooAKKKKAPw9/wCCvXiPWviH4q+F/wCyN4XlKT+K9TS6uQvoXFvAT7AvI3/AQe1fnF/wVO+IOnXfxx034AeDj5Xhv4ZaXb6TawKfkE7Ro0rY6bgvlofdT61+h3xNj/4Tv/gtd4Q0W/8Anh8P6THLGp6Ax21xOD/324Nfg3+0T4kufGH7QHjjxRdtukvte1GXJ9PPcKPwAAr8a4pxUpLEz6zq8n/btOK0+cpX9Uf1d4c5bCDy+lbSlh3W/wC38RNq/qoQcV5M8fjiaeRYEOGchR9TxX9svw9+GHijSvgB4I+Gnwr1ceGbGztYY728gijluhbohLLAJVeJZJJCN0jo+0Zwu4gj+JgO8REsf3l+YfUV/bf8DNVm+Mv7KtkNF1CTTZ9Z0d4Ir2D/AFls9zF8sidPmj3hhz1Fel4YyhGtiHpzWjur6e9f9Dw/H9VXQwFvgvUv62hb8Oa3zPoDwfp6aPoiaONXuNaa1Zo3ubp43nLDqrmJEXI/3QfWukmjMsTRBihYEbl6jPcZ7ivxk/4JMf8ABOv9ob9hBvFdr8ZvGOma9Yak22wttLiYGV2kMkl1dzSIkkkzcKisX8tSwDYNfs/X7Bj6NOlXlCjUU4/zJWv8j+ZsNUnOmpThyvtufj/+1Z8Hf2bPi58PviF8Avh/4wl1j4iaBZjXG0m71qfU7i3u4FMqt5VxLL5LzR70Kx7AVfJXAXH8sysGUMOh5r+vzwl+wD+y78KP2wPEX7bGn6xqFv4g8QmWa70+fUFGlrdTRmGS4EGATIY2dRvdlXe+0DPH83Nh+x58bPFev3kljpSaVYPcymGS+kEX7oudp2DL/dx/DX4Z4xZxkmCxGHxdTGRTlG0ueUU7q3S93a7S69Oh/SngTnNSjgcZhMc1CnCSlC70966dr/4U7Lq79T5Kr9MP+CWfxHsNH+P138EPF2JvDfxL0640a9gc/I0vls0Rx6kb4x/v1x3iz9hmHwV4KuPFHiLxlaWkltGzt5sBSAsBkIHL7iT0GFJJ7V8qfAnxLc+D/jd4N8V2bFJdP1vT5wR/szoT+YyK/NeD+Lcvx+Ijisuq88ackpPlklruveSvdX2ufsmavC5zlWKw9CV7xaTs1aSV4tXS1Ukmrdj9/P8Agkfrus/DD4ifFP8AY/8AEkrPJ4Z1F7y1DeiSG3mI9m2wv/wI1+5Ffhd4Ki/4Qf8A4Lb+INM0/wCSHxDpDySqOhL2cMx/8fizX7o1/RnC7ccLPDP/AJdTnBeid1+DP5M8RkqmZUselZ4ijSqv1lG0vvcWwooor6Q+BCiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/S/v4ooooAKKKKACiiigD8LfiNIfBP/BbLwpq9/wDJDr2kJHGTwCZLS4gH/j0eK/Bj9oPw7c+Evj3428M3ilZLHXtRiIPoJ3x+Ywa/fL/grnoWsfDPx98K/wBrzw5EzyeGNSS0uSvokguYQfZtsy/8CFfnB/wVP+HNho/7QFp8bvCeJvDnxK0231mznQfI0vlqsoz6kbJD/v1+M8U4WUViYW1hV5/+3akVr/4FG3qz+r/DnMYTeX1b6VcP7L/t/Dzenq4Tcl5I/M2v6yP+CR3j4eLP2XbLRZZN0uku9sRnp5bMB/45sr+Tev3u/wCCJXj7yNW8T/DyZ+C6XUak9pUw36xD865uAcV7LNFTf24tfd736Hd405d9Y4cddLWlOMvk7wf/AKUvuP6Kq/P/APaa+InjJfF8vge3lez06KONgIyVM+8ZJYjkgHIx045r9AK/Gr/gsB8UPHXwg8N+AvFfgV4oWmv7u3uTJEsiyL5SsiNkZxkMeCDmvU8bsgzPN+Fa+FyrEujUUot6tKcdnBtapO6fny2ejZ/OnAOFWJzqjheVOU+ZK+yaTlfr2t8z85td/b18H6D4n1DQLrw5fSLY3Elv5okRWcxsVJKMAVyR0yTivEPHf7f3jjVFe18BaXb6PGeBPcH7RN9QMBAfqGrFP7UPwj8c3f2/4y/DuzvbxgA93ZNtd8dyGwT+Lmuvh/aP/ZT8IxC58EfD0y3Y5UzwxKAf99mlP5Cv49wvCeBwUoc3D9Sday3qRlTb73c7Wf8Aej8j+rKWVUKLV8vlKf8AiTj/AOlW+9Hw74w8ceNvHl8NX8bajc6jK2SjTsSo/wBxeFUf7orovgf4dufF3xp8H+F7NS0uoa3p8Cgf7c6A/pW98avjx4q+NmoW0mswW9jY2G/7LaWy4WPfjJLHlicD0HoBX13/AMEtPhrZeI/2jH+L3inEPh34cWE+t31w/wBxJFRliBPqPmkH/XOv3fhXCVa/1ahUoRoybV4RacYq/dKK0jq7Ky1s3uezm+PeByeviqkFBxhK0U767RirJattLTqz9H/CMg8af8Futd1DT/ni8P6OySsOxSyiiP8A49Niv3Qr8NP+CS+j6t8V/iv8V/2wdfiZD4i1B7K0LDtLJ9olUf7imFfwr9y6/oLhe88LUxPSrUnNejdl+CP5G8RWqeY0cAnd4ejSpP8AxRjd/c5NBRRRX0h8CFFFFAEF1/x7Sf7p/lXHV2N1/wAe0n+6f5Vx1AH/0/7+KKKKACiiigAooooA8M/aT+B+iftGfBLxB8INcIjGrWxFvORnyLmMh4ZB/uSAE46jI71+AfwU8N3H7SXwL8Qf8E5fjFt0r4kfD65nuvCstycbmhz5ltuPVcE4x1idWHEdf031+UX/AAUL/Yj8T/FG/sv2mP2c5H074keGtkoFufLe+jg5Taennx9Ezw6/Ie2PleI8slUtjKUOZpOM4/zwe6X96L1j5/cfpPAXEMKF8rxNX2cZSU6VR7Uq0dE3/cmvcn5dldn8r/iXw3r/AIN8Q3vhPxXZy6fqemzPb3VtMNskUsZwysPY/n1HFfe3/BL3x/8A8IP+1bptvK+2HVbeSBvdoyso/RWH419SX8fwg/4Kc6QmleIpLfwB8f8ASI/ssiXCGC11kwfLtZSNwkGMbceZH0w6Dj88tM+HvxW/ZK/aO8OQ/FvR7nQ7uw1OElpV/czQs+x2ilGUkUqTypPvivy3DYWWX46hjaT56HOrSXa+ql/LK26fy0P6LzDMYZ3lGMynEx9ni/ZyvTfV2bjKD+3BtJqS9HZn9gnxB/aM+Cvwp8XWXgj4ja/Bo+o6hB9ogW5DrG0ZYoCZNvlr8wI+Zh0r48/4KkfDey+NP7GOqeIPDUsV7L4elh1u0khYOskcOVl2MCQcwu5GDyRXwx/wVBnbVPH3gjxGeVvPDwUt2LxzOW/9Cr87tO8PfFXVdPisbDS9avNImbzLNILa4mtXfo5j2KULZwDjmvqs+4srKvi8rqYfnjays2nqlq9JX3v0P4FwfiDisjzqNanQU3RnGUbNq9rOz0ej207nxZovhrV9enMNhHwpwztwq/U+vt1qrrWlT6JqUumXBDNHj5l6EEZr7U+IHhHxF8JvEUHhL4j2Umiald2sV/Hb3Q8t2hnztbB75BDKfmVgQQCK8e0f4N/E349/FRvBvwh0a41y+YRq/kD91ECPvSyHCRqPVmFfl8aNZ1vYcj59rWd79rbn9T+HPjFnnEPE1WhmmEWEwKw8qkVJNbSppTdSSimmpO1ko2a3aueH+H/D+ueLNds/DHhi0lv9R1CZLe2toV3SSyyHCqoHUk1+yfxl8N3X7Ln7P+h/8E9/hOF1X4nfEm4gufFDWp3FBMR5dqGHRTgLzx5au5wJKtaZZ/B7/gmFpBhsJLbx78fdVi+zwQWyma00UzjbgAfMZDnGMCSToAiElvv/AP4J7fsS+LPh5q15+1H+0q76h8R/Em+ZUuSHksI5/vFj0E8g4YDiNPkH8VfeZJkVTnlhYfxpK02tqUHur7c8trdFfzt9dxdxjQ9lDMKi/wBlpvmpRejxFVfDK26o03713bmla2yv90/sw/ArRv2bvgboHwh0crK2mQZup1GPPu5Tvmk9fmcnGei4HavfKKK/YaFGFGnGlTVoxSSXkj+WMXi6uKr1MTXlec25N923dsKKKK1OcKKKKAILr/j2k/3T/KuOrsbr/j2k/wB0/wAq46gD/9T+/iiiigAooooAKKKKACiiigD87P2wf+Ccnwm/ahmbxvosh8K+NY8NHq1onyzOn3ftEYK7yMcSKVkX1IAFfnT4m8f/ALdv7L+gyfDn9rjwFb/GLwFD8q3ssf2srGOjfaAjspA6GeMMOzV/RTRXz+N4eo1akq+Hm6VR7uNrS/xRekvzPuMo45xOGoQweOpRxFCPwqd1KH/XuorSh8m0uiPwz0L/AIKEf8E3vi6miH4saHd6Xc6B5gs4tWs3vYIPNILAGFpA65UcSLxjgCvtS1/4KT/sLWVlHFZePrCGCJAqRJa3K7VHQBRFxj0xXv8A48/Zc/Zx+J0z3Xj3wPoupzyHLTS2cfnE+8iqH/WvGP8Ah23+w953n/8ACu9PznOPMn2/98+bj9K5oYTOqMpSpyoyb3k4yjJ2015Xqac/BNSbrPD4mlKW6hKlJf8AgUkpP5n5zfta/tof8Ex/jPq+k+IPHelan491HQlljtI7KGWyikWUqSkryNCzJlcgc4JPHNcZ4V+Iv7c37TGgJ8N/2Ovh7bfB7wHN8pvoo/shMZ4LfaSiMxx1MERf/ar9sPAn7LH7N3wxmS68B+BtF02eM5WaOzjMwI9JGBf9a98AAGBWSyDF16kquKrqPN8Xso8rfrN3lY9SXG+WYPDww2W4SdRQ+B4io5xjre6pRtTvfW+up+cv7H//AATg+FX7MdynjzxHMfFnjeTLvqt2vyQO/wB77OjFtpOeZGLSH1AOK/Rqiivo8FgaGEpKjh4KMV/V33fmz4LNs5xuZ4h4rHVXOb6vouyWyS6JJIKKKK6zzAooooAKKKKAILr/AI9pP90/yrjq7G6/49pP90/yrjqAP//Z"), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true), audioPlayer: .constant(nil), playbackState: .constant(.noPlayback), playbackTime: .constant(nil)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .directSnd, .now, "hello", .sndSent(sndProgress: .complete), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directSnd), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "https://simplex.chat", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "hi", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directSnd, .now, "👍", .sndSent(sndProgress: .complete), quotedItem: CIQuote.getSample(1, .now, "Hello too", chatDir: .directRcv), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this covers -", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "hello there too!!! this text has the time on the same line ", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "https://simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(2, .directRcv, .now, "chaT@simplex.chat", .rcvRead, itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello", quotedItem: CIQuote.getSample(1, .now, "hi there hello hello hello ther hello hello", chatDir: .directSnd, image: "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAuKADAAQAAAABAAAAYAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgAYAC4AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQADP/aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKK+CP2vP+ChXwZ/ZPibw7dMfEHi2VAYdGs3G9N33TO/IiU9hgu3ZSOa/NzXNL/4KJ/td6JJ49+NXiq2+Cvw7kG/ZNKbDMLcjKblmfI/57SRqewrwMdxBRo1HQoRdWqt1HaP+KT0j838j7XKOCMXiqEcbjKkcPh5bSne8/wDr3BXlN+is+5+43jb45/Bf4bs0fj/xZpGjSL1jvL2KF/8AvlmDfpXjH/DfH7GQuPsv/CydD35x/wAfIx+fT9a/AO58D/8ABJj4UzvF4v8AFfif4l6mp/evpkfkWzP3w2Isg+omb61X/wCF0/8ABJr/AI9f+FQeJPL6ed9vbzPrj7ZivnavFuIT+KhHyc5Sf3wjY+7w/hlgZQv7PF1P70aUKa+SqTUvwP6afBXx2+CnxIZYvAHi3R9ZkfpHZ3sUz/8AfKsW/SvVq/lItvBf/BJX4rTLF4V8UeJ/hpqTH91JqUfn2yv2y2JcD3MqfUV9OaFon/BRH9krQ4vH3wI8XW3xq+HkY3+XDKb/ABCvJxHuaZMDr5Ergd1ruwvFNVrmq0VOK3lSkp29Y6SS+R5GY+HGGi1DD4qVKo9oYmm6XN5RqK9Nvsro/obor4A/ZC/4KH/Bv9qxV8MLnw54vjU+bo9443SFPvG3k4EoHdcB17rjmvv+vqcHjaGKpKth5qUX1X9aPyZ+b5rlOMy3ESwmOpOFRdH+aezT6NXTCiiiuo84KKKKACiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/Q/v4ooooAKKKKACiiigAr8tf+ChP7cWs/BEWfwD+A8R1P4k+JQkUCQr5rWUc52o+zndNIf9Up4H324wD9x/tDfGjw/wDs9fBnX/i/4jAeHRrZpI4c4M87YWKIe7yFV9gc9q/n6+B3iOb4GfCLxL/wU1+Oypq3jzxndT2nhK2uBwZptyvcBeoQBSq4xthjwPvivluIs0lSthKM+WUk5Sl/JBbtebekfM/R+BOHaeIcszxVL2kISUKdP/n7WlrGL/uxXvT8u6uizc6b8I/+CbmmRePPi9HD8Q/j7rifbktLmTz7bSGm582ZzktITyX++5+5tX5z5L8LPgv+0X/wVH12+8ZfEbxneW/2SRxB9o02eTSosdY4XRlgjYZGV++e5Jr8xvF3i7xN4+8UX/jXxney6jquqTNcXVzMcvJI5ySfQdgBwBgDgV+sP/BPX9jj9oL9oXw9H4tuvG2s+DfAVlM8VsthcyJLdSBsyCBNwREDZ3SEHLcBTgkfmuX4j+0MXHB06LdBXagna/8AenK6u+7el9Ej9+zvA/2Jls81r4uMcY7J1px5lHf93ShaVo9FFJNq8pMyPil/wRs/aj8D6dLq3gq70vxdHECxgtZGtrogf3UmAQn2EmT2r8rPEPh3xB4R1u58M+KrGfTdRsnMdxa3MbRTROOzKwBBr+674VfCnTfhNoI0DTtX1jWFAGZtYvpL2U4934X/AICAK8V/aW/Yf/Z9/areHUvibpkkerWsRhg1KxkMFyqHkBiMrIAeQJFYDJxjJr6bNPD+nOkqmAfLP+WTuvk7XX4/I/PeHvG6tSxDo5zH2lLpUhHll6uN7NelmvPY/iir2T4KftA/GD9njxMvir4Q65caTPkGWFTutrgD+GaE/I4+oyOxB5r2n9tb9jTxj+x18RYvD+pTtqmgaqrS6VqezZ5qpjfHIBwsseRuA4IIYdcD4yr80q0sRgcQ4SvCpB+jT8mvzP6Bw2JwOcYGNany1aFRdVdNdmn22aauno9T9tLO0+D/APwUr02Txd8NI4Ph38ftGT7b5NtIYLXWGh58yJwQVkBGd/8ArEP3i6fMP0R/4J7ftw6/8YZ7z9nb9oGJtN+JPhoPFIJ18p75IPlclegnj/5aKOGHzrxnH8rPhXxT4j8D+JbHxj4QvZdO1TTJkuLW5hba8UqHIIP8x0I4PFfsZ8bPEdx+0N8FvDv/AAUl+CgXSfiJ4EuYLXxZBbDALw4CXO0clMEZznMLlSf3Zr7PJM+nzyxUF+9ir1IrRVILeVtlOO+lrr5n5RxfwbRdKGXVXfDzfLRm9ZUKr+GDlq3RqP3UnfllZfy2/ptorw/9m/43aF+0X8FNA+L+gARpq1uGnhByYLlCUmiP+44IHqMHvXuFfsNGtCrTjVpu8ZJNPyZ/LWKwtXDVp4evG04Nxa7NOzX3hRRRWhzhRRRQBBdf8e0n+6f5Vx1djdf8e0n+6f5Vx1AH/9H+/iiiigAooooAKKKKAPw9/wCCvXiPWviH4q+F/wCyN4XlKT+K9TS6uQvoXFvAT7AvI3/AQe1fnF/wVO+IOnXfxx034AeDj5Xhv4ZaXb6TawKfkE7Ro0rY6bgvlofdT61+h3xNj/4Tv/gtd4Q0W/8Anh8P6THLGp6Ax21xOD/324Nfg3+0T4kufGH7QHjjxRdtukvte1GXJ9PPcKPwAAr8a4pxUpLEz6zq8n/btOK0+cpX9Uf1d4c5bCDy+lbSlh3W/wC38RNq/qoQcV5M8fjiaeRYEOGchR9TxX9svw9+GHijSvgB4I+Gnwr1ceGbGztYY728gijluhbohLLAJVeJZJJCN0jo+0Zwu4gj+JgO8REsf3l+YfUV/bf8DNVm+Mv7KtkNF1CTTZ9Z0d4Ir2D/AFls9zF8sidPmj3hhz1Fel4YyhGtiHpzWjur6e9f9Dw/H9VXQwFvgvUv62hb8Oa3zPoDwfp6aPoiaONXuNaa1Zo3ubp43nLDqrmJEXI/3QfWukmjMsTRBihYEbl6jPcZ7ivxk/4JMf8ABOv9ob9hBvFdr8ZvGOma9Yak22wttLiYGV2kMkl1dzSIkkkzcKisX8tSwDYNfs/X7Bj6NOlXlCjUU4/zJWv8j+ZsNUnOmpThyvtufj/+1Z8Hf2bPi58PviF8Avh/4wl1j4iaBZjXG0m71qfU7i3u4FMqt5VxLL5LzR70Kx7AVfJXAXH8sysGUMOh5r+vzwl+wD+y78KP2wPEX7bGn6xqFv4g8QmWa70+fUFGlrdTRmGS4EGATIY2dRvdlXe+0DPH83Nh+x58bPFev3kljpSaVYPcymGS+kEX7oudp2DL/dx/DX4Z4xZxkmCxGHxdTGRTlG0ueUU7q3S93a7S69Oh/SngTnNSjgcZhMc1CnCSlC70966dr/4U7Lq79T5Kr9MP+CWfxHsNH+P138EPF2JvDfxL0640a9gc/I0vls0Rx6kb4x/v1x3iz9hmHwV4KuPFHiLxlaWkltGzt5sBSAsBkIHL7iT0GFJJ7V8qfAnxLc+D/jd4N8V2bFJdP1vT5wR/szoT+YyK/NeD+Lcvx+Ijisuq88ackpPlklruveSvdX2ufsmavC5zlWKw9CV7xaTs1aSV4tXS1Ukmrdj9/P8Agkfrus/DD4ifFP8AY/8AEkrPJ4Z1F7y1DeiSG3mI9m2wv/wI1+5Ffhd4Ki/4Qf8A4Lb+INM0/wCSHxDpDySqOhL2cMx/8fizX7o1/RnC7ccLPDP/AJdTnBeid1+DP5M8RkqmZUselZ4ijSqv1lG0vvcWwooor6Q+BCiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/S/v4ooooAKKKKACiiigD8LfiNIfBP/BbLwpq9/wDJDr2kJHGTwCZLS4gH/j0eK/Bj9oPw7c+Evj3428M3ilZLHXtRiIPoJ3x+Ywa/fL/grnoWsfDPx98K/wBrzw5EzyeGNSS0uSvokguYQfZtsy/8CFfnB/wVP+HNho/7QFp8bvCeJvDnxK0231mznQfI0vlqsoz6kbJD/v1+M8U4WUViYW1hV5/+3akVr/4FG3qz+r/DnMYTeX1b6VcP7L/t/Dzenq4Tcl5I/M2v6yP+CR3j4eLP2XbLRZZN0uku9sRnp5bMB/45sr+Tev3u/wCCJXj7yNW8T/DyZ+C6XUak9pUw36xD865uAcV7LNFTf24tfd736Hd405d9Y4cddLWlOMvk7wf/AKUvuP6Kq/P/APaa+InjJfF8vge3lez06KONgIyVM+8ZJYjkgHIx045r9AK/Gr/gsB8UPHXwg8N+AvFfgV4oWmv7u3uTJEsiyL5SsiNkZxkMeCDmvU8bsgzPN+Fa+FyrEujUUot6tKcdnBtapO6fny2ejZ/OnAOFWJzqjheVOU+ZK+yaTlfr2t8z85td/b18H6D4n1DQLrw5fSLY3Elv5okRWcxsVJKMAVyR0yTivEPHf7f3jjVFe18BaXb6PGeBPcH7RN9QMBAfqGrFP7UPwj8c3f2/4y/DuzvbxgA93ZNtd8dyGwT+Lmuvh/aP/ZT8IxC58EfD0y3Y5UzwxKAf99mlP5Cv49wvCeBwUoc3D9Sday3qRlTb73c7Wf8Aej8j+rKWVUKLV8vlKf8AiTj/AOlW+9Hw74w8ceNvHl8NX8bajc6jK2SjTsSo/wBxeFUf7orovgf4dufF3xp8H+F7NS0uoa3p8Cgf7c6A/pW98avjx4q+NmoW0mswW9jY2G/7LaWy4WPfjJLHlicD0HoBX13/AMEtPhrZeI/2jH+L3inEPh34cWE+t31w/wBxJFRliBPqPmkH/XOv3fhXCVa/1ahUoRoybV4RacYq/dKK0jq7Ky1s3uezm+PeByeviqkFBxhK0U767RirJattLTqz9H/CMg8af8Futd1DT/ni8P6OySsOxSyiiP8A49Niv3Qr8NP+CS+j6t8V/iv8V/2wdfiZD4i1B7K0LDtLJ9olUf7imFfwr9y6/oLhe88LUxPSrUnNejdl+CP5G8RWqeY0cAnd4ejSpP8AxRjd/c5NBRRRX0h8CFFFFAEF1/x7Sf7p/lXHV2N1/wAe0n+6f5Vx1AH/0/7+KKKKACiiigAooooA8M/aT+B+iftGfBLxB8INcIjGrWxFvORnyLmMh4ZB/uSAE46jI71+AfwU8N3H7SXwL8Qf8E5fjFt0r4kfD65nuvCstycbmhz5ltuPVcE4x1idWHEdf031+UX/AAUL/Yj8T/FG/sv2mP2c5H074keGtkoFufLe+jg5Taennx9Ezw6/Ie2PleI8slUtjKUOZpOM4/zwe6X96L1j5/cfpPAXEMKF8rxNX2cZSU6VR7Uq0dE3/cmvcn5dldn8r/iXw3r/AIN8Q3vhPxXZy6fqemzPb3VtMNskUsZwysPY/n1HFfe3/BL3x/8A8IP+1bptvK+2HVbeSBvdoyso/RWH419SX8fwg/4Kc6QmleIpLfwB8f8ASI/ssiXCGC11kwfLtZSNwkGMbceZH0w6Dj88tM+HvxW/ZK/aO8OQ/FvR7nQ7uw1OElpV/czQs+x2ilGUkUqTypPvivy3DYWWX46hjaT56HOrSXa+ql/LK26fy0P6LzDMYZ3lGMynEx9ni/ZyvTfV2bjKD+3BtJqS9HZn9gnxB/aM+Cvwp8XWXgj4ja/Bo+o6hB9ogW5DrG0ZYoCZNvlr8wI+Zh0r48/4KkfDey+NP7GOqeIPDUsV7L4elh1u0khYOskcOVl2MCQcwu5GDyRXwx/wVBnbVPH3gjxGeVvPDwUt2LxzOW/9Cr87tO8PfFXVdPisbDS9avNImbzLNILa4mtXfo5j2KULZwDjmvqs+4srKvi8rqYfnjays2nqlq9JX3v0P4FwfiDisjzqNanQU3RnGUbNq9rOz0ej207nxZovhrV9enMNhHwpwztwq/U+vt1qrrWlT6JqUumXBDNHj5l6EEZr7U+IHhHxF8JvEUHhL4j2Umiald2sV/Hb3Q8t2hnztbB75BDKfmVgQQCK8e0f4N/E349/FRvBvwh0a41y+YRq/kD91ECPvSyHCRqPVmFfl8aNZ1vYcj59rWd79rbn9T+HPjFnnEPE1WhmmEWEwKw8qkVJNbSppTdSSimmpO1ko2a3aueH+H/D+ueLNds/DHhi0lv9R1CZLe2toV3SSyyHCqoHUk1+yfxl8N3X7Ln7P+h/8E9/hOF1X4nfEm4gufFDWp3FBMR5dqGHRTgLzx5au5wJKtaZZ/B7/gmFpBhsJLbx78fdVi+zwQWyma00UzjbgAfMZDnGMCSToAiElvv/AP4J7fsS+LPh5q15+1H+0q76h8R/Em+ZUuSHksI5/vFj0E8g4YDiNPkH8VfeZJkVTnlhYfxpK02tqUHur7c8trdFfzt9dxdxjQ9lDMKi/wBlpvmpRejxFVfDK26o03713bmla2yv90/sw/ArRv2bvgboHwh0crK2mQZup1GPPu5Tvmk9fmcnGei4HavfKKK/YaFGFGnGlTVoxSSXkj+WMXi6uKr1MTXlec25N923dsKKKK1OcKKKKAILr/j2k/3T/KuOrsbr/j2k/wB0/wAq46gD/9T+/iiiigAooooAKKKKACiiigD87P2wf+Ccnwm/ahmbxvosh8K+NY8NHq1onyzOn3ftEYK7yMcSKVkX1IAFfnT4m8f/ALdv7L+gyfDn9rjwFb/GLwFD8q3ssf2srGOjfaAjspA6GeMMOzV/RTRXz+N4eo1akq+Hm6VR7uNrS/xRekvzPuMo45xOGoQweOpRxFCPwqd1KH/XuorSh8m0uiPwz0L/AIKEf8E3vi6miH4saHd6Xc6B5gs4tWs3vYIPNILAGFpA65UcSLxjgCvtS1/4KT/sLWVlHFZePrCGCJAqRJa3K7VHQBRFxj0xXv8A48/Zc/Zx+J0z3Xj3wPoupzyHLTS2cfnE+8iqH/WvGP8Ah23+w953n/8ACu9PznOPMn2/98+bj9K5oYTOqMpSpyoyb3k4yjJ2015Xqac/BNSbrPD4mlKW6hKlJf8AgUkpP5n5zfta/tof8Ex/jPq+k+IPHelan491HQlljtI7KGWyikWUqSkryNCzJlcgc4JPHNcZ4V+Iv7c37TGgJ8N/2Ovh7bfB7wHN8pvoo/shMZ4LfaSiMxx1MERf/ar9sPAn7LH7N3wxmS68B+BtF02eM5WaOzjMwI9JGBf9a98AAGBWSyDF16kquKrqPN8Xso8rfrN3lY9SXG+WYPDww2W4SdRQ+B4io5xjre6pRtTvfW+up+cv7H//AATg+FX7MdynjzxHMfFnjeTLvqt2vyQO/wB77OjFtpOeZGLSH1AOK/Rqiivo8FgaGEpKjh4KMV/V33fmz4LNs5xuZ4h4rHVXOb6vouyWyS6JJIKKKK6zzAooooAKKKKAILr/AI9pP90/yrjq7G6/49pP90/yrjqAP//Z"), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) + FramedItemView(chat: Chat.sampleData, chatItem: ChatItem.getSample(1, .groupRcv(groupMember: GroupMember.sampleData), .now, "hello there this is a long text", quotedItem: CIQuote.getSample(1, .now, "hi there", chatDir: .directSnd, image: "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAuKADAAQAAAABAAAAYAAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgAYAC4AwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQADP/aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKK+CP2vP+ChXwZ/ZPibw7dMfEHi2VAYdGs3G9N33TO/IiU9hgu3ZSOa/NzXNL/4KJ/td6JJ49+NXiq2+Cvw7kG/ZNKbDMLcjKblmfI/57SRqewrwMdxBRo1HQoRdWqt1HaP+KT0j838j7XKOCMXiqEcbjKkcPh5bSne8/wDr3BXlN+is+5+43jb45/Bf4bs0fj/xZpGjSL1jvL2KF/8AvlmDfpXjH/DfH7GQuPsv/CydD35x/wAfIx+fT9a/AO58D/8ABJj4UzvF4v8AFfif4l6mp/evpkfkWzP3w2Isg+omb61X/wCF0/8ABJr/AI9f+FQeJPL6ed9vbzPrj7ZivnavFuIT+KhHyc5Sf3wjY+7w/hlgZQv7PF1P70aUKa+SqTUvwP6afBXx2+CnxIZYvAHi3R9ZkfpHZ3sUz/8AfKsW/SvVq/lItvBf/BJX4rTLF4V8UeJ/hpqTH91JqUfn2yv2y2JcD3MqfUV9OaFon/BRH9krQ4vH3wI8XW3xq+HkY3+XDKb/ABCvJxHuaZMDr5Ergd1ruwvFNVrmq0VOK3lSkp29Y6SS+R5GY+HGGi1DD4qVKo9oYmm6XN5RqK9Nvsro/obor4A/ZC/4KH/Bv9qxV8MLnw54vjU+bo9443SFPvG3k4EoHdcB17rjmvv+vqcHjaGKpKth5qUX1X9aPyZ+b5rlOMy3ESwmOpOFRdH+aezT6NXTCiiiuo84KKKKACiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/Q/v4ooooAKKKKACiiigAr8tf+ChP7cWs/BEWfwD+A8R1P4k+JQkUCQr5rWUc52o+zndNIf9Up4H324wD9x/tDfGjw/wDs9fBnX/i/4jAeHRrZpI4c4M87YWKIe7yFV9gc9q/n6+B3iOb4GfCLxL/wU1+Oypq3jzxndT2nhK2uBwZptyvcBeoQBSq4xthjwPvivluIs0lSthKM+WUk5Sl/JBbtebekfM/R+BOHaeIcszxVL2kISUKdP/n7WlrGL/uxXvT8u6uizc6b8I/+CbmmRePPi9HD8Q/j7rifbktLmTz7bSGm582ZzktITyX++5+5tX5z5L8LPgv+0X/wVH12+8ZfEbxneW/2SRxB9o02eTSosdY4XRlgjYZGV++e5Jr8xvF3i7xN4+8UX/jXxney6jquqTNcXVzMcvJI5ySfQdgBwBgDgV+sP/BPX9jj9oL9oXw9H4tuvG2s+DfAVlM8VsthcyJLdSBsyCBNwREDZ3SEHLcBTgkfmuX4j+0MXHB06LdBXagna/8AenK6u+7el9Ej9+zvA/2Jls81r4uMcY7J1px5lHf93ShaVo9FFJNq8pMyPil/wRs/aj8D6dLq3gq70vxdHECxgtZGtrogf3UmAQn2EmT2r8rPEPh3xB4R1u58M+KrGfTdRsnMdxa3MbRTROOzKwBBr+674VfCnTfhNoI0DTtX1jWFAGZtYvpL2U4934X/AICAK8V/aW/Yf/Z9/areHUvibpkkerWsRhg1KxkMFyqHkBiMrIAeQJFYDJxjJr6bNPD+nOkqmAfLP+WTuvk7XX4/I/PeHvG6tSxDo5zH2lLpUhHll6uN7NelmvPY/iir2T4KftA/GD9njxMvir4Q65caTPkGWFTutrgD+GaE/I4+oyOxB5r2n9tb9jTxj+x18RYvD+pTtqmgaqrS6VqezZ5qpjfHIBwsseRuA4IIYdcD4yr80q0sRgcQ4SvCpB+jT8mvzP6Bw2JwOcYGNany1aFRdVdNdmn22aauno9T9tLO0+D/APwUr02Txd8NI4Ph38ftGT7b5NtIYLXWGh58yJwQVkBGd/8ArEP3i6fMP0R/4J7ftw6/8YZ7z9nb9oGJtN+JPhoPFIJ18p75IPlclegnj/5aKOGHzrxnH8rPhXxT4j8D+JbHxj4QvZdO1TTJkuLW5hba8UqHIIP8x0I4PFfsZ8bPEdx+0N8FvDv/AAUl+CgXSfiJ4EuYLXxZBbDALw4CXO0clMEZznMLlSf3Zr7PJM+nzyxUF+9ir1IrRVILeVtlOO+lrr5n5RxfwbRdKGXVXfDzfLRm9ZUKr+GDlq3RqP3UnfllZfy2/ptorw/9m/43aF+0X8FNA+L+gARpq1uGnhByYLlCUmiP+44IHqMHvXuFfsNGtCrTjVpu8ZJNPyZ/LWKwtXDVp4evG04Nxa7NOzX3hRRRWhzhRRRQBBdf8e0n+6f5Vx1djdf8e0n+6f5Vx1AH/9H+/iiiigAooooAKKKKAPw9/wCCvXiPWviH4q+F/wCyN4XlKT+K9TS6uQvoXFvAT7AvI3/AQe1fnF/wVO+IOnXfxx034AeDj5Xhv4ZaXb6TawKfkE7Ro0rY6bgvlofdT61+h3xNj/4Tv/gtd4Q0W/8Anh8P6THLGp6Ax21xOD/324Nfg3+0T4kufGH7QHjjxRdtukvte1GXJ9PPcKPwAAr8a4pxUpLEz6zq8n/btOK0+cpX9Uf1d4c5bCDy+lbSlh3W/wC38RNq/qoQcV5M8fjiaeRYEOGchR9TxX9svw9+GHijSvgB4I+Gnwr1ceGbGztYY728gijluhbohLLAJVeJZJJCN0jo+0Zwu4gj+JgO8REsf3l+YfUV/bf8DNVm+Mv7KtkNF1CTTZ9Z0d4Ir2D/AFls9zF8sidPmj3hhz1Fel4YyhGtiHpzWjur6e9f9Dw/H9VXQwFvgvUv62hb8Oa3zPoDwfp6aPoiaONXuNaa1Zo3ubp43nLDqrmJEXI/3QfWukmjMsTRBihYEbl6jPcZ7ivxk/4JMf8ABOv9ob9hBvFdr8ZvGOma9Yak22wttLiYGV2kMkl1dzSIkkkzcKisX8tSwDYNfs/X7Bj6NOlXlCjUU4/zJWv8j+ZsNUnOmpThyvtufj/+1Z8Hf2bPi58PviF8Avh/4wl1j4iaBZjXG0m71qfU7i3u4FMqt5VxLL5LzR70Kx7AVfJXAXH8sysGUMOh5r+vzwl+wD+y78KP2wPEX7bGn6xqFv4g8QmWa70+fUFGlrdTRmGS4EGATIY2dRvdlXe+0DPH83Nh+x58bPFev3kljpSaVYPcymGS+kEX7oudp2DL/dx/DX4Z4xZxkmCxGHxdTGRTlG0ueUU7q3S93a7S69Oh/SngTnNSjgcZhMc1CnCSlC70966dr/4U7Lq79T5Kr9MP+CWfxHsNH+P138EPF2JvDfxL0640a9gc/I0vls0Rx6kb4x/v1x3iz9hmHwV4KuPFHiLxlaWkltGzt5sBSAsBkIHL7iT0GFJJ7V8qfAnxLc+D/jd4N8V2bFJdP1vT5wR/szoT+YyK/NeD+Lcvx+Ijisuq88ackpPlklruveSvdX2ufsmavC5zlWKw9CV7xaTs1aSV4tXS1Ukmrdj9/P8Agkfrus/DD4ifFP8AY/8AEkrPJ4Z1F7y1DeiSG3mI9m2wv/wI1+5Ffhd4Ki/4Qf8A4Lb+INM0/wCSHxDpDySqOhL2cMx/8fizX7o1/RnC7ccLPDP/AJdTnBeid1+DP5M8RkqmZUselZ4ijSqv1lG0vvcWwooor6Q+BCiiigCC6/49pP8AdP8AKuOrsbr/AI9pP90/yrjqAP/S/v4ooooAKKKKACiiigD8LfiNIfBP/BbLwpq9/wDJDr2kJHGTwCZLS4gH/j0eK/Bj9oPw7c+Evj3428M3ilZLHXtRiIPoJ3x+Ywa/fL/grnoWsfDPx98K/wBrzw5EzyeGNSS0uSvokguYQfZtsy/8CFfnB/wVP+HNho/7QFp8bvCeJvDnxK0231mznQfI0vlqsoz6kbJD/v1+M8U4WUViYW1hV5/+3akVr/4FG3qz+r/DnMYTeX1b6VcP7L/t/Dzenq4Tcl5I/M2v6yP+CR3j4eLP2XbLRZZN0uku9sRnp5bMB/45sr+Tev3u/wCCJXj7yNW8T/DyZ+C6XUak9pUw36xD865uAcV7LNFTf24tfd736Hd405d9Y4cddLWlOMvk7wf/AKUvuP6Kq/P/APaa+InjJfF8vge3lez06KONgIyVM+8ZJYjkgHIx045r9AK/Gr/gsB8UPHXwg8N+AvFfgV4oWmv7u3uTJEsiyL5SsiNkZxkMeCDmvU8bsgzPN+Fa+FyrEujUUot6tKcdnBtapO6fny2ejZ/OnAOFWJzqjheVOU+ZK+yaTlfr2t8z85td/b18H6D4n1DQLrw5fSLY3Elv5okRWcxsVJKMAVyR0yTivEPHf7f3jjVFe18BaXb6PGeBPcH7RN9QMBAfqGrFP7UPwj8c3f2/4y/DuzvbxgA93ZNtd8dyGwT+Lmuvh/aP/ZT8IxC58EfD0y3Y5UzwxKAf99mlP5Cv49wvCeBwUoc3D9Sday3qRlTb73c7Wf8Aej8j+rKWVUKLV8vlKf8AiTj/AOlW+9Hw74w8ceNvHl8NX8bajc6jK2SjTsSo/wBxeFUf7orovgf4dufF3xp8H+F7NS0uoa3p8Cgf7c6A/pW98avjx4q+NmoW0mswW9jY2G/7LaWy4WPfjJLHlicD0HoBX13/AMEtPhrZeI/2jH+L3inEPh34cWE+t31w/wBxJFRliBPqPmkH/XOv3fhXCVa/1ahUoRoybV4RacYq/dKK0jq7Ky1s3uezm+PeByeviqkFBxhK0U767RirJattLTqz9H/CMg8af8Futd1DT/ni8P6OySsOxSyiiP8A49Niv3Qr8NP+CS+j6t8V/iv8V/2wdfiZD4i1B7K0LDtLJ9olUf7imFfwr9y6/oLhe88LUxPSrUnNejdl+CP5G8RWqeY0cAnd4ejSpP8AxRjd/c5NBRRRX0h8CFFFFAEF1/x7Sf7p/lXHV2N1/wAe0n+6f5Vx1AH/0/7+KKKKACiiigAooooA8M/aT+B+iftGfBLxB8INcIjGrWxFvORnyLmMh4ZB/uSAE46jI71+AfwU8N3H7SXwL8Qf8E5fjFt0r4kfD65nuvCstycbmhz5ltuPVcE4x1idWHEdf031+UX/AAUL/Yj8T/FG/sv2mP2c5H074keGtkoFufLe+jg5Taennx9Ezw6/Ie2PleI8slUtjKUOZpOM4/zwe6X96L1j5/cfpPAXEMKF8rxNX2cZSU6VR7Uq0dE3/cmvcn5dldn8r/iXw3r/AIN8Q3vhPxXZy6fqemzPb3VtMNskUsZwysPY/n1HFfe3/BL3x/8A8IP+1bptvK+2HVbeSBvdoyso/RWH419SX8fwg/4Kc6QmleIpLfwB8f8ASI/ssiXCGC11kwfLtZSNwkGMbceZH0w6Dj88tM+HvxW/ZK/aO8OQ/FvR7nQ7uw1OElpV/czQs+x2ilGUkUqTypPvivy3DYWWX46hjaT56HOrSXa+ql/LK26fy0P6LzDMYZ3lGMynEx9ni/ZyvTfV2bjKD+3BtJqS9HZn9gnxB/aM+Cvwp8XWXgj4ja/Bo+o6hB9ogW5DrG0ZYoCZNvlr8wI+Zh0r48/4KkfDey+NP7GOqeIPDUsV7L4elh1u0khYOskcOVl2MCQcwu5GDyRXwx/wVBnbVPH3gjxGeVvPDwUt2LxzOW/9Cr87tO8PfFXVdPisbDS9avNImbzLNILa4mtXfo5j2KULZwDjmvqs+4srKvi8rqYfnjays2nqlq9JX3v0P4FwfiDisjzqNanQU3RnGUbNq9rOz0ej207nxZovhrV9enMNhHwpwztwq/U+vt1qrrWlT6JqUumXBDNHj5l6EEZr7U+IHhHxF8JvEUHhL4j2Umiald2sV/Hb3Q8t2hnztbB75BDKfmVgQQCK8e0f4N/E349/FRvBvwh0a41y+YRq/kD91ECPvSyHCRqPVmFfl8aNZ1vYcj59rWd79rbn9T+HPjFnnEPE1WhmmEWEwKw8qkVJNbSppTdSSimmpO1ko2a3aueH+H/D+ueLNds/DHhi0lv9R1CZLe2toV3SSyyHCqoHUk1+yfxl8N3X7Ln7P+h/8E9/hOF1X4nfEm4gufFDWp3FBMR5dqGHRTgLzx5au5wJKtaZZ/B7/gmFpBhsJLbx78fdVi+zwQWyma00UzjbgAfMZDnGMCSToAiElvv/AP4J7fsS+LPh5q15+1H+0q76h8R/Em+ZUuSHksI5/vFj0E8g4YDiNPkH8VfeZJkVTnlhYfxpK02tqUHur7c8trdFfzt9dxdxjQ9lDMKi/wBlpvmpRejxFVfDK26o03713bmla2yv90/sw/ArRv2bvgboHwh0crK2mQZup1GPPu5Tvmk9fmcnGei4HavfKKK/YaFGFGnGlTVoxSSXkj+WMXi6uKr1MTXlec25N923dsKKKK1OcKKKKAILr/j2k/3T/KuOrsbr/j2k/wB0/wAq46gD/9T+/iiiigAooooAKKKKACiiigD87P2wf+Ccnwm/ahmbxvosh8K+NY8NHq1onyzOn3ftEYK7yMcSKVkX1IAFfnT4m8f/ALdv7L+gyfDn9rjwFb/GLwFD8q3ssf2srGOjfaAjspA6GeMMOzV/RTRXz+N4eo1akq+Hm6VR7uNrS/xRekvzPuMo45xOGoQweOpRxFCPwqd1KH/XuorSh8m0uiPwz0L/AIKEf8E3vi6miH4saHd6Xc6B5gs4tWs3vYIPNILAGFpA65UcSLxjgCvtS1/4KT/sLWVlHFZePrCGCJAqRJa3K7VHQBRFxj0xXv8A48/Zc/Zx+J0z3Xj3wPoupzyHLTS2cfnE+8iqH/WvGP8Ah23+w953n/8ACu9PznOPMn2/98+bj9K5oYTOqMpSpyoyb3k4yjJ2015Xqac/BNSbrPD4mlKW6hKlJf8AgUkpP5n5zfta/tof8Ex/jPq+k+IPHelan491HQlljtI7KGWyikWUqSkryNCzJlcgc4JPHNcZ4V+Iv7c37TGgJ8N/2Ovh7bfB7wHN8pvoo/shMZ4LfaSiMxx1MERf/ar9sPAn7LH7N3wxmS68B+BtF02eM5WaOzjMwI9JGBf9a98AAGBWSyDF16kquKrqPN8Xso8rfrN3lY9SXG+WYPDww2W4SdRQ+B4io5xjre6pRtTvfW+up+cv7H//AATg+FX7MdynjzxHMfFnjeTLvqt2vyQO/wB77OjFtpOeZGLSH1AOK/Rqiivo8FgaGEpKjh4KMV/V33fmz4LNs5xuZ4h4rHVXOb6vouyWyS6JJIKKKK6zzAooooAKKKKAILr/AI9pP90/yrjq7G6/49pP90/yrjqAP//Z"), itemDeleted: .deleted(deletedTs: .now)), revealed: Binding.constant(true), allowMenu: Binding.constant(true)) } .previewLayout(.fixed(width: 360, height: 200)) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift index 0e721acdcb..a80c5412b6 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FullScreenMediaView.swift @@ -13,12 +13,12 @@ import AVKit struct FullScreenMediaView: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var scrollModel: ReverseListScrollModel @State var chatItem: ChatItem @State var image: UIImage? @State var player: AVPlayer? = nil @State var url: URL? = nil @Binding var showView: Bool - @State var scrollProxy: ScrollViewProxy? @State private var showNext = false @State private var nextImage: UIImage? @State private var nextPlayer: AVPlayer? @@ -71,9 +71,7 @@ struct FullScreenMediaView: View { let w = abs(t.width) if t.height > 60 && t.height > w * 2 { showView = false - if let proxy = scrollProxy { - proxy.scrollTo(chatItem.viewId) - } + scrollModel.scrollToItem(id: chatItem.id) } else if w > 60 && w > abs(t.height) * 2 && !scrolling { let previous = t.width > 0 scrolling = true diff --git a/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift index 1aa0093c9a..822dda4d06 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/IntegrityErrorItemView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct IntegrityErrorItemView: View { @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme var msgError: MsgErrorType var chatItem: ChatItem @@ -54,6 +55,7 @@ struct IntegrityErrorItemView: View { struct CIMsgError: View { @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme var chatItem: ChatItem var onTap: () -> Void @@ -62,13 +64,12 @@ struct CIMsgError: View { Text(chatItem.content.text) .foregroundColor(.red) .italic() - CIMetaView(chat: chat, chatItem: chatItem) + CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary) .padding(.horizontal, 12) } .padding(.leading, 12) .padding(.vertical, 6) .background(Color(uiColor: .tertiarySystemGroupedBackground)) - .cornerRadius(18) .textSelection(.disabled) .onTapGesture(perform: onTap) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift index cb0b61f537..25e06b9ea4 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MarkedDeletedItemView.swift @@ -11,7 +11,7 @@ import SimpleXChat struct MarkedDeletedItemView: View { @EnvironmentObject var m: ChatModel - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat var chatItem: ChatItem @Binding var revealed: Bool @@ -19,11 +19,10 @@ struct MarkedDeletedItemView: View { var body: some View { (Text(mergedMarkedDeletedText).italic() + Text(" ") + chatItem.timestampText) .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .padding(.horizontal, 12) .padding(.vertical, 6) - .background(chatItemFrameColor(chatItem, colorScheme)) - .cornerRadius(18) + .background(chatItemFrameColor(chatItem, theme)) .textSelection(.disabled) } @@ -36,8 +35,8 @@ struct MarkedDeletedItemView: View { var blockedByAdmin = 0 var deleted = 0 var moderatedBy: Set = [] - while i < m.reversedChatItems.count, - let ci = .some(m.reversedChatItems[i]), + while i < ItemsModel.shared.reversedChatItems.count, + let ci = .some(ItemsModel.shared.reversedChatItems[i]), ci.mergeCategory == ciCategory, let itemDeleted = ci.meta.itemDeleted { switch itemDeleted { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index ccd7ac0a12..999f99b294 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -26,6 +26,7 @@ private func typing(_ w: Font.Weight = .light) -> Text { struct MsgContentView: View { @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme var text: String var formattedText: [FormattedText]? = nil var sender: String? = nil @@ -35,6 +36,8 @@ struct MsgContentView: View { @State private var typingIdx = 0 @State private var timer: Timer? + @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false + var body: some View { if meta?.isLive == true { msgContentView() @@ -63,7 +66,7 @@ struct MsgContentView: View { } private func msgContentView() -> Text { - var v = messageText(text, formattedText, sender, showSecrets: showSecrets) + var v = messageText(text, formattedText, sender, showSecrets: showSecrets, secondaryColor: theme.colors.secondary) if let mt = meta { if mt.isLive { v = v + typingIndicator(mt.recent) @@ -77,15 +80,15 @@ struct MsgContentView: View { return (recent ? typingIndicators[typingIdx] : noTyping) .font(.body.monospaced()) .kerning(-2) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } private func reserveSpaceForMeta(_ mt: CIMeta) -> Text { - (rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true) + (rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy) } } -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, showSecrets: Bool, secondaryColor: Color) -> Text { let s = text var res: Text if let ft = formattedText, ft.count > 0 && ft.count <= 200 { @@ -100,7 +103,7 @@ func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: St } if let i = icon { - res = Text(Image(systemName: i)).foregroundColor(Color(uiColor: .tertiaryLabel)) + Text(" ") + res + res = Text(Image(systemName: i)).foregroundColor(secondaryColor) + Text(" ") + res } if let s = sender { diff --git a/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift b/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift index 7237711a2a..32993d1a76 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemForwardingView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct ChatItemForwardingView: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss var ci: ChatItem @@ -19,6 +20,8 @@ struct ChatItemForwardingView: View { @State private var searchText: String = "" @FocusState private var searchFocused + @State private var alert: SomeAlert? + private let chatsToForwardTo = filterChatsToForwardTo(chats: ChatModel.shared.chats) var body: some View { NavigationView { @@ -35,98 +38,74 @@ struct ChatItemForwardingView: View { } } } + .modifier(ThemedBackground()) + .alert(item: $alert) { $0.alert } } @ViewBuilder private func forwardListView() -> some View { VStack(alignment: .leading) { - let chatsToForwardTo = filterChatsToForwardTo() if !chatsToForwardTo.isEmpty { - ScrollView { - LazyVStack(alignment: .leading, spacing: 8) { - searchFieldView(text: $searchText, focussed: $searchFocused) - .padding(.leading, 2) - let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase - let chats = s == "" ? chatsToForwardTo : chatsToForwardTo.filter { filterChatSearched($0, s) } - ForEach(chats) { chat in - Divider() - forwardListNavLinkView(chat) - .disabled(chatModel.deletedChats.contains(chat.chatInfo.id)) - } + List { + searchFieldView(text: $searchText, focussed: $searchFocused, theme.colors.onBackground, theme.colors.secondary) + .padding(.leading, 2) + let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase + let chats = s == "" ? chatsToForwardTo : chatsToForwardTo.filter { foundChat($0, s) } + ForEach(chats) { chat in + forwardListChatView(chat) + .disabled(chatModel.deletedChats.contains(chat.chatInfo.id)) } - .padding(.horizontal) - .padding(.vertical, 8) - .background(Color(uiColor: .systemBackground)) - .cornerRadius(12) - .padding(.horizontal) } - .background(Color(.systemGroupedBackground)) + .modifier(ThemedBackground(grouped: true)) } else { - emptyList() + ZStack { + emptyList() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .modifier(ThemedBackground()) } } } - private func filterChatsToForwardTo() -> [Chat] { - var filteredChats = chatModel.chats.filter({ canForwardToChat($0) }) - if let index = filteredChats.firstIndex(where: { $0.chatInfo.chatType == .local }) { - let privateNotes = filteredChats.remove(at: index) - filteredChats.insert(privateNotes, at: 0) - } - return filteredChats - } - - private func filterChatSearched(_ chat: Chat, _ searchStr: String) -> Bool { - let cInfo = chat.chatInfo - return switch cInfo { - case let .direct(contact): - viewNameContains(cInfo, searchStr) || - contact.profile.displayName.localizedLowercase.contains(searchStr) || - contact.fullName.localizedLowercase.contains(searchStr) - default: - viewNameContains(cInfo, searchStr) - } - - func viewNameContains(_ cInfo: ChatInfo, _ s: String) -> Bool { - cInfo.chatViewName.localizedLowercase.contains(s) - } - } - - private func canForwardToChat(_ chat: Chat) -> Bool { - switch chat.chatInfo { - case let .direct(contact): contact.sendMsgEnabled && !contact.nextSendGrpInv - case let .group(groupInfo): groupInfo.sendMsgEnabled - case let .local(noteFolder): noteFolder.sendMsgEnabled - case .contactRequest: false - case .contactConnection: false - case .invalidJSON: false - } - } - private func emptyList() -> some View { Text("No filtered chats") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .frame(maxWidth: .infinity) } - @ViewBuilder private func forwardListNavLinkView(_ chat: Chat) -> some View { + @ViewBuilder private func forwardListChatView(_ chat: Chat) -> some View { + let prohibited = chat.prohibitedByPref( + hasSimplexLink: hasSimplexLink(ci.content.msgContent?.text), + isMediaOrFileAttachment: ci.content.msgContent?.isMediaOrFileAttachment ?? false, + isVoice: ci.content.msgContent?.isVoice ?? false + ) Button { - dismiss() - if chat.id == fromChatInfo.id { - composeState = ComposeState( - message: composeState.message, - preview: composeState.linkPreview != nil ? composeState.preview : .noPreview, - contextItem: .forwardingItem(chatItem: ci, fromChatInfo: fromChatInfo) - ) + if prohibited { + alert = SomeAlert( + alert: mkAlert( + title: "Cannot forward message", + message: "Selected chat preferences prohibit this message." + ), + id: "forward prohibited by preferences" + ) } else { - composeState = ComposeState.init(forwardingItem: ci, fromChatInfo: fromChatInfo) - chatModel.chatId = chat.id + dismiss() + if chat.id == fromChatInfo.id { + composeState = ComposeState( + message: composeState.message, + preview: composeState.linkPreview != nil ? composeState.preview : .noPreview, + contextItem: .forwardingItem(chatItem: ci, fromChatInfo: fromChatInfo) + ) + } else { + composeState = ComposeState.init(forwardingItem: ci, fromChatInfo: fromChatInfo) + ItemsModel.shared.loadOpenChat(chat.id) + } } } label: { HStack { ChatInfoImage(chat: chat, size: 30) .padding(.trailing, 2) Text(chat.chatInfo.chatViewName) - .foregroundColor(.primary) + .foregroundColor(prohibited ? theme.colors.secondary : theme.colors.onBackground) .lineLimit(1) if chat.chatInfo.incognito { Spacer() @@ -134,7 +113,7 @@ struct ChatItemForwardingView: View { .resizable() .scaledToFit() .frame(width: 22, height: 22) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } } .frame(maxWidth: .infinity, alignment: .leading) @@ -147,5 +126,6 @@ struct ChatItemForwardingView: View { ci: ChatItem.getSample(1, .directSnd, .now, "hello"), fromChatInfo: .direct(contact: Contact.sampleData), composeState: Binding.constant(ComposeState(message: "hello")) - ) + ).environmentObject(CurrentColors.toAppTheme()) } + diff --git a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift index 19aa261396..f6a856dad1 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift @@ -12,11 +12,13 @@ import SimpleXChat struct ChatItemInfoView: View { @EnvironmentObject var chatModel: ChatModel @Environment(\.dismiss) var dismiss - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme var ci: ChatItem @Binding var chatItemInfo: ChatItemInfo? @State private var selection: CIInfoTab = .history @State private var alert: CIInfoViewAlert? = nil + @State private var messageStatusLimited: Bool = true + @State private var fileStatusLimited: Bool = true @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false enum CIInfoTab { @@ -99,12 +101,14 @@ struct ChatItemInfoView: View { Label("History", systemImage: "clock") } .tag(CIInfoTab.history) + .modifier(ThemedBackground()) if let qi = ci.quotedItem { quoteTab(qi) .tabItem { Label("In reply to", systemImage: "arrowshape.turn.up.left") } .tag(CIInfoTab.quote) + .modifier(ThemedBackground()) } if let forwardedFromItem = chatItemInfo?.forwardedFromChatItem { forwardedFromTab(forwardedFromItem) @@ -112,6 +116,7 @@ struct ChatItemInfoView: View { Label(local ? "Saved" : "Forwarded", systemImage: "arrowshape.turn.up.forward") } .tag(CIInfoTab.forwarded) + .modifier(ThemedBackground()) } } .onAppear { @@ -121,6 +126,7 @@ struct ChatItemInfoView: View { } } else { historyTab() + .modifier(ThemedBackground()) } } @@ -157,6 +163,35 @@ struct ChatItemInfoView: View { if developerTools { infoRow("Database ID", "\(meta.itemId)") infoRow("Record updated at", localTimestamp(meta.updatedAt)) + let msv = infoRow("Message status", ci.meta.itemStatus.id) + Group { + if messageStatusLimited { + msv.lineLimit(1) + } else { + msv + } + } + .onTapGesture { + withAnimation { + messageStatusLimited.toggle() + } + } + + if let file = ci.file { + let fsv = infoRow("File status", file.fileStatus.id) + Group { + if fileStatusLimited { + fsv.lineLimit(1) + } else { + fsv + } + } + .onTapGesture { + withAnimation { + fileStatusLimited.toggle() + } + } + } } } } @@ -181,7 +216,7 @@ struct ChatItemInfoView: View { } else { Text("No history") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .frame(maxWidth: .infinity) } } @@ -196,8 +231,8 @@ struct ChatItemInfoView: View { textBubble(itemVersion.msgContent.text, itemVersion.formattedText, nil) .padding(.horizontal, 12) .padding(.vertical, 6) - .background(chatItemFrameColor(ci, colorScheme)) - .cornerRadius(18) + .background(chatItemFrameColor(ci, theme)) + .modifier(ChatItemClipped()) .contextMenu { if itemVersion.msgContent.text != "" { Button { @@ -227,18 +262,19 @@ struct ChatItemInfoView: View { } else { Text("no text") .italic() - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } } private struct TextBubble: View { + @EnvironmentObject var theme: AppTheme 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)) + toggleSecrets(formattedText, $showSecrets, messageText(text, formattedText, sender, showSecrets: showSecrets, secondaryColor: theme.colors.secondary)) } } @@ -265,8 +301,8 @@ struct ChatItemInfoView: View { textBubble(qi.text, qi.formattedText, qi.getSender(nil)) .padding(.horizontal, 12) .padding(.vertical, 6) - .background(quotedMsgFrameColor(qi, colorScheme)) - .cornerRadius(18) + .background(quotedMsgFrameColor(qi, theme)) + .modifier(ChatItemClipped()) .contextMenu { if qi.text != "" { Button { @@ -289,10 +325,10 @@ struct ChatItemInfoView: View { .frame(maxWidth: maxWidth, alignment: .leading) } - func quotedMsgFrameColor(_ qi: CIQuote, _ colorScheme: ColorScheme) -> Color { + func quotedMsgFrameColor(_ qi: CIQuote, _ theme: AppTheme) -> Color { (qi.chatDir?.sent ?? false) - ? (colorScheme == .light ? sentColorLight : sentColorDark) - : Color(uiColor: .tertiarySystemGroupedBackground) + ? theme.appColors.sentMessage + : theme.appColors.receivedMessage } @ViewBuilder private func forwardedFromTab(_ forwardedFromItem: AChatItem) -> some View { @@ -315,7 +351,7 @@ struct ChatItemInfoView: View { Button { Task { await MainActor.run { - chatModel.chatId = forwardedFromItem.chatInfo.id + ItemsModel.shared.loadOpenChat(forwardedFromItem.chatInfo.id) dismiss() } } @@ -327,7 +363,7 @@ struct ChatItemInfoView: View { Divider().padding(.top, 32) Text("Recipient(s) can't see who this message is from.") .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } } } @@ -341,23 +377,23 @@ struct ChatItemInfoView: View { VStack(alignment: .leading) { Text("you") .italic() - .foregroundColor(.primary) + .foregroundColor(theme.colors.onBackground) Text(forwardedFromItem.chatInfo.chatViewName) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .lineLimit(1) } } else if case let .groupRcv(groupMember) = forwardedFromItem.chatItem.chatDir { VStack(alignment: .leading) { Text(groupMember.chatViewName) - .foregroundColor(.primary) + .foregroundColor(theme.colors.onBackground) .lineLimit(1) Text(forwardedFromItem.chatInfo.chatViewName) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .lineLimit(1) } } else { Text(forwardedFromItem.chatInfo.chatViewName) - .foregroundColor(.primary) + .foregroundColor(theme.colors.onBackground) .lineLimit(1) } } @@ -379,54 +415,54 @@ struct ChatItemInfoView: View { } @ViewBuilder private func memberDeliveryStatusesView(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> some View { - VStack(alignment: .leading, spacing: 12) { + LazyVStack(alignment: .leading, spacing: 12) { let mss = membersStatuses(memberDeliveryStatuses) if !mss.isEmpty { ForEach(mss, id: \.0.groupMemberId) { memberStatus in - memberDeliveryStatusView(memberStatus.0, memberStatus.1) + memberDeliveryStatusView(memberStatus.0, memberStatus.1, memberStatus.2) } } else { Text("No delivery information") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } } } - private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, CIStatus)] { + private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, GroupSndStatus, Bool?)] { memberDeliveryStatuses.compactMap({ mds in if let mem = chatModel.getGroupMember(mds.groupMemberId) { - return (mem.wrapped, mds.memberDeliveryStatus) + return (mem.wrapped, mds.memberDeliveryStatus, mds.sentViaProxy) } else { return nil } }) } - private func memberDeliveryStatusView(_ member: GroupMember, _ status: CIStatus) -> some View { + private func memberDeliveryStatusView(_ member: GroupMember, _ status: GroupSndStatus, _ sentViaProxy: Bool?) -> some View { HStack{ - ProfileImage(imageStr: member.image, size: 30) + MemberProfileImage(member, size: 30) .padding(.trailing, 2) Text(member.chatViewName) .lineLimit(1) Spacer() + if sentViaProxy == true { + Image(systemName: "arrow.forward") + .foregroundColor(theme.colors.secondary).opacity(0.67) + } let v = Group { - if let (icon, statusColor) = status.statusIcon(Color.secondary) { - switch status { - case .sndRcvd: - ZStack(alignment: .trailing) { - Image(systemName: icon) - .foregroundColor(statusColor.opacity(0.67)) - .padding(.trailing, 6) - Image(systemName: icon) - .foregroundColor(statusColor.opacity(0.67)) - } - default: + let (icon, statusColor) = status.statusIcon(theme.colors.secondary, theme.colors.primary) + switch status { + case .rcvd: + ZStack(alignment: .trailing) { Image(systemName: icon) - .foregroundColor(statusColor) + .foregroundColor(statusColor.opacity(0.67)) + .padding(.trailing, 6) + Image(systemName: icon) + .foregroundColor(statusColor.opacity(0.67)) } - } else { - Image(systemName: "ellipsis") - .foregroundColor(Color.secondary) + default: + Image(systemName: icon) + .foregroundColor(statusColor) } } @@ -469,8 +505,12 @@ struct ChatItemInfoView: View { if developerTools { shareText += [ String.localizedStringWithFormat(NSLocalizedString("Database ID: %d", comment: "copied message info"), meta.itemId), - String.localizedStringWithFormat(NSLocalizedString("Record updated at: %@", comment: "copied message info"), localTimestamp(meta.updatedAt)) + String.localizedStringWithFormat(NSLocalizedString("Record updated at: %@", comment: "copied message info"), localTimestamp(meta.updatedAt)), + String.localizedStringWithFormat(NSLocalizedString("Message status: %@", comment: "copied message info"), meta.itemStatus.id) ] + if let file = ci.file { + shareText += [String.localizedStringWithFormat(NSLocalizedString("File status: %@", comment: "copied message info"), file.fileStatus.id)] + } } if let qi = ci.quotedItem { shareText += ["", NSLocalizedString("## In reply to", comment: "copied message info")] diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index d580fb5f3e..870fe30108 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -11,35 +11,25 @@ import SimpleXChat struct ChatItemView: View { @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme var chatItem: ChatItem var maxWidth: CGFloat = .infinity - @State var scrollProxy: ScrollViewProxy? = nil @Binding var revealed: Bool @Binding var allowMenu: Bool - @Binding var audioPlayer: AudioPlayer? - @Binding var playbackState: VoiceMessagePlaybackState - @Binding var playbackTime: TimeInterval? + init( chat: Chat, chatItem: ChatItem, showMember: Bool = false, maxWidth: CGFloat = .infinity, - scrollProxy: ScrollViewProxy? = nil, revealed: Binding, - allowMenu: Binding = .constant(false), - audioPlayer: Binding = .constant(nil), - playbackState: Binding = .constant(.noPlayback), - playbackTime: Binding = .constant(nil) + allowMenu: Binding = .constant(false) ) { self.chat = chat self.chatItem = chatItem self.maxWidth = maxWidth - _scrollProxy = .init(initialValue: scrollProxy) _revealed = revealed _allowMenu = allowMenu - _audioPlayer = audioPlayer - _playbackState = playbackState - _playbackTime = playbackTime } var body: some View { @@ -50,7 +40,7 @@ struct ChatItemView: View { if let mc = ci.content.msgContent, mc.isText && isShortEmoji(ci.content.text) { EmojiItemView(chat: chat, chatItem: ci) } else if ci.content.text.isEmpty, case let .voice(_, duration) = ci.content.msgContent { - CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime, allowMenu: $allowMenu) + CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, allowMenu: $allowMenu) } else if ci.content.msgContent == nil { ChatItemContentView(chat: chat, chatItem: chatItem, revealed: $revealed, msgContentView: { Text(ci.text) }) // msgContent is unreachable branch in this case } else { @@ -62,12 +52,38 @@ struct ChatItemView: View { } private func framedItemView() -> some View { - FramedItemView(chat: chat, chatItem: chatItem, revealed: $revealed, maxWidth: maxWidth, scrollProxy: scrollProxy, allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime) + let preview = chatItem.content.msgContent + .flatMap { + switch $0 { + case let .image(_, image): image + case let .video(_, image, _): image + default: nil + } + } + .flatMap { UIImage(base64Encoded: $0) } + let adjustedMaxWidth = { + if let preview, preview.size.width <= preview.size.height { + maxWidth * 0.75 + } else { + maxWidth + } + }() + return FramedItemView( + chat: chat, + chatItem: chatItem, + preview: preview, + revealed: $revealed, + maxWidth: maxWidth, + imgWidth: adjustedMaxWidth, + videoWidth: adjustedMaxWidth, + allowMenu: $allowMenu + ) } } struct ChatItemContentView: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat var chatItem: ChatItem @Binding var revealed: Bool @@ -97,14 +113,14 @@ struct ChatItemContentView: View { case .sndGroupEvent: eventItemView() case .rcvConnEvent: eventItemView() case .sndConnEvent: eventItemView() - case let .rcvChatFeature(feature, enabled, _): chatFeatureView(feature, enabled.iconColor) - case let .sndChatFeature(feature, enabled, _): chatFeatureView(feature, enabled.iconColor) + case let .rcvChatFeature(feature, enabled, _): chatFeatureView(feature, enabled.iconColor(theme.colors.secondary)) + case let .sndChatFeature(feature, enabled, _): chatFeatureView(feature, enabled.iconColor(theme.colors.secondary)) case let .rcvChatPreference(feature, allowed, param): CIFeaturePreferenceView(chat: chat, chatItem: chatItem, feature: feature, allowed: allowed, param: param) case let .sndChatPreference(feature, _, _): - CIChatFeatureView(chat: chat, chatItem: chatItem, revealed: $revealed, feature: feature, icon: feature.icon, iconColor: .secondary) - case let .rcvGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor) - case let .sndGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor) + CIChatFeatureView(chat: chat, chatItem: chatItem, revealed: $revealed, feature: feature, icon: feature.icon, iconColor: theme.colors.secondary) + case let .rcvGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary)) + case let .sndGroupFeature(feature, preference, _, role): chatFeatureView(feature, preference.enabled(role, for: chat.chatInfo.groupInfo?.membership).iconColor(theme.colors.secondary)) case let .rcvChatFeatureRejected(feature): chatFeatureView(feature, .red) case let .rcvGroupFeatureRejected(feature): chatFeatureView(feature, .red) case .sndModerated: deletedItemView() @@ -131,20 +147,20 @@ struct ChatItemContentView: View { } private func eventItemView() -> some View { - return CIEventView(eventText: eventItemViewText()) + CIEventView(eventText: eventItemViewText(theme.colors.secondary)) } - private func eventItemViewText() -> Text { + private func eventItemViewText(_ secondaryColor: Color) -> Text { if !revealed, let t = mergedGroupEventText { - return chatEventText(t + Text(" ") + chatItem.timestampText) + return chatEventText(t + Text(" ") + chatItem.timestampText, secondaryColor) } else if let member = chatItem.memberDisplayName { return Text(member + " ") .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(secondaryColor) .fontWeight(.light) - + chatEventText(chatItem) + + chatEventText(chatItem, secondaryColor) } else { - return chatEventText(chatItem) + return chatEventText(chatItem, secondaryColor) } } @@ -179,7 +195,7 @@ struct ChatItemContentView: View { info.pqEnabled ? Text("Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery.") .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .fontWeight(.light) : e2eeInfoNoPQText() } @@ -187,24 +203,24 @@ struct ChatItemContentView: View { private func e2eeInfoNoPQText() -> Text { Text("Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery.") .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .fontWeight(.light) } } -func chatEventText(_ text: Text) -> Text { +func chatEventText(_ text: Text, _ secondaryColor: Color) -> Text { text .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(secondaryColor) .fontWeight(.light) } -func chatEventText(_ eventText: LocalizedStringKey, _ ts: Text) -> Text { - chatEventText(Text(eventText) + Text(" ") + ts) +func chatEventText(_ eventText: LocalizedStringKey, _ ts: Text, _ secondaryColor: Color) -> Text { + chatEventText(Text(eventText) + Text(" ") + ts, secondaryColor) } -func chatEventText(_ ci: ChatItem) -> Text { - chatEventText("\(ci.content.text)", ci.timestampText) +func chatEventText(_ ci: ChatItem, _ secondaryColor: Color) -> Text { + chatEventText("\(ci.content.text)", ci.timestampText, secondaryColor) } struct ChatItemView_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 4055ca2b28..655dd8aaed 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -9,16 +9,21 @@ import SwiftUI import SimpleXChat import SwiftyGif +import Combine private let memberImageSize: CGFloat = 34 struct ChatView: View { @EnvironmentObject var chatModel: ChatModel - @Environment(\.colorScheme) var colorScheme + @ObservedObject var im = ItemsModel.shared + @State var theme: AppTheme = buildTheme() @Environment(\.dismiss) var dismiss + @Environment(\.colorScheme) var colorScheme @Environment(\.presentationMode) var presentationMode @Environment(\.scenePhase) var scenePhase @State @ObservedObject var chat: Chat + @StateObject private var scrollModel = ReverseListScrollModel() + @StateObject private var floatingButtonModel = FloatingButtonModel() @State private var showChatInfoSheet: Bool = false @State private var showAddMembersSheet: Bool = false @State private var composeState = ComposeState() @@ -26,125 +31,187 @@ struct ChatView: View { @State private var connectionStats: ConnectionStats? @State private var customUserProfile: Profile? @State private var connectionCode: String? - @State private var tableView: UITableView? @State private var loadingItems = false @State private var firstPage = false - @State private var itemsInView: Set = [] - @State private var scrollProxy: ScrollViewProxy? + @State private var revealedChatItem: ChatItem? @State private var searchMode = false @State private var searchText: String = "" @FocusState private var searchFocussed // opening GroupMemberInfoView on member icon - @State private var membersLoaded = false @State private var selectedMember: GMember? = nil // opening GroupLinkView on link button (incognito) @State private var showGroupLinkSheet: Bool = false @State private var groupLink: String? @State private var groupLinkMemberRole: GroupMemberRole = .member + @State private var selectedChatItems: Set? = nil + @State private var showDeleteSelectedMessages: Bool = false + @State private var allowToDeleteSelectedMessagesForAll: Bool = false + + @AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial var body: some View { if #available(iOS 16.0, *) { viewBody - .scrollDismissesKeyboard(.immediately) - .keyboardPadding() + .scrollDismissesKeyboard(.immediately) + .toolbarBackground(.hidden, for: .navigationBar) } else { viewBody } } + @ViewBuilder private var viewBody: some View { let cInfo = chat.chatInfo - return VStack(spacing: 0) { - if searchMode { - searchToolbar() - Divider() + ZStack { + let wallpaperImage = theme.wallpaper.type.image + let wallpaperType = theme.wallpaper.type + let backgroundColor = theme.wallpaper.background ?? wallpaperType.defaultBackgroundColor(theme.base, theme.colors.background) + let tintColor = theme.wallpaper.tint ?? wallpaperType.defaultTintColor(theme.base) + Color.clear.ignoresSafeArea(.all) + .if(wallpaperImage != nil) { view in + view.modifier( + ChatViewBackground(image: wallpaperImage!, imageType: wallpaperType, background: backgroundColor, tint: tintColor) + ) } - ZStack(alignment: .trailing) { - chatItemsList() - if let proxy = scrollProxy { - floatingButtons(proxy) + VStack(spacing: 0) { + ZStack(alignment: .bottomTrailing) { + chatItemsList() + floatingButtons(counts: floatingButtonModel.unreadChatItemCounts) + } + connectingText() + if selectedChatItems == nil { + ComposeView( + chat: chat, + composeState: $composeState, + keyboardVisible: $keyboardVisible + ) + .disabled(!cInfo.sendMsgEnabled) + } else { + SelectedItemsBottomToolbar( + chatItems: ItemsModel.shared.reversedChatItems, + selectedChatItems: $selectedChatItems, + chatInfo: chat.chatInfo, + deleteItems: { forAll in + allowToDeleteSelectedMessagesForAll = forAll + showDeleteSelectedMessages = true + }, + moderateItems: { + if case let .group(groupInfo) = chat.chatInfo { + showModerateSelectedMessagesAlert(groupInfo) + } + } + ) } } - - Spacer(minLength: 0) - - connectingText() - ComposeView( - chat: chat, - composeState: $composeState, - keyboardVisible: $keyboardVisible - ) - .disabled(!cInfo.sendMsgEnabled) } - .padding(.top, 1) + .safeAreaInset(edge: .top) { + VStack(spacing: .zero) { + if searchMode { searchToolbar() } + Divider() + } + .background(ToolbarMaterial.material(toolbarMaterial)) + } .navigationTitle(cInfo.chatViewName) + .background(theme.colors.background) .navigationBarTitleDisplayMode(.inline) + .environmentObject(theme) + .confirmationDialog(selectedChatItems?.count == 1 ? "Delete message?" : "Delete \((selectedChatItems?.count ?? 0)) messages?", isPresented: $showDeleteSelectedMessages, titleVisibility: .visible) { + Button("Delete for me", role: .destructive) { + if let selected = selectedChatItems { + deleteMessages(chat, selected.sorted(), .cidmInternal, moderate: false, deletedSelectedMessages) } + } + if allowToDeleteSelectedMessagesForAll { + Button(broadcastDeleteButtonText(chat), role: .destructive) { + if let selected = selectedChatItems { + allowToDeleteSelectedMessagesForAll = false + deleteMessages(chat, selected.sorted(), .cidmBroadcast, moderate: false, deletedSelectedMessages) + } + } + } + } + .appSheet(item: $selectedMember) { member in + Group { + if case let .group(groupInfo) = chat.chatInfo { + GroupMemberInfoView(groupInfo: groupInfo, groupMember: member, navigation: true) + } + } + } .onAppear { + selectedChatItems = nil initChatView() } .onChange(of: chatModel.chatId) { cId in showChatInfoSheet = false + selectedChatItems = nil + scrollModel.scrollToBottom() + stopAudioPlayer() if let cId { if let c = chatModel.getChat(cId) { chat = c } initChatView() + theme = buildTheme() } else { dismiss() } } + .onChange(of: revealedChatItem) { _ in + NotificationCenter.postReverseListNeedsLayout() + } + .onChange(of: im.isLoading) { isLoading in + if !isLoading, + im.reversedChatItems.count <= loadItemsPerPage, + filtered(im.reversedChatItems).count < 10 { + loadChatItems(chat.chatInfo) + } + } + .environmentObject(scrollModel) .onDisappear { VideoPlayerView.players.removeAll() + stopAudioPlayer() if chatModel.chatId == cInfo.id && !presentationMode.wrappedValue.isPresented { - chatModel.chatId = nil DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { if chatModel.chatId == nil { chatModel.chatItemStatuses = [:] - chatModel.reversedChatItems = [] + ItemsModel.shared.reversedChatItems = [] chatModel.groupMembers = [] - membersLoaded = false + chatModel.groupMembersIndexes.removeAll() + chatModel.membersLoaded = false } } } } + .onChange(of: colorScheme) { _ in + theme = buildTheme() + } .toolbar { ToolbarItem(placement: .principal) { - if case let .direct(contact) = cInfo { + if selectedChatItems != nil { + SelectedItemsTopToolbar(selectedChatItems: $selectedChatItems) + } else if case let .direct(contact) = cInfo { Button { Task { - do { - let (stats, profile) = try await apiContactInfo(chat.chatInfo.apiId) - let (ct, code) = try await apiGetContactCode(chat.chatInfo.apiId) - await MainActor.run { - connectionStats = stats - customUserProfile = profile - connectionCode = code - if contact.activeConn?.connectionCode != ct.activeConn?.connectionCode { - chat.chatInfo = .direct(contact: ct) - } - } - } catch let error { - logger.error("apiContactInfo or apiGetContactCode error: \(responseError(error))") - } - await MainActor.run { showChatInfoSheet = true } + showChatInfoSheet = true } } label: { ChatInfoToolbar(chat: chat) } - .sheet(isPresented: $showChatInfoSheet, onDismiss: { - connectionStats = nil - customUserProfile = nil - connectionCode = nil - }) { - ChatInfoView(chat: chat, contact: contact, connectionStats: $connectionStats, customUserProfile: $customUserProfile, localAlias: chat.chatInfo.localAlias, connectionCode: $connectionCode) + .appSheet(isPresented: $showChatInfoSheet, onDismiss: { theme = buildTheme() }) { + ChatInfoView( + chat: chat, + contact: contact, + localAlias: chat.chatInfo.localAlias, + onSearch: { focusSearch() } + ) } } else if case let .group(groupInfo) = cInfo { Button { - Task { await loadGroupMembers(groupInfo) { showChatInfoSheet = true } } + Task { await chatModel.loadGroupMembers(groupInfo) { showChatInfoSheet = true } } } label: { ChatInfoToolbar(chat: chat) + .tint(theme.colors.primary) } - .appSheet(isPresented: $showChatInfoSheet) { + .appSheet(isPresented: $showChatInfoSheet, onDismiss: { theme = buildTheme() }) { GroupChatInfoView( chat: chat, groupInfo: Binding( @@ -153,7 +220,8 @@ struct ChatView: View { chat.chatInfo = .group(groupInfo: gInfo) chat.created = Date.now } - ) + ), + onSearch: { focusSearch() } ) } } else if case .local = cInfo { @@ -161,82 +229,90 @@ struct ChatView: View { } } ToolbarItem(placement: .navigationBarTrailing) { - switch cInfo { - case let .direct(contact): - HStack { - let callsPrefEnabled = contact.mergedPreferences.calls.enabled.forUser - if callsPrefEnabled { - if chatModel.activeCall == nil { - callButton(contact, .audio, imageName: "phone") - .disabled(!contact.ready || !contact.active) - } else if let call = chatModel.activeCall, call.contact.id == cInfo.id { - endCallButton(call) - } + let isLoading = im.isLoading && im.showLoadingProgress + if selectedChatItems != nil { + Button { + withAnimation { + selectedChatItems = nil } - Menu { - if callsPrefEnabled && chatModel.activeCall == nil { - Button { - CallController.shared.startCall(contact, .video) - } label: { - Label("Video call", systemImage: "video") + } label: { + Text("Cancel") + } + } else { + switch cInfo { + case let .direct(contact): + HStack { + let callsPrefEnabled = contact.mergedPreferences.calls.enabled.forUser + if callsPrefEnabled { + if chatModel.activeCall == nil { + callButton(contact, .audio, imageName: "phone") + .disabled(!contact.ready || !contact.active) + } else if let call = chatModel.activeCall, call.contact.id == cInfo.id { + endCallButton(call) } - .disabled(!contact.ready || !contact.active) } - searchButton() - ToggleNtfsButton(chat: chat) - .disabled(!contact.ready || !contact.active) - } label: { - Image(systemName: "ellipsis") - } - } - case let .group(groupInfo): - HStack { - if groupInfo.canAddMembers { - if (chat.chatInfo.incognito) { - groupLinkButton() - .appSheet(isPresented: $showGroupLinkSheet) { - GroupLinkView( - groupId: groupInfo.groupId, - groupLink: $groupLink, - groupLinkMemberRole: $groupLinkMemberRole, - showTitle: true, - creatingGroup: false - ) - } - } else { - addMembersButton() - .appSheet(isPresented: $showAddMembersSheet) { - AddGroupMembersView(chat: chat, groupInfo: groupInfo) + Menu { + if !isLoading { + if callsPrefEnabled && chatModel.activeCall == nil { + Button { + CallController.shared.startCall(contact, .video) + } label: { + Label("Video call", systemImage: "video") + } + .disabled(!contact.ready || !contact.active) } + searchButton() + ToggleNtfsButton(chat: chat) + .disabled(!contact.ready || !contact.active) + } + } label: { + Image(systemName: "ellipsis") + .tint(isLoading ? Color.clear : nil) + .overlay { if isLoading { ProgressView() } } } } - Menu { - searchButton() - ToggleNtfsButton(chat: chat) - } label: { - Image(systemName: "ellipsis") + case let .group(groupInfo): + HStack { + if groupInfo.canAddMembers { + if (chat.chatInfo.incognito) { + groupLinkButton() + .appSheet(isPresented: $showGroupLinkSheet) { + GroupLinkView( + groupId: groupInfo.groupId, + groupLink: $groupLink, + groupLinkMemberRole: $groupLinkMemberRole, + showTitle: true, + creatingGroup: false + ) + } + } else { + addMembersButton() + .appSheet(isPresented: $showAddMembersSheet) { + AddGroupMembersView(chat: chat, groupInfo: groupInfo) + } + } + } + Menu { + if !isLoading { + searchButton() + ToggleNtfsButton(chat: chat) + } + } label: { + Image(systemName: "ellipsis") + .tint(isLoading ? Color.clear : nil) + .overlay { if isLoading { ProgressView() } } + } } + case .local: + searchButton() + default: + EmptyView() } - case .local: - searchButton() - default: - EmptyView() } } } } - - private func loadGroupMembers(_ groupInfo: GroupInfo, updateView: @escaping () -> Void = {}) async { - let groupMembers = await apiListMembers(groupInfo.groupId) - await MainActor.run { - if chatModel.chatId == groupInfo.id { - chatModel.groupMembers = groupMembers.map { GMember.init($0) } - membersLoaded = true - updateView() - } - } - } - + private func initChatView() { let cInfo = chat.chatInfo // This check prevents the call to apiContactInfo after the app is suspended, and the database is closed. @@ -272,7 +348,7 @@ struct ChatView: View { Image(systemName: "magnifyingglass") TextField("Search", text: $searchText) .focused($searchFocussed) - .foregroundColor(.primary) + .foregroundColor(theme.colors.onBackground) .frame(maxWidth: .infinity) Button { @@ -282,7 +358,7 @@ struct ChatView: View { } } .padding(EdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7)) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .background(Color(.tertiarySystemFill)) .cornerRadius(10.0) @@ -290,10 +366,7 @@ struct ChatView: View { searchText = "" searchMode = false searchFocussed = false - DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { - chatModel.reversedChatItems = [] - loadChat(chat: chat) - } + Task { await loadChat(chat: chat) } } } .padding(.horizontal) @@ -304,95 +377,151 @@ struct ChatView: View { ci.content.msgContent?.isVoice == true && ci.content.text.count == 0 && ci.quotedItem == nil && ci.meta.itemForwarded == nil } + private func filtered(_ reversedChatItems: Array) -> Array { + reversedChatItems + .enumerated() + .filter { (index, chatItem) in + if let mergeCategory = chatItem.mergeCategory, index > 0 { + mergeCategory != reversedChatItems[index - 1].mergeCategory + } else { + true + } + } + .map { $0.element } + } + + private func chatItemsList() -> some View { let cInfo = chat.chatInfo + let mergedItems = filtered(im.reversedChatItems) return GeometryReader { g in - ScrollViewReader { proxy in - ScrollView { - LazyVStack(spacing: 0) { - ForEach(chatModel.reversedChatItems, id: \.viewId) { ci in - let voiceNoFrame = voiceWithoutFrame(ci) - let maxWidth = cInfo.chatType == .group - ? voiceNoFrame - ? (g.size.width - 28) - 42 - : (g.size.width - 28) * 0.84 - 42 - : voiceNoFrame - ? (g.size.width - 32) - : (g.size.width - 32) * 0.84 - chatItemView(ci, maxWidth) - .scaleEffect(x: 1, y: -1, anchor: .center) - .onAppear { - itemsInView.insert(ci.viewId) - loadChatItems(cInfo, ci, proxy) - if ci.isRcvNew { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { - if chatModel.chatId == cInfo.id && itemsInView.contains(ci.viewId) { - Task { - await apiMarkChatItemRead(cInfo, ci) - } - } - } - } - } - .onDisappear { - itemsInView.remove(ci.viewId) - } - } - } - } + ReverseList(items: mergedItems, scrollState: $scrollModel.state) { ci in + let voiceNoFrame = voiceWithoutFrame(ci) + let maxWidth = cInfo.chatType == .group + ? voiceNoFrame + ? (g.size.width - 28) - 42 + : (g.size.width - 28) * 0.84 - 42 + : voiceNoFrame + ? (g.size.width - 32) + : (g.size.width - 32) * 0.84 + return ChatItemWithMenu( + chat: $chat, + chatItem: ci, + maxWidth: maxWidth, + composeState: $composeState, + selectedMember: $selectedMember, + revealedChatItem: $revealedChatItem, + selectedChatItems: $selectedChatItems + ) .onAppear { - scrollProxy = proxy + floatingButtonModel.appeared(viewId: ci.viewId) } - .onTapGesture { hideKeyboard() } - .onChange(of: searchText) { _ in - loadChat(chat: chat, search: searchText) + .onDisappear { + floatingButtonModel.disappeared(viewId: ci.viewId) } - .onChange(of: chatModel.chatId) { chatId in - if let chatId, let c = chatModel.getChat(chatId) { - chat = c - showChatInfoSheet = false - loadChat(chat: c) - DispatchQueue.main.async { - scrollToBottom(proxy) - } + .id(ci.id) // Required to trigger `onAppear` on iOS15 + } loadPage: { + loadChatItems(cInfo) + } + .opacity(ItemsModel.shared.isLoading ? 0 : 1) + .padding(.vertical, -InvertedTableView.inset) + .onTapGesture { hideKeyboard() } + .onChange(of: searchText) { _ in + Task { await loadChat(chat: chat, search: searchText) } + } + .onChange(of: im.reversedChatItems) { _ in + floatingButtonModel.chatItemsChanged() + } + .onChange(of: im.itemAdded) { added in + if added { + im.itemAdded = false + if floatingButtonModel.unreadChatItemCounts.isReallyNearBottom { + scrollModel.scrollToBottom() } } } } - .scaleEffect(x: 1, y: -1, anchor: .center) } @ViewBuilder private func connectingText() -> some View { if case let .direct(contact) = chat.chatInfo, - !contact.ready, + !contact.sndReady, contact.active, !contact.nextSendGrpInv { Text("connecting…") .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .padding(.top) } else { EmptyView() } } - - private func floatingButtons(_ proxy: ScrollViewProxy) -> some View { - let counts = chatModel.unreadChatItemCounts(itemsInView: itemsInView) - return VStack { + + class FloatingButtonModel: ObservableObject { + private enum Event { + case appeared(String) + case disappeared(String) + case chatItemsChanged + } + + @Published var unreadChatItemCounts: UnreadChatItemCounts + + private let events = PassthroughSubject() + private var bag = Set() + + init() { + unreadChatItemCounts = UnreadChatItemCounts( + isNearBottom: true, + isReallyNearBottom: true, + unreadBelow: 0 + ) + events + .receive(on: DispatchQueue.global(qos: .background)) + .scan(Set()) { itemsInView, event in + var updated = itemsInView + switch event { + case let .appeared(viewId): updated.insert(viewId) + case let .disappeared(viewId): updated.remove(viewId) + case .chatItemsChanged: () + } + return updated + } + .map { ChatModel.shared.unreadChatItemCounts(itemsInView: $0) } + .removeDuplicates() + .throttle(for: .seconds(0.2), scheduler: DispatchQueue.main, latest: true) + .assign(to: \.unreadChatItemCounts, on: self) + .store(in: &bag) + } + + func appeared(viewId: String) { + events.send(.appeared(viewId)) + } + + func disappeared(viewId: String) { + events.send(.disappeared(viewId)) + } + + func chatItemsChanged() { + events.send(.chatItemsChanged) + } + } + + private func floatingButtons(counts: UnreadChatItemCounts) -> some View { + VStack { let unreadAbove = chat.chatStats.unreadCount - counts.unreadBelow if unreadAbove > 0 { circleButton { unreadCountText(unreadAbove) .font(.callout) - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) + } + .onTapGesture { + scrollModel.scrollToNextPage() } - .onTapGesture { scrollUp(proxy) } .contextMenu { Button { - if let ci = chatModel.topItemInView(itemsInView: itemsInView) { - Task { - await markChatRead(chat, aboveItem: ci) - } + Task { + await markChatRead(chat) } } label: { Label("Mark read", systemImage: "checkmark") @@ -404,20 +533,22 @@ struct ChatView: View { circleButton { unreadCountText(counts.unreadBelow) .font(.callout) - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) } - .onTapGesture { scrollToBottom(proxy) } - } else if counts.totalBelow > 16 { + .onTapGesture { + scrollModel.scrollToBottom() + } + } else if !counts.isNearBottom { circleButton { Image(systemName: "chevron.down") - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) } - .onTapGesture { scrollToBottom(proxy) } + .onTapGesture { scrollModel.scrollToBottom() } } } .padding() } - + private func circleButton(_ content: @escaping () -> Content) -> some View { ZStack { Circle() @@ -426,7 +557,7 @@ struct ChatView: View { content() } } - + private func callButton(_ contact: Contact, _ media: CallMediaType, imageName: String) -> some View { Button { CallController.shared.startCall(contact, media) @@ -449,18 +580,22 @@ struct ChatView: View { private func searchButton() -> some View { Button { - searchMode = true - searchFocussed = true - searchText = "" + focusSearch() } label: { Label("Search", systemImage: "magnifyingglass") } } - + + private func focusSearch() { + searchMode = true + searchFocussed = true + searchText = "" + } + private func addMembersButton() -> some View { Button { if case let .group(gInfo) = chat.chatInfo { - Task { await loadGroupMembers(gInfo) { showAddMembersSheet = true } } + Task { await chatModel.loadGroupMembers(gInfo) { showAddMembersSheet = true } } } } label: { Image(systemName: "person.crop.circle.badge.plus") @@ -486,149 +621,252 @@ struct ChatView: View { } } - private func loadChatItems(_ cInfo: ChatInfo, _ ci: ChatItem, _ proxy: ScrollViewProxy) { - if let firstItem = chatModel.reversedChatItems.last, firstItem.id == ci.id { - if loadingItems || firstPage { return } - loadingItems = true - Task { - do { - let items = try await apiGetChatItems( - type: cInfo.chatType, - id: cInfo.apiId, - pagination: .before(chatItemId: firstItem.id, count: 50), - search: searchText - ) - await MainActor.run { - if items.count == 0 { - firstPage = true - } else { - chatModel.reversedChatItems.append(contentsOf: items.reversed()) - } - loadingItems = false - } - } catch let error { - logger.error("apiGetChat error: \(responseError(error))") - await MainActor.run { loadingItems = false } + private func showModerateSelectedMessagesAlert(_ groupInfo: GroupInfo) { + guard let count = selectedChatItems?.count, count > 0 else { return } + + AlertManager.shared.showAlert(Alert( + title: Text(count == 1 ? "Delete member message?" : "Delete \(count) messages of members?"), + message: Text( + groupInfo.fullGroupPreferences.fullDelete.on + ? (count == 1 ? "The message will be deleted for all members." : "The messages will be deleted for all members.") + : (count == 1 ? "The message will be marked as moderated for all members." : "The messages will be marked as moderated for all members.") + ), + primaryButton: .destructive(Text("Delete")) { + if let selected = selectedChatItems { + deleteMessages(chat, selected.sorted(), .cidmBroadcast, moderate: true, deletedSelectedMessages) } + }, + secondaryButton: .cancel() + )) + } + + private func deletedSelectedMessages() async { + await MainActor.run { + withAnimation { + selectedChatItems = nil } } } - - @ViewBuilder private func chatItemView(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View { - ChatItemWithMenu( - chat: chat, - chatItem: ci, - maxWidth: maxWidth, - itemWidth: maxWidth, - composeState: $composeState, - selectedMember: $selectedMember, - chatView: self - ) + + private func loadChatItems(_ cInfo: ChatInfo) { + Task { + if loadingItems || firstPage { return } + loadingItems = true + do { + var reversedPage = Array() + var chatItemsAvailable = true + // Load additional items until the page is +50 large after merging + while chatItemsAvailable && filtered(reversedPage).count < loadItemsPerPage { + let pagination: ChatPagination = + if let lastItem = reversedPage.last ?? im.reversedChatItems.last { + .before(chatItemId: lastItem.id, count: loadItemsPerPage) + } else { + .last(count: loadItemsPerPage) + } + let chatItems = try await apiGetChatItems( + type: cInfo.chatType, + id: cInfo.apiId, + pagination: pagination, + search: searchText + ) + chatItemsAvailable = !chatItems.isEmpty + reversedPage.append(contentsOf: chatItems.reversed()) + } + await MainActor.run { + if reversedPage.count == 0 { + firstPage = true + } else { + im.reversedChatItems.append(contentsOf: reversedPage) + } + loadingItems = false + } + } catch let error { + logger.error("apiGetChat error: \(responseError(error))") + await MainActor.run { loadingItems = false } + } + } + } + + func stopAudioPlayer() { + VoiceItemState.chatView.values.forEach { $0.audioPlayer?.stop() } + VoiceItemState.chatView = [:] } private struct ChatItemWithMenu: View { @EnvironmentObject var m: ChatModel - @Environment(\.colorScheme) var colorScheme - @ObservedObject var chat: Chat - var chatItem: ChatItem - var maxWidth: CGFloat - @State var itemWidth: CGFloat + @EnvironmentObject var theme: AppTheme + @Binding @ObservedObject var chat: Chat + let chatItem: ChatItem + let maxWidth: CGFloat @Binding var composeState: ComposeState @Binding var selectedMember: GMember? - var chatView: ChatView + @Binding var revealedChatItem: ChatItem? @State private var deletingItem: ChatItem? = nil @State private var showDeleteMessage = false @State private var deletingItems: [Int64] = [] @State private var showDeleteMessages = false - @State private var revealed = false @State private var showChatItemInfoSheet: Bool = false @State private var chatItemInfo: ChatItemInfo? @State private var showForwardingSheet: Bool = false + @Binding var selectedChatItems: Set? + @State private var allowMenu: Bool = true - @State private var audioPlayer: AudioPlayer? - @State private var playbackState: VoiceMessagePlaybackState = .noPlayback - @State private var playbackTime: TimeInterval? + var revealed: Bool { chatItem == revealedChatItem } var body: some View { - let (currIndex, nextItem) = m.getNextChatItem(chatItem) + let (currIndex, _) = m.getNextChatItem(chatItem) let ciCategory = chatItem.mergeCategory - if (ciCategory != nil && ciCategory == nextItem?.mergeCategory) { - // memberConnected events and deleted items are aggregated at the last chat item in a row, see ChatItemView - ZStack {} // scroll doesn't work if it's EmptyView() - } else { - let (prevHidden, prevItem) = m.getPrevShownChatItem(currIndex, ciCategory) - let range = itemsRange(currIndex, prevHidden) + let (prevHidden, prevItem) = m.getPrevShownChatItem(currIndex, ciCategory) + let range = itemsRange(currIndex, prevHidden) + let im = ItemsModel.shared + Group { if revealed, let range = range { - let items = Array(zip(Array(range), m.reversedChatItems[range])) - ForEach(items, id: \.1.viewId) { (i, ci) in - let prev = i == prevHidden ? prevItem : m.reversedChatItems[i + 1] + let items = Array(zip(Array(range), im.reversedChatItems[range])) + ForEach(items.reversed(), id: \.1.viewId) { (i, ci) in + let prev = i == prevHidden ? prevItem : im.reversedChatItems[i + 1] chatItemView(ci, nil, prev) + .overlay { + if let selected = selectedChatItems, ci.canBeDeletedForSelf { + Color.clear + .contentShape(Rectangle()) + .onTapGesture { + let checked = selected.contains(ci.id) + selectUnselectChatItem(select: !checked, ci) + } + } + } } } else { - // Switch branches just to work around context menu problem when 'revealed' changes but size of item isn't - if revealed { - chatItemView(chatItem, range, prevItem) - } else { - chatItemView(chatItem, range, prevItem) + chatItemView(chatItem, range, prevItem) + .overlay { + if let selected = selectedChatItems, chatItem.canBeDeletedForSelf { + Color.clear + .contentShape(Rectangle()) + .onTapGesture { + let checked = selected.contains(chatItem.id) + selectUnselectChatItem(select: !checked, chatItem) + } + } } } } + .onAppear { + if let range { + if let items = unreadItems(range) { + waitToMarkRead { + for ci in items { + await apiMarkChatItemRead(chat.chatInfo, ci) + } + } + } + } else if chatItem.isRcvNew { + waitToMarkRead { + await apiMarkChatItemRead(chat.chatInfo, chatItem) + } + } + } + } + + private func unreadItems(_ range: ClosedRange) -> [ChatItem]? { + let im = ItemsModel.shared + let items = range.compactMap { i in + if i >= 0 && i < im.reversedChatItems.count { + let ci = im.reversedChatItems[i] + return if ci.isRcvNew { ci } else { nil } + } else { + return nil + } + } + return if items.isEmpty { nil } else { items } + } + + private func waitToMarkRead(_ op: @Sendable @escaping () async -> Void) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { + if m.chatId == chat.chatInfo.id { + Task(operation: op) + } + } } @ViewBuilder func chatItemView(_ ci: ChatItem, _ range: ClosedRange?, _ prevItem: ChatItem?) -> some View { if case let .groupRcv(member) = ci.chatDir, case let .group(groupInfo) = chat.chatInfo { let (prevMember, memCount): (GroupMember?, Int) = - if let range = range { - m.getPrevHiddenMember(member, range) - } else { - (nil, 1) - } + if let range = range { + m.getPrevHiddenMember(member, range) + } else { + (nil, 1) + } if prevItem == nil || showMemberImage(member, prevItem) || prevMember != nil { VStack(alignment: .leading, spacing: 4) { if ci.content.showMemberName { - Text(memberNames(member, prevMember, memCount)) + let t = if memCount == 1 && member.memberRole > .member { + Text(member.memberRole.text + " ").fontWeight(.semibold) + Text(member.displayName) + } else { + Text(memberNames(member, prevMember, memCount)) + } + t .font(.caption) .foregroundStyle(.secondary) .lineLimit(2) - .padding(.leading, memberImageSize + 14) + .padding(.leading, memberImageSize + 14 + (selectedChatItems != nil && ci.canBeDeletedForSelf ? 12 + 24 : 0)) .padding(.top, 7) } - HStack(alignment: .top, spacing: 8) { - ProfileImage(imageStr: member.memberProfile.image, size: memberImageSize) - .onTapGesture { - if chatView.membersLoaded { - selectedMember = m.getGroupMember(member.groupMemberId) - } else { - Task { - await chatView.loadGroupMembers(groupInfo) { - selectedMember = m.getGroupMember(member.groupMemberId) + HStack(alignment: .center, spacing: 0) { + if selectedChatItems != nil && ci.canBeDeletedForSelf { + SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems) + .padding(.trailing, 12) + } + HStack(alignment: .top, spacing: 8) { + MemberProfileImage(member, size: memberImageSize, backgroundColor: theme.colors.background) + .onTapGesture { + if let member = m.getGroupMember(member.groupMemberId) { + selectedMember = member + } else { + Task { + await m.loadGroupMembers(groupInfo) { + selectedMember = m.getGroupMember(member.groupMemberId) + } } } } - } - .appSheet(item: $selectedMember) { member in - GroupMemberInfoView(groupInfo: groupInfo, groupMember: member, navigation: true) - } - chatItemWithMenu(ci, range, maxWidth) + chatItemWithMenu(ci, range, maxWidth) + } } } - .padding(.top, 5) + .padding(.bottom, 5) .padding(.trailing) .padding(.leading, 12) } else { - chatItemWithMenu(ci, range, maxWidth) - .padding(.top, 5) - .padding(.trailing) - .padding(.leading, memberImageSize + 8 + 12) + HStack(alignment: .center, spacing: 0) { + if selectedChatItems != nil && ci.canBeDeletedForSelf { + SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems) + .padding(.leading, 12) + } + chatItemWithMenu(ci, range, maxWidth) + .padding(.trailing) + .padding(.leading, memberImageSize + 8 + 12) + } + .padding(.bottom, 5) } } else { - chatItemWithMenu(ci, range, maxWidth) - .padding(.horizontal) - .padding(.top, 5) + HStack(alignment: .center, spacing: 0) { + if selectedChatItems != nil && ci.canBeDeletedForSelf { + if chat.chatInfo.chatType == .group { + SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems) + .padding(.leading, 12) + } else { + SelectedChatItem(ciId: ci.id, selectedChatItems: $selectedChatItems) + .padding(.leading) + } + } + chatItemWithMenu(ci, range, maxWidth) + .padding(.horizontal) + } + .padding(.bottom, 5) } } @@ -645,24 +883,16 @@ struct ChatView: View { @ViewBuilder func chatItemWithMenu(_ ci: ChatItem, _ range: ClosedRange?, _ maxWidth: CGFloat) -> some View { let alignment: Alignment = ci.chatDir.sent ? .trailing : .leading - let uiMenu: Binding = Binding( - get: { UIMenu(title: "", children: menu(ci, range, live: composeState.liveMessage != nil)) }, - set: { _ in } - ) - VStack(alignment: alignment.horizontal, spacing: 3) { ChatItemView( chat: chat, chatItem: ci, maxWidth: maxWidth, - scrollProxy: chatView.scrollProxy, - revealed: $revealed, - allowMenu: $allowMenu, - audioPlayer: $audioPlayer, - playbackState: $playbackState, - playbackTime: $playbackTime + revealed: .constant(revealed), + allowMenu: $allowMenu ) - .uiKitContextMenu(hasImageOrVideo: ci.content.msgContent?.isImageOrVideo == true, maxWidth: maxWidth, itemWidth: $itemWidth, menu: uiMenu, allowMenu: $allowMenu) + .modifier(ChatItemClipped(ci)) + .contextMenu { menu(ci, range, live: composeState.liveMessage != nil) } .accessibilityLabel("") if ci.content.msgContent != nil && (ci.meta.itemDeleted == nil || revealed) && ci.reactions.count > 0 { chatItemReactions(ci) @@ -671,29 +901,21 @@ struct ChatView: View { } .confirmationDialog("Delete message?", isPresented: $showDeleteMessage, titleVisibility: .visible) { Button("Delete for me", role: .destructive) { - deleteMessage(.cidmInternal) + deleteMessage(.cidmInternal, moderate: false) } if let di = deletingItem, di.meta.deletable && !di.localNote { - Button(broadcastDeleteButtonText, role: .destructive) { - deleteMessage(.cidmBroadcast) + Button(broadcastDeleteButtonText(chat), role: .destructive) { + deleteMessage(.cidmBroadcast, moderate: false) } } } .confirmationDialog(deleteMessagesTitle, isPresented: $showDeleteMessages, titleVisibility: .visible) { Button("Delete for me", role: .destructive) { - deleteMessages() + deleteMessages(chat, deletingItems, moderate: false) } } .frame(maxWidth: maxWidth, maxHeight: .infinity, alignment: alignment) .frame(minWidth: 0, maxWidth: .infinity, alignment: alignment) - .onDisappear { - if ci.content.msgContent?.isVoice == true { - allowMenu = true - audioPlayer?.stop() - playbackState = .noPlayback - playbackTime = TimeInterval(0) - } - } .sheet(isPresented: $showChatItemInfoSheet, onDismiss: { chatItemInfo = nil }) { @@ -729,7 +951,7 @@ struct ChatView: View { Text("\(r.totalReacted)") .font(.caption) .fontWeight(r.userReacted ? .bold : .light) - .foregroundColor(r.userReacted ? .accentColor : .secondary) + .foregroundColor(r.userReacted ? theme.colors.primary : theme.colors.secondary) } } .padding(.horizontal, 6) @@ -746,149 +968,156 @@ struct ChatView: View { } } - private func menu(_ ci: ChatItem, _ range: ClosedRange?, live: Bool) -> [UIMenuElement] { - var menu: [UIMenuElement] = [] + @ViewBuilder + private func menu(_ ci: ChatItem, _ range: ClosedRange?, live: Bool) -> some View { if let mc = ci.content.msgContent, ci.meta.itemDeleted == nil || revealed { - let rs = allReactions(ci) if chat.chatInfo.featureEnabled(.reactions) && ci.allowAddReaction, - rs.count > 0 { - var rm: UIMenu - if #available(iOS 16, *) { - var children: [UIMenuElement] = Array(rs.prefix(topReactionsCount(rs))) - if let sm = reactionUIMenu(rs) { - children.append(sm) - } - rm = UIMenu(title: "", options: .displayInline, children: children) - rm.preferredElementSize = .small - } else { - rm = reactionUIMenuPreiOS16(rs) - } - menu.append(rm) + availableReactions.count > 0 { + reactionsGroup } if ci.meta.itemDeleted == nil && !ci.isLiveDummy && !live && !ci.localNote { - menu.append(replyUIAction(ci)) + replyButton } 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)) + shareButton(ci) + copyButton(ci) } if let fileSource = fileSource, fileExists { if case .image = ci.content.msgContent, let image = getLoadedImage(ci.file) { if image.imageData != nil { - menu.append(saveFileAction(fileSource)) + saveButton(file: fileSource) } else { - menu.append(saveImageAction(image)) + saveButton(image: image) } } else { - menu.append(saveFileAction(fileSource)) + saveButton(file: fileSource) } } else if let file = ci.file, case .rcvInvitation = file.fileStatus, fileSizeValid(file) { - menu.append(downloadFileAction(file)) + downloadButton(file: file) } if ci.meta.editable && !mc.isVoice && !live { - menu.append(editAction(ci)) + editButton(chatItem) } if ci.meta.itemDeleted == nil && (ci.file == nil || (fileSource != nil && fileExists)) && !ci.isLiveDummy && !live { - menu.append(forwardUIAction(ci)) + forwardButton } if !ci.isLiveDummy { - menu.append(viewInfoUIAction(ci)) + viewInfoButton(ci) } if revealed { - menu.append(hideUIAction()) + hideButton() } if ci.meta.itemDeleted == nil && !ci.localNote, let file = ci.file, let cancelAction = file.cancelAction { - menu.append(cancelFileUIAction(file.fileId, cancelAction)) + cancelFileButton(file.fileId, cancelAction) } if !live || !ci.meta.isLive { - menu.append(deleteUIAction(ci)) + deleteButton(ci) } - if let (groupInfo, _) = ci.memberToModerate(chat.chatInfo) { - menu.append(moderateUIAction(ci, groupInfo)) + if let (groupInfo, _) = ci.memberToModerate(chat.chatInfo), ci.chatDir != .groupSnd { + moderateButton(ci, groupInfo) } } else if ci.meta.itemDeleted != nil { if revealed { - menu.append(hideUIAction()) + hideButton() } else if !ci.isDeletedContent { - menu.append(revealUIAction()) + revealButton(ci) } else if range != nil { - menu.append(expandUIAction()) + expandButton() } - menu.append(viewInfoUIAction(ci)) - menu.append(deleteUIAction(ci)) + viewInfoButton(ci) + deleteButton(ci) } else if ci.isDeletedContent { - menu.append(viewInfoUIAction(ci)) - menu.append(deleteUIAction(ci)) + viewInfoButton(ci) + deleteButton(ci) } else if ci.mergeCategory != nil && ((range?.count ?? 0) > 1 || revealed) { - menu.append(revealed ? shrinkUIAction() : expandUIAction()) - menu.append(deleteUIAction(ci)) + if revealed { shrinkButton() } else { expandButton() } + deleteButton(ci) } else if ci.showLocalDelete { - menu.append(deleteUIAction(ci)) + deleteButton(ci) + } else { + EmptyView() + } + if selectedChatItems == nil && ci.canBeDeletedForSelf { + Divider() + selectButton(ci) } - return menu } - - private func replyUIAction(_ ci: ChatItem) -> UIAction { - UIAction( - title: NSLocalizedString("Reply", comment: "chat item action"), - image: UIImage(systemName: "arrowshape.turn.up.left") - ) { _ in + + var replyButton: Button { + Button { withAnimation { if composeState.editing { - composeState = ComposeState(contextItem: .quotedItem(chatItem: ci)) + composeState = ComposeState(contextItem: .quotedItem(chatItem: chatItem)) } else { - composeState = composeState.copy(contextItem: .quotedItem(chatItem: ci)) + composeState = composeState.copy(contextItem: .quotedItem(chatItem: chatItem)) } } + } label: { + Label( + NSLocalizedString("Reply", comment: "chat item action"), + systemImage: "arrowshape.turn.up.left" + ) + } + } + + var forwardButton: Button { + Button { + showForwardingSheet = true + } label: { + Label( + NSLocalizedString("Forward", comment: "chat item action"), + systemImage: "arrowshape.turn.up.forward" + ) + } + } + + private var reactionsGroup: some View { + if #available(iOS 16.4, *) { + return ControlGroup { + if availableReactions.count > 4 { + reactions(till: 3) + Menu { + reactions(from: 3) + } label: { + Image(systemName: "ellipsis") + } + } else { reactions() } + }.controlGroupStyle(.compactMenu) + } else { + return Menu { + reactions() + } label: { + Label( + NSLocalizedString("React…", comment: "chat item menu"), + systemImage: "face.smiling" + ) + } } } - private func forwardUIAction(_ ci: ChatItem) -> UIAction { - UIAction( - title: NSLocalizedString("Forward", comment: "chat item action"), - image: UIImage(systemName: "arrowshape.turn.up.forward") - ) { _ in - showForwardingSheet = true + func reactions(from: Int? = nil, till: Int? = nil) -> some View { + ForEach(availableReactions[(from ?? 0)..<(till ?? availableReactions.count)]) { reaction in + Button(reaction.text) { + setReaction(chatItem, add: true, reaction: reaction) + } } } - private func reactionUIMenuPreiOS16(_ rs: [UIAction]) -> UIMenu { - UIMenu( - title: NSLocalizedString("React…", comment: "chat item menu"), - image: UIImage(systemName: "face.smiling"), - children: rs - ) - } - - @available(iOS 16.0, *) - private func reactionUIMenu(_ rs: [UIAction]) -> UIMenu? { - var children = rs - children.removeFirst(min(rs.count, topReactionsCount(rs))) - if children.count == 0 { return nil } - return UIMenu( - title: "", - image: UIImage(systemName: "ellipsis"), - children: children - ) - } - - private func allReactions(_ ci: ChatItem) -> [UIAction] { - MsgReaction.values.compactMap { r in - ci.reactions.contains(where: { $0.userReacted && $0.reaction == r }) - ? nil - : UIAction(title: r.text) { _ in setReaction(ci, add: true, reaction: r) } - } - } - - private func topReactionsCount(_ rs: [UIAction]) -> Int { - rs.count > 4 ? 3 : 4 + /// Reactions, which has not been used yet + private var availableReactions: Array { + MsgReaction.values + .filter { reaction in + !chatItem.reactions.contains { + $0.userReacted && $0.reaction == reaction + } + } } private func setReaction(_ ci: ChatItem, add: Bool, reaction: MsgReaction) { @@ -911,24 +1140,23 @@ struct ChatView: View { } } - private func shareUIAction(_ ci: ChatItem) -> UIAction { - UIAction( - title: NSLocalizedString("Share", comment: "chat item action"), - image: UIImage(systemName: "square.and.arrow.up") - ) { _ in + private func shareButton(_ ci: ChatItem) -> Button { + Button { var shareItems: [Any] = [ci.content.text] if case .image = ci.content.msgContent, let image = getLoadedImage(ci.file) { shareItems.append(image) } showShareSheet(items: shareItems) + } label: { + Label( + NSLocalizedString("Share", comment: "chat item action"), + systemImage: "square.and.arrow.up" + ) } } - - private func copyUIAction(_ ci: ChatItem) -> UIAction { - UIAction( - title: NSLocalizedString("Copy", comment: "chat item action"), - image: UIImage(systemName: "doc.on.doc") - ) { _ in + + private func copyButton(_ ci: ChatItem) -> Button { + Button { if case let .image(text, _) = ci.content.msgContent, text == "", let image = getLoadedImage(ci.file) { @@ -936,57 +1164,79 @@ struct ChatView: View { } else { UIPasteboard.general.string = ci.content.text } - } - } - - private func saveImageAction(_ image: UIImage) -> UIAction { - UIAction( - title: NSLocalizedString("Save", comment: "chat item action"), - image: UIImage(systemName: "square.and.arrow.down") - ) { _ in - UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil) - } - } - - private func saveFileAction(_ fileSource: CryptoFile) -> UIAction { - UIAction( - title: NSLocalizedString("Save", comment: "chat item action"), - image: UIImage(systemName: fileSource.cryptoArgs == nil ? "square.and.arrow.down" : "lock.open") - ) { _ in - saveCryptoFile(fileSource) + } label: { + Label("Copy", systemImage: "doc.on.doc") } } - private func downloadFileAction(_ file: CIFile) -> UIAction { - UIAction( - title: NSLocalizedString("Download", comment: "chat item action"), - image: UIImage(systemName: "arrow.down.doc") - ) { _ in + func saveButton(image: UIImage) -> Button { + Button { + UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil) + } label: { + Label( + NSLocalizedString("Save", comment: "chat item action"), + systemImage: "square.and.arrow.down" + ) + } + } + + func saveButton(file: CryptoFile) -> Button { + Button { + saveCryptoFile(file) + } label: { + Label( + NSLocalizedString("Save", comment: "chat item action"), + systemImage: file.cryptoArgs == nil ? "square.and.arrow.down" : "lock.open" + ) + } + } + + func downloadButton(file: CIFile) -> Button { + Button { Task { logger.debug("ChatView downloadFileAction, in Task") if let user = m.currentUser { await receiveFile(user: user, fileId: file.fileId) } } + } label: { + Label( + NSLocalizedString("Download", comment: "chat item action"), + systemImage: "arrow.down.doc" + ) } } - private func editAction(_ ci: ChatItem) -> UIAction { - UIAction( - title: NSLocalizedString("Edit", comment: "chat item action"), - image: UIImage(systemName: "square.and.pencil") - ) { _ in + private func editButton(_ ci: ChatItem) -> Button { + Button { withAnimation { composeState = ComposeState(editingItem: ci) } + } label: { + Label( + NSLocalizedString("Edit", comment: "chat item action"), + systemImage: "square.and.pencil" + ) } } - private func viewInfoUIAction(_ ci: ChatItem) -> UIAction { - UIAction( - title: NSLocalizedString("Info", comment: "chat item action"), - image: UIImage(systemName: "info.circle") - ) { _ in + private func selectButton(_ ci: ChatItem) -> Button { + Button { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + withAnimation { + selectUnselectChatItem(select: true, ci) + } + } + } label: { + Label( + NSLocalizedString("Select", comment: "chat item action"), + systemImage: "checkmark.circle" + ) + } + } + + private func viewInfoButton(_ ci: ChatItem) -> Button { + Button { Task { do { let cInfo = chat.chatInfo @@ -995,22 +1245,23 @@ struct ChatView: View { chatItemInfo = ciInfo } if case let .group(gInfo) = chat.chatInfo { - await chatView.loadGroupMembers(gInfo) + await m.loadGroupMembers(gInfo) } } catch let error { logger.error("apiGetChatItemInfo error: \(responseError(error))") } await MainActor.run { showChatItemInfoSheet = true } } + } label: { + Label( + NSLocalizedString("Info", comment: "chat item action"), + systemImage: "info.circle" + ) } } - private func cancelFileUIAction(_ fileId: Int64, _ cancelAction: CancelAction) -> UIAction { - return UIAction( - title: cancelAction.uiAction, - image: UIImage(systemName: "xmark"), - attributes: [.destructive] - ) { _ in + private func cancelFileButton(_ fileId: Int64, _ cancelAction: CancelAction) -> Button { + Button { AlertManager.shared.showAlert(Alert( title: Text(cancelAction.alert.title), message: Text(cancelAction.alert.message), @@ -1023,26 +1274,29 @@ struct ChatView: View { }, secondaryButton: .cancel() )) + } label: { + Label( + cancelAction.uiAction, + systemImage: "xmark" + ) } } - private func hideUIAction() -> UIAction { - UIAction( - title: NSLocalizedString("Hide", comment: "chat item action"), - image: UIImage(systemName: "eye.slash") - ) { _ in - withAnimation { - revealed = false + private func hideButton() -> Button { + Button { + withConditionalAnimation { + revealedChatItem = nil } + } label: { + Label( + NSLocalizedString("Hide", comment: "chat item action"), + systemImage: "eye.slash" + ) } } - - private func deleteUIAction(_ ci: ChatItem) -> UIAction { - UIAction( - title: NSLocalizedString("Delete", comment: "chat item action"), - image: UIImage(systemName: "trash"), - attributes: [.destructive] - ) { _ in + + private func deleteButton(_ ci: ChatItem) -> Button { + Button(role: .destructive) { if !revealed, let currIndex = m.getChatItemIndex(ci), let ciCategory = ci.mergeCategory { @@ -1050,7 +1304,7 @@ struct ChatView: View { if let range = itemsRange(currIndex, prevHidden) { var itemIds: [Int64] = [] for i in range { - itemIds.append(m.reversedChatItems[i].id) + itemIds.append(ItemsModel.shared.reversedChatItems[i].id) } showDeleteMessages = true deletingItems = itemIds @@ -1062,6 +1316,11 @@ struct ChatView: View { showDeleteMessage = true deletingItem = ci } + } label: { + Label( + NSLocalizedString("Delete", comment: "chat item action"), + systemImage: "trash" + ) } } @@ -1075,12 +1334,8 @@ struct ChatView: View { } } - private func moderateUIAction(_ ci: ChatItem, _ groupInfo: GroupInfo) -> UIAction { - UIAction( - title: NSLocalizedString("Moderate", comment: "chat item action"), - image: UIImage(systemName: "flag"), - attributes: [.destructive] - ) { _ in + private func moderateButton(_ ci: ChatItem, _ groupInfo: GroupInfo) -> Button { + Button(role: .destructive) { AlertManager.shared.showAlert(Alert( title: Text("Delete member message?"), message: Text( @@ -1090,112 +1345,117 @@ struct ChatView: View { ), primaryButton: .destructive(Text("Delete")) { deletingItem = ci - deleteMessage(.cidmBroadcast) + deleteMessage(.cidmBroadcast, moderate: true) }, secondaryButton: .cancel() )) + } label: { + Label( + NSLocalizedString("Moderate", comment: "chat item action"), + systemImage: "flag" + ) } } - private func revealUIAction() -> UIAction { - UIAction( - title: NSLocalizedString("Reveal", comment: "chat item action"), - image: UIImage(systemName: "eye") - ) { _ in - withAnimation { - revealed = true + private func revealButton(_ ci: ChatItem) -> Button { + Button { + withConditionalAnimation { + revealedChatItem = ci } + } label: { + Label( + NSLocalizedString("Reveal", comment: "chat item action"), + systemImage: "eye" + ) } } - private func expandUIAction() -> UIAction { - UIAction( - title: NSLocalizedString("Expand", comment: "chat item action"), - image: UIImage(systemName: "arrow.up.and.line.horizontal.and.arrow.down") - ) { _ in - withAnimation { - revealed = true + private func expandButton() -> Button { + Button { + withConditionalAnimation { + revealedChatItem = chatItem } + } label: { + Label( + NSLocalizedString("Expand", comment: "chat item action"), + systemImage: "arrow.up.and.line.horizontal.and.arrow.down" + ) } } - private func shrinkUIAction() -> UIAction { - UIAction( - title: NSLocalizedString("Hide", comment: "chat item action"), - image: UIImage(systemName: "arrow.down.and.line.horizontal.and.arrow.up") - ) { _ in - withAnimation { - revealed = false + private func shrinkButton() -> Button { + Button { + withConditionalAnimation { + revealedChatItem = nil } + } label: { + Label ( + NSLocalizedString("Hide", comment: "chat item action"), + systemImage: "arrow.down.and.line.horizontal.and.arrow.up" + ) } } - private var broadcastDeleteButtonText: LocalizedStringKey { - chat.chatInfo.featureEnabled(.fullDelete) ? "Delete for everyone" : "Mark deleted for everyone" - } - var deleteMessagesTitle: LocalizedStringKey { let n = deletingItems.count return n == 1 ? "Delete message?" : "Delete \(n) messages?" } - private func deleteMessages() { - let itemIds = deletingItems - if itemIds.count > 0 { - let chatInfo = chat.chatInfo - Task { - var deletedItems: [ChatItem] = [] - for itemId in itemIds { - do { - let (di, _) = try await apiDeleteChatItem( - type: chatInfo.chatType, - id: chatInfo.apiId, - itemId: itemId, - mode: .cidmInternal - ) - deletedItems.append(di) - } catch { - logger.error("ChatView.deleteMessage error: \(error.localizedDescription)") - } - } - await MainActor.run { - for di in deletedItems { - m.removeChatItem(chatInfo, di) - } + private func selectUnselectChatItem(select: Bool, _ ci: ChatItem) { + selectedChatItems = selectedChatItems ?? [] + var itemIds: [Int64] = [] + if !revealed, + let currIndex = m.getChatItemIndex(ci), + let ciCategory = ci.mergeCategory { + let (prevHidden, _) = m.getPrevShownChatItem(currIndex, ciCategory) + if let range = itemsRange(currIndex, prevHidden) { + for i in range { + itemIds.append(ItemsModel.shared.reversedChatItems[i].id) } + } else { + itemIds.append(ci.id) } + } else { + itemIds.append(ci.id) + } + if select { + if let sel = selectedChatItems { + selectedChatItems = sel.union(itemIds) + } + } else { + itemIds.forEach { selectedChatItems?.remove($0) } } } - private func deleteMessage(_ mode: CIDeleteMode) { + private func deleteMessage(_ mode: CIDeleteMode, moderate: Bool) { logger.debug("ChatView deleteMessage") Task { logger.debug("ChatView deleteMessage: in Task") do { if let di = deletingItem { - var deletedItem: ChatItem - var toItem: ChatItem? - if case .cidmBroadcast = mode, - let (groupInfo, groupMember) = di.memberToModerate(chat.chatInfo) { - (deletedItem, toItem) = try await apiDeleteMemberChatItem( + let r = if case .cidmBroadcast = mode, + moderate, + let (groupInfo, _) = di.memberToModerate(chat.chatInfo) { + try await apiDeleteMemberChatItems( groupId: groupInfo.apiId, - groupMemberId: groupMember.groupMemberId, - itemId: di.id + itemIds: [di.id] ) } else { - (deletedItem, toItem) = try await apiDeleteChatItem( + try await apiDeleteChatItems( type: chat.chatInfo.chatType, id: chat.chatInfo.apiId, - itemId: di.id, + itemIds: [di.id], mode: mode ) } - DispatchQueue.main.async { - deletingItem = nil - if let toItem = toItem { - _ = m.upsertChatItem(chat.chatInfo, toItem) - } else { - m.removeChatItem(chat.chatInfo, deletedItem) + if let itemDeletion = r.first { + await MainActor.run { + deletingItem = nil + if let toItem = itemDeletion.toChatItem { + _ = m.upsertChatItem(chat.chatInfo, toItem.chatItem) + } else { + m.removeChatItem(chat.chatInfo, itemDeletion.deletedChatItem.chatItem) + } } } } @@ -1204,21 +1464,92 @@ struct ChatView: View { } } } - } - private func scrollToBottom(_ proxy: ScrollViewProxy) { - if let ci = chatModel.reversedChatItems.first { - withAnimation { proxy.scrollTo(ci.viewId, anchor: .top) } + private struct SelectedChatItem: View { + @EnvironmentObject var theme: AppTheme + var ciId: Int64 + @Binding var selectedChatItems: Set? + @State var checked: Bool = false + var body: some View { + Image(systemName: checked ? "checkmark.circle.fill" : "circle") + .resizable() + .foregroundColor(checked ? theme.colors.primary : Color(uiColor: .tertiaryLabel)) + .frame(width: 24, height: 24) + .onAppear { + checked = selectedChatItems?.contains(ciId) == true + } + .onChange(of: selectedChatItems) { selected in + checked = selected?.contains(ciId) == true + } + } } } - - private func scrollUp(_ proxy: ScrollViewProxy) { - if let ci = chatModel.topItemInView(itemsInView: itemsInView) { - withAnimation { proxy.scrollTo(ci.viewId, anchor: .top) } +} + +private func broadcastDeleteButtonText(_ chat: Chat) -> LocalizedStringKey { + chat.chatInfo.featureEnabled(.fullDelete) ? "Delete for everyone" : "Mark deleted for everyone" +} + +private func deleteMessages(_ chat: Chat, _ deletingItems: [Int64], _ mode: CIDeleteMode = .cidmInternal, moderate: Bool, _ onSuccess: @escaping () async -> Void = {}) { + let itemIds = deletingItems + if itemIds.count > 0 { + let chatInfo = chat.chatInfo + Task { + do { + let deletedItems = if case .cidmBroadcast = mode, + moderate, + case .group = chat.chatInfo { + try await apiDeleteMemberChatItems( + groupId: chatInfo.apiId, + itemIds: itemIds + ) + } else { + try await apiDeleteChatItems( + type: chatInfo.chatType, + id: chatInfo.apiId, + itemIds: itemIds, + mode: mode + ) + } + + await MainActor.run { + for di in deletedItems { + if let toItem = di.toChatItem { + _ = ChatModel.shared.upsertChatItem(chat.chatInfo, toItem.chatItem) + } else { + ChatModel.shared.removeChatItem(chatInfo, di.deletedChatItem.chatItem) + } + } + } + await onSuccess() + } catch { + logger.error("ChatView.deleteMessages error: \(error.localizedDescription)") + } } } } +private func buildTheme() -> AppTheme { + if let cId = ChatModel.shared.chatId, let chat = ChatModel.shared.getChat(cId) { + let perChatTheme = if case let .direct(contact) = chat.chatInfo { + contact.uiThemes?.preferredMode(!AppTheme.shared.colors.isLight) + } else if case let .group(groupInfo) = chat.chatInfo { + groupInfo.uiThemes?.preferredMode(!AppTheme.shared.colors.isLight) + } else { + nil as ThemeModeOverride? + } + let overrides = if perChatTheme != nil { + ThemeManager.currentColors(nil, perChatTheme, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + } else { + nil as ThemeManager.ActiveTheme? + } + let theme = overrides ?? CurrentColors + return AppTheme(name: theme.name, base: theme.base, colors: theme.colors, appColors: theme.appColors, wallpaper: theme.wallpaper) + } else { + return AppTheme.shared + } +} + struct ToggleNtfsButton: View { @ObservedObject var chat: Chat @@ -1272,7 +1603,7 @@ struct ChatView_Previews: PreviewProvider { static var previews: some View { let chatModel = ChatModel() chatModel.chatId = "@1" - chatModel.reversedChatItems = [ + ItemsModel.shared.reversedChatItems = [ ChatItem.getSample(1, .directSnd, .now, "hello"), ChatItem.getSample(2, .directRcv, .now, "hi"), ChatItem.getSample(3, .directRcv, .now, "hi there"), diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeFileView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeFileView.swift index bc6a96aa86..1ec46816f5 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeFileView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeFileView.swift @@ -9,7 +9,7 @@ import SwiftUI struct ComposeFileView: View { - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme let fileName: String let cancelFile: (() -> Void) let cancelEnabled: Bool @@ -32,9 +32,8 @@ struct ComposeFileView: View { } .padding(.vertical, 1) .padding(.trailing, 12) - .frame(height: 50) - .background(colorScheme == .light ? sentColorLight : sentColorDark) + .frame(height: 54) + .background(theme.appColors.sentMessage) .frame(maxWidth: .infinity) - .padding(.top, 8) } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeImageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeImageView.swift index edaf86912c..df3a8caf55 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeImageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeImageView.swift @@ -10,7 +10,7 @@ import SwiftUI import SimpleXChat struct ComposeImageView: View { - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme let images: [String] let cancelImage: (() -> Void) let cancelEnabled: Bool @@ -18,10 +18,7 @@ struct ComposeImageView: View { var body: some View { HStack(alignment: .center, spacing: 8) { let imgs: [UIImage] = images.compactMap { image in - if let data = Data(base64Encoded: dropImagePrefix(image)) { - return UIImage(data: data) - } - return nil + UIImage(base64Encoded: image) } if imgs.count == 0 { ProgressView() @@ -48,9 +45,9 @@ struct ComposeImageView: View { } .padding(.vertical, 1) .padding(.trailing, 12) - .background(colorScheme == .light ? sentColorLight : sentColorDark) + .background(theme.appColors.sentMessage) + .frame(minHeight: 54) .frame(maxWidth: .infinity) - .padding(.top, 8) } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift index cc779851ab..f7f1a89299 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift @@ -10,37 +10,8 @@ import SwiftUI import LinkPresentation import SimpleXChat -func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) { - logger.debug("getLinkMetadata: fetching URL preview") - LPMetadataProvider().startFetchingMetadata(for: url){ metadata, error in - if let e = error { - logger.error("Error retrieving link metadata: \(e.localizedDescription)") - } - if let metadata = metadata, - let imageProvider = metadata.imageProvider, - imageProvider.canLoadObject(ofClass: UIImage.self) { - imageProvider.loadObject(ofClass: UIImage.self){ object, error in - var linkPreview: LinkPreview? = nil - if let error = error { - logger.error("Couldn't load image preview from link metadata with error: \(error.localizedDescription)") - } else { - if let image = object as? UIImage, - let resized = resizeImageToStrSize(image, maxDataSize: 14000), - let title = metadata.title, - let uri = metadata.originalURL { - linkPreview = LinkPreview(uri: uri, title: title, image: resized) - } - } - cb(linkPreview) - } - } else { - cb(nil) - } - } -} - struct ComposeLinkView: View { - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme let linkPreview: LinkPreview? var cancelPreview: (() -> Void)? = nil let cancelEnabled: Bool @@ -62,15 +33,14 @@ struct ComposeLinkView: View { } .padding(.vertical, 1) .padding(.trailing, 12) - .background(colorScheme == .light ? sentColorLight : sentColorDark) + .background(theme.appColors.sentMessage) + .frame(minHeight: 54) .frame(maxWidth: .infinity) - .padding(.top, 8) } private func linkPreviewView(_ linkPreview: LinkPreview) -> some View { HStack(alignment: .center, spacing: 8) { - if let data = Data(base64Encoded: dropImagePrefix(linkPreview.image)), - let uiImage = UIImage(data: data) { + if let uiImage = UIImage(base64Encoded: linkPreview.image) { Image(uiImage: uiImage) .resizable() .aspectRatio(contentMode: .fit) @@ -82,10 +52,10 @@ struct ComposeLinkView: View { Text(linkPreview.uri.absoluteString) .font(.caption) .lineLimit(1) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } .padding(.vertical, 5) - .frame(maxWidth: .infinity, minHeight: 60, maxHeight: 60) + .frame(maxWidth: .infinity, minHeight: 60) } } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 6cf9df782b..78cae78cf5 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -254,6 +254,7 @@ enum UploadContent: Equatable { struct ComposeView: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat @Binding var composeState: ComposeState @Binding var keyboardVisible: Bool @@ -280,21 +281,28 @@ struct ComposeView: View { @State private var stopPlayback: Bool = false @AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true + @AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial var body: some View { VStack(spacing: 0) { + Divider() if chat.chatInfo.contact?.nextSendGrpInv ?? false { ContextInvitingContactMemberView() + Divider() } + // preference checks should match checks in forwarding list let simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(.simplexLinks) let fileProhibited = composeState.attachmentPreview && !chat.groupFeatureEnabled(.files) let voiceProhibited = composeState.voicePreview && !chat.chatInfo.featureEnabled(.voice) if simplexLinkProhibited { msgNotAllowedView("SimpleX links not allowed", icon: "link") + Divider() } else if fileProhibited { msgNotAllowedView("Files and media not allowed", icon: "doc") + Divider() } else if voiceProhibited { msgNotAllowedView("Voice messages not allowed", icon: "mic") + Divider() } contextItemView() switch (composeState.editing, composeState.preview) { @@ -313,6 +321,7 @@ struct ComposeView: View { .frame(width: 25, height: 25) .padding(.bottom, 12) .padding(.leading, 12) + .tint(theme.colors.primary) if case let .group(g) = chat.chatInfo, !g.fullGroupPreferences.files.on(for: g.membership) { b.disabled(true).onTapGesture { @@ -353,16 +362,15 @@ struct ComposeView: View { keyboardVisible: $keyboardVisible, sendButtonColor: chat.chatInfo.incognito ? .indigo.opacity(colorScheme == .dark ? 1 : 0.7) - : .accentColor + : theme.colors.primary ) .padding(.trailing, 12) - .background(.background) .disabled(!chat.userCanSend) if chat.userIsObserver { Text("you are observer") .italic() - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .padding(.horizontal, 12) .onTapGesture { AlertManager.shared.showAlertMsg( @@ -374,6 +382,11 @@ struct ComposeView: View { } } } + .background { + Color.clear + .overlay(ToolbarMaterial.material(toolbarMaterial)) + .ignoresSafeArea(.all, edges: .bottom) + } .onChange(of: composeState.message) { msg in if composeState.linkPreviewAllowed { if msg.count > 0 { @@ -622,6 +635,7 @@ struct ComposeView: View { cancelPreview: cancelLinkPreview, cancelEnabled: !composeState.inProgress ) + Divider() case let .mediaPreviews(mediaPreviews: media): ComposeImageView( images: media.map { (img, _) in img }, @@ -630,6 +644,7 @@ struct ComposeView: View { chosenMedia = [] }, cancelEnabled: !composeState.editing && !composeState.inProgress) + Divider() case let .voicePreview(recordingFileName, _): ComposeVoiceView( recordingFileName: recordingFileName, @@ -642,6 +657,7 @@ struct ComposeView: View { cancelEnabled: !composeState.editing && !composeState.inProgress, stopPlayback: $stopPlayback ) + Divider() case let .filePreview(fileName, _): ComposeFileView( fileName: fileName, @@ -649,19 +665,19 @@ struct ComposeView: View { composeState = composeState.copy(preview: .noPreview) }, cancelEnabled: !composeState.editing && !composeState.inProgress) + Divider() } } private func msgNotAllowedView(_ reason: LocalizedStringKey, icon: String) -> some View { HStack { - Image(systemName: icon).foregroundColor(.secondary) + Image(systemName: icon).foregroundColor(theme.colors.secondary) Text(reason).italic() } .padding(12) - .frame(minHeight: 50) + .frame(minHeight: 54) .frame(maxWidth: .infinity, alignment: .leading) - .background(Color(uiColor: .tertiarySystemGroupedBackground)) - .padding(.top, 8) + .background(.thinMaterial) } @ViewBuilder private func contextItemView() -> some View { @@ -675,6 +691,7 @@ struct ComposeView: View { contextIcon: "arrowshape.turn.up.left", cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) } ) + Divider() case let .editingItem(chatItem: editingItem): ContextItemView( chat: chat, @@ -682,6 +699,7 @@ struct ComposeView: View { contextIcon: "pencil", cancelContextItem: { clearState() } ) + Divider() case let .forwardingItem(chatItem: forwardedItem, _): ContextItemView( chat: chat, @@ -690,6 +708,7 @@ struct ComposeView: View { cancelContextItem: { composeState = composeState.copy(contextItem: .noContextItem) }, showSender: false ) + Divider() } } @@ -712,9 +731,9 @@ struct ComposeView: View { if chat.chatInfo.contact?.nextSendGrpInv ?? false { await sendMemberContactInvitation() } else if case let .forwardingItem(ci, fromChatInfo) = composeState.contextItem { - sent = await forwardItem(ci, fromChatInfo) + sent = await forwardItem(ci, fromChatInfo, ttl) if !composeState.message.isEmpty { - sent = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: nil) + sent = await send(checkLinkPreview(), quoted: sent?.id, live: false, ttl: ttl) } } else if case let .editingItem(ci) = composeState.contextItem { sent = await updateMessage(ci, live: live) @@ -846,6 +865,7 @@ struct ComposeView: View { func sendVideo(_ imageData: (String, UploadContent?), text: String = "", quoted: Int64? = nil, live: Bool = false, ttl: Int?) async -> ChatItem? { let (image, data) = imageData if case let .video(_, url, duration) = data, let savedFile = moveTempFileFromURL(url) { + ChatModel.shared.filesToDelete.remove(url) return await send(.video(text: text, image: image, duration: duration), quoted: quoted, file: savedFile, live: live, ttl: ttl) } return nil @@ -890,13 +910,14 @@ struct ComposeView: View { return nil } - func forwardItem(_ forwardedItem: ChatItem, _ fromChatInfo: ChatInfo) async -> ChatItem? { + func forwardItem(_ forwardedItem: ChatItem, _ fromChatInfo: ChatInfo, _ ttl: Int?) async -> ChatItem? { if let chatItem = await apiForwardChatItem( toChatType: chat.chatInfo.chatType, toChatId: chat.chatInfo.apiId, fromChatType: fromChatInfo.chatType, fromChatId: fromChatInfo.apiId, - itemId: forwardedItem.id + itemId: forwardedItem.id, + ttl: ttl ) { await MainActor.run { chatModel.addChatItem(chat.chatInfo, chatItem) @@ -1064,7 +1085,7 @@ struct ComposeView: View { } else { nil } - let simplexLink = parsedMsg.contains(where: { ft in ft.format?.isSimplexLink ?? false }) + let simplexLink = parsedMsgHasSimplexLink(parsedMsg) return (url, simplexLink) } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift index 2617bc77bc..441a68fccb 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift @@ -25,7 +25,7 @@ func voiceMessageTime_(_ time: TimeInterval?) -> String { struct ComposeVoiceView: View { @EnvironmentObject var chatModel: ChatModel - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme var recordingFileName: String @Binding var recordingTime: TimeInterval? @Binding var recordingState: VoiceMessageRecordingState @@ -50,9 +50,9 @@ struct ComposeVoiceView: View { } .padding(.vertical, 1) .frame(height: ComposeVoiceView.previewHeight) - .background(colorScheme == .light ? sentColorLight : sentColorDark) + .background(theme.appColors.sentMessage) + .frame(minHeight: 54) .frame(maxWidth: .infinity) - .padding(.top, 8) } private func recordingMode() -> some View { @@ -80,7 +80,7 @@ struct ComposeVoiceView: View { Button { startPlayback() } label: { - playPauseIcon("play.fill") + playPauseIcon("play.fill", theme.colors.primary) } Text(voiceMessageTime_(recordingTime)) case .playing: @@ -88,7 +88,7 @@ struct ComposeVoiceView: View { audioPlayer?.pause() playbackState = .paused } label: { - playPauseIcon("pause.fill") + playPauseIcon("pause.fill", theme.colors.primary) } Text(voiceMessageTime_(playbackTime)) case .paused: @@ -96,7 +96,7 @@ struct ComposeVoiceView: View { audioPlayer?.play() playbackState = .playing } label: { - playPauseIcon("play.fill") + playPauseIcon("play.fill", theme.colors.primary) } Text(voiceMessageTime_(playbackTime)) } @@ -131,7 +131,7 @@ struct ComposeVoiceView: View { } } - private func playPauseIcon(_ image: String, _ color: Color = .accentColor) -> some View { + private func playPauseIcon(_ image: String, _ color: Color) -> some View { Image(systemName: image) .resizable() .aspectRatio(contentMode: .fit) @@ -147,9 +147,11 @@ struct ComposeVoiceView: View { } label: { Image(systemName: "multiply") } + .tint(theme.colors.primary) } struct SliderBar: View { + @EnvironmentObject var theme: AppTheme var length: TimeInterval @Binding var progress: TimeInterval? var seek: (TimeInterval) -> Void @@ -158,10 +160,12 @@ struct ComposeVoiceView: View { Slider(value: Binding(get: { progress ?? TimeInterval(0) }, set: { seek($0) }), in: 0 ... length) .frame(maxWidth: .infinity) .frame(height: 4) + .tint(theme.colors.primary) } } private struct ProgressBar: View { + @EnvironmentObject var theme: AppTheme var length: TimeInterval @Binding var progress: TimeInterval? @@ -169,7 +173,7 @@ struct ComposeVoiceView: View { GeometryReader { geometry in ZStack { Rectangle() - .fill(Color.accentColor) + .fill(theme.colors.primary) .frame(width: min(CGFloat((progress ?? TimeInterval(0)) / length) * geometry.size.width, geometry.size.width), height: 4) .animation(.linear, value: progress) } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextInvitingContactMemberView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextInvitingContactMemberView.swift index acb4f6d3e1..82090f312a 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextInvitingContactMemberView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextInvitingContactMemberView.swift @@ -9,19 +9,18 @@ import SwiftUI struct ContextInvitingContactMemberView: View { - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme var body: some View { HStack { Image(systemName: "message") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) Text("Send direct message to connect") } .padding(12) - .frame(minHeight: 50) + .frame(minHeight: 54) .frame(maxWidth: .infinity, alignment: .leading) - .background(colorScheme == .light ? sentColorLight : sentColorDark) - .padding(.top, 8) + .background(.thinMaterial) } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift index 2777d8321c..6245bbe21f 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift @@ -10,7 +10,7 @@ import SwiftUI import SimpleXChat struct ContextItemView: View { - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat let contextItem: ChatItem let contextIcon: String @@ -23,10 +23,10 @@ struct ContextItemView: View { .resizable() .aspectRatio(contentMode: .fit) .frame(width: 16, height: 16) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) if showSender, let sender = contextItem.memberDisplayName { VStack(alignment: .leading, spacing: 4) { - Text(sender).font(.caption).foregroundColor(.secondary) + Text(sender).font(.caption).foregroundColor(theme.colors.secondary) msgContentView(lines: 2) } } else { @@ -40,12 +40,12 @@ struct ContextItemView: View { } label: { Image(systemName: "multiply") } + .tint(theme.colors.primary) } .padding(12) - .frame(minHeight: 50) + .frame(minHeight: 54) .frame(maxWidth: .infinity) - .background(chatItemFrameColor(contextItem, colorScheme)) - .padding(.top, 8) + .background(chatItemFrameColor(contextItem, theme)) } private func msgContentView(lines: Int) -> some View { @@ -55,7 +55,7 @@ struct ContextItemView: View { } private func contextMsgPreview() -> Text { - return attachment() + messageText(contextItem.text, contextItem.formattedText, nil, preview: true, showSecrets: false) + return attachment() + messageText(contextItem.text, contextItem.formattedText, nil, preview: true, showSecrets: false, secondaryColor: theme.colors.secondary) func attachment() -> Text { switch contextItem.content.msgContent { diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift index f2c7221835..ad47b7351a 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/NativeTextEditor.swift @@ -28,6 +28,7 @@ struct NativeTextEditor: UIViewRepresentable { func makeUIView(context: Context) -> UITextView { let field = CustomUITextField(height: _height) + field.backgroundColor = .clear field.text = text field.textAlignment = alignment(text) field.autocapitalizationType = .sentences diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift index 8b528a201c..9ad6e986bd 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/SendMessageView.swift @@ -13,6 +13,7 @@ private let liveMsgInterval: UInt64 = 3000_000000 struct SendMessageView: View { @Binding var composeState: ComposeState + @EnvironmentObject var theme: AppTheme var sendMessage: (Int?) -> Void var sendLiveMessage: (() async -> Void)? = nil var updateLiveMessage: (() async -> Void)? = nil @@ -43,13 +44,14 @@ struct SendMessageView: View { var body: some View { ZStack { + let composeShape = RoundedRectangle(cornerSize: CGSize(width: 20, height: 20)) HStack(alignment: .bottom) { ZStack(alignment: .leading) { if case .voicePreview = composeState.preview { Text("Voice message…") .font(teFont.italic()) .multilineTextAlignment(.leading) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .padding(.horizontal, 10) .padding(.vertical, 8) .frame(maxWidth: .infinity) @@ -65,7 +67,6 @@ struct SendMessageView: View { .fixedSize(horizontal: false, vertical: true) } } - if progressByTimeout { ProgressView() .scaleEffect(1.4) @@ -83,10 +84,9 @@ struct SendMessageView: View { } } .padding(.vertical, 1) - .overlay( - RoundedRectangle(cornerSize: CGSize(width: 20, height: 20)) - .strokeBorder(.secondary, lineWidth: 0.3, antialiased: true) - ) + .background(theme.colors.background) + .clipShape(composeShape) + .overlay(composeShape.strokeBorder(.secondary, lineWidth: 0.5).opacity(0.7)) } .onChange(of: composeState.message, perform: { text in updateFont(text) }) .onChange(of: composeState.inProgress) { inProgress in @@ -224,8 +224,7 @@ struct SendMessageView: View { @ViewBuilder private func sendButtonContextMenuItems() -> some View { if composeState.liveMessage == nil, - !composeState.editing, - !composeState.forwarding { + !composeState.editing { if case .noContextItem = composeState.contextItem, !composeState.voicePreview, let send = sendLiveMessage, @@ -248,6 +247,7 @@ struct SendMessageView: View { } private struct RecordVoiceMessageButton: View { + @EnvironmentObject var theme: AppTheme var startVoiceMessageRecording: (() -> Void)? var finishVoiceMessageRecording: (() -> Void)? @Binding var holdingVMR: Bool @@ -257,7 +257,10 @@ struct SendMessageView: View { var body: some View { Button(action: {}) { Image(systemName: "mic.fill") - .foregroundColor(.accentColor) + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .foregroundColor(theme.colors.primary) } .disabled(disabled) .frame(width: 29, height: 29) @@ -310,7 +313,10 @@ struct SendMessageView: View { } } label: { Image(systemName: "mic") - .foregroundColor(.secondary) + .resizable() + .scaledToFit() + .frame(width: 20, height: 20) + .foregroundColor(theme.colors.secondary) } .disabled(composeState.inProgress) .frame(width: 29, height: 29) @@ -324,7 +330,7 @@ struct SendMessageView: View { Image(systemName: "multiply") .resizable() .scaledToFit() - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) .frame(width: 15, height: 15) } .frame(width: 29, height: 29) @@ -341,7 +347,7 @@ struct SendMessageView: View { Image(systemName: "bolt.fill") .resizable() .scaledToFit() - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) .frame(width: 20, height: 20) } .frame(width: 29, height: 29) @@ -384,7 +390,7 @@ struct SendMessageView: View { } Task { _ = try? await Task.sleep(nanoseconds: liveMsgInterval) - while composeState.liveMessage != nil { + while await composeState.liveMessage != nil { await update() _ = try? await Task.sleep(nanoseconds: liveMsgInterval) } @@ -395,7 +401,7 @@ struct SendMessageView: View { private func finishVoiceMessageRecordingButton() -> some View { Button(action: { finishVoiceMessageRecording?() }) { Image(systemName: "stop.fill") - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) } .disabled(composeState.inProgress) .frame(width: 29, height: 29) diff --git a/apps/ios/Shared/Views/Chat/ContactPreferencesView.swift b/apps/ios/Shared/Views/Chat/ContactPreferencesView.swift index 86acbf6d54..b3fab958bc 100644 --- a/apps/ios/Shared/Views/Chat/ContactPreferencesView.swift +++ b/apps/ios/Shared/Views/Chat/ContactPreferencesView.swift @@ -12,6 +12,7 @@ import SimpleXChat struct ContactPreferencesView: View { @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme @Binding var contact: Contact @State var featuresAllowed: ContactFeaturesAllowed @State var currentFeaturesAllowed: ContactFeaturesAllowed @@ -66,8 +67,8 @@ struct ContactPreferencesView: View { .frame(height: 36) infoRow("Contact allows", pref.contactPreference.allow.text) } - header: { featureHeader(feature, enabled) } - footer: { featureFooter(feature, enabled) } + header: { featureHeader(feature, enabled).foregroundColor(theme.colors.secondary) } + footer: { featureFooter(feature, enabled).foregroundColor(theme.colors.secondary) } } private func timedMessagesFeatureSection() -> some View { @@ -102,8 +103,8 @@ struct ContactPreferencesView: View { infoRow("Delete after", timeText(pref.contactPreference.ttl)) } } - header: { featureHeader(.timedMessages, enabled) } - footer: { featureFooter(.timedMessages, enabled) } + header: { featureHeader(.timedMessages, enabled).foregroundColor(theme.colors.secondary) } + footer: { featureFooter(.timedMessages, enabled).foregroundColor(theme.colors.secondary) } } private func featureHeader(_ feature: ChatFeature, _ enabled: FeatureEnabled) -> some View { diff --git a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift index ed2afb91b3..189ab95494 100644 --- a/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift +++ b/apps/ios/Shared/Views/Chat/Group/AddGroupMembersView.swift @@ -21,6 +21,7 @@ struct AddGroupMembersView: View { struct AddGroupMembersViewCommon: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme var chat: Chat @State var groupInfo: GroupInfo var creatingGroup: Bool = false @@ -34,7 +35,7 @@ struct AddGroupMembersViewCommon: View { private enum AddGroupMembersAlert: Identifiable { case prohibitedToInviteIncognito - case error(title: LocalizedStringKey, error: LocalizedStringKey = "") + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -46,14 +47,13 @@ struct AddGroupMembersViewCommon: View { var body: some View { if creatingGroup { - NavigationView { - addGroupMembersView() - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button ("Skip") { addedMembersCb(selectedContacts) } - } + addGroupMembersView() + .navigationBarBackButtonHidden() + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button ("Skip") { addedMembersCb(selectedContacts) } } - } + } } else { addGroupMembersView() } @@ -70,7 +70,7 @@ struct AddGroupMembersViewCommon: View { if (membersToAdd.isEmpty) { Text("No contacts to add") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .padding() .frame(maxWidth: .infinity, alignment: .center) .listRowBackground(Color.clear) @@ -90,16 +90,18 @@ struct AddGroupMembersViewCommon: View { Button { selectedContacts.removeAll() } label: { Text("Clear").font(.caption) } Spacer() Text("\(count) contact(s) selected") + .foregroundColor(theme.colors.secondary) } } else { Text("No contacts selected") .frame(maxWidth: .infinity, alignment: .trailing) + .foregroundColor(theme.colors.secondary) } } } Section { - searchFieldView(text: $searchText, focussed: $searchFocussed) + searchFieldView(text: $searchText, focussed: $searchFocussed, theme.colors.primary, theme.colors.secondary) .padding(.leading, 2) let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase let members = s == "" ? membersToAdd : membersToAdd.filter { $0.chatViewName.localizedLowercase.contains(s) } @@ -119,12 +121,13 @@ struct AddGroupMembersViewCommon: View { message: Text("You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile") ) case let .error(title, error): - return Alert(title: Text(title), message: Text(error)) + return mkAlert(title: title, message: error) } } .onChange(of: selectedContacts) { _ in searchFocussed = false } + .modifier(ThemedBackground(grouped: true)) } private func inviteMembersButton() -> some View { @@ -172,14 +175,14 @@ struct AddGroupMembersViewCommon: View { var iconColor: Color if prohibitedToInviteIncognito { icon = "theatermasks.circle.fill" - iconColor = Color(uiColor: .tertiaryLabel) + iconColor = Color(uiColor: .tertiaryLabel).asAnotherColorFromSecondary(theme) } else { if checked { icon = "checkmark.circle.fill" - iconColor = .accentColor + iconColor = theme.colors.primary } else { icon = "circle" - iconColor = Color(uiColor: .tertiaryLabel) + iconColor = Color(uiColor: .tertiaryLabel).asAnotherColorFromSecondary(theme) } } return Button { @@ -197,7 +200,7 @@ struct AddGroupMembersViewCommon: View { ProfileImage(imageStr: contact.image, size: 30) .padding(.trailing, 2) Text(ChatInfo.direct(contact: contact).chatViewName) - .foregroundColor(prohibitedToInviteIncognito ? .secondary : .primary) + .foregroundColor(prohibitedToInviteIncognito ? theme.colors.secondary : theme.colors.onBackground) .lineLimit(1) Spacer() Image(systemName: icon) @@ -207,7 +210,7 @@ struct AddGroupMembersViewCommon: View { } } -func searchFieldView(text: Binding, focussed: FocusState.Binding) -> some View { +func searchFieldView(text: Binding, focussed: FocusState.Binding, _ onBackgroundColor: Color, _ secondaryColor: Color) -> some View { HStack { Image(systemName: "magnifyingglass") .resizable() @@ -216,7 +219,7 @@ func searchFieldView(text: Binding, focussed: FocusState.Binding) .padding(.trailing, 10) TextField("Search", text: text) .focused(focussed) - .foregroundColor(.primary) + .foregroundColor(onBackgroundColor) .frame(maxWidth: .infinity) Image(systemName: "xmark.circle.fill") .resizable() @@ -228,7 +231,7 @@ func searchFieldView(text: Binding, focussed: FocusState.Binding) focussed.wrappedValue = false } } - .foregroundColor(.secondary) + .foregroundColor(secondaryColor) .frame(height: 36) } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index c22f3f0fed..9385633060 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -13,13 +13,16 @@ let SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20 struct GroupChatInfoView: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss: DismissAction @ObservedObject var chat: Chat @Binding var groupInfo: GroupInfo + var onSearch: () -> Void @State private var alert: GroupChatInfoViewAlert? = nil @State private var groupLink: String? @State private var groupLinkMemberRole: GroupMemberRole = .member - @State private var showAddMembersSheet: Bool = false + @State private var groupLinkNavLinkActive: Bool = false + @State private var addMembersNavLinkActive: Bool = false @State private var connectionStats: ConnectionStats? @State private var connectionCode: String? @State private var sendReceipts = SendReceipts.userDefault(true) @@ -39,7 +42,7 @@ struct GroupChatInfoView: View { case blockForAllAlert(mem: GroupMember) case unblockForAllAlert(mem: GroupMember) case removeMemberAlert(mem: GroupMember) - case error(title: LocalizedStringKey, error: LocalizedStringKey) + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -67,6 +70,15 @@ struct GroupChatInfoView: View { List { groupInfoHeader() .listRowBackground(Color.clear) + .padding(.bottom, 18) + + infoActionButtons() + .padding(.horizontal) + .frame(maxWidth: .infinity) + .frame(height: infoViewActionButtonHeight) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) Section { if groupInfo.canEdit { @@ -81,13 +93,19 @@ struct GroupChatInfoView: View { } else { sendReceiptsOptionDisabled() } + NavigationLink { + ChatWallpaperEditorSheet(chat: chat) + } label: { + Label("Chat theme", systemImage: "photo") + } } header: { Text("") } footer: { Text("Only group owners can change group preferences.") + .foregroundColor(theme.colors.secondary) } - Section("\(members.count + 1) members") { + Section(header: Text("\(members.count + 1) members").foregroundColor(theme.colors.secondary)) { if groupInfo.canAddMembers { groupLinkButton() if (chat.chatInfo.incognito) { @@ -99,7 +117,7 @@ struct GroupChatInfoView: View { } } if members.count > 8 { - searchFieldView(text: $searchText, focussed: $searchFocussed) + searchFieldView(text: $searchText, focussed: $searchFocussed, theme.colors.onBackground, theme.colors.secondary) .padding(.leading, 8) } let s = searchText.trimmingCharacters(in: .whitespaces).localizedLowercase @@ -129,12 +147,13 @@ struct GroupChatInfoView: View { } if developerTools { - Section(header: Text("For console")) { + Section(header: Text("For console").foregroundColor(theme.colors.secondary)) { infoRow("Local name", chat.chatInfo.localDisplayName) infoRow("Database ID", "\(chat.chatInfo.apiId)") } } } + .modifier(ThemedBackground(grouped: true)) .navigationBarHidden(true) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) @@ -150,7 +169,7 @@ struct GroupChatInfoView: View { case let .blockForAllAlert(mem): return blockForAllAlert(groupInfo, mem) case let .unblockForAllAlert(mem): return unblockForAllAlert(groupInfo, mem) case let .removeMemberAlert(mem): return removeMemberAlert(mem) - case let .error(title, error): return Alert(title: Text(title), message: Text(error)) + case let .error(title, error): return mkAlert(title: title, message: error) } } .onAppear { @@ -166,7 +185,6 @@ struct GroupChatInfoView: View { logger.error("GroupChatInfoView apiGetGroupLink: \(responseError(error))") } } - .keyboardPadding() } private func groupInfoHeader() -> some View { @@ -190,44 +208,116 @@ struct GroupChatInfoView: View { .frame(maxWidth: .infinity, alignment: .center) } + func infoActionButtons() -> some View { + GeometryReader { g in + let buttonWidth = g.size.width / 4 + HStack(alignment: .center, spacing: 8) { + searchButton(width: buttonWidth) + if groupInfo.canAddMembers { + addMembersActionButton(width: buttonWidth) + } + muteButton(width: buttonWidth) + } + .frame(maxWidth: .infinity, alignment: .center) + } + } + + private func searchButton(width: CGFloat) -> some View { + InfoViewButton(image: "magnifyingglass", title: "search", width: width) { + dismiss() + onSearch() + } + .disabled(!groupInfo.ready || chat.chatItems.isEmpty) + } + + @ViewBuilder private func addMembersActionButton(width: CGFloat) -> some View { + if chat.chatInfo.incognito { + ZStack { + InfoViewButton(image: "link.badge.plus", title: "invite", width: width) { + groupLinkNavLinkActive = true + } + + NavigationLink(isActive: $groupLinkNavLinkActive) { + groupLinkDestinationView() + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() + } + .disabled(!groupInfo.ready) + } else { + ZStack { + InfoViewButton(image: "person.fill.badge.plus", title: "invite", width: width) { + addMembersNavLinkActive = true + } + + NavigationLink(isActive: $addMembersNavLinkActive) { + addMembersDestinationView() + } label: { + EmptyView() + } + .frame(width: 1, height: 1) + .hidden() + } + .disabled(!groupInfo.ready) + } + } + + private func muteButton(width: CGFloat) -> some View { + InfoViewButton( + image: chat.chatInfo.ntfsEnabled ? "speaker.slash.fill" : "speaker.wave.2.fill", + title: chat.chatInfo.ntfsEnabled ? "mute" : "unmute", + width: width + ) { + toggleNotifications(chat, enableNtfs: !chat.chatInfo.ntfsEnabled) + } + .disabled(!groupInfo.ready) + } + private func addMembersButton() -> some View { NavigationLink { - AddGroupMembersView(chat: chat, groupInfo: groupInfo) - .onAppear { - searchFocussed = false - Task { - let groupMembers = await apiListMembers(groupInfo.groupId) - await MainActor.run { - chatModel.groupMembers = groupMembers.map { GMember.init($0) } - } - } - } + addMembersDestinationView() } label: { Label("Invite members", systemImage: "plus") } } + private func addMembersDestinationView() -> some View { + AddGroupMembersView(chat: chat, groupInfo: groupInfo) + .onAppear { + searchFocussed = false + Task { + let groupMembers = await apiListMembers(groupInfo.groupId) + await MainActor.run { + chatModel.groupMembers = groupMembers.map { GMember.init($0) } + chatModel.populateGroupMembersIndexes() + } + } + } + } + private struct MemberRowView: View { var groupInfo: GroupInfo @ObservedObject var groupMember: GMember + @EnvironmentObject var theme: AppTheme var user: Bool = false @Binding var alert: GroupChatInfoViewAlert? var body: some View { let member = groupMember.wrapped let v = HStack{ - ProfileImage(imageStr: member.image, size: 38) + MemberProfileImage(member, size: 38) .padding(.trailing, 2) // TODO server connection status VStack(alignment: .leading) { - let t = Text(member.chatViewName).foregroundColor(member.memberIncognito ? .indigo : .primary) + let t = Text(member.chatViewName).foregroundColor(member.memberIncognito ? .indigo : theme.colors.onBackground) (member.verified ? memberVerifiedShield + t : t) .lineLimit(1) - let s = Text(member.memberStatus.shortText) - (user ? Text ("you: ") + s : s) + (user ? Text ("you: ") + Text(member.memberStatus.shortText) : Text(memberConnStatus(member))) .lineLimit(1) .font(.caption) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } Spacer() memberInfo(member) @@ -257,15 +347,25 @@ struct GroupChatInfoView: View { } } + private func memberConnStatus(_ member: GroupMember) -> LocalizedStringKey { + if member.activeConn?.connDisabled ?? false { + return "disabled" + } else if member.activeConn?.connInactive ?? false { + return "inactive" + } else { + return member.memberStatus.shortText + } + } + @ViewBuilder private func memberInfo(_ member: GroupMember) -> some View { if member.blocked { Text("blocked") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } else { let role = member.memberRole if [.owner, .admin, .observer].contains(role) { Text(member.memberRole.text) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } } } @@ -276,13 +376,13 @@ struct GroupChatInfoView: View { Button { alert = .blockMemberAlert(mem: member) } label: { - Label("Block member", systemImage: "hand.raised").foregroundColor(.secondary) + Label("Block member", systemImage: "hand.raised").foregroundColor(theme.colors.secondary) } } else { Button { alert = .unblockMemberAlert(mem: member) } label: { - Label("Unblock member", systemImage: "hand.raised.slash").foregroundColor(.accentColor) + Label("Unblock member", systemImage: "hand.raised.slash").foregroundColor(theme.colors.primary) } } } @@ -294,13 +394,13 @@ struct GroupChatInfoView: View { Button { alert = .unblockForAllAlert(mem: member) } label: { - Label("Unblock for all", systemImage: "hand.raised.slash").foregroundColor(.accentColor) + Label("Unblock for all", systemImage: "hand.raised.slash").foregroundColor(theme.colors.primary) } } else { Button { alert = .blockForAllAlert(mem: member) } label: { - Label("Block for all", systemImage: "hand.raised").foregroundColor(.secondary) + Label("Block for all", systemImage: "hand.raised").foregroundColor(theme.colors.secondary) } } } @@ -316,6 +416,14 @@ struct GroupChatInfoView: View { } } } + + private var memberVerifiedShield: Text { + (Text(Image(systemName: "checkmark.shield")) + Text(" ")) + .font(.caption) + .baselineOffset(2) + .kerning(-2) + .foregroundColor(theme.colors.secondary) + } } private func memberInfoView(_ groupMember: GMember) -> some View { @@ -325,15 +433,7 @@ struct GroupChatInfoView: View { private func groupLinkButton() -> some View { NavigationLink { - GroupLinkView( - groupId: groupInfo.groupId, - groupLink: $groupLink, - groupLinkMemberRole: $groupLinkMemberRole, - showTitle: false, - creatingGroup: false - ) - .navigationBarTitle("Group link") - .navigationBarTitleDisplayMode(.large) + groupLinkDestinationView() } label: { if groupLink == nil { Label("Create group link", systemImage: "link.badge.plus") @@ -343,6 +443,19 @@ struct GroupChatInfoView: View { } } + private func groupLinkDestinationView() -> some View { + GroupLinkView( + groupId: groupInfo.groupId, + groupLink: $groupLink, + groupLinkMemberRole: $groupLinkMemberRole, + showTitle: false, + creatingGroup: false + ) + .navigationBarTitle("Group link") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + } + private func editGroupButton() -> some View { NavigationLink { GroupProfileView( @@ -350,6 +463,7 @@ struct GroupChatInfoView: View { groupProfile: groupInfo.groupProfile ) .navigationBarTitle("Group profile") + .modifier(ThemedBackground()) .navigationBarTitleDisplayMode(.large) } label: { Label("Edit group profile", systemImage: "pencil") @@ -364,6 +478,7 @@ struct GroupChatInfoView: View { welcomeText: groupInfo.groupProfile.description ?? "" ) .navigationTitle("Welcome message") + .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) } label: { groupInfo.groupProfile.description == nil @@ -518,6 +633,7 @@ func groupPreferencesButton(_ groupInfo: Binding, _ creatingGroup: Bo creatingGroup: creatingGroup ) .navigationBarTitle("Group preferences") + .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) } label: { if creatingGroup { @@ -528,14 +644,6 @@ func groupPreferencesButton(_ groupInfo: Binding, _ creatingGroup: Bo } } -private var memberVerifiedShield: Text { - (Text(Image(systemName: "checkmark.shield")) + Text(" ")) - .font(.caption) - .baselineOffset(2) - .kerning(-2) - .foregroundColor(.secondary) -} - func cantInviteIncognitoAlert() -> Alert { Alert( title: Text("Can't invite contacts!"), @@ -554,7 +662,8 @@ struct GroupChatInfoView_Previews: PreviewProvider { static var previews: some View { GroupChatInfoView( chat: Chat(chatInfo: ChatInfo.sampleData.group, chatItems: []), - groupInfo: Binding.constant(GroupInfo.sampleData) + groupInfo: Binding.constant(GroupInfo.sampleData), + onSearch: {} ) } } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift index c782e2a717..39288e2d52 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift @@ -22,7 +22,7 @@ struct GroupLinkView: View { private enum GroupLinkAlert: Identifiable { case deleteLink - case error(title: LocalizedStringKey, error: LocalizedStringKey = "") + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -34,14 +34,13 @@ struct GroupLinkView: View { var body: some View { if creatingGroup { - NavigationView { - groupLinkView() - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button ("Continue") { linkCreatedCb?() } - } + groupLinkView() + .navigationBarBackButtonHidden() + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button ("Continue") { linkCreatedCb?() } } - } + } } else { groupLinkView() } @@ -113,7 +112,7 @@ struct GroupLinkView: View { }, secondaryButton: .cancel() ) case let .error(title, error): - return Alert(title: Text(title), message: Text(error)) + return mkAlert(title: title, message: error) } } .onChange(of: groupLinkMemberRole) { _ in @@ -133,6 +132,7 @@ struct GroupLinkView: View { shouldCreate = false } } + .modifier(ThemedBackground(grouped: true)) } private func createGroupLink() { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index a24608b7e7..ddf3b8e4b9 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct GroupMemberInfoView: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss: DismissAction @State var groupInfo: GroupInfo @ObservedObject var groupMember: GMember @@ -35,7 +36,9 @@ struct GroupMemberInfoView: View { case abortSwitchAddressAlert case syncConnectionForceAlert case planAndConnectAlert(alert: PlanAndConnectAlert) - case error(title: LocalizedStringKey, error: LocalizedStringKey) + case queueInfo(info: String) + case someAlert(alert: SomeAlert) + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -49,6 +52,8 @@ struct GroupMemberInfoView: View { case .abortSwitchAddressAlert: return "abortSwitchAddressAlert" case .syncConnectionForceAlert: return "syncConnectionForceAlert" case let .planAndConnectAlert(alert): return "planAndConnectAlert \(alert.id)" + case let .queueInfo(info): return "queueInfo \(info)" + case let .someAlert(alert): return "someAlert \(alert.id)" case let .error(title, _): return "error \(title)" } } @@ -62,10 +67,11 @@ struct GroupMemberInfoView: View { } } - private func knownDirectChat(_ contactId: Int64) -> Chat? { + private func knownDirectChat(_ contactId: Int64) -> (Chat, Contact)? { if let chat = chatModel.getContactChat(contactId), - chat.chatInfo.contact?.directOrUsed == true { - return chat + let contact = chat.chatInfo.contact, + contact.directOrUsed == true { + return (chat, contact) } else { return nil } @@ -73,143 +79,154 @@ struct GroupMemberInfoView: View { private func groupMemberInfoView() -> some View { ZStack { - VStack { - let member = groupMember.wrapped - List { - groupMemberInfoHeader(member) - .listRowBackground(Color.clear) + let member = groupMember.wrapped + List { + groupMemberInfoHeader(member) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .padding(.bottom, 18) - if member.memberActive { - Section { - if let contactId = member.memberContactId, let chat = knownDirectChat(contactId) { - knownDirectChatButton(chat) - } else if groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) { - if let contactId = member.memberContactId { - newDirectChatButton(contactId) - } else if member.activeConn?.peerChatVRange.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) ?? false { - createMemberContactButton() - } - } - if let code = connectionCode { verifyCodeButton(code) } - if let connStats = connectionStats, - connStats.ratchetSyncAllowed { - synchronizeConnectionButton() - } - // } else if developerTools { - // synchronizeConnectionButtonForce() - // } + infoActionButtons(member) + .padding(.horizontal) + .frame(maxWidth: .infinity) + .frame(height: infoViewActionButtonHeight) + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + + if member.memberActive { + Section { + if let code = connectionCode { verifyCodeButton(code) } + if let connStats = connectionStats, + connStats.ratchetSyncAllowed { + synchronizeConnectionButton() } + // } else if developerTools { + // synchronizeConnectionButtonForce() + // } } + } - if let contactLink = member.contactLink { - Section { - SimpleXLinkQRCode(uri: contactLink) - Button { - showShareSheet(items: [simplexChatLink(contactLink)]) - } label: { - Label("Share address", systemImage: "square.and.arrow.up") - } - if let contactId = member.memberContactId { - if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) { - connectViaAddressButton(contactLink) - } - } else { + if let contactLink = member.contactLink { + Section { + SimpleXLinkQRCode(uri: contactLink) + Button { + showShareSheet(items: [simplexChatLink(contactLink)]) + } label: { + Label("Share address", systemImage: "square.and.arrow.up") + } + if let contactId = member.memberContactId { + if knownDirectChat(contactId) == nil && !groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) { connectViaAddressButton(contactLink) } - } header: { - Text("Address") - } footer: { - Text("You can share this address with your contacts to let them connect with **\(member.displayName)**.") - } - } - - Section("Member") { - infoRow("Group", groupInfo.displayName) - - if let roles = member.canChangeRoleTo(groupInfo: groupInfo) { - Picker("Change role", selection: $newRole) { - ForEach(roles) { role in - Text(role.text) - } - } - .frame(height: 36) } else { - infoRow("Role", member.memberRole.text) + connectViaAddressButton(contactLink) } + } header: { + Text("Address") + .foregroundColor(theme.colors.secondary) + } footer: { + Text("You can share this address with your contacts to let them connect with **\(member.displayName)**.") + .foregroundColor(theme.colors.secondary) + } + } - // TODO invited by - need to get contact by contact id + Section(header: Text("Member").foregroundColor(theme.colors.secondary)) { + infoRow("Group", groupInfo.displayName) + + if let roles = member.canChangeRoleTo(groupInfo: groupInfo) { + Picker("Change role", selection: $newRole) { + ForEach(roles) { role in + Text(role.text) + } + } + .frame(height: 36) + } else { + infoRow("Role", member.memberRole.text) + } + } + + if let connStats = connectionStats { + Section(header: Text("Servers").foregroundColor(theme.colors.secondary)) { + // TODO network connection status + Button("Change receiving address") { + alert = .switchAddressAlert + } + .disabled( + connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil } + || connStats.ratchetSyncSendProhibited + ) + if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) { + Button("Abort changing address") { + alert = .abortSwitchAddressAlert + } + .disabled( + connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch } + || connStats.ratchetSyncSendProhibited + ) + } + smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer }, theme.colors.secondary) + smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer }, theme.colors.secondary) + } + } + + if groupInfo.membership.memberRole >= .admin { + adminDestructiveSection(member) + } else { + nonAdminBlockSection(member) + } + + if developerTools { + Section(header: Text("For console").foregroundColor(theme.colors.secondary)) { + infoRow("Local name", member.localDisplayName) + infoRow("Database ID", "\(member.groupMemberId)") if let conn = member.activeConn { let connLevelDesc = conn.connLevel == 0 ? NSLocalizedString("direct", comment: "connection level description") : String.localizedStringWithFormat(NSLocalizedString("indirect (%d)", comment: "connection level description"), conn.connLevel) infoRow("Connection", connLevelDesc) } - } - - if let connStats = connectionStats { - Section("Servers") { - // TODO network connection status - Button("Change receiving address") { - alert = .switchAddressAlert - } - .disabled( - connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil } - || connStats.ratchetSyncSendProhibited - ) - if connStats.rcvQueuesInfo.contains(where: { $0.rcvSwitchStatus != nil }) { - Button("Abort changing address") { - alert = .abortSwitchAddressAlert + Button ("Debug delivery") { + Task { + do { + let info = queueInfoText(try await apiGroupMemberQueueInfo(groupInfo.apiId, member.groupMemberId)) + await MainActor.run { alert = .queueInfo(info: info) } + } catch let e { + logger.error("apiContactQueueInfo error: \(responseError(e))") + let a = getErrorAlert(e, "Error") + await MainActor.run { alert = .error(title: a.title, error: a.message) } } - .disabled( - connStats.rcvQueuesInfo.contains { $0.rcvSwitchStatus != nil && !$0.canAbortSwitch } - || connStats.ratchetSyncSendProhibited - ) } - smpServers("Receiving via", connStats.rcvQueuesInfo.map { $0.rcvServer }) - smpServers("Sending via", connStats.sndQueuesInfo.map { $0.sndServer }) - } - } - - if groupInfo.membership.memberRole >= .admin { - adminDestructiveSection(member) - } else { - nonAdminBlockSection(member) - } - - if developerTools { - Section("For console") { - infoRow("Local name", member.localDisplayName) - infoRow("Database ID", "\(member.groupMemberId)") } } } - .navigationBarHidden(true) - .onAppear { - if #unavailable(iOS 16) { - // 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))") - } + } + .navigationBarHidden(true) + .onAppear { + if #unavailable(iOS 16) { + // 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))") } } - .onChange(of: newRole) { newRole in - if newRole != member.memberRole { - alert = .changeMemberRoleAlert(mem: member, role: newRole) - } - } - .onChange(of: member.memberRole) { role in - newRole = role + } + .onChange(of: newRole) { newRole in + if newRole != member.memberRole { + alert = .changeMemberRoleAlert(mem: member, role: newRole) } } + .onChange(of: member.memberRole) { role in + newRole = role + } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .alert(item: $alert) { alertItem in switch(alertItem) { @@ -223,7 +240,9 @@ struct GroupMemberInfoView: View { case .abortSwitchAddressAlert: return abortSwitchAddressAlert(abortSwitchMemberAddress) case .syncConnectionForceAlert: return syncConnectionForceAlert({ syncMemberConnection(force: true) }) case let .planAndConnectAlert(alert): return planAndConnectAlert(alert, dismiss: true) - case let .error(title, error): return Alert(title: Text(title), message: Text(error)) + case let .queueInfo(info): return queueInfoAlert(info) + case let .someAlert(a): return a.alert + case let .error(title, error): return mkAlert(title: title, message: error) } } .actionSheet(item: $sheet) { s in planAndConnectActionSheet(s, dismiss: true) } @@ -232,6 +251,58 @@ struct GroupMemberInfoView: View { ProgressView().scaleEffect(2) } } + .modifier(ThemedBackground(grouped: true)) + } + + func infoActionButtons(_ member: GroupMember) -> some View { + GeometryReader { g in + let buttonWidth = g.size.width / 4 + HStack(alignment: .center, spacing: 8) { + if let contactId = member.memberContactId, let (chat, contact) = knownDirectChat(contactId) { + knownDirectChatButton(chat, width: buttonWidth) + AudioCallButton(chat: chat, contact: contact, width: buttonWidth) { alert = .someAlert(alert: $0) } + VideoButton(chat: chat, contact: contact, width: buttonWidth) { alert = .someAlert(alert: $0) } + } else if groupInfo.fullGroupPreferences.directMessages.on(for: groupInfo.membership) { + if let contactId = member.memberContactId { + newDirectChatButton(contactId, width: buttonWidth) + } else if member.activeConn?.peerChatVRange.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) ?? false { + createMemberContactButton(width: buttonWidth) + } + InfoViewButton(image: "phone.fill", title: "call", disabledLook: true, width: buttonWidth) { showSendMessageToEnableCallsAlert() + } + InfoViewButton(image: "video.fill", title: "video", disabledLook: true, width: buttonWidth) { showSendMessageToEnableCallsAlert() + } + } else { // no known contact chat && directMessages are off + InfoViewButton(image: "message.fill", title: "message", disabledLook: true, width: buttonWidth) { showDirectMessagesProhibitedAlert("Can't message member") + } + InfoViewButton(image: "phone.fill", title: "call", disabledLook: true, width: buttonWidth) { showDirectMessagesProhibitedAlert("Can't call member") + } + InfoViewButton(image: "video.fill", title: "video", disabledLook: true, width: buttonWidth) { showDirectMessagesProhibitedAlert("Can't call member") + } + } + } + .frame(maxWidth: .infinity, alignment: .center) + } + } + + func showSendMessageToEnableCallsAlert() { + alert = .someAlert(alert: SomeAlert( + alert: mkAlert( + title: "Can't call member", + message: "Send message to enable calls." + ), + id: "can't call member, send message" + )) + } + + func showDirectMessagesProhibitedAlert(_ title: LocalizedStringKey) { + alert = .someAlert(alert: SomeAlert( + alert: mkAlert( + title: title, + message: "Direct messages between members are prohibited in this group." + ), + id: "can't message member, direct messages prohibited" + )) } func connectViaAddressButton(_ contactLink: String) -> some View { @@ -248,36 +319,32 @@ struct GroupMemberInfoView: View { } } - func knownDirectChatButton(_ chat: Chat) -> some View { - Button { - dismissAllSheets(animated: true) - DispatchQueue.main.async { - chatModel.chatId = chat.id - } - } label: { - Label("Send direct message", systemImage: "message") - } - } - - func newDirectChatButton(_ contactId: Int64) -> some View { - Button { - do { - let chat = try apiGetChat(type: .direct, id: contactId) - chatModel.addChat(chat) + func knownDirectChatButton(_ chat: Chat, width: CGFloat) -> some View { + InfoViewButton(image: "message.fill", title: "message", width: width) { + ItemsModel.shared.loadOpenChat(chat.id) { dismissAllSheets(animated: true) - DispatchQueue.main.async { - chatModel.chatId = chat.id - } - } catch let error { - logger.error("openDirectChatButton apiGetChat error: \(responseError(error))") } - } label: { - Label("Send direct message", systemImage: "message") } } - func createMemberContactButton() -> some View { - Button { + func newDirectChatButton(_ contactId: Int64, width: CGFloat) -> some View { + InfoViewButton(image: "message.fill", title: "message", width: width) { + Task { + do { + let chat = try await apiGetChat(type: .direct, id: contactId) + chatModel.addChat(chat) + ItemsModel.shared.loadOpenChat(chat.id) { + dismissAllSheets(animated: true) + } + } catch let error { + logger.error("openDirectChatButton apiGetChat error: \(responseError(error))") + } + } + } + } + + func createMemberContactButton(width: CGFloat) -> some View { + InfoViewButton(image: "message.fill", title: "message", width: width) { progressIndicator = true Task { do { @@ -285,9 +352,10 @@ struct GroupMemberInfoView: View { await MainActor.run { progressIndicator = false chatModel.addChat(Chat(chatInfo: .direct(contact: memberContact))) - dismissAllSheets(animated: true) - chatModel.chatId = memberContact.id - chatModel.setContactNetworkStatus(memberContact, .connected) + ItemsModel.shared.loadOpenChat(memberContact.id) { + dismissAllSheets(animated: true) + } + NetworkModel.shared.setContactNetworkStatus(memberContact, .connected) } } catch let error { logger.error("createMemberContactButton apiCreateMemberContact error: \(responseError(error))") @@ -298,20 +366,18 @@ struct GroupMemberInfoView: View { } } } - } label: { - Label("Send direct message", systemImage: "message") } } private func groupMemberInfoHeader(_ mem: GroupMember) -> some View { VStack { - ProfileImage(imageStr: mem.image, size: 192, color: Color(uiColor: .tertiarySystemFill)) + MemberProfileImage(mem, size: 192, color: Color(uiColor: .tertiarySystemFill)) .padding(.top, 12) .padding() if mem.verified { ( Text(Image(systemName: "checkmark.shield")) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .font(.title2) + Text(" ") + Text(mem.displayName) @@ -359,6 +425,7 @@ struct GroupMemberInfoView: View { ) .navigationBarTitleDisplayMode(.inline) .navigationTitle("Security code") + .modifier(ThemedBackground()) } label: { Label( member.verified ? "View security code" : "Verify security code", @@ -408,7 +475,7 @@ struct GroupMemberInfoView: View { Section { if mem.blockedByAdmin { Label("Blocked by admin", systemImage: "hand.raised") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } else if mem.memberSettings.showMessages { blockMemberButton(mem) } else { @@ -566,6 +633,21 @@ struct GroupMemberInfoView: View { } } +func MemberProfileImage( + _ mem: GroupMember, + size: CGFloat, + color: Color = Color(uiColor: .tertiarySystemGroupedBackground), + backgroundColor: Color? = nil +) -> some View { + ProfileImage( + imageStr: mem.image, + size: size, + color: color, + backgroundColor: backgroundColor, + blurred: mem.blocked + ) +} + func blockMemberAlert(_ gInfo: GroupInfo, _ mem: GroupMember) -> Alert { Alert( title: Text("Block member?"), diff --git a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift index b4e1992848..2b0d05375b 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift @@ -18,6 +18,7 @@ private let featureRoles: [(role: GroupMemberRole?, text: LocalizedStringKey)] = struct GroupPreferencesView: View { @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme @Binding var groupInfo: GroupInfo @State var preferences: FullGroupPreferences @State var currentPreferences: FullGroupPreferences @@ -34,8 +35,7 @@ struct GroupPreferencesView: View { featureSection(.reactions, $preferences.reactions.enable) featureSection(.voice, $preferences.voice.enable, $preferences.voice.role) featureSection(.files, $preferences.files.enable, $preferences.files.role) - // TODO enable simplexLinks preference in 5.8 - // featureSection(.simplexLinks, $preferences.simplexLinks.enable, $preferences.simplexLinks.role) + featureSection(.simplexLinks, $preferences.simplexLinks.enable, $preferences.simplexLinks.role) featureSection(.history, $preferences.history.enable) if groupInfo.canEdit { @@ -74,7 +74,7 @@ struct GroupPreferencesView: View { private func featureSection(_ feature: GroupFeature, _ enableFeature: Binding, _ enableForRole: Binding? = nil) -> some View { Section { - let color: Color = enableFeature.wrappedValue == .on ? .green : .secondary + let color: Color = enableFeature.wrappedValue == .on ? .green : theme.colors.secondary let icon = enableFeature.wrappedValue == .on ? feature.iconFilled : feature.icon let timedOn = feature == .timedMessages && enableFeature.wrappedValue == .on if groupInfo.canEdit { @@ -102,8 +102,6 @@ struct GroupPreferencesView: View { } } .frame(height: 36) - // remove in v5.8 - .disabled(true) } } else { settingsRow(icon, color: color) { @@ -114,18 +112,19 @@ struct GroupPreferencesView: View { } if enableFeature.wrappedValue == .on, let enableForRole { HStack { - Text("Enabled for").foregroundColor(.secondary) + Text("Enabled for").foregroundColor(theme.colors.secondary) Spacer() Text( featureRoles.first(where: { fr in fr.role == enableForRole.wrappedValue })?.text ?? "all members" ) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } } } } footer: { Text(feature.enableDescription(enableFeature.wrappedValue, groupInfo.canEdit)) + .foregroundColor(theme.colors.secondary) } .onChange(of: enableFeature.wrappedValue) { enabled in if case .off = enabled { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift b/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift index 00d4f8c37b..9a9002f9dc 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupWelcomeView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct GroupWelcomeView: View { @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var theme: AppTheme @Binding var groupInfo: GroupInfo @State var groupProfile: GroupProfile @State var welcomeText: String @@ -57,7 +58,7 @@ struct GroupWelcomeView: View { } private func textPreview() -> some View { - messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil, showSecrets: false) + messageText(welcomeText, parseSimpleXMarkdown(welcomeText), nil, showSecrets: false, secondaryColor: theme.colors.secondary) .frame(minHeight: 130, alignment: .topLeading) .frame(maxWidth: .infinity, alignment: .leading) } @@ -70,7 +71,7 @@ struct GroupWelcomeView: View { Group { if welcomeText.isEmpty { TextEditor(text: Binding.constant(NSLocalizedString("Enter welcome message…", comment: "placeholder"))) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .disabled(true) } TextEditor(text: $welcomeText) diff --git a/apps/ios/Shared/Views/Chat/ReverseList.swift b/apps/ios/Shared/Views/Chat/ReverseList.swift new file mode 100644 index 0000000000..94d160e1b4 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ReverseList.swift @@ -0,0 +1,316 @@ +// +// ReverseList.swift +// SimpleX (iOS) +// +// Created by Levitating Pineapple on 11/06/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import Combine + +/// A List, which displays it's items in reverse order - from bottom to top +struct ReverseList: UIViewControllerRepresentable { + let items: Array + + @Binding var scrollState: ReverseListScrollModel.State + + /// Closure, that returns user interface for a given item + let content: (Item) -> Content + + let loadPage: () -> Void + + func makeUIViewController(context: Context) -> Controller { + Controller(representer: self) + } + + func updateUIViewController(_ controller: Controller, context: Context) { + if case let .scrollingTo(destination) = scrollState, !items.isEmpty { + switch destination { + case .nextPage: + controller.scrollToNextPage() + case let .item(id): + controller.scroll(to: items.firstIndex(where: { $0.id == id }), position: .bottom) + case .bottom: + controller.scroll(to: 0, position: .top) + } + } else { + controller.update(items: items) + } + } + + /// Controller, which hosts SwiftUI cells + class Controller: UITableViewController { + private enum Section { case main } + private let representer: ReverseList + private var dataSource: UITableViewDiffableDataSource! + private var itemCount: Int = 0 + private var bag = Set() + + init(representer: ReverseList) { + self.representer = representer + super.init(style: .plain) + + // 1. Style + tableView = InvertedTableView() + tableView.separatorStyle = .none + tableView.transform = .verticalFlip + tableView.backgroundColor = .clear + + // 2. Register cells + if #available(iOS 16.0, *) { + tableView.register( + UITableViewCell.self, + forCellReuseIdentifier: cellReuseId + ) + } else { + tableView.register( + HostingCell.self, + forCellReuseIdentifier: cellReuseId + ) + } + + // 3. Configure data source + self.dataSource = UITableViewDiffableDataSource( + tableView: tableView + ) { (tableView, indexPath, item) -> UITableViewCell? in + if indexPath.item > self.itemCount - 8, self.itemCount > 8 { + self.representer.loadPage() + } + let cell = tableView.dequeueReusableCell(withIdentifier: cellReuseId, for: indexPath) + if #available(iOS 16.0, *) { + cell.contentConfiguration = UIHostingConfiguration { self.representer.content(item) } + .margins(.all, 0) + .minSize(height: 1) // Passing zero will result in system default of 44 points being used + } else { + if let cell = cell as? HostingCell { + cell.set(content: self.representer.content(item), parent: self) + } else { + fatalError("Unexpected Cell Type for: \(item)") + } + } + cell.transform = .verticalFlip + cell.selectionStyle = .none + cell.backgroundColor = .clear + return cell + } + + // 4. External state changes will require manual layout updates + NotificationCenter.default + .addObserver( + self, + selector: #selector(updateLayout), + name: notificationName, + object: nil + ) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError() } + + deinit { NotificationCenter.default.removeObserver(self) } + + @objc private func updateLayout() { + if #available(iOS 16.0, *) { + tableView.setNeedsLayout() + tableView.layoutIfNeeded() + } else { + tableView.reloadData() + } + } + + /// Hides keyboard, when user begins to scroll. + /// Equivalent to `.scrollDismissesKeyboard(.immediately)` + override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + UIApplication.shared + .sendAction( + #selector(UIResponder.resignFirstResponder), + to: nil, + from: nil, + for: nil + ) + NotificationCenter.default.post(name: .chatViewWillBeginScrolling, object: nil) + } + + override func viewDidAppear(_ animated: Bool) { + tableView.clipsToBounds = false + parent?.viewIfLoaded?.clipsToBounds = false + } + + /// Scrolls up + func scrollToNextPage() { + tableView.setContentOffset( + CGPoint( + x: tableView.contentOffset.x, + y: tableView.contentOffset.y + tableView.bounds.height + ), + animated: true + ) + Task { representer.scrollState = .atDestination } + } + + /// Scrolls to Item at index path + /// - Parameter indexPath: Item to scroll to - will scroll to beginning of the list, if `nil` + func scroll(to index: Int?, position: UITableView.ScrollPosition) { + var animated = false + if #available(iOS 16.0, *) { + animated = true + } + if let index, tableView.numberOfRows(inSection: 0) != 0 { + tableView.scrollToRow( + at: IndexPath(row: index, section: 0), + at: position, + animated: animated + ) + } else { + tableView.setContentOffset( + CGPoint(x: .zero, y: -InvertedTableView.inset), + animated: animated + ) + } + Task { representer.scrollState = .atDestination } + } + + func update(items: Array) { + var snapshot = NSDiffableDataSourceSnapshot() + snapshot.appendSections([.main]) + snapshot.appendItems(items) + dataSource.defaultRowAnimation = .none + dataSource.apply( + snapshot, + animatingDifferences: itemCount != 0 && abs(items.count - itemCount) == 1 + ) + // Sets content offset on initial load + if itemCount == 0 { + tableView.setContentOffset( + CGPoint(x: 0, y: -InvertedTableView.inset), + animated: false + ) + } + itemCount = items.count + } + } + + /// `UIHostingConfiguration` back-port for iOS14 and iOS15 + /// Implemented as a `UITableViewCell` that wraps and manages a generic `UIHostingController` + private final class HostingCell: UITableViewCell { + private let hostingController = UIHostingController(rootView: nil) + + /// Updates content of the cell + /// For reference: https://noahgilmore.com/blog/swiftui-self-sizing-cells/ + func set(content: Hosted, parent: UIViewController) { + hostingController.view.backgroundColor = .clear + hostingController.rootView = content + if let hostingView = hostingController.view { + hostingView.invalidateIntrinsicContentSize() + if hostingController.parent != parent { parent.addChild(hostingController) } + if !contentView.subviews.contains(hostingController.view) { + contentView.addSubview(hostingController.view) + hostingView.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + hostingView.leadingAnchor + .constraint(equalTo: contentView.leadingAnchor), + hostingView.trailingAnchor + .constraint(equalTo: contentView.trailingAnchor), + hostingView.topAnchor + .constraint(equalTo: contentView.topAnchor), + hostingView.bottomAnchor + .constraint(equalTo: contentView.bottomAnchor) + ]) + } + if hostingController.parent != parent { hostingController.didMove(toParent: parent) } + } else { + fatalError("Hosting View not loaded \(hostingController)") + } + } + + override func prepareForReuse() { + super.prepareForReuse() + hostingController.rootView = nil + } + } +} + +/// Manages ``ReverseList`` scrolling +class ReverseListScrollModel: ObservableObject { + /// Represents Scroll State of ``ReverseList`` + enum State: Equatable { + enum Destination: Equatable { + case nextPage + case item(Item.ID) + case bottom + } + + case scrollingTo(Destination) + case atDestination + } + + @Published var state: State = .atDestination + + func scrollToNextPage() { + state = .scrollingTo(.nextPage) + } + + func scrollToBottom() { + state = .scrollingTo(.bottom) + } + + func scrollToItem(id: Item.ID) { + state = .scrollingTo(.item(id)) + } +} + +fileprivate let cellReuseId = "hostingCell" + +fileprivate let notificationName = NSNotification.Name(rawValue: "reverseListNeedsLayout") + +fileprivate extension CGAffineTransform { + /// Transform that vertically flips the view, preserving it's location + static let verticalFlip = CGAffineTransform(scaleX: 1, y: -1) +} + +extension NotificationCenter { + static func postReverseListNeedsLayout() { + NotificationCenter.default.post( + name: notificationName, + object: nil + ) + } +} + +/// Disable animation on iOS 15 +func withConditionalAnimation( + _ animation: Animation? = .default, + _ body: () throws -> Result +) rethrows -> Result { + if #available(iOS 16.0, *) { + try withAnimation(animation, body) + } else { + try body() + } +} + +class InvertedTableView: UITableView { + static let inset = CGFloat(100) + + static let insets = UIEdgeInsets( + top: inset, + left: .zero, + bottom: inset, + right: .zero + ) + + override var contentInsetAdjustmentBehavior: UIScrollView.ContentInsetAdjustmentBehavior { + get { .never } + set { } + } + + override var contentInset: UIEdgeInsets { + get { Self.insets } + set { } + } + + override var adjustedContentInset: UIEdgeInsets { + Self.insets + } +} diff --git a/apps/ios/Shared/Views/Chat/SelectableChatItemToolbars.swift b/apps/ios/Shared/Views/Chat/SelectableChatItemToolbars.swift new file mode 100644 index 0000000000..87bc73a60e --- /dev/null +++ b/apps/ios/Shared/Views/Chat/SelectableChatItemToolbars.swift @@ -0,0 +1,133 @@ +// +// SelectableChatItemToolbars.swift +// SimpleX (iOS) +// +// Created by Stanislav Dmitrenko on 30.07.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct SelectedItemsTopToolbar: View { + @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme + @Binding var selectedChatItems: Set? + + var body: some View { + let count = selectedChatItems?.count ?? 0 + return Text(count == 0 ? "Nothing selected" : "Selected \(count)").font(.headline) + .foregroundColor(theme.colors.onBackground) + .frame(width: 220) + } +} + +struct SelectedItemsBottomToolbar: View { + @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme + let chatItems: [ChatItem] + @Binding var selectedChatItems: Set? + var chatInfo: ChatInfo + // Bool - delete for everyone is possible + var deleteItems: (Bool) -> Void + var moderateItems: () -> Void + //var shareItems: () -> Void + @State var deleteEnabled: Bool = false + @State var deleteForEveryoneEnabled: Bool = false + + @State var canModerate: Bool = false + @State var moderateEnabled: Bool = false + + @State var allButtonsDisabled = false + + var body: some View { + VStack(spacing: 0) { + Divider() + + HStack(alignment: .center) { + Button { + deleteItems(deleteForEveryoneEnabled) + } label: { + Image(systemName: "trash") + .resizable() + .frame(width: 20, height: 20, alignment: .center) + .foregroundColor(!deleteEnabled || allButtonsDisabled ? theme.colors.secondary: .red) + } + .disabled(!deleteEnabled || allButtonsDisabled) + + Spacer() + Button { + moderateItems() + } label: { + Image(systemName: "flag") + .resizable() + .frame(width: 20, height: 20, alignment: .center) + .foregroundColor(!moderateEnabled || allButtonsDisabled ? theme.colors.secondary : .red) + } + .disabled(!moderateEnabled || allButtonsDisabled) + .opacity(canModerate ? 1 : 0) + + + Spacer() + Button { + //shareItems() + } label: { + Image(systemName: "square.and.arrow.up") + .resizable() + .frame(width: 20, height: 20, alignment: .center) + .foregroundColor(allButtonsDisabled ? theme.colors.secondary : theme.colors.primary) + } + .disabled(allButtonsDisabled) + .opacity(0) + } + .frame(maxHeight: .infinity) + .padding([.leading, .trailing], 12) + } + .onAppear { + recheckItems(chatInfo, chatItems, selectedChatItems) + } + .onChange(of: chatInfo) { info in + recheckItems(info, chatItems, selectedChatItems) + } + .onChange(of: chatItems) { items in + recheckItems(chatInfo, items, selectedChatItems) + } + .onChange(of: selectedChatItems) { selected in + recheckItems(chatInfo, chatItems, selected) + } + .frame(height: 55.5) + .background(.thinMaterial) + } + + private func recheckItems(_ chatInfo: ChatInfo, _ chatItems: [ChatItem], _ selectedItems: Set?) { + let count = selectedItems?.count ?? 0 + allButtonsDisabled = count == 0 || count > 20 + canModerate = possibleToModerate(chatInfo) + if let selected = selectedItems { + let me: Bool + let onlyOwnGroupItems: Bool + (deleteEnabled, deleteForEveryoneEnabled, me, onlyOwnGroupItems, selectedChatItems) = chatItems.reduce((true, true, true, true, [])) { (r, ci) in + if selected.contains(ci.id) { + var (de, dee, me, onlyOwnGroupItems, sel) = r + de = de && ci.canBeDeletedForSelf + dee = dee && ci.meta.deletable && !ci.localNote + onlyOwnGroupItems = onlyOwnGroupItems && ci.chatDir == .groupSnd + me = me && ci.content.msgContent != nil && ci.memberToModerate(chatInfo) != nil + sel.insert(ci.id) // we are collecting new selected items here to account for any changes in chat items list + return (de, dee, me, onlyOwnGroupItems, sel) + } else { + return r + } + } + moderateEnabled = me && !onlyOwnGroupItems + } + } + + private func possibleToModerate(_ chatInfo: ChatInfo) -> Bool { + return switch chatInfo { + case let .group(groupInfo): + groupInfo.membership.memberRole >= .admin + default: false + } + } +} diff --git a/apps/ios/Shared/Views/Chat/VerifyCodeView.swift b/apps/ios/Shared/Views/Chat/VerifyCodeView.swift index 75e31c26ed..7b01fe0300 100644 --- a/apps/ios/Shared/Views/Chat/VerifyCodeView.swift +++ b/apps/ios/Shared/Views/Chat/VerifyCodeView.swift @@ -10,6 +10,7 @@ import SwiftUI struct VerifyCodeView: View { @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var theme: AppTheme var displayName: String @State var connectionCode: String? @State var connectionVerified: Bool @@ -30,7 +31,7 @@ struct VerifyCodeView: View { HStack { if connectionVerified { Image(systemName: "checkmark.shield") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) Text("\(displayName) is verified") } else { Text("\(displayName) is not verified") @@ -66,6 +67,7 @@ struct VerifyCodeView: View { ScanCodeView(connectionVerified: $connectionVerified, verify: verify) .navigationBarTitleDisplayMode(.large) .navigationTitle("Scan code") + .modifier(ThemedBackground()) } label: { Label("Scan code", systemImage: "qrcode") } @@ -122,5 +124,6 @@ struct VerifyCodeView: View { struct VerifyCodeView_Previews: PreviewProvider { static var previews: some View { VerifyCodeView(displayName: "alice", connectionCode: "12345 67890 12345 67890", connectionVerified: false, verify: {_ in nil}) + .environmentObject(CurrentColors.toAppTheme()) } } diff --git a/apps/ios/Shared/Views/ChatList/ChatHelp.swift b/apps/ios/Shared/Views/ChatList/ChatHelp.swift index 2435c9a4f5..c84cdb0b97 100644 --- a/apps/ios/Shared/Views/ChatList/ChatHelp.swift +++ b/apps/ios/Shared/Views/ChatList/ChatHelp.swift @@ -11,7 +11,6 @@ import SwiftUI struct ChatHelp: View { @EnvironmentObject var chatModel: ChatModel @Binding var showSettings: Bool - @State private var newChatMenuOption: NewChatMenuOption? = nil var body: some View { ScrollView { chatHelp() } @@ -39,7 +38,7 @@ struct ChatHelp: View { HStack(spacing: 8) { Text("Tap button ") - NewChatMenuButton(newChatMenuOption: $newChatMenuOption) + NewChatMenuButton() Text("above, then choose:") } diff --git a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index efe254323e..d2a93b9bd1 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -9,34 +9,56 @@ import SwiftUI import SimpleXChat -private let rowHeights: [DynamicTypeSize: CGFloat] = [ - .xSmall: 68, - .small: 72, - .medium: 76, - .large: 80, - .xLarge: 88, - .xxLarge: 94, - .xxxLarge: 104, - .accessibility1: 90, - .accessibility2: 100, - .accessibility3: 120, - .accessibility4: 130, - .accessibility5: 140 +typealias DynamicSizes = ( + rowHeight: CGFloat, + profileImageSize: CGFloat, + mediaSize: CGFloat, + incognitoSize: CGFloat, + chatInfoSize: CGFloat, + unreadCorner: CGFloat, + unreadPadding: CGFloat +) + +private let dynamicSizes: [DynamicTypeSize: DynamicSizes] = [ + .xSmall: (68, 55, 33, 22, 18, 9, 3), + .small: (72, 57, 34, 22, 18, 9, 3), + .medium: (76, 60, 36, 22, 18, 10, 4), + .large: (80, 63, 38, 24, 20, 10, 4), + .xLarge: (88, 67, 41, 24, 20, 10, 4), + .xxLarge: (100, 71, 44, 27, 22, 11, 4), + .xxxLarge: (110, 75, 48, 30, 24, 12, 5), + .accessibility1: (110, 75, 48, 30, 24, 12, 5), + .accessibility2: (114, 75, 48, 30, 24, 12, 5), + .accessibility3: (124, 75, 48, 30, 24, 12, 5), + .accessibility4: (134, 75, 48, 30, 24, 12, 5), + .accessibility5: (144, 75, 48, 30, 24, 12, 5) ] +private let defaultDynamicSizes: DynamicSizes = dynamicSizes[.large]! + +func dynamicSize(_ font: DynamicTypeSize) -> DynamicSizes { + dynamicSizes[font] ?? defaultDynamicSizes +} + struct ChatListNavLink: View { @EnvironmentObject var chatModel: ChatModel - @Environment(\.dynamicTypeSize) private var dynamicTypeSize + @EnvironmentObject var theme: AppTheme + @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize + @AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = false @ObservedObject var chat: Chat @State private var showContactRequestDialog = false @State private var showJoinGroupDialog = false @State private var showContactConnectionInfo = false @State private var showInvalidJSON = false - @State private var showDeleteContactActionSheet = false + @State private var alert: SomeAlert? = nil + @State private var actionSheet: SomeActionSheet? = nil + @State private var sheet: SomeSheet? = nil @State private var showConnectContactViaAddressDialog = false @State private var inProgress = false @State private var progressByTimeout = false + var dynamicRowHeight: CGFloat { dynamicSize(userFont).rowHeight } + var body: some View { Group { switch chat.chatInfo { @@ -64,17 +86,24 @@ struct ChatListNavLink: View { } } } - + @ViewBuilder private func contactNavLink(_ contact: Contact) -> some View { Group { - if contact.activeConn == nil && contact.profile.contactLink != nil { + if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) - .frame(height: rowHeights[dynamicTypeSize]) + .frame(height: dynamicRowHeight) .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button { - showDeleteContactActionSheet = true + deleteContactDialog( + chat, + contact, + dismissToChatList: false, + showAlert: { alert = $0 }, + showActionSheet: { actionSheet = $0 }, + showSheetContent: { sheet = $0 } + ) } label: { - Label("Delete", systemImage: "trash") + deleteLabel } .tint(.red) } @@ -85,51 +114,44 @@ struct ChatListNavLink: View { } } else { NavLinkPlain( - tag: chat.chatInfo.id, + chatId: chat.chatInfo.id, selection: $chatModel.chatId, label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) } ) .swipeActions(edge: .leading, allowsFullSwipe: true) { markReadButton() toggleFavoriteButton() - ToggleNtfsButton(chat: chat) + toggleNtfsButton(chat: chat) } .swipeActions(edge: .trailing, allowsFullSwipe: true) { if !chat.chatItems.isEmpty { clearChatButton() } Button { - if contact.ready || !contact.active { - showDeleteContactActionSheet = true - } else { - AlertManager.shared.showAlert(deletePendingContactAlert(chat, contact)) - } + deleteContactDialog( + chat, + contact, + dismissToChatList: false, + showAlert: { alert = $0 }, + showActionSheet: { actionSheet = $0 }, + showSheetContent: { sheet = $0 } + ) } label: { - Label("Delete", systemImage: "trash") + deleteLabel } .tint(.red) } - .frame(height: rowHeights[dynamicTypeSize]) + .frame(height: dynamicRowHeight) } } - .actionSheet(isPresented: $showDeleteContactActionSheet) { - if contact.ready && contact.active { - return ActionSheet( - title: Text("Delete contact?\nThis cannot be undone!"), - buttons: [ - .destructive(Text("Delete and notify contact")) { Task { await deleteChat(chat, notify: true) } }, - .destructive(Text("Delete")) { Task { await deleteChat(chat, notify: false) } }, - .cancel() - ] - ) + .alert(item: $alert) { $0.alert } + .actionSheet(item: $actionSheet) { $0.actionSheet } + .sheet(item: $sheet) { + if #available(iOS 16.0, *) { + $0.content + .presentationDetents([.fraction(0.4)]) } else { - return ActionSheet( - title: Text("Delete contact?\nThis cannot be undone!"), - buttons: [ - .destructive(Text("Delete")) { Task { await deleteChat(chat) } }, - .cancel() - ] - ) + $0.content } } } @@ -138,7 +160,7 @@ struct ChatListNavLink: View { switch (groupInfo.membership.memberStatus) { case .memInvited: ChatPreviewView(chat: chat, progressByTimeout: $progressByTimeout) - .frame(height: rowHeights[dynamicTypeSize]) + .frame(height: dynamicRowHeight) .swipeActions(edge: .trailing, allowsFullSwipe: true) { joinGroupButton() if groupInfo.canDelete { @@ -158,7 +180,7 @@ struct ChatListNavLink: View { .disabled(inProgress) case .memAccepted: ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) - .frame(height: rowHeights[dynamicTypeSize]) + .frame(height: dynamicRowHeight) .onTapGesture { AlertManager.shared.showAlert(groupInvitationAcceptedAlert()) } @@ -172,16 +194,16 @@ struct ChatListNavLink: View { } default: NavLinkPlain( - tag: chat.chatInfo.id, + chatId: chat.chatInfo.id, selection: $chatModel.chatId, label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) }, disabled: !groupInfo.ready ) - .frame(height: rowHeights[dynamicTypeSize]) + .frame(height: dynamicRowHeight) .swipeActions(edge: .leading, allowsFullSwipe: true) { markReadButton() toggleFavoriteButton() - ToggleNtfsButton(chat: chat) + toggleNtfsButton(chat: chat) } .swipeActions(edge: .trailing, allowsFullSwipe: true) { if !chat.chatItems.isEmpty { @@ -199,12 +221,12 @@ struct ChatListNavLink: View { @ViewBuilder private func noteFolderNavLink(_ noteFolder: NoteFolder) -> some View { NavLinkPlain( - tag: chat.chatInfo.id, + chatId: chat.chatInfo.id, selection: $chatModel.chatId, label: { ChatPreviewView(chat: chat, progressByTimeout: Binding.constant(false)) }, disabled: !noteFolder.ready ) - .frame(height: rowHeights[dynamicTypeSize]) + .frame(height: dynamicRowHeight) .swipeActions(edge: .leading, allowsFullSwipe: true) { markReadButton() } @@ -222,9 +244,9 @@ struct ChatListNavLink: View { await MainActor.run { inProgress = false } } } label: { - Label("Join", systemImage: chat.chatInfo.incognito ? "theatermasks" : "ipad.and.arrow.forward") + SwipeLabel(NSLocalizedString("Join", comment: "swipe action"), systemImage: chat.chatInfo.incognito ? "theatermasks" : "ipad.and.arrow.forward", inverted: oneHandUI) } - .tint(chat.chatInfo.incognito ? .indigo : .accentColor) + .tint(chat.chatInfo.incognito ? .indigo : theme.colors.primary) } @ViewBuilder private func markReadButton() -> some View { @@ -232,16 +254,16 @@ struct ChatListNavLink: View { Button { Task { await markChatRead(chat) } } label: { - Label("Read", systemImage: "checkmark") + SwipeLabel(NSLocalizedString("Read", comment: "swipe action"), systemImage: "checkmark", inverted: oneHandUI) } - .tint(Color.accentColor) + .tint(theme.colors.primary) } else { Button { Task { await markChatUnread(chat) } } label: { - Label("Unread", systemImage: "circlebadge.fill") + SwipeLabel(NSLocalizedString("Unread", comment: "swipe action"), systemImage: "circlebadge.fill", inverted: oneHandUI) } - .tint(Color.accentColor) + .tint(theme.colors.primary) } } @@ -251,24 +273,36 @@ struct ChatListNavLink: View { Button { toggleChatFavorite(chat, favorite: false) } label: { - Label("Unfav.", systemImage: "star.slash") + SwipeLabel(NSLocalizedString("Unfav.", comment: "swipe action"), systemImage: "star.slash.fill", inverted: oneHandUI) } .tint(.green) } else { Button { toggleChatFavorite(chat, favorite: true) } label: { - Label("Favorite", systemImage: "star.fill") + SwipeLabel(NSLocalizedString("Favorite", comment: "swipe action"), systemImage: "star.fill", inverted: oneHandUI) } .tint(.green) } } + @ViewBuilder private func toggleNtfsButton(chat: Chat) -> some View { + Button { + toggleNotifications(chat, enableNtfs: !chat.chatInfo.ntfsEnabled) + } label: { + if chat.chatInfo.ntfsEnabled { + SwipeLabel(NSLocalizedString("Mute", comment: "swipe action"), systemImage: "speaker.slash.fill", inverted: oneHandUI) + } else { + SwipeLabel(NSLocalizedString("Unmute", comment: "swipe action"), systemImage: "speaker.wave.2.fill", inverted: oneHandUI) + } + } + } + private func clearChatButton() -> some View { Button { AlertManager.shared.showAlert(clearChatAlert()) } label: { - Label("Clear", systemImage: "gobackward") + SwipeLabel(NSLocalizedString("Clear", comment: "swipe action"), systemImage: "gobackward", inverted: oneHandUI) } .tint(Color.orange) } @@ -277,7 +311,7 @@ struct ChatListNavLink: View { Button { AlertManager.shared.showAlert(clearNoteFolderAlert()) } label: { - Label("Clear", systemImage: "gobackward") + SwipeLabel(NSLocalizedString("Clear", comment: "swipe action"), systemImage: "gobackward", inverted: oneHandUI) } .tint(Color.orange) } @@ -286,7 +320,7 @@ struct ChatListNavLink: View { Button { AlertManager.shared.showAlert(leaveGroupAlert(groupInfo)) } label: { - Label("Leave", systemImage: "rectangle.portrait.and.arrow.right") + SwipeLabel(NSLocalizedString("Leave", comment: "swipe action"), systemImage: "rectangle.portrait.and.arrow.right.fill", inverted: oneHandUI) } .tint(Color.yellow) } @@ -295,7 +329,7 @@ struct ChatListNavLink: View { Button { AlertManager.shared.showAlert(deleteGroupAlert(groupInfo)) } label: { - Label("Delete", systemImage: "trash") + deleteLabel } .tint(.red) } @@ -305,22 +339,23 @@ struct ChatListNavLink: View { .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button { Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) } - } label: { Label("Accept", systemImage: "checkmark") } - .tint(.accentColor) + } label: { SwipeLabel(NSLocalizedString("Accept", comment: "swipe action"), systemImage: "checkmark", inverted: oneHandUI) } + .tint(theme.colors.primary) Button { Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) } } label: { - Label("Accept incognito", systemImage: "theatermasks") + SwipeLabel(NSLocalizedString("Accept incognito", comment: "swipe action"), systemImage: "theatermasks.fill", inverted: oneHandUI) } .tint(.indigo) Button { AlertManager.shared.showAlert(rejectContactRequestAlert(contactRequest)) } label: { - Label("Reject", systemImage: "multiply") + SwipeLabel(NSLocalizedString("Reject", comment: "swipe action"), systemImage: "multiply.fill", inverted: oneHandUI) } .tint(.red) } - .frame(height: rowHeights[dynamicTypeSize]) + .frame(height: dynamicRowHeight) + .contentShape(Rectangle()) .onTapGesture { showContactRequestDialog = true } .confirmationDialog("Accept connection request?", isPresented: $showContactRequestDialog, titleVisibility: .visible) { Button("Accept") { Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) } } @@ -337,29 +372,37 @@ struct ChatListNavLink: View { AlertManager.shared.showAlertMsg(title: a.title, message: a.message) }) } label: { - Label("Delete", systemImage: "trash") + deleteLabel } .tint(.red) Button { showContactConnectionInfo = true } label: { - Label("Name", systemImage: "pencil") + SwipeLabel(NSLocalizedString("Name", comment: "swipe action"), systemImage: "pencil", inverted: oneHandUI) } - .tint(.accentColor) + .tint(theme.colors.primary) } - .frame(height: rowHeights[dynamicTypeSize]) - .sheet(isPresented: $showContactConnectionInfo) { - if case let .contactConnection(contactConnection) = chat.chatInfo { - ContactConnectionInfo(contactConnection: contactConnection) - .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) + .frame(height: dynamicRowHeight) + .appSheet(isPresented: $showContactConnectionInfo) { + Group { + if case let .contactConnection(contactConnection) = chat.chatInfo { + ContactConnectionInfo(contactConnection: contactConnection) + .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) + .modifier(ThemedBackground(grouped: true)) + } } } + .contentShape(Rectangle()) .onTapGesture { showContactConnectionInfo = true } } + private var deleteLabel: some View { + SwipeLabel(NSLocalizedString("Delete", comment: "swipe action"), systemImage: "trash.fill", inverted: oneHandUI) + } + private func deleteGroupAlert(_ groupInfo: GroupInfo) -> Alert { Alert( title: Text("Delete group?"), @@ -408,28 +451,6 @@ struct ChatListNavLink: View { ) } - private func rejectContactRequestAlert(_ contactRequest: UserContactRequest) -> Alert { - Alert( - title: Text("Reject contact request"), - message: Text("The sender will NOT be notified"), - primaryButton: .destructive(Text("Reject")) { - Task { await rejectContactRequest(contactRequest) } - }, - secondaryButton: .cancel() - ) - } - - private func pendingContactAlert(_ chat: Chat, _ contact: Contact) -> Alert { - Alert( - title: Text("Contact is not connected yet!"), - message: Text("Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)."), - primaryButton: .cancel(), - secondaryButton: .destructive(Text("Delete Contact")) { - removePendingContact(chat, contact) - } - ) - } - private func groupInvitationAcceptedAlert() -> Alert { Alert( title: Text("Joining group"), @@ -437,37 +458,13 @@ struct ChatListNavLink: View { ) } - private func deletePendingContactAlert(_ chat: Chat, _ contact: Contact) -> Alert { - Alert( - title: Text("Delete pending connection"), - message: Text("Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)."), - primaryButton: .destructive(Text("Delete")) { - removePendingContact(chat, contact) - }, - secondaryButton: .cancel() - ) - } - - private func removePendingContact(_ chat: Chat, _ contact: Contact) { - Task { - do { - try await apiDeleteChat(type: chat.chatInfo.chatType, id: chat.chatInfo.apiId) - DispatchQueue.main.async { - chatModel.removeChat(contact.id) - } - } catch let error { - logger.error("ChatListNavLink.removePendingContact apiDeleteChat error: \(responseError(error))") - } - } - } - private func invalidJSONPreview(_ json: String) -> some View { Text("invalid chat data") .foregroundColor(.red) .padding(4) - .frame(height: rowHeights[dynamicTypeSize]) + .frame(height: dynamicRowHeight) .onTapGesture { showInvalidJSON = true } - .sheet(isPresented: $showInvalidJSON) { + .appSheet(isPresented: $showInvalidJSON) { invalidJSONView(json) .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) } @@ -475,16 +472,26 @@ struct ChatListNavLink: View { private func connectContactViaAddress_(_ contact: Contact, _ incognito: Bool) { Task { - let ok = await connectContactViaAddress(contact.contactId, incognito) + let ok = await connectContactViaAddress(contact.contactId, incognito, showAlert: { AlertManager.shared.showAlert($0) }) if ok { - await MainActor.run { - chatModel.chatId = contact.id - } + ItemsModel.shared.loadOpenChat(contact.id) + AlertManager.shared.showAlert(connReqSentAlert(.contact)) } } } } +func rejectContactRequestAlert(_ contactRequest: UserContactRequest) -> Alert { + Alert( + title: Text("Reject contact request"), + message: Text("The sender will NOT be notified"), + primaryButton: .destructive(Text("Reject")) { + Task { await rejectContactRequest(contactRequest) } + }, + secondaryButton: .cancel() + ) +} + func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, showError: @escaping (ErrorAlert) -> Void, success: @escaping () -> Void = {}) -> Alert { Alert( title: Text("Delete pending connection?"), @@ -511,15 +518,14 @@ func deleteContactConnectionAlert(_ contactConnection: PendingContactConnection, ) } -func connectContactViaAddress(_ contactId: Int64, _ incognito: Bool) async -> Bool { +func connectContactViaAddress(_ contactId: Int64, _ incognito: Bool, showAlert: (Alert) -> Void) async -> Bool { let (contact, alert) = await apiConnectContactViaAddress(incognito: incognito, contactId: contactId) if let alert = alert { - AlertManager.shared.showAlert(alert) + showAlert(alert) return false } else if let contact = contact { await MainActor.run { ChatModel.shared.updateContact(contact) - AlertManager.shared.showAlert(connReqSentAlert(.contact)) } return true } @@ -560,18 +566,11 @@ func joinGroup(_ groupId: Int64, _ onComplete: @escaping () async -> Void) { } } -struct ErrorAlert { - var title: LocalizedStringKey - var message: LocalizedStringKey -} - func getErrorAlert(_ error: Error, _ title: LocalizedStringKey) -> ErrorAlert { - switch error as? ChatResponse { - case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TIMEOUT))): - return ErrorAlert(title: "Connection timeout", message: "Please check your network connection with \(serverHostname(addr)) and try again.") - case let .chatCmdError(_, .errorAgent(.BROKER(addr, .NETWORK))): - return ErrorAlert(title: "Connection error", message: "Please check your network connection with \(serverHostname(addr)) and try again.") - default: + if let r = error as? ChatResponse, + let alert = getNetworkErrorAlert(r) { + return alert + } else { return ErrorAlert(title: title, message: "Error: \(responseError(error))") } } diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 6bf63bb2e3..8ad03236f1 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -11,16 +11,21 @@ import SimpleXChat struct ChatListView: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme @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 userPickerVisible = false @State private var showConnectDesktop = false + @State private var scrollToSearchBar = false + @AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false + @AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true + @AppStorage(DEFAULT_ONE_HAND_UI_CARD_SHOWN) private var oneHandUICardShown = false + @AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial var body: some View { if #available(iOS 16.0, *) { @@ -31,21 +36,16 @@ struct ChatListView: View { } private var viewBody: some View { - ZStack(alignment: .topLeading) { + ZStack(alignment: oneHandUI ? .bottomLeading : .topLeading) { NavStackCompat( isActive: Binding( get: { chatModel.chatId != nil }, - set: { _ in } + set: { active in + if !active { chatModel.chatId = nil } + } ), destination: chatView - ) { - VStack { - if chatModel.chats.isEmpty { - onboardingButtons() - } - chatListView - } - } + ) { chatListView } if userPickerVisible { Rectangle().fill(.white.opacity(0.001)).onTapGesture { withAnimation { @@ -65,9 +65,14 @@ struct ChatListView: View { } private var chatListView: some View { - VStack { + let tm = ToolbarMaterial.material(toolbarMaterial) + return withToolbar(tm) { chatList + .background(theme.colors.background) + .navigationBarTitleDisplayMode(.inline) + .navigationBarHidden(searchMode || oneHandUI) } + .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) .onDisappear() { withAnimation { userPickerVisible = false } } .refreshable { AlertManager.shared.showAlert(Alert( @@ -85,65 +90,111 @@ struct ChatListView: View { secondaryButton: .cancel() )) } - .listStyle(.plain) - .navigationBarTitleDisplayMode(.inline) - .navigationBarHidden(searchMode) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - let user = chatModel.currentUser ?? User.sampleData - ZStack(alignment: .topTrailing) { - ProfileImage(imageStr: user.image, size: 32, color: Color(uiColor: .quaternaryLabel)) - .padding(.trailing, 4) - let allRead = chatModel.users - .filter { u in !u.user.activeUser && !u.user.hidden } - .allSatisfy { u in u.unreadCount == 0 } - if !allRead { - unreadBadge(size: 12) - } - } - .onTapGesture { - if chatModel.users.filter({ u in u.user.activeUser || !u.user.hidden }).count > 1 { - withAnimation { - userPickerVisible.toggle() - } - } else { - showSettings = true - } - } - } - ToolbarItem(placement: .principal) { - HStack(spacing: 4) { - Text("Chats") - .font(.headline) - if chatModel.chats.count > 0 { - toggleFilterButton() - } - } - .frame(maxWidth: .infinity, alignment: .center) - } - ToolbarItem(placement: .navigationBarTrailing) { - switch chatModel.chatRunning { - case .some(true): NewChatMenuButton(newChatMenuOption: $newChatMenuOption) - case .some(false): chatStoppedIcon() - case .none: EmptyView() - } + .safeAreaInset(edge: .top) { + if oneHandUI { Divider().background(tm) } + } + .safeAreaInset(edge: .bottom) { + if oneHandUI { + Divider().padding(.bottom, Self.hasHomeIndicator ? 0 : 8).background(tm) } } } - private func toggleFilterButton() -> some View { - Button { - showUnreadAndFavorites = !showUnreadAndFavorites - } label: { - Image(systemName: "line.3.horizontal.decrease.circle" + (showUnreadAndFavorites ? ".fill" : "")) - .foregroundColor(.accentColor) + static var hasHomeIndicator: Bool = { + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first { + window.safeAreaInsets.bottom > 0 + } else { false } + }() + + @ViewBuilder func withToolbar(_ material: Material, content: () -> some View) -> some View { + if #available(iOS 16.0, *) { + if oneHandUI { + content() + .toolbarBackground(.hidden, for: .bottomBar) + .toolbar { bottomToolbar } + } else { + content() + .toolbarBackground(.automatic, for: .navigationBar) + .toolbarBackground(material) + .toolbar { topToolbar } + } + } else { + if oneHandUI { + content().toolbar { bottomToolbarGroup } + } else { + content().toolbar { topToolbar } + } + } + } + + @ToolbarContentBuilder var topToolbar: some ToolbarContent { + ToolbarItem(placement: .topBarLeading) { leadingToolbarItem } + ToolbarItem(placement: .principal) { SubsStatusIndicator() } + ToolbarItem(placement: .topBarTrailing) { trailingToolbarItem } + } + + @ToolbarContentBuilder var bottomToolbar: some ToolbarContent { + let padding: Double = Self.hasHomeIndicator ? 0 : 14 + ToolbarItem(placement: .bottomBar) { + HStack { + leadingToolbarItem.padding(.bottom, padding) + Spacer() + SubsStatusIndicator().padding(.bottom, padding) + Spacer() + trailingToolbarItem.padding(.bottom, padding) + } + .contentShape(Rectangle()) + .onTapGesture { scrollToSearchBar = true } + } + } + + @ToolbarContentBuilder var bottomToolbarGroup: some ToolbarContent { + let padding: Double = Self.hasHomeIndicator ? 0 : 14 + ToolbarItemGroup(placement: .bottomBar) { + leadingToolbarItem.padding(.bottom, padding) + Spacer() + SubsStatusIndicator().padding(.bottom, padding) + Spacer() + trailingToolbarItem.padding(.bottom, padding) + } + } + + @ViewBuilder var leadingToolbarItem: some View { + let user = chatModel.currentUser ?? User.sampleData + ZStack(alignment: .topTrailing) { + ProfileImage(imageStr: user.image, size: 32, color: Color(uiColor: .quaternaryLabel)) + .padding(.trailing, 4) + let allRead = chatModel.users + .filter { u in !u.user.activeUser && !u.user.hidden } + .allSatisfy { u in u.unreadCount == 0 } + if !allRead { + unreadBadge(size: 12) + } + } + .onTapGesture { + if chatModel.users.filter({ u in u.user.activeUser || !u.user.hidden }).count > 1 { + withAnimation { + userPickerVisible.toggle() + } + } else { + showSettings = true + } + } + } + + @ViewBuilder var trailingToolbarItem: some View { + switch chatModel.chatRunning { + case .some(true): NewChatMenuButton() + case .some(false): chatStoppedIcon() + case .none: EmptyView() } } @ViewBuilder private var chatList: some View { let cs = filteredChats() ZStack { - VStack { + ScrollViewReader { scrollProxy in List { if !chatModel.chats.isEmpty { ChatListSearchBar( @@ -153,25 +204,67 @@ struct ChatListView: View { searchShowingSimplexLink: $searchShowingSimplexLink, searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink ) + .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) .listRowSeparator(.hidden) + .listRowBackground(Color.clear) .frame(maxWidth: .infinity) + .padding(.top, oneHandUI ? 8 : 0) + .id("searchBar") } - ForEach(cs, id: \.viewId) { chat in - ChatListNavLink(chat: chat) - .padding(.trailing, -16) + if !oneHandUICardShown { + OneHandUICard() + .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + } + if #available(iOS 16.0, *) { + ForEach(cs, id: \.viewId) { chat in + ChatListNavLink(chat: chat) + .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) + .padding(.trailing, -16) + .disabled(chatModel.chatRunning != true || chatModel.deletedChats.contains(chat.chatInfo.id)) + .listRowBackground(Color.clear) + } + .offset(x: -8) + } else { + ForEach(cs, id: \.viewId) { chat in + VStack(spacing: .zero) { + Divider() + .padding(.leading, 16) + ChatListNavLink(chat: chat) + .padding(.horizontal, 8) + .padding(.vertical, 6) + } + .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets()) + .background { theme.colors.background } // Hides default list selection colour .disabled(chatModel.chatRunning != true || chatModel.deletedChats.contains(chat.chatInfo.id)) + } } - .offset(x: -8) } - } - .onChange(of: chatModel.chatId) { _ in - if chatModel.chatId == nil, let chatId = chatModel.chatToTop { - chatModel.chatToTop = nil - chatModel.popChat(chatId) + .listStyle(.plain) + .onChange(of: chatModel.chatId) { currentChatId in + if let chatId = chatModel.chatToTop, currentChatId != chatId { + chatModel.chatToTop = nil + chatModel.popChat(chatId) + } + stopAudioPlayer() + } + .onChange(of: chatModel.currentUser?.userId) { _ in + stopAudioPlayer() + } + .onChange(of: scrollToSearchBar) { scrollToSearchBar in + if scrollToSearchBar { + Task { self.scrollToSearchBar = false } + withAnimation { scrollProxy.scrollTo("searchBar") } + } } } if cs.isEmpty && !chatModel.chats.isEmpty { - Text("No filtered chats").foregroundColor(.secondary) + Text("No filtered chats") + .scaleEffect(x: 1, y: oneHandUI ? -1 : 1, anchor: .center) + .foregroundColor(.secondary) } } } @@ -179,69 +272,40 @@ struct ChatListView: View { private func unreadBadge(_ text: Text? = Text(" "), size: CGFloat = 18) -> some View { Circle() .frame(width: size, height: size) - .foregroundColor(.accentColor) - } - - private func onboardingButtons() -> some View { - VStack(alignment: .trailing, spacing: 0) { - Path { p in - p.move(to: CGPoint(x: 8, y: 0)) - p.addLine(to: CGPoint(x: 16, y: 10)) - p.addLine(to: CGPoint(x: 0, y: 10)) - p.addLine(to: CGPoint(x: 8, y: 0)) - } - .fill(Color.accentColor) - .frame(width: 20, height: 10) - .padding(.trailing, 12) - - connectButton("Tap to start a new chat") { - newChatMenuOption = .newContact - } - - Spacer() - Text("You have no chats") - .foregroundColor(.secondary) - .frame(maxWidth: .infinity) - } - .padding(.trailing, 6) - .frame(maxHeight: .infinity) - } - - private func connectButton(_ label: LocalizedStringKey, action: @escaping () -> Void) -> some View { - Button(action: action) { - Text(label) - .padding(.vertical, 10) - .padding(.horizontal, 20) - } - .background(Color.accentColor) - .foregroundColor(.white) - .clipShape(RoundedRectangle(cornerRadius: 16)) + .foregroundColor(theme.colors.primary) } @ViewBuilder private func chatView() -> some View { if let chatId = chatModel.chatId, let chat = chatModel.getChat(chatId) { - ChatView(chat: chat).onAppear { - loadChat(chat: chat) - } + ChatView(chat: chat) } } + func stopAudioPlayer() { + VoiceItemState.smallView.values.forEach { $0.audioPlayer?.stop() } + VoiceItemState.smallView = [:] + } + private func filteredChats() -> [Chat] { if let linkChatId = searchChatFilteredBySimplexLink { return chatModel.chats.filter { $0.id == linkChatId } } else { let s = searchString() return s == "" && !showUnreadAndFavorites - ? chatModel.chats + ? chatModel.chats.filter { chat in + !chat.chatInfo.chatDeleted && chatContactType(chat: chat) != ContactType.card + } : 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)) + return !contact.chatDeleted && chatContactType(chat: chat) != ContactType.card && ( + 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) @@ -274,17 +338,76 @@ struct ChatListView: View { } } +struct SubsStatusIndicator: View { + @State private var subs: SMPServerSubs = SMPServerSubs.newSMPServerSubs + @State private var hasSess: Bool = false + @State private var task: Task? + @State private var showServersSummary = false + + @AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false + + var body: some View { + Button { + showServersSummary = true + } label: { + HStack(spacing: 4) { + Text("Chats").foregroundStyle(Color.primary).fixedSize().font(.headline) + SubscriptionStatusIndicatorView(subs: subs, hasSess: hasSess) + if showSubscriptionPercentage { + SubscriptionStatusPercentageView(subs: subs, hasSess: hasSess) + } + } + } + .disabled(ChatModel.shared.chatRunning != true) + .onAppear { + startTask() + } + .onDisappear { + stopTask() + } + .appSheet(isPresented: $showServersSummary) { + ServersSummaryView() + .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) + } + } + + private func startTask() { + task = Task { + while !Task.isCancelled { + if AppChatState.shared.value == .active { + do { + let (subs, hasSess) = try await getAgentSubsTotal() + await MainActor.run { + self.subs = subs + self.hasSess = hasSess + } + } catch let error { + logger.error("getSubsTotal error: \(responseError(error))") + } + } + try? await Task.sleep(nanoseconds: 1_000_000_000) // Sleep for 1 second + } + } + } + + func stopTask() { + task?.cancel() + task = nil + } +} + struct ChatListSearchBar: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @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? + @AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false var body: some View { VStack(spacing: 12) { @@ -292,7 +415,7 @@ struct ChatListSearchBar: View { HStack(spacing: 4) { Image(systemName: "magnifyingglass") TextField("Search or paste SimpleX link", text: $searchText) - .foregroundColor(searchShowingSimplexLink ? .secondary : .primary) + .foregroundColor(searchShowingSimplexLink ? theme.colors.secondary : theme.colors.onBackground) .disabled(searchShowingSimplexLink) .focused($searchFocussed) .frame(maxWidth: .infinity) @@ -301,47 +424,24 @@ struct ChatListSearchBar: View { .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) + .foregroundColor(theme.colors.secondary) .background(Color(.tertiarySystemFill)) .cornerRadius(10.0) if searchFocussed { Text("Cancel") - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) .onTapGesture { searchText = "" searchFocussed = false } + } else if m.chats.count > 0 { + toggleFilterButton() } } - Divider() - } - .sheet(isPresented: $showScanCodeSheet) { - NewChatView(selection: .connect, showQRCodeScanner: true) - .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) // fixes .refreshable in ChatListView affecting nested view } .onChange(of: searchFocussed) { sf in withAnimation { searchMode = sf } @@ -376,6 +476,21 @@ struct ChatListSearchBar: View { } } + private func toggleFilterButton() -> some View { + ZStack { + Color.clear + .frame(width: 22, height: 22) + Image(systemName: showUnreadAndFavorites ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease") + .resizable() + .scaledToFit() + .foregroundColor(showUnreadAndFavorites ? theme.colors.primary : theme.colors.secondary) + .frame(width: showUnreadAndFavorites ? 22 : 16, height: showUnreadAndFavorites ? 22 : 16) + .onTapGesture { + showUnreadAndFavorites = !showUnreadAndFavorites + } + } + } + private func connect(_ link: String) { planAndConnect( link, @@ -403,21 +518,21 @@ func chatStoppedIcon() -> some View { struct ChatListView_Previews: PreviewProvider { static var previews: some View { let chatModel = ChatModel() - chatModel.chats = [ - Chat( + chatModel.updateChats([ + ChatData( chatInfo: ChatInfo.sampleData.direct, chatItems: [ChatItem.getSample(1, .directSnd, .now, "hello")] ), - Chat( + ChatData( chatInfo: ChatInfo.sampleData.group, chatItems: [ChatItem.getSample(1, .directSnd, .now, "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")] ), - Chat( + ChatData( chatInfo: ChatInfo.sampleData.contactRequest, chatItems: [] ) - ] + ]) return Group { ChatListView(showSettings: Binding.constant(false)) .environmentObject(chatModel) diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index fe8fd8b28e..9e6d3005b6 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -11,19 +11,25 @@ import SimpleXChat struct ChatPreviewView: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme + @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize @ObservedObject var chat: Chat @Binding var progressByTimeout: Bool @State var deleting: Bool = false - @Environment(\.colorScheme) var colorScheme var darkGreen = Color(red: 0, green: 0.5, blue: 0) + @State private var activeContentPreview: ActiveContentPreview? = nil + @State private var showFullscreenGallery: Bool = false @AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true + var dynamicMediaSize: CGFloat { dynamicSize(userFont).mediaSize } + var dynamicChatInfoSize: CGFloat { dynamicSize(userFont).chatInfoSize } + var body: some View { let cItem = chat.chatItems.last return HStack(spacing: 8) { ZStack(alignment: .bottomTrailing) { - ChatInfoImage(chat: chat, size: 63) + ChatInfoImage(chat: chat, size: dynamicSize(userFont).profileImageSize) chatPreviewImageOverlayIcon() .padding([.bottom, .trailing], 1) } @@ -36,18 +42,45 @@ struct ChatPreviewView: View { (cItem?.timestampText ?? formatTimestampText(chat.chatInfo.chatTs)) .font(.subheadline) .frame(minWidth: 60, alignment: .trailing) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .padding(.top, 4) } .padding(.bottom, 4) .padding(.horizontal, 8) ZStack(alignment: .topTrailing) { - chatMessagePreview(cItem) + let chat = activeContentPreview?.chat ?? chat + let ci = activeContentPreview?.ci ?? chat.chatItems.last + let mc = ci?.content.msgContent + HStack(alignment: .top) { + let deleted = ci?.isDeletedContent == true || ci?.meta.itemDeleted != nil + let showContentPreview = (showChatPreviews && chatModel.draftChatId != chat.id && !deleted) || activeContentPreview != nil + if let ci, showContentPreview { + chatItemContentPreview(chat, ci) + } + let mcIsVoice = switch mc { case .voice: true; default: false } + if !mcIsVoice || !showContentPreview || mc?.text != "" || chatModel.draftChatId == chat.id { + let hasFilePreview = if case .file = mc { true } else { false } + chatMessagePreview(cItem, hasFilePreview) + } else { + Spacer() + chatInfoIcon(chat).frame(minWidth: 37, alignment: .trailing) + } + } + .onChange(of: chatModel.stopPreviousRecPlay?.path) { _ in + checkActiveContentPreview(chat, ci, mc) + } + .onChange(of: activeContentPreview) { _ in + checkActiveContentPreview(chat, ci, mc) + } + .onChange(of: showFullscreenGallery) { _ in + checkActiveContentPreview(chat, ci, mc) + } chatStatusImage() - .padding(.top, 26) + .padding(.top, dynamicChatInfoSize * 1.44) .frame(maxWidth: .infinity, alignment: .trailing) } + .frame(maxWidth: .infinity, alignment: .leading) .padding(.trailing, 8) Spacer() @@ -57,6 +90,33 @@ struct ChatPreviewView: View { .padding(.bottom, -8) .onChange(of: chatModel.deletedChats.contains(chat.chatInfo.id)) { contains in deleting = contains + // Stop voice when deleting the chat + if contains, let ci = activeContentPreview?.ci { + VoiceItemState.stopVoiceInSmallView(chat.chatInfo, ci) + } + } + + func checkActiveContentPreview(_ chat: Chat, _ ci: ChatItem?, _ mc: MsgContent?) { + let playing = chatModel.stopPreviousRecPlay + if case .voice = activeContentPreview?.mc, playing == nil { + activeContentPreview = nil + } else if activeContentPreview == nil { + if case .image = mc, let ci, let mc, showFullscreenGallery { + activeContentPreview = ActiveContentPreview(chat: chat, ci: ci, mc: mc) + } + if case .video = mc, let ci, let mc, showFullscreenGallery { + activeContentPreview = ActiveContentPreview(chat: chat, ci: ci, mc: mc) + } + if case .voice = mc, let ci, let mc, let fileSource = ci.file?.fileSource, playing?.path.hasSuffix(fileSource.filePath) == true { + activeContentPreview = ActiveContentPreview(chat: chat, ci: ci, mc: mc) + } + } else if case .voice = activeContentPreview?.mc { + if let playing, let fileSource = ci?.file?.fileSource, !playing.path.hasSuffix(fileSource.filePath) { + activeContentPreview = nil + } + } else if !showFullscreenGallery { + activeContentPreview = nil + } } } @@ -94,9 +154,9 @@ struct ChatPreviewView: View { case let .group(groupInfo): let v = previewTitle(t) switch (groupInfo.membership.memberStatus) { - case .memInvited: v.foregroundColor(deleting ? .secondary : chat.chatInfo.incognito ? .indigo : .accentColor) - case .memAccepted: v.foregroundColor(.secondary) - default: if deleting { v.foregroundColor(.secondary) } else { v } + case .memInvited: v.foregroundColor(deleting ? theme.colors.secondary : chat.chatInfo.incognito ? .indigo : theme.colors.primary) + case .memAccepted: v.foregroundColor(theme.colors.secondary) + default: if deleting { v.foregroundColor(theme.colors.secondary) } else { v } } default: previewTitle(t) } @@ -108,52 +168,63 @@ struct ChatPreviewView: View { private var verifiedIcon: Text { (Text(Image(systemName: "checkmark.shield")) + Text(" ")) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .baselineOffset(1) .kerning(-2) } - private func chatPreviewLayout(_ text: Text, draft: Bool = false) -> some View { + private func chatPreviewLayout(_ text: Text?, draft: Bool = false, _ hasFilePreview: Bool = false) -> some View { ZStack(alignment: .topTrailing) { let t = text - .lineLimit(2) + .lineLimit(userFont <= .xxxLarge ? 2 : 1) .multilineTextAlignment(.leading) .frame(maxWidth: .infinity, alignment: .topLeading) - .padding(.leading, 8) - .padding(.trailing, 36) + .padding(.leading, hasFilePreview ? 0 : 8) + .padding(.trailing, hasFilePreview ? 38 : 36) + .offset(x: hasFilePreview ? -2 : 0) + .fixedSize(horizontal: false, vertical: true) if !showChatPreviews && !draft { t.privacySensitive(true).redacted(reason: .privacy) } else { t } - let s = chat.chatStats - if s.unreadCount > 0 || s.unreadChat { - unreadCountText(s.unreadCount) - .font(.caption) - .foregroundColor(.white) - .padding(.horizontal, 4) - .frame(minWidth: 18, minHeight: 18) - .background(chat.chatInfo.ntfsEnabled || chat.chatInfo.chatType == .local ? Color.accentColor : Color.secondary) - .cornerRadius(10) - } else if !chat.chatInfo.ntfsEnabled && chat.chatInfo.chatType != .local { - Image(systemName: "speaker.slash.fill") - .foregroundColor(.secondary) - } else if chat.chatInfo.chatSettings?.favorite ?? false { - Image(systemName: "star.fill") - .resizable() - .scaledToFill() - .frame(width: 18, height: 18) - .padding(.trailing, 1) - .foregroundColor(.secondary.opacity(0.65)) - } + chatInfoIcon(chat).frame(minWidth: 37, alignment: .trailing) + } + } + + @ViewBuilder private func chatInfoIcon(_ chat: Chat) -> some View { + let s = chat.chatStats + if s.unreadCount > 0 || s.unreadChat { + unreadCountText(s.unreadCount) + .font(userFont <= .xxxLarge ? .caption : .caption2) + .foregroundColor(.white) + .padding(.horizontal, dynamicSize(userFont).unreadPadding) + .frame(minWidth: dynamicChatInfoSize, minHeight: dynamicChatInfoSize) + .background(chat.chatInfo.ntfsEnabled || chat.chatInfo.chatType == .local ? theme.colors.primary : theme.colors.secondary) + .cornerRadius(dynamicSize(userFont).unreadCorner) + } else if !chat.chatInfo.ntfsEnabled && chat.chatInfo.chatType != .local { + Image(systemName: "speaker.slash.fill") + .resizable() + .scaledToFill() + .frame(width: dynamicChatInfoSize, height: dynamicChatInfoSize) + .foregroundColor(theme.colors.secondary) + } else if chat.chatInfo.chatSettings?.favorite ?? false { + Image(systemName: "star.fill") + .resizable() + .scaledToFill() + .frame(width: dynamicChatInfoSize, height: dynamicChatInfoSize) + .padding(.trailing, 1) + .foregroundColor(theme.colors.secondary.opacity(0.65)) + } else { + Color.clear.frame(width: 0) } } private func messageDraft(_ draft: ComposeState) -> Text { let msg = draft.message - return image("rectangle.and.pencil.and.ellipsis", color: .accentColor) + return image("rectangle.and.pencil.and.ellipsis", color: theme.colors.primary) + attachment() - + messageText(msg, parseSimpleXMarkdown(msg), nil, preview: true, showSecrets: false) + + messageText(msg, parseSimpleXMarkdown(msg), nil, preview: true, showSecrets: false, secondaryColor: theme.colors.secondary) func image(_ s: String, color: Color = Color(uiColor: .tertiaryLabel)) -> Text { Text(Image(systemName: s)).foregroundColor(color) + Text(" ") @@ -172,7 +243,7 @@ struct ChatPreviewView: View { func chatItemPreview(_ cItem: ChatItem) -> Text { let itemText = cItem.meta.itemDeleted == nil ? cItem.text : markedDeletedText() 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: nil, preview: true, showSecrets: false, secondaryColor: theme.colors.secondary) // same texts are in markedDeletedText in MarkedDeletedItemView, but it returns LocalizedStringKey; // can be refactored into a single function if functions calling these are changed to return same type @@ -196,18 +267,18 @@ struct ChatPreviewView: View { } } - @ViewBuilder private func chatMessagePreview(_ cItem: ChatItem?) -> some View { + @ViewBuilder private func chatMessagePreview(_ cItem: ChatItem?, _ hasFilePreview: Bool = false) -> some View { if chatModel.draftChatId == chat.id, let draft = chatModel.draft { - chatPreviewLayout(messageDraft(draft), draft: true) + chatPreviewLayout(messageDraft(draft), draft: true, hasFilePreview) } else if let cItem = cItem { - chatPreviewLayout(itemStatusMark(cItem) + chatItemPreview(cItem)) + chatPreviewLayout(itemStatusMark(cItem) + chatItemPreview(cItem), hasFilePreview) } else { switch (chat.chatInfo) { case let .direct(contact): - if contact.activeConn == nil && contact.profile.contactLink != nil { + if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active { chatPreviewInfoText("Tap to Connect") - .foregroundColor(.accentColor) - } else if !contact.ready && contact.activeConn != nil { + .foregroundColor(theme.colors.primary) + } else if !contact.sndReady && contact.activeConn != nil { if contact.nextSendGrpInv { chatPreviewInfoText("send direct message") } else if contact.active { @@ -225,6 +296,54 @@ struct ChatPreviewView: View { } } + @ViewBuilder func chatItemContentPreview(_ chat: Chat, _ ci: ChatItem) -> some View { + let mc = ci.content.msgContent + switch mc { + case let .link(_, preview): + smallContentPreview(size: dynamicMediaSize) { + ZStack(alignment: .topTrailing) { + Image(uiImage: UIImage(base64Encoded: preview.image) ?? UIImage(systemName: "arrow.up.right")!) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: dynamicMediaSize, height: dynamicMediaSize) + ZStack { + Image(systemName: "arrow.up.right") + .resizable() + .foregroundColor(Color.white) + .font(.system(size: 15, weight: .black)) + .frame(width: 8, height: 8) + } + .frame(width: 16, height: 16) + .background(Color.black.opacity(0.25)) + .cornerRadius(8) + } + .onTapGesture { + UIApplication.shared.open(preview.uri) + } + } + case let .image(_, image): + smallContentPreview(size: dynamicMediaSize) { + CIImageView(chatItem: ci, preview: UIImage(base64Encoded: image), maxWidth: dynamicMediaSize, smallView: true, showFullScreenImage: $showFullscreenGallery) + .environmentObject(ReverseListScrollModel()) + } + case let .video(_,image, duration): + smallContentPreview(size: dynamicMediaSize) { + CIVideoView(chatItem: ci, preview: UIImage(base64Encoded: image), duration: duration, maxWidth: dynamicMediaSize, videoWidth: nil, smallView: true, showFullscreenPlayer: $showFullscreenGallery) + .environmentObject(ReverseListScrollModel()) + } + case let .voice(_, duration): + smallContentPreviewVoice(size: dynamicMediaSize) { + CIVoiceView(chat: chat, chatItem: ci, recordingFile: ci.file, duration: duration, allowMenu: Binding.constant(true), smallViewSize: dynamicMediaSize) + } + case .file: + smallContentPreviewFile(size: dynamicMediaSize) { + CIFileView(file: ci.file, edited: ci.meta.itemEdited, smallViewSize: dynamicMediaSize) + } + default: EmptyView() + } + } + + @ViewBuilder private func groupInvitationPreviewText(_ groupInfo: GroupInfo) -> some View { groupInfo.membership.memberIncognito ? chatPreviewInfoText("join as \(groupInfo.membership.memberProfile.displayName)") @@ -240,64 +359,114 @@ struct ChatPreviewView: View { private func itemStatusMark(_ cItem: ChatItem) -> Text { switch cItem.meta.itemStatus { - case .sndErrorAuth: + case .sndErrorAuth, .sndError: return Text(Image(systemName: "multiply")) .font(.caption) .foregroundColor(.red) + Text(" ") - case .sndError: + case .sndWarning: return Text(Image(systemName: "exclamationmark.triangle.fill")) .font(.caption) - .foregroundColor(.yellow) + Text(" ") + .foregroundColor(.orange) + Text(" ") default: return Text("") } } @ViewBuilder private func chatStatusImage() -> some View { + let size = dynamicSize(userFont).incognitoSize switch chat.chatInfo { case let .direct(contact): if contact.active && contact.activeConn != nil { - switch (chatModel.contactNetworkStatus(contact)) { - case .connected: incognitoIcon(chat.chatInfo.incognito) - case .error: - Image(systemName: "exclamationmark.circle") - .resizable() - .scaledToFit() - .frame(width: 17, height: 17) - .foregroundColor(.secondary) - default: - ProgressView() - } + NetworkStatusView(contact: contact, size: size) } else { - incognitoIcon(chat.chatInfo.incognito) + incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary, size: size) } case .group: if progressByTimeout { ProgressView() } else { - incognitoIcon(chat.chatInfo.incognito) + incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary, size: size) } default: - incognitoIcon(chat.chatInfo.incognito) + incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary, size: size) + } + } + + struct NetworkStatusView: View { + @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize + @EnvironmentObject var theme: AppTheme + @ObservedObject var networkModel = NetworkModel.shared + + let contact: Contact + let size: CGFloat + + var body: some View { + let dynamicChatInfoSize = dynamicSize(userFont).chatInfoSize + switch (networkModel.contactNetworkStatus(contact)) { + case .connected: incognitoIcon(contact.contactConnIncognito, theme.colors.secondary, size: size) + case .error: + Image(systemName: "exclamationmark.circle") + .resizable() + .scaledToFit() + .frame(width: dynamicChatInfoSize, height: dynamicChatInfoSize) + .foregroundColor(theme.colors.secondary) + default: + ProgressView() + } } } } -@ViewBuilder func incognitoIcon(_ incognito: Bool) -> some View { +@ViewBuilder func incognitoIcon(_ incognito: Bool, _ secondaryColor: Color, size: CGFloat) -> some View { if incognito { Image(systemName: "theatermasks") .resizable() .scaledToFit() - .frame(width: 22, height: 22) - .foregroundColor(.secondary) + .frame(width: size, height: size) + .foregroundColor(secondaryColor) } else { EmptyView() } } +func smallContentPreview(size: CGFloat, _ view: @escaping () -> some View) -> some View { + view() + .frame(width: size, height: size) + .cornerRadius(8) + .overlay(RoundedRectangle(cornerSize: CGSize(width: 8, height: 8)) + .strokeBorder(.secondary, lineWidth: 0.3, antialiased: true)) + .padding(.vertical, size / 6) + .padding(.leading, 3) + .offset(x: 6) +} + +func smallContentPreviewVoice(size: CGFloat, _ view: @escaping () -> some View) -> some View { + view() + .frame(height: voiceMessageSizeBasedOnSquareSize(size)) + .padding(.vertical, size / 6) + .padding(.leading, 8) +} + +func smallContentPreviewFile(size: CGFloat, _ view: @escaping () -> some View) -> some View { + view() + .frame(width: size, height: size) + .padding(.vertical, size / 7) + .padding(.leading, 5) +} + func unreadCountText(_ n: Int) -> Text { Text(n > 999 ? "\(n / 1000)k" : n > 0 ? "\(n)" : "") } +private struct ActiveContentPreview: Equatable { + var chat: Chat + var ci: ChatItem + var mc: MsgContent + + static func == (lhs: ActiveContentPreview, rhs: ActiveContentPreview) -> Bool { + lhs.chat.id == rhs.chat.id && lhs.ci.id == rhs.ci.id && lhs.mc == rhs.mc + } +} + struct ChatPreviewView_Previews: PreviewProvider { static var previews: some View { Group { diff --git a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift index 42e90232d6..0f64b632dc 100644 --- a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift +++ b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift @@ -11,6 +11,7 @@ import SimpleXChat struct ContactConnectionInfo: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss: DismissAction @State var contactConnection: PendingContactConnection @State private var alert: CCInfoAlert? @@ -20,7 +21,7 @@ struct ContactConnectionInfo: View { enum CCInfoAlert: Identifiable { case deleteInvitationAlert - case error(title: LocalizedStringKey, error: LocalizedStringKey) + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -48,7 +49,7 @@ struct ContactConnectionInfo: View { Section { if contactConnection.groupLinkId == nil { - settingsRow("pencil") { + settingsRow("pencil", color: theme.colors.secondary) { TextField("Set contact name…", text: $localAlias) .autocapitalization(.none) .autocorrectionDisabled(true) @@ -63,14 +64,15 @@ struct ContactConnectionInfo: View { let connReqInv = contactConnection.connReqInv { SimpleXLinkQRCode(uri: simplexChatLink(connReqInv)) incognitoEnabled() - shareLinkButton(connReqInv) - oneTimeLinkLearnMoreButton() + shareLinkButton(connReqInv, theme.colors.secondary) + oneTimeLinkLearnMoreButton(theme.colors.secondary) } else { incognitoEnabled() - oneTimeLinkLearnMoreButton() + oneTimeLinkLearnMoreButton(theme.colors.secondary) } } footer: { sharedProfileInfo(contactConnection.incognito) + .foregroundColor(theme.colors.secondary) } Section { @@ -82,6 +84,7 @@ struct ContactConnectionInfo: View { } } } + .modifier(ThemedBackground(grouped: true)) if #available(iOS 16, *) { v } else { @@ -99,7 +102,7 @@ struct ContactConnectionInfo: View { } success: { dismiss() } - case let .error(title, error): return Alert(title: Text(title), message: Text(error)) + case let .error(title, error): return mkAlert(title: title, message: error) } } .onAppear { @@ -149,7 +152,7 @@ struct ContactConnectionInfo: View { HStack(spacing: 6) { Text("Incognito") Image(systemName: "info.circle") - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) .font(.system(size: 14)) } .onTapGesture { @@ -164,23 +167,24 @@ struct ContactConnectionInfo: View { } } -private func shareLinkButton(_ connReqInvitation: String) -> some View { +private func shareLinkButton(_ connReqInvitation: String, _ secondaryColor: Color) -> some View { Button { showShareSheet(items: [simplexChatLink(connReqInvitation)]) } label: { - settingsRow("square.and.arrow.up") { + settingsRow("square.and.arrow.up", color: secondaryColor) { Text("Share 1-time link") } } } -private func oneTimeLinkLearnMoreButton() -> some View { +private func oneTimeLinkLearnMoreButton(_ secondaryColor: Color) -> some View { NavigationLink { AddContactLearnMore(showTitle: false) .navigationTitle("One-time invitation link") + .modifier(ThemedBackground()) .navigationBarTitleDisplayMode(.large) } label: { - settingsRow("info.circle") { + settingsRow("info.circle", color: secondaryColor) { Text("Learn more") } } diff --git a/apps/ios/Shared/Views/ChatList/ContactConnectionView.swift b/apps/ios/Shared/Views/ChatList/ContactConnectionView.swift index d21f347881..f5156d86b8 100644 --- a/apps/ios/Shared/Views/ChatList/ContactConnectionView.swift +++ b/apps/ios/Shared/Views/ChatList/ContactConnectionView.swift @@ -12,9 +12,10 @@ import SimpleXChat struct ContactConnectionView: View { @EnvironmentObject var m: ChatModel @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme + @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize @State private var localAlias = "" @FocusState private var aliasTextFieldFocused: Bool - @State private var showContactConnectionInfo = false var body: some View { if case let .contactConnection(conn) = chat.chatInfo { @@ -29,8 +30,7 @@ struct ContactConnectionView: View { .resizable() .scaledToFill() .frame(width: 48, height: 48) - .foregroundColor(Color(uiColor: .secondarySystemBackground)) - .onTapGesture { showContactConnectionInfo = true } + .foregroundColor(Color(uiColor: .tertiarySystemGroupedBackground).asAnotherColorFromSecondaryVariant(theme)) } .frame(width: 63, height: 63) .padding(.leading, 4) @@ -41,7 +41,7 @@ struct ContactConnectionView: View { .font(.title3) .bold() .allowsTightening(false) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .padding(.horizontal, 8) .padding(.top, 1) .padding(.bottom, 0.5) @@ -54,14 +54,14 @@ struct ContactConnectionView: View { .padding(.trailing, 8) .padding(.vertical, 4) .frame(minWidth: 60, alignment: .trailing) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } .padding(.bottom, 2) ZStack(alignment: .topTrailing) { Text(contactConnection.description) .frame(maxWidth: .infinity, alignment: .leading) - incognitoIcon(contactConnection.incognito) + incognitoIcon(contactConnection.incognito, theme.colors.secondary, size: dynamicSize(userFont).incognitoSize) .padding(.top, 26) .frame(maxWidth: .infinity, alignment: .trailing) } @@ -70,9 +70,6 @@ struct ContactConnectionView: View { Spacer() } .frame(maxHeight: .infinity) - .appSheet(isPresented: $showContactConnectionInfo) { - ContactConnectionInfo(contactConnection: contactConnection) - } } } } diff --git a/apps/ios/Shared/Views/ChatList/ContactRequestView.swift b/apps/ios/Shared/Views/ChatList/ContactRequestView.swift index dacf51a5e8..9276bbfc78 100644 --- a/apps/ios/Shared/Views/ChatList/ContactRequestView.swift +++ b/apps/ios/Shared/Views/ChatList/ContactRequestView.swift @@ -11,19 +11,21 @@ import SimpleXChat struct ContactRequestView: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme + @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize var contactRequest: UserContactRequest @ObservedObject var chat: Chat var body: some View { HStack(spacing: 8) { - ChatInfoImage(chat: chat, size: 63) + ChatInfoImage(chat: chat, size: dynamicSize(userFont).profileImageSize) .padding(.leading, 4) VStack(alignment: .leading, spacing: 0) { HStack(alignment: .top) { Text(contactRequest.chatViewName) .font(.title3) .fontWeight(.bold) - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) .padding(.leading, 8) .frame(alignment: .topLeading) Spacer() @@ -32,7 +34,7 @@ struct ContactRequestView: View { .padding(.trailing, 8) .padding(.top, 4) .frame(minWidth: 60, alignment: .trailing) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } .padding(.bottom, 2) diff --git a/apps/ios/Shared/Views/ChatList/OneHandUICard.swift b/apps/ios/Shared/Views/ChatList/OneHandUICard.swift new file mode 100644 index 0000000000..636d165114 --- /dev/null +++ b/apps/ios/Shared/Views/ChatList/OneHandUICard.swift @@ -0,0 +1,52 @@ +// +// OneHandUICard.swift +// SimpleX (iOS) +// +// Created by EP on 06/08/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct OneHandUICard: View { + @EnvironmentObject var theme: AppTheme + @Environment(\.dynamicTypeSize) private var userFont: DynamicTypeSize + @AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true + @AppStorage(DEFAULT_ONE_HAND_UI_CARD_SHOWN) private var oneHandUICardShown = false + @State private var showOneHandUIAlert = false + + var body: some View { + ZStack(alignment: .topTrailing) { + VStack(alignment: .leading, spacing: 8) { + Text("Toggle chat list:").font(.title3) + Toggle("Reachable chat toolbar", isOn: $oneHandUI) + } + Image(systemName: "multiply") + .foregroundColor(theme.colors.secondary) + .onTapGesture { + showOneHandUIAlert = true + } + } + .padding() + .background(theme.appColors.sentMessage) + .cornerRadius(12) + .frame(height: dynamicSize(userFont).rowHeight) + .padding(.vertical, 12) + .alert(isPresented: $showOneHandUIAlert) { + Alert( + title: Text("Reachable chat toolbar"), + message: Text("You can change it in Appearance settings."), + dismissButton: .default(Text("Ok")) { + withAnimation { + oneHandUICardShown = true + } + } + ) + } + } +} + +#Preview { + OneHandUICard() +} diff --git a/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift b/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift new file mode 100644 index 0000000000..477a78e36d --- /dev/null +++ b/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift @@ -0,0 +1,752 @@ +// +// ServersSummaryView.swift +// SimpleX (iOS) +// +// Created by spaced4ndy on 25.06.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct ServersSummaryView: View { + @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme + @State private var serversSummary: PresentedServersSummary? = nil + @State private var selectedUserCategory: PresentedUserCategory = .allUsers + @State private var selectedServerType: PresentedServerType = .smp + @State private var selectedSMPServer: String? = nil + @State private var selectedXFTPServer: String? = nil + @State private var timer: Timer? = nil + @State private var alert: SomeAlert? + + @AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false + + enum PresentedUserCategory { + case currentUser + case allUsers + } + + enum PresentedServerType { + case smp + case xftp + } + + var body: some View { + NavigationView { + viewBody() + .navigationTitle("Servers info") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + shareButton() + } + } + } + .onAppear { + if m.users.filter({ u in u.user.activeUser || !u.user.hidden }).count == 1 { + selectedUserCategory = .currentUser + } + getServersSummary() + startTimer() + } + .onDisappear { + stopTimer() + } + .alert(item: $alert) { $0.alert } + } + + private func startTimer() { + timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in + if AppChatState.shared.value == .active { + getServersSummary() + } + } + } + + private func getServersSummary() { + do { + serversSummary = try getAgentServersSummary() + } catch let error { + logger.error("getAgentServersSummary error: \(responseError(error))") + } + } + + private func stopTimer() { + timer?.invalidate() + timer = nil + } + + private func shareButton() -> some View { + Button { + if let serversSummary = serversSummary { + showShareSheet(items: [encodePrettyPrinted(serversSummary)]) + } + } label: { + Image(systemName: "square.and.arrow.up") + } + .disabled(serversSummary == nil) + } + + public func encodePrettyPrinted(_ value: T) -> String { + let encoder = jsonEncoder + encoder.outputFormatting = .prettyPrinted + let data = try! encoder.encode(value) + return String(decoding: data, as: UTF8.self) + } + + @ViewBuilder private func viewBody() -> some View { + if let summ = serversSummary { + List { + Group { + if m.users.filter({ u in u.user.activeUser || !u.user.hidden }).count > 1 { + Picker("User selection", selection: $selectedUserCategory) { + Text("All profiles").tag(PresentedUserCategory.allUsers) + Text("Current profile").tag(PresentedUserCategory.currentUser) + } + .pickerStyle(.segmented) + } + + Picker("Server type", selection: $selectedServerType) { + Text("Messages").tag(PresentedServerType.smp) + Text("Files").tag(PresentedServerType.xftp) + } + .pickerStyle(.segmented) + } + .listRowBackground(Color.clear) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + + switch (selectedUserCategory, selectedServerType) { + case (.allUsers, .smp): + let smpSumm = summ.allUsersSMP + let (totals, curr, prev, prox) = (smpSumm.smpTotals, smpSumm.currentlyUsedSMPServers, smpSumm.previouslyUsedSMPServers, smpSumm.onlyProxiedSMPServers) + + SMPStatsView(stats: totals.stats, statsStartedAt: summ.statsStartedAt) + + smpSubsSection(totals) + + if curr.count > 0 { + smpServersListView(curr, summ.statsStartedAt, "Connected servers") + } + if prev.count > 0 { + smpServersListView(prev, summ.statsStartedAt, "Previously connected servers") + } + if prox.count > 0 { + smpServersListView(prox, summ.statsStartedAt, "Proxied servers", "You are not connected to these servers. Private routing is used to deliver messages to them.") + } + + ServerSessionsView(sess: totals.sessions) + case (.currentUser, .smp): + let smpSumm = summ.currentUserSMP + let (totals, curr, prev, prox) = (smpSumm.smpTotals, smpSumm.currentlyUsedSMPServers, smpSumm.previouslyUsedSMPServers, smpSumm.onlyProxiedSMPServers) + + SMPStatsView(stats: totals.stats, statsStartedAt: summ.statsStartedAt) + + smpSubsSection(totals) + + if curr.count > 0 { + smpServersListView(curr, summ.statsStartedAt, "Connected servers") + } + if prev.count > 0 { + smpServersListView(prev, summ.statsStartedAt, "Previously connected servers") + } + if prox.count > 0 { + smpServersListView(prox, summ.statsStartedAt, "Proxied servers", "You are not connected to these servers. Private routing is used to deliver messages to them.") + } + + ServerSessionsView(sess: totals.sessions) + case (.allUsers, .xftp): + let xftpSumm = summ.allUsersXFTP + let (totals, curr, prev) = (xftpSumm.xftpTotals, xftpSumm.currentlyUsedXFTPServers, xftpSumm.previouslyUsedXFTPServers) + + XFTPStatsView(stats: totals.stats, statsStartedAt: summ.statsStartedAt) + + if curr.count > 0 { + xftpServersListView(curr, summ.statsStartedAt, "Connected servers") + } + if prev.count > 0 { + xftpServersListView(prev, summ.statsStartedAt, "Previously connected servers") + } + + ServerSessionsView(sess: totals.sessions) + case (.currentUser, .xftp): + let xftpSumm = summ.currentUserXFTP + let (totals, curr, prev) = (xftpSumm.xftpTotals, xftpSumm.currentlyUsedXFTPServers, xftpSumm.previouslyUsedXFTPServers) + + XFTPStatsView(stats: totals.stats, statsStartedAt: summ.statsStartedAt) + + if curr.count > 0 { + xftpServersListView(curr, summ.statsStartedAt, "Connected servers") + } + if prev.count > 0 { + xftpServersListView(prev, summ.statsStartedAt, "Previously connected servers") + } + + ServerSessionsView(sess: totals.sessions) + } + + Section { + reconnectAllButton() + resetStatsButton() + } + } + } else { + Text("No info, try to reload") + .foregroundColor(theme.colors.secondary) + .background(theme.colors.background) + } + } + + private func smpSubsSection(_ totals: SMPTotals) -> some View { + Section { + infoRow("Active connections", numOrDash(totals.subs.ssActive)) + infoRow("Total", numOrDash(totals.subs.total)) + Toggle("Show percentage", isOn: $showSubscriptionPercentage) + } header: { + HStack { + Text("Message reception") + SubscriptionStatusIndicatorView(subs: totals.subs, hasSess: totals.sessions.hasSess) + if showSubscriptionPercentage { + SubscriptionStatusPercentageView(subs: totals.subs, hasSess: totals.sessions.hasSess) + } + } + } + } + + private func reconnectAllButton() -> some View { + Button { + alert = SomeAlert( + alert: Alert( + title: Text("Reconnect all servers?"), + message: Text("Reconnect all connected servers to force message delivery. It uses additional traffic."), + primaryButton: .default(Text("Ok")) { + Task { + do { + try await reconnectAllServers() + } catch let error { + alert = SomeAlert( + alert: mkAlert( + title: "Error reconnecting servers", + message: "\(responseError(error))" + ), + id: "error reconnecting servers" + ) + } + } + }, + secondaryButton: .cancel() + ), + id: "reconnect servers question" + ) + } label: { + Text("Reconnect all servers") + } + } + + @ViewBuilder private func smpServersListView( + _ servers: [SMPServerSummary], + _ statsStartedAt: Date, + _ header: LocalizedStringKey? = nil, + _ footer: LocalizedStringKey? = nil + ) -> some View { + let sortedServers = servers.sorted { + $0.hasSubs == $1.hasSubs + ? serverAddress($0.smpServer) < serverAddress($1.smpServer) + : $0.hasSubs && !$1.hasSubs + } + Section { + ForEach(sortedServers) { server in + smpServerView(server, statsStartedAt) + } + } header: { + if let header = header { + Text(header) + } + } footer: { + if let footer = footer { + Text(footer) + } + } + } + + private func smpServerView(_ srvSumm: SMPServerSummary, _ statsStartedAt: Date) -> some View { + NavigationLink(tag: srvSumm.id, selection: $selectedSMPServer) { + SMPServerSummaryView( + summary: srvSumm, + statsStartedAt: statsStartedAt + ) + .navigationBarTitle("SMP server") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } label: { + HStack { + Text(serverAddress(srvSumm.smpServer)) + .lineLimit(1) + if let subs = srvSumm.subs { + Spacer() + if showSubscriptionPercentage { + SubscriptionStatusPercentageView(subs: subs, hasSess: srvSumm.sessionsOrNew.hasSess) + } + SubscriptionStatusIndicatorView(subs: subs, hasSess: srvSumm.sessionsOrNew.hasSess) + } else if let sess = srvSumm.sessions { + Spacer() + Image(systemName: "arrow.up.circle") + .symbolRenderingMode(.palette) + .foregroundStyle(sessIconColor(sess), Color.clear) + } + } + } + } + + private func serverAddress(_ server: String) -> String { + parseServerAddress(server)?.hostnames.first ?? server + } + + private func sessIconColor(_ sess: ServerSessions) -> Color { + let online = m.networkInfo.online + return ( + online && sess.ssConnected > 0 + ? sessionActiveColor + : Color(uiColor: .tertiaryLabel) + ) + } + + private var sessionActiveColor: Color { + let onionHosts = networkUseOnionHostsGroupDefault.get() + return onionHosts == .require ? .indigo : .accentColor + } + + @ViewBuilder private func xftpServersListView( + _ servers: [XFTPServerSummary], + _ statsStartedAt: Date, + _ header: LocalizedStringKey? = nil, + _ footer: LocalizedStringKey? = nil + ) -> some View { + let sortedServers = servers.sorted { serverAddress($0.xftpServer) < serverAddress($1.xftpServer) } + Section { + ForEach(sortedServers) { server in + xftpServerView(server, statsStartedAt) + } + } header: { + if let header = header { + Text(header) + } + } footer: { + if let footer = footer { + Text(footer) + } + } + } + + private func xftpServerView(_ srvSumm: XFTPServerSummary, _ statsStartedAt: Date) -> some View { + NavigationLink(tag: srvSumm.id, selection: $selectedXFTPServer) { + XFTPServerSummaryView( + summary: srvSumm, + statsStartedAt: statsStartedAt + ) + .navigationBarTitle("XFTP server") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } label: { + HStack { + Text(serverAddress(srvSumm.xftpServer)) + .lineLimit(1) + if let inProgressIcon = inProgressIcon(srvSumm) { + Spacer() + Image(systemName: inProgressIcon) + .symbolRenderingMode(.palette) + .foregroundStyle(sessionActiveColor, Color.clear) + } + } + } + } + + private func inProgressIcon(_ srvSumm: XFTPServerSummary) -> String? { + switch (srvSumm.rcvInProgress, srvSumm.sndInProgress, srvSumm.delInProgress) { + case (false, false, false): nil + case (true, false, false): "arrow.down.circle" + case (false, true, false): "arrow.up.circle" + case (false, false, true): "trash.circle" + default: "arrow.up.arrow.down.circle" + } + } + + private func resetStatsButton() -> some View { + Button { + alert = SomeAlert( + alert: Alert( + title: Text("Reset all statistics?"), + message: Text("Servers statistics will be reset - this cannot be undone!"), + primaryButton: .destructive(Text("Reset")) { + Task { + do { + try await resetAgentServersStats() + getServersSummary() + } catch let error { + alert = SomeAlert( + alert: mkAlert( + title: "Error resetting statistics", + message: "\(responseError(error))" + ), + id: "error resetting statistics" + ) + } + } + }, + secondaryButton: .cancel() + ), + id: "reset statistics question" + ) + } label: { + Text("Reset all statistics") + } + } +} + +struct SubscriptionStatusIndicatorView: View { + @EnvironmentObject var m: ChatModel + var subs: SMPServerSubs + var hasSess: Bool + + var body: some View { + let onionHosts = networkUseOnionHostsGroupDefault.get() + let (color, variableValue, opacity, _) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, hasSess) + if #available(iOS 16.0, *) { + Image(systemName: "dot.radiowaves.up.forward", variableValue: variableValue) + .foregroundColor(color) + } else { + Image(systemName: "dot.radiowaves.up.forward") + .foregroundColor(color.opacity(opacity)) + } + } +} + +struct SubscriptionStatusPercentageView: View { + @EnvironmentObject var m: ChatModel + var subs: SMPServerSubs + var hasSess: Bool + + var body: some View { + let onionHosts = networkUseOnionHostsGroupDefault.get() + let (_, _, _, statusPercent) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, hasSess) + Text(verbatim: "\(Int(floor(statusPercent * 100)))%") + .foregroundColor(.secondary) + .font(.caption) + } +} + +func subscriptionStatusColorAndPercentage(_ online: Bool, _ onionHosts: OnionHosts, _ subs: SMPServerSubs, _ hasSess: Bool) -> (Color, Double, Double, Double) { + func roundedToQuarter(_ n: Double) -> Double { + n >= 1 ? 1 + : n <= 0 ? 0 + : (n * 4).rounded() / 4 + } + + let activeColor: Color = onionHosts == .require ? .indigo : .accentColor + let noConnColorAndPercent: (Color, Double, Double, Double) = (Color(uiColor: .tertiaryLabel), 1, 1, 0) + let activeSubsRounded = roundedToQuarter(subs.shareOfActive) + + return !online + ? noConnColorAndPercent + : ( + subs.total == 0 && !hasSess + ? (activeColor, 0, 0.33, 0) // On freshly installed app (without chats) and on app start + : ( + subs.ssActive == 0 + ? ( + hasSess ? (activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) : noConnColorAndPercent + ) + : ( // ssActive > 0 + hasSess + ? (activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) + : (.orange, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) // This would mean implementation error + ) + ) + ) +} + +struct SMPServerSummaryView: View { + var summary: SMPServerSummary + var statsStartedAt: Date + @State private var alert: SomeAlert? + + @AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false + + var body: some View { + List { + Section("Server address") { + Text(summary.smpServer) + .textSelection(.enabled) + if summary.known == true { + NavigationLink { + ProtocolServersView(serverProtocol: .smp) + .navigationTitle("Your SMP servers") + .modifier(ThemedBackground(grouped: true)) + } label: { + Text("Open server settings") + } + } + } + + if let stats = summary.stats { + SMPStatsView(stats: stats, statsStartedAt: statsStartedAt) + } + + if let subs = summary.subs { + smpSubsSection(subs) + } + + if let sess = summary.sessions { + ServerSessionsView(sess: sess) + } + } + .alert(item: $alert) { $0.alert } + } + + private func smpSubsSection(_ subs: SMPServerSubs) -> some View { + Section { + infoRow("Active connections", numOrDash(subs.ssActive)) + infoRow("Pending", numOrDash(subs.ssPending)) + infoRow("Total", numOrDash(subs.total)) + reconnectButton() + } header: { + HStack { + Text("Message reception") + SubscriptionStatusIndicatorView(subs: subs, hasSess: summary.sessionsOrNew.hasSess) + if showSubscriptionPercentage { + SubscriptionStatusPercentageView(subs: subs, hasSess: summary.sessionsOrNew.hasSess) + } + } + } + } + + private func reconnectButton() -> some View { + Button { + alert = SomeAlert( + alert: Alert( + title: Text("Reconnect server?"), + message: Text("Reconnect server to force message delivery. It uses additional traffic."), + primaryButton: .default(Text("Ok")) { + Task { + do { + try await reconnectServer(smpServer: summary.smpServer) + } catch let error { + alert = SomeAlert( + alert: mkAlert( + title: "Error reconnecting server", + message: "\(responseError(error))" + ), + id: "error reconnecting server" + ) + } + } + }, + secondaryButton: .cancel() + ), + id: "reconnect server question" + ) + } label: { + Text("Reconnect") + } + } +} + +struct ServerSessionsView: View { + var sess: ServerSessions + + var body: some View { + Section("Transport sessions") { + infoRow("Connected", numOrDash(sess.ssConnected)) + infoRow("Errors", numOrDash(sess.ssErrors)) + infoRow("Connecting", numOrDash(sess.ssConnecting)) + } + } +} + +struct SMPStatsView: View { + var stats: AgentSMPServerStatsData + var statsStartedAt: Date + + var body: some View { + Section { + infoRow("Messages sent", numOrDash(stats._sentDirect + stats._sentViaProxy)) + infoRow("Messages received", numOrDash(stats._recvMsgs)) + NavigationLink { + DetailedSMPStatsView(stats: stats, statsStartedAt: statsStartedAt) + .navigationTitle("Detailed statistics") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } label: { + Text("Details") + } + } header: { + Text("Statistics") + } footer: { + Text("Starting from \(localTimestamp(statsStartedAt)).") + Text("\n") + Text("All data is private to your device.") + } + } +} + +private func numOrDash(_ n: Int) -> String { + n == 0 ? "-" : "\(n)" +} + +struct DetailedSMPStatsView: View { + var stats: AgentSMPServerStatsData + var statsStartedAt: Date + + var body: some View { + List { + Section("Sent messages") { + infoRow("Sent total", numOrDash(stats._sentDirect + stats._sentViaProxy)) + infoRowTwoValues("Sent directly", "attempts", stats._sentDirect, stats._sentDirectAttempts) + infoRowTwoValues("Sent via proxy", "attempts", stats._sentViaProxy, stats._sentViaProxyAttempts) + infoRowTwoValues("Proxied", "attempts", stats._sentProxied, stats._sentProxiedAttempts) + Text("Send errors") + infoRow(Text(verbatim: "AUTH"), numOrDash(stats._sentAuthErrs)).padding(.leading, 24) + infoRow(Text(verbatim: "QUOTA"), numOrDash(stats._sentQuotaErrs)).padding(.leading, 24) + infoRow("expired", numOrDash(stats._sentExpiredErrs)).padding(.leading, 24) + infoRow("other", numOrDash(stats._sentOtherErrs)).padding(.leading, 24) + } + Section("Received messages") { + infoRow("Received total", numOrDash(stats._recvMsgs)) + Text("Receive errors") + infoRow("duplicates", numOrDash(stats._recvDuplicates)).padding(.leading, 24) + infoRow("decryption errors", numOrDash(stats._recvCryptoErrs)).padding(.leading, 24) + infoRow("other errors", numOrDash(stats._recvErrs)).padding(.leading, 24) + infoRowTwoValues("Acknowledged", "attempts", stats._ackMsgs, stats._ackAttempts) + Text("Acknowledgement errors") + infoRow(Text(verbatim: "NO_MSG errors"), numOrDash(stats._ackNoMsgErrs)).padding(.leading, 24) + infoRow("other errors", numOrDash(stats._ackOtherErrs)).padding(.leading, 24) + } + Section("Connections") { + infoRow("Created", numOrDash(stats._connCreated)) + infoRow("Secured", numOrDash(stats._connCreated)) + infoRow("Completed", numOrDash(stats._connCompleted)) + infoRowTwoValues("Deleted", "attempts", stats._connDeleted, stats._connDelAttempts) + infoRow("Deletion errors", numOrDash(stats._connDelErrs)) + infoRowTwoValues("Subscribed", "attempts", stats._connSubscribed, stats._connSubAttempts) + infoRow("Subscriptions ignored", numOrDash(stats._connSubIgnored)) + infoRow("Subscription errors", numOrDash(stats._connSubErrs)) + } + Section { + infoRowTwoValues("Enabled", "attempts", stats._ntfKey, stats._ntfKeyAttempts) + infoRowTwoValues("Disabled", "attempts", stats._ntfKeyDeleted, stats._ntfKeyDeleteAttempts) + } header: { + Text("Connection notifications") + } footer: { + Text("Starting from \(localTimestamp(statsStartedAt)).") + } + } + } +} + +private func infoRowTwoValues(_ title: LocalizedStringKey, _ title2: LocalizedStringKey, _ value: Int, _ value2: Int) -> some View { + HStack { + Text(title) + Text(verbatim: " / ").font(.caption2) + Text(title2).font(.caption2) + Spacer() + Group { + if value == 0 && value2 == 0 { + Text(verbatim: "-") + } else { + Text(numOrDash(value)) + Text(verbatim: " / ").font(.caption2) + Text(numOrDash(value2)).font(.caption2) + } + } + .foregroundStyle(.secondary) + } +} + +struct XFTPServerSummaryView: View { + var summary: XFTPServerSummary + var statsStartedAt: Date + + var body: some View { + List { + Section("Server address") { + Text(summary.xftpServer) + .textSelection(.enabled) + if summary.known == true { + NavigationLink { + ProtocolServersView(serverProtocol: .xftp) + .navigationTitle("Your XFTP servers") + .modifier(ThemedBackground(grouped: true)) + } label: { + Text("Open server settings") + } + } + } + + if let stats = summary.stats { + XFTPStatsView(stats: stats, statsStartedAt: statsStartedAt) + } + + if let sess = summary.sessions { + ServerSessionsView(sess: sess) + } + } + } +} + +struct XFTPStatsView: View { + var stats: AgentXFTPServerStatsData + var statsStartedAt: Date + @State private var expanded = false + + var body: some View { + Section { + infoRow("Uploaded", prettySize(stats._uploadsSize)) + infoRow("Downloaded", prettySize(stats._downloadsSize)) + NavigationLink { + DetailedXFTPStatsView(stats: stats, statsStartedAt: statsStartedAt) + .navigationTitle("Detailed statistics") + .navigationBarTitleDisplayMode(.large) + .modifier(ThemedBackground(grouped: true)) + } label: { + Text("Details") + } + } header: { + Text("Statistics") + } footer: { + Text("Starting from \(localTimestamp(statsStartedAt)).") + Text("\n") + Text("All data is private to your device.") + } + } +} + +private func prettySize(_ sizeInKB: Int64) -> String { + let kb: Int64 = 1024 + return sizeInKB == 0 ? "-" : ByteCountFormatter.string(fromByteCount: sizeInKB * kb, countStyle: .binary) +} + +struct DetailedXFTPStatsView: View { + var stats: AgentXFTPServerStatsData + var statsStartedAt: Date + + var body: some View { + List { + Section("Uploaded files") { + infoRow("Size", prettySize(stats._uploadsSize)) + infoRowTwoValues("Chunks uploaded", "attempts", stats._uploads, stats._uploadAttempts) + infoRow("Upload errors", numOrDash(stats._uploadErrs)) + infoRowTwoValues("Chunks deleted", "attempts", stats._deletions, stats._deleteAttempts) + infoRow("Deletion errors", numOrDash(stats._deleteErrs)) + } + Section { + infoRow("Size", prettySize(stats._downloadsSize)) + infoRowTwoValues("Chunks downloaded", "attempts", stats._downloads, stats._downloadAttempts) + Text("Download errors") + infoRow(Text(verbatim: "AUTH"), numOrDash(stats._downloadAuthErrs)).padding(.leading, 24) + infoRow("other", numOrDash(stats._downloadErrs)).padding(.leading, 24) + } header: { + Text("Downloaded files") + } footer: { + Text("Starting from \(localTimestamp(statsStartedAt)).") + } + } + } +} + +#Preview { + ServersSummaryView() +} diff --git a/apps/ios/Shared/Views/ChatList/UserPicker.swift b/apps/ios/Shared/Views/ChatList/UserPicker.swift index a615f9c118..5041e093db 100644 --- a/apps/ios/Shared/Views/ChatList/UserPicker.swift +++ b/apps/ios/Shared/Views/ChatList/UserPicker.swift @@ -6,13 +6,10 @@ import SwiftUI import SimpleXChat -private let fillColorDark = Color(uiColor: UIColor(red: 0.11, green: 0.11, blue: 0.11, alpha: 255)) -private let fillColorLight = Color(uiColor: UIColor(red: 0.99, green: 0.99, blue: 0.99, alpha: 255)) - struct UserPicker: View { @EnvironmentObject var m: ChatModel - @Environment(\.colorScheme) var colorScheme @Environment(\.scenePhase) var scenePhase + @EnvironmentObject var theme: AppTheme @Binding var showSettings: Bool @Binding var showConnectDesktop: Bool @Binding var userPickerVisible: Bool @@ -21,9 +18,6 @@ struct UserPicker: View { private let menuButtonHeight: CGFloat = 68 @State var chatViewNameWidth: CGFloat = 0 - var fillColor: Color { - colorScheme == .dark ? fillColorDark : fillColorLight - } var body: some View { VStack { @@ -82,7 +76,7 @@ struct UserPicker: View { .clipShape(RoundedRectangle(cornerRadius: 16)) .background( Rectangle() - .fill(fillColor) + .fill(theme.colors.surface) .cornerRadius(16) .shadow(color: .black.opacity(0.12), radius: 24, x: 0, y: 0) ) @@ -91,15 +85,18 @@ struct UserPicker: View { .padding(8) .opacity(userPickerVisible ? 1.0 : 0.0) .onAppear { - do { - // This check prevents the call of listUsers after the app is suspended, and the database is closed. - if case .active = scenePhase { - m.users = try listUsers() - } - } catch let error { - logger.error("Error loading users \(responseError(error))") - } - } + // This check prevents the call of listUsers after the app is suspended, and the database is closed. + if case .active = scenePhase { + Task { + do { + let users = try await listUsersAsync() + await MainActor.run { m.users = users } + } catch { + logger.error("Error loading users \(responseError(error))") + } + } + } + } } private func userView(_ u: UserInfo) -> some View { @@ -131,13 +128,13 @@ struct UserPicker: View { .padding(.trailing, 12) Text(user.chatViewName) .fontWeight(user.activeUser ? .medium : .regular) - .foregroundColor(.primary) + .foregroundColor(theme.colors.onBackground) .overlay(DetermineWidth()) Spacer() if user.activeUser { Image(systemName: "checkmark") } else if u.unreadCount > 0 { - unreadCounter(u.unreadCount, color: user.showNtfs ? .accentColor : .secondary) + unreadCounter(u.unreadCount, color: user.showNtfs ? theme.colors.primary : theme.colors.secondary) } else if !user.showNtfs { Image(systemName: "speaker.slash") } @@ -145,7 +142,7 @@ struct UserPicker: View { .padding(.trailing) .padding([.leading, .vertical], 12) }) - .buttonStyle(PressedButtonStyle(defaultColor: fillColor, pressedColor: Color(uiColor: .secondarySystemFill))) + .buttonStyle(PressedButtonStyle(defaultColor: theme.colors.surface, pressedColor: Color(uiColor: .secondarySystemFill))) } private func menuButton(_ title: LocalizedStringKey, icon: String, action: @escaping () -> Void) -> some View { @@ -156,13 +153,13 @@ struct UserPicker: View { Spacer() Image(systemName: icon) .symbolRenderingMode(.monochrome) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } .padding(.horizontal) .padding(.vertical, 22) .frame(height: menuButtonHeight) } - .buttonStyle(PressedButtonStyle(defaultColor: fillColor, pressedColor: Color(uiColor: .secondarySystemFill))) + .buttonStyle(PressedButtonStyle(defaultColor: theme.colors.surface, pressedColor: Color(uiColor: .secondarySystemFill))) } } diff --git a/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift b/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift new file mode 100644 index 0000000000..4b43610236 --- /dev/null +++ b/apps/ios/Shared/Views/Contacts/ContactListNavLink.swift @@ -0,0 +1,267 @@ +// +// ContactListNavLink.swift +// SimpleX (iOS) +// +// Created by Diogo Cunha on 01/08/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct ContactListNavLink: View { + @EnvironmentObject var theme: AppTheme + @ObservedObject var chat: Chat + var showDeletedChatIcon: Bool + @State private var alert: SomeAlert? = nil + @State private var actionSheet: SomeActionSheet? = nil + @State private var sheet: SomeSheet? = nil + @State private var showConnectContactViaAddressDialog = false + @State private var showContactRequestDialog = false + + var body: some View { + let contactType = chatContactType(chat: chat) + + Group { + switch (chat.chatInfo) { + case let .direct(contact): + switch contactType { + case .recent: + recentContactNavLink(contact) + case .chatDeleted: + deletedChatNavLink(contact) + case .card: + contactCardNavLink(contact) + default: + EmptyView() + } + case let .contactRequest(contactRequest): + contactRequestNavLink(contactRequest) + default: + EmptyView() + } + } + .alert(item: $alert) { $0.alert } + .actionSheet(item: $actionSheet) { $0.actionSheet } + .sheet(item: $sheet) { + if #available(iOS 16.0, *) { + $0.content + .presentationDetents([.fraction(0.4)]) + } else { + $0.content + } + } + } + + func recentContactNavLink(_ contact: Contact) -> some View { + Button { + dismissAllSheets(animated: true) { + ItemsModel.shared.loadOpenChat(contact.id) + } + } label: { + contactPreview(contact, titleColor: theme.colors.onBackground) + } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + Button { + deleteContactDialog( + chat, + contact, + dismissToChatList: false, + showAlert: { alert = $0 }, + showActionSheet: { actionSheet = $0 }, + showSheetContent: { sheet = $0 } + ) + } label: { + Label("Delete", systemImage: "trash") + } + .tint(.red) + } + } + + func deletedChatNavLink(_ contact: Contact) -> some View { + Button { + Task { + await MainActor.run { + dismissAllSheets(animated: true) { + ItemsModel.shared.loadOpenChat(contact.id) + } + } + } + } label: { + contactPreview(contact, titleColor: theme.colors.onBackground) + } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + Button { + deleteContactDialog( + chat, + contact, + dismissToChatList: false, + showAlert: { alert = $0 }, + showActionSheet: { actionSheet = $0 }, + showSheetContent: { sheet = $0 } + ) + } label: { + Label("Delete", systemImage: "trash") + } + .tint(.red) + } + } + + func contactPreview(_ contact: Contact, titleColor: Color) -> some View { + HStack{ + ProfileImage(imageStr: contact.image, size: 30) + + previewTitle(contact, titleColor: titleColor) + + Spacer() + + HStack { + if showDeletedChatIcon && contact.chatDeleted { + Image(systemName: "archivebox") + .resizable() + .scaledToFit() + .frame(width: 18, height: 18) + .foregroundColor(.secondary.opacity(0.65)) + } else if chat.chatInfo.chatSettings?.favorite ?? false { + Image(systemName: "star.fill") + .resizable() + .scaledToFill() + .frame(width: 18, height: 18) + .foregroundColor(.secondary.opacity(0.65)) + } + if contact.contactConnIncognito { + Image(systemName: "theatermasks") + .resizable() + .scaledToFit() + .frame(width: 22, height: 22) + .foregroundColor(.secondary) + } + } + } + } + + @ViewBuilder private func previewTitle(_ contact: Contact, titleColor: Color) -> some View { + let t = Text(chat.chatInfo.chatViewName).foregroundColor(titleColor) + ( + contact.verified == true + ? verifiedIcon + t + : t + ) + .lineLimit(1) + } + + private var verifiedIcon: Text { + (Text(Image(systemName: "checkmark.shield")) + Text(" ")) + .foregroundColor(.secondary) + .baselineOffset(1) + .kerning(-2) + } + + func contactCardNavLink(_ contact: Contact) -> some View { + Button { + showConnectContactViaAddressDialog = true + } label: { + contactCardPreview(contact) + } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + Button { + deleteContactDialog( + chat, + contact, + dismissToChatList: false, + showAlert: { alert = $0 }, + showActionSheet: { actionSheet = $0 }, + showSheetContent: { sheet = $0 } + ) + } label: { + Label("Delete", systemImage: "trash") + } + .tint(.red) + } + .confirmationDialog("Connect with \(contact.chatViewName)", isPresented: $showConnectContactViaAddressDialog, titleVisibility: .visible) { + Button("Use current profile") { connectContactViaAddress_(contact, false) } + Button("Use new incognito profile") { connectContactViaAddress_(contact, true) } + } + } + + private func connectContactViaAddress_(_ contact: Contact, _ incognito: Bool) { + Task { + let ok = await connectContactViaAddress(contact.contactId, incognito, showAlert: { alert = SomeAlert(alert: $0, id: "ContactListNavLink connectContactViaAddress") }) + if ok { + ItemsModel.shared.loadOpenChat(contact.id) + DispatchQueue.main.async { + dismissAllSheets(animated: true) { + AlertManager.shared.showAlert(connReqSentAlert(.contact)) + } + } + } + } + } + + func contactCardPreview(_ contact: Contact) -> some View { + HStack{ + ProfileImage(imageStr: contact.image, size: 30) + + Text(chat.chatInfo.chatViewName) + .foregroundColor(.accentColor) + .lineLimit(1) + + Spacer() + + Image(systemName: "envelope") + .resizable() + .scaledToFill() + .frame(width: 14, height: 14) + .foregroundColor(.accentColor) + } + } + + func contactRequestNavLink(_ contactRequest: UserContactRequest) -> some View { + Button { + showContactRequestDialog = true + } label: { + contactRequestPreview(contactRequest) + } + .swipeActions(edge: .trailing, allowsFullSwipe: true) { + Button { + Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) } + } label: { Label("Accept", systemImage: "checkmark") } + .tint(theme.colors.primary) + Button { + Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) } + } label: { + Label("Accept incognito", systemImage: "theatermasks") + } + .tint(.indigo) + Button { + alert = SomeAlert(alert: rejectContactRequestAlert(contactRequest), id: "rejectContactRequestAlert") + } label: { + Label("Reject", systemImage: "multiply") + } + .tint(.red) + } + .confirmationDialog("Accept connection request?", isPresented: $showContactRequestDialog, titleVisibility: .visible) { + Button("Accept") { Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) } } + Button("Accept incognito") { Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) } } + Button("Reject (sender NOT notified)", role: .destructive) { Task { await rejectContactRequest(contactRequest) } } + } + } + + func contactRequestPreview(_ contactRequest: UserContactRequest) -> some View { + HStack{ + ProfileImage(imageStr: contactRequest.image, size: 30) + + Text(chat.chatInfo.chatViewName) + .foregroundColor(.accentColor) + .lineLimit(1) + + Spacer() + + Image(systemName: "checkmark") + .resizable() + .scaledToFill() + .frame(width: 14, height: 14) + .foregroundColor(.accentColor) + } + } +} diff --git a/apps/ios/Shared/Views/Database/ChatArchiveView.swift b/apps/ios/Shared/Views/Database/ChatArchiveView.swift index 65913343d5..3ab4ac9a31 100644 --- a/apps/ios/Shared/Views/Database/ChatArchiveView.swift +++ b/apps/ios/Shared/Views/Database/ChatArchiveView.swift @@ -10,6 +10,7 @@ import SwiftUI import SimpleXChat struct ChatArchiveView: View { + @EnvironmentObject var theme: AppTheme var archiveName: String @AppStorage(DEFAULT_CHAT_ARCHIVE_NAME) private var chatArchiveName: String? @AppStorage(DEFAULT_CHAT_ARCHIVE_TIME) private var chatArchiveTime: Double = 0 @@ -20,14 +21,14 @@ struct ChatArchiveView: View { let fileTs = chatArchiveTimeDefault.get() List { Section { - settingsRow("square.and.arrow.up") { + settingsRow("square.and.arrow.up", color: theme.colors.secondary) { Button { showShareSheet(items: [fileUrl]) } label: { Text("Save archive") } } - settingsRow("trash") { + settingsRow("trash", color: theme.colors.secondary) { Button { showDeleteAlert = true } label: { @@ -36,8 +37,10 @@ struct ChatArchiveView: View { } } header: { Text("Chat archive") + .foregroundColor(theme.colors.secondary) } footer: { Text("Created on \(fileTs)") + .foregroundColor(theme.colors.secondary) } } .alert(isPresented: $showDeleteAlert) { diff --git a/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift b/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift index 4031c3e00a..be167b92b9 100644 --- a/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseEncryptionView.swift @@ -35,6 +35,7 @@ enum DatabaseEncryptionAlert: Identifiable { struct DatabaseEncryptionView: View { @EnvironmentObject private var m: ChatModel + @EnvironmentObject private var theme: AppTheme @Binding var useKeychain: Bool var migration: Bool @State private var alert: DatabaseEncryptionAlert? = nil @@ -63,7 +64,7 @@ struct DatabaseEncryptionView: View { private func databaseEncryptionView() -> some View { Section { - settingsRow(storedKey ? "key.fill" : "key", color: storedKey ? .green : .secondary) { + settingsRow(storedKey ? "key.fill" : "key", color: storedKey ? .green : theme.colors.secondary) { Toggle("Save passphrase in Keychain", isOn: $useKeychainToggle) .onChange(of: useKeychainToggle) { _ in if useKeychainToggle { @@ -85,7 +86,7 @@ struct DatabaseEncryptionView: View { PassphraseField(key: $newKey, placeholder: "New passphrase…", valid: validKey(newKey), showStrength: true) PassphraseField(key: $confirmNewKey, placeholder: "Confirm new passphrase…", valid: confirmNewKey == "" || newKey == confirmNewKey) - settingsRow("lock.rotation") { + settingsRow("lock.rotation", color: theme.colors.secondary) { Button(migration ? "Set passphrase" : "Update database passphrase") { alert = currentKey == "" ? (useKeychain ? .encryptDatabaseSaved : .encryptDatabase) @@ -102,6 +103,7 @@ struct DatabaseEncryptionView: View { ) } header: { Text(migration ? "Database passphrase" : "") + .foregroundColor(theme.colors.secondary) } footer: { VStack(alignment: .leading, spacing: 16) { if m.chatDbEncrypted == false { @@ -125,6 +127,7 @@ struct DatabaseEncryptionView: View { } } } + .foregroundColor(theme.colors.secondary) .padding(.top, 1) .font(.callout) } @@ -277,6 +280,7 @@ struct DatabaseEncryptionView: View { struct PassphraseField: View { + @EnvironmentObject var theme: AppTheme @Binding var key: String var placeholder: LocalizedStringKey var valid: Bool @@ -287,7 +291,7 @@ struct PassphraseField: View { var body: some View { ZStack(alignment: .leading) { let iconColor = valid - ? (showStrength && key != "" ? PassphraseStrength(passphrase: key).color : .secondary) + ? (showStrength && key != "" ? PassphraseStrength(passphrase: key).color : theme.colors.secondary) : .red Image(systemName: valid ? (showKey ? "eye.slash" : "eye") : "exclamationmark.circle") .resizable() diff --git a/apps/ios/Shared/Views/Database/DatabaseErrorView.swift b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift index f8d282a6d1..9d71e2a788 100644 --- a/apps/ios/Shared/Views/Database/DatabaseErrorView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift @@ -64,7 +64,7 @@ struct DatabaseErrorView: View { case let .migrationError(mtrError): titleText("Incompatible database version") fileNameText(dbFile) - Text("Error: ") + Text(DatabaseErrorView.mtrErrorDescription(mtrError)) + Text("Error: ") + Text(mtrErrorDescription(mtrError)) } case let .errorSQL(dbFile, migrationSQLError): titleText("Database error") @@ -105,15 +105,6 @@ struct DatabaseErrorView: View { Text("Migrations: \(ms.joined(separator: ", "))") } - static func mtrErrorDescription(_ err: MTRError) -> LocalizedStringKey { - switch err { - case let .noDown(dbMigrations): - return "database version is newer than the app, but no down migration for: \(dbMigrations.joined(separator: ", "))" - case let .different(appMigration, dbMigration): - return "different migration in the app/database: \(appMigration) / \(dbMigration)" - } - } - private func databaseKeyField(onSubmit: @escaping () -> Void) -> some View { PassphraseField(key: $dbKey, placeholder: "Enter passphrase…", valid: validKey(dbKey), onSubmit: onSubmit) } diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index 2e0cd7738f..f5b5287971 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -15,6 +15,7 @@ enum DatabaseAlert: Identifiable { case importArchive case archiveImported case archiveImportedWithErrors(archiveErrors: [ArchiveError]) + case archiveExportedWithErrors(archivePath: URL, archiveErrors: [ArchiveError]) case deleteChat case chatDeleted case deleteLegacyDatabase @@ -29,6 +30,7 @@ enum DatabaseAlert: Identifiable { case .importArchive: return "importArchive" case .archiveImported: return "archiveImported" case .archiveImportedWithErrors: return "archiveImportedWithErrors" + case .archiveExportedWithErrors: return "archiveExportedWithErrors" case .deleteChat: return "deleteChat" case .chatDeleted: return "chatDeleted" case .deleteLegacyDatabase: return "deleteLegacyDatabase" @@ -41,6 +43,7 @@ enum DatabaseAlert: Identifiable { struct DatabaseView: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @Binding var showSettings: Bool @State private var runChat = false @State private var alert: DatabaseAlert? = nil @@ -82,8 +85,10 @@ struct DatabaseView: View { .disabled(stopped || progressIndicator) } header: { Text("Messages") + .foregroundColor(theme.colors.secondary) } footer: { Text("This setting applies to messages in your current chat profile **\(m.currentUser?.displayName ?? "")**.") + .foregroundColor(theme.colors.secondary) } Section { @@ -105,24 +110,27 @@ struct DatabaseView: View { } } header: { Text("Run chat") + .foregroundColor(theme.colors.secondary) } footer: { if case .documents = dbContainer { Text("Database will be migrated when the app restarts") + .foregroundColor(theme.colors.secondary) } } Section { let unencrypted = m.chatDbEncrypted == false - let color: Color = unencrypted ? .orange : .secondary + let color: Color = unencrypted ? .orange : theme.colors.secondary settingsRow(unencrypted ? "lock.open" : useKeychain ? "key" : "lock", color: color) { NavigationLink { DatabaseEncryptionView(useKeychain: $useKeychain, migration: false) .navigationTitle("Database passphrase") + .modifier(ThemedBackground(grouped: true)) } label: { Text("Database passphrase") } } - settingsRow("square.and.arrow.up") { + settingsRow("square.and.arrow.up", color: theme.colors.secondary) { Button("Export database") { if initialRandomDBPassphraseGroupDefault.get() && !unencrypted { alert = .exportProhibited @@ -131,7 +139,7 @@ struct DatabaseView: View { } } } - settingsRow("square.and.arrow.down") { + settingsRow("square.and.arrow.down", color: theme.colors.secondary) { Button("Import database", role: .destructive) { showFileImporter = true } @@ -140,34 +148,37 @@ struct DatabaseView: View { let title: LocalizedStringKey = chatArchiveTimeDefault.get() < chatLastStartGroupDefault.get() ? "Old database archive" : "New database archive" - settingsRow("archivebox") { + settingsRow("archivebox", color: theme.colors.secondary) { NavigationLink { ChatArchiveView(archiveName: archiveName) .navigationTitle(title) + .modifier(ThemedBackground(grouped: true)) } label: { Text(title) } } } - settingsRow("trash.slash") { + settingsRow("trash.slash", color: theme.colors.secondary) { Button("Delete database", role: .destructive) { alert = .deleteChat } } } header: { Text("Chat database") + .foregroundColor(theme.colors.secondary) } footer: { Text( stopped ? "You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." : "Stop chat to enable database actions" ) + .foregroundColor(theme.colors.secondary) } .disabled(!stopped) if case .group = dbContainer, legacyDatabase { - Section("Old database") { - settingsRow("trash") { + Section(header: Text("Old database").foregroundColor(theme.colors.secondary)) { + settingsRow("trash", color: theme.colors.secondary) { Button("Delete old database") { alert = .deleteLegacyDatabase } @@ -182,12 +193,15 @@ struct DatabaseView: View { .disabled(!stopped || appFilesCountAndSize?.0 == 0) } header: { Text("Files & media") + .foregroundColor(theme.colors.secondary) } footer: { if let (fileCount, size) = appFilesCountAndSize { if fileCount == 0 { Text("No received or sent files") + .foregroundColor(theme.colors.secondary) } else { Text("\(fileCount) file(s) with total size of \(ByteCountFormatter.string(fromByteCount: Int64(size), countStyle: .binary))") + .foregroundColor(theme.colors.secondary) } } } @@ -253,10 +267,18 @@ struct DatabaseView: View { title: Text("Chat database imported"), message: Text("Restart the app to use imported chat database") ) - case .archiveImportedWithErrors: + case let .archiveImportedWithErrors(errs): return Alert( title: Text("Chat database imported"), - message: Text("Restart the app to use imported chat database") + Text("\n") + Text("Some non-fatal errors occurred during import - you may see Chat console for more details.") + message: Text("Restart the app to use imported chat database") + Text(verbatim: "\n\n") + Text("Some non-fatal errors occurred during import:") + archiveErrorsText(errs) + ) + case let .archiveExportedWithErrors(archivePath, errs): + return Alert( + title: Text("Chat database exported"), + message: Text("You may save the exported archive.") + Text(verbatim: "\n\n") + Text("Some file(s) were not exported:") + archiveErrorsText(errs), + dismissButton: .default(Text("Continue")) { + showShareSheet(items: [archivePath]) + } ) case .deleteChat: return Alert( @@ -337,9 +359,16 @@ struct DatabaseView: View { progressIndicator = true Task { do { - let archivePath = try await exportChatArchive() - showShareSheet(items: [archivePath]) - await MainActor.run { progressIndicator = false } + let (archivePath, archiveErrors) = try await exportChatArchive() + if archiveErrors.isEmpty { + showShareSheet(items: [archivePath]) + await MainActor.run { progressIndicator = false } + } else { + await MainActor.run { + alert = .archiveExportedWithErrors(archivePath: archivePath, archiveErrors: archiveErrors) + progressIndicator = false + } + } } catch let error { await MainActor.run { alert = .error(title: "Error exporting chat database", error: responseError(error)) @@ -355,6 +384,7 @@ struct DatabaseView: View { Task { do { try await apiDeleteStorage() + try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true) do { let config = ArchiveConfig(archivePath: archivePath.path) let archiveErrors = try await apiImportArchive(config: config) @@ -461,7 +491,7 @@ struct DatabaseView: View { appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory()) do { let chats = try apiGetChats() - m.updateChats(with: chats) + m.updateChats(chats) } catch let error { logger.error("apiGetChats: cannot update chats \(responseError(error))") } @@ -473,6 +503,17 @@ struct DatabaseView: View { } } +func archiveErrorsText(_ errs: [ArchiveError]) -> Text { + return Text("\n" + errs.map(showArchiveError).joined(separator: "\n")) + + func showArchiveError(_ err: ArchiveError) -> String { + switch err { + case let .import(importError): importError + case let .fileError(file, fileError): "\(file): \(fileError)" + } + } +} + func stopChatAsync() async throws { try await apiStopChat() ChatReceiver.shared.stop() diff --git a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift index ae6af24f53..e79f24c6d9 100644 --- a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift +++ b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift @@ -189,7 +189,8 @@ struct MigrateToAppGroupView: View { Task { do { try apiSaveAppSettings(settings: AppSettings.current.prepareForExport()) - try await apiExportArchive(config: config) + try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true) + _ = try await apiExportArchive(config: config) await MainActor.run { setV3DBMigration(.exported) } } catch let error { await MainActor.run { @@ -221,7 +222,7 @@ struct MigrateToAppGroupView: View { } } -func exportChatArchive(_ storagePath: URL? = nil) async throws -> URL { +func exportChatArchive(_ storagePath: URL? = nil) async throws -> (URL, [ArchiveError]) { let archiveTime = Date.now let ts = archiveTime.ISO8601Format(Date.ISO8601FormatStyle(timeSeparator: .omitted)) let archiveName = "simplex-chat.\(ts).zip" @@ -231,13 +232,14 @@ func exportChatArchive(_ storagePath: URL? = nil) async throws -> URL { if !ChatModel.shared.chatDbChanged { try apiSaveAppSettings(settings: AppSettings.current.prepareForExport()) } - try await apiExportArchive(config: config) + try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true) + let errs = try await apiExportArchive(config: config) if storagePath == nil { deleteOldArchive() UserDefaults.standard.set(archiveName, forKey: DEFAULT_CHAT_ARCHIVE_NAME) chatArchiveTimeDefault.set(archiveTime) } - return archivePath + return (archivePath, errs) } func deleteOldArchive() { diff --git a/apps/ios/Shared/Views/Helpers/AppSheet.swift b/apps/ios/Shared/Views/Helpers/AppSheet.swift index 0e64776ed6..0ade1c0d8e 100644 --- a/apps/ios/Shared/Views/Helpers/AppSheet.swift +++ b/apps/ios/Shared/Views/Helpers/AppSheet.swift @@ -8,41 +8,21 @@ import SwiftUI -private struct SheetIsPresented: ViewModifier where C: View { - var isPresented: Binding - var onDismiss: (() -> Void)? - var sheetContent: () -> C - @Environment(\.scenePhase) var scenePhase - - func body(content: Content) -> some View { - content.sheet(isPresented: isPresented, onDismiss: onDismiss) { - sheetContent().modifier(PrivacySensitive()) - } - } -} - -private struct SheetForItem: ViewModifier where T: Identifiable, C: View { - var item: Binding - var onDismiss: (() -> Void)? - var sheetContent: (T) -> C - @Environment(\.scenePhase) var scenePhase - - func body(content: Content) -> some View { - content.sheet(item: item, onDismiss: onDismiss) { it in - sheetContent(it).modifier(PrivacySensitive()) - } - } +class AppSheetState: ObservableObject { + static let shared = AppSheetState() + @Published var scenePhaseActive: Bool = false } private struct PrivacySensitive: ViewModifier { @AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false - @Environment(\.scenePhase) var scenePhase + // Screen protection doesn't work for appSheet on iOS 16 if @Environment(\.scenePhase) is used instead of global state + @ObservedObject var appSheetState: AppSheetState = AppSheetState.shared func body(content: Content) -> some View { - if case .active = scenePhase { + if !protectScreen { content } else { - content.privacySensitive(protectScreen).redacted(reason: .privacy) + content.privacySensitive(!appSheetState.scenePhaseActive).redacted(reason: .privacy) } } } @@ -53,7 +33,9 @@ extension View { onDismiss: (() -> Void)? = nil, content: @escaping () -> Content ) -> some View where Content: View { - modifier(SheetIsPresented(isPresented: isPresented, onDismiss: onDismiss, sheetContent: content)) + sheet(isPresented: isPresented, onDismiss: onDismiss) { + content().modifier(PrivacySensitive()) + } } func appSheet( @@ -61,6 +43,8 @@ extension View { onDismiss: (() -> Void)? = nil, content: @escaping (T) -> Content ) -> some View where T: Identifiable, Content: View { - modifier(SheetForItem(item: item, onDismiss: onDismiss, sheetContent: content)) + sheet(item: item, onDismiss: onDismiss) { it in + content(it).modifier(PrivacySensitive()) + } } } diff --git a/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift b/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift index 0180b066ab..40d62e009b 100644 --- a/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift +++ b/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift @@ -10,25 +10,16 @@ import SwiftUI import SimpleXChat struct ChatInfoImage: View { - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat var size: CGFloat var color = Color(uiColor: .tertiarySystemGroupedBackground) var body: some View { - var iconName: String - switch chat.chatInfo { - case .direct: iconName = "person.crop.circle.fill" - case .group: iconName = "person.2.circle.fill" - case .local: iconName = "folder.circle.fill" - case .contactRequest: iconName = "person.crop.circle.fill" - default: iconName = "circle.fill" - } - let notesColor = colorScheme == .light ? notesChatColorLight : notesChatColorDark - let iconColor = if case .local = chat.chatInfo { notesColor } else { color } + let iconColor = if case .local = chat.chatInfo { theme.appColors.primaryVariant2 } else { color } return ProfileImage( imageStr: chat.chatInfo.image, - iconName: iconName, + iconName: chatIconName(chat.chatInfo), size: size, color: iconColor ) diff --git a/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift b/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift new file mode 100644 index 0000000000..477dc567eb --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/ChatItemClipShape.swift @@ -0,0 +1,68 @@ +// +// ChatItemClipShape.swift +// SimpleX (iOS) +// +// Created by Levitating Pineapple on 04/07/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +/// Modifier, which provides clipping mask for ``ChatItemWithMenu`` view +/// and it's previews: (drag interaction, context menu, etc.) +/// Supports [Dynamic Type](https://developer.apple.com/documentation/uikit/uifont/scaling_fonts_automatically) +/// by retaining pill shape, even when ``ChatItem``'s height is less that twice its corner radius +struct ChatItemClipped: ViewModifier { + struct ClipShape: Shape { + let maxCornerRadius: Double + + func path(in rect: CGRect) -> Path { + Path( + roundedRect: rect, + cornerRadius: min((rect.height / 2), maxCornerRadius), + style: .circular + ) + } + } + + init() { + clipShape = ClipShape( + maxCornerRadius: 18 + ) + } + + init(_ chatItem: ChatItem) { + clipShape = ClipShape( + maxCornerRadius: { + switch chatItem.content { + case + .sndMsgContent, + .rcvMsgContent, + .rcvDecryptionError, + .rcvGroupInvitation, + .sndGroupInvitation, + .sndDeleted, + .rcvDeleted, + .rcvIntegrityError, + .sndModerated, + .rcvModerated, + .rcvBlocked, + .invalidJSON: 18 + default: 8 + } + }() + ) + } + + private let clipShape: ClipShape + + func body(content: Content) -> some View { + content + .contentShape(.dragPreview, clipShape) + .contentShape(.contextMenuPreview, clipShape) + .clipShape(clipShape) + } +} + + diff --git a/apps/ios/Shared/Views/Helpers/ChatWallpaper.swift b/apps/ios/Shared/Views/Helpers/ChatWallpaper.swift new file mode 100644 index 0000000000..cc5be9e7bb --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/ChatWallpaper.swift @@ -0,0 +1,113 @@ +// +// ChatWallpaper.swift +// SimpleX (iOS) +// +// Created by Avently on 14.06.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import Foundation +import SwiftUI +import SimpleXChat + +struct ChatViewBackground: ViewModifier { + @Environment(\.colorScheme) var colorScheme + var image: Image + var imageType: WallpaperType + var background: Color + var tint: Color + + func body(content: Content) -> some View { + // Workaround a problem (SwiftUI bug?) when wallpaper is not updated when user changes global theme in iOS settings from dark to light and vice versa + if colorScheme == .light { + back(content) + } else { + back(content) + } + } + + func back(_ content: Content) -> some View { + content.background( + Canvas { context, size in + var image = context.resolve(image) + let rect = CGRectMake(0, 0, size.width, size.height) + func repeatDraw(_ imageScale: CGFloat) { + // Prevent range bounds crash and dividing by zero + if size.height == 0 || size.width == 0 || image.size.height == 0 || image.size.width == 0 { return } + image.shading = .color(tint) + let scale = imageScale * 2.5 // scale wallpaper for iOS + for h in 0 ... Int(size.height / image.size.height / scale) { + for w in 0 ... Int(size.width / image.size.width / scale) { + let rect = CGRectMake(CGFloat(w) * image.size.width * scale, CGFloat(h) * image.size.height * scale, image.size.width * scale, image.size.height * scale) + context.draw(image, in: rect, style: FillStyle()) + } + } + } + context.fill(Path(rect), with: .color(background)) + switch imageType { + case let WallpaperType.preset(filename, scale): + repeatDraw(CGFloat((scale ?? 1) * (PresetWallpaper.from(filename)?.scale ?? 1))) + case let WallpaperType.image(_, scale, scaleType): + let scaleType = scaleType ?? WallpaperScaleType.fill + switch scaleType { + case WallpaperScaleType.repeat: repeatDraw(CGFloat(scale ?? 1)) + case WallpaperScaleType.fill: fallthrough + case WallpaperScaleType.fit: + let scale = scaleType.computeScaleFactor(image.size, size) + let scaledWidth = (image.size.width * scale.0) + let scaledHeight = (image.size.height * scale.1) + context.draw(image, in: CGRectMake(((size.width - scaledWidth) / 2), ((size.height - scaledHeight) / 2), scaledWidth, scaledHeight), style: FillStyle()) + if case WallpaperScaleType.fit = scaleType { + if scaledWidth < size.width { + // has black lines at left and right sides + var x = (size.width - scaledWidth) / 2 + while x > 0 { + context.draw(image, in: CGRectMake((x - scaledWidth), ((size.height - scaledHeight) / 2), scaledWidth, scaledHeight), style: FillStyle()) + x -= scaledWidth + } + x = size.width - (size.width - scaledWidth) / 2 + while x < size.width { + context.draw(image, in: CGRectMake(x, ((size.height - scaledHeight) / 2), scaledWidth, scaledHeight), style: FillStyle()) + + x += scaledWidth + } + } else { + // has black lines at top and bottom sides + var y = (size.height - scaledHeight) / 2 + while y > 0 { + context.draw(image, in: CGRectMake(((size.width - scaledWidth) / 2), (y - scaledHeight), scaledWidth, scaledHeight), style: FillStyle()) + y -= scaledHeight + } + y = size.height - (size.height - scaledHeight) / 2 + while y < size.height { + context.draw(image, in: CGRectMake(((size.width - scaledWidth) / 2), y, scaledWidth, scaledHeight), style: FillStyle()) + y += scaledHeight + } + } + } + context.fill(Path(rect), with: .color(tint)) + } + case WallpaperType.empty: () + } + } + ).ignoresSafeArea(.all) + } +} + +extension PresetWallpaper { + public func toType(_ base: DefaultTheme, _ scale: Float? = nil) -> WallpaperType { + let scale = if let scale { + scale + } else if let type = ChatModel.shared.currentUser?.uiThemes?.preferredMode(base.mode == DefaultThemeMode.dark)?.wallpaper?.toAppWallpaper().type, type.sameType(WallpaperType.preset(filename, nil)) { + type.scale + } else if let scale = themeOverridesDefault.get().first(where: { $0.wallpaper != nil && $0.wallpaper!.preset == filename && $0.base == base })?.wallpaper?.scale { + scale + } else { + Float(1.0) + } + return WallpaperType.preset( + filename, + scale + ) + } +} diff --git a/apps/ios/Shared/Views/Helpers/ContextMenu.swift b/apps/ios/Shared/Views/Helpers/ContextMenu.swift deleted file mode 100644 index 9504d919ef..0000000000 --- a/apps/ios/Shared/Views/Helpers/ContextMenu.swift +++ /dev/null @@ -1,112 +0,0 @@ -// -// ContextMenu2.swift -// SimpleX (iOS) -// -// Created by Evgeny on 09/08/2022. -// Copyright © 2022 SimpleX Chat. All rights reserved. -// - -import Foundation -import UIKit -import SwiftUI - -extension View { - func uiKitContextMenu(hasImageOrVideo: Bool, maxWidth: CGFloat, itemWidth: Binding, menu: Binding, allowMenu: Binding) -> some View { - Group { - if allowMenu.wrappedValue { - if hasImageOrVideo { - InteractionView(content: - self.environmentObject(ChatModel.shared) - .overlay(DetermineWidthImageVideoItem()) - .onPreferenceChange(DetermineWidthImageVideoItem.Key.self) { itemWidth.wrappedValue = $0 == 0 ? maxWidth : $0 } - , maxWidth: maxWidth, itemWidth: itemWidth, menu: menu) - .frame(maxWidth: itemWidth.wrappedValue) - } else { - InteractionView(content: self.environmentObject(ChatModel.shared), maxWidth: maxWidth, itemWidth: itemWidth, menu: menu) - .fixedSize(horizontal: true, vertical: false) - } - } else { - self - } - } - } -} - -private class HostingViewHolder: UIView { - var contentSize: CGSize = CGSizeMake(0, 0) - override var intrinsicContentSize: CGSize { get { contentSize } } -} - -struct InteractionView: UIViewRepresentable { - let content: Content - var maxWidth: CGFloat - var itemWidth: Binding - @Binding var menu: UIMenu - - func makeUIView(context: Context) -> UIView { - let view = HostingViewHolder() - view.backgroundColor = .clear - let hostView = UIHostingController(rootView: content) - view.contentSize = hostView.view.intrinsicContentSize - hostView.view.translatesAutoresizingMaskIntoConstraints = false - let constraints = [ - hostView.view.topAnchor.constraint(equalTo: view.topAnchor), - hostView.view.leadingAnchor.constraint(equalTo: view.leadingAnchor), - hostView.view.trailingAnchor.constraint(equalTo: view.trailingAnchor), - hostView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), - hostView.view.widthAnchor.constraint(equalTo: view.widthAnchor), - hostView.view.heightAnchor.constraint(equalTo: view.heightAnchor) - ] - view.addSubview(hostView.view) - view.addConstraints(constraints) - view.layer.cornerRadius = 18 - hostView.view.layer.cornerRadius = 18 - let menuInteraction = UIContextMenuInteraction(delegate: context.coordinator) - view.addInteraction(menuInteraction) - return view - } - - func updateUIView(_ uiView: UIView, context: Context) { - let was = (uiView as! HostingViewHolder).contentSize - (uiView as! HostingViewHolder).contentSize = uiView.subviews[0].sizeThatFits(CGSizeMake(itemWidth.wrappedValue, .infinity)) - if was != (uiView as! HostingViewHolder).contentSize { - uiView.invalidateIntrinsicContentSize() - } - } - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - class Coordinator: NSObject, UIContextMenuInteractionDelegate { - let parent: InteractionView - - init(_ parent: InteractionView) { - self.parent = parent - } - - func contextMenuInteraction( - _ interaction: UIContextMenuInteraction, - configurationForMenuAtLocation location: CGPoint - ) -> UIContextMenuConfiguration? { - UIContextMenuConfiguration( - identifier: nil, - previewProvider: nil, - actionProvider: { [weak self] _ in - guard let self = self else { return nil } - return self.parent.menu - } - ) - } - - // func contextMenuInteraction( - // _ interaction: UIContextMenuInteraction, - // willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, - // animator: UIContextMenuInteractionCommitAnimating - // ) { - // animator.addCompletion { - // print("user tapped") - // } - // } - } -} diff --git a/apps/ios/Shared/Views/Helpers/ImagePicker.swift b/apps/ios/Shared/Views/Helpers/ImagePicker.swift index fe8d5bbdd4..d7525027e0 100644 --- a/apps/ios/Shared/Views/Helpers/ImagePicker.swift +++ b/apps/ios/Shared/Views/Helpers/ImagePicker.swift @@ -33,6 +33,7 @@ struct LibraryMediaListPicker: UIViewControllerRepresentable { typealias UIViewControllerType = PHPickerViewController var addMedia: (_ content: UploadContent) async -> Void var selectionLimit: Int + var filter: PHPickerFilter = .any(of: [.images, .videos]) var finishedPreprocessing: () -> Void = {} var didFinishPicking: (_ didSelectItems: Bool) async -> Void @@ -148,7 +149,7 @@ struct LibraryMediaListPicker: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> PHPickerViewController { var config = PHPickerConfiguration() - config.filter = .any(of: [.images, .videos]) + config.filter = filter config.selectionLimit = selectionLimit config.selection = .ordered config.preferredAssetRepresentationMode = .current diff --git a/apps/ios/Shared/Views/Helpers/KeyboardPadding.swift b/apps/ios/Shared/Views/Helpers/KeyboardPadding.swift deleted file mode 100644 index 45d766ddfd..0000000000 --- a/apps/ios/Shared/Views/Helpers/KeyboardPadding.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// KeyboardPadding.swift -// SimpleX (iOS) -// -// Created by Evgeny on 10/07/2023. -// Copyright © 2023 SimpleX Chat. All rights reserved. -// - -import SwiftUI - -extension View { - @ViewBuilder func keyboardPadding() -> some View { - if #available(iOS 17.0, *) { - GeometryReader { g in - self.padding(.bottom, max(0, ChatModel.shared.keyboardHeight - g.safeAreaInsets.bottom)) - } - } else { - self - } - } -} diff --git a/apps/ios/Shared/Views/Helpers/NavLinkPlain.swift b/apps/ios/Shared/Views/Helpers/NavLinkPlain.swift index 2d5458b9d3..fdc3f2129f 100644 --- a/apps/ios/Shared/Views/Helpers/NavLinkPlain.swift +++ b/apps/ios/Shared/Views/Helpers/NavLinkPlain.swift @@ -7,16 +7,17 @@ // import SwiftUI +import SimpleXChat -struct NavLinkPlain: View { - @State var tag: V - @Binding var selection: V? +struct NavLinkPlain: View { + let chatId: ChatId + @Binding var selection: ChatId? @ViewBuilder var label: () -> Label var disabled = false var body: some View { ZStack { - Button("") { DispatchQueue.main.async { selection = tag } } + Button("") { ItemsModel.shared.loadOpenChat(chatId) } .disabled(disabled) label() } diff --git a/apps/ios/Shared/Views/Helpers/NavStackCompat.swift b/apps/ios/Shared/Views/Helpers/NavStackCompat.swift index 6e3b89c9b8..e9383fc073 100644 --- a/apps/ios/Shared/Views/Helpers/NavStackCompat.swift +++ b/apps/ios/Shared/Views/Helpers/NavStackCompat.swift @@ -17,7 +17,9 @@ struct NavStackCompat : View { if #available(iOS 16, *) { NavigationStack(path: Binding( get: { isActive.wrappedValue ? [true] : [] }, - set: { _ in } + set: { path in + if path.isEmpty { isActive.wrappedValue = false } + } )) { ZStack { NavigationLink(value: true) { EmptyView() } diff --git a/apps/ios/Shared/Views/Helpers/ProfileImage.swift b/apps/ios/Shared/Views/Helpers/ProfileImage.swift index 6b8439504a..248504c59b 100644 --- a/apps/ios/Shared/Views/Helpers/ProfileImage.swift +++ b/apps/ios/Shared/Views/Helpers/ProfileImage.swift @@ -9,45 +9,49 @@ import SwiftUI import SimpleXChat -let defaultProfileImageCorner = 22.5 - struct ProfileImage: View { + @EnvironmentObject var theme: AppTheme var imageStr: String? = nil var iconName: String = "person.crop.circle.fill" var size: CGFloat var color = Color(uiColor: .tertiarySystemGroupedBackground) + var backgroundColor: Color? = nil + var blurred = false @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner var body: some View { - if let image = imageStr, - let data = Data(base64Encoded: dropImagePrefix(image)), - let uiImage = UIImage(data: data) { - clipProfileImage(Image(uiImage: uiImage), size: size, radius: radius) + if let uiImage = UIImage(base64Encoded: imageStr) { + clipProfileImage(Image(uiImage: uiImage), size: size, radius: radius, blurred: blurred) } else { + let c = color.asAnotherColorFromSecondaryVariant(theme) Image(systemName: iconName) .resizable() - .foregroundColor(color) + .foregroundColor(c) .frame(width: size, height: size) + .background(Circle().fill(backgroundColor != nil ? backgroundColor! : .clear)) } } } -private let squareToCircleRatio = 0.935 +extension Color { + func asAnotherColorFromSecondary(_ theme: AppTheme) -> Color { + return self + } -private let radiusFactor = (1 - squareToCircleRatio) / 50 - -@ViewBuilder func clipProfileImage(_ img: Image, size: CGFloat, radius: Double) -> some View { - let v = img.resizable() - if radius >= 50 { - v.frame(width: size, height: size).clipShape(Circle()) - } else if radius <= 0 { - let sz = size * squareToCircleRatio - v.frame(width: sz, height: sz).padding((size - sz) / 2) - } else { - let sz = size * (squareToCircleRatio + radius * radiusFactor) - v.frame(width: sz, height: sz) - .clipShape(RoundedRectangle(cornerRadius: sz * radius / 100, style: .continuous)) - .padding((size - sz) / 2) + func asAnotherColorFromSecondaryVariant(_ theme: AppTheme) -> Color { + let s = theme.colors.secondaryVariant + let l = theme.colors.isLight + return switch self { + case Color(uiColor: .tertiaryLabel): // ChatView title + l ? s.darker(0.05) : s.lighter(0.2) + case Color(uiColor: .tertiarySystemFill): // SettingsView, ChatInfoView + l ? s.darker(0.065) : s.lighter(0.085) + case Color(uiColor: .quaternaryLabel): // ChatListView user picker + l ? s.darker(0.1) : s.lighter(0.1) + case Color(uiColor: .tertiarySystemGroupedBackground): // ChatListView items, forward view + s.asGroupedBackground(theme.base.mode) + default: self + } } } diff --git a/apps/ios/Shared/Views/Helpers/SwipeLabel.swift b/apps/ios/Shared/Views/Helpers/SwipeLabel.swift new file mode 100644 index 0000000000..564589be6f --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/SwipeLabel.swift @@ -0,0 +1,80 @@ +// +// SwipeLabel.swift +// SimpleX (iOS) +// +// Created by Levitating Pineapple on 06/08/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +struct SwipeLabel: View { + private let text: String + private let systemImage: String + private let inverted: Bool + + init(_ text: String, systemImage: String, inverted: Bool) { + self.text = text + self.systemImage = systemImage + self.inverted = inverted + } + + var body: some View { + if inverted { + Image( + uiImage: SwipeActionView( + systemName: systemImage, + text: text + ).snapshot(inverted: inverted) + ) + } else { + Label(text, systemImage: systemImage) + } + } + + private class SwipeActionView: UIView { + private let imageView = UIImageView() + private let label = UILabel() + private let fontSize: CGFloat + + init(systemName: String, text: String) { + fontSize = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .subheadline).pointSize + super.init(frame: CGRect(x: 0, y: 0, width: 64, height: 32 + fontSize)) + imageView.image = UIImage(systemName: systemName) + imageView.contentMode = .scaleAspectFit + label.text = text + label.textAlignment = .center + label.font = UIFont.systemFont(ofSize: fontSize, weight: .medium) + addSubview(imageView) + addSubview(label) + } + + override func layoutSubviews() { + imageView.frame = CGRect( + x: 20, + y: 0, + width: 24, + height: 24 + ) + label.frame = CGRect( + x: 0, + y: 32, + width: 64, + height: fontSize + ) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { fatalError("not implemented") } + + func snapshot(inverted: Bool) -> UIImage { + UIGraphicsImageRenderer(bounds: bounds).image { context in + if inverted { + context.cgContext.scaleBy(x: 1, y: -1) + context.cgContext.translateBy(x: 0, y: -bounds.height) + } + layer.render(in: context.cgContext) + }.withRenderingMode(.alwaysTemplate) + } + } +} diff --git a/apps/ios/Shared/Views/Helpers/ThemeModeEditor.swift b/apps/ios/Shared/Views/Helpers/ThemeModeEditor.swift new file mode 100644 index 0000000000..ae94b4685c --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/ThemeModeEditor.swift @@ -0,0 +1,445 @@ +// +// ThemeModeEditor.swift +// SimpleX (iOS) +// +// Created by Avently on 20.06.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import Foundation +import SwiftUI +import SimpleXChat + +struct UserWallpaperEditor: View { + @EnvironmentObject var theme: AppTheme + var initialTheme: ThemeModeOverride + @State var themeModeOverride: ThemeModeOverride + @State var applyToMode: DefaultThemeMode? + @State var showMore: Bool = false + @Binding var globalThemeUsed: Bool + var save: (DefaultThemeMode?, ThemeModeOverride?) async -> Void + + @State private var showImageImporter: Bool = false + + var body: some View { + List { + let wallpaperType = theme.wallpaper.type + + WallpaperPresetSelector( + selectedWallpaper: wallpaperType, + currentColors: { type in + // If applying for : + // - all themes: no overrides needed + // - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected + let perUserOverride: ThemeModeOverrides? = wallpaperType.sameType(type) ? ChatModel.shared.currentUser?.uiThemes : nil + return ThemeManager.currentColors(type, nil, perUserOverride, themeOverridesDefault.get()) + }, + onChooseType: onChooseType + ) + .padding(.bottom, 10) + .listRowInsets(.init()) + .listRowBackground(Color.clear) + .modifier(WallpaperImporter(showImageImporter: $showImageImporter, onChooseImage: { image in + if let filename = saveWallpaperFile(image: image) { + _ = onTypeCopyFromSameTheme(WallpaperType.image(filename, 1, WallpaperScaleType.fill)) + } + })) + + WallpaperSetupView( + wallpaperType: themeModeOverride.type, + base: theme.base, + initialWallpaper: theme.wallpaper, + editColor: { name in editColor(name, theme) }, + onTypeChange: onTypeChange + ) + + Section { + if !globalThemeUsed { + ResetToGlobalThemeButton(true, theme.colors.primary) { + themeModeOverride = ThemeManager.defaultActiveTheme(ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + globalThemeUsed = true + Task { + await save(applyToMode, nil) + await MainActor.run { + // Change accent color globally + ThemeManager.applyTheme(currentThemeDefault.get()) + } + } + } + } + + SetDefaultThemeButton(theme.colors.primary) { + globalThemeUsed = false + let lightBase = DefaultTheme.LIGHT + let darkBase = if theme.base != DefaultTheme.LIGHT { theme.base } else if systemDarkThemeDefault.get() == DefaultTheme.DARK.themeName { DefaultTheme.DARK } else if systemDarkThemeDefault.get() == DefaultTheme.BLACK.themeName { DefaultTheme.BLACK } else { DefaultTheme.SIMPLEX } + let mode = themeModeOverride.mode + Task { + // Saving for both modes in one place by changing mode once per save + if applyToMode == nil { + let oppositeMode = mode == DefaultThemeMode.light ? DefaultThemeMode.dark : DefaultThemeMode.light + await save(oppositeMode, ThemeModeOverride.withFilledAppDefaults(oppositeMode, oppositeMode == DefaultThemeMode.light ? lightBase : darkBase)) + } + await MainActor.run { + themeModeOverride = ThemeModeOverride.withFilledAppDefaults(mode, mode == DefaultThemeMode.light ? lightBase : darkBase) + } + await save(themeModeOverride.mode, themeModeOverride) + await MainActor.run { + // Change accent color globally + ThemeManager.applyTheme(currentThemeDefault.get()) + } + } + }.onChange(of: initialTheme.mode) { mode in + themeModeOverride = initialTheme + if applyToMode != nil { + applyToMode = mode + } + } + .onChange(of: theme) { _ in + // Applies updated global theme if current one tracks global theme + if globalThemeUsed { + themeModeOverride = ThemeManager.defaultActiveTheme(ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + globalThemeUsed = true + } + } + } + + if showMore { + let values = [ + (nil, "All modes"), + (DefaultThemeMode.light, "Light mode"), + (DefaultThemeMode.dark, "Dark mode") + ] + Picker("Apply to", selection: $applyToMode) { + ForEach(values, id: \.0) { (_, text) in + Text(text) + } + } + .frame(height: 36) + .onChange(of: applyToMode) { mode in + if let mode, mode != theme.base.mode { + let lightBase = DefaultTheme.LIGHT + let darkBase = if theme.base != DefaultTheme.LIGHT { theme.base } else if systemDarkThemeDefault.get() == DefaultTheme.DARK.themeName { DefaultTheme.DARK } else if systemDarkThemeDefault.get() == DefaultTheme.BLACK.themeName { DefaultTheme.BLACK } else { DefaultTheme.SIMPLEX } + ThemeManager.applyTheme(mode == DefaultThemeMode.light ? lightBase.themeName : darkBase.themeName) + } + } + + CustomizeThemeColorsSection(editColor: { name in editColor(name, theme) }) + + ImportExportThemeSection(perChat: nil, perUser: ChatModel.shared.currentUser?.uiThemes) { imported in + let importedFromString = imported.wallpaper?.importFromString() + let importedType = importedFromString?.toAppWallpaper().type + let currentTheme = ThemeManager.currentColors(nil, nil, nil, themeOverridesDefault.get()) + let type: WallpaperType? = if importedType?.sameType(currentTheme.wallpaper.type) == true { nil } else { importedType } + let colors = ThemeManager.currentThemeOverridesForExport(type, nil, nil).colors + let res = ThemeModeOverride(mode: imported.base.mode, colors: imported.colors, wallpaper: importedFromString).removeSameColors(imported.base, colorsToCompare: colors) + Task { + await MainActor.run { + themeModeOverride = res + } + await save(applyToMode, res) + } + } + } else { + AdvancedSettingsButton(theme.colors.primary) { showMore = true } + } + } + } + + private func onTypeCopyFromSameTheme(_ type: WallpaperType?) -> Bool { + _ = ThemeManager.copyFromSameThemeOverrides(type, nil, $themeModeOverride) + Task { + await save(applyToMode, themeModeOverride) + } + globalThemeUsed = false + return true + } + + private func preApplyGlobalIfNeeded(_ type: WallpaperType?) { + if globalThemeUsed { + _ = onTypeCopyFromSameTheme(type) + } + } + + private func onTypeChange(_ type: WallpaperType?) { + if globalThemeUsed { + preApplyGlobalIfNeeded(type) + // Saves copied static image instead of original from global theme + ThemeManager.applyWallpaper(themeModeOverride.type, $themeModeOverride) + } else { + ThemeManager.applyWallpaper(type, $themeModeOverride) + } + Task { + await save(applyToMode, themeModeOverride) + } + } + + private func currentColors(_ type: WallpaperType?) -> ThemeManager.ActiveTheme { + // If applying for : + // - all themes: no overrides needed + // - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected + let perUserOverride: ThemeModeOverrides? = theme.wallpaper.type.sameType(type) ? ChatModel.shared.currentUser?.uiThemes : nil + return ThemeManager.currentColors(type, nil, perUserOverride, themeOverridesDefault.get()) + } + + private func onChooseType(_ type: WallpaperType?) { + if let type, case WallpaperType.image = type { + if theme.wallpaper.type.isImage || currentColors(type).wallpaper.type.image == nil { + showImageImporter = true + } else { + _ = onTypeCopyFromSameTheme(currentColors(type).wallpaper.type) + } + } else if themeModeOverride.type != type || theme.wallpaper.type != type { + _ = onTypeCopyFromSameTheme(type) + } else { + onTypeChange(type) + } + } + + private func editColor(_ name: ThemeColor, _ currentTheme: AppTheme) -> Binding { + editColorBinding( + name: name, + wallpaperType: theme.wallpaper.type, + wallpaperImage: theme.wallpaper.type.image, + theme: currentTheme, + onColorChange: { color in + preApplyGlobalIfNeeded(themeModeOverride.type) + ThemeManager.applyThemeColor(name: name, color: color, pref: $themeModeOverride) + Task { await save(applyToMode, themeModeOverride) } + }) + } +} + +struct ChatWallpaperEditor: View { + @EnvironmentObject var theme: AppTheme + @State private var currentTheme: ThemeManager.ActiveTheme + var initialTheme: ThemeModeOverride + @State var themeModeOverride: ThemeModeOverride + @State var applyToMode: DefaultThemeMode? + @State var showMore: Bool = false + @Binding var globalThemeUsed: Bool + var save: (DefaultThemeMode?, ThemeModeOverride?) async -> Void + + @State private var showImageImporter: Bool = false + + init(initialTheme: ThemeModeOverride, themeModeOverride: ThemeModeOverride, applyToMode: DefaultThemeMode? = nil, globalThemeUsed: Binding, save: @escaping (DefaultThemeMode?, ThemeModeOverride?) async -> Void) { + let cur = ThemeManager.currentColors(nil, globalThemeUsed.wrappedValue ? nil : themeModeOverride, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + self.currentTheme = cur + self.initialTheme = initialTheme + self.themeModeOverride = themeModeOverride + self.applyToMode = applyToMode + self._globalThemeUsed = globalThemeUsed + self.save = save + } + + var body: some View { + List { + WallpaperPresetSelector( + selectedWallpaper: currentTheme.wallpaper.type, + activeBackgroundColor: currentTheme.wallpaper.background, + activeTintColor: currentTheme.wallpaper.tint, + currentColors: currentColors, + onChooseType: onChooseType + ) + .padding(.bottom, 10) + .listRowInsets(.init()) + .listRowBackground(Color.clear) + .modifier(WallpaperImporter(showImageImporter: $showImageImporter, onChooseImage: { image in + if let filename = saveWallpaperFile(image: image) { + _ = onTypeCopyFromSameTheme(WallpaperType.image(filename, 1, WallpaperScaleType.fill)) + } + })) + + WallpaperSetupView( + wallpaperType: themeModeOverride.type, + base: currentTheme.base, + initialWallpaper: currentTheme.wallpaper, + editColor: editColor, + onTypeChange: onTypeChange + ) + + Section { + if !globalThemeUsed { + ResetToGlobalThemeButton(ChatModel.shared.currentUser?.uiThemes?.preferredMode(isInDarkTheme()) == nil, theme.colors.primary) { + themeModeOverride = ThemeManager.defaultActiveTheme(ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + globalThemeUsed = true + Task { + await save(applyToMode, nil) + } + } + } + + SetDefaultThemeButton(theme.colors.primary) { + globalThemeUsed = false + let lightBase = DefaultTheme.LIGHT + let darkBase = if currentTheme.base != DefaultTheme.LIGHT { currentTheme.base } else if systemDarkThemeDefault.get() == DefaultTheme.DARK.themeName { DefaultTheme.DARK } else if systemDarkThemeDefault.get() == DefaultTheme.BLACK.themeName { DefaultTheme.BLACK } else { DefaultTheme.SIMPLEX } + let mode = themeModeOverride.mode + Task { + // Saving for both modes in one place by changing mode once per save + if applyToMode == nil { + let oppositeMode = mode == DefaultThemeMode.light ? DefaultThemeMode.dark : DefaultThemeMode.light + await save(oppositeMode, ThemeModeOverride.withFilledAppDefaults(oppositeMode, oppositeMode == DefaultThemeMode.light ? lightBase : darkBase)) + } + await MainActor.run { + themeModeOverride = ThemeModeOverride.withFilledAppDefaults(mode, mode == DefaultThemeMode.light ? lightBase : darkBase) + } + await save(themeModeOverride.mode, themeModeOverride) + } + } + .onChange(of: initialTheme) { initial in + if initial.mode != themeModeOverride.mode { + themeModeOverride = initial + currentTheme = ThemeManager.currentColors(nil, globalThemeUsed ? nil : themeModeOverride, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + if applyToMode != nil { + applyToMode = initial.mode + } + } + } + .onChange(of: currentTheme) { _ in + // Applies updated global theme if current one tracks global theme + if globalThemeUsed { + themeModeOverride = ThemeManager.defaultActiveTheme(ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + globalThemeUsed = true + } + } + .onChange(of: themeModeOverride) { override in + currentTheme = ThemeManager.currentColors(nil, globalThemeUsed ? nil : override, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + } + } + + if showMore { + let values = [ + (nil, "All modes"), + (DefaultThemeMode.light, "Light mode"), + (DefaultThemeMode.dark, "Dark mode") + ] + Picker("Apply to", selection: $applyToMode) { + ForEach(values, id: \.0) { (_, text) in + Text(text) + } + } + .frame(height: 36) + .onChange(of: applyToMode) { mode in + if let mode, mode != currentTheme.base.mode { + let lightBase = DefaultTheme.LIGHT + let darkBase = if currentTheme.base != DefaultTheme.LIGHT { currentTheme.base } else if systemDarkThemeDefault.get() == DefaultTheme.DARK.themeName { DefaultTheme.DARK } else if systemDarkThemeDefault.get() == DefaultTheme.BLACK.themeName { DefaultTheme.BLACK } else { DefaultTheme.SIMPLEX } + ThemeManager.applyTheme(mode == DefaultThemeMode.light ? lightBase.themeName : darkBase.themeName) + } + } + + CustomizeThemeColorsSection(editColor: editColor) + + ImportExportThemeSection(perChat: themeModeOverride, perUser: ChatModel.shared.currentUser?.uiThemes) { imported in + let importedFromString = imported.wallpaper?.importFromString() + let importedType = importedFromString?.toAppWallpaper().type + let currentTheme = ThemeManager.currentColors(nil, nil, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + let type: WallpaperType? = if importedType?.sameType(currentTheme.wallpaper.type) == true { nil } else { importedType } + let colors = ThemeManager.currentThemeOverridesForExport(type, nil, ChatModel.shared.currentUser?.uiThemes).colors + let res = ThemeModeOverride(mode: imported.base.mode, colors: imported.colors, wallpaper: importedFromString).removeSameColors(imported.base, colorsToCompare: colors) + Task { + await MainActor.run { + themeModeOverride = res + } + await save(applyToMode, res) + } + } + } else { + AdvancedSettingsButton(theme.colors.primary) { showMore = true } + } + } + } + + private func onTypeCopyFromSameTheme(_ type: WallpaperType?) -> Bool { + let success = ThemeManager.copyFromSameThemeOverrides(type, ChatModel.shared.currentUser?.uiThemes?.preferredMode(!currentTheme.colors.isLight), $themeModeOverride) + if success { + Task { + await save(applyToMode, themeModeOverride) + } + globalThemeUsed = false + } + return success + } + + private func preApplyGlobalIfNeeded(_ type: WallpaperType?) { + if globalThemeUsed { + _ = onTypeCopyFromSameTheme(type) + } + } + + private func onTypeChange(_ type: WallpaperType?) { + if globalThemeUsed { + preApplyGlobalIfNeeded(type) + // Saves copied static image instead of original from global theme + ThemeManager.applyWallpaper(themeModeOverride.type, $themeModeOverride) + } else { + ThemeManager.applyWallpaper(type, $themeModeOverride) + } + Task { + await save(applyToMode, themeModeOverride) + } + } + + private func currentColors(_ type: WallpaperType?) -> ThemeManager.ActiveTheme { + // If applying for : + // - all themes: no overrides needed + // - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected + let perChatOverride: ThemeModeOverride? = type?.sameType(themeModeOverride.type) == true ? themeModeOverride : nil + return ThemeManager.currentColors(type, perChatOverride, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + } + + private func onChooseType(_ type: WallpaperType?) { + if let type, case WallpaperType.image = type { + if (themeModeOverride.type?.isImage == true && !globalThemeUsed) || currentColors(type).wallpaper.type.image == nil { + showImageImporter = true + } else if !onTypeCopyFromSameTheme(currentColors(type).wallpaper.type) { + showImageImporter = true + } + } else if globalThemeUsed || themeModeOverride.type != type || themeModeOverride.type != type { + _ = onTypeCopyFromSameTheme(type) + } else { + onTypeChange(type) + } + } + + private func editColor(_ name: ThemeColor) -> Binding { + editColorBinding( + name: name, + wallpaperType: themeModeOverride.type, + wallpaperImage: themeModeOverride.type?.image, + theme: currentTheme.toAppTheme(), + onColorChange: { color in + preApplyGlobalIfNeeded(themeModeOverride.type) + ThemeManager.applyThemeColor(name: name, color: color, pref: $themeModeOverride) + currentTheme = ThemeManager.currentColors(nil, globalThemeUsed ? nil : themeModeOverride, ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + Task { await save(applyToMode, themeModeOverride) } + }) + } +} + +private func ResetToGlobalThemeButton(_ app: Bool, _ primaryColor: Color, _ onClick: @escaping () -> Void) -> some View { + Button { + onClick() + } label: { + Text(app ? "Reset to app theme" : "Reset to user theme") + .foregroundColor(primaryColor) + } +} + +private func SetDefaultThemeButton(_ primaryColor: Color, _ onClick: @escaping () -> Void) -> some View { + Button { + onClick() + } label: { + Text("Set default theme") + .foregroundColor(primaryColor) + } +} + +private func AdvancedSettingsButton(_ primaryColor: Color, _ onClick: @escaping () -> Void) -> some View { + Button { + onClick() + } label: { + HStack { + Image(systemName: "chevron.down") + Text("Advanced settings") + }.foregroundColor(primaryColor) + } +} diff --git a/apps/ios/Shared/Views/Helpers/VideoUtils.swift b/apps/ios/Shared/Views/Helpers/VideoUtils.swift deleted file mode 100644 index e13893de6e..0000000000 --- a/apps/ios/Shared/Views/Helpers/VideoUtils.swift +++ /dev/null @@ -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 -} diff --git a/apps/ios/Shared/Views/Helpers/ViewModifiers.swift b/apps/ios/Shared/Views/Helpers/ViewModifiers.swift new file mode 100644 index 0000000000..c790b9cff2 --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/ViewModifiers.swift @@ -0,0 +1,53 @@ +// +// ViewModifiers.swift +// SimpleX (iOS) +// +// Created by Avently on 12.06.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +extension View { + @ViewBuilder func `if`(_ condition: Bool, transform: (Self) -> Content) -> some View { + if condition { + transform(self) + } else { + self + } + } +} + +extension Notification.Name { + static let chatViewWillBeginScrolling = Notification.Name("chatWillBeginScrolling") +} + +struct PrivacyBlur: ViewModifier { + var enabled: Bool = true + @Binding var blurred: Bool + @AppStorage(DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) private var blurRadius: Int = 0 + + func body(content: Content) -> some View { + if blurRadius > 0 { + // parallel ifs are necessary here because otherwise some views flicker, + // e.g. when playing video + content + .blur(radius: blurred && enabled ? CGFloat(blurRadius) * 0.5 : 0) + .overlay { + if (blurred && enabled) { + Color.clear.contentShape(Rectangle()) + .onTapGesture { + blurred = false + } + } + } + .onReceive(NotificationCenter.default.publisher(for: .chatViewWillBeginScrolling)) { _ in + if !blurred { + blurred = true + } + } + } else { + content + } + } +} diff --git a/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift b/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift index 9691a9efd3..27bb95b599 100644 --- a/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift +++ b/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift @@ -64,8 +64,8 @@ struct LocalAuthView: View { deleteAppDatabaseAndFiles() // Clear sensitive data on screen just in case app fails to hide its views while new database is created m.chatId = nil - m.reversedChatItems = [] - m.chats = [] + ItemsModel.shared.reversedChatItems = [] + m.updateChats([]) m.users = [] _ = kcAppPassword.set(password) _ = kcSelfDestructPassword.remove() diff --git a/apps/ios/Shared/Views/LocalAuth/PasscodeEntry.swift b/apps/ios/Shared/Views/LocalAuth/PasscodeEntry.swift index 46ce66678a..609943bcb6 100644 --- a/apps/ios/Shared/Views/LocalAuth/PasscodeEntry.swift +++ b/apps/ios/Shared/Views/LocalAuth/PasscodeEntry.swift @@ -10,6 +10,7 @@ import SwiftUI struct PasscodeEntry: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme var width: CGFloat var height: CGFloat @Binding var password: String @@ -140,11 +141,11 @@ struct PasscodeEntry: View { ZStack { Circle() .frame(width: h, height: h) - .foregroundColor(Color(uiColor: .systemBackground)) + .foregroundColor(AppTheme.shared.colors.background) label() } } - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .frame(width: size, height: h) } } diff --git a/apps/ios/Shared/Views/LocalAuth/PasscodeView.swift b/apps/ios/Shared/Views/LocalAuth/PasscodeView.swift index 9e0d7f38b5..ca30fa5ce8 100644 --- a/apps/ios/Shared/Views/LocalAuth/PasscodeView.swift +++ b/apps/ios/Shared/Views/LocalAuth/PasscodeView.swift @@ -29,7 +29,7 @@ struct PasscodeView: View { } .padding(.horizontal, 40) .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(Color(uiColor: .systemBackground)) + .background(AppTheme.shared.colors.background) } private func verticalPasscodeView(_ g: GeometryProxy) -> some View { diff --git a/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift b/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift index 645de4c3f8..9cc229ba80 100644 --- a/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift +++ b/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift @@ -32,6 +32,7 @@ private enum MigrateFromDeviceViewAlert: Identifiable { case keychainError(_ title: LocalizedStringKey = "Keychain error") case databaseError(_ title: LocalizedStringKey = "Database error", message: String) case unknownError(_ title: LocalizedStringKey = "Unknown error", message: String) + case archiveExportedWithErrors(archivePath: URL, archiveErrors: [ArchiveError]) case error(title: LocalizedStringKey, error: String = "") @@ -45,6 +46,7 @@ private enum MigrateFromDeviceViewAlert: Identifiable { case .keychainError: return "keychainError" case let .databaseError(title, message): return "\(title) \(message)" case let .unknownError(title, message): return "\(title) \(message)" + case let .archiveExportedWithErrors(path, _): return "archiveExportedWithErrors \(path)" case let .error(title, _): return "error \(title)" } @@ -53,6 +55,7 @@ private enum MigrateFromDeviceViewAlert: Identifiable { struct MigrateFromDevice: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss: DismissAction @Binding var showSettings: Bool @Binding var showProgressOnSettings: Bool @@ -165,6 +168,14 @@ struct MigrateFromDevice: View { return Alert(title: Text(title), message: Text(message)) case let .unknownError(title, message): return Alert(title: Text(title), message: Text(message)) + case let .archiveExportedWithErrors(archivePath, errs): + return Alert( + title: Text("Chat database exported"), + message: Text("You may migrate the exported database.") + Text(verbatim: "\n\n") + Text("Some file(s) were not exported:") + archiveErrorsText(errs), + dismissButton: .default(Text("Continue")) { + Task { await uploadArchive(path: archivePath) } + } + ) case let .error(title, error): return Alert(title: Text(title), message: Text(error)) } @@ -177,6 +188,7 @@ struct MigrateFromDevice: View { List { Section {} header: { Text("Stopping chat") + .foregroundColor(theme.colors.secondary) } } progressView() @@ -188,14 +200,16 @@ struct MigrateFromDevice: View { Section { Text(reason) Button(action: stopChat) { - settingsRow("stop.fill") { + settingsRow("stop.fill", color: theme.colors.secondary) { Text("Stop chat").foregroundColor(.red) } } } header: { Text("Error stopping chat") + .foregroundColor(theme.colors.secondary) } footer: { Text("In order to continue, chat should be stopped.") + .foregroundColor(theme.colors.secondary) .font(.callout) } } @@ -214,14 +228,16 @@ struct MigrateFromDevice: View { List { Section { Button(action: { migrationState = .archiving }) { - settingsRow("tray.and.arrow.up") { - Text("Archive and upload").foregroundColor(.accentColor) + settingsRow("tray.and.arrow.up", color: theme.colors.secondary) { + Text("Archive and upload").foregroundColor(theme.colors.primary) } } } header: { Text("Confirm upload") + .foregroundColor(theme.colors.secondary) } footer: { Text("All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.") + .foregroundColor(theme.colors.secondary) .font(.callout) } } @@ -232,6 +248,7 @@ struct MigrateFromDevice: View { List { Section {} header: { Text("Archiving database") + .foregroundColor(theme.colors.secondary) } } progressView() @@ -246,10 +263,11 @@ struct MigrateFromDevice: View { List { Section {} header: { Text("Uploading archive") + .foregroundColor(theme.colors.secondary) } } let ratio = Float(uploadedBytes) / Float(totalBytes) - MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: uploadedBytes, countStyle: .binary)) uploaded") + MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: uploadedBytes, countStyle: .binary)) uploaded", theme.colors.primary) } .onAppear { startUploading(totalBytes, archivePath) @@ -262,14 +280,16 @@ struct MigrateFromDevice: View { Button(action: { migrationState = .uploadProgress(uploadedBytes: 0, totalBytes: totalBytes, fileId: 0, archivePath: archivePath, ctrl: nil) }) { - settingsRow("tray.and.arrow.up") { - Text("Repeat upload").foregroundColor(.accentColor) + settingsRow("tray.and.arrow.up", color: theme.colors.secondary) { + Text("Repeat upload").foregroundColor(theme.colors.primary) } } } header: { Text("Upload failed") + .foregroundColor(theme.colors.secondary) } footer: { Text("You can give another try.") + .foregroundColor(theme.colors.secondary) .font(.callout) } } @@ -283,6 +303,7 @@ struct MigrateFromDevice: View { List { Section {} header: { Text("Creating archive link") + .foregroundColor(theme.colors.secondary) } } progressView() @@ -293,13 +314,13 @@ struct MigrateFromDevice: View { List { Section { Button(action: { cancelMigration(fileId, ctrl) }) { - settingsRow("multiply") { + settingsRow("multiply", color: theme.colors.secondary) { Text("Cancel migration").foregroundColor(.red) } } Button(action: { finishMigration(fileId, ctrl) }) { - settingsRow("checkmark") { - Text("Finalize migration").foregroundColor(.accentColor) + settingsRow("checkmark", color: theme.colors.secondary) { + Text("Finalize migration").foregroundColor(theme.colors.primary) } } } footer: { @@ -307,9 +328,10 @@ struct MigrateFromDevice: View { Text("**Warning**: the archive will be removed.") Text("Choose _Migrate from another device_ on the new device and scan QR code.") } + .foregroundColor(theme.colors.secondary) .font(.callout) } - Section("Show QR code") { + Section(header: Text("Show QR code").foregroundColor(theme.colors.secondary)) { SimpleXLinkQRCode(uri: link) .padding() .background( @@ -322,7 +344,7 @@ struct MigrateFromDevice: View { .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) } - Section("Or securely share this file link") { + Section(header: Text("Or securely share this file link").foregroundColor(theme.colors.secondary)) { shareLinkView(link) } .listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 10)) @@ -334,22 +356,24 @@ struct MigrateFromDevice: View { List { Section { Button(action: { alert = .startChat() }) { - settingsRow("play.fill") { + settingsRow("play.fill", color: theme.colors.secondary) { Text("Start chat").foregroundColor(.red) } } Button(action: { alert = .deleteChat() }) { - settingsRow("trash.fill") { - Text("Delete database from this device").foregroundColor(.accentColor) + settingsRow("trash.fill", color: theme.colors.secondary) { + Text("Delete database from this device").foregroundColor(theme.colors.primary) } } } header: { Text("Migration complete") + .foregroundColor(theme.colors.secondary) } footer: { VStack(alignment: .leading, spacing: 16) { Text("You **must not** use the same database on two devices.") Text("**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection.") } + .foregroundColor(theme.colors.secondary) .font(.callout) } } @@ -379,7 +403,7 @@ struct MigrateFromDevice: View { .truncationMode(.middle) } - static func largeProgressView(_ value: Float, _ title: String, _ description: LocalizedStringKey) -> some View { + static func largeProgressView(_ value: Float, _ title: String, _ description: LocalizedStringKey, _ primaryColor: Color) -> some View { ZStack { VStack { Text(description) @@ -389,7 +413,7 @@ struct MigrateFromDevice: View { Text(title) .font(.system(size: 54)) .bold() - .foregroundColor(.accentColor) + .foregroundColor(primaryColor) Text(description) .font(.title3) @@ -398,7 +422,7 @@ struct MigrateFromDevice: View { Circle() .trim(from: 0, to: CGFloat(value)) .stroke( - Color.accentColor, + primaryColor, style: StrokeStyle(lineWidth: 27) ) .rotationEffect(.degrees(180)) @@ -435,15 +459,12 @@ struct MigrateFromDevice: View { Task { do { try? FileManager.default.createDirectory(at: getMigrationTempFilesDirectory(), withIntermediateDirectories: true) - let archivePath = try await exportChatArchive(getMigrationTempFilesDirectory()) - if let attrs = try? FileManager.default.attributesOfItem(atPath: archivePath.path), - let totalBytes = attrs[.size] as? Int64 { - await MainActor.run { - migrationState = .uploadProgress(uploadedBytes: 0, totalBytes: totalBytes, fileId: 0, archivePath: archivePath, ctrl: nil) - } + let (archivePath, errs) = try await exportChatArchive(getMigrationTempFilesDirectory()) + if errs.isEmpty { + await uploadArchive(path: archivePath) } else { await MainActor.run { - alert = .error(title: "Exported file doesn't exist") + alert = .archiveExportedWithErrors(archivePath: archivePath, archiveErrors: errs) migrationState = .uploadConfirmation } } @@ -455,6 +476,20 @@ struct MigrateFromDevice: View { } } } + + private func uploadArchive(path archivePath: URL) async { + if let attrs = try? FileManager.default.attributesOfItem(atPath: archivePath.path), + let totalBytes = attrs[.size] as? Int64 { + await MainActor.run { + migrationState = .uploadProgress(uploadedBytes: 0, totalBytes: totalBytes, fileId: 0, archivePath: archivePath, ctrl: nil) + } + } else { + await MainActor.run { + alert = .error(title: "Exported file doesn't exist") + migrationState = .uploadConfirmation + } + } + } private func initTemporaryDatabase() -> (chat_ctrl, User)? { let (status, ctrl) = chatInitTemporaryDatabase(url: tempDatabaseUrl) @@ -590,6 +625,7 @@ struct MigrateFromDevice: View { } private struct PassphraseConfirmationView: View { + @EnvironmentObject var theme: AppTheme @Binding var migrationState: MigrationFromState @State private var useKeychain = storeDBPassphraseGroupDefault.get() @State private var currentKey: String = "" @@ -612,15 +648,17 @@ private struct PassphraseConfirmationView: View { verifyingPassphrase = false } }) { - settingsRow(useKeychain ? "key" : "lock", color: .secondary) { + settingsRow(useKeychain ? "key" : "lock", color: theme.colors.secondary) { Text("Verify passphrase") } } .disabled(verifyingPassphrase || currentKey.isEmpty) } header: { Text("Verify database passphrase") + .foregroundColor(theme.colors.secondary) } footer: { Text("Confirm that you remember database passphrase to migrate it.") + .foregroundColor(theme.colors.secondary) .font(.callout) } .onAppear { @@ -645,7 +683,7 @@ private struct PassphraseConfirmationView: View { if case .chatCmdError(_, .errorDatabase(.errorOpen(.errorNotADatabase))) = error as? ChatResponse { showErrorOnMigrationIfNeeded(.errorNotADatabase(dbFile: ""), $alert) } else { - alert = .error(title: "Error", error: NSLocalizedString("Error verifying passphrase:", comment: "") + " " + String(String(describing: error))) + alert = .error(title: "Error", error: NSLocalizedString("Error verifying passphrase:", comment: "") + " " + String(responseError(error))) } } } diff --git a/apps/ios/Shared/Views/Migration/MigrateToDevice.swift b/apps/ios/Shared/Views/Migration/MigrateToDevice.swift index e290537b46..67ea1008cd 100644 --- a/apps/ios/Shared/Views/Migration/MigrateToDevice.swift +++ b/apps/ios/Shared/Views/Migration/MigrateToDevice.swift @@ -91,6 +91,7 @@ private enum MigrateToDeviceViewAlert: Identifiable { struct MigrateToDevice: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss: DismissAction @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @Binding var migrationState: MigrationToState? @@ -180,7 +181,7 @@ struct MigrateToDevice: View { private func pasteOrScanLinkView() -> some View { ZStack { List { - Section("Scan QR code") { + Section(header: Text("Scan QR code").foregroundColor(theme.colors.secondary)) { ScannerInView(showQRCodeScanner: $showQRCodeScanner) { resp in switch resp { case let .success(r): @@ -197,7 +198,7 @@ struct MigrateToDevice: View { } } if developerTools { - Section("Or paste archive link") { + Section(header: Text("Or paste archive link").foregroundColor(theme.colors.secondary)) { pasteLinkView() } } @@ -226,6 +227,7 @@ struct MigrateToDevice: View { List { Section {} header: { Text("Downloading link details") + .foregroundColor(theme.colors.secondary) } } progressView() @@ -240,10 +242,11 @@ struct MigrateToDevice: View { List { Section {} header: { Text("Downloading archive") + .foregroundColor(theme.colors.secondary) } } let ratio = Float(downloadedBytes) / Float(max(totalBytes, 1)) - MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: downloadedBytes, countStyle: .binary)) downloaded") + MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: downloadedBytes, countStyle: .binary)) downloaded", theme.colors.primary) } } @@ -254,14 +257,16 @@ struct MigrateToDevice: View { try? FileManager.default.removeItem(atPath: archivePath) migrationState = .linkDownloading(link: link) }) { - settingsRow("tray.and.arrow.down") { - Text("Repeat download").foregroundColor(.accentColor) + settingsRow("tray.and.arrow.down", color: theme.colors.secondary) { + Text("Repeat download").foregroundColor(theme.colors.primary) } } } header: { Text("Download failed") + .foregroundColor(theme.colors.secondary) } footer: { Text("You can give another try.") + .foregroundColor(theme.colors.secondary) .font(.callout) } } @@ -277,6 +282,7 @@ struct MigrateToDevice: View { List { Section {} header: { Text("Importing archive") + .foregroundColor(theme.colors.secondary) } } progressView() @@ -292,14 +298,16 @@ struct MigrateToDevice: View { Button(action: { migrationState = .archiveImport(archivePath: archivePath) }) { - settingsRow("square.and.arrow.down") { - Text("Repeat import").foregroundColor(.accentColor) + settingsRow("square.and.arrow.down", color: theme.colors.secondary) { + Text("Repeat import").foregroundColor(theme.colors.primary) } } } header: { Text("Import failed") + .foregroundColor(theme.colors.secondary) } footer: { Text("You can give another try.") + .foregroundColor(theme.colors.secondary) .font(.callout) } } @@ -323,7 +331,7 @@ struct MigrateToDevice: View { case let .migrationError(mtrError): ("Incompatible database version", nil, - "\(NSLocalizedString("Error: ", comment: "")) \(DatabaseErrorView.mtrErrorDescription(mtrError))", + "\(NSLocalizedString("Error: ", comment: "")) \(mtrErrorDescription(mtrError))", nil) } default: ("Error", nil, "Unknown error", nil) @@ -333,8 +341,8 @@ struct MigrateToDevice: View { Button(action: { migrationState = .migration(passphrase: passphrase, confirmation: confirmation, useKeychain: useKeychain) }) { - settingsRow("square.and.arrow.down") { - Text(button).foregroundColor(.accentColor) + settingsRow("square.and.arrow.down", color: theme.colors.secondary) { + Text(button).foregroundColor(theme.colors.primary) } } } else { @@ -342,8 +350,10 @@ struct MigrateToDevice: View { } } header: { Text(header) + .foregroundColor(theme.colors.secondary) } footer: { Text(footer) + .foregroundColor(theme.colors.secondary) .font(.callout) } } @@ -354,6 +364,7 @@ struct MigrateToDevice: View { List { Section {} header: { Text("Migrating") + .foregroundColor(theme.colors.secondary) } } progressView() @@ -364,6 +375,7 @@ struct MigrateToDevice: View { } struct OnionView: View { + @EnvironmentObject var theme: AppTheme @State var appSettings: AppSettings @State private var onionHosts: OnionHosts = .no var finishMigration: (AppSettings) -> Void @@ -380,18 +392,20 @@ struct MigrateToDevice: View { appSettings.networkConfig = updated finishMigration(appSettings) }) { - settingsRow("checkmark") { - Text("Apply").foregroundColor(.accentColor) + settingsRow("checkmark", color: theme.colors.secondary) { + Text("Apply").foregroundColor(theme.colors.primary) } } } header: { Text("Confirm network settings") + .foregroundColor(theme.colors.secondary) } footer: { Text("Please confirm that network settings are correct for this device.") + .foregroundColor(theme.colors.secondary) .font(.callout) } - Section("Network settings") { + Section(header: Text("Network settings").foregroundColor(theme.colors.secondary)) { Picker("Use .onion hosts", selection: $onionHosts) { ForEach(OnionHosts.values, id: \.self) { Text($0.text) } } @@ -475,6 +489,7 @@ struct MigrateToDevice: View { chatInitControllerRemovingDatabases() } try await apiDeleteStorage() + try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true) do { let config = ArchiveConfig(archivePath: archivePath) let archiveErrors = try await apiImportArchive(config: config) @@ -566,6 +581,7 @@ struct MigrateToDevice: View { } private struct PassphraseEnteringView: View { + @EnvironmentObject var theme: AppTheme @Binding var migrationState: MigrationToState? @State private var useKeychain = true @State var currentKey: String @@ -577,7 +593,7 @@ private struct PassphraseEnteringView: View { ZStack { List { Section { - settingsRow("key", color: .secondary) { + settingsRow("key", color: theme.colors.secondary) { Toggle("Save passphrase in Keychain", isOn: $useKeychain) } @@ -606,13 +622,14 @@ private struct PassphraseEnteringView: View { verifyingPassphrase = false } }) { - settingsRow("key", color: .secondary) { + settingsRow("key", color: theme.colors.secondary) { Text("Open chat") } } .disabled(verifyingPassphrase || currentKey.isEmpty) } header: { Text("Enter passphrase") + .foregroundColor(theme.colors.secondary) } footer: { VStack(alignment: .leading, spacing: 16) { if useKeychain { @@ -623,6 +640,7 @@ private struct PassphraseEnteringView: View { Text("**Warning**: Instant push notifications require passphrase saved in Keychain.") } } + .foregroundColor(theme.colors.secondary) .font(.callout) .padding(.top, 1) .onTapGesture { keyboardVisible = false } diff --git a/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift b/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift index 45eb783326..6001dff790 100644 --- a/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift +++ b/apps/ios/Shared/Views/NewChat/AddContactLearnMore.swift @@ -30,6 +30,7 @@ struct AddContactLearnMore: View { } .listRowBackground(Color.clear) } + .modifier(ThemedBackground(grouped: true)) } } diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index 4b272f4caa..33c187b64b 100644 --- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift +++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct AddGroupView: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss: DismissAction @AppStorage(GROUP_DEFAULT_INCOGNITO, store: groupDefaults) private var incognitoDefault = false @State private var chat: Chat? @@ -34,43 +35,40 @@ struct AddGroupView: View { creatingGroup: true, showFooterCounter: false ) { _ in - dismiss() DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - m.chatId = groupInfo.id + dismissAllSheets(animated: true) { + ItemsModel.shared.loadOpenChat(groupInfo.id) + } } } + .navigationBarTitleDisplayMode(.inline) } else { GroupLinkView( groupId: groupInfo.groupId, groupLink: $groupLink, groupLinkMemberRole: $groupLinkMemberRole, - showTitle: true, + showTitle: false, creatingGroup: true ) { - dismiss() DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - m.chatId = groupInfo.id + dismissAllSheets(animated: true) { + ItemsModel.shared.loadOpenChat(groupInfo.id) + } } } + .navigationBarTitle("Group link") } } else { - createGroupView().keyboardPadding() + createGroupView() } } func createGroupView() -> some View { List { Group { - Text("Create secret group") - .font(.largeTitle) - .bold() - .fixedSize(horizontal: false, vertical: true) - .padding(.bottom, 24) - .onTapGesture(perform: hideKeyboard) - ZStack(alignment: .center) { ZStack(alignment: .topTrailing) { - ProfileImage(imageStr: profile.image, size: 128, color: Color(uiColor: .secondarySystemGroupedBackground)) + ProfileImage(imageStr: profile.image, size: 128) if profile.image != nil { Button { profile.image = nil @@ -95,7 +93,7 @@ struct AddGroupView: View { Section { groupNameTextField() Button(action: createGroup) { - settingsRow("checkmark", color: .accentColor) { Text("Create group") } + settingsRow("checkmark", color: theme.colors.primary) { Text("Create group") } } .disabled(!canCreateProfile()) IncognitoToggle(incognitoEnabled: $incognitoDefault) @@ -104,6 +102,7 @@ struct AddGroupView: View { sharedGroupProfileInfo(incognitoDefault) Text("Fully decentralized – visible only to members.") } + .foregroundColor(theme.colors.secondary) .frame(maxWidth: .infinity, alignment: .leading) .onTapGesture(perform: hideKeyboard) } @@ -144,6 +143,7 @@ struct AddGroupView: View { profile.image = nil } } + .modifier(ThemedBackground(grouped: true)) } func groupNameTextField() -> some View { @@ -156,7 +156,7 @@ struct AddGroupView: View { Image(systemName: "exclamationmark.circle").foregroundColor(.red) } } else { - Image(systemName: "pencil").foregroundColor(.secondary) + Image(systemName: "pencil").foregroundColor(theme.colors.secondary) } textField("Enter group name…", text: $profile.displayName) .focused($focusDisplayName) @@ -191,6 +191,7 @@ struct AddGroupView: View { let groupMembers = await apiListMembers(gInfo.groupId) await MainActor.run { m.groupMembers = groupMembers.map { GMember.init($0) } + m.populateGroupMembersIndexes() } } let c = Chat(chatInfo: .group(groupInfo: gInfo), chatItems: []) @@ -200,13 +201,14 @@ struct AddGroupView: View { chat = c } } catch { - dismiss() - AlertManager.shared.showAlert( - Alert( - title: Text("Error creating group"), - message: Text(responseError(error)) + dismissAllSheets(animated: true) { + AlertManager.shared.showAlert( + Alert( + title: Text("Error creating group"), + message: Text(responseError(error)) + ) ) - ) + } } } diff --git a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift index c3452ce18d..bcca763a75 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift @@ -7,46 +7,499 @@ // import SwiftUI +import SimpleXChat -enum NewChatMenuOption: Identifiable { - case newContact - case newGroup - - var id: Self { self } +enum ContactType: Int { + case card, request, recent, chatDeleted, unlisted } struct NewChatMenuButton: View { - @Binding var newChatMenuOption: NewChatMenuOption? + @State private var showNewChatSheet = false + @State private var alert: SomeAlert? = nil + @State private var globalAlert: SomeAlert? = nil var body: some View { - Menu { Button { - newChatMenuOption = .newContact - } label: { - Text("Add contact") - } - Button { - newChatMenuOption = .newGroup - } label: { - Text("Create group") - } + showNewChatSheet = true } 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() + .appSheet(isPresented: $showNewChatSheet) { + NewChatSheet(alert: $alert) + .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) + .alert(item: $alert) { a in + return a.alert + } + } + // This is a workaround to show "Keep unused invitation" alert in both following cases: + // - on going back from NewChatView to NewChatSheet, + // - on dismissing NewChatMenuButton sheet while on NewChatView (skipping NewChatSheet) + .onChange(of: alert?.id) { a in + if !showNewChatSheet && alert != nil { + globalAlert = alert + alert = nil } } + .alert(item: $globalAlert) { a in + return a.alert + } } } -#Preview { - NewChatMenuButton( - newChatMenuOption: Binding.constant(nil) - ) +private var indent: CGFloat = 36 + +struct NewChatSheet: View { + @EnvironmentObject var theme: AppTheme + @State private var baseContactTypes: [ContactType] = [.card, .request, .recent] + @EnvironmentObject var chatModel: ChatModel + @State private var searchMode = false + @FocusState var searchFocussed: Bool + @State private var searchText = "" + @State private var searchShowingSimplexLink = false + @State private var searchChatFilteredBySimplexLink: String? = nil + @Binding var alert: SomeAlert? + + // Sheet height management + @State private var isAddContactActive = false + @State private var isScanPasteLinkActive = false + @State private var isLargeSheet = false + @State private var allowSmallSheet = true + + @AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true + + var body: some View { + let showArchive = !filterContactTypes(chats: chatModel.chats, contactTypes: [.chatDeleted]).isEmpty + let v = NavigationView { + viewBody(showArchive) + .navigationTitle("New message") + .navigationBarTitleDisplayMode(.large) + .navigationBarHidden(searchMode) + .modifier(ThemedBackground(grouped: true)) + } + if #available(iOS 16.0, *), oneHandUI { + let sheetHeight: CGFloat = showArchive ? 575 : 500 + v.presentationDetents( + allowSmallSheet ? [.height(sheetHeight), .large] : [.large], + selection: Binding( + get: { isLargeSheet || !allowSmallSheet ? .large : .height(sheetHeight) }, + set: { isLargeSheet = $0 == .large } + ) + ) + } else { + v + } + } + + @ViewBuilder private func viewBody(_ showArchive: Bool) -> some View { + List { + HStack { + ContactsListSearchBar( + searchMode: $searchMode, + searchFocussed: $searchFocussed, + searchText: $searchText, + searchShowingSimplexLink: $searchShowingSimplexLink, + searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink + ) + .frame(maxWidth: .infinity) + } + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + + if (searchText.isEmpty) { + Section { + NavigationLink(isActive: $isAddContactActive) { + NewChatView(selection: .invite, parentAlert: $alert) + .navigationTitle("New chat") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + } label: { + navigateOnTap(Label("Add contact", systemImage: "link.badge.plus")) { + isAddContactActive = true + } + } + NavigationLink(isActive: $isScanPasteLinkActive) { + NewChatView(selection: .connect, showQRCodeScanner: true, parentAlert: $alert) + .navigationTitle("New chat") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + } label: { + navigateOnTap(Label("Scan / Paste link", systemImage: "qrcode")) { + isScanPasteLinkActive = true + } + } + NavigationLink { + AddGroupView() + .navigationTitle("Create secret group") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.large) + } label: { + Label("Create group", systemImage: "person.2.circle.fill") + } + } + + if (showArchive) { + Section { + NavigationLink { + DeletedChats() + } label: { + newChatActionButton("archivebox", color: theme.colors.secondary) { Text("Archived contacts") } + } + } + } + } + + ContactsList( + baseContactTypes: $baseContactTypes, + searchMode: $searchMode, + searchText: $searchText, + header: "Your Contacts", + searchFocussed: $searchFocussed, + searchShowingSimplexLink: $searchShowingSimplexLink, + searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink, + showDeletedChatIcon: true + ) + } + } + + /// Extends label's tap area to match `.insetGrouped` list row insets + private func navigateOnTap(_ label: L, setActive: @escaping () -> Void) -> some View { + label + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.leading, 16).padding(.vertical, 8).padding(.trailing, 32) + .contentShape(Rectangle()) + .onTapGesture { + isLargeSheet = true + DispatchQueue.main.async { + allowSmallSheet = false + setActive() + } + } + .padding(.leading, -16).padding(.vertical, -8).padding(.trailing, -32) + } + + func newChatActionButton(_ icon: String, color: Color/* = .secondary*/, content: @escaping () -> Content) -> some View { + ZStack(alignment: .leading) { + Image(systemName: icon) + .resizable() + .scaledToFit() + .frame(maxWidth: 24, maxHeight: 24, alignment: .center) + .symbolRenderingMode(.monochrome) + .foregroundColor(color) + content().foregroundColor(theme.colors.onBackground).padding(.leading, indent) + } + } +} + +func chatContactType(chat: Chat) -> ContactType { + switch chat.chatInfo { + case .contactRequest: + return .request + case let .direct(contact): + if contact.activeConn == nil && contact.profile.contactLink != nil { + return .card + } else if contact.chatDeleted { + return .chatDeleted + } else if contact.contactStatus == .active { + return .recent + } else { + return .unlisted + } + default: + return .unlisted + } +} + +private func filterContactTypes(chats: [Chat], contactTypes: [ContactType]) -> [Chat] { + return chats.filter { chat in + contactTypes.contains(chatContactType(chat: chat)) + } +} + +struct ContactsList: View { + @EnvironmentObject var theme: AppTheme + @EnvironmentObject var chatModel: ChatModel + @Binding var baseContactTypes: [ContactType] + @Binding var searchMode: Bool + @Binding var searchText: String + var header: String? = nil + @FocusState.Binding var searchFocussed: Bool + @Binding var searchShowingSimplexLink: Bool + @Binding var searchChatFilteredBySimplexLink: String? + var showDeletedChatIcon: Bool + @AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false + + var body: some View { + let contactTypes = contactTypesSearchTargets(baseContactTypes: baseContactTypes, searchEmpty: searchText.isEmpty) + let contactChats = filterContactTypes(chats: chatModel.chats, contactTypes: contactTypes) + let filteredContactChats = filteredContactChats( + showUnreadAndFavorites: showUnreadAndFavorites, + searchShowingSimplexLink: searchShowingSimplexLink, + searchChatFilteredBySimplexLink: searchChatFilteredBySimplexLink, + searchText: searchText, + contactChats: contactChats + ) + + if !filteredContactChats.isEmpty { + Section(header: Group { + if let header = header { + Text(header) + .textCase(.uppercase) + .foregroundColor(theme.colors.secondary) + } + } + ) { + ForEach(filteredContactChats, id: \.viewId) { chat in + ContactListNavLink(chat: chat, showDeletedChatIcon: showDeletedChatIcon) + .disabled(chatModel.chatRunning != true) + } + } + } + + if filteredContactChats.isEmpty && !contactChats.isEmpty { + noResultSection(text: "No filtered contacts") + } else if contactChats.isEmpty { + noResultSection(text: "No contacts") + } + } + + @ViewBuilder private func noResultSection(text: String) -> some View { + Section { + Text(text) + .foregroundColor(theme.colors.secondary) + .frame(maxWidth: .infinity, alignment: .center) + + } + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(top: 7, leading: 0, bottom: 7, trailing: 0)) + } + + private func contactTypesSearchTargets(baseContactTypes: [ContactType], searchEmpty: Bool) -> [ContactType] { + if baseContactTypes.contains(.chatDeleted) || searchEmpty { + return baseContactTypes + } else { + return baseContactTypes + [.chatDeleted] + } + } + + private func chatsByTypeComparator(chat1: Chat, chat2: Chat) -> Bool { + let chat1Type = chatContactType(chat: chat1) + let chat2Type = chatContactType(chat: chat2) + + if chat1Type.rawValue < chat2Type.rawValue { + return true + } else if chat1Type.rawValue > chat2Type.rawValue { + return false + } else { + return chat2.chatInfo.chatTs < chat1.chatInfo.chatTs + } + } + + private func filterChat(chat: Chat, searchText: String, showUnreadAndFavorites: Bool) -> Bool { + var meetsPredicate = true + let s = searchText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + let cInfo = chat.chatInfo + + if !searchText.isEmpty { + if (!cInfo.chatViewName.lowercased().contains(searchText.lowercased())) { + if case let .direct(contact) = cInfo { + meetsPredicate = contact.profile.displayName.lowercased().contains(s) || contact.fullName.lowercased().contains(s) + } else { + meetsPredicate = false + } + } + } + + if showUnreadAndFavorites { + meetsPredicate = meetsPredicate && (cInfo.chatSettings?.favorite ?? false) + } + + return meetsPredicate + } + + func filteredContactChats( + showUnreadAndFavorites: Bool, + searchShowingSimplexLink: Bool, + searchChatFilteredBySimplexLink: String?, + searchText: String, + contactChats: [Chat] + ) -> [Chat] { + let linkChatId = searchChatFilteredBySimplexLink + let s = searchShowingSimplexLink ? "" : searchText.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + + let filteredChats: [Chat] + + if let linkChatId = linkChatId { + filteredChats = contactChats.filter { $0.id == linkChatId } + } else { + filteredChats = contactChats.filter { chat in + filterChat(chat: chat, searchText: s, showUnreadAndFavorites: showUnreadAndFavorites) + } + } + + return filteredChats.sorted(by: chatsByTypeComparator) + } +} + +struct ContactsListSearchBar: View { + @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme + @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 alert: PlanAndConnectAlert? + @State private var sheet: PlanAndConnectActionSheet? + @AppStorage(DEFAULT_SHOW_UNREAD_AND_FAVORITES) private var showUnreadAndFavorites = false + + var body: some View { + HStack(spacing: 12) { + HStack(spacing: 4) { + Spacer() + .frame(width: 8) + Image(systemName: "magnifyingglass") + .resizable() + .scaledToFit() + .frame(width: 16, height: 16) + TextField("Search or paste SimpleX link", text: $searchText) + .foregroundColor(searchShowingSimplexLink ? theme.colors.secondary : theme.colors.onBackground) + .disabled(searchShowingSimplexLink) + .focused($searchFocussed) + .frame(maxWidth: .infinity) + if !searchText.isEmpty { + Image(systemName: "xmark.circle.fill") + .resizable() + .scaledToFit() + .frame(width: 16, height: 16) + .onTapGesture { + searchText = "" + } + } + } + .padding(EdgeInsets(top: 7, leading: 7, bottom: 7, trailing: 7)) + .foregroundColor(theme.colors.secondary) + .background(Color(uiColor: .secondarySystemGroupedBackground)) + .cornerRadius(10.0) + + if searchFocussed { + Text("Cancel") + .foregroundColor(theme.colors.primary) + .onTapGesture { + searchText = "" + searchFocussed = false + } + } else if m.chats.count > 0 { + toggleFilterButton() + } + } + .padding(.top, 24) + .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 toggleFilterButton() -> some View { + ZStack { + Color.clear + .frame(width: 22, height: 22) + Image(systemName: showUnreadAndFavorites ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease") + .resizable() + .scaledToFit() + .foregroundColor(showUnreadAndFavorites ? theme.colors.primary : theme.colors.secondary) + .frame(width: showUnreadAndFavorites ? 22 : 16, height: showUnreadAndFavorites ? 22 : 16) + .onTapGesture { + showUnreadAndFavorites = !showUnreadAndFavorites + } + } + } + + private func connect(_ link: String) { + planAndConnect( + link, + showAlert: { alert = $0 }, + showActionSheet: { sheet = $0 }, + dismiss: true, + incognito: nil, + filterKnownContact: { searchChatFilteredBySimplexLink = $0.id } + ) + } +} + + +struct DeletedChats: View { + @State private var baseContactTypes: [ContactType] = [.chatDeleted] + @State private var searchMode = false + @FocusState var searchFocussed: Bool + @State private var searchText = "" + @State private var searchShowingSimplexLink = false + @State private var searchChatFilteredBySimplexLink: String? = nil + + var body: some View { + List { + ContactsListSearchBar( + searchMode: $searchMode, + searchFocussed: $searchFocussed, + searchText: $searchText, + searchShowingSimplexLink: $searchShowingSimplexLink, + searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink + ) + .listRowSeparator(.hidden) + .listRowBackground(Color.clear) + .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) + .frame(maxWidth: .infinity) + + ContactsList( + baseContactTypes: $baseContactTypes, + searchMode: $searchMode, + searchText: $searchText, + searchFocussed: $searchFocussed, + searchShowingSimplexLink: $searchShowingSimplexLink, + searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink, + showDeletedChatIcon: false + ) + } + .navigationTitle("Archived contacts") + .navigationBarTitleDisplayMode(.large) + .navigationBarHidden(searchMode) + .modifier(ThemedBackground(grouped: true)) + + } +} + +#Preview { + NewChatMenuButton() } diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 7ece4fdee6..6cbc65e7c9 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -10,21 +10,26 @@ import SwiftUI import SimpleXChat import CodeScanner import AVFoundation +import SimpleXChat -enum SomeAlert: Identifiable { - case someAlert(alert: Alert, id: String) +struct SomeAlert: Identifiable { + var alert: Alert + var id: String +} - var id: String { - switch self { - case let .someAlert(_, id): return id - } - } +struct SomeActionSheet: Identifiable { + var actionSheet: ActionSheet + var id: String +} + +struct SomeSheet: Identifiable { + @ViewBuilder var content: Content + var id: String } 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)" @@ -42,6 +47,7 @@ enum NewChatOption: Identifiable { struct NewChatView: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @State var selection: NewChatOption @State var showQRCodeScanner = false @State private var invitationUsed: Bool = false @@ -50,22 +56,10 @@ struct NewChatView: View { @State private var creatingConnReq = false @State private var pastedLink: String = "" @State private var alert: NewChatViewAlert? + @Binding var parentAlert: SomeAlert? 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) @@ -91,10 +85,11 @@ struct NewChatView: View { } } .frame(maxWidth: .infinity, maxHeight: .infinity) + .modifier(ThemedBackground(grouped: true)) .background( // Rectangle is needed for swipe gesture to work on mostly empty views (creatingLinkProgressView and retryButton) Rectangle() - .fill(Color(uiColor: .systemGroupedBackground)) + .fill(theme.base == DefaultTheme.LIGHT ? theme.colors.background.asGroupedBackground(theme.base.mode) : theme.colors.background) ) .animation(.easeInOut(duration: 0.3333), value: selection) .gesture(DragGesture(minimumDistance: 20.0, coordinateSpace: .local) @@ -113,7 +108,14 @@ struct NewChatView: View { } ) } - .background(Color(.systemGroupedBackground)) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + InfoSheetButton { + AddContactLearnMore(showTitle: true) + } + } + } + .modifier(ThemedBackground(grouped: true)) .onChange(of: invitationUsed) { used in if used && !(m.showingInvitation?.connChatUsed ?? true) { m.markShowingInvitationUsed() @@ -122,19 +124,22 @@ struct NewChatView: View { .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: [] - )) + parentAlert = SomeAlert( + alert: Alert( + title: Text("Keep unused invitation?"), + message: Text("You can view invitation link again in connection details."), + primaryButton: .default(Text("Keep")) {}, + secondaryButton: .destructive(Text("Delete")) { + Task { + await deleteChat(Chat( + chatInfo: .contactConnection(contactConnection: conn), + chatItems: [] + )) + } } - } - )) + ), + id: "keepUnusedInvitation" + ) } m.showingInvitation = nil } @@ -142,8 +147,8 @@ struct NewChatView: View { switch(a) { case let .planAndConnectAlert(alert): return planAndConnectAlert(alert, dismiss: true, cleanup: { pastedLink = "" }) - case let .newChatSomeAlert(.someAlert(alert, _)): - return alert + case let .newChatSomeAlert(a): + return a.alert } } } @@ -181,7 +186,7 @@ struct NewChatView: View { await MainActor.run { creatingConnReq = false if let apiAlert = apiAlert { - alert = .newChatSomeAlert(alert: .someAlert(alert: apiAlert, id: "createInvitation error")) + alert = .newChatSomeAlert(alert: SomeAlert(alert: apiAlert, id: "createInvitation error")) } } } @@ -207,6 +212,7 @@ struct NewChatView: View { private struct InviteView: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme @Binding var invitationUsed: Bool @Binding var contactConnection: PendingContactConnection? var connReqInvitation: String @@ -214,7 +220,7 @@ private struct InviteView: View { var body: some View { List { - Section("Share this 1-time invite link") { + Section(header: Text("Share this 1-time invite link").foregroundColor(theme.colors.secondary)) { shareLinkView() } .listRowInsets(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 10)) @@ -225,6 +231,7 @@ private struct InviteView: View { IncognitoToggle(incognitoEnabled: $incognitoDefault) } footer: { sharedProfileInfo(incognitoDefault) + .foregroundColor(theme.colors.secondary) } } .onChange(of: incognitoDefault) { incognito in @@ -261,7 +268,7 @@ private struct InviteView: View { } private func qrCodeView() -> some View { - Section("Or show this code") { + Section(header: Text("Or show this code").foregroundColor(theme.colors.secondary)) { SimpleXLinkQRCode(uri: connReqInvitation, onShare: setInvitationUsed) .padding() .background( @@ -284,6 +291,7 @@ private struct InviteView: View { private struct ConnectView: View { @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var theme: AppTheme @Binding var showQRCodeScanner: Bool @Binding var pastedLink: String @Binding var alert: NewChatViewAlert? @@ -291,10 +299,10 @@ private struct ConnectView: View { var body: some View { List { - Section("Paste the link you received") { + Section(header: Text("Paste the link you received").foregroundColor(theme.colors.secondary)) { pasteLinkView() } - Section("Or scan QR code") { + Section(header: Text("Or scan QR code").foregroundColor(theme.colors.secondary)) { ScannerInView(showQRCodeScanner: $showQRCodeScanner, processQRCode: processQRCode) } } @@ -315,7 +323,7 @@ private struct ConnectView: View { // showQRCodeScanner = false connect(pastedLink) } else { - alert = .newChatSomeAlert(alert: .someAlert( + 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" )) @@ -338,14 +346,14 @@ private struct ConnectView: View { if strIsSimplexLink(r.string) { connect(link) } else { - alert = .newChatSomeAlert(alert: .someAlert( + 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 = .newChatSomeAlert(alert: SomeAlert( alert: mkAlert(title: "Invalid QR code", message: "Error scanning code: \(e.localizedDescription)"), id: "processQRCode: failure" )) @@ -367,11 +375,12 @@ struct ScannerInView: View { @Binding var showQRCodeScanner: Bool let processQRCode: (_ resp: Result) -> Void @State private var cameraAuthorizationStatus: AVAuthorizationStatus? + var scanMode: ScanMode = .continuous var body: some View { Group { if showQRCodeScanner, case .authorized = cameraAuthorizationStatus { - CodeScannerView(codeTypes: [.qr], scanMode: .continuous, completion: processQRCode) + CodeScannerView(codeTypes: [.qr], scanMode: scanMode, completion: processQRCode) .aspectRatio(1, contentMode: .fit) .cornerRadius(12) .listRowBackground(Color.clear) @@ -436,6 +445,7 @@ struct ScannerInView: View { } } + private func linkTextView(_ link: String) -> some View { Text(link) .lineLimit(1) @@ -486,6 +496,7 @@ func strHasSingleSimplexLink(_ str: String) -> FormattedText? { } struct IncognitoToggle: View { + @EnvironmentObject var theme: AppTheme @Binding var incognitoEnabled: Bool @State private var showIncognitoSheet = false @@ -493,13 +504,13 @@ struct IncognitoToggle: View { ZStack(alignment: .leading) { Image(systemName: incognitoEnabled ? "theatermasks.fill" : "theatermasks") .frame(maxWidth: 24, maxHeight: 24, alignment: .center) - .foregroundColor(incognitoEnabled ? Color.indigo : .secondary) + .foregroundColor(incognitoEnabled ? Color.indigo : theme.colors.secondary) .font(.system(size: 14)) Toggle(isOn: $incognitoEnabled) { HStack(spacing: 6) { Text("Incognito") Image(systemName: "info.circle") - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) .font(.system(size: 14)) } .onTapGesture { @@ -834,7 +845,10 @@ private func connectContactViaAddress_(_ contact: Contact, dismiss: Bool, incogn dismissAllSheets(animated: true) } } - _ = await connectContactViaAddress(contact.contactId, incognito) + let ok = await connectContactViaAddress(contact.contactId, incognito, showAlert: { AlertManager.shared.showAlert($0) }) + if ok { + AlertManager.shared.showAlert(connReqSentAlert(.contact)) + } cleanup?() } } @@ -884,11 +898,11 @@ func openKnownContact(_ contact: Contact, dismiss: Bool, showAlreadyExistsAlert: DispatchQueue.main.async { if dismiss { dismissAllSheets(animated: true) { - m.chatId = c.id + ItemsModel.shared.loadOpenChat(c.id) showAlreadyExistsAlert?() } } else { - m.chatId = c.id + ItemsModel.shared.loadOpenChat(c.id) showAlreadyExistsAlert?() } } @@ -903,11 +917,11 @@ func openKnownGroup(_ groupInfo: GroupInfo, dismiss: Bool, showAlreadyExistsAler DispatchQueue.main.async { if dismiss { dismissAllSheets(animated: true) { - m.chatId = g.id + ItemsModel.shared.loadOpenChat(g.id) showAlreadyExistsAlert?() } } else { - m.chatId = g.id + ItemsModel.shared.loadOpenChat(g.id) showAlreadyExistsAlert?() } } @@ -958,8 +972,13 @@ func connReqSentAlert(_ type: ConnReqType) -> Alert { ) } -#Preview { - NewChatView( - selection: .invite - ) +struct NewChatView_Previews: PreviewProvider { + static var previews: some View { + @State var parentAlert: SomeAlert? + + NewChatView( + selection: .invite, + parentAlert: $parentAlert + ) + } } diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift index 0ee6baa765..487f4ccdeb 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift @@ -27,6 +27,7 @@ enum UserProfileAlert: Identifiable { struct CreateProfile: View { @Environment(\.dismiss) var dismiss + @EnvironmentObject var theme: AppTheme @State private var displayName: String = "" @FocusState private var focusDisplayName @State private var alert: UserProfileAlert? @@ -45,6 +46,8 @@ struct CreateProfile: View { } header: { HStack { Text("Your profile") + .foregroundColor(theme.colors.secondary) + let name = displayName.trimmingCharacters(in: .whitespaces) let validName = mkValidName(name) if name != validName { @@ -62,22 +65,24 @@ struct CreateProfile: View { Text("Your profile, contacts and delivered messages are stored on your device.") Text("The profile is only shared with your contacts.") } + .foregroundColor(theme.colors.secondary) .frame(maxWidth: .infinity, alignment: .leading) } } .navigationTitle("Create your profile") + .modifier(ThemedBackground(grouped: true)) .alert(item: $alert) { a in userProfileAlert(a, $displayName) } .onAppear() { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { focusDisplayName = true } } - .keyboardPadding() } } struct CreateFirstProfile: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss @State private var displayName: String = "" @FocusState private var focusDisplayName @@ -89,9 +94,9 @@ struct CreateFirstProfile: View { .font(.largeTitle) .bold() Text("Your profile, contacts and delivered messages are stored on your device.") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) Text("The profile is only shared with your contacts.") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .padding(.bottom) } .padding(.bottom) @@ -122,7 +127,6 @@ struct CreateFirstProfile: View { } .padding() .frame(maxWidth: .infinity, alignment: .leading) - .keyboardPadding() } func onboardingButtons() -> some View { diff --git a/apps/ios/Shared/Views/Onboarding/HowItWorks.swift b/apps/ios/Shared/Views/Onboarding/HowItWorks.swift index fdd73d2632..c1975765d2 100644 --- a/apps/ios/Shared/Views/Onboarding/HowItWorks.swift +++ b/apps/ios/Shared/Views/Onboarding/HowItWorks.swift @@ -44,6 +44,7 @@ struct HowItWorks: View { .lineLimit(10) .padding() .frame(maxHeight: .infinity, alignment: .top) + .modifier(ThemedBackground()) } } diff --git a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift index 3bbd7a5c94..7681a42a77 100644 --- a/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift +++ b/apps/ios/Shared/Views/Onboarding/SetNotificationsMode.swift @@ -78,6 +78,7 @@ struct SetNotificationsMode: View { } struct NtfModeSelector: View { + @EnvironmentObject var theme: AppTheme var mode: NotificationsMode @Binding var selection: NotificationsMode @State private var tapped = false @@ -87,7 +88,7 @@ struct NtfModeSelector: View { VStack(alignment: .leading, spacing: 4) { Text(mode.label) .font(.headline) - .foregroundColor(selection == mode ? .accentColor : .secondary) + .foregroundColor(selection == mode ? theme.colors.primary : theme.colors.secondary) Text(ntfModeDescription(mode)) .lineLimit(10) .font(.subheadline) @@ -95,11 +96,11 @@ struct NtfModeSelector: View { .padding(12) } .frame(maxWidth: .infinity, alignment: .leading) - .background(Color(uiColor: tapped ? .secondarySystemFill : .systemBackground)) + .background(tapped ? Color(uiColor: .secondarySystemFill) : theme.colors.background) .clipShape(RoundedRectangle(cornerRadius: 18)) .overlay( RoundedRectangle(cornerRadius: 18) - .stroke(selection == mode ? Color.accentColor : Color(uiColor: .secondarySystemFill), lineWidth: 2) + .stroke(selection == mode ? theme.colors.primary : Color(uiColor: .secondarySystemFill), lineWidth: 2) ) ._onButtonGesture { down in tapped = down diff --git a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift index 94e281be7d..ee5a618e68 100644 --- a/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift +++ b/apps/ios/Shared/Views/Onboarding/SimpleXInfo.swift @@ -79,7 +79,7 @@ struct SimpleXInfo: View { MigrateToDevice(migrationState: $m.migrationState) } .navigationTitle("Migrate here") - .background(colorScheme == .light ? Color(uiColor: .tertiarySystemGroupedBackground) : .clear) + .modifier(ThemedBackground(grouped: true)) } } .sheet(isPresented: $showHowItWorks) { diff --git a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift index 209c440b16..ed3adcfe7d 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -15,9 +15,10 @@ private struct VersionDescription { } private struct FeatureDescription { - var icon: String + var icon: String? var title: LocalizedStringKey - var description: LocalizedStringKey + var description: LocalizedStringKey? + var subfeatures: [(icon: String, description: LocalizedStringKey)] = [] } private let versionDescriptions: [VersionDescription] = [ @@ -406,7 +407,66 @@ private let versionDescriptions: [VersionDescription] = [ description: "More reliable network connection." ) ] - ) + ), + VersionDescription( + version: "v5.8", + post: URL(string: "https://simplex.chat/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.html"), + features: [ + FeatureDescription( + icon: "arrow.forward", + title: "Private message routing 🚀", + description: "Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." + ), + FeatureDescription( + icon: "network.badge.shield.half.filled", + title: "Safely receive files", + description: "Confirm files from unknown servers." + ), + FeatureDescription( + icon: "battery.50", + title: "Improved message delivery", + description: "With reduced battery usage." + ) + ] + ), + VersionDescription( + version: "v6.0", + post: URL(string: "https://simplex.chat/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.html"), + features: [ + FeatureDescription( + icon: nil, + title: "New chat experience 🎉", + description: nil, + subfeatures: [ + ("link.badge.plus", "Connect to your friends faster."), + ("archivebox", "Archive contacts to chat later."), + ("trash", "Delete up to 20 messages at once."), + ("platter.filled.bottom.and.arrow.down.iphone", "Use the app with one hand."), + ("paintpalette", "Color chats with the new themes."), + ] + ), + FeatureDescription( + icon: nil, + title: "New media options", + description: nil, + subfeatures: [ + ("square.and.arrow.up", "Share from other apps."), + ("play.circle", "Play from the chat list."), + ("circle.filled.pattern.diagonalline.rectangle", "Blur for better privacy.") + ] + ), + FeatureDescription( + icon: "arrow.forward", + title: "Private message routing 🚀", + description: "It protects your IP address and connections." + ), + FeatureDescription( + icon: "network", + title: "Better networking", + description: "Connection and servers status." + ) + ] + ), ] private let lastVersion = versionDescriptions.last!.version @@ -423,6 +483,7 @@ func shouldShowWhatsNew() -> Bool { struct WhatsNewView: View { @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var theme: AppTheme @State var currentVersion = versionDescriptions.count - 1 @State var currentVersionNav = versionDescriptions.count - 1 var viaSettings = false @@ -431,35 +492,37 @@ struct WhatsNewView: View { VStack { TabView(selection: $currentVersion) { ForEach(Array(versionDescriptions.enumerated()), id: \.0) { (i, v) in - VStack(alignment: .leading, spacing: 16) { - Text("New in \(v.version)") - .font(.title) - .foregroundColor(.secondary) - .frame(maxWidth: .infinity) - .padding(.vertical) - ForEach(v.features, id: \.icon) { f in - featureDescription(f.icon, f.title, f.description) - .padding(.bottom, 8) - } - if let post = v.post { - Link(destination: post) { - HStack { - Text("Read more") - Image(systemName: "arrow.up.right.circle") + ScrollView { + VStack(alignment: .leading, spacing: 16) { + Text("New in \(v.version)") + .font(.title) + .foregroundColor(theme.colors.secondary) + .frame(maxWidth: .infinity) + .padding(.vertical) + ForEach(v.features, id: \.title) { f in + featureDescription(f) + .padding(.bottom, 8) + } + if let post = v.post { + Link(destination: post) { + HStack { + Text("Read more") + Image(systemName: "arrow.up.right.circle") + } } } - } - if !viaSettings { - Spacer() - Button("Ok") { - dismiss() + if !viaSettings { + Spacer() + Button("Ok") { + dismiss() + } + .font(.title3) + .frame(maxWidth: .infinity, alignment: .center) + Spacer() } - .font(.title3) - .frame(maxWidth: .infinity, alignment: .center) - Spacer() } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) } - .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .tag(i) } } @@ -473,18 +536,37 @@ struct WhatsNewView: View { } } - private func featureDescription(_ icon: String, _ title: LocalizedStringKey, _ description: LocalizedStringKey) -> some View { + private func featureDescription(_ f: FeatureDescription) -> some View { VStack(alignment: .leading, spacing: 4) { - HStack(alignment: .center, spacing: 4) { - Image(systemName: icon) - .symbolRenderingMode(.monochrome) - .foregroundColor(.secondary) - .frame(minWidth: 30, alignment: .center) - Text(title).font(.title3).bold() + if let icon = f.icon { + HStack(alignment: .center, spacing: 4) { + Image(systemName: icon) + .symbolRenderingMode(.monochrome) + .foregroundColor(theme.colors.secondary) + .frame(minWidth: 30, alignment: .center) + Text(f.title).font(.title3).bold() + } + } else { + Text(f.title).font(.title3).bold() + } + if let d = f.description { + Text(d) + .multilineTextAlignment(.leading) + .lineLimit(10) + } + if f.subfeatures.count > 0 { + ForEach(f.subfeatures, id: \.icon) { s in + HStack(alignment: .center, spacing: 4) { + Image(systemName: s.icon) + .symbolRenderingMode(.monochrome) + .foregroundColor(theme.colors.secondary) + .frame(minWidth: 30, alignment: .center) + Text(s.description) + .multilineTextAlignment(.leading) + .lineLimit(3) + } + } } - Text(description) - .multilineTextAlignment(.leading) - .lineLimit(10) } } diff --git a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift index 3059b049a3..be063334d3 100644 --- a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift +++ b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift @@ -12,6 +12,7 @@ import CodeScanner struct ConnectDesktopView: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss: DismissAction var viaSettings = false @AppStorage(DEFAULT_DEVICE_NAME_FOR_REMOTE_ACCESS) private var deviceName = UIDevice.current.name @@ -36,7 +37,7 @@ struct ConnectDesktopView: View { case badInvitationError case badVersionError(version: String?) case desktopDisconnectedError - case error(title: LocalizedStringKey, error: LocalizedStringKey = "") + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -159,7 +160,7 @@ struct ConnectDesktopView: View { case .desktopDisconnectedError: Alert(title: Text("Connection terminated")) case let .error(title, error): - Alert(title: Text(title), message: Text(error)) + mkAlert(title: title, message: error) } } .interactiveDismissDisabled(m.activeRemoteCtrl) @@ -167,7 +168,7 @@ struct ConnectDesktopView: View { private func connectDesktopView(showScanner: Bool = true) -> some View { List { - Section("This device name") { + Section(header: Text("This device name").foregroundColor(theme.colors.secondary)) { devicesView() } if showScanner { @@ -178,34 +179,40 @@ struct ConnectDesktopView: View { } } .navigationTitle("Connect to desktop") + .modifier(ThemedBackground(grouped: true)) } private func connectingDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> some View { - List { - Section("Connecting to desktop") { - ctrlDeviceNameText(session, rc) - ctrlDeviceVersionText(session) - } + ZStack { + List { + Section(header: Text("Connecting to desktop").foregroundColor(theme.colors.secondary)) { + ctrlDeviceNameText(session, rc) + ctrlDeviceVersionText(session) + } - if let sessCode = session.sessionCode { - Section("Session code") { - sessionCodeText(sessCode) + if let sessCode = session.sessionCode { + Section(header: Text("Session code").foregroundColor(theme.colors.secondary)) { + sessionCodeText(sessCode) + } + } + + Section { + disconnectButton() } } + .navigationTitle("Connecting to desktop") - Section { - disconnectButton() - } + ProgressView().scaleEffect(2) } - .navigationTitle("Connecting to desktop") + .modifier(ThemedBackground(grouped: true)) } private func searchingDesktopView() -> some View { List { - Section("This device name") { + Section(header: Text("This device name").foregroundColor(theme.colors.secondary)) { devicesView() } - Section("Found desktop") { + Section(header: Text("Found desktop").foregroundColor(theme.colors.secondary)) { Text("Waiting for desktop...").italic() Button { disconnectDesktop() @@ -215,14 +222,15 @@ struct ConnectDesktopView: View { } } .navigationTitle("Connecting to desktop") + .modifier(ThemedBackground(grouped: true)) } @ViewBuilder private func foundDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo, _ compatible: Bool) -> some View { let v = List { - Section("This device name") { + Section(header: Text("This device name").foregroundColor(theme.colors.secondary)) { devicesView() } - Section("Found desktop") { + Section(header: Text("Found desktop").foregroundColor(theme.colors.secondary)) { ctrlDeviceNameText(session, rc) ctrlDeviceVersionText(session) if !compatible { @@ -242,6 +250,7 @@ struct ConnectDesktopView: View { } } .navigationTitle("Found desktop") + .modifier(ThemedBackground(grouped: true)) if compatible && connectRemoteViaMulticastAuto { v.onAppear { confirmKnownDesktop(rc) } @@ -252,12 +261,12 @@ struct ConnectDesktopView: View { private func verifySessionView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?, _ sessCode: String) -> some View { List { - Section("Connected to desktop") { + Section(header: Text("Connected to desktop").foregroundColor(theme.colors.secondary)) { ctrlDeviceNameText(session, rc) ctrlDeviceVersionText(session) } - Section("Verify code with desktop") { + Section(header: Text("Verify code with desktop").foregroundColor(theme.colors.secondary)) { sessionCodeText(sessCode) Button { verifyDesktopSessionCode(sessCode) @@ -271,6 +280,7 @@ struct ConnectDesktopView: View { } } .navigationTitle("Verify connection") + .modifier(ThemedBackground(grouped: true)) } private func ctrlDeviceNameText(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> Text { @@ -292,13 +302,13 @@ struct ConnectDesktopView: View { private func activeSessionView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo) -> some View { List { - Section("Connected desktop") { + Section(header: Text("Connected desktop").foregroundColor(theme.colors.secondary)) { Text(rc.deviceViewName) ctrlDeviceVersionText(session) } if let sessCode = session.sessionCode { - Section("Session code") { + Section(header: Text("Session code").foregroundColor(theme.colors.secondary)) { sessionCodeText(sessCode) } } @@ -308,9 +318,11 @@ struct ConnectDesktopView: View { } footer: { // This is specific to iOS Text("Keep the app open to use it from desktop") + .foregroundColor(theme.colors.secondary) } } .navigationTitle("Connected to desktop") + .modifier(ThemedBackground(grouped: true)) } private func sessionCodeText(_ code: String) -> some View { @@ -331,19 +343,13 @@ struct ConnectDesktopView: View { } 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) + Section(header: Text("Scan QR code from desktop").foregroundColor(theme.colors.secondary)) { + ScannerInView(showQRCodeScanner: $showQRCodeScanner, processQRCode: processDesktopQRCode, scanMode: .oncePerCode) } } private func desktopAddressView() -> some View { - Section("Desktop address") { + Section(header: Text("Desktop address").foregroundColor(theme.colors.secondary)) { if sessionAddress.isEmpty { Button { sessionAddress = UIPasteboard.general.string ?? "" @@ -356,7 +362,7 @@ struct ConnectDesktopView: View { Text(sessionAddress).lineLimit(1) Spacer() Image(systemName: "multiply.circle.fill") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .onTapGesture { sessionAddress = "" } } } @@ -371,7 +377,7 @@ struct ConnectDesktopView: View { private func linkedDesktopsView() -> some View { List { - Section("Desktop devices") { + Section(header: Text("Desktop devices").foregroundColor(theme.colors.secondary)) { ForEach(remoteCtrls, id: \.remoteCtrlId) { rc in remoteCtrlView(rc) } @@ -382,7 +388,7 @@ struct ConnectDesktopView: View { } } - Section("Linked desktop options") { + Section(header: Text("Linked desktop options").foregroundColor(theme.colors.secondary)) { Toggle("Verify connections", isOn: $confirmRemoteSessions) Toggle("Discover via local network", isOn: $connectRemoteViaMulticast) if connectRemoteViaMulticast { @@ -391,6 +397,7 @@ struct ConnectDesktopView: View { } } .navigationTitle("Linked desktops") + .modifier(ThemedBackground(grouped: true)) } private func remoteCtrlView(_ rc: RemoteCtrlInfo) -> some View { diff --git a/apps/ios/Shared/Views/TerminalView.swift b/apps/ios/Shared/Views/TerminalView.swift index 94a8937db6..d209ced128 100644 --- a/apps/ios/Shared/Views/TerminalView.swift +++ b/apps/ios/Shared/Views/TerminalView.swift @@ -106,6 +106,7 @@ struct TerminalView: View { } .navigationViewStyle(.stack) .navigationTitle("Chat console") + .modifier(ThemedBackground()) } func scrollToBottom(_ proxy: ScrollViewProxy, animation: Animation = .default) { @@ -121,6 +122,7 @@ struct TerminalView: View { return ScrollView { Text(s.prefix(maxItemSize)) .padding() + .frame(maxWidth: .infinity) } .toolbar { ToolbarItem(placement: .navigationBarTrailing) { @@ -130,6 +132,7 @@ struct TerminalView: View { } } .onDisappear { terminalItem = nil } + .modifier(ThemedBackground()) } func consoleSendMessage() { diff --git a/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift b/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift index f027127db3..99c0a588eb 100644 --- a/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift @@ -24,37 +24,118 @@ enum NetworkSettingsAlert: Identifiable { } struct AdvancedNetworkSettings: View { + @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var theme: AppTheme + @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false + @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false @State private var netCfg = NetCfg.defaults @State private var currentNetCfg = NetCfg.defaults @State private var cfgLoaded = false @State private var enableKeepAlive = true @State private var keepAliveOpts = KeepAliveOpts.defaults @State private var showSettingsAlert: NetworkSettingsAlert? + @State private var onionHosts: OnionHosts = .no + @State private var showSaveDialog = false var body: some View { VStack { List { Section { - Button { - updateNetCfgView(NetCfg.defaults) - showSettingsAlert = .update + NavigationLink { + List { + Section { + SelectionListView(list: SMPProxyMode.values, selection: $netCfg.smpProxyMode) { mode in + netCfg.smpProxyMode = mode + } + } footer: { + Text(proxyModeInfo(netCfg.smpProxyMode)) + .font(.callout) + .foregroundColor(theme.colors.secondary) + } + } + .navigationTitle("Private routing") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.inline) } label: { - Text("Reset to defaults") + HStack { + Text("Private routing") + Spacer() + Text(netCfg.smpProxyMode.label) + } } - .disabled(currentNetCfg == NetCfg.defaults) - - Button { - updateNetCfgView(NetCfg.proxyDefaults) - showSettingsAlert = .update + + NavigationLink { + List { + Section { + SelectionListView(list: SMPProxyFallback.values, selection: $netCfg.smpProxyFallback) { mode in + netCfg.smpProxyFallback = mode + } + .disabled(netCfg.smpProxyMode == .never) + } footer: { + Text(proxyFallbackInfo(netCfg.smpProxyFallback)) + .font(.callout) + .foregroundColor(theme.colors.secondary) + } + } + .navigationTitle("Allow downgrade") + .modifier(ThemedBackground(grouped: true)) + .navigationBarTitleDisplayMode(.inline) } label: { - Text("Set timeouts for proxy/VPN") + HStack { + Text("Allow downgrade") + Spacer() + Text(netCfg.smpProxyFallback.label) + } } - .disabled(currentNetCfg == NetCfg.proxyDefaults) - timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [10_000000, 15_000000, 20_000000, 25_000000, 35_000000, 50_000000], label: secondsLabel) + Toggle("Show message status", isOn: $showSentViaProxy) + } header: { + Text("Private message routing") + .foregroundColor(theme.colors.secondary) + } footer: { + VStack(alignment: .leading) { + Text("To protect your IP address, private routing uses your SMP servers to deliver messages.") + if showSentViaProxy { + Text("Show → on messages sent via private routing.") + } + } + .foregroundColor(theme.colors.secondary) + } + + Section { + Picker("Use .onion hosts", selection: $onionHosts) { + ForEach(OnionHosts.values, id: \.self) { Text($0.text) } + } + .frame(height: 36) + } footer: { + Text(onionHostsInfo(onionHosts)) + .foregroundColor(theme.colors.secondary) + } + .onChange(of: onionHosts) { hosts in + if hosts != OnionHosts(netCfg: currentNetCfg) { + let (hostMode, requiredHostMode) = hosts.hostMode + netCfg.hostMode = hostMode + netCfg.requiredHostMode = requiredHostMode + } + } + + if developerTools { + Section { + Picker("Transport isolation", selection: $netCfg.sessionMode) { + ForEach(TransportSessionMode.values, id: \.self) { Text($0.text) } + } + .frame(height: 36) + } footer: { + Text(sessionModeInfo(netCfg.sessionMode)) + .foregroundColor(theme.colors.secondary) + } + } + + Section("TCP connection") { + timeoutSettingPicker("TCP connection timeout", selection: $netCfg.tcpConnectTimeout, values: [10_000000, 15_000000, 20_000000, 30_000000, 45_000000, 60_000000, 90_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: [2_500, 5_000, 10_000, 15_000, 20_000, 30_000], label: secondsLabel) - intSettingPicker("Receiving concurrency", selection: $netCfg.rcvConcurrency, values: [1, 2, 4, 8, 12, 16, 24], label: "") + // intSettingPicker("Receiving concurrency", selection: $netCfg.rcvConcurrency, values: [1, 2, 4, 8, 12, 16, 24], label: "") timeoutSettingPicker("PING interval", selection: $netCfg.smpPingInterval, values: [120_000000, 300_000000, 600_000000, 1200_000000, 2400_000000, 3600_000000], label: secondsLabel) intSettingPicker("PING count", selection: $netCfg.smpPingCount, values: [1, 2, 3, 5, 8], label: "") Toggle("Enable TCP keep-alive", isOn: $enableKeepAlive) @@ -69,25 +150,23 @@ struct AdvancedNetworkSettings: View { Text("TCP_KEEPINTVL") Text("TCP_KEEPCNT") } - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } - } header: { - Text("") - } footer: { - HStack { - Button { - updateNetCfgView(currentNetCfg) - } label: { - Label("Revert", systemImage: "arrow.counterclockwise").font(.callout) - } + } + + Section { + Button("Reset to defaults") { + updateNetCfgView(NetCfg.defaults) + } + .disabled(netCfg == NetCfg.defaults) - Spacer() - - Button { - showSettingsAlert = .update - } label: { - Label("Save", systemImage: "checkmark").font(.callout) - } + Button("Set timeouts for proxy/VPN") { + updateNetCfgView(netCfg.withProxyTimeouts) + } + .disabled(netCfg.hasProxyTimeouts) + + Button("Save and reconnect") { + showSettingsAlert = .update } .disabled(netCfg == currentNetCfg) } @@ -109,10 +188,10 @@ struct AdvancedNetworkSettings: View { switch a { case .update: return Alert( - title: Text("Update network settings?"), + title: Text("Update settings?"), message: Text("Updating settings will re-connect the client to all servers."), primaryButton: .default(Text("Ok")) { - saveNetCfg() + _ = saveNetCfg() }, secondaryButton: .cancel() ) @@ -123,23 +202,43 @@ struct AdvancedNetworkSettings: View { ) } } + .modifier(BackButton(disabled: Binding.constant(false)) { + if netCfg == currentNetCfg { + dismiss() + cfgLoaded = false + } else { + showSaveDialog = true + } + }) + .confirmationDialog("Update network settings?", isPresented: $showSaveDialog, titleVisibility: .visible) { + Button("Save and reconnect") { + if saveNetCfg() { + dismiss() + cfgLoaded = false + } + } + Button("Exit without saving") { dismiss() } + } } private func updateNetCfgView(_ cfg: NetCfg) { netCfg = cfg + onionHosts = OnionHosts(netCfg: netCfg) enableKeepAlive = netCfg.enableKeepAlive keepAliveOpts = netCfg.tcpKeepAlive ?? KeepAliveOpts.defaults } - private func saveNetCfg() { + private func saveNetCfg() -> Bool { do { try setNetworkConfig(netCfg) currentNetCfg = netCfg setNetCfg(netCfg) + return true } catch let error { let err = responseError(error) showSettingsAlert = .error(err: err) logger.error("\(err)") + return false } } @@ -162,6 +261,38 @@ struct AdvancedNetworkSettings: View { } .frame(height: 36) } + + private func onionHostsInfo(_ hosts: OnionHosts) -> LocalizedStringKey { + switch hosts { + case .no: return "Onion hosts will not be used." + case .prefer: return "Onion hosts will be used when available.\nRequires compatible VPN." + case .require: return "Onion hosts will be **required** for connection.\nRequires compatible VPN." + } + } + + private func sessionModeInfo(_ mode: TransportSessionMode) -> LocalizedStringKey { + switch mode { + case .user: return "A separate TCP connection will be used **for each chat profile you have in the app**." + case .entity: return "A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." + } + } + + private func proxyModeInfo(_ mode: SMPProxyMode) -> LocalizedStringKey { + switch mode { + case .always: return "Always use private routing." + case .unknown: return "Use private routing with unknown servers." + case .unprotected: return "Use private routing with unknown servers when IP address is not protected." + case .never: return "Do NOT use private routing." + } + } + + private func proxyFallbackInfo(_ proxyFallback: SMPProxyFallback) -> LocalizedStringKey { + switch proxyFallback { + case .allow: return "Send messages directly when your or destination server does not support private routing." + case .allowProtected: return "Send messages directly when IP address is protected and your or destination server does not support private routing." + case .prohibit: return "Do NOT send messages directly, even if your or destination server does not support private routing." + } + } } struct AdvancedNetworkSettings_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/UserSettings/AppSettings.swift b/apps/ios/Shared/Views/UserSettings/AppSettings.swift index ba192b333c..6247777bf2 100644 --- a/apps/ios/Shared/Views/UserSettings/AppSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AppSettings.swift @@ -23,14 +23,19 @@ extension AppSettings { setNetCfg(val) } if let val = privacyEncryptLocalFiles { privacyEncryptLocalFilesGroupDefault.set(val) } + if let val = privacyAskToApproveRelays { privacyAskToApproveRelaysGroupDefault.set(val) } if let val = privacyAcceptImages { privacyAcceptImagesGroupDefault.set(val) def.setValue(val, forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES) } - if let val = privacyLinkPreviews { def.setValue(val, forKey: DEFAULT_PRIVACY_LINK_PREVIEWS) } + if let val = privacyLinkPreviews { + privacyLinkPreviewsGroupDefault.set(val) + def.setValue(val, forKey: DEFAULT_PRIVACY_LINK_PREVIEWS) + } if let val = privacyShowChatPreviews { def.setValue(val, forKey: DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) } if let val = privacySaveLastDraft { def.setValue(val, forKey: DEFAULT_PRIVACY_SAVE_LAST_DRAFT) } if let val = privacyProtectScreen { def.setValue(val, forKey: DEFAULT_PRIVACY_PROTECT_SCREEN) } + if let val = privacyMediaBlurRadius { def.setValue(val, forKey: DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) } if let val = notificationMode { ChatModel.shared.notificationMode = val.toNotificationsMode() } if let val = notificationPreviewMode { ntfPreviewModeGroupDefault.set(val) } if let val = webrtcPolicyRelay { def.setValue(val, forKey: DEFAULT_WEBRTC_POLICY_RELAY) } @@ -43,6 +48,15 @@ extension AppSettings { if let val = androidCallOnLockScreen { def.setValue(val.rawValue, forKey: ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN) } if let val = iosCallKitEnabled { callKitEnabledGroupDefault.set(val) } if let val = iosCallKitCallsInRecents { def.setValue(val, forKey: DEFAULT_CALL_KIT_CALLS_IN_RECENTS) } + if let val = uiProfileImageCornerRadius { + profileImageCornerRadiusGroupDefault.set(val) + def.setValue(val, forKey: DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) + } + if let val = uiColorScheme { def.setValue(val, forKey: DEFAULT_CURRENT_THEME) } + if let val = uiDarkColorScheme { def.setValue(val, forKey: DEFAULT_SYSTEM_DARK_THEME) } + if let val = uiCurrentThemeIds { def.setValue(val, forKey: DEFAULT_CURRENT_THEME_IDS) } + if let val = uiThemes { def.setValue(val.skipDuplicates(), forKey: DEFAULT_THEME_OVERRIDES) } + if let val = oneHandUI { groupDefaults.setValue(val, forKey: GROUP_DEFAULT_ONE_HAND_UI) } } public static var current: AppSettings { @@ -50,11 +64,13 @@ extension AppSettings { var c = AppSettings.defaults c.networkConfig = getNetCfg() c.privacyEncryptLocalFiles = privacyEncryptLocalFilesGroupDefault.get() + c.privacyAskToApproveRelays = privacyAskToApproveRelaysGroupDefault.get() c.privacyAcceptImages = privacyAcceptImagesGroupDefault.get() c.privacyLinkPreviews = def.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS) c.privacyShowChatPreviews = def.bool(forKey: DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) c.privacySaveLastDraft = def.bool(forKey: DEFAULT_PRIVACY_SAVE_LAST_DRAFT) c.privacyProtectScreen = def.bool(forKey: DEFAULT_PRIVACY_PROTECT_SCREEN) + c.privacyMediaBlurRadius = def.integer(forKey: DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) c.notificationMode = AppSettingsNotificationMode.from(ChatModel.shared.notificationMode) c.notificationPreviewMode = ntfPreviewModeGroupDefault.get() c.webrtcPolicyRelay = def.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY) @@ -67,6 +83,12 @@ extension AppSettings { c.androidCallOnLockScreen = AppSettingsLockScreenCalls(rawValue: def.string(forKey: ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN)!) c.iosCallKitEnabled = callKitEnabledGroupDefault.get() c.iosCallKitCallsInRecents = def.bool(forKey: DEFAULT_CALL_KIT_CALLS_IN_RECENTS) + c.uiProfileImageCornerRadius = def.double(forKey: DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) + c.uiColorScheme = currentThemeDefault.get() + c.uiDarkColorScheme = systemDarkThemeDefault.get() + c.uiCurrentThemeIds = currentThemeIdsDefault.get() + c.uiThemes = themeOverridesDefault.get() + c.oneHandUI = groupDefaults.bool(forKey: GROUP_DEFAULT_ONE_HAND_UI) return c } } diff --git a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift index b91d2c9369..73a789f108 100644 --- a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift @@ -7,12 +7,17 @@ // import SwiftUI +import SimpleXChat +import Yams -let defaultAccentColor = CGColor.init(red: 0, green: 0.533, blue: 1, alpha: 1) +let colorModesLocalized: [LocalizedStringKey] = ["System", "Light", "Dark"] +let colorModesNames: [DefaultThemeMode?] = [nil, DefaultThemeMode.light, DefaultThemeMode.dark] -let interfaceStyles: [UIUserInterfaceStyle] = [.unspecified, .light, .dark] +let darkThemesLocalized: [LocalizedStringKey] = ["Dark", "SimpleX", "Black"] +let darkThemesNames: [String] = [DefaultTheme.DARK.themeName, DefaultTheme.SIMPLEX.themeName, DefaultTheme.BLACK.themeName] -let interfaceStyleNames: [LocalizedStringKey] = ["System", "Light", "Dark"] +let darkThemesWithoutBlackLocalized: [LocalizedStringKey] = ["Dark", "SimpleX"] +let darkThemesWithoutBlackNames: [String] = [DefaultTheme.DARK.themeName, DefaultTheme.SIMPLEX.themeName] let appSettingsURL = URL(string: UIApplication.openSettingsURLString)! @@ -20,11 +25,31 @@ struct AppearanceSettings: View { @EnvironmentObject var m: ChatModel @Environment(\.colorScheme) var colorScheme @EnvironmentObject var sceneDelegate: SceneDelegate + @EnvironmentObject var theme: AppTheme @State private var iconLightTapped = false @State private var iconDarkTapped = false - @State private var userInterfaceStyle = getUserInterfaceStyleDefault() - @State private var uiTintColor = getUIAccentColorDefault() + @State private var colorMode: DefaultThemeMode? = { + if currentThemeDefault.get() == DefaultTheme.SYSTEM_THEME_NAME { nil as DefaultThemeMode? } else { CurrentColors.base.mode } + }() + @State private var darkModeTheme: String = UserDefaults.standard.string(forKey: DEFAULT_SYSTEM_DARK_THEME) ?? DefaultTheme.DARK.themeName @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var profileImageCornerRadius = defaultProfileImageCorner + @AppStorage(GROUP_DEFAULT_ONE_HAND_UI, store: groupDefaults) private var oneHandUI = true + @AppStorage(DEFAULT_TOOLBAR_MATERIAL) private var toolbarMaterial = ToolbarMaterial.defaultMaterial + + @State var themeUserDestination: (Int64, ThemeModeOverrides?)? = { + if let currentUser = ChatModel.shared.currentUser, let uiThemes = currentUser.uiThemes, uiThemes.preferredMode(!CurrentColors.colors.isLight) != nil { + (currentUser.userId, uiThemes) + } else { + nil + } + }() + + @State var perUserTheme: ThemeModeOverride = { + ChatModel.shared.currentUser?.uiThemes?.preferredMode(!CurrentColors.colors.isLight) ?? ThemeModeOverride(mode: CurrentColors.base.mode) + }() + + @State var showImageImporter: Bool = false + @State var customizeThemeIsOpen: Bool = false var body: some View { VStack{ @@ -39,15 +64,122 @@ struct AppearanceSettings: View { } } - Section("App icon") { - HStack { - updateAppIcon(image: "icon-light", icon: nil, tapped: $iconLightTapped) - Spacer().frame(width: 16) - updateAppIcon(image: "icon-dark", icon: "DarkAppIcon", tapped: $iconDarkTapped) + Section("Chat list") { + Toggle("Reachable chat toolbar", isOn: $oneHandUI) + Picker("Toolbar opacity", selection: $toolbarMaterial) { + ForEach(ToolbarMaterial.allCases, id: \.rawValue) { tm in + Text(tm.text).tag(tm.rawValue) + } + } + .frame(height: 36) + } + + Section { + ThemeDestinationPicker(themeUserDestination: $themeUserDestination, themeUserDest: themeUserDestination?.0, customizeThemeIsOpen: $customizeThemeIsOpen) + + WallpaperPresetSelector( + selectedWallpaper: theme.wallpaper.type, + currentColors: currentColors, + onChooseType: onChooseType + ) + .padding(.bottom, 10) + .listRowInsets(.init()) + .listRowBackground(Color.clear) + .modifier(WallpaperImporter(showImageImporter: $showImageImporter, onChooseImage: { image in + if let filename = saveWallpaperFile(image: image) { + if themeUserDestination == nil, case let WallpaperType.image(filename, _, _) = theme.wallpaper.type { + removeWallpaperFile(fileName: filename) + } else if let type = perUserTheme.type, case let WallpaperType.image(filename, _, _) = type { + removeWallpaperFile(fileName: filename) + } + onTypeChange(WallpaperType.image(filename, 1, WallpaperScaleType.fill)) + } + })) + + if case let WallpaperType.image(filename, _, _) = theme.wallpaper.type, (themeUserDestination == nil || perUserTheme.wallpaper?.imageFile != nil) { + Button { + if themeUserDestination == nil { + let defaultActiveTheme = ThemeManager.defaultActiveTheme(themeOverridesDefault.get()) + ThemeManager.saveAndApplyWallpaper(theme.base, nil, themeOverridesDefault) + ThemeManager.removeTheme(defaultActiveTheme?.themeId) + removeWallpaperFile(fileName: filename) + } else { + removeUserThemeModeOverrides($themeUserDestination, $perUserTheme) + } + saveThemeToDatabase(themeUserDestination) + } label: { + Text("Remove image") + .foregroundColor(theme.colors.primary) + } + .listRowBackground(Color.clear) + } + + Picker("Color mode", selection: $colorMode) { + ForEach(Array(colorModesNames.enumerated()), id: \.element) { index, mode in + Text(colorModesLocalized[index]) + } + } + .frame(height: 36) + Picker("Dark mode colors", selection: $darkModeTheme) { + if theme.base == .BLACK || themeOverridesDefault.get().contains(where: { $0.base == .BLACK }) { + ForEach(Array(darkThemesNames.enumerated()), id: \.element) { index, darkTheme in + Text(darkThemesLocalized[index]) + } + } else { + ForEach(Array(darkThemesWithoutBlackNames.enumerated()), id: \.element) { index, darkTheme in + Text(darkThemesLocalized[index]) + } + } + } + .frame(height: 36) + + NavigationLink { + let userId = themeUserDestination?.0 + if let userId { + UserWallpaperEditorSheet(userId: userId) + .onAppear { + customizeThemeIsOpen = true + } + } else { + CustomizeThemeView(onChooseType: onChooseType) + .navigationTitle("Customize theme") + .modifier(ThemedBackground(grouped: true)) + .onAppear { + customizeThemeIsOpen = true + } + } + } label: { + Text("Customize theme") + } + } header: { + Text("Themes") + .foregroundColor(theme.colors.secondary) + } + .onChange(of: profileImageCornerRadius) { cornerRadius in + profileImageCornerRadiusGroupDefault.set(cornerRadius) + saveThemeToDatabase(nil) + } + .onChange(of: colorMode) { mode in + guard let mode else { + ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME) + return + } + if case DefaultThemeMode.light = mode { + ThemeManager.applyTheme(DefaultTheme.LIGHT.themeName) + } else if case DefaultThemeMode.dark = mode { + ThemeManager.applyTheme(systemDarkThemeDefault.get()) + } + } + .onChange(of: darkModeTheme) { darkTheme in + ThemeManager.changeDarkTheme(darkTheme) + if currentThemeDefault.get() == DefaultTheme.SYSTEM_THEME_NAME { + ThemeManager.applyTheme(currentThemeDefault.get()) + } else if currentThemeDefault.get() != DefaultTheme.LIGHT.themeName { + ThemeManager.applyTheme(systemDarkThemeDefault.get()) } } - Section("Profile images") { + Section(header: Text("Profile images").foregroundColor(theme.colors.secondary)) { HStack(spacing: 16) { if let img = m.currentUser?.image, img != "" { ProfileImage(imageStr: img, size: 60) @@ -61,37 +193,91 @@ struct AppearanceSettings: View { step: 2.5 ) } - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } - Section { - Picker("Theme", selection: $userInterfaceStyle) { - ForEach(interfaceStyles, id: \.self) { style in - Text(interfaceStyleNames[interfaceStyles.firstIndex(of: style) ?? 0]) - } + Section(header: Text("App icon").foregroundColor(theme.colors.secondary)) { + HStack { + updateAppIcon(image: "icon-light", icon: nil, tapped: $iconLightTapped) + Spacer().frame(width: 16) + updateAppIcon(image: "icon-dark", icon: "DarkAppIcon", tapped: $iconDarkTapped) } - .frame(height: 36) - ColorPicker("Accent color", selection: $uiTintColor, supportsOpacity: false) - } header: { - Text("Colors") - } footer: { - Button { - uiTintColor = defaultAccentColor - setUIAccentColorDefault(defaultAccentColor) - } label: { - Text("Reset colors").font(.callout) - } - } - .onChange(of: userInterfaceStyle) { _ in - sceneDelegate.window?.overrideUserInterfaceStyle = userInterfaceStyle - setUserInterfaceStyleDefault(userInterfaceStyle) - } - .onChange(of: uiTintColor) { _ in - sceneDelegate.window?.tintColor = UIColor(cgColor: uiTintColor) - setUIAccentColorDefault(uiTintColor) } } } + .onAppear { + customizeThemeIsOpen = false + } + } + + private func updateThemeUserDestination() { + if let dest = themeUserDestination { + var (userId, themes) = dest + themes = themes ?? ThemeModeOverrides() + if case DefaultThemeMode.light = perUserTheme.mode { + themes?.light = perUserTheme + } else { + themes?.dark = perUserTheme + } + themeUserDestination = (userId, themes) + } + } + + private func onTypeCopyFromSameTheme(_ type: WallpaperType?) -> Bool { + if themeUserDestination == nil { + ThemeManager.saveAndApplyWallpaper(theme.base, type, themeOverridesDefault) + } else { + var wallpaperFiles = Set([perUserTheme.wallpaper?.imageFile]) + _ = ThemeManager.copyFromSameThemeOverrides(type, nil, $perUserTheme) + wallpaperFiles.remove(perUserTheme.wallpaper?.imageFile) + wallpaperFiles.forEach(removeWallpaperFile) + updateThemeUserDestination() + } + saveThemeToDatabase(themeUserDestination) + return true + } + + private func onTypeChange(_ type: WallpaperType?) { + if themeUserDestination == nil { + ThemeManager.saveAndApplyWallpaper(theme.base, type, themeOverridesDefault) + } else { + ThemeManager.applyWallpaper(type, $perUserTheme) + updateThemeUserDestination() + } + saveThemeToDatabase(themeUserDestination) + } + + private func currentColors(_ type: WallpaperType?) -> ThemeManager.ActiveTheme { + // If applying for : + // - all themes: no overrides needed + // - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected + let perUserOverride: ThemeModeOverrides? = themeUserDestination == nil + ? nil + : theme.wallpaper.type.sameType(type) + ? m.currentUser?.uiThemes + : nil + return ThemeManager.currentColors(type, nil, perUserOverride, themeOverridesDefault.get()) + } + + private func onChooseType(_ type: WallpaperType?) { + // don't have image in parent or already selected wallpaper with custom image + if let type, case WallpaperType.image = type { + if case WallpaperType.image = theme.wallpaper.type, themeUserDestination?.1 != nil { + showImageImporter = true + } else if currentColors(type).wallpaper.type.image == nil { + showImageImporter = true + } else if currentColors(type).wallpaper.type.image != nil, case WallpaperType.image = theme.wallpaper.type, themeUserDestination == nil { + showImageImporter = true + } else if themeUserDestination == nil { + onTypeChange(currentColors(type).wallpaper.type) + } else { + _ = onTypeCopyFromSameTheme(currentColors(type).wallpaper.type) + } + } else if (themeUserDestination != nil && themeUserDestination?.1?.preferredMode(!CurrentColors.colors.isLight)?.type != type) || theme.wallpaper.type != type { + _ = onTypeCopyFromSameTheme(type) + } else { + onTypeChange(type) + } } private var currentLanguage: String { @@ -117,6 +303,774 @@ struct AppearanceSettings: View { } } +enum ToolbarMaterial: String, CaseIterable { + case bar + case ultraThin + case thin + case regular + case thick + case ultraThick + + static func material(_ s: String) -> Material { + ToolbarMaterial(rawValue: s)?.material ?? Material.bar + } + + static let defaultMaterial: String = ToolbarMaterial.regular.rawValue + + var material: Material { + switch self { + case .bar: .bar + case .ultraThin: .ultraThin + case .thin: .thin + case .regular: .regular + case .thick: .thick + case .ultraThick: .ultraThick + } + } + + var text: String { + switch self { + case .bar: "System" + case .ultraThin: "Ultra thin" + case .thin: "Thin" + case .regular: "Regular" + case .thick: "Thick" + case .ultraThick: "Ultra thick" + } + } +} + +struct ChatThemePreview: View { + @EnvironmentObject var theme: AppTheme + var base: DefaultTheme + var wallpaperType: WallpaperType? + var backgroundColor: Color? + var tintColor: Color? + var withMessages: Bool = true + + var body: some View { + let themeBackgroundColor = theme.colors.background + let backgroundColor = backgroundColor ?? wallpaperType?.defaultBackgroundColor(theme.base, theme.colors.background) + let tintColor = tintColor ?? wallpaperType?.defaultTintColor(theme.base) + let view = VStack { + if withMessages { + let alice = ChatItem.getSample(1, CIDirection.directRcv, Date.now, NSLocalizedString("Good afternoon!", comment: "message preview")) + let bob = ChatItem.getSample(2, CIDirection.directSnd, Date.now, NSLocalizedString("Good morning!", comment: "message preview"), quotedItem: CIQuote.getSample(alice.id, alice.meta.itemTs, alice.content.text, chatDir: alice.chatDir)) + HStack { + ChatItemView(chat: Chat.sampleData, chatItem: alice, revealed: Binding.constant(false)) + .modifier(ChatItemClipped()) + Spacer() + } + HStack { + Spacer() + ChatItemView(chat: Chat.sampleData, chatItem: bob, revealed: Binding.constant(false)) + .modifier(ChatItemClipped()) + .frame(alignment: .trailing) + } + } else { + Rectangle().fill(.clear) + } + } + .padding(10) + .frame(maxWidth: .infinity) + + if let wallpaperType, let wallpaperImage = wallpaperType.image, let backgroundColor, let tintColor { + view.modifier(ChatViewBackground(image: wallpaperImage, imageType: wallpaperType, background: backgroundColor, tint: tintColor)) + } else { + view.background(themeBackgroundColor) + } + } +} + +struct WallpaperPresetSelector: View { + @EnvironmentObject var theme: AppTheme + var selectedWallpaper: WallpaperType? + var activeBackgroundColor: Color? = nil + var activeTintColor: Color? = nil + var currentColors: (WallpaperType?) -> ThemeManager.ActiveTheme + var onChooseType: (WallpaperType?) -> Void + let width: Double = 80 + let height: Double = 80 + let backgrounds = PresetWallpaper.allCases + + private let cornerRadius: Double = 22.5 + + var baseTheme: DefaultTheme { theme.base } + + var body: some View { + VStack { + ChatThemePreview( + base: theme.base, + wallpaperType: selectedWallpaper, + backgroundColor: activeBackgroundColor ?? theme.wallpaper.background, + tintColor: activeTintColor ?? theme.wallpaper.tint + ) + .environmentObject(currentColors(selectedWallpaper).toAppTheme()) + ScrollView(.horizontal, showsIndicators: false) { + HStack { + BackgroundItem(nil) + ForEach(backgrounds, id: \.self) { background in + BackgroundItem(background) + } + OwnBackgroundItem(selectedWallpaper) + } + } + } + } + + func plus() -> some View { + Image(systemName: "plus") + .tint(theme.colors.primary) + .frame(width: 25, height: 25) + } + + func BackgroundItem(_ background: PresetWallpaper?) -> some View { + let checked = (background == nil && (selectedWallpaper == nil || selectedWallpaper?.isEmpty == true)) || selectedWallpaper?.samePreset(other: background) == true + let type = background?.toType(baseTheme, checked ? selectedWallpaper?.scale : nil) + let overrides = currentColors(type).toAppTheme() + return ZStack { + if let type { + ChatThemePreview( + base: baseTheme, + wallpaperType: type, + backgroundColor: checked ? activeBackgroundColor ?? overrides.wallpaper.background : overrides.wallpaper.background, + tintColor: checked ? activeTintColor ?? overrides.wallpaper.tint : overrides.wallpaper.tint, + withMessages: false + ) + .environmentObject(overrides) + } else { + Rectangle().fill(overrides.colors.background) + } + } + .frame(width: CGFloat(width), height: CGFloat(height)) + .clipShape(RoundedRectangle(cornerRadius: width / 100 * cornerRadius)) + .overlay(RoundedRectangle(cornerRadius: width / 100 * cornerRadius) + .strokeBorder(checked ? theme.colors.primary.opacity(0.8) : theme.colors.onBackground.opacity(isInDarkTheme() ? 0.2 : 0.1), lineWidth: 1) + ) + .onTapGesture { + onChooseType(background?.toType(baseTheme)) + } + } + + func OwnBackgroundItem(_ type: WallpaperType?) -> some View { + let overrides = currentColors(WallpaperType.image("", nil, nil)) + let appWallpaper = overrides.wallpaper + let backgroundColor = appWallpaper.background + let tintColor = appWallpaper.tint + let wallpaperImage = appWallpaper.type.image + let checked = if let type, case WallpaperType.image = type, wallpaperImage != nil { true } else { false } + let borderColor = if let type, case WallpaperType.image = type { theme.colors.primary.opacity(0.8) } else { theme.colors.onBackground.opacity(0.1) } + return ZStack { + if checked || wallpaperImage != nil { + ChatThemePreview( + base: baseTheme, + wallpaperType: checked ? type : appWallpaper.type, + backgroundColor: checked ? activeBackgroundColor ?? backgroundColor : backgroundColor, + tintColor: checked ? activeTintColor ?? tintColor : tintColor, + withMessages: false + ) + .environmentObject(currentColors(type).toAppTheme()) + } else { + plus() + } + } + .frame(width: width, height: height) + .clipShape(RoundedRectangle(cornerRadius: width / 100 * cornerRadius)) + .overlay(RoundedRectangle(cornerRadius: width / 100 * cornerRadius) + .strokeBorder(borderColor, lineWidth: 1) + ) + .onTapGesture { + onChooseType(WallpaperType.image("", nil, nil)) + } + } +} + +struct CustomizeThemeView: View { + @EnvironmentObject var theme: AppTheme + var onChooseType: (WallpaperType?) -> Void + @State private var showFileImporter = false + + var body: some View { + List { + let wallpaperImage = theme.wallpaper.type.image + let wallpaperType = theme.wallpaper.type + let baseTheme = theme.base + + let editColor: (ThemeColor) -> Binding = { name in + editColorBinding( + name: name, + wallpaperType: wallpaperType, + wallpaperImage: wallpaperImage, + theme: theme, + onColorChange: { color in + updateBackendTask.cancel() + updateBackendTask = Task { + if (try? await Task.sleep(nanoseconds: 200_000000)) != nil { + ThemeManager.saveAndApplyThemeColor(baseTheme, name, color) + saveThemeToDatabase(nil) + } + } + }) + } + WallpaperPresetSelector( + selectedWallpaper: wallpaperType, + currentColors: { type in + ThemeManager.currentColors(type, nil, nil, themeOverridesDefault.get()) + }, + onChooseType: onChooseType + ) + .listRowInsets(.init()) + .listRowBackground(Color.clear) + + if case let WallpaperType.image(filename, _, _) = theme.wallpaper.type { + Button { + let defaultActiveTheme = ThemeManager.defaultActiveTheme(themeOverridesDefault.get()) + ThemeManager.saveAndApplyWallpaper(baseTheme, nil, themeOverridesDefault) + ThemeManager.removeTheme(defaultActiveTheme?.themeId) + removeWallpaperFile(fileName: filename) + saveThemeToDatabase(nil) + } label: { + Text("Remove image") + .foregroundColor(theme.colors.primary) + } + .listRowBackground(Color.clear) + } + + Section { + WallpaperSetupView( + wallpaperType: wallpaperType, + base: baseTheme, + initialWallpaper: theme.wallpaper, + editColor: { name in + editColor(name) + }, + onTypeChange: { type in + ThemeManager.saveAndApplyWallpaper(baseTheme, type, themeOverridesDefault) + updateBackendTask.cancel() + updateBackendTask = Task { + if (try? await Task.sleep(nanoseconds: 200_000000)) != nil { + saveThemeToDatabase(nil) + } + } + } + ) + } header: { + Text("Chat colors") + .foregroundColor(theme.colors.secondary) + } + + CustomizeThemeColorsSection(editColor: editColor) + + let currentOverrides = ThemeManager.defaultActiveTheme(themeOverridesDefault.get()) + let canResetColors = theme.base.hasChangedAnyColor(currentOverrides) + if canResetColors { + Button { + ThemeManager.resetAllThemeColors() + saveThemeToDatabase(nil) + } label: { + Text("Reset colors").font(.callout).foregroundColor(theme.colors.primary) + } + } + + ImportExportThemeSection(perChat: nil, perUser: nil, save: { theme in + ThemeManager.saveAndApplyThemeOverrides(theme) + saveThemeToDatabase(nil) + }) + } + /// When changing app theme, user overrides are hidden. User overrides will be returned back after closing Appearance screen, see ThemeDestinationPicker() + .interactiveDismissDisabled(true) + } +} + +struct ImportExportThemeSection: View { + @EnvironmentObject var theme: AppTheme + var perChat: ThemeModeOverride? + var perUser: ThemeModeOverrides? + var save: (ThemeOverrides) -> Void + @State private var showFileImporter = false + + var body: some View { + Section { + Button { + let overrides = ThemeManager.currentThemeOverridesForExport(nil, perChat, perUser) + do { + let encoded = try encodeThemeOverrides(overrides) + var lines = encoded.split(separator: "\n") + // Removing theme id without using custom serializer or data class + lines.remove(at: 0) + let theme = lines.joined(separator: "\n") + let tempUrl = getTempFilesDirectory().appendingPathComponent("simplex.theme") + try? FileManager.default.removeItem(at: tempUrl) + if FileManager.default.createFile(atPath: tempUrl.path, contents: theme.data(using: .utf8)) { + showShareSheet(items: [tempUrl]) + } + } catch { + AlertManager.shared.showAlertMsg(title: "Error", message: "Error exporting theme: \(error.localizedDescription)") + } + } label: { + Text("Export theme").foregroundColor(theme.colors.primary) + } + Button { + showFileImporter = true + } label: { + Text("Import theme").foregroundColor(theme.colors.primary) + } + .fileImporter( + isPresented: $showFileImporter, + allowedContentTypes: [.data/*.plainText*/], + allowsMultipleSelection: false + ) { result in + if case let .success(files) = result, let fileURL = files.first { + do { + var fileSize: Int? = nil + if fileURL.startAccessingSecurityScopedResource() { + let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey]) + fileSize = resourceValues.fileSize + } + if let fileSize = fileSize, + // Same as Android/desktop + fileSize <= 5_500_000 { + if let string = try? String(contentsOf: fileURL, encoding: .utf8), let theme: ThemeOverrides = decodeYAML("themeId: \(UUID().uuidString)\n" + string) { + save(theme) + logger.error("Saved theme from file") + } else { + logger.error("Error decoding theme file") + } + fileURL.stopAccessingSecurityScopedResource() + } else { + fileURL.stopAccessingSecurityScopedResource() + let prettyMaxFileSize = ByteCountFormatter.string(fromByteCount: 5_500_000, countStyle: .binary) + AlertManager.shared.showAlertMsg( + title: "Large file!", + message: "Currently maximum supported file size is \(prettyMaxFileSize)." + ) + } + } catch { + logger.error("Appearance fileImporter error \(error.localizedDescription)") + } + } + } + } + } +} + +struct UserWallpaperEditorSheet: View { + @Environment(\.dismiss) var dismiss + @EnvironmentObject var theme: AppTheme + @State var userId: Int64 + @State private var globalThemeUsed: Bool = false + + @State private var themes = ChatModel.shared.currentUser?.uiThemes ?? ThemeModeOverrides() + + var body: some View { + let preferred = themes.preferredMode(!theme.colors.isLight) + let initialTheme = preferred ?? ThemeManager.defaultActiveTheme(ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + UserWallpaperEditor( + initialTheme: initialTheme, + themeModeOverride: initialTheme, + applyToMode: themes.light == themes.dark ? nil : initialTheme.mode, + globalThemeUsed: $globalThemeUsed, + save: { applyToMode, newTheme in + updateBackendTask.cancel() + updateBackendTask = Task { + let themes = ChatModel.shared.currentUser?.uiThemes ?? ThemeModeOverrides() + let initialTheme = themes.preferredMode(!theme.colors.isLight) ?? ThemeManager.defaultActiveTheme(ChatModel.shared.currentUser?.uiThemes, themeOverridesDefault.get()) + + await save( + applyToMode, + newTheme, + themes, + userId, + realtimeUpdate: + initialTheme.wallpaper?.preset != newTheme?.wallpaper?.preset || + initialTheme.wallpaper?.imageFile != newTheme?.wallpaper?.imageFile || + initialTheme.wallpaper?.scale != newTheme?.wallpaper?.scale || + initialTheme.wallpaper?.scaleType != newTheme?.wallpaper?.scaleType + ) + } + } + ) + .navigationTitle("Profile theme") + .modifier(ThemedBackground(grouped: true)) + .onAppear { + globalThemeUsed = preferred == nil + } + .onChange(of: theme.base.mode) { _ in + globalThemeUsed = (ChatModel.shared.currentUser?.uiThemes ?? ThemeModeOverrides()).preferredMode(!theme.colors.isLight) == nil + } + .onChange(of: ChatModel.shared.currentUser?.userId) { _ in + dismiss() + } + } + + private func save( + _ applyToMode: DefaultThemeMode?, + _ newTheme: ThemeModeOverride?, + _ themes: ThemeModeOverrides?, + _ userId: Int64, + realtimeUpdate: Bool + ) async { + let unchangedThemes: ThemeModeOverrides = themes ?? ThemeModeOverrides() + var wallpaperFiles = Set([unchangedThemes.light?.wallpaper?.imageFile, unchangedThemes.dark?.wallpaper?.imageFile]) + var changedThemes: ThemeModeOverrides? = unchangedThemes + let light: ThemeModeOverride? = if let newTheme { + ThemeModeOverride(mode: DefaultThemeMode.light, colors: newTheme.colors, wallpaper: newTheme.wallpaper?.withFilledWallpaperPath()) + } else { + nil + } + let dark: ThemeModeOverride? = if let newTheme { + ThemeModeOverride(mode: DefaultThemeMode.dark, colors: newTheme.colors, wallpaper: newTheme.wallpaper?.withFilledWallpaperPath()) + } else { + nil + } + + if let applyToMode { + switch applyToMode { + case DefaultThemeMode.light: + changedThemes?.light = light + case DefaultThemeMode.dark: + changedThemes?.dark = dark + } + } else { + changedThemes?.light = light + changedThemes?.dark = dark + } + if changedThemes?.light != nil || changedThemes?.dark != nil { + let light = changedThemes?.light + let dark = changedThemes?.dark + let currentMode = CurrentColors.base.mode + // same image file for both modes, copy image to make them as different files + if var light, var dark, let lightWallpaper = light.wallpaper, let darkWallpaper = dark.wallpaper, let lightImageFile = lightWallpaper.imageFile, let darkImageFile = darkWallpaper.imageFile, lightWallpaper.imageFile == darkWallpaper.imageFile { + let imageFile = if currentMode == DefaultThemeMode.light { + darkImageFile + } else { + lightImageFile + } + let filePath = saveWallpaperFile(url: getWallpaperFilePath(imageFile)) + if currentMode == DefaultThemeMode.light { + dark.wallpaper?.imageFile = filePath + changedThemes = ThemeModeOverrides(light: changedThemes?.light, dark: dark) + } else { + light.wallpaper?.imageFile = filePath + changedThemes = ThemeModeOverrides(light: light, dark: changedThemes?.dark) + } + } + } else { + changedThemes = nil + } + wallpaperFiles.remove(changedThemes?.light?.wallpaper?.imageFile) + wallpaperFiles.remove(changedThemes?.dark?.wallpaper?.imageFile) + wallpaperFiles.forEach(removeWallpaperFile) + + let oldThemes = ChatModel.shared.currentUser?.uiThemes + let changedThemesConstant = changedThemes + if realtimeUpdate { + await MainActor.run { + ChatModel.shared.updateCurrentUserUiThemes(uiThemes: changedThemesConstant) + } + } + do { + try await Task.sleep(nanoseconds: 200_000000) + } catch { + return + } + if !realtimeUpdate { + await MainActor.run { + ChatModel.shared.updateCurrentUserUiThemes(uiThemes: changedThemesConstant) + } + } + + if await !apiSetUserUIThemes(userId: userId, themes: changedThemesConstant) { + await MainActor.run { + // If failed to apply for some reason return the old themes + ChatModel.shared.updateCurrentUserUiThemes(uiThemes: oldThemes) + } + } + } +} + +struct ThemeDestinationPicker: View { + @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme + @Binding var themeUserDestination: (Int64, ThemeModeOverrides?)? + @State var themeUserDest: Int64? + @Binding var customizeThemeIsOpen: Bool + + var body: some View { + let values = [(nil, "All profiles")] + m.users.filter { $0.user.activeUser }.map { ($0.user.userId, $0.user.chatViewName)} + + if values.contains(where: { (userId, text) in userId == themeUserDestination?.0 }) { + Picker("Apply to", selection: $themeUserDest) { + ForEach(values, id: \.0) { (_, text) in + Text(text) + } + } + .frame(height: 36) + .onChange(of: themeUserDest) { userId in + themeUserDest = userId + if let userId { + themeUserDestination = (userId, m.users.first { $0.user.userId == userId }?.user.uiThemes) + } else { + themeUserDestination = nil + } + if let userId, userId != m.currentUser?.userId { + changeActiveUser(userId, viewPwd: nil) + } + } + .onChange(of: themeUserDestination == nil) { isNil in + if isNil { + // Easiest way to hide per-user customization. + // Otherwise, it would be needed to make global variable and to use it everywhere for making a decision to include these overrides into active theme constructing or not + m.currentUser?.uiThemes = nil + } else { + m.updateCurrentUserUiThemes(uiThemes: m.users.first(where: { $0.user.userId == m.currentUser?.userId })?.user.uiThemes) + } + } + .onDisappear { + // Skip when Appearance screen is not hidden yet + if customizeThemeIsOpen { return } + // Restore user overrides from stored list of users + m.updateCurrentUserUiThemes(uiThemes: m.users.first(where: { $0.user.userId == m.currentUser?.userId })?.user.uiThemes) + themeUserDestination = if let currentUser = m.currentUser, let uiThemes = currentUser.uiThemes { + (currentUser.userId, uiThemes) + } else { + nil + } + } + } else { + EmptyView() + .onAppear { + themeUserDestination = nil + themeUserDest = nil + } + } + } +} + +struct CustomizeThemeColorsSection: View { + @EnvironmentObject var theme: AppTheme + var editColor: (ThemeColor) -> Binding + + var body: some View { + Section { + picker(.primary, editColor) + picker(.primaryVariant, editColor) + picker(.secondary, editColor) + picker(.secondaryVariant, editColor) + picker(.background, editColor) + picker(.surface, editColor) + //picker(.title, editColor) + picker(.primaryVariant2, editColor) + } header: { + Text("Interface colors") + .foregroundColor(theme.colors.secondary) + } + } +} + +func editColorBinding(name: ThemeColor, wallpaperType: WallpaperType?, wallpaperImage: Image?, theme: AppTheme, onColorChange: @escaping (Color?) -> Void) -> Binding { + Binding(get: { + let baseTheme = theme.base + let wallpaperBackgroundColor = theme.wallpaper.background ?? wallpaperType?.defaultBackgroundColor(baseTheme, theme.colors.background) ?? Color.clear + let wallpaperTintColor = theme.wallpaper.tint ?? wallpaperType?.defaultTintColor(baseTheme) ?? Color.clear + return switch name { + case ThemeColor.wallpaperBackground: wallpaperBackgroundColor + case ThemeColor.wallpaperTint: wallpaperTintColor + case ThemeColor.primary: theme.colors.primary + case ThemeColor.primaryVariant: theme.colors.primaryVariant + case ThemeColor.secondary: theme.colors.secondary + case ThemeColor.secondaryVariant: theme.colors.secondaryVariant + case ThemeColor.background: theme.colors.background + case ThemeColor.surface: theme.colors.surface + case ThemeColor.title: theme.appColors.title + case ThemeColor.primaryVariant2: theme.appColors.primaryVariant2 + case ThemeColor.sentMessage: theme.appColors.sentMessage + case ThemeColor.sentQuote: theme.appColors.sentQuote + case ThemeColor.receivedMessage: theme.appColors.receivedMessage + case ThemeColor.receivedQuote: theme.appColors.receivedQuote + } + }, set: onColorChange) +} + +struct WallpaperSetupView: View { + var wallpaperType: WallpaperType? + var base: DefaultTheme + var initialWallpaper: AppWallpaper? + var editColor: (ThemeColor) -> Binding + var onTypeChange: (WallpaperType?) -> Void + + var body: some View { + if let wallpaperType, case let WallpaperType.image(_, _, scaleType) = wallpaperType { + let wallpaperScaleType = if let scaleType { + scaleType + } else if let initialWallpaper, case let WallpaperType.image(_, _, scaleType) = initialWallpaper.type, let scaleType { + scaleType + } else { + WallpaperScaleType.fill + } + WallpaperScaleTypeChooser(wallpaperScaleType: Binding.constant(wallpaperScaleType), wallpaperType: wallpaperType, onTypeChange: onTypeChange) + } + + + if let wallpaperType, wallpaperType.isPreset { + WallpaperScaleChooser(wallpaperScale: Binding.constant(initialWallpaper?.type.scale ?? 1), wallpaperType: wallpaperType, onTypeChange: onTypeChange) + } else if let wallpaperType, case let WallpaperType.image(_, _, scaleType) = wallpaperType, scaleType == WallpaperScaleType.repeat { + WallpaperScaleChooser(wallpaperScale: Binding.constant(initialWallpaper?.type.scale ?? 1), wallpaperType: wallpaperType, onTypeChange: onTypeChange) + } + + if wallpaperType?.isPreset == true || wallpaperType?.isImage == true { + picker(.wallpaperBackground, editColor) + picker(.wallpaperTint, editColor) + } + + picker(.sentMessage, editColor) + picker(.sentQuote, editColor) + picker(.receivedMessage, editColor) + picker(.receivedQuote, editColor) + + } + + private struct WallpaperScaleChooser: View { + @Binding var wallpaperScale: Float + var wallpaperType: WallpaperType? + var onTypeChange: (WallpaperType?) -> Void + + var body: some View { + HStack { + Text("\(wallpaperScale)".prefix(4)) + .frame(width: 40, height: 36, alignment: .leading) + Slider( + value: Binding(get: { wallpaperScale }, set: { scale in + if let wallpaperType, case let WallpaperType.preset(filename, _) = wallpaperType { + onTypeChange(WallpaperType.preset(filename, Float("\(scale)".prefix(9)))) + } else if let wallpaperType, case let WallpaperType.image(filename, _, scaleType) = wallpaperType { + onTypeChange(WallpaperType.image(filename, Float("\(scale)".prefix(9)), scaleType)) + } + }), + in: 0.5...2, + step: 0.0000001 + ) + .frame(height: 36) + } + } + } + + private struct WallpaperScaleTypeChooser: View { + @Binding var wallpaperScaleType: WallpaperScaleType + var wallpaperType: WallpaperType? + var onTypeChange: (WallpaperType?) -> Void + + var body: some View { + Picker("Scale", selection: Binding(get: { wallpaperScaleType }, set: { scaleType in + if let wallpaperType, case let WallpaperType.image(filename, scale, _) = wallpaperType { + onTypeChange(WallpaperType.image(filename, scale, scaleType)) + } + })) { + ForEach(Array(WallpaperScaleType.allCases), id: \.self) { type in + Text(type.text) + } + } + .frame(height: 36) + } + } +} + +private struct picker: View { + var name: ThemeColor + @State var color: Color + var editColor: (ThemeColor) -> Binding + // Prevent a race between setting a color here and applying externally changed color to the binding + @State private var lastColorUpdate: Date = .now + + init(_ name: ThemeColor, _ editColor: @escaping (ThemeColor) -> Binding) { + self.name = name + self.color = editColor(name).wrappedValue + self.editColor = editColor + } + + var body: some View { + ColorPickerView(name: name, selection: $color) + .onChange(of: color) { newColor in + let editedColor = editColor(name) + if editedColor.wrappedValue != newColor { + editedColor.wrappedValue = newColor + lastColorUpdate = .now + } + } + .onChange(of: editColor(name).wrappedValue) { newValue in + // Allows to update underlying color in the picker when color changed externally, for example, by reseting colors of a theme or changing the theme + if lastColorUpdate < Date.now - 1 && newValue != color { + color = newValue + } + } + } +} + +struct ColorPickerView: View { + var name: ThemeColor + @State var selection: Binding + + var body: some View { + let supportsOpacity = switch name { + case .wallpaperTint: true + case .sentMessage: true + case .sentQuote: true + case .receivedMessage: true + case .receivedQuote: true + default: UIColor(selection.wrappedValue).cgColor.alpha < 1 + } + ColorPicker(name.text, selection: selection, supportsOpacity: supportsOpacity) + } +} + +struct WallpaperImporter: ViewModifier { + @Binding var showImageImporter: Bool + var onChooseImage: (UIImage) -> Void + + func body(content: Content) -> some View { + content.sheet(isPresented: $showImageImporter) { + // LALAL TODO: limit by 5 mb + LibraryMediaListPicker(addMedia: { onChooseImage($0.uiImage) }, selectionLimit: 1, filter: .images, finishedPreprocessing: { }) { itemsSelected in + await MainActor.run { + showImageImporter = false + } + } + } + // content.fileImporter( + // isPresented: $showImageImporter, + // allowedContentTypes: [.image], + // allowsMultipleSelection: false + // ) { result in + // if case let .success(files) = result, let fileURL = files.first { + // do { + // var fileSize: Int? = nil + // if fileURL.startAccessingSecurityScopedResource() { + // let resourceValues = try fileURL.resourceValues(forKeys: [.fileSizeKey]) + // fileSize = resourceValues.fileSize + // } + // fileURL.stopAccessingSecurityScopedResource() + // if let fileSize = fileSize, + // // Same as Android/desktop + // fileSize <= 5_000_000, + // let image = UIImage(contentsOfFile: fileURL.path){ + // onChooseImage(image) + // } else { + // let prettyMaxFileSize = ByteCountFormatter.string(fromByteCount: 5_500_000, countStyle: .binary) + // AlertManager.shared.showAlertMsg( + // title: "Large file!", + // message: "Currently maximum supported file size is \(prettyMaxFileSize)." + // ) + // } + // } catch { + // logger.error("Appearance fileImporter error \(error.localizedDescription)") + // } + // } + // } + } +} + + +/// deprecated. Remove in 2025 func getUIAccentColorDefault() -> CGColor { let defs = UserDefaults.standard return CGColor( @@ -127,15 +1081,78 @@ func getUIAccentColorDefault() -> CGColor { ) } -func setUIAccentColorDefault(_ color: CGColor) { - if let cs = color.components { - let defs = UserDefaults.standard - defs.set(cs[0], forKey: DEFAULT_ACCENT_COLOR_RED) - defs.set(cs[1], forKey: DEFAULT_ACCENT_COLOR_GREEN) - defs.set(cs[2], forKey: DEFAULT_ACCENT_COLOR_BLUE) +private var updateBackendTask: Task = Task {} + +private func saveThemeToDatabase(_ themeUserDestination: (Int64, ThemeModeOverrides?)?) { + let m = ChatModel.shared + let oldThemes = m.currentUser?.uiThemes + if let themeUserDestination { + DispatchQueue.main.async { + // Update before save to make it work seamless + m.updateCurrentUserUiThemes(uiThemes: themeUserDestination.1) + } + } + Task { + if themeUserDestination == nil { + do { + try apiSaveAppSettings(settings: AppSettings.current.prepareForExport()) + } catch { + logger.error("Error saving settings: \(error)") + } + } else if let themeUserDestination, await !apiSetUserUIThemes(userId: themeUserDestination.0, themes: themeUserDestination.1) { + // If failed to apply for some reason return the old themes + m.updateCurrentUserUiThemes(uiThemes: oldThemes) + } } } +private func removeUserThemeModeOverrides(_ themeUserDestination: Binding<(Int64, ThemeModeOverrides?)?>, _ perUserTheme: Binding) { + guard let dest = themeUserDestination.wrappedValue else { return } + perUserTheme.wrappedValue = ThemeModeOverride(mode: CurrentColors.base.mode) + themeUserDestination.wrappedValue = (dest.0, nil) + var wallpaperFilesToDelete: [String] = [] + if let type = ChatModel.shared.currentUser?.uiThemes?.light?.type, case let WallpaperType.image(filename, _, _) = type { + wallpaperFilesToDelete.append(filename) + } + if let type = ChatModel.shared.currentUser?.uiThemes?.dark?.type, case let WallpaperType.image(filename, _, _) = type { + wallpaperFilesToDelete.append(filename) + } + wallpaperFilesToDelete.forEach(removeWallpaperFile) +} + +private func decodeYAML(_ string: String) -> T? { + do { + return try YAMLDecoder().decode(T.self, from: string) + } catch { + logger.error("Error decoding YAML: \(error)") + return nil + } +} + +private func encodeThemeOverrides(_ value: ThemeOverrides) throws -> String { + let encoder = YAMLEncoder() + encoder.options = YAMLEncoder.Options(sequenceStyle: .block, mappingStyle: .block, newLineScalarStyle: .doubleQuoted) + + guard var node = try Yams.compose(yaml: try encoder.encode(value)) else { + throw RuntimeError("Error while composing a node from object") + } + node["base"]?.scalar?.style = .doubleQuoted + + ThemeColors.CodingKeys.allCases.forEach { key in + node["colors"]?[key.stringValue]?.scalar?.style = .doubleQuoted + } + + ThemeWallpaper.CodingKeys.allCases.forEach { key in + if case .scale = key { + // let number be without quotes + } else { + node["wallpaper"]?[key.stringValue]?.scalar?.style = .doubleQuoted + } + } + return try Yams.serialize(node: node) +} + +/// deprecated. Remove in 2025 func getUserInterfaceStyleDefault() -> UIUserInterfaceStyle { switch UserDefaults.standard.integer(forKey: DEFAULT_USER_INTERFACE_STYLE) { case 1: return .light @@ -144,17 +1161,6 @@ func getUserInterfaceStyleDefault() -> UIUserInterfaceStyle { } } -func setUserInterfaceStyleDefault(_ style: UIUserInterfaceStyle) { - var v: Int - switch style { - case .unspecified: v = 0 - case .light: v = 1 - case .dark: v = 2 - default: v = 0 - } - UserDefaults.standard.set(v, forKey: DEFAULT_USER_INTERFACE_STYLE) -} - struct AppearanceSettings_Previews: PreviewProvider { static var previews: some View { AppearanceSettings() diff --git a/apps/ios/Shared/Views/UserSettings/CallSettings.swift b/apps/ios/Shared/Views/UserSettings/CallSettings.swift index 3409e7ab0e..bae343ee88 100644 --- a/apps/ios/Shared/Views/UserSettings/CallSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/CallSettings.swift @@ -10,6 +10,7 @@ import SwiftUI import SimpleXChat struct CallSettings: View { + @EnvironmentObject var theme: AppTheme @AppStorage(DEFAULT_WEBRTC_POLICY_RELAY) private var webrtcPolicyRelay = true @AppStorage(GROUP_DEFAULT_CALL_KIT_ENABLED, store: groupDefaults) private var callKitEnabled = true @AppStorage(DEFAULT_CALL_KIT_CALLS_IN_RECENTS) private var callKitCallsInRecents = false @@ -22,17 +23,21 @@ struct CallSettings: View { NavigationLink { RTCServers() .navigationTitle("Your ICE servers") + .modifier(ThemedBackground(grouped: true)) } label: { Text("WebRTC ICE servers") } Toggle("Always use relay", isOn: $webrtcPolicyRelay) } header: { Text("Settings") + .foregroundColor(theme.colors.secondary) } footer: { if webrtcPolicyRelay { Text("Relay server protects your IP address, but it can observe the duration of the call.") + .foregroundColor(theme.colors.secondary) } else { Text("Relay server is only used if necessary. Another party can observe your IP address.") + .foregroundColor(theme.colors.secondary) } } @@ -46,6 +51,7 @@ struct CallSettings: View { } } header: { Text("Interface") + .foregroundColor(theme.colors.secondary) } footer: { if callKitEnabled { Text("You can accept calls from lock screen, without device and app authentication.") @@ -55,7 +61,7 @@ struct CallSettings: View { } } - Section("Limitations") { + Section(header: Text("Limitations").foregroundColor(theme.colors.secondary)) { VStack(alignment: .leading, spacing: 8) { textListItem("1.", "Do NOT use SimpleX for emergency calls.") textListItem("2.", "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions.") diff --git a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift index 3bbfbfe33e..4ef05bd998 100644 --- a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift +++ b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift @@ -10,8 +10,11 @@ import SwiftUI import SimpleXChat struct DeveloperView: View { + @EnvironmentObject var theme: AppTheme @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @AppStorage(GROUP_DEFAULT_CONFIRM_DB_UPGRADES, store: groupDefaults) private var confirmDatabaseUpgrades = false + @State private var hintsUnchanged = hintDefaultsUnchanged() + @Environment(\.colorScheme) var colorScheme var body: some View { @@ -23,28 +26,56 @@ struct DeveloperView: View { .resizable() .frame(width: 24, height: 24) .opacity(0.5) + .colorMultiply(theme.colors.secondary) Text("Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat)") .padding(.leading, 36) } NavigationLink { TerminalView() } label: { - settingsRow("terminal") { Text("Chat console") } + settingsRow("terminal", color: theme.colors.secondary) { Text("Chat console") } } - settingsRow("internaldrive") { - Toggle("Confirm database upgrades", isOn: $confirmDatabaseUpgrades) + settingsRow("lightbulb.max", color: theme.colors.secondary) { + Button("Reset all hints", action: resetHintDefaults) + .disabled(hintsUnchanged) } - settingsRow("chevron.left.forwardslash.chevron.right") { + settingsRow("chevron.left.forwardslash.chevron.right", color: theme.colors.secondary) { Toggle("Show developer options", isOn: $developerTools) } } header: { Text("") } footer: { - (developerTools ? Text("Show:") : Text("Hide:")) + Text(" ") + Text("Database IDs and Transport isolation option.") + ((developerTools ? Text("Show:") : Text("Hide:")) + Text(" ") + Text("Database IDs and Transport isolation option.")) + .foregroundColor(theme.colors.secondary) + } + + if developerTools { + Section { + settingsRow("internaldrive", color: theme.colors.secondary) { + Toggle("Confirm database upgrades", isOn: $confirmDatabaseUpgrades) + } + } header: { + Text("Developer options") + } } } } } + + private func resetHintDefaults() { + for def in hintDefaults { + if let val = appDefaults[def] as? Bool { + UserDefaults.standard.set(val, forKey: def) + } + } + hintsUnchanged = true + } +} + +private func hintDefaultsUnchanged() -> Bool { + hintDefaults.allSatisfy { def in + appDefaults[def] as? Bool == UserDefaults.standard.bool(forKey: def) + } } struct DeveloperView_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/UserSettings/HiddenProfileView.swift b/apps/ios/Shared/Views/UserSettings/HiddenProfileView.swift index 509874619f..5f20055b2b 100644 --- a/apps/ios/Shared/Views/UserSettings/HiddenProfileView.swift +++ b/apps/ios/Shared/Views/UserSettings/HiddenProfileView.swift @@ -13,6 +13,7 @@ struct HiddenProfileView: View { @State var user: User @Binding var profileHidden: Bool @EnvironmentObject private var m: ChatModel + @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss: DismissAction @State private var hidePassword = "" @State private var confirmHidePassword = "" @@ -36,7 +37,7 @@ struct HiddenProfileView: View { PassphraseField(key: $hidePassword, placeholder: "Password to show", valid: passwordValid, showStrength: true) PassphraseField(key: $confirmHidePassword, placeholder: "Confirm password", valid: confirmValid) - settingsRow("lock") { + settingsRow("lock", color: theme.colors.secondary) { Button("Save profile password") { Task { do { @@ -58,8 +59,10 @@ struct HiddenProfileView: View { .disabled(saveDisabled) } header: { Text("Hidden profile password") + .foregroundColor(theme.colors.secondary) } footer: { Text("To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page.") + .foregroundColor(theme.colors.secondary) .font(.body) .padding(.top, 8) } @@ -70,6 +73,7 @@ struct HiddenProfileView: View { message: Text(savePasswordError ?? "") ) } + .modifier(ThemedBackground(grouped: true)) } var passwordValid: Bool { hidePassword == hidePassword.trimmingCharacters(in: .whitespaces) } diff --git a/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift b/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift index fc478596a9..a0250afddf 100644 --- a/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift +++ b/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift @@ -27,6 +27,7 @@ struct IncognitoHelp: View { } .listRowBackground(Color.clear) } + .modifier(ThemedBackground()) } } diff --git a/apps/ios/Shared/Views/UserSettings/MarkdownHelp.swift b/apps/ios/Shared/Views/UserSettings/MarkdownHelp.swift index afb0af66c1..cf9cada592 100644 --- a/apps/ios/Shared/Views/UserSettings/MarkdownHelp.swift +++ b/apps/ios/Shared/Views/UserSettings/MarkdownHelp.swift @@ -9,6 +9,8 @@ import SwiftUI struct MarkdownHelp: View { + @EnvironmentObject var theme: AppTheme + var body: some View { VStack(alignment: .leading, spacing: 8) { Text("You can use markdown to format messages:") @@ -21,7 +23,7 @@ struct MarkdownHelp: View { ( mdFormat("#secret#", Text("secret") .foregroundColor(.clear) - .underline(color: .primary) + Text(" (can be copied)")) + .underline(color: theme.colors.onBackground) + Text(" (can be copied)")) ) .textSelection(.enabled) } diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift index a6702b1821..155a3956be 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift @@ -10,14 +10,10 @@ import SwiftUI import SimpleXChat private enum NetworkAlert: Identifiable { - case updateOnionHosts(hosts: OnionHosts) - case updateSessionMode(mode: TransportSessionMode) case error(err: String) var id: String { switch self { - case let .updateOnionHosts(hosts): return "updateOnionHosts \(hosts)" - case let .updateSessionMode(mode): return "updateSessionMode \(mode)" case let .error(err): return "error \(err)" } } @@ -25,13 +21,7 @@ private enum NetworkAlert: Identifiable { struct NetworkAndServers: View { @EnvironmentObject var m: ChatModel - @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false - @State private var cfgLoaded = false - @State private var currentNetCfg = NetCfg.defaults - @State private var netCfg = NetCfg.defaults - @State private var onionHosts: OnionHosts = .no - @State private var sessionMode: TransportSessionMode = .user - @State private var alert: NetworkAlert? + @EnvironmentObject var theme: AppTheme var body: some View { VStack { @@ -40,51 +30,42 @@ struct NetworkAndServers: View { NavigationLink { ProtocolServersView(serverProtocol: .smp) .navigationTitle("Your SMP servers") + .modifier(ThemedBackground(grouped: true)) } label: { - Text("SMP servers") + Text("Message servers") } NavigationLink { ProtocolServersView(serverProtocol: .xftp) .navigationTitle("Your XFTP servers") + .modifier(ThemedBackground(grouped: true)) } label: { - Text("XFTP servers") - } - - Picker("Use .onion hosts", selection: $onionHosts) { - ForEach(OnionHosts.values, id: \.self) { Text($0.text) } - } - .frame(height: 36) - - if developerTools { - Picker("Transport isolation", selection: $sessionMode) { - ForEach(TransportSessionMode.values, id: \.self) { Text($0.text) } - } - .frame(height: 36) + Text("Media & file servers") } NavigationLink { AdvancedNetworkSettings() - .navigationTitle("Network settings") + .navigationTitle("Advanced settings") + .modifier(ThemedBackground(grouped: true)) } label: { Text("Advanced network settings") } } header: { Text("Messages & files") - } footer: { - Text("Using .onion hosts requires compatible VPN provider.") + .foregroundColor(theme.colors.secondary) } - Section("Calls") { + Section(header: Text("Calls").foregroundColor(theme.colors.secondary)) { NavigationLink { RTCServers() .navigationTitle("Your ICE servers") + .modifier(ThemedBackground(grouped: true)) } label: { Text("WebRTC ICE servers") } } - Section("Network connection") { + Section(header: Text("Network connection").foregroundColor(theme.colors.secondary)) { HStack { Text(m.networkInfo.networkType.text) Spacer() @@ -93,94 +74,6 @@ struct NetworkAndServers: View { } } } - .onAppear { - if cfgLoaded { return } - cfgLoaded = true - currentNetCfg = getNetCfg() - resetNetCfgView() - } - .onChange(of: onionHosts) { _ in - if onionHosts != OnionHosts(netCfg: currentNetCfg) { - alert = .updateOnionHosts(hosts: onionHosts) - } - } - .onChange(of: sessionMode) { _ in - if sessionMode != netCfg.sessionMode { - alert = .updateSessionMode(mode: sessionMode) - } - } - .alert(item: $alert) { a in - switch a { - case let .updateOnionHosts(hosts): - return Alert( - title: Text("Update .onion hosts setting?"), - message: Text(onionHostsInfo(hosts)) + Text("\n") + Text("Updating this setting will re-connect the client to all servers."), - primaryButton: .default(Text("Ok")) { - let (hostMode, requiredHostMode) = hosts.hostMode - netCfg.hostMode = hostMode - netCfg.requiredHostMode = requiredHostMode - saveNetCfg() - }, - secondaryButton: .cancel() { - resetNetCfgView() - } - ) - case let .updateSessionMode(mode): - return Alert( - title: Text("Update transport isolation mode?"), - message: Text(sessionModeInfo(mode)) + Text("\n") + Text("Updating this setting will re-connect the client to all servers."), - primaryButton: .default(Text("Ok")) { - netCfg.sessionMode = mode - saveNetCfg() - }, - secondaryButton: .cancel() { - resetNetCfgView() - } - ) - case let .error(err): - return Alert( - title: Text("Error updating settings"), - message: Text(err) - ) - } - } - } - - private func saveNetCfg() { - do { - let def = netCfg.hostMode == .onionHost ? NetCfg.proxyDefaults : NetCfg.defaults - netCfg.tcpConnectTimeout = def.tcpConnectTimeout - netCfg.tcpTimeout = def.tcpTimeout - try setNetworkConfig(netCfg) - currentNetCfg = netCfg - setNetCfg(netCfg) - } catch let error { - let err = responseError(error) - resetNetCfgView() - alert = .error(err: err) - logger.error("\(err)") - } - } - - private func resetNetCfgView() { - netCfg = currentNetCfg - onionHosts = OnionHosts(netCfg: netCfg) - sessionMode = netCfg.sessionMode - } - - private func onionHostsInfo(_ hosts: OnionHosts) -> LocalizedStringKey { - switch hosts { - case .no: return "Onion hosts will not be used." - case .prefer: return "Onion hosts will be used when available. Requires enabling VPN." - case .require: return "Onion hosts will be required for connection. Requires enabling VPN." - } - } - - private func sessionModeInfo(_ mode: TransportSessionMode) -> LocalizedStringKey { - switch mode { - case .user: return "A separate TCP connection will be used **for each chat profile you have in the app**." - case .entity: return "A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." - } } } diff --git a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift index 4876d60eca..b9c92c9919 100644 --- a/apps/ios/Shared/Views/UserSettings/NotificationsView.swift +++ b/apps/ios/Shared/Views/UserSettings/NotificationsView.swift @@ -11,11 +11,30 @@ import SimpleXChat struct NotificationsView: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @State private var notificationMode: NotificationsMode = ChatModel.shared.notificationMode @State private var showAlert: NotificationAlert? @State private var legacyDatabase = dbContainerGroupDefault.get() == .documents + @State private var testing = false + @State private var testedSuccess: Bool? = nil var body: some View { + ZStack { + viewBody() + if testing { + ProgressView().scaleEffect(2) + } + } + .alert(item: $showAlert) { alert in + if let token = m.deviceToken { + return notificationAlert(alert, token) + } else { + return Alert(title: Text("No device token!")) + } + } + } + + private func viewBody() -> some View { List { Section { NavigationLink { @@ -27,20 +46,15 @@ struct NotificationsView: View { } footer: { VStack(alignment: .leading) { Text(ntfModeDescription(notificationMode)) + .foregroundColor(theme.colors.secondary) } .font(.callout) .padding(.top, 1) } } .navigationTitle("Send notifications") + .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.inline) - .alert(item: $showAlert) { alert in - if let token = m.deviceToken { - return notificationAlert(alert, token) - } else { - return Alert(title: Text("No device token!")) - } - } } label: { HStack { Text("Send notifications") @@ -59,6 +73,7 @@ struct NotificationsView: View { } footer: { VStack(alignment: .leading, spacing: 1) { Text("You can set lock screen notification preview via settings.") + .foregroundColor(theme.colors.secondary) Button("Open Settings") { DispatchQueue.main.async { UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:], completionHandler: nil) @@ -68,6 +83,7 @@ struct NotificationsView: View { } } .navigationTitle("Show preview") + .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.inline) } label: { HStack { @@ -78,13 +94,16 @@ struct NotificationsView: View { } if let server = m.notificationServer { - smpServers("Push server", [server]) + smpServers("Push server", [server], theme.colors.secondary) + testServerButton(server) } } header: { Text("Push notifications") + .foregroundColor(theme.colors.secondary) } footer: { if legacyDatabase { Text("Please restart the app and migrate the database to enable push notifications.") + .foregroundColor(theme.colors.secondary) .font(.callout) .padding(.top, 1) } @@ -109,6 +128,11 @@ struct NotificationsView: View { notificationMode = m.notificationMode } ) + case let .testFailure(testFailure): + return Alert( + title: Text("Server test failed!"), + message: Text(testFailure.localizedDescription) + ) case let .error(title, error): return Alert(title: Text(title), message: Text(error)) } @@ -133,6 +157,7 @@ struct NotificationsView: View { notificationMode = .off m.notificationMode = .off m.notificationServer = nil + testedSuccess = nil } } catch let error { await MainActor.run { @@ -150,6 +175,7 @@ struct NotificationsView: View { notificationMode = ntfMode m.notificationMode = ntfMode m.notificationServer = ntfServer + testedSuccess = nil } } catch let error { await MainActor.run { @@ -161,6 +187,52 @@ struct NotificationsView: View { } } } + + private func testServerButton(_ server: String) -> some View { + HStack { + Button("Test server") { + testing = true + Task { + await testServer(server) + await MainActor.run { testing = false } + } + } + .disabled(testing) + if !testing { + Spacer() + showTestStatus() + } + } + } + + @ViewBuilder func showTestStatus() -> some View { + if testedSuccess == true { + Image(systemName: "checkmark") + .foregroundColor(.green) + } else if testedSuccess == false { + Image(systemName: "multiply") + .foregroundColor(.red) + } + } + + private func testServer(_ server: String) async { + do { + let r = try await testProtoServer(server: server) + switch r { + case .success: + await MainActor.run { + testedSuccess = true + } + case let .failure(f): + await MainActor.run { + showAlert = .testFailure(testFailure: f) + testedSuccess = false + } + } + } catch let error { + logger.error("testServerConnection \(responseError(error))") + } + } } func ntfModeDescription(_ mode: NotificationsMode) -> LocalizedStringKey { @@ -172,6 +244,7 @@ func ntfModeDescription(_ mode: NotificationsMode) -> LocalizedStringKey { } struct SelectionListView: View { + @EnvironmentObject var theme: AppTheme var list: [Item] @Binding var selection: Item var onSelection: ((Item) -> Void)? @@ -179,32 +252,24 @@ struct SelectionListView: View { var body: some View { ForEach(list) { item in - HStack { - Text(item.label) - Spacer() - if selection == item { - Image(systemName: "checkmark") - .resizable().scaledToFit().frame(width: 16) - .foregroundColor(.accentColor) - } - } - .contentShape(Rectangle()) - .listRowBackground(Color(uiColor: tapped == item ? .secondarySystemFill : .systemBackground)) - .onTapGesture { + Button { if selection == item { return } if let f = onSelection { f(item) } else { selection = item } - } - ._onButtonGesture { down in - if down { - tapped = item - } else { - tapped = nil + } label: { + HStack { + Text(item.label).foregroundColor(theme.colors.onBackground) + Spacer() + if selection == item { + Image(systemName: "checkmark") + .resizable().scaledToFit().frame(width: 16) + .foregroundColor(theme.colors.primary) + } } - } perform: {} + } } .environment(\.editMode, .constant(.active)) } @@ -212,11 +277,13 @@ struct SelectionListView: View { enum NotificationAlert: Identifiable { case setMode(mode: NotificationsMode) + case testFailure(testFailure: ProtocolTestFailure) case error(title: LocalizedStringKey, error: String) var id: String { switch self { case let .setMode(mode): return "enable \(mode.rawValue)" + case let .testFailure(testFailure): return "testFailure \(testFailure.testStep) \(testFailure.testError)" case let .error(title, error): return "error \(title): \(error)" } } diff --git a/apps/ios/Shared/Views/UserSettings/PreferencesView.swift b/apps/ios/Shared/Views/UserSettings/PreferencesView.swift index 2e560f8578..0c10da2103 100644 --- a/apps/ios/Shared/Views/UserSettings/PreferencesView.swift +++ b/apps/ios/Shared/Views/UserSettings/PreferencesView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct PreferencesView: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme @State var profile: LocalProfile @State var preferences: FullPreferences @State var currentPreferences: FullPreferences @@ -35,7 +36,7 @@ struct PreferencesView: View { private func featureSection(_ feature: ChatFeature, _ allowFeature: Binding) -> some View { Section { - settingsRow(feature.icon) { + settingsRow(feature.icon, color: theme.colors.secondary) { Picker(feature.text, selection: allowFeature) { ForEach(FeatureAllowed.values) { allow in Text(allow.text) @@ -44,7 +45,7 @@ struct PreferencesView: View { .frame(height: 36) } } - footer: { featureFooter(feature, allowFeature) } + footer: { featureFooter(feature, allowFeature).foregroundColor(theme.colors.secondary) } } @@ -54,11 +55,11 @@ struct PreferencesView: View { get: { allowFeature.wrappedValue == .always || allowFeature.wrappedValue == .yes }, set: { yes, _ in allowFeature.wrappedValue = yes ? .yes : .no } ) - settingsRow(ChatFeature.timedMessages.icon) { + settingsRow(ChatFeature.timedMessages.icon, color: theme.colors.secondary) { Toggle(ChatFeature.timedMessages.text, isOn: allow) } } - footer: { featureFooter(.timedMessages, allowFeature) } + footer: { featureFooter(.timedMessages, allowFeature).foregroundColor(theme.colors.secondary) } } private func featureFooter(_ feature: ChatFeature, _ allowFeature: Binding) -> some View { diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index 8d13c6fb39..62aad348a7 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -11,15 +11,18 @@ import SimpleXChat struct PrivacySettings: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @AppStorage(DEFAULT_PRIVACY_ACCEPT_IMAGES) private var autoAcceptImages = true @AppStorage(DEFAULT_PRIVACY_LINK_PREVIEWS) private var useLinkPreviews = true @AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true @AppStorage(DEFAULT_PRIVACY_SAVE_LAST_DRAFT) private var saveLastDraft = true @AppStorage(GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES, store: groupDefaults) private var encryptLocalFiles = true + @AppStorage(GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS, store: groupDefaults) private var askToApproveRelays = true @State private var simplexLinkMode = privacySimplexLinkModeDefault.get() @AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false @AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false @State private var currentLAMode = privacyLocalAuthModeDefault.get() + @AppStorage(DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) private var privacyMediaBlurRadius: Int = 0 @State private var contactReceipts = false @State private var contactReceiptsReset = false @State private var contactReceiptsOverrides = 0 @@ -43,46 +46,38 @@ struct PrivacySettings: View { var body: some View { VStack { List { - Section("Device") { + Section(header: Text("Device").foregroundColor(theme.colors.secondary)) { NavigationLink { SimplexLockView(prefPerformLA: $prefPerformLA, currentLAMode: $currentLAMode) .navigationTitle("SimpleX Lock") + .modifier(ThemedBackground(grouped: true)) } label: { if prefPerformLA { settingsRow("lock.fill", color: .green) { simplexLockRow(currentLAMode.text) } } else { - settingsRow("lock") { + settingsRow("lock", color: theme.colors.secondary) { simplexLockRow("Off") } } } - settingsRow("eye.slash") { + settingsRow("eye.slash", color: theme.colors.secondary) { Toggle("Protect app screen", isOn: $protectScreen) } } Section { - settingsRow("lock.doc") { - Toggle("Encrypt local files", isOn: $encryptLocalFiles) - .onChange(of: encryptLocalFiles) { - setEncryptLocalFiles($0) - } - } - settingsRow("photo") { - Toggle("Auto-accept images", isOn: $autoAcceptImages) - .onChange(of: autoAcceptImages) { - privacyAcceptImagesGroupDefault.set($0) - } - } - settingsRow("network") { + settingsRow("network", color: theme.colors.secondary) { Toggle("Send link previews", isOn: $useLinkPreviews) + .onChange(of: useLinkPreviews) { linkPreviews in + privacyLinkPreviewsGroupDefault.set(linkPreviews) + } } - settingsRow("message") { + settingsRow("message", color: theme.colors.secondary) { Toggle("Show last messages", isOn: $showChatPreviews) } - settingsRow("rectangle.and.pencil.and.ellipsis") { + settingsRow("rectangle.and.pencil.and.ellipsis", color: theme.colors.secondary) { Toggle("Message draft", isOn: $saveLastDraft) } .onChange(of: saveLastDraft) { saveDraft in @@ -91,7 +86,7 @@ struct PrivacySettings: View { m.draftChatId = nil } } - settingsRow("link") { + settingsRow("link", color: theme.colors.secondary) { Picker("SimpleX links", selection: $simplexLinkMode) { ForEach( SimpleXLinkMode.values + (SimpleXLinkMode.values.contains(simplexLinkMode) ? [] : [simplexLinkMode]) @@ -106,22 +101,70 @@ struct PrivacySettings: View { } } header: { Text("Chats") + .foregroundColor(theme.colors.secondary) } Section { - settingsRow("person") { + settingsRow("lock.doc", color: theme.colors.secondary) { + Toggle("Encrypt local files", isOn: $encryptLocalFiles) + .onChange(of: encryptLocalFiles) { + setEncryptLocalFiles($0) + } + } + settingsRow("photo", color: theme.colors.secondary) { + Toggle("Auto-accept images", isOn: $autoAcceptImages) + .onChange(of: autoAcceptImages) { + privacyAcceptImagesGroupDefault.set($0) + } + } + settingsRow("circle.filled.pattern.diagonalline.rectangle", color: theme.colors.secondary) { + Picker("Blur media", selection: $privacyMediaBlurRadius) { + let values = [0, 12, 24, 48] + ([0, 12, 24, 48].contains(privacyMediaBlurRadius) ? [] : [privacyMediaBlurRadius]) + ForEach(values, id: \.self) { radius in + let text: String = switch radius { + case 0: NSLocalizedString("Off", comment: "blur media") + case 12: NSLocalizedString("Soft", comment: "blur media") + case 24: NSLocalizedString("Medium", comment: "blur media") + case 48: NSLocalizedString("Strong", comment: "blur media") + default: "\(radius)" + } + Text(text) + } + } + } + .frame(height: 36) + settingsRow("network.badge.shield.half.filled", color: theme.colors.secondary) { + Toggle("Protect IP address", isOn: $askToApproveRelays) + } + } header: { + Text("Files") + .foregroundColor(theme.colors.secondary) + } footer: { + if askToApproveRelays { + Text("The app will ask to confirm downloads from unknown file servers (except .onion).") + .foregroundColor(theme.colors.secondary) + } else { + Text("Without Tor or VPN, your IP address will be visible to file servers.") + .foregroundColor(theme.colors.secondary) + } + } + + Section { + settingsRow("person", color: theme.colors.secondary) { Toggle("Contacts", isOn: $contactReceipts) } - settingsRow("person.2") { + settingsRow("person.2", color: theme.colors.secondary) { Toggle("Small groups (max 20)", isOn: $groupReceipts) } } header: { Text("Send delivery receipts to") + .foregroundColor(theme.colors.secondary) } footer: { VStack(alignment: .leading) { Text("These settings are for your current profile **\(m.currentUser?.displayName ?? "")**.") Text("They can be overridden in contact and group settings.") } + .foregroundColor(theme.colors.secondary) .frame(maxWidth: .infinity, alignment: .leading) } .confirmationDialog(contactReceiptsDialogTitle, isPresented: $contactReceiptsDialogue, titleVisibility: .visible) { @@ -317,6 +360,7 @@ struct SimplexLockView: View { @Binding var prefPerformLA: Bool @Binding var currentLAMode: LAMode @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false @State private var laMode: LAMode = privacyLocalAuthModeDefault.get() @AppStorage(DEFAULT_LA_LOCK_DELAY) private var laLockDelay = 30 @@ -324,6 +368,7 @@ struct SimplexLockView: View { @State private var selfDestruct: Bool = UserDefaults.standard.bool(forKey: DEFAULT_LA_SELF_DESTRUCT) @State private var currentSelfDestruct: Bool = UserDefaults.standard.bool(forKey: DEFAULT_LA_SELF_DESTRUCT) @AppStorage(DEFAULT_LA_SELF_DESTRUCT_DISPLAY_NAME) private var selfDestructDisplayName = "" + @AppStorage(GROUP_DEFAULT_ALLOW_SHARE_EXTENSION, store: groupDefaults) private var allowShareExtension = false @State private var performLAToggleReset = false @State private var performLAModeReset = false @State private var performLASelfDestructReset = false @@ -395,13 +440,19 @@ struct SimplexLockView: View { } } + if performLA { + Section("Share to SimpleX") { + Toggle("Allow sharing", isOn: $allowShareExtension) + } + } + if performLA && laMode == .passcode { - Section("Self-destruct passcode") { + Section(header: Text("Self-destruct passcode").foregroundColor(theme.colors.secondary)) { Toggle(isOn: $selfDestruct) { HStack(spacing: 6) { Text("Enable self-destruct") Image(systemName: "info.circle") - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) .font(.system(size: 14)) } .onTapGesture { @@ -419,6 +470,7 @@ struct SimplexLockView: View { } } .onChange(of: performLA) { performLAToggle in + appLocalAuthEnabledGroupDefault.set(performLAToggle) prefLANoticeShown = true if performLAToggleReset { performLAToggleReset = false diff --git a/apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift b/apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift index 6702ab7ce8..da29dfac29 100644 --- a/apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift +++ b/apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct ProtocolServerView: View { @Environment(\.dismiss) var dismiss: DismissAction + @EnvironmentObject var theme: AppTheme let serverProtocol: ServerProtocol @Binding var server: ServerCfg @State var serverToEdit: ServerCfg @@ -49,7 +50,7 @@ struct ProtocolServerView: View { private func presetServer() -> some View { return VStack { List { - Section("Preset server address") { + Section(header: Text("Preset server address").foregroundColor(theme.colors.secondary)) { Text(serverToEdit.server) .textSelection(.enabled) } @@ -75,6 +76,7 @@ struct ProtocolServerView: View { } header: { HStack { Text("Your server address") + .foregroundColor(theme.colors.secondary) if !valid { Spacer() Image(systemName: "exclamationmark.circle").foregroundColor(.red) @@ -83,7 +85,7 @@ struct ProtocolServerView: View { } useServerSection(valid) if valid { - Section("Add to another device") { + Section(header: Text("Add to another device").foregroundColor(theme.colors.secondary)) { MutableQRCode(uri: $serverToEdit.server) .listRowInsets(EdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12)) } @@ -93,7 +95,7 @@ struct ProtocolServerView: View { } private func useServerSection(_ valid: Bool) -> some View { - Section("Use server") { + Section(header: Text("Use server").foregroundColor(theme.colors.secondary)) { HStack { Button("Test server") { testing = true @@ -110,7 +112,10 @@ struct ProtocolServerView: View { Spacer() showTestStatus(server: serverToEdit) } + let useForNewDisabled = serverToEdit.tested != true && !serverToEdit.preset Toggle("Use for new connections", isOn: $serverToEdit.enabled) + .disabled(useForNewDisabled) + .foregroundColor(useForNewDisabled ? theme.colors.secondary : theme.colors.onBackground) } } } @@ -170,10 +175,6 @@ func testServerConnection(server: Binding) async -> ProtocolTestFailu } } -func serverHostname(_ srv: String) -> String { - parseServerAddress(srv)?.hostnames.first ?? srv -} - struct ProtocolServerView_Previews: PreviewProvider { static var previews: some View { ProtocolServerView( diff --git a/apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift b/apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift index b9163d4bad..0fb37d5c49 100644 --- a/apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift +++ b/apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift @@ -14,11 +14,13 @@ private let howToUrl = URL(string: "https://simplex.chat/docs/server.html")! struct ProtocolServersView: View { @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject private var m: ChatModel + @EnvironmentObject var theme: AppTheme @Environment(\.editMode) private var editMode let serverProtocol: ServerProtocol @State private var currServers: [ServerCfg] = [] - @State private var presetServers: [String] = [] - @State private var servers: [ServerCfg] = [] + @State private var presetServers: [ServerCfg] = [] + @State private var configuredServers: [ServerCfg] = [] + @State private var otherServers: [ServerCfg] = [] @State private var selectedServer: String? = nil @State private var showAddServer = false @State private var showScanProtoServer = false @@ -52,29 +54,53 @@ struct ProtocolServersView: View { private func protocolServersView() -> some View { List { - Section { - ForEach($servers) { srv in - protocolServerView(srv) + if !configuredServers.isEmpty { + Section { + ForEach($configuredServers) { srv in + protocolServerView(srv) + } + .onMove { indexSet, offset in + configuredServers.move(fromOffsets: indexSet, toOffset: offset) + } + .onDelete { indexSet in + configuredServers.remove(atOffsets: indexSet) + } + } header: { + Text("Configured \(proto) servers") + .foregroundColor(theme.colors.secondary) + } footer: { + Text("The servers for new connections of your current chat profile **\(m.currentUser?.displayName ?? "")**.") + .foregroundColor(theme.colors.secondary) + .lineLimit(10) } - .onMove { indexSet, offset in - servers.move(fromOffsets: indexSet, toOffset: offset) + } + + if !otherServers.isEmpty { + Section { + ForEach($otherServers) { srv in + protocolServerView(srv) + } + .onMove { indexSet, offset in + otherServers.move(fromOffsets: indexSet, toOffset: offset) + } + .onDelete { indexSet in + otherServers.remove(atOffsets: indexSet) + } + } header: { + Text("Other \(proto) servers") + .foregroundColor(theme.colors.secondary) } - .onDelete { indexSet in - servers.remove(atOffsets: indexSet) - } - Button("Add server…") { - showAddServer = true - } - } header: { - Text("\(proto) servers") - } footer: { - Text("The servers for new connections of your current chat profile **\(m.currentUser?.displayName ?? "")**.") - .lineLimit(10) } Section { - Button("Reset") { servers = currServers } - .disabled(servers == currServers || testing) + Button("Add server") { + showAddServer = true + } + } + + Section { + Button("Reset") { partitionServers(currServers) } + .disabled(Set(allServers) == Set(currServers) || testing) Button("Test servers", action: testServers) .disabled(testing || allServersDisabled) Button("Save servers", action: saveServers) @@ -83,17 +109,18 @@ struct ProtocolServersView: View { } } .toolbar { EditButton() } - .confirmationDialog("Add server…", isPresented: $showAddServer, titleVisibility: .hidden) { + .confirmationDialog("Add server", isPresented: $showAddServer, titleVisibility: .hidden) { Button("Enter server manually") { - servers.append(ServerCfg.empty) - selectedServer = servers.last?.id + otherServers.append(ServerCfg.empty) + selectedServer = allServers.last?.id } Button("Scan server QR code") { showScanProtoServer = true } Button("Add preset servers", action: addAllPresets) .disabled(hasAllPresets()) } .sheet(isPresented: $showScanProtoServer) { - ScanProtocolServer(servers: $servers) + ScanProtocolServer(servers: $otherServers) + .modifier(ThemedBackground(grouped: true)) } .modifier(BackButton(disabled: Binding.constant(false)) { if saveDisabled { @@ -103,7 +130,7 @@ struct ProtocolServersView: View { showSaveDialog = true } }) - .confirmationDialog("Save servers?", isPresented: $showSaveDialog) { + .confirmationDialog("Save servers?", isPresented: $showSaveDialog, titleVisibility: .visible) { Button("Save") { saveServers() dismiss() @@ -129,27 +156,39 @@ struct ProtocolServersView: View { } .onAppear { // this condition is needed to prevent re-setting the servers when exiting single server view - if !justOpened { return } - do { - let r = try getUserProtoServers(serverProtocol) - currServers = r.protoServers - presetServers = r.presetServers - servers = currServers - } catch let error { - alert = .error( - title: "Error loading \(proto) servers", - error: "Error: \(responseError(error))" - ) + if justOpened { + do { + let r = try getUserProtoServers(serverProtocol) + currServers = r.protoServers + presetServers = r.presetServers + partitionServers(currServers) + } catch let error { + alert = .error( + title: "Error loading \(proto) servers", + error: "Error: \(responseError(error))" + ) + } + justOpened = false + } else { + partitionServers(allServers) } - justOpened = false } } + private func partitionServers(_ servers: [ServerCfg]) { + configuredServers = servers.filter { $0.preset || $0.enabled } + otherServers = servers.filter { !($0.preset || $0.enabled) } + } + + private var allServers: [ServerCfg] { + configuredServers + otherServers + } + private var saveDisabled: Bool { - servers.isEmpty || - servers == currServers || + allServers.isEmpty || + Set(allServers) == Set(currServers) || testing || - !servers.allSatisfy { srv in + !allServers.allSatisfy { srv in if let address = parseServerAddress(srv.server) { return uniqueAddress(srv, address) } @@ -159,7 +198,7 @@ struct ProtocolServersView: View { } private var allServersDisabled: Bool { - servers.allSatisfy { !$0.enabled } + allServers.allSatisfy { !$0.enabled } } private func protocolServerView(_ server: Binding) -> some View { @@ -171,6 +210,7 @@ struct ProtocolServersView: View { serverToEdit: srv ) .navigationBarTitle(srv.preset ? "Preset server" : "Your server") + .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) } label: { let address = parseServerAddress(srv.server) @@ -182,7 +222,7 @@ struct ProtocolServersView: View { } else if !uniqueAddress(srv, address) { Image(systemName: "exclamationmark.circle").foregroundColor(.red) } else if !srv.enabled { - Image(systemName: "slash.circle").foregroundColor(.secondary) + Image(systemName: "slash.circle").foregroundColor(theme.colors.secondary) } else { showTestStatus(server: srv) } @@ -197,7 +237,7 @@ struct ProtocolServersView: View { if srv.enabled { v } else { - v.foregroundColor(.secondary) + v.foregroundColor(theme.colors.secondary) } } } @@ -221,7 +261,7 @@ struct ProtocolServersView: View { } private func uniqueAddress(_ s: ServerCfg, _ address: ServerAddress) -> Bool { - servers.allSatisfy { srv in + allServers.allSatisfy { srv in address.hostnames.allSatisfy { host in srv.id == s.id || !srv.server.contains(host) } @@ -235,13 +275,13 @@ struct ProtocolServersView: View { private func addAllPresets() { for srv in presetServers { if !hasPreset(srv) { - servers.append(ServerCfg(server: srv, preset: true, tested: nil, enabled: true)) + configuredServers.append(srv) } } } - private func hasPreset(_ srv: String) -> Bool { - servers.contains(where: { $0.server == srv }) + private func hasPreset(_ srv: ServerCfg) -> Bool { + allServers.contains(where: { $0.server == srv.server }) } private func testServers() { @@ -259,19 +299,31 @@ struct ProtocolServersView: View { } private func resetTestStatus() { - for i in 0.. [String: ProtocolTestFailure] { var fs: [String: ProtocolTestFailure] = [:] - for i in 0..(defaults: UserDefaults let customDisappearingMessageTimeDefault = IntDefault(defaults: UserDefaults.standard, forKey: DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME) +let showDeleteConversationNoticeDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOW_DELETE_CONVERSATION_NOTICE) +let showDeleteContactNoticeDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOW_DELETE_CONTACT_NOTICE) + +let currentThemeDefault = StringDefault(defaults: UserDefaults.standard, forKey: DEFAULT_CURRENT_THEME, withDefault: DefaultTheme.SYSTEM_THEME_NAME) +let systemDarkThemeDefault = StringDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SYSTEM_DARK_THEME, withDefault: DefaultTheme.DARK.themeName) +let currentThemeIdsDefault = CodableDefault<[String: String]>(defaults: UserDefaults.standard, forKey: DEFAULT_CURRENT_THEME_IDS, withDefault: [:] ) +let themeOverridesDefault: CodableDefault<[ThemeOverrides]> = CodableDefault(defaults: UserDefaults.standard, forKey: DEFAULT_THEME_OVERRIDES, withDefault: []) + func setGroupDefaults() { privacyAcceptImagesGroupDefault.set(UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_ACCEPT_IMAGES)) + appLocalAuthEnabledGroupDefault.set(UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA)) + privacyLinkPreviewsGroupDefault.set(UserDefaults.standard.bool(forKey: DEFAULT_PRIVACY_LINK_PREVIEWS)) + profileImageCornerRadiusGroupDefault.set(UserDefaults.standard.double(forKey: DEFAULT_PROFILE_IMAGE_CORNER_RADIUS)) } +public class StringDefault { + var defaults: UserDefaults + var key: String + var defaultValue: String + + public init(defaults: UserDefaults = UserDefaults.standard, forKey: String, withDefault: String) { + self.defaults = defaults + self.key = forKey + self.defaultValue = withDefault + } + + public func get() -> String { + defaults.string(forKey: key) ?? defaultValue + } + + public func set(_ value: String) { + defaults.set(value, forKey: key) + defaults.synchronize() + } +} + +public class CodableDefault { + var defaults: UserDefaults + var key: String + var defaultValue: T + + public init(defaults: UserDefaults = UserDefaults.standard, forKey: String, withDefault: T) { + self.defaults = defaults + self.key = forKey + self.defaultValue = withDefault + } + + var cache: T? = nil + + public func get() -> T { + if let cache { + return cache + } else if let value = defaults.string(forKey: key) { + let res = decodeJSON(value) ?? defaultValue + cache = res + return res + } + return defaultValue + } + + public func set(_ value: T) { + defaults.set(encodeJSON(value), forKey: key) + cache = value + //defaults.synchronize() + } +} + + struct SettingsView: View { @Environment(\.colorScheme) var colorScheme @EnvironmentObject var chatModel: ChatModel @EnvironmentObject var sceneDelegate: SceneDelegate + @EnvironmentObject var theme: AppTheme @Binding var showSettings: Bool @State private var showProgress: Bool = false @@ -173,11 +270,12 @@ struct SettingsView: View { let user = chatModel.currentUser NavigationView { List { - Section("You") { + Section(header: Text("You").foregroundColor(theme.colors.secondary)) { if let user = user { NavigationLink { UserProfile() .navigationTitle("Your current profile") + .modifier(ThemedBackground()) } label: { ProfilePreview(profileOf: user) .padding(.leading, -8) @@ -187,7 +285,7 @@ struct SettingsView: View { NavigationLink { UserProfilesView(showSettings: $showSettings) } label: { - settingsRow("person.crop.rectangle.stack") { Text("Your chat profiles") } + settingsRow("person.crop.rectangle.stack", color: theme.colors.secondary) { Text("Your chat profiles") } } @@ -195,39 +293,43 @@ struct SettingsView: View { NavigationLink { UserAddressView(shareViaProfile: user.addressShared) .navigationTitle("SimpleX address") + .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) } label: { - settingsRow("qrcode") { Text("Your SimpleX address") } + settingsRow("qrcode", color: theme.colors.secondary) { Text("Your SimpleX address") } } NavigationLink { PreferencesView(profile: user.profile, preferences: user.fullPreferences, currentPreferences: user.fullPreferences) .navigationTitle("Your preferences") + .modifier(ThemedBackground(grouped: true)) } label: { - settingsRow("switch.2") { Text("Chat preferences") } + settingsRow("switch.2", color: theme.colors.secondary) { Text("Chat preferences") } } } NavigationLink { ConnectDesktopView(viaSettings: true) } label: { - settingsRow("desktopcomputer") { Text("Use from desktop") } + settingsRow("desktopcomputer", color: theme.colors.secondary) { Text("Use from desktop") } } NavigationLink { MigrateFromDevice(showSettings: $showSettings, showProgressOnSettings: $showProgress) .navigationTitle("Migrate device") + .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) } label: { - settingsRow("tray.and.arrow.up") { Text("Migrate to another device") } + settingsRow("tray.and.arrow.up", color: theme.colors.secondary) { Text("Migrate to another device") } } } .disabled(chatModel.chatRunning != true) - Section("Settings") { + Section(header: Text("Settings").foregroundColor(theme.colors.secondary)) { NavigationLink { NotificationsView() .navigationTitle("Notifications") + .modifier(ThemedBackground(grouped: true)) } label: { HStack { notificationsIcon() @@ -239,24 +341,27 @@ struct SettingsView: View { NavigationLink { NetworkAndServers() .navigationTitle("Network & servers") + .modifier(ThemedBackground(grouped: true)) } label: { - settingsRow("externaldrive.connected.to.line.below") { Text("Network & servers") } + settingsRow("externaldrive.connected.to.line.below", color: theme.colors.secondary) { Text("Network & servers") } } .disabled(chatModel.chatRunning != true) NavigationLink { CallSettings() .navigationTitle("Your calls") + .modifier(ThemedBackground(grouped: true)) } label: { - settingsRow("video") { Text("Audio & video calls") } + settingsRow("video", color: theme.colors.secondary) { Text("Audio & video calls") } } .disabled(chatModel.chatRunning != true) NavigationLink { PrivacySettings() .navigationTitle("Your privacy") + .modifier(ThemedBackground(grouped: true)) } label: { - settingsRow("lock") { Text("Privacy & security") } + settingsRow("lock", color: theme.colors.secondary) { Text("Privacy & security") } } .disabled(chatModel.chatRunning != true) @@ -264,8 +369,9 @@ struct SettingsView: View { NavigationLink { AppearanceSettings() .navigationTitle("Appearance") + .modifier(ThemedBackground(grouped: true)) } label: { - settingsRow("sun.max") { Text("Appearance") } + settingsRow("sun.max", color: theme.colors.secondary) { Text("Appearance") } } .disabled(chatModel.chatRunning != true) } @@ -273,30 +379,33 @@ struct SettingsView: View { chatDatabaseRow() } - Section("Help") { + Section(header: Text("Help").foregroundColor(theme.colors.secondary)) { if let user = user { NavigationLink { ChatHelp(showSettings: $showSettings) .navigationTitle("Welcome \(user.displayName)!") + .modifier(ThemedBackground()) .frame(maxHeight: .infinity, alignment: .top) } label: { - settingsRow("questionmark") { Text("How to use it") } + settingsRow("questionmark", color: theme.colors.secondary) { Text("How to use it") } } } NavigationLink { WhatsNewView(viaSettings: true) + .modifier(ThemedBackground()) .navigationBarTitleDisplayMode(.inline) } label: { - settingsRow("plus") { Text("What's new") } + settingsRow("plus", color: theme.colors.secondary) { Text("What's new") } } NavigationLink { SimpleXInfo(onboarding: false) .navigationBarTitle("", displayMode: .inline) + .modifier(ThemedBackground()) .frame(maxHeight: .infinity, alignment: .top) } label: { - settingsRow("info") { Text("About SimpleX Chat") } + settingsRow("info", color: theme.colors.secondary) { Text("About SimpleX Chat") } } - settingsRow("number") { + settingsRow("number", color: theme.colors.secondary) { Button("Send questions and ideas") { showSettings = false DispatchQueue.main.async { @@ -305,12 +414,12 @@ struct SettingsView: View { } } .disabled(chatModel.chatRunning != true) - settingsRow("envelope") { Text("[Send us email](mailto:chat@simplex.chat)") } + settingsRow("envelope", color: theme.colors.secondary) { Text("[Send us email](mailto:chat@simplex.chat)") } } - Section("Support SimpleX Chat") { - settingsRow("keyboard") { Text("[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)") } - settingsRow("star") { + Section(header: Text("Support SimpleX Chat").foregroundColor(theme.colors.secondary)) { + settingsRow("keyboard", color: theme.colors.secondary) { Text("[Contribute](https://github.com/simplex-chat/simplex-chat#contribute)") } + settingsRow("star", color: theme.colors.secondary) { Button("Rate the app") { if let scene = sceneDelegate.windowScene { SKStoreReviewController.requestReview(in: scene) @@ -322,27 +431,31 @@ struct SettingsView: View { .resizable() .frame(width: 24, height: 24) .opacity(0.5) + .colorMultiply(theme.colors.secondary) Text("[Star on GitHub](https://github.com/simplex-chat/simplex-chat)") .padding(.leading, indent) } } - Section("Develop") { + Section(header: Text("Develop").foregroundColor(theme.colors.secondary)) { NavigationLink { DeveloperView() .navigationTitle("Developer tools") + .modifier(ThemedBackground(grouped: true)) } label: { - settingsRow("chevron.left.forwardslash.chevron.right") { Text("Developer tools") } + settingsRow("chevron.left.forwardslash.chevron.right", color: theme.colors.secondary) { Text("Developer tools") } } NavigationLink { VersionView() .navigationBarTitle("App version") + .modifier(ThemedBackground()) } label: { Text("v\(appVersion ?? "?") (\(appBuild ?? "?"))") } } } .navigationTitle("Your settings") + .modifier(ThemedBackground(grouped: true)) } .onDisappear { chatModel.showingTerminal = false @@ -354,8 +467,9 @@ struct SettingsView: View { NavigationLink { DatabaseView(showSettings: $showSettings, chatItemTTL: chatModel.chatItemTTL) .navigationTitle("Your chat database") + .modifier(ThemedBackground(grouped: true)) } label: { - let color: Color = chatModel.chatDbEncrypted == false ? .orange : .secondary + let color: Color = chatModel.chatDbEncrypted == false ? .orange : theme.colors.secondary settingsRow("internaldrive", color: color) { HStack { Text("Database passphrase & export") @@ -386,13 +500,13 @@ struct SettingsView: View { switch (chatModel.tokenStatus) { case .new: icon = "bolt" - color = .secondary + color = theme.colors.secondary case .registered: icon = "bolt.fill" - color = .secondary + color = theme.colors.secondary case .invalid: icon = "bolt.slash" - color = .secondary + color = theme.colors.secondary case .confirmed: icon = "bolt.fill" color = .yellow @@ -401,10 +515,10 @@ struct SettingsView: View { color = .green case .expired: icon = "bolt.slash.fill" - color = .secondary + color = theme.colors.secondary case .none: icon = "bolt" - color = .secondary + color = theme.colors.secondary } return Image(systemName: icon) .padding(.trailing, 9) @@ -412,7 +526,7 @@ struct SettingsView: View { } } -func settingsRow(_ icon: String, color: Color = .secondary, content: @escaping () -> Content) -> some View { +func settingsRow(_ 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) @@ -423,7 +537,7 @@ func settingsRow(_ icon: String, color: Color = .secondary, cont struct ProfilePreview: View { var profileOf: NamedChat - var color = Color(uiColor: .tertiarySystemFill) + var color = Color(uiColor: .tertiarySystemGroupedBackground) var body: some View { HStack { diff --git a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift index 96eeffd16d..fa95c51d36 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddressView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddressView.swift @@ -13,6 +13,7 @@ import SimpleXChat struct UserAddressView: View { @Environment(\.dismiss) var dismiss: DismissAction @EnvironmentObject private var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme @State var viaCreateLinkView = false @State var shareViaProfile = false @State private var aas = AutoAcceptState() @@ -29,7 +30,7 @@ struct UserAddressView: View { case deleteAddress case profileAddress(on: Bool) case shareOnCreate - case error(title: LocalizedStringKey, error: LocalizedStringKey = "") + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -110,6 +111,7 @@ struct UserAddressView: View { createAddressButton() } footer: { Text("Create an address to let people connect with you.") + .foregroundColor(theme.colors.secondary) } Section { @@ -183,7 +185,7 @@ struct UserAddressView: View { }, secondaryButton: .cancel() ) case let .error(title, error): - return Alert(title: Text(title), message: Text(error)) + return mkAlert(title: title, message: error) } } } @@ -201,6 +203,7 @@ struct UserAddressView: View { learnMoreButton() } header: { Text("Address") + .foregroundColor(theme.colors.secondary) } if aas.enable { @@ -211,6 +214,7 @@ struct UserAddressView: View { deleteAddressButton() } footer: { Text("Your contacts will remain connected.") + .foregroundColor(theme.colors.secondary) } .id(bottomID) } @@ -251,7 +255,7 @@ struct UserAddressView: View { Button { showShareSheet(items: [simplexChatLink(userAddress.connReqContact)]) } label: { - settingsRow("square.and.arrow.up") { + settingsRow("square.and.arrow.up", color: theme.colors.secondary) { Text("Share address") } } @@ -261,7 +265,7 @@ struct UserAddressView: View { Button { showMailView = true } label: { - settingsRow("envelope") { + settingsRow("envelope", color: theme.colors.secondary) { Text("Invite friends") } } @@ -288,7 +292,7 @@ struct UserAddressView: View { } private func autoAcceptToggle() -> some View { - settingsRow("checkmark") { + settingsRow("checkmark", color: theme.colors.secondary) { Toggle("Auto-accept", isOn: $aas.enable) .onChange(of: aas.enable) { _ in saveAAS() @@ -300,16 +304,17 @@ struct UserAddressView: View { NavigationLink { UserAddressLearnMore() .navigationTitle("SimpleX address") + .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) } label: { - settingsRow("info.circle") { + settingsRow("info.circle", color: theme.colors.secondary) { Text("About SimpleX address") } } } private func shareWithContactsButton() -> some View { - settingsRow("person") { + settingsRow("person", color: theme.colors.secondary) { Toggle("Share with contacts", isOn: $shareViaProfile) .onChange(of: shareViaProfile) { on in if ignoreShareViaProfileChange { @@ -384,13 +389,14 @@ struct UserAddressView: View { .disabled(aas == savedAAS) } header: { Text("Auto-accept") + .foregroundColor(theme.colors.secondary) } } private func acceptIncognitoToggle() -> some View { settingsRow( aas.incognito ? "theatermasks.fill" : "theatermasks", - color: aas.incognito ? .indigo : .secondary + color: aas.incognito ? .indigo : theme.colors.secondary ) { Toggle("Accept incognito", isOn: $aas.incognito) } @@ -401,7 +407,7 @@ struct UserAddressView: View { Group { if aas.welcomeText.isEmpty { TextEditor(text: Binding.constant(NSLocalizedString("Enter welcome message… (optional)", comment: "placeholder"))) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .disabled(true) } TextEditor(text: $aas.welcomeText) diff --git a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift index 8c1a3bf4e1..160130bccc 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift @@ -8,6 +8,7 @@ import SimpleXChat struct UserProfilesView: View { @EnvironmentObject private var m: ChatModel + @EnvironmentObject private var theme: AppTheme @Binding var showSettings: Bool @Environment(\.editMode) private var editMode @AppStorage(DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE) private var showHiddenProfilesNotice = true @@ -29,7 +30,7 @@ struct UserProfilesView: View { case hiddenProfilesNotice case muteProfileAlert case activateUserError(error: String) - case error(title: LocalizedStringKey, error: LocalizedStringKey = "") + case error(title: LocalizedStringKey, error: LocalizedStringKey?) var id: String { switch self { @@ -100,6 +101,7 @@ struct UserProfilesView: View { } } footer: { Text("Tap to activate profile.") + .foregroundColor(theme.colors.secondary) .font(.body) .padding(.top, 8) @@ -111,6 +113,7 @@ struct UserProfilesView: View { } } .navigationTitle("Your chat profiles") + .modifier(ThemedBackground(grouped: true)) .searchable(text: $searchTextOrPassword, placement: .navigationBarDrawer(displayMode: .always)) .autocorrectionDisabled(true) .textInputAutocapitalization(.never) @@ -123,7 +126,7 @@ struct UserProfilesView: View { deleteModeButton("Profile and server connections", true) deleteModeButton("Local profile data only", false) } - .sheet(item: $selectedUser) { user in + .appSheet(item: $selectedUser) { user in HiddenProfileView(user: user, profileHidden: $profileHidden) } .onChange(of: profileHidden) { _ in @@ -131,7 +134,7 @@ struct UserProfilesView: View { withAnimation { profileHidden = false } } } - .sheet(item: $profileAction) { action in + .appSheet(item: $profileAction) { action in profileActionView(action) } .alert(item: $alert) { alert in @@ -169,7 +172,7 @@ struct UserProfilesView: View { message: Text(err) ) case let .error(title, error): - return Alert(title: Text(title), message: Text(error)) + return mkAlert(title: title, message: error) } } } @@ -210,7 +213,7 @@ struct UserProfilesView: View { actionHeader("Delete profile", user) Section { passwordField - settingsRow("trash") { + settingsRow("trash", color: theme.colors.secondary) { Button("Delete chat profile", role: .destructive) { profileAction = nil Task { await removeUser(user, delSMPQueues, viewPwd: actionPassword) } @@ -220,6 +223,7 @@ struct UserProfilesView: View { } footer: { if actionEnabled(user) { Text("All chats and messages will be deleted - this cannot be undone!") + .foregroundColor(theme.colors.secondary) .font(.callout) } } @@ -227,7 +231,7 @@ struct UserProfilesView: View { actionHeader("Unhide profile", user) Section { passwordField - settingsRow("lock.open") { + settingsRow("lock.open", color: theme.colors.secondary) { Button("Unhide chat profile") { profileAction = nil setUserPrivacy(user) { try await apiUnhideUser(user.userId, viewPwd: actionPassword) } @@ -237,6 +241,7 @@ struct UserProfilesView: View { } } } + .modifier(ThemedBackground()) } @ViewBuilder func actionHeader(_ title: LocalizedStringKey, _ user: User) -> some View { @@ -309,23 +314,23 @@ struct UserProfilesView: View { } } label: { HStack { - ProfileImage(imageStr: user.image, size: 44, color: Color(uiColor: .tertiarySystemFill)) + ProfileImage(imageStr: user.image, size: 44) .padding(.vertical, 4) .padding(.trailing, 12) Text(user.chatViewName) Spacer() if user.activeUser { - Image(systemName: "checkmark").foregroundColor(.primary) + Image(systemName: "checkmark").foregroundColor(theme.colors.onBackground) } else if user.hidden { - Image(systemName: "lock").foregroundColor(.secondary) + Image(systemName: "lock").foregroundColor(theme.colors.secondary) } else if !user.showNtfs { - Image(systemName: "speaker.slash").foregroundColor(.secondary) + Image(systemName: "speaker.slash").foregroundColor(theme.colors.secondary) } else { Image(systemName: "checkmark").foregroundColor(.clear) } } } - .foregroundColor(.primary) + .foregroundColor(theme.colors.onBackground) .swipeActions(edge: .leading, allowsFullSwipe: true) { if user.hidden { Button("Unhide") { @@ -356,7 +361,7 @@ struct UserProfilesView: View { } } } - .tint(.accentColor) + .tint(theme.colors.primary) } } if #available(iOS 16, *) { diff --git a/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff b/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff index e0477899be..40481d81f1 100644 --- a/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff +++ b/apps/ios/SimpleX Localizations/ar.xcloc/Localized Contents/ar.xliff @@ -39,7 +39,7 @@ !1 colored! - ! 1 ملون! + ! 1 مُلوَّن! No comment provided by engineer. @@ -69,7 +69,7 @@ %@ is not verified - %@ لم يتم التحقق منها + %@ لم يتم التحقق منه No comment provided by engineer. @@ -107,8 +107,9 @@ %d ثانية message ttl - + %d skipped message(s) + %d الرسائل المتخطية integrity error chat item @@ -121,12 +122,14 @@ %lld %@ No comment provided by engineer. - + %lld contact(s) selected + %lld تم اختيار جهات الاتصال No comment provided by engineer. - + %lld file(s) with total size of %@ + %lld الملفات ذات الحجم الإجمالي %@ No comment provided by engineer. @@ -134,8 +137,9 @@ %lld أعضاء No comment provided by engineer. - + %lld second(s) + %lld ثوانى No comment provided by engineer. @@ -185,7 +189,7 @@ **Add new contact**: to create your one-time QR Code or link for your contact. - ** إضافة جهة اتصال جديدة **: لإنشاء رمز QR لمرة واحدة أو رابط جهة الاتصال الخاصة بك. + ** إضافة جهة اتصال جديدة **: لإنشاء رمز QR لمرة واحدة أو رابط جهة الاتصال الخاصة بكم. No comment provided by engineer. @@ -195,12 +199,12 @@ **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. - ** المزيد من الخصوصية **: تحقق من الرسائل الجديدة كل 20 دقيقة. تتم مشاركة رمز الجهاز مع خادم SimpleX Chat ، ولكن ليس عدد جهات الاتصال أو الرسائل لديك. + ** المزيد من الخصوصية **: تحققوا من الرسائل الجديدة كل 20 دقيقة. تتم مشاركة رمز الجهاز مع خادم SimpleX Chat ، ولكن ليس عدد جهات الاتصال أو الرسائل لديكم. No comment provided by engineer. **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). - ** الأكثر خصوصية **: لا تستخدم خادم إشعارات SimpleX Chat ، وتحقق من الرسائل بشكل دوري في الخلفية (يعتمد على عدد مرات استخدامك للتطبيق). + ** الأكثر خصوصية **: لا تستخدم خادم إشعارات SimpleX Chat ، وتحقق من الرسائل بشكل دوري في الخلفية (يعتمد على عدد مرات استخدامكم للتطبيق). No comment provided by engineer. @@ -210,7 +214,7 @@ **Please note**: you will NOT be able to recover or change passphrase if you lose it. - ** يرجى ملاحظة **: لن تتمكن من استعادة أو تغيير عبارة المرور إذا فقدتها. + ** يرجى ملاحظة **: لن تتمكنوا من استعادة أو تغيير عبارة المرور إذا فقدتموها. No comment provided by engineer. @@ -305,7 +309,7 @@ A separate TCP connection will be used **for each chat profile you have in the app**. - سيتم استخدام اتصال TCP منفصل ** لكل ملف تعريف دردشة لديك في التطبيق **. + سيتم استخدام اتصال TCP منفصل ** لكل ملف تعريف دردشة لديكم في التطبيق **. No comment provided by engineer. @@ -355,24 +359,29 @@ Accept requests No comment provided by engineer. - + Add preset servers + إضافة خوادم محددة مسبقا No comment provided by engineer. - + Add profile + إضافة الملف الشخصي No comment provided by engineer. - + Add servers by scanning QR codes. + إضافة خوادم عن طريق مسح رموز QR. No comment provided by engineer. - - Add server… + + Add server + أضف الخادم No comment provided by engineer. - + Add to another device + أضف إلى جهاز آخر No comment provided by engineer. @@ -3667,7 +3676,7 @@ SimpleX servers cannot see your profile. ## In reply to - ## ردًا على + ## ردًّا على copied message info @@ -3675,6 +3684,208 @@ SimpleX servers cannot see your profile. %@ و %@ متصل No comment provided by engineer. + + %@ downloaded + %@ تم التنزيل + + + %@ and %@ + %@ و %@ + + + %@ connected + %@ متصل + + + %lld minutes + %lld دقائق + + + %@, %@ and %lld members + %@, %@ و %lld أعضاء + + + %d weeks + %d أسابيع + + + %@ uploaded + %@ تم الرفع + + + %@, %@ and %lld other members connected + %@, %@ و %lld أعضاء آخرين متصلين + + + %lld seconds + %lld ثواني + + + %u messages failed to decrypt. + %u فشلت عملية فك تشفير الرسائل. + + + %lld messages marked deleted + %lld الرسائل معلمه بالحذف + + + %lld messages moderated by %@ + %lld رسائل تمت إدارتها بواسطة %@ + + + %lld new interface languages + %lld لغات واجهة جديدة + + + %lld group events + %lld أحداث المجموعة + + + %lld messages blocked by admin + %lld رسائل محظورة بواسطه المسؤول + + + %lld messages blocked + %lld رسائل تم حظرها + + + %u messages skipped. + %u تم تخطي الرسائل. + + + **Add contact**: to create a new invitation link, or connect via a link you received. + **إضافة جهة اتصال**: لإنشاء رابط دعوة جديد، أو الاتصال عبر الرابط الذي تلقيتوهم. + + + **Create group**: to create a new group. + **إنشاء مجموعة**: لإنشاء مجموعة جديدة. + + + (this device v%@) + (هذا الجهاز v%@) + + + (new) + (جديد) + + + **Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection. + **يرجى الملاحظة**: سيؤدي استخدام نفس قاعدة البيانات على جهازين إلى كسر فك تشفير الرسائل من اتصالاتكم كحماية أمنية. + + + A new random profile will be shared. + سيتم مشاركة ملف تعريفي عشوائي جديد. + + + 30 seconds + 30 ثانيه + + + - more stable message delivery. +- a bit better groups. +- and more! + - تسليم رسائل أكثر استقرارًا. +- مجموعات أفضل قليلاً. +- والمزيد! + + + 0 sec + 0 ثانيه + + + 1 minute + 1 دقيقة + + + 5 minutes + 5 دقائق + + + <p>Hi!</p> +<p><a href="%@">Connect to me via SimpleX Chat</a></p> + <p>مرحبا!</p> +<p><a href="%@">أتصل بى من خلال SimpleX Chat</a></p> + + + 0s + 0 ث + + + A few more things + بعض الأشياء الأخرى + + + - connect to [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- delivery receipts (up to 20 members). +- faster and more stable. + - أتصل بـ [directory service](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) (BETA)! +- delivery receipts (up to 20 members). +- أسرع و أكثر اسْتِقْرارًا. + + + **Warning**: the archive will be removed. + **تحذير**: سيتم إزالة الأرشيف. + + + - optionally notify deleted contacts. +- profile names with spaces. +- and more! + - إخطار جهات الاتصال المحذوفة بشكل اختياري. +- أسماء الملفات الشخصية مع المسافات. +- والمزيد! + + + - voice messages up to 5 minutes. +- custom time to disappear. +- editing history. + - رسائل صوتية تصل مدتها إلى 5 دقائق. +- وقت مخصص للاختفاء. +- تعديل السجل. + + + Add welcome message + إضافة رسالة ترحيب + + + Abort changing address? + هل تريد إلغاء تغيير العنوان؟ + + + Add contact + إضافة جهة اتصال + + + Abort + إحباط + + + About SimpleX address + حول عنوان SimpleX + + + Accept connection request? + قبول طلب الاتصال؟ + + + Acknowledged + معترف به + + + Acknowledgement errors + أخطاء الإقرار + + + Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. + أضف عنوانًا إلى ملفكم الشخصي، حتى تتمكن جهات الاتصال الخاصة بكم من مشاركته مع أشخاص اخرين. سيتم إرسال تحديث الملف الشخصي إلى جهات الاتصال الخاصة بكم. + + + Abort changing address + إحباط تغيير العنوان + + + Active connections + اتصالات نشطة + diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index 3d0bb2bf2c..419f0ae864 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff @@ -2,7 +2,7 @@
- +
@@ -127,11 +127,6 @@ %@ е потвърдено No comment provided by engineer. - - %@ servers - %@ сървъри - No comment provided by engineer. - %@ uploaded %@ качено @@ -557,16 +552,16 @@ Повече за SimpleX адреса No comment provided by engineer. - - Accent color - Основен цвят + + Accent No comment provided by engineer. Accept Приеми accept contact request via notification - accept incoming call via notification + accept incoming call via notification + swipe action Accept connection request? @@ -581,7 +576,20 @@ Accept incognito Приеми инкогнито - accept contact request via notification + accept contact request via notification + swipe action + + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + No comment provided by engineer. + + + Active connections + No comment provided by engineer. Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. @@ -603,16 +611,16 @@ Добави профил No comment provided by engineer. + + Add server + Добави сървър + No comment provided by engineer. + Add servers by scanning QR codes. Добави сървъри чрез сканиране на QR кодове. No comment provided by engineer. - - Add server… - Добави сървър… - No comment provided by engineer. - Add to another device Добави към друго устройство @@ -623,6 +631,18 @@ Добави съобщение при посрещане No comment provided by engineer. + + Additional accent + No comment provided by engineer. + + + Additional accent 2 + No comment provided by engineer. + + + Additional secondary + No comment provided by engineer. + Address Адрес @@ -648,6 +668,10 @@ Разширени мрежови настройки No comment provided by engineer. + + Advanced settings + No comment provided by engineer. + All app data is deleted. Всички данни от приложението бяха изтрити. @@ -663,6 +687,10 @@ Всички данни се изтриват при въвеждане. No comment provided by engineer. + + All data is private to your device. + No comment provided by engineer. + All group members will remain connected. Всички членове на групата ще останат свързани. @@ -683,6 +711,10 @@ Всички нови съобщения от %@ ще бъдат скрити! No comment provided by engineer. + + All profiles + No comment provided by engineer. + All your contacts will remain connected. Всички ваши контакти ще останат свързани. @@ -708,11 +740,19 @@ Позволи обаждания само ако вашият контакт ги разрешава. No comment provided by engineer. + + Allow calls? + No comment provided by engineer. + Allow disappearing messages only if your contact allows it to you. Позволи изчезващи съобщения само ако вашият контакт ги разрешава. No comment provided by engineer. + + Allow downgrade + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Позволи необратимо изтриване на съобщение само ако вашият контакт го рарешава. (24 часа) @@ -738,6 +778,10 @@ Разреши изпращането на изчезващи съобщения. No comment provided by engineer. + + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Позволи необратимо изтриване на изпратените съобщения. (24 часа) @@ -808,6 +852,10 @@ Вече се присъединихте към групата! No comment provided by engineer. + + Always use private routing. + No comment provided by engineer. + Always use relay Винаги използвай реле @@ -873,11 +921,23 @@ Приложи No comment provided by engineer. + + Apply to + No comment provided by engineer. + Archive and upload Архивиране и качване No comment provided by engineer. + + Archive contacts to chat later. + No comment provided by engineer. + + + Archived contacts + No comment provided by engineer. + Archiving database Архивиране на база данни @@ -948,6 +1008,10 @@ Назад No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address Грешен адрес на настолното устройство @@ -973,6 +1037,14 @@ По-добри съобщения No comment provided by engineer. + + Better networking + No comment provided by engineer. + + + Black + No comment provided by engineer. + Block Блокирай @@ -1008,6 +1080,14 @@ Блокиран от админ No comment provided by engineer. + + Blur for better privacy. + No comment provided by engineer. + + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. И вие, и вашият контакт можете да добавяте реакции към съобщението. @@ -1053,11 +1133,23 @@ Обаждания No comment provided by engineer. + + Calls prohibited! + No comment provided by engineer. + Camera not available Камерата е неодстъпна No comment provided by engineer. + + Can't call contact + No comment provided by engineer. + + + Can't call member + No comment provided by engineer. + Can't invite contact! Не може да покани контакта! @@ -1068,6 +1160,10 @@ Не може да поканят контактите! No comment provided by engineer. + + Can't message member + No comment provided by engineer. + Cancel Отказ @@ -1083,11 +1179,19 @@ Няма достъп до Keychain за запазване на паролата за базата данни No comment provided by engineer. + + Cannot forward message + No comment provided by engineer. + Cannot receive file Файлът не може да бъде получен No comment provided by engineer. + + Capacity exceeded - recipient did not receive previously sent messages. + snd error text + Cellular Мобилна мрежа @@ -1149,6 +1253,10 @@ Архив на чата No comment provided by engineer. + + Chat colors + No comment provided by engineer. + Chat console Конзола @@ -1164,6 +1272,10 @@ Базата данни на чата е изтрита No comment provided by engineer. + + Chat database exported + No comment provided by engineer. + Chat database imported Базата данни на чат е импортирана @@ -1184,6 +1296,10 @@ Чатът е спрян. Ако вече сте използвали тази база данни на друго устройство, трябва да я прехвърлите обратно, преди да стартирате чата отново. No comment provided by engineer. + + Chat list + No comment provided by engineer. + Chat migrated! Чатът е мигриран! @@ -1194,6 +1310,10 @@ Чат настройки No comment provided by engineer. + + Chat theme + No comment provided by engineer. + Chats Чатове @@ -1224,10 +1344,22 @@ Избери от библиотеката No comment provided by engineer. + + Chunks deleted + No comment provided by engineer. + + + Chunks downloaded + No comment provided by engineer. + + + Chunks uploaded + No comment provided by engineer. + Clear Изчисти - No comment provided by engineer. + swipe action Clear conversation @@ -1249,9 +1381,12 @@ Изчисти проверката No comment provided by engineer. - - Colors - Цветове + + Color chats with the new themes. + No comment provided by engineer. + + + Color mode No comment provided by engineer. @@ -1264,11 +1399,19 @@ Сравнете кодовете за сигурност с вашите контакти. No comment provided by engineer. + + Completed + No comment provided by engineer. + Configure ICE servers Конфигурирай ICE сървъри No comment provided by engineer. + + Configured %@ servers + No comment provided by engineer. + Confirm Потвърди @@ -1279,11 +1422,19 @@ Потвърди kодa за достъп No comment provided by engineer. + + Confirm contact deletion? + No comment provided by engineer. + Confirm database upgrades Потвърди актуализаациите на базата данни No comment provided by engineer. + + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings Потвърди мрежовите настройки @@ -1329,6 +1480,10 @@ Свързване с настолно устройство No comment provided by engineer. + + Connect to your friends faster. + No comment provided by engineer. + Connect to yourself? Свърване със себе си? @@ -1368,16 +1523,28 @@ This is your own one-time link! Свързване с %@ No comment provided by engineer. + + Connected + No comment provided by engineer. + Connected desktop Свързано настолно устройство No comment provided by engineer. + + Connected servers + No comment provided by engineer. + Connected to desktop Свързан с настолно устройство No comment provided by engineer. + + Connecting + No comment provided by engineer. + Connecting to server… Свързване със сървъра… @@ -1388,6 +1555,10 @@ This is your own one-time link! Свързване със сървър…(грешка: %@) No comment provided by engineer. + + Connecting to contact, please wait or check later! + No comment provided by engineer. + Connecting to desktop Свързване с настолно устройство @@ -1398,6 +1569,10 @@ This is your own one-time link! Връзка No comment provided by engineer. + + Connection and servers status. + No comment provided by engineer. + Connection error Грешка при свързване @@ -1408,6 +1583,10 @@ This is your own one-time link! Грешка при свързване (AUTH) No comment provided by engineer. + + Connection notifications + No comment provided by engineer. + Connection request sent! Заявката за връзка е изпратена! @@ -1423,6 +1602,14 @@ This is your own one-time link! Времето на изчакване за установяване на връзката изтече No comment provided by engineer. + + Connection with desktop stopped + No comment provided by engineer. + + + Connections + No comment provided by engineer. + Contact allows Контактът позволява @@ -1433,6 +1620,10 @@ This is your own one-time link! Контактът вече съществува No comment provided by engineer. + + Contact deleted! + No comment provided by engineer. + Contact hidden: Контактът е скрит: @@ -1443,9 +1634,8 @@ This is your own one-time link! Контактът е свързан notification - - Contact is not connected yet! - Контактът все още не е свързан! + + Contact is deleted. No comment provided by engineer. @@ -1458,6 +1648,10 @@ This is your own one-time link! Настройки за контакт No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + No comment provided by engineer. + Contacts Контакти @@ -1473,10 +1667,18 @@ This is your own one-time link! Продължи No comment provided by engineer. + + Conversation deleted! + No comment provided by engineer. + Copy Копирай - chat item action + No comment provided by engineer. + + + Copy error + No comment provided by engineer. Core version: v%@ @@ -1553,6 +1755,10 @@ This is your own one-time link! Създай своя профил No comment provided by engineer. + + Created + No comment provided by engineer. + Created at Създаден на @@ -1588,6 +1794,10 @@ This is your own one-time link! Текуща парола… No comment provided by engineer. + + Current profile + No comment provided by engineer. + Currently maximum supported file size is %@. В момента максималният поддържан размер на файла е %@. @@ -1598,11 +1808,19 @@ This is your own one-time link! Персонализирано време No comment provided by engineer. + + Customize theme + No comment provided by engineer. + Dark Тъмна No comment provided by engineer. + + Dark mode colors + No comment provided by engineer. + Database ID ID в базата данни @@ -1701,6 +1919,10 @@ This is your own one-time link! Базата данни ще бъде мигрирана, когато приложението се рестартира No comment provided by engineer. + + Debug delivery + No comment provided by engineer. + Decentralized Децентрализиран @@ -1714,18 +1936,18 @@ This is your own one-time link! Delete Изтрий - chat item action + chat item action + swipe action + + + Delete %lld messages of members? + No comment provided by engineer. Delete %lld messages? Изтриване на %lld съобщения? No comment provided by engineer. - - Delete Contact - Изтрий контакт - No comment provided by engineer. - Delete address Изтрий адрес @@ -1781,11 +2003,8 @@ This is your own one-time link! Изтрий контакт No comment provided by engineer. - - Delete contact? -This cannot be undone! - Изтрий контакт? -Това не може да бъде отменено! + + Delete contact? No comment provided by engineer. @@ -1878,11 +2097,6 @@ This cannot be undone! Изтрий старата база данни? No comment provided by engineer. - - Delete pending connection - Изтрий предстоящата връзка - No comment provided by engineer. - Delete pending connection? Изтрий предстоящата връзка? @@ -1898,11 +2112,23 @@ This cannot be undone! Изтрий опашка server test step + + Delete up to 20 messages at once. + No comment provided by engineer. + Delete user profile? Изтрий потребителския профил? No comment provided by engineer. + + Delete without notification + No comment provided by engineer. + + + Deleted + No comment provided by engineer. + Deleted at Изтрито на @@ -1913,6 +2139,10 @@ This cannot be undone! Изтрито на: %@ copied message info + + Deletion errors + No comment provided by engineer. + Delivery Доставка @@ -1948,11 +2178,35 @@ This cannot be undone! Настолни устройства No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + No comment provided by engineer. + + + Destination server error: %@ + snd error text + + + Destination server version of %@ is incompatible with forwarding server %@. + No comment provided by engineer. + + + Detailed statistics + No comment provided by engineer. + + + Details + No comment provided by engineer. + Develop Разработване No comment provided by engineer. + + Developer options + No comment provided by engineer. + Developer tools Инструменти за разработчици @@ -2003,6 +2257,10 @@ This cannot be undone! Деактивиране за всички No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message Изчезващо съобщение @@ -2053,11 +2311,19 @@ This cannot be undone! Откриване през локалната мрежа No comment provided by engineer. + + Do NOT send messages directly, even if your or destination server does not support private routing. + No comment provided by engineer. + Do NOT use SimpleX for emergency calls. НЕ използвайте SimpleX за спешни повиквания. No comment provided by engineer. + + Do NOT use private routing. + No comment provided by engineer. + Do it later Отложи @@ -2093,6 +2359,10 @@ This cannot be undone! Изтегли chat item action + + Download errors + No comment provided by engineer. + Download failed Неуспешно изтегляне @@ -2103,6 +2373,14 @@ This cannot be undone! Свали файл server test step + + Downloaded + No comment provided by engineer. + + + Downloaded files + No comment provided by engineer. + Downloading archive Архива се изтегля @@ -2203,6 +2481,10 @@ This cannot be undone! Активирай kод за достъп за самоунищожение set passcode view + + Enabled + No comment provided by engineer. + Enabled for Активирано за @@ -2373,6 +2655,10 @@ This cannot be undone! Грешка при промяна на настройката No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address Грешка при създаване на адрес @@ -2423,11 +2709,6 @@ This cannot be undone! Грешка при изтриване на връзката No comment provided by engineer. - - Error deleting contact - Грешка при изтриване на контакт - No comment provided by engineer. - Error deleting database Грешка при изтриване на базата данни @@ -2473,6 +2754,10 @@ This cannot be undone! Грешка при експортиране на чат базата данни No comment provided by engineer. + + Error exporting theme: %@ + No comment provided by engineer. + Error importing chat database Грешка при импортиране на чат базата данни @@ -2498,11 +2783,23 @@ This cannot be undone! Грешка при получаване на файл No comment provided by engineer. + + Error reconnecting server + No comment provided by engineer. + + + Error reconnecting servers + No comment provided by engineer. + Error removing member Грешка при отстраняване на член No comment provided by engineer. + + Error resetting statistics + No comment provided by engineer. + Error saving %@ servers Грешка при запазване на %@ сървъра @@ -2621,7 +2918,8 @@ This cannot be undone! Error: %@ Грешка: %@ - No comment provided by engineer. + file error text + snd error text Error: URL is invalid @@ -2633,6 +2931,10 @@ This cannot be undone! Грешка: няма файл с база данни No comment provided by engineer. + + Errors + No comment provided by engineer. + Even when disabled in the conversation. Дори когато е деактивиран в разговора. @@ -2658,6 +2960,10 @@ This cannot be undone! Грешка при експортиране: No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. Експортиран архив на базата данни. @@ -2691,8 +2997,28 @@ This cannot be undone! Favorite Любим + swipe action + + + File error No comment provided by engineer. + + File not found - most likely file was deleted or cancelled. + file error text + + + File server error: %@ + file error text + + + File status + No comment provided by engineer. + + + File status: %@ + copied message info + File will be deleted from servers. Файлът ще бъде изтрит от сървърите. @@ -2713,6 +3039,10 @@ This cannot be undone! Файл: %@ No comment provided by engineer. + + Files + No comment provided by engineer. + Files & media Файлове и медия @@ -2818,6 +3148,28 @@ This cannot be undone! Препратено от No comment provided by engineer. + + Forwarding server %@ failed to connect to destination server %@. Please try later. + No comment provided by engineer. + + + Forwarding server address is incompatible with network settings: %@. + No comment provided by engineer. + + + Forwarding server version is incompatible with network settings: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + snd error text + Found desktop Намерено настолно устройство @@ -2863,6 +3215,14 @@ This cannot be undone! GIF файлове и стикери No comment provided by engineer. + + Good afternoon! + message preview + + + Good morning! + message preview + Group Група @@ -3143,6 +3503,10 @@ This cannot be undone! Неуспешно импортиране No comment provided by engineer. + + Import theme + No comment provided by engineer. + Importing archive Импортиране на архив @@ -3265,6 +3629,10 @@ This cannot be undone! Интерфейс No comment provided by engineer. + + Interface colors + No comment provided by engineer. + Invalid QR code Невалиден QR код @@ -3366,6 +3734,10 @@ This cannot be undone! 3. Връзката е била компрометирана. No comment provided by engineer. + + It protects your IP address and connections. + No comment provided by engineer. + It seems like you are already connected via this link. If it is not the case, there was an error (%@). Изглежда, че вече сте свързани чрез този линк. Ако не е така, има грешка (%@). @@ -3384,7 +3756,7 @@ This cannot be undone! Join Присъединяване - No comment provided by engineer. + swipe action Join group @@ -3428,6 +3800,10 @@ This is your link for group %@! Запази No comment provided by engineer. + + Keep conversation + No comment provided by engineer. + Keep the app open to use it from desktop Дръжте приложението отворено, за да го използвате от настолното устройство @@ -3471,7 +3847,7 @@ This is your link for group %@! Leave Напусни - No comment provided by engineer. + swipe action Leave group @@ -3603,11 +3979,23 @@ This is your link for group %@! Макс. 30 секунди, получено незабавно. No comment provided by engineer. + + Media & file servers + No comment provided by engineer. + + + Medium + blur media + Member Член No comment provided by engineer. + + Member inactive + item status text + Member role will be changed to "%@". All group members will be notified. Ролята на члена ще бъде променена на "%@". Всички членове на групата ще бъдат уведомени. @@ -3623,6 +4011,10 @@ This is your link for group %@! Членът ще бъде премахнат от групата - това не може да бъде отменено! No comment provided by engineer. + + Menus + No comment provided by engineer. + Message delivery error Грешка при доставката на съобщението @@ -3633,11 +4025,27 @@ This is your link for group %@! Потвърждениe за доставка на съобщения! No comment provided by engineer. + + Message delivery warning + item status text + Message draft Чернова на съобщение No comment provided by engineer. + + Message forwarded + item status text + + + Message may be delivered later if member becomes active. + item status description + + + Message queue info + No comment provided by engineer. + Message reactions Реакции на съобщения @@ -3653,11 +4061,27 @@ This is your link for group %@! Реакциите на съобщения са забранени в тази група. No comment provided by engineer. + + Message reception + No comment provided by engineer. + + + Message servers + No comment provided by engineer. + Message source remains private. Източникът на съобщението остава скрит. No comment provided by engineer. + + Message status + No comment provided by engineer. + + + Message status: %@ + copied message info + Message text Текст на съобщението @@ -3683,6 +4107,14 @@ This is your link for group %@! Съобщенията от %@ ще бъдат показани! No comment provided by engineer. + + Messages received + No comment provided by engineer. + + + Messages sent + No comment provided by engineer. + Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. Съобщенията, файловете и разговорите са защитени чрез **криптиране от край до край** с перфектна секретност при препращане, правдоподобно опровержение и възстановяване при взлом. @@ -3783,11 +4215,6 @@ This is your link for group %@! Най-вероятно тази връзка е изтрита. item status description - - Most likely this contact has deleted the connection with you. - Най-вероятно този контакт е изтрил връзката с вас. - No comment provided by engineer. - Multiple chat profiles Множество профили за чат @@ -3796,7 +4223,7 @@ This is your link for group %@! Mute Без звук - No comment provided by engineer. + swipe action Muted when inactive! @@ -3806,7 +4233,7 @@ This is your link for group %@! Name Име - No comment provided by engineer. + swipe action Network & servers @@ -3818,6 +4245,10 @@ This is your link for group %@! Мрежова връзка No comment provided by engineer. + + Network issues - message expired after many attempts to send it. + snd error text + Network management Управление на мрежата @@ -3843,6 +4274,10 @@ This is your link for group %@! Нов чат No comment provided by engineer. + + New chat experience 🎉 + No comment provided by engineer. + New contact request Нова заявка за контакт @@ -3873,6 +4308,10 @@ This is your link for group %@! Ново в %@ No comment provided by engineer. + + New media options + No comment provided by engineer. + New member role Нова членска роля @@ -3918,6 +4357,10 @@ This is your link for group %@! Няма токен за устройство! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + item status description + No filtered chats Няма филтрирани чатове @@ -3933,6 +4376,10 @@ This is your link for group %@! Няма история No comment provided by engineer. + + No info, try to reload + No comment provided by engineer. + No network connection Няма мрежова връзка @@ -3953,6 +4400,10 @@ This is your link for group %@! Несъвместим! No comment provided by engineer. + + Nothing selected + No comment provided by engineer. + Notifications Известия @@ -3980,7 +4431,7 @@ This is your link for group %@! Off Изключено - No comment provided by engineer. + blur media Ok @@ -4002,14 +4453,18 @@ This is your link for group %@! Линк за еднократна покана No comment provided by engineer. - - Onion hosts will be required for connection. Requires enabling VPN. - За свързване ще са необходими Onion хостове. Изисква се активиране на VPN. + + Onion hosts will be **required** for connection. +Requires compatible VPN. + За свързване ще са **необходими** Onion хостове. +Изисква се активиране на VPN. No comment provided by engineer. - - Onion hosts will be used when available. Requires enabling VPN. - Ще се използват Onion хостове, когато са налични. Изисква се активиране на VPN. + + Onion hosts will be used when available. +Requires compatible VPN. + Ще се използват Onion хостове, когато са налични. +Изисква се активиране на VPN. No comment provided by engineer. @@ -4022,6 +4477,10 @@ This is your link for group %@! Само потребителските устройства съхраняват потребителски профили, контакти, групи и съобщения, изпратени с **двуслойно криптиране от край до край**. No comment provided by engineer. + + Only delete conversation + No comment provided by engineer. + Only group owners can change group preferences. Само собствениците на групата могат да променят груповите настройки. @@ -4117,6 +4576,10 @@ This is your link for group %@! Отвори миграцията към друго устройство authentication reason + + Open server settings + No comment provided by engineer. + Open user profiles Отвори потребителските профили @@ -4157,6 +4620,10 @@ This is your link for group %@! Други No comment provided by engineer. + + Other %@ servers + No comment provided by engineer. + PING count PING бройка @@ -4222,6 +4689,10 @@ This is your link for group %@! Постави получения линк No comment provided by engineer. + + Pending + No comment provided by engineer. + People can connect to you only via the links you share. Хората могат да се свържат с вас само чрез ликовете, които споделяте. @@ -4242,11 +4713,24 @@ This is your link for group %@! Обаждания "картина в картина" No comment provided by engineer. + + Play from the chat list. + No comment provided by engineer. + + + Please ask your contact to enable calls. + No comment provided by engineer. + Please ask your contact to enable sending voice messages. Моля, попитайте вашия контакт, за да активирате изпращане на гласови съобщения. No comment provided by engineer. + + Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers. + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. Моля, проверете дали сте използвали правилния линк или поискайте вашия контакт, за да ви изпрати друг. @@ -4344,6 +4828,10 @@ Error: %@ Визуализация No comment provided by engineer. + + Previously connected servers + No comment provided by engineer. + Privacy & security Поверителност и сигурност @@ -4359,11 +4847,27 @@ Error: %@ Поверителни имена на файлове No comment provided by engineer. + + Private message routing + No comment provided by engineer. + + + Private message routing 🚀 + No comment provided by engineer. + Private notes Лични бележки name of notes to self + + Private routing + No comment provided by engineer. + + + Private routing error + No comment provided by engineer. + Profile and server connections Профилни и сървърни връзки @@ -4394,6 +4898,10 @@ Error: %@ Профилна парола No comment provided by engineer. + + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. Актуализацията на профила ще бъде изпратена до вашите контакти. @@ -4444,11 +4952,20 @@ Error: %@ Забрани изпращането на гласови съобщения. No comment provided by engineer. + + Protect IP address + No comment provided by engineer. + Protect app screen Защити екрана на приложението No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! Защитете чат профилите с парола! @@ -4464,6 +4981,14 @@ Error: %@ Време за изчакване на протокола за KB No comment provided by engineer. + + Proxied + No comment provided by engineer. + + + Proxied servers + No comment provided by engineer. + Push notifications Push известия @@ -4484,6 +5009,10 @@ Error: %@ Оценете приложението No comment provided by engineer. + + Reachable chat toolbar + No comment provided by engineer. + React… Реагирай… @@ -4492,7 +5021,7 @@ Error: %@ Read Прочетено - No comment provided by engineer. + swipe action Read more @@ -4529,6 +5058,10 @@ Error: %@ Потвърждениeто за доставка е деактивирано No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at Получено в @@ -4549,16 +5082,23 @@ Error: %@ Получено съобщение message info title + + Received messages + No comment provided by engineer. + + + Received reply + No comment provided by engineer. + + + Received total + No comment provided by engineer. + Receiving address will be changed to a different server. Address change will complete after sender comes online. Получаващият адрес ще бъде променен към друг сървър. Промяната на адреса ще завърши, след като подателят е онлайн. No comment provided by engineer. - - Receiving concurrency - Паралелност на получаване - No comment provided by engineer. - Receiving file will be stopped. Получаващият се файл ще бъде спрян. @@ -4584,11 +5124,31 @@ Error: %@ Получателите виждат актуализации, докато ги въвеждате. No comment provided by engineer. + + Reconnect + No comment provided by engineer. + Reconnect all connected servers to force message delivery. It uses additional traffic. Повторно се свържете с всички свързани сървъри, за да принудите доставката на съобщенията. Използва се допълнителен трафик. No comment provided by engineer. + + Reconnect all servers + No comment provided by engineer. + + + Reconnect all servers? + No comment provided by engineer. + + + Reconnect server to force message delivery. It uses additional traffic. + No comment provided by engineer. + + + Reconnect server? + No comment provided by engineer. + Reconnect servers? Повторно свърване със сървърите? @@ -4612,7 +5172,8 @@ Error: %@ Reject Отхвърляне - reject incoming call via notification + reject incoming call via notification + swipe action Reject (sender NOT notified) @@ -4639,6 +5200,10 @@ Error: %@ Премахване No comment provided by engineer. + + Remove image + No comment provided by engineer. + Remove member Острани член @@ -4709,16 +5274,36 @@ Error: %@ Нулиране No comment provided by engineer. + + Reset all hints + No comment provided by engineer. + + + Reset all statistics + No comment provided by engineer. + + + Reset all statistics? + No comment provided by engineer. + Reset colors Нулирай цветовете No comment provided by engineer. + + Reset to app theme + No comment provided by engineer. + Reset to defaults Възстановяване на настройките по подразбиране No comment provided by engineer. + + Reset to user theme + No comment provided by engineer. + Restart the app to create a new chat profile Рестартирайте приложението, за да създадете нов чат профил @@ -4759,11 +5344,6 @@ Error: %@ Покажи chat item action - - Revert - Отмени промените - No comment provided by engineer. - Revoke Отзови @@ -4789,9 +5369,12 @@ Error: %@ Стартиране на чат No comment provided by engineer. - - SMP servers - SMP сървъри + + SMP server + No comment provided by engineer. + + + Safely receive files No comment provided by engineer. @@ -4819,6 +5402,10 @@ Error: %@ Запази и уведоми членовете на групата No comment provided by engineer. + + Save and reconnect + No comment provided by engineer. + Save and update group profile Запази и актуализирай профила на групата @@ -4899,6 +5486,14 @@ Error: %@ Запазено съобщение message info title + + Scale + No comment provided by engineer. + + + Scan / Paste link + No comment provided by engineer. + Scan QR code Сканирай QR код @@ -4939,11 +5534,19 @@ Error: %@ Търсене или поставяне на SimpleX линк No comment provided by engineer. + + Secondary + No comment provided by engineer. + Secure queue Сигурна опашка server test step + + Secured + No comment provided by engineer. + Security assessment Оценка на сигурността @@ -4957,6 +5560,14 @@ Error: %@ Select Избери + chat item action + + + Selected %lld + No comment provided by engineer. + + + Selected chat preferences prohibit this message. No comment provided by engineer. @@ -4994,11 +5605,6 @@ Error: %@ Изпращайте потвърждениe за доставка на No comment provided by engineer. - - Send direct message - Изпрати лично съобщение - No comment provided by engineer. - Send direct message to connect Изпрати лично съобщение за свързване @@ -5009,6 +5615,10 @@ Error: %@ Изпрати изчезващо съобщение No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews Изпрати визуализация на линковете @@ -5019,6 +5629,18 @@ Error: %@ Изпрати съобщение на живо No comment provided by engineer. + + Send message to enable calls. + No comment provided by engineer. + + + Send messages directly when IP address is protected and your or destination server does not support private routing. + No comment provided by engineer. + + + Send messages directly when your or destination server does not support private routing. + No comment provided by engineer. + Send notifications Изпращай известия @@ -5109,6 +5731,10 @@ Error: %@ Изпратено на: %@ copied message info + + Sent directly + No comment provided by engineer. + Sent file event Събитие за изпратен файл @@ -5119,11 +5745,39 @@ Error: %@ Изпратено съобщение message info title + + Sent messages + No comment provided by engineer. + Sent messages will be deleted after set time. Изпратените съобщения ще бъдат изтрити след зададеното време. No comment provided by engineer. + + Sent reply + No comment provided by engineer. + + + Sent total + No comment provided by engineer. + + + Sent via proxy + No comment provided by engineer. + + + Server address + No comment provided by engineer. + + + Server address is incompatible with network settings. + srv error text. + + + Server address is incompatible with network settings: %@. + No comment provided by engineer. + Server requires authorization to create queues, check password Сървърът изисква оторизация за създаване на опашки, проверете паролата @@ -5139,11 +5793,31 @@ Error: %@ Тестът на сървъра е неуспешен! No comment provided by engineer. + + Server type + No comment provided by engineer. + + + Server version is incompatible with network settings. + srv error text + + + Server version is incompatible with your app: %@. + No comment provided by engineer. + Servers Сървъри No comment provided by engineer. + + Servers info + No comment provided by engineer. + + + Servers statistics will be reset - this cannot be undone! + No comment provided by engineer. + Session code Код на сесията @@ -5159,6 +5833,10 @@ Error: %@ Задай име на контакт… No comment provided by engineer. + + Set default theme + No comment provided by engineer. + Set group preferences Задай групови настройки @@ -5224,6 +5902,10 @@ Error: %@ Сподели адреса с контактите? No comment provided by engineer. + + Share from other apps. + No comment provided by engineer. + Share link Сподели линк @@ -5234,6 +5916,10 @@ Error: %@ Сподели този еднократен линк за връзка No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts Сподели с контактите @@ -5259,16 +5945,32 @@ Error: %@ Показване на последните съобщения в листа с чатовете No comment provided by engineer. + + Show message status + No comment provided by engineer. + + + Show percentage + No comment provided by engineer. + Show preview Показване на визуализация No comment provided by engineer. + + Show → on messages sent via private routing. + No comment provided by engineer. + Show: Покажи: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address SimpleX Адрес @@ -5344,6 +6046,10 @@ Error: %@ Опростен режим инкогнито No comment provided by engineer. + + Size + No comment provided by engineer. + Skip Пропускане @@ -5359,11 +6065,23 @@ Error: %@ Малки групи (максимум 20) No comment provided by engineer. + + Soft + blur media + + + Some file(s) were not exported: + No comment provided by engineer. + Some non-fatal errors occurred during import - you may see Chat console for more details. Някои не-фатални грешки са възникнали по време на импортиране - може да видите конзолата за повече подробности. No comment provided by engineer. + + Some non-fatal errors occurred during import: + No comment provided by engineer. + Somebody Някой @@ -5389,6 +6107,14 @@ Error: %@ Започни миграция No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop Спри @@ -5449,11 +6175,27 @@ Error: %@ Спиране на чата No comment provided by engineer. + + Strong + blur media + Submit Изпрати No comment provided by engineer. + + Subscribed + No comment provided by engineer. + + + Subscription errors + No comment provided by engineer. + + + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat Подкрепете SimpleX Chat @@ -5469,6 +6211,10 @@ Error: %@ Системна идентификация No comment provided by engineer. + + TCP connection + No comment provided by engineer. + TCP connection timeout Времето на изчакване за установяване на TCP връзка @@ -5529,9 +6275,8 @@ Error: %@ Докосни за сканиране No comment provided by engineer. - - Tap to start a new chat - Докосни за започване на нов чат + + Temporary file error No comment provided by engineer. @@ -5586,6 +6331,10 @@ It can happen because of some bug or when the connection is compromised.Приложението може да ви уведоми, когато получите съобщения или заявки за контакт - моля, отворете настройките, за да активирате. No comment provided by engineer. + + The app will ask to confirm downloads from unknown file servers (except .onion). + No comment provided by engineer. + The attempt to change database passphrase was not completed. Опитът за промяна на паролата на базата данни не беше завършен. @@ -5631,6 +6380,14 @@ It can happen because of some bug or when the connection is compromised.Съобщението ще бъде маркирано като модерирано за всички членове. No comment provided by engineer. + + The messages will be deleted for all members. + No comment provided by engineer. + + + The messages will be marked as moderated for all members. + No comment provided by engineer. + The next generation of private messaging Ново поколение поверителни съобщения @@ -5666,9 +6423,8 @@ It can happen because of some bug or when the connection is compromised.Текстът, който поставихте, не е SimpleX линк за връзка. No comment provided by engineer. - - Theme - Тема + + Themes No comment provided by engineer. @@ -5736,11 +6492,19 @@ It can happen because of some bug or when the connection is compromised.Това е вашят еднократен линк за връзка! No comment provided by engineer. + + This link was used with another mobile device, please create a new link on the desktop. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. Тази настройка се прилага за съобщения в текущия ви профил **%@**. No comment provided by engineer. + + Title + No comment provided by engineer. + To ask any questions and to receive updates: За да задавате въпроси и да получавате актуализации: @@ -5771,6 +6535,10 @@ It can happen because of some bug or when the connection is compromised.За да не се разкрива часовата зона, файловете с изображения/глас използват UTC. No comment provided by engineer. + + To protect your IP address, private routing uses your SMP servers to deliver messages. + No comment provided by engineer. + To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5798,16 +6566,32 @@ You will be prompted to complete authentication before this feature is enabled.< За да проверите криптирането от край до край с вашия контакт, сравнете (или сканирайте) кода на вашите устройства. No comment provided by engineer. + + Toggle chat list: + No comment provided by engineer. + Toggle incognito when connecting. Избор на инкогнито при свързване. No comment provided by engineer. + + Toolbar opacity + No comment provided by engineer. + + + Total + No comment provided by engineer. + Transport isolation Транспортна изолация No comment provided by engineer. + + Transport sessions + No comment provided by engineer. + Trying to connect to the server used to receive messages from this contact (error: %@). Опит за свързване със сървъра, използван за получаване на съобщения от този контакт (грешка: %@). @@ -5863,11 +6647,6 @@ You will be prompted to complete authentication before this feature is enabled.< Отблокирай член? No comment provided by engineer. - - Unexpected error: %@ - Неочаквана грешка: %@ - item status description - Unexpected migration state Неочаквано състояние на миграция @@ -5876,7 +6655,7 @@ You will be prompted to complete authentication before this feature is enabled.< Unfav. Премахни от любимите - No comment provided by engineer. + swipe action Unhide @@ -5913,6 +6692,10 @@ You will be prompted to complete authentication before this feature is enabled.< Непозната грешка No comment provided by engineer. + + Unknown servers! + No comment provided by engineer. + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. Освен ако не използвате интерфейса за повикване на iOS, активирайте режима "Не безпокой", за да избегнете прекъсвания. @@ -5948,12 +6731,12 @@ To connect, please ask your contact to create another connection link and check Unmute Уведомявай - No comment provided by engineer. + swipe action Unread Непрочетено - No comment provided by engineer. + swipe action Up to 100 last messages are sent to new members. @@ -5965,11 +6748,6 @@ To connect, please ask your contact to create another connection link and check Актуализация No comment provided by engineer. - - Update .onion hosts setting? - Актуализиране на настройката за .onion хостове? - No comment provided by engineer. - Update database passphrase Актуализирай паролата на базата данни @@ -5980,9 +6758,8 @@ To connect, please ask your contact to create another connection link and check Актуализиране на мрежовите настройки? No comment provided by engineer. - - Update transport isolation mode? - Актуализиране на режима на изолация на транспорта? + + Update settings? No comment provided by engineer. @@ -5990,16 +6767,15 @@ To connect, please ask your contact to create another connection link and check Актуализирането на настройките ще свърже отново клиента към всички сървъри. No comment provided by engineer. - - Updating this setting will re-connect the client to all servers. - Актуализирането на тази настройка ще свърже повторно клиента към всички сървъри. - No comment provided by engineer. - Upgrade and open chat Актуализирай и отвори чата No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed Неуспешно качване @@ -6010,6 +6786,14 @@ To connect, please ask your contact to create another connection link and check Качи файл server test step + + Uploaded + No comment provided by engineer. + + + Uploaded files + No comment provided by engineer. + Uploading archive Архивът се качва @@ -6060,6 +6844,14 @@ To connect, please ask your contact to create another connection link and check Използвай само локални известия? No comment provided by engineer. + + Use private routing with unknown servers when IP address is not protected. + No comment provided by engineer. + + + Use private routing with unknown servers. + No comment provided by engineer. + Use server Използвай сървър @@ -6070,14 +6862,17 @@ To connect, please ask your contact to create another connection link and check Използвайте приложението по време на разговора. No comment provided by engineer. + + Use the app with one hand. + No comment provided by engineer. + User profile Потребителски профил No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - Използването на .onion хостове изисква съвместим VPN доставчик. + + User selection No comment provided by engineer. @@ -6210,6 +7005,14 @@ To connect, please ask your contact to create another connection link and check Изчаква се получаването на видеото No comment provided by engineer. + + Wallpaper accent + No comment provided by engineer. + + + Wallpaper background + No comment provided by engineer. + Warning: starting chat on multiple devices is not supported and will cause message delivery failures Внимание: стартирането на чата на множество устройства не се поддържа и ще доведе до неуспешно изпращане на съобщения @@ -6295,19 +7098,34 @@ To connect, please ask your contact to create another connection link and check С намален разход на батерията. No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + No comment provided by engineer. + Wrong database passphrase Грешна парола за базата данни No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + file error text + Wrong passphrase! Грешна парола! No comment provided by engineer. - - XFTP servers - XFTP сървъри + + XFTP server No comment provided by engineer. @@ -6387,11 +7205,19 @@ Repeat join request? Поканени сте в групата No comment provided by engineer. + + You are not connected to these servers. Private routing is used to deliver messages to them. + No comment provided by engineer. + You can accept calls from lock screen, without device and app authentication. Можете да приемате обаждания от заключен екран, без идентификация на устройство и приложението. No comment provided by engineer. + + You can change it in Appearance settings. + No comment provided by engineer. + You can create it later Можете да го създадете по-късно @@ -6422,11 +7248,15 @@ Repeat join request? Можете да го направите видим за вашите контакти в SimpleX чрез Настройки. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Вече можете да изпращате съобщения до %@ notification body + + You can send messages to %@ from Archived contacts. + No comment provided by engineer. + You can set lock screen notification preview via settings. Можете да зададете визуализация на известията на заключен екран през настройките. @@ -6452,6 +7282,10 @@ Repeat join request? Можете да започнете чат през Настройки на приложението / База данни или като рестартирате приложението No comment provided by engineer. + + You can still view conversation with %@ in the list of chats. + No comment provided by engineer. + You can turn on SimpleX Lock via Settings. Можете да включите SimpleX заключване през Настройки. @@ -6494,11 +7328,6 @@ Repeat connection request? Изпрати отново заявката за свързване? No comment provided by engineer. - - You have no chats - Нямате чатове - No comment provided by engineer. - You have to enter passphrase every time the app starts - it is not stored on the device. Трябва да въвеждате парола при всяко стартиране на приложението - тя не се съхранява на устройството. @@ -6519,11 +7348,23 @@ Repeat connection request? Вие се присъединихте към тази група. Свързване с поканващия член на групата. No comment provided by engineer. + + You may migrate the exported database. + No comment provided by engineer. + + + You may save the exported archive. + No comment provided by engineer. + You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. Трябва да използвате най-новата версия на вашата чат база данни САМО на едно устройство, в противен случай може да спрете да получавате съобщения от някои контакти. No comment provided by engineer. + + You need to allow your contact to call to be able to call them. + No comment provided by engineer. + You need to allow your contact to send voice messages to be able to send them. Трябва да разрешите на вашия контакт да изпраща гласови съобщения, за да можете да ги изпращате. @@ -6639,13 +7480,6 @@ Repeat connection request? Вашите чат профили No comment provided by engineer. - - Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link). - Вашият контакт трябва да бъде онлайн, за да осъществите връзката. -Можете да откажете тази връзка и да премахнете контакта (и да опитате по -късно с нов линк). - No comment provided by engineer. - Your contact sent a file that is larger than currently supported maximum size (%@). Вашият контакт изпрати файл, който е по-голям от поддържания в момента максимален размер (%@). @@ -6793,6 +7627,10 @@ SimpleX сървърите не могат да видят вашия профи и %lld други събития No comment provided by engineer. + + attempts + No comment provided by engineer. + audio call (not e2e encrypted) аудио разговор (не е e2e криптиран) @@ -6833,6 +7671,10 @@ SimpleX сървърите не могат да видят вашия профи удебелен No comment provided by engineer. + + call + No comment provided by engineer. + call error грешка при повикване @@ -6983,6 +7825,10 @@ SimpleX сървърите не могат да видят вашия профи дни time unit + + decryption errors + No comment provided by engineer. + default (%@) по подразбиране (%@) @@ -7033,6 +7879,10 @@ SimpleX сървърите не могат да видят вашия профи дублирано съобщение integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted e2e криптиран @@ -7113,6 +7963,10 @@ SimpleX сървърите не могат да видят вашия профи събитие се случи No comment provided by engineer. + + expired + No comment provided by engineer. + forwarded препратено @@ -7143,6 +7997,10 @@ SimpleX сървърите не могат да видят вашия профи iOS Keychain ще се използва за сигурно съхраняване на паролата, след като рестартирате приложението или промените паролата - това ще позволи получаването на push известия. No comment provided by engineer. + + inactive + No comment provided by engineer. + incognito via contact address link инкогнито чрез линк с адрес за контакт @@ -7183,6 +8041,10 @@ SimpleX сървърите не могат да видят вашия профи покана за група %@ group name + + invite + No comment provided by engineer. + invited поканен @@ -7238,6 +8100,10 @@ SimpleX сървърите не могат да видят вашия профи свързан rcv group event chat item + + message + No comment provided by engineer. + message received получено съобщение @@ -7268,6 +8134,10 @@ SimpleX сървърите не могат да видят вашия профи месеци time unit + + mute + No comment provided by engineer. + never никога @@ -7320,6 +8190,14 @@ SimpleX сървърите не могат да видят вашия профи включено group pref value + + other + No comment provided by engineer. + + + other errors + No comment provided by engineer. + owner собственик @@ -7390,6 +8268,10 @@ SimpleX сървърите не могат да видят вашия профи запазено от %@ No comment provided by engineer. + + search + No comment provided by engineer. + sec сек. @@ -7415,6 +8297,12 @@ SimpleX сървърите не могат да видят вашия профи изпрати лично съобщение No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + queue info + set new contact address зададен нов адрес за контакт @@ -7455,11 +8343,23 @@ SimpleX сървърите не могат да видят вашия профи неизвестен connection info + + unknown servers + No comment provided by engineer. + unknown status неизвестен статус No comment provided by engineer. + + unmute + No comment provided by engineer. + + + unprotected + No comment provided by engineer. + updated group profile актуализиран профил на групата @@ -7500,6 +8400,10 @@ SimpleX сървърите не могат да видят вашия профи чрез реле No comment provided by engineer. + + video + No comment provided by engineer. + video call (not e2e encrypted) видео разговор (не е e2e криптиран) @@ -7525,6 +8429,10 @@ SimpleX сървърите не могат да видят вашия профи седмици time unit + + when IP hidden + No comment provided by engineer. + yes да @@ -7609,7 +8517,7 @@ SimpleX сървърите не могат да видят вашия профи
- +
@@ -7646,7 +8554,7 @@ SimpleX сървърите не могат да видят вашия профи
- +
@@ -7666,4 +8574,178 @@ SimpleX сървърите не могат да видят вашия профи
+ +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + No comment provided by engineer. + + + App is locked! + No comment provided by engineer. + + + Cancel + No comment provided by engineer. + + + Cannot access keychain to save database password + No comment provided by engineer. + + + Cannot forward message + No comment provided by engineer. + + + Comment + No comment provided by engineer. + + + Currently maximum supported file size is %@. + No comment provided by engineer. + + + Database downgrade required + No comment provided by engineer. + + + Database encrypted! + No comment provided by engineer. + + + Database error + No comment provided by engineer. + + + Database passphrase is different from saved in the keychain. + No comment provided by engineer. + + + Database passphrase is required to open chat. + No comment provided by engineer. + + + Database upgrade required + No comment provided by engineer. + + + Error preparing file + No comment provided by engineer. + + + Error preparing message + No comment provided by engineer. + + + Error: %@ + No comment provided by engineer. + + + File error + No comment provided by engineer. + + + Incompatible database version + No comment provided by engineer. + + + Invalid migration confirmation + No comment provided by engineer. + + + Keychain error + No comment provided by engineer. + + + Large file! + No comment provided by engineer. + + + No active profile + No comment provided by engineer. + + + Ok + No comment provided by engineer. + + + Open the app to downgrade the database. + No comment provided by engineer. + + + Open the app to upgrade the database. + No comment provided by engineer. + + + Passphrase + No comment provided by engineer. + + + Please create a profile in the SimpleX app + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending a message takes longer than expected. + No comment provided by engineer. + + + Sending message… + No comment provided by engineer. + + + Share + No comment provided by engineer. + + + Slow network? + No comment provided by engineer. + + + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + No comment provided by engineer. + + + Wait + No comment provided by engineer. + + + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/bg.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/bg.xcloc/contents.json b/apps/ios/SimpleX Localizations/bg.xcloc/contents.json index 23e8239ce8..5356e25a2e 100644 --- a/apps/ios/SimpleX Localizations/bg.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/bg.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "bg", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "15F31d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "15.4" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff b/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff index f599f9c300..b92196b78b 100644 --- a/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff +++ b/apps/ios/SimpleX Localizations/bn.xcloc/Localized Contents/bn.xliff @@ -386,8 +386,8 @@ Add servers by scanning QR codes. No comment provided by engineer.
- - Add server… + + Add server No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index a1ef39b2fb..a02203e630 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -2,7 +2,7 @@
- +
@@ -126,11 +126,6 @@ %@ je ověřený No comment provided by engineer. - - %@ servers - %@ servery - No comment provided by engineer. - %@ uploaded No comment provided by engineer. @@ -539,16 +534,16 @@ O SimpleX adrese No comment provided by engineer. - - Accent color - Zbarvení + + Accent No comment provided by engineer. Accept Přijmout accept contact request via notification - accept incoming call via notification + accept incoming call via notification + swipe action Accept connection request? @@ -563,7 +558,20 @@ Accept incognito Přijmout inkognito - accept contact request via notification + accept contact request via notification + swipe action + + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + No comment provided by engineer. + + + Active connections + No comment provided by engineer. Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. @@ -584,16 +592,16 @@ Přidat profil No comment provided by engineer. + + Add server + Přidat server + No comment provided by engineer. + Add servers by scanning QR codes. Přidejte servery skenováním QR kódů. No comment provided by engineer. - - Add server… - Přidat server… - No comment provided by engineer. - Add to another device Přidat do jiného zařízení @@ -604,6 +612,18 @@ Přidat uvítací zprávu No comment provided by engineer. + + Additional accent + No comment provided by engineer. + + + Additional accent 2 + No comment provided by engineer. + + + Additional secondary + No comment provided by engineer. + Address Adresa @@ -628,6 +648,10 @@ Pokročilá nastavení sítě No comment provided by engineer. + + Advanced settings + No comment provided by engineer. + All app data is deleted. Všechna data aplikace jsou smazána. @@ -643,6 +667,10 @@ Všechna data se při zadání vymažou. No comment provided by engineer. + + All data is private to your device. + No comment provided by engineer. + All group members will remain connected. Všichni členové skupiny zůstanou připojeni. @@ -661,6 +689,10 @@ All new messages from %@ will be hidden! No comment provided by engineer. + + All profiles + No comment provided by engineer. + All your contacts will remain connected. Všechny vaše kontakty zůstanou připojeny. @@ -685,11 +717,19 @@ Povolte hovory, pouze pokud je váš kontakt povolí. No comment provided by engineer. + + Allow calls? + No comment provided by engineer. + Allow disappearing messages only if your contact allows it to you. Povolte mizící zprávy, pouze pokud vám to váš kontakt dovolí. No comment provided by engineer. + + Allow downgrade + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Povolte nevratné smazání zprávy pouze v případě, že vám to váš kontakt dovolí. (24 hodin) @@ -715,6 +755,10 @@ Povolit odesílání mizících zpráv. No comment provided by engineer. + + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Povolit nevratné smazání odeslaných zpráv. (24 hodin) @@ -782,6 +826,10 @@ Already joining the group! No comment provided by engineer. + + Always use private routing. + No comment provided by engineer. + Always use relay Spojení přes relé @@ -845,10 +893,22 @@ Apply No comment provided by engineer. + + Apply to + No comment provided by engineer. + Archive and upload No comment provided by engineer. + + Archive contacts to chat later. + No comment provided by engineer. + + + Archived contacts + No comment provided by engineer. + Archiving database No comment provided by engineer. @@ -918,6 +978,10 @@ Zpět No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address No comment provided by engineer. @@ -941,6 +1005,14 @@ Lepší zprávy No comment provided by engineer. + + Better networking + No comment provided by engineer. + + + Black + No comment provided by engineer. + Block No comment provided by engineer. @@ -969,6 +1041,14 @@ Blocked by admin No comment provided by engineer. + + Blur for better privacy. + No comment provided by engineer. + + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. Vy i váš kontakt můžete přidávat reakce na zprávy. @@ -1014,10 +1094,22 @@ Hovory No comment provided by engineer. + + Calls prohibited! + No comment provided by engineer. + Camera not available No comment provided by engineer. + + Can't call contact + No comment provided by engineer. + + + Can't call member + No comment provided by engineer. + Can't invite contact! Nelze pozvat kontakt! @@ -1028,6 +1120,10 @@ Nelze pozvat kontakty! No comment provided by engineer. + + Can't message member + No comment provided by engineer. + Cancel Zrušit @@ -1042,11 +1138,19 @@ Nelze získat přístup ke klíčence pro uložení hesla databáze No comment provided by engineer. + + Cannot forward message + No comment provided by engineer. + Cannot receive file Nelze přijmout soubor No comment provided by engineer. + + Capacity exceeded - recipient did not receive previously sent messages. + snd error text + Cellular No comment provided by engineer. @@ -1107,6 +1211,10 @@ Chat se archivuje No comment provided by engineer. + + Chat colors + No comment provided by engineer. + Chat console Konzola pro chat @@ -1122,6 +1230,10 @@ Databáze chatu odstraněna No comment provided by engineer. + + Chat database exported + No comment provided by engineer. + Chat database imported Importovaná databáze chatu @@ -1141,6 +1253,10 @@ Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat. No comment provided by engineer. + + Chat list + No comment provided by engineer. + Chat migrated! No comment provided by engineer. @@ -1150,6 +1266,10 @@ Předvolby chatu No comment provided by engineer. + + Chat theme + No comment provided by engineer. + Chats Chaty @@ -1179,10 +1299,22 @@ Vybrat z knihovny No comment provided by engineer. + + Chunks deleted + No comment provided by engineer. + + + Chunks downloaded + No comment provided by engineer. + + + Chunks uploaded + No comment provided by engineer. + Clear Vyčistit - No comment provided by engineer. + swipe action Clear conversation @@ -1203,9 +1335,12 @@ Zrušte ověření No comment provided by engineer. - - Colors - Barvy + + Color chats with the new themes. + No comment provided by engineer. + + + Color mode No comment provided by engineer. @@ -1218,11 +1353,19 @@ Porovnejte bezpečnostní kódy se svými kontakty. No comment provided by engineer. + + Completed + No comment provided by engineer. + Configure ICE servers Konfigurace serverů ICE No comment provided by engineer. + + Configured %@ servers + No comment provided by engineer. + Confirm Potvrdit @@ -1233,11 +1376,19 @@ Potvrdit heslo No comment provided by engineer. + + Confirm contact deletion? + No comment provided by engineer. + Confirm database upgrades Potvrdit aktualizaci databáze No comment provided by engineer. + + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings No comment provided by engineer. @@ -1278,6 +1429,10 @@ Connect to desktop No comment provided by engineer. + + Connect to your friends faster. + No comment provided by engineer. + Connect to yourself? No comment provided by engineer. @@ -1310,14 +1465,26 @@ This is your own one-time link! Connect with %@ No comment provided by engineer. + + Connected + No comment provided by engineer. + Connected desktop No comment provided by engineer. + + Connected servers + No comment provided by engineer. + Connected to desktop No comment provided by engineer. + + Connecting + No comment provided by engineer. + Connecting to server… Připojování k serveru… @@ -1328,6 +1495,10 @@ This is your own one-time link! Připojování k serveru... (chyba: %@) No comment provided by engineer. + + Connecting to contact, please wait or check later! + No comment provided by engineer. + Connecting to desktop No comment provided by engineer. @@ -1337,6 +1508,10 @@ This is your own one-time link! Připojení No comment provided by engineer. + + Connection and servers status. + No comment provided by engineer. + Connection error Chyba připojení @@ -1347,6 +1522,10 @@ This is your own one-time link! Chyba spojení (AUTH) No comment provided by engineer. + + Connection notifications + No comment provided by engineer. + Connection request sent! Požadavek na připojení byl odeslán! @@ -1361,6 +1540,14 @@ This is your own one-time link! Časový limit připojení No comment provided by engineer. + + Connection with desktop stopped + No comment provided by engineer. + + + Connections + No comment provided by engineer. + Contact allows Kontakt povolil @@ -1371,6 +1558,10 @@ This is your own one-time link! Kontakt již existuje No comment provided by engineer. + + Contact deleted! + No comment provided by engineer. + Contact hidden: Skrytý kontakt: @@ -1381,9 +1572,8 @@ This is your own one-time link! Kontakt je připojen notification - - Contact is not connected yet! - Kontakt ještě není připojen! + + Contact is deleted. No comment provided by engineer. @@ -1396,6 +1586,10 @@ This is your own one-time link! Předvolby kontaktů No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + No comment provided by engineer. + Contacts Kontakty @@ -1411,10 +1605,18 @@ This is your own one-time link! Pokračovat No comment provided by engineer. + + Conversation deleted! + No comment provided by engineer. + Copy Kopírovat - chat item action + No comment provided by engineer. + + + Copy error + No comment provided by engineer. Core version: v%@ @@ -1487,6 +1689,10 @@ This is your own one-time link! Vytvořte si profil No comment provided by engineer. + + Created + No comment provided by engineer. + Created at No comment provided by engineer. @@ -1518,6 +1724,10 @@ This is your own one-time link! Aktuální přístupová fráze… No comment provided by engineer. + + Current profile + No comment provided by engineer. + Currently maximum supported file size is %@. Aktuálně maximální podporovaná velikost souboru je %@. @@ -1528,11 +1738,19 @@ This is your own one-time link! Vlastní čas No comment provided by engineer. + + Customize theme + No comment provided by engineer. + Dark Tmavý No comment provided by engineer. + + Dark mode colors + No comment provided by engineer. + Database ID ID databáze @@ -1631,6 +1849,10 @@ This is your own one-time link! Databáze bude přenesena po restartu aplikace No comment provided by engineer. + + Debug delivery + No comment provided by engineer. + Decentralized Decentralizované @@ -1644,17 +1866,17 @@ This is your own one-time link! Delete Smazat - chat item action + chat item action + swipe action + + + Delete %lld messages of members? + No comment provided by engineer. Delete %lld messages? No comment provided by engineer. - - Delete Contact - Smazat kontakt - No comment provided by engineer. - Delete address Odstranit adresu @@ -1709,9 +1931,8 @@ This is your own one-time link! Smazat kontakt No comment provided by engineer. - - Delete contact? -This cannot be undone! + + Delete contact? No comment provided by engineer. @@ -1803,11 +2024,6 @@ This cannot be undone! Smazat starou databázi? No comment provided by engineer. - - Delete pending connection - Smazat čekající připojení - No comment provided by engineer. - Delete pending connection? Smazat čekající připojení? @@ -1823,11 +2039,23 @@ This cannot be undone! Odstranit frontu server test step + + Delete up to 20 messages at once. + No comment provided by engineer. + Delete user profile? Smazat uživatelský profil? No comment provided by engineer. + + Delete without notification + No comment provided by engineer. + + + Deleted + No comment provided by engineer. + Deleted at Smazáno v @@ -1838,6 +2066,10 @@ This cannot be undone! Smazáno v: %@ copied message info + + Deletion errors + No comment provided by engineer. + Delivery Doručenka @@ -1870,11 +2102,35 @@ This cannot be undone! Desktop devices No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + No comment provided by engineer. + + + Destination server error: %@ + snd error text + + + Destination server version of %@ is incompatible with forwarding server %@. + No comment provided by engineer. + + + Detailed statistics + No comment provided by engineer. + + + Details + No comment provided by engineer. + Develop Vyvinout No comment provided by engineer. + + Developer options + No comment provided by engineer. + Developer tools Nástroje pro vývojáře @@ -1925,6 +2181,10 @@ This cannot be undone! Vypnout pro všechny No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message Mizící zpráva @@ -1973,11 +2233,19 @@ This cannot be undone! Discover via local network No comment provided by engineer. + + Do NOT send messages directly, even if your or destination server does not support private routing. + No comment provided by engineer. + Do NOT use SimpleX for emergency calls. NEpoužívejte SimpleX pro tísňová volání. No comment provided by engineer. + + Do NOT use private routing. + No comment provided by engineer. + Do it later Udělat později @@ -2011,6 +2279,10 @@ This cannot be undone! Download chat item action + + Download errors + No comment provided by engineer. + Download failed No comment provided by engineer. @@ -2020,6 +2292,14 @@ This cannot be undone! Stáhnout soubor server test step + + Downloaded + No comment provided by engineer. + + + Downloaded files + No comment provided by engineer. + Downloading archive No comment provided by engineer. @@ -2116,6 +2396,10 @@ This cannot be undone! Povolit sebedestrukční heslo set passcode view + + Enabled + No comment provided by engineer. + Enabled for No comment provided by engineer. @@ -2278,6 +2562,10 @@ This cannot be undone! Chyba změny nastavení No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address Chyba při vytváření adresy @@ -2327,11 +2615,6 @@ This cannot be undone! Chyba při mazání připojení No comment provided by engineer. - - Error deleting contact - Chyba mazání kontaktu - No comment provided by engineer. - Error deleting database Chyba při mazání databáze @@ -2376,6 +2659,10 @@ This cannot be undone! Chyba při exportu databáze chatu No comment provided by engineer. + + Error exporting theme: %@ + No comment provided by engineer. + Error importing chat database Chyba při importu databáze chatu @@ -2400,11 +2687,23 @@ This cannot be undone! Chyba při příjmu souboru No comment provided by engineer. + + Error reconnecting server + No comment provided by engineer. + + + Error reconnecting servers + No comment provided by engineer. + Error removing member Chyba při odebrání člena No comment provided by engineer. + + Error resetting statistics + No comment provided by engineer. + Error saving %@ servers Chyba při ukládání serverů %@ @@ -2519,7 +2818,8 @@ This cannot be undone! Error: %@ Chyba: %@ - No comment provided by engineer. + file error text + snd error text Error: URL is invalid @@ -2531,6 +2831,10 @@ This cannot be undone! Chyba: žádný soubor databáze No comment provided by engineer. + + Errors + No comment provided by engineer. + Even when disabled in the conversation. I při vypnutí v konverzaci. @@ -2555,6 +2859,10 @@ This cannot be undone! Chyba exportu: No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. Exportovaný archiv databáze. @@ -2586,8 +2894,28 @@ This cannot be undone! Favorite Oblíbené + swipe action + + + File error No comment provided by engineer. + + File not found - most likely file was deleted or cancelled. + file error text + + + File server error: %@ + file error text + + + File status + No comment provided by engineer. + + + File status: %@ + copied message info + File will be deleted from servers. Soubor bude smazán ze serverů. @@ -2608,6 +2936,10 @@ This cannot be undone! Soubor: %@ No comment provided by engineer. + + Files + No comment provided by engineer. + Files & media Soubory a média @@ -2706,6 +3038,28 @@ This cannot be undone! Forwarded from No comment provided by engineer. + + Forwarding server %@ failed to connect to destination server %@. Please try later. + No comment provided by engineer. + + + Forwarding server address is incompatible with network settings: %@. + No comment provided by engineer. + + + Forwarding server version is incompatible with network settings: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + snd error text + Found desktop No comment provided by engineer. @@ -2749,6 +3103,14 @@ This cannot be undone! GIFy a nálepky No comment provided by engineer. + + Good afternoon! + message preview + + + Good morning! + message preview + Group Skupina @@ -3023,6 +3385,10 @@ This cannot be undone! Import failed No comment provided by engineer. + + Import theme + No comment provided by engineer. + Importing archive No comment provided by engineer. @@ -3139,6 +3505,10 @@ This cannot be undone! Rozhranní No comment provided by engineer. + + Interface colors + No comment provided by engineer. + Invalid QR code No comment provided by engineer. @@ -3234,6 +3604,10 @@ This cannot be undone! 3. Spojení je kompromitováno. No comment provided by engineer. + + It protects your IP address and connections. + No comment provided by engineer. + It seems like you are already connected via this link. If it is not the case, there was an error (%@). Zdá se, že jste již připojeni prostřednictvím tohoto odkazu. Pokud tomu tak není, došlo k chybě (%@). @@ -3252,7 +3626,7 @@ This cannot be undone! Join Připojte se na - No comment provided by engineer. + swipe action Join group @@ -3290,6 +3664,10 @@ This is your link for group %@! Keep No comment provided by engineer. + + Keep conversation + No comment provided by engineer. + Keep the app open to use it from desktop No comment provided by engineer. @@ -3331,7 +3709,7 @@ This is your link for group %@! Leave Opustit - No comment provided by engineer. + swipe action Leave group @@ -3460,11 +3838,23 @@ This is your link for group %@! Max 30 vteřin, přijato okamžitě. No comment provided by engineer. + + Media & file servers + No comment provided by engineer. + + + Medium + blur media + Member Člen No comment provided by engineer. + + Member inactive + item status text + Member role will be changed to "%@". All group members will be notified. Role člena se změní na "%@". Všichni členové skupiny budou upozorněni. @@ -3480,6 +3870,10 @@ This is your link for group %@! Člen bude odstraněn ze skupiny - toto nelze vzít zpět! No comment provided by engineer. + + Menus + No comment provided by engineer. + Message delivery error Chyba doručení zprávy @@ -3490,11 +3884,27 @@ This is your link for group %@! Potvrzení o doručení zprávy! No comment provided by engineer. + + Message delivery warning + item status text + Message draft Návrh zprávy No comment provided by engineer. + + Message forwarded + item status text + + + Message may be delivered later if member becomes active. + item status description + + + Message queue info + No comment provided by engineer. + Message reactions Reakce na zprávy @@ -3510,10 +3920,26 @@ This is your link for group %@! Reakce na zprávy jsou v této skupině zakázány. No comment provided by engineer. + + Message reception + No comment provided by engineer. + + + Message servers + No comment provided by engineer. + Message source remains private. No comment provided by engineer. + + Message status + No comment provided by engineer. + + + Message status: %@ + copied message info + Message text Text zprávy @@ -3537,6 +3963,14 @@ This is your link for group %@! Messages from %@ will be shown! No comment provided by engineer. + + Messages received + No comment provided by engineer. + + + Messages sent + No comment provided by engineer. + Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. No comment provided by engineer. @@ -3627,11 +4061,6 @@ This is your link for group %@! Pravděpodobně je toto spojení smazáno. item status description - - Most likely this contact has deleted the connection with you. - Tento kontakt s největší pravděpodobností smazal spojení s vámi. - No comment provided by engineer. - Multiple chat profiles Více chatovacích profilů @@ -3640,7 +4069,7 @@ This is your link for group %@! Mute Ztlumit - No comment provided by engineer. + swipe action Muted when inactive! @@ -3650,7 +4079,7 @@ This is your link for group %@! Name Jméno - No comment provided by engineer. + swipe action Network & servers @@ -3661,6 +4090,10 @@ This is your link for group %@! Network connection No comment provided by engineer. + + Network issues - message expired after many attempts to send it. + snd error text + Network management No comment provided by engineer. @@ -3684,6 +4117,10 @@ This is your link for group %@! New chat No comment provided by engineer. + + New chat experience 🎉 + No comment provided by engineer. + New contact request Žádost o nový kontakt @@ -3714,6 +4151,10 @@ This is your link for group %@! Nový V %@ No comment provided by engineer. + + New media options + No comment provided by engineer. + New member role Nová role člena @@ -3759,6 +4200,10 @@ This is your link for group %@! Žádný token zařízení! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + item status description + No filtered chats Žádné filtrované chaty @@ -3774,6 +4219,10 @@ This is your link for group %@! Žádná historie No comment provided by engineer. + + No info, try to reload + No comment provided by engineer. + No network connection No comment provided by engineer. @@ -3792,6 +4241,10 @@ This is your link for group %@! Not compatible! No comment provided by engineer. + + Nothing selected + No comment provided by engineer. + Notifications Oznámení @@ -3818,7 +4271,7 @@ This is your link for group %@! Off Vypnout - No comment provided by engineer. + blur media Ok @@ -3840,14 +4293,18 @@ This is your link for group %@! Jednorázový zvací odkaz No comment provided by engineer. - - Onion hosts will be required for connection. Requires enabling VPN. - Pro připojení budou vyžadováni Onion hostitelé. Vyžaduje povolení sítě VPN. + + Onion hosts will be **required** for connection. +Requires compatible VPN. + Pro připojení budou vyžadováni Onion hostitelé. +Vyžaduje povolení sítě VPN. No comment provided by engineer. - - Onion hosts will be used when available. Requires enabling VPN. - Onion hostitelé budou použiti, pokud jsou k dispozici. Vyžaduje povolení sítě VPN. + + Onion hosts will be used when available. +Requires compatible VPN. + Onion hostitelé budou použiti, pokud jsou k dispozici. +Vyžaduje povolení sítě VPN. No comment provided by engineer. @@ -3860,6 +4317,10 @@ This is your link for group %@! Pouze klientská zařízení ukládají uživatelské profily, kontakty, skupiny a zprávy odeslané s **2vrstvým šifrováním typu end-to-end**. No comment provided by engineer. + + Only delete conversation + No comment provided by engineer. + Only group owners can change group preferences. Předvolby skupiny mohou měnit pouze vlastníci skupiny. @@ -3953,6 +4414,10 @@ This is your link for group %@! Open migration to another device authentication reason + + Open server settings + No comment provided by engineer. + Open user profiles Otevřít uživatelské profily @@ -3987,6 +4452,10 @@ This is your link for group %@! Other No comment provided by engineer. + + Other %@ servers + No comment provided by engineer. + PING count Počet PING @@ -4048,6 +4517,10 @@ This is your link for group %@! Paste the link you received No comment provided by engineer. + + Pending + No comment provided by engineer. + People can connect to you only via the links you share. Lidé se s vámi mohou spojit pouze prostřednictvím odkazů, které sdílíte. @@ -4067,11 +4540,24 @@ This is your link for group %@! Picture-in-picture calls No comment provided by engineer. + + Play from the chat list. + No comment provided by engineer. + + + Please ask your contact to enable calls. + No comment provided by engineer. + Please ask your contact to enable sending voice messages. Prosím, požádejte kontaktní osobu, aby umožnila odesílání hlasových zpráv. No comment provided by engineer. + + Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers. + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. Zkontrolujte, zda jste použili správný odkaz, nebo požádejte kontakt, aby vám poslal jiný. @@ -4166,6 +4652,10 @@ Error: %@ Náhled No comment provided by engineer. + + Previously connected servers + No comment provided by engineer. + Privacy & security Ochrana osobních údajů a zabezpečení @@ -4181,10 +4671,26 @@ Error: %@ Soukromé názvy souborů No comment provided by engineer. + + Private message routing + No comment provided by engineer. + + + Private message routing 🚀 + No comment provided by engineer. + Private notes name of notes to self + + Private routing + No comment provided by engineer. + + + Private routing error + No comment provided by engineer. + Profile and server connections Profil a připojení k serveru @@ -4212,6 +4718,10 @@ Error: %@ Heslo profilu No comment provided by engineer. + + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. Aktualizace profilu bude zaslána vašim kontaktům. @@ -4261,11 +4771,20 @@ Error: %@ Zakázat odesílání hlasových zpráv. No comment provided by engineer. + + Protect IP address + No comment provided by engineer. + Protect app screen Ochrana obrazovky aplikace No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! Chraňte své chat profily heslem! @@ -4281,6 +4800,14 @@ Error: %@ Časový limit protokolu na KB No comment provided by engineer. + + Proxied + No comment provided by engineer. + + + Proxied servers + No comment provided by engineer. + Push notifications Nabízená oznámení @@ -4299,6 +4826,10 @@ Error: %@ Ohodnoťte aplikaci No comment provided by engineer. + + Reachable chat toolbar + No comment provided by engineer. + React… Reagovat… @@ -4307,7 +4838,7 @@ Error: %@ Read Číst - No comment provided by engineer. + swipe action Read more @@ -4343,6 +4874,10 @@ Error: %@ Informace o dodání jsou zakázány No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at Přijato v @@ -4363,15 +4898,23 @@ Error: %@ Přijatá zpráva message info title + + Received messages + No comment provided by engineer. + + + Received reply + No comment provided by engineer. + + + Received total + No comment provided by engineer. + Receiving address will be changed to a different server. Address change will complete after sender comes online. Přijímací adresa bude změněna na jiný server. Změna adresy bude dokončena po připojení odesílatele. No comment provided by engineer. - - Receiving concurrency - No comment provided by engineer. - Receiving file will be stopped. Příjem souboru bude zastaven. @@ -4395,11 +4938,31 @@ Error: %@ Příjemci uvidí aktualizace během jejich psaní. No comment provided by engineer. + + Reconnect + No comment provided by engineer. + Reconnect all connected servers to force message delivery. It uses additional traffic. Znovu připojte všechny připojené servery a vynuťte doručení zprávy. Využívá další provoz. No comment provided by engineer. + + Reconnect all servers + No comment provided by engineer. + + + Reconnect all servers? + No comment provided by engineer. + + + Reconnect server to force message delivery. It uses additional traffic. + No comment provided by engineer. + + + Reconnect server? + No comment provided by engineer. + Reconnect servers? Znovu připojit servery? @@ -4423,7 +4986,8 @@ Error: %@ Reject Odmítnout - reject incoming call via notification + reject incoming call via notification + swipe action Reject (sender NOT notified) @@ -4450,6 +5014,10 @@ Error: %@ Odstranit No comment provided by engineer. + + Remove image + No comment provided by engineer. + Remove member Odstranit člena @@ -4515,16 +5083,36 @@ Error: %@ Obnovit No comment provided by engineer. + + Reset all hints + No comment provided by engineer. + + + Reset all statistics + No comment provided by engineer. + + + Reset all statistics? + No comment provided by engineer. + Reset colors Obnovení barev No comment provided by engineer. + + Reset to app theme + No comment provided by engineer. + Reset to defaults Obnovení výchozího nastavení No comment provided by engineer. + + Reset to user theme + No comment provided by engineer. + Restart the app to create a new chat profile Restartujte aplikaci pro vytvoření nového chat profilu @@ -4564,11 +5152,6 @@ Error: %@ Odhalit chat item action - - Revert - Vrátit - No comment provided by engineer. - Revoke Odvolat @@ -4594,9 +5177,12 @@ Error: %@ Spustit chat No comment provided by engineer. - - SMP servers - SMP servery + + SMP server + No comment provided by engineer. + + + Safely receive files No comment provided by engineer. @@ -4623,6 +5209,10 @@ Error: %@ Uložit a upozornit členy skupiny No comment provided by engineer. + + Save and reconnect + No comment provided by engineer. + Save and update group profile Uložit a aktualizovat profil skupiny @@ -4700,6 +5290,14 @@ Error: %@ Saved message message info title + + Scale + No comment provided by engineer. + + + Scan / Paste link + No comment provided by engineer. + Scan QR code Skenovat QR kód @@ -4737,11 +5335,19 @@ Error: %@ Search or paste SimpleX link No comment provided by engineer. + + Secondary + No comment provided by engineer. + Secure queue Zabezpečit frontu server test step + + Secured + No comment provided by engineer. + Security assessment Posouzení bezpečnosti @@ -4755,6 +5361,14 @@ Error: %@ Select Vybrat + chat item action + + + Selected %lld + No comment provided by engineer. + + + Selected chat preferences prohibit this message. No comment provided by engineer. @@ -4792,11 +5406,6 @@ Error: %@ Potvrzení o doručení zasílat na No comment provided by engineer. - - Send direct message - Odeslat přímou zprávu - No comment provided by engineer. - Send direct message to connect Odeslat přímou zprávu pro připojení @@ -4807,6 +5416,10 @@ Error: %@ Poslat mizící zprávu No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews Odesílání náhledů odkazů @@ -4817,6 +5430,18 @@ Error: %@ Odeslat živou zprávu No comment provided by engineer. + + Send message to enable calls. + No comment provided by engineer. + + + Send messages directly when IP address is protected and your or destination server does not support private routing. + No comment provided by engineer. + + + Send messages directly when your or destination server does not support private routing. + No comment provided by engineer. + Send notifications Odeslat oznámení @@ -4906,6 +5531,10 @@ Error: %@ Posláno v: % @ copied message info + + Sent directly + No comment provided by engineer. + Sent file event Odeslaná událost souboru @@ -4916,11 +5545,39 @@ Error: %@ Poslaná zpráva message info title + + Sent messages + No comment provided by engineer. + Sent messages will be deleted after set time. Odeslané zprávy se po uplynutí nastavené doby odstraní. No comment provided by engineer. + + Sent reply + No comment provided by engineer. + + + Sent total + No comment provided by engineer. + + + Sent via proxy + No comment provided by engineer. + + + Server address + No comment provided by engineer. + + + Server address is incompatible with network settings. + srv error text. + + + Server address is incompatible with network settings: %@. + No comment provided by engineer. + Server requires authorization to create queues, check password Server vyžaduje autorizaci pro vytváření front, zkontrolujte heslo @@ -4936,11 +5593,31 @@ Error: %@ Test serveru se nezdařil! No comment provided by engineer. + + Server type + No comment provided by engineer. + + + Server version is incompatible with network settings. + srv error text + + + Server version is incompatible with your app: %@. + No comment provided by engineer. + Servers Servery No comment provided by engineer. + + Servers info + No comment provided by engineer. + + + Servers statistics will be reset - this cannot be undone! + No comment provided by engineer. + Session code No comment provided by engineer. @@ -4955,6 +5632,10 @@ Error: %@ Nastavení jména kontaktu… No comment provided by engineer. + + Set default theme + No comment provided by engineer. + Set group preferences Nastavení skupinových předvoleb @@ -5018,6 +5699,10 @@ Error: %@ Sdílet adresu s kontakty? No comment provided by engineer. + + Share from other apps. + No comment provided by engineer. + Share link Sdílet odkaz @@ -5027,6 +5712,10 @@ Error: %@ Share this 1-time invite link No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts Sdílet s kontakty @@ -5051,16 +5740,32 @@ Error: %@ Zobrazit poslední zprávy No comment provided by engineer. + + Show message status + No comment provided by engineer. + + + Show percentage + No comment provided by engineer. + Show preview Zobrazení náhledu No comment provided by engineer. + + Show → on messages sent via private routing. + No comment provided by engineer. + Show: Zobrazit: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address SimpleX Adresa @@ -5134,6 +5839,10 @@ Error: %@ Zjednodušený inkognito režim No comment provided by engineer. + + Size + No comment provided by engineer. + Skip Přeskočit @@ -5149,11 +5858,23 @@ Error: %@ Malé skupiny (max. 20) No comment provided by engineer. + + Soft + blur media + + + Some file(s) were not exported: + No comment provided by engineer. + Some non-fatal errors occurred during import - you may see Chat console for more details. Během importu došlo k nezávažným chybám - podrobnosti naleznete v chat konzoli. No comment provided by engineer. + + Some non-fatal errors occurred during import: + No comment provided by engineer. + Somebody Někdo @@ -5177,6 +5898,14 @@ Error: %@ Zahájit přenesení No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop Zastavit @@ -5235,11 +5964,27 @@ Error: %@ Stopping chat No comment provided by engineer. + + Strong + blur media + Submit Odeslat No comment provided by engineer. + + Subscribed + No comment provided by engineer. + + + Subscription errors + No comment provided by engineer. + + + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat Podpořte SimpleX Chat @@ -5255,6 +6000,10 @@ Error: %@ Ověření systému No comment provided by engineer. + + TCP connection + No comment provided by engineer. + TCP connection timeout Časový limit připojení TCP @@ -5312,9 +6061,8 @@ Error: %@ Tap to scan No comment provided by engineer. - - Tap to start a new chat - Klepnutím na zahájíte nový chat + + Temporary file error No comment provided by engineer. @@ -5369,6 +6117,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován Aplikace vás může upozornit na přijaté zprávy nebo žádosti o kontakt - povolte to v nastavení. No comment provided by engineer. + + The app will ask to confirm downloads from unknown file servers (except .onion). + No comment provided by engineer. + The attempt to change database passphrase was not completed. Pokus o změnu přístupové fráze databáze nebyl dokončen. @@ -5413,6 +6165,14 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován Zpráva bude pro všechny členy označena jako moderovaná. No comment provided by engineer. + + The messages will be deleted for all members. + No comment provided by engineer. + + + The messages will be marked as moderated for all members. + No comment provided by engineer. + The next generation of private messaging Nová generace soukromých zpráv @@ -5447,9 +6207,8 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován The text you pasted is not a SimpleX link. No comment provided by engineer. - - Theme - Téma + + Themes No comment provided by engineer. @@ -5511,11 +6270,19 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován This is your own one-time link! No comment provided by engineer. + + This link was used with another mobile device, please create a new link on the desktop. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. Toto nastavení platí pro zprávy ve vašem aktuálním chat profilu **%@**. No comment provided by engineer. + + Title + No comment provided by engineer. + To ask any questions and to receive updates: Chcete-li položit jakékoli dotazy a dostávat aktuality: @@ -5545,6 +6312,10 @@ Může se to stát kvůli nějaké chybě, nebo pokud je spojení kompromitován K ochraně časového pásma používají obrazové/hlasové soubory UTC. No comment provided by engineer. + + To protect your IP address, private routing uses your SMP servers to deliver messages. + No comment provided by engineer. + To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5572,16 +6343,32 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. Chcete-li ověřit koncové šifrování u svého kontaktu, porovnejte (nebo naskenujte) kód na svých zařízeních. No comment provided by engineer. + + Toggle chat list: + No comment provided by engineer. + Toggle incognito when connecting. Změnit inkognito režim při připojení. No comment provided by engineer. + + Toolbar opacity + No comment provided by engineer. + + + Total + No comment provided by engineer. + Transport isolation Izolace transportu No comment provided by engineer. + + Transport sessions + No comment provided by engineer. + Trying to connect to the server used to receive messages from this contact (error: %@). Pokus o připojení k serveru používanému k přijímání zpráv od tohoto kontaktu (chyba: %@). @@ -5631,11 +6418,6 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. Unblock member? No comment provided by engineer. - - Unexpected error: %@ - Neočekávaná chyba: %@ - item status description - Unexpected migration state Neočekávaný stav přenášení @@ -5644,7 +6426,7 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. Unfav. Odobl. - No comment provided by engineer. + swipe action Unhide @@ -5681,6 +6463,10 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. Neznámá chyba No comment provided by engineer. + + Unknown servers! + No comment provided by engineer. + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. Při nepoužívání rozhraní volání iOS, povolte režim Nerušit, abyste se vyhnuli vyrušování. @@ -5714,12 +6500,12 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Unmute Zrušit ztlumení - No comment provided by engineer. + swipe action Unread Nepřečtený - No comment provided by engineer. + swipe action Up to 100 last messages are sent to new members. @@ -5730,11 +6516,6 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Aktualizovat No comment provided by engineer. - - Update .onion hosts setting? - Aktualizovat nastavení hostitelů .onion? - No comment provided by engineer. - Update database passphrase Aktualizovat přístupovou frázi databáze @@ -5745,9 +6526,8 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Aktualizovat nastavení sítě? No comment provided by engineer. - - Update transport isolation mode? - Aktualizovat režim dopravní izolace? + + Update settings? No comment provided by engineer. @@ -5755,16 +6535,15 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Aktualizací nastavení se klient znovu připojí ke všem serverům. No comment provided by engineer. - - Updating this setting will re-connect the client to all servers. - Aktualizace tohoto nastavení znovu připojí klienta ke všem serverům. - No comment provided by engineer. - Upgrade and open chat Zvýšit a otevřít chat No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed No comment provided by engineer. @@ -5774,6 +6553,14 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Nahrát soubor server test step + + Uploaded + No comment provided by engineer. + + + Uploaded files + No comment provided by engineer. + Uploading archive No comment provided by engineer. @@ -5821,6 +6608,14 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Use only local notifications? No comment provided by engineer. + + Use private routing with unknown servers when IP address is not protected. + No comment provided by engineer. + + + Use private routing with unknown servers. + No comment provided by engineer. + Use server Použít server @@ -5830,14 +6625,17 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Use the app while in the call. No comment provided by engineer. + + Use the app with one hand. + No comment provided by engineer. + User profile Profil uživatele No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - Použití hostitelů .onion vyžaduje kompatibilního poskytovatele VPN. + + User selection No comment provided by engineer. @@ -5961,6 +6759,14 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Čekám na video No comment provided by engineer. + + Wallpaper accent + No comment provided by engineer. + + + Wallpaper background + No comment provided by engineer. + Warning: starting chat on multiple devices is not supported and will cause message delivery failures No comment provided by engineer. @@ -6038,19 +6844,34 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu With reduced battery usage. No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + No comment provided by engineer. + Wrong database passphrase Špatná přístupová fráze k databázi No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + file error text + Wrong passphrase! Špatná přístupová fráze! No comment provided by engineer. - - XFTP servers - XFTP servery + + XFTP server No comment provided by engineer. @@ -6121,11 +6942,19 @@ Repeat join request? Jste pozváni do skupiny No comment provided by engineer. + + You are not connected to these servers. Private routing is used to deliver messages to them. + No comment provided by engineer. + You can accept calls from lock screen, without device and app authentication. Můžete přijímat hovory z obrazovky zámku, bez ověření zařízení a aplikace. No comment provided by engineer. + + You can change it in Appearance settings. + No comment provided by engineer. + You can create it later Můžete vytvořit později @@ -6154,11 +6983,15 @@ Repeat join request? You can make it visible to your SimpleX contacts via Settings. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Nyní můžete posílat zprávy %@ notification body + + You can send messages to %@ from Archived contacts. + No comment provided by engineer. + You can set lock screen notification preview via settings. Náhled oznámení na zamykací obrazovce můžete změnit v nastavení. @@ -6184,6 +7017,10 @@ Repeat join request? Chat můžete zahájit prostřednictvím aplikace Nastavení / Databáze nebo restartováním aplikace No comment provided by engineer. + + You can still view conversation with %@ in the list of chats. + No comment provided by engineer. + You can turn on SimpleX Lock via Settings. Zámek SimpleX můžete zapnout v Nastavení. @@ -6222,11 +7059,6 @@ Repeat join request? Repeat connection request? No comment provided by engineer. - - You have no chats - Nemáte žádné konverzace - No comment provided by engineer. - You have to enter passphrase every time the app starts - it is not stored on the device. Musíte zadat přístupovou frázi při každém spuštění aplikace - není uložena v zařízení. @@ -6247,11 +7079,23 @@ Repeat connection request? Připojili jste se k této skupině. Připojení k pozvání člena skupiny. No comment provided by engineer. + + You may migrate the exported database. + No comment provided by engineer. + + + You may save the exported archive. + No comment provided by engineer. + You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. Nejnovější verzi databáze chatu musíte používat POUZE v jednom zařízení, jinak se může stát, že přestanete přijímat zprávy od některých kontaktů. No comment provided by engineer. + + You need to allow your contact to call to be able to call them. + No comment provided by engineer. + You need to allow your contact to send voice messages to be able to send them. Abyste mohli odesílat hlasové zprávy, musíte je povolit svému kontaktu. @@ -6365,13 +7209,6 @@ Repeat connection request? Vaše chat profily No comment provided by engineer. - - Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link). - K dokončení připojení, musí být váš kontakt online. -Toto připojení můžete zrušit a kontakt odebrat (a zkusit to později s novým odkazem). - No comment provided by engineer. - Your contact sent a file that is larger than currently supported maximum size (%@). Kontakt odeslal soubor, který je větší než aktuálně podporovaná maximální velikost (%@). @@ -6515,6 +7352,10 @@ Servery SimpleX nevidí váš profil. and %lld other events No comment provided by engineer. + + attempts + No comment provided by engineer. + audio call (not e2e encrypted) zvukový hovor (nešifrovaný e2e) @@ -6551,6 +7392,10 @@ Servery SimpleX nevidí váš profil. tučně No comment provided by engineer. + + call + No comment provided by engineer. + call error chyba volání @@ -6700,6 +7545,10 @@ Servery SimpleX nevidí váš profil. dní time unit + + decryption errors + No comment provided by engineer. + default (%@) výchozí (%@) @@ -6749,6 +7598,10 @@ Servery SimpleX nevidí váš profil. duplicitní zpráva integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted e2e šifrované @@ -6828,6 +7681,10 @@ Servery SimpleX nevidí váš profil. event happened No comment provided by engineer. + + expired + No comment provided by engineer. + forwarded No comment provided by engineer. @@ -6857,6 +7714,10 @@ Servery SimpleX nevidí váš profil. Klíčenka pro iOS bude použita k bezpečnému uložení přístupové fráze po restartování aplikace nebo změně přístupové fráze – umožní příjem oznámení push. No comment provided by engineer. + + inactive + No comment provided by engineer. + incognito via contact address link inkognito přes odkaz na kontaktní adresu @@ -6897,6 +7758,10 @@ Servery SimpleX nevidí váš profil. pozvánka do skupiny %@ group name + + invite + No comment provided by engineer. + invited pozvánka @@ -6951,6 +7816,10 @@ Servery SimpleX nevidí váš profil. připojeno rcv group event chat item + + message + No comment provided by engineer. + message received zpráva přijata @@ -6981,6 +7850,10 @@ Servery SimpleX nevidí váš profil. měsíců time unit + + mute + No comment provided by engineer. + never nikdy @@ -7033,6 +7906,14 @@ Servery SimpleX nevidí váš profil. zapnuto group pref value + + other + No comment provided by engineer. + + + other errors + No comment provided by engineer. + owner vlastník @@ -7097,6 +7978,10 @@ Servery SimpleX nevidí váš profil. saved from %@ No comment provided by engineer. + + search + No comment provided by engineer. + sec sek @@ -7122,6 +8007,12 @@ Servery SimpleX nevidí váš profil. odeslat přímou zprávu No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + queue info + set new contact address profile update event chat item @@ -7158,10 +8049,22 @@ Servery SimpleX nevidí váš profil. neznámý connection info + + unknown servers + No comment provided by engineer. + unknown status No comment provided by engineer. + + unmute + No comment provided by engineer. + + + unprotected + No comment provided by engineer. + updated group profile aktualizoval profil skupiny @@ -7200,6 +8103,10 @@ Servery SimpleX nevidí váš profil. přes relé No comment provided by engineer. + + video + No comment provided by engineer. + video call (not e2e encrypted) videohovoru (nešifrovaného e2e) @@ -7225,6 +8132,10 @@ Servery SimpleX nevidí váš profil. týdnů time unit + + when IP hidden + No comment provided by engineer. + yes ano @@ -7306,7 +8217,7 @@ Servery SimpleX nevidí váš profil.
- +
@@ -7342,7 +8253,7 @@ Servery SimpleX nevidí váš profil.
- +
@@ -7362,4 +8273,178 @@ Servery SimpleX nevidí váš profil.
+ +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + No comment provided by engineer. + + + App is locked! + No comment provided by engineer. + + + Cancel + No comment provided by engineer. + + + Cannot access keychain to save database password + No comment provided by engineer. + + + Cannot forward message + No comment provided by engineer. + + + Comment + No comment provided by engineer. + + + Currently maximum supported file size is %@. + No comment provided by engineer. + + + Database downgrade required + No comment provided by engineer. + + + Database encrypted! + No comment provided by engineer. + + + Database error + No comment provided by engineer. + + + Database passphrase is different from saved in the keychain. + No comment provided by engineer. + + + Database passphrase is required to open chat. + No comment provided by engineer. + + + Database upgrade required + No comment provided by engineer. + + + Error preparing file + No comment provided by engineer. + + + Error preparing message + No comment provided by engineer. + + + Error: %@ + No comment provided by engineer. + + + File error + No comment provided by engineer. + + + Incompatible database version + No comment provided by engineer. + + + Invalid migration confirmation + No comment provided by engineer. + + + Keychain error + No comment provided by engineer. + + + Large file! + No comment provided by engineer. + + + No active profile + No comment provided by engineer. + + + Ok + No comment provided by engineer. + + + Open the app to downgrade the database. + No comment provided by engineer. + + + Open the app to upgrade the database. + No comment provided by engineer. + + + Passphrase + No comment provided by engineer. + + + Please create a profile in the SimpleX app + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending a message takes longer than expected. + No comment provided by engineer. + + + Sending message… + No comment provided by engineer. + + + Share + No comment provided by engineer. + + + Slow network? + No comment provided by engineer. + + + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + No comment provided by engineer. + + + Wait + No comment provided by engineer. + + + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/contents.json b/apps/ios/SimpleX Localizations/cs.xcloc/contents.json index 5c7c929ee3..aaa2ed1ee0 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/cs.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "cs", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "15F31d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "15.4" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index 29384aecc4..29adfbabf0 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -2,7 +2,7 @@
- +
@@ -127,11 +127,6 @@ %@ wurde erfolgreich überprüft No comment provided by engineer. - - %@ servers - %@-Server - No comment provided by engineer. - %@ uploaded %@ hochgeladen @@ -529,17 +524,17 @@ Abort - Abbrechen + Beenden No comment provided by engineer. Abort changing address - Wechsel der Empfängeradresse abbrechen + Wechsel der Empfängeradresse beenden No comment provided by engineer. Abort changing address? - Wechsel der Empfängeradresse abbrechen? + Wechsel der Empfängeradresse beenden? No comment provided by engineer. @@ -557,16 +552,17 @@ Über die SimpleX-Adresse No comment provided by engineer. - - Accent color - Akzentfarbe + + Accent + Akzent No comment provided by engineer. Accept Annehmen accept contact request via notification - accept incoming call via notification + accept incoming call via notification + swipe action Accept connection request? @@ -581,7 +577,23 @@ Accept incognito Inkognito akzeptieren - accept contact request via notification + accept contact request via notification + swipe action + + + Acknowledged + Bestätigt + No comment provided by engineer. + + + Acknowledgement errors + Fehler bei der Bestätigung + No comment provided by engineer. + + + Active connections + Aktive Verbindungen + No comment provided by engineer. Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. @@ -603,16 +615,16 @@ Profil hinzufügen No comment provided by engineer. + + Add server + Füge Server hinzu + No comment provided by engineer. + Add servers by scanning QR codes. Fügen Sie Server durch Scannen der QR Codes hinzu. No comment provided by engineer. - - Add server… - Füge Server hinzu… - No comment provided by engineer. - Add to another device Einem anderen Gerät hinzufügen @@ -623,6 +635,21 @@ Begrüßungsmeldung hinzufügen No comment provided by engineer. + + Additional accent + Erste Akzentfarbe + No comment provided by engineer. + + + Additional accent 2 + Zusätzlicher Akzent 2 + No comment provided by engineer. + + + Additional secondary + Zweite Akzentfarbe + No comment provided by engineer. + Address Adresse @@ -630,7 +657,7 @@ Address change will be aborted. Old receiving address will be used. - Der Wechsel der Empfängeradresse wird abgebrochen. Die bisherige Adresse wird weiter verwendet. + Der Wechsel der Empfängeradresse wird beendet. Die bisherige Adresse wird weiter verwendet. No comment provided by engineer. @@ -648,6 +675,11 @@ Erweiterte Netzwerkeinstellungen No comment provided by engineer. + + Advanced settings + Erweiterte Einstellungen + No comment provided by engineer. + All app data is deleted. Werden die App-Daten komplett gelöscht. @@ -655,7 +687,7 @@ All chats and messages will be deleted - this cannot be undone! - Alle Chats und Nachrichten werden gelöscht! Dies kann nicht rückgängig gemacht werden! + Es werden alle Chats und Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! No comment provided by engineer. @@ -663,6 +695,11 @@ Alle Daten werden gelöscht, sobald dieser eingegeben wird. No comment provided by engineer. + + All data is private to your device. + Alle Daten werden nur auf Ihrem Gerät gespeichert. + No comment provided by engineer. + All group members will remain connected. Alle Gruppenmitglieder bleiben verbunden. @@ -670,12 +707,12 @@ All messages will be deleted - this cannot be undone! - Es werden alle Nachrichten gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden! + Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! No comment provided by engineer. All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you. - Alle Nachrichten werden gelöscht - dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht. + Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht. No comment provided by engineer. @@ -683,6 +720,11 @@ Von %@ werden alle neuen Nachrichten ausgeblendet! No comment provided by engineer. + + All profiles + Alle Profile + No comment provided by engineer. + All your contacts will remain connected. Alle Ihre Kontakte bleiben verbunden. @@ -695,7 +737,7 @@ All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays. - Alle Ihre Kontakte, Unterhaltungen und Dateien werden sicher verschlüsselt und in Daten-Paketen auf die konfigurierten XTFP-Server hochgeladen. + Alle Ihre Kontakte, Unterhaltungen und Dateien werden sicher verschlüsselt und in Daten-Paketen auf die konfigurierten XTFP-Relais hochgeladen. No comment provided by engineer. @@ -708,11 +750,21 @@ Erlauben Sie Anrufe nur dann, wenn es Ihr Kontakt ebenfalls erlaubt. No comment provided by engineer. + + Allow calls? + Anrufe erlauben? + No comment provided by engineer. + Allow disappearing messages only if your contact allows it to you. Erlauben Sie verschwindende Nachrichten nur dann, wenn es Ihr Kontakt ebenfalls erlaubt. No comment provided by engineer. + + Allow downgrade + Herabstufung erlauben + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Erlauben Sie das unwiederbringliche Löschen von Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt. (24 Stunden) @@ -738,6 +790,11 @@ Das Senden von verschwindenden Nachrichten erlauben. No comment provided by engineer. + + Allow sharing + Teilen erlauben + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Unwiederbringliches löschen von gesendeten Nachrichten erlauben. (24 Stunden) @@ -808,6 +865,11 @@ Sie sind bereits Mitglied der Gruppe! No comment provided by engineer. + + Always use private routing. + Sie nutzen immer privates Routing. + No comment provided by engineer. + Always use relay Über ein Relais verbinden @@ -873,11 +935,26 @@ Anwenden No comment provided by engineer. + + Apply to + Anwenden auf + No comment provided by engineer. + Archive and upload Archivieren und Hochladen No comment provided by engineer. + + Archive contacts to chat later. + Kontakte für spätere Chats archivieren. + No comment provided by engineer. + + + Archived contacts + Archivierte Kontakte + No comment provided by engineer. + Archiving database Datenbank wird archiviert @@ -948,6 +1025,11 @@ Zurück No comment provided by engineer. + + Background + Hintergrund-Farbe + No comment provided by engineer. + Bad desktop address Falsche Desktop-Adresse @@ -973,6 +1055,16 @@ Verbesserungen bei Nachrichten No comment provided by engineer. + + Better networking + Kontrollieren Sie Ihr Netzwerk + No comment provided by engineer. + + + Black + Schwarz + No comment provided by engineer. + Block Blockieren @@ -1008,6 +1100,16 @@ wurde vom Administrator blockiert No comment provided by engineer. + + Blur for better privacy. + Für bessere Privatsphäre verpixeln. + No comment provided by engineer. + + + Blur media + Medium unscharf machen + No comment provided by engineer. + Both you and your contact can add message reactions. Sowohl Sie, als auch Ihr Kontakt können Reaktionen auf Nachrichten geben. @@ -1053,11 +1155,26 @@ Anrufe No comment provided by engineer. + + Calls prohibited! + Anrufe nicht zugelassen! + No comment provided by engineer. + Camera not available Kamera nicht verfügbar No comment provided by engineer. + + Can't call contact + Kontakt kann nicht angerufen werden + No comment provided by engineer. + + + Can't call member + Mitglied kann nicht angerufen werden + No comment provided by engineer. + Can't invite contact! Kontakt kann nicht eingeladen werden! @@ -1068,6 +1185,11 @@ Kontakte können nicht eingeladen werden! No comment provided by engineer. + + Can't message member + Mitglied kann nicht benachrichtigt werden + No comment provided by engineer. + Cancel Abbrechen @@ -1083,14 +1205,24 @@ Die App kann nicht auf den Schlüsselbund zugreifen, um das Datenbank-Passwort zu speichern No comment provided by engineer. + + Cannot forward message + Die Nachricht kann nicht weitergeleitet werden + No comment provided by engineer. + Cannot receive file Datei kann nicht empfangen werden No comment provided by engineer. + + Capacity exceeded - recipient did not receive previously sent messages. + Kapazität überschritten - der Empfänger hat die zuvor gesendeten Nachrichten nicht empfangen. + snd error text + Cellular - Zellulär + Mobilfunknetz No comment provided by engineer. @@ -1149,6 +1281,11 @@ Datenbank Archiv No comment provided by engineer. + + Chat colors + Chat-Farben + No comment provided by engineer. + Chat console Chat-Konsole @@ -1164,6 +1301,11 @@ Chat-Datenbank gelöscht No comment provided by engineer. + + Chat database exported + Chat-Datenbank wurde exportiert + No comment provided by engineer. + Chat database imported Chat-Datenbank importiert @@ -1184,6 +1326,11 @@ Der Chat ist angehalten. Wenn Sie diese Datenbank bereits auf einem anderen Gerät genutzt haben, sollten Sie diese vor dem Starten des Chats wieder zurückspielen. No comment provided by engineer. + + Chat list + Chat-Liste + No comment provided by engineer. + Chat migrated! Chat wurde migriert! @@ -1194,6 +1341,11 @@ Chat-Präferenzen No comment provided by engineer. + + Chat theme + Chat-Design + No comment provided by engineer. + Chats Chats @@ -1224,10 +1376,25 @@ Aus dem Fotoalbum auswählen No comment provided by engineer. + + Chunks deleted + Daten-Pakete gelöscht + No comment provided by engineer. + + + Chunks downloaded + Daten-Pakete heruntergeladen + No comment provided by engineer. + + + Chunks uploaded + Daten-Pakete hochgeladen + No comment provided by engineer. + Clear Löschen - No comment provided by engineer. + swipe action Clear conversation @@ -1249,9 +1416,14 @@ Überprüfung zurücknehmen No comment provided by engineer. - - Colors - Farben + + Color chats with the new themes. + Farbige Chats mit neuen Designs. + No comment provided by engineer. + + + Color mode + Farbvariante No comment provided by engineer. @@ -1264,11 +1436,21 @@ Vergleichen Sie die Sicherheitscodes mit Ihren Kontakten. No comment provided by engineer. + + Completed + Abgeschlossen + No comment provided by engineer. + Configure ICE servers ICE-Server konfigurieren No comment provided by engineer. + + Configured %@ servers + Konfigurierte %@ Server + No comment provided by engineer. + Confirm Bestätigen @@ -1279,11 +1461,21 @@ Zugangscode bestätigen No comment provided by engineer. + + Confirm contact deletion? + Löschen des Kontakts bestätigen? + No comment provided by engineer. + Confirm database upgrades Datenbank-Aktualisierungen bestätigen No comment provided by engineer. + + Confirm files from unknown servers. + Dateien von unbekannten Servern bestätigen. + No comment provided by engineer. + Confirm network settings Bestätigen Sie die Netzwerkeinstellungen @@ -1329,6 +1521,11 @@ Mit dem Desktop verbinden No comment provided by engineer. + + Connect to your friends faster. + Schneller mit Ihren Freunden verbinden. + No comment provided by engineer. + Connect to yourself? Mit Ihnen selbst verbinden? @@ -1368,16 +1565,31 @@ Das ist Ihr eigener Einmal-Link! Mit %@ verbinden No comment provided by engineer. + + Connected + Verbunden + No comment provided by engineer. + Connected desktop Verbundener Desktop No comment provided by engineer. + + Connected servers + Verbundene Server + No comment provided by engineer. + Connected to desktop Mit dem Desktop verbunden No comment provided by engineer. + + Connecting + Verbinden + No comment provided by engineer. + Connecting to server… Mit dem Server verbinden… @@ -1388,6 +1600,11 @@ Das ist Ihr eigener Einmal-Link! Mit dem Server verbinden… (Fehler: %@) No comment provided by engineer. + + Connecting to contact, please wait or check later! + Verbinde mit Kontakt, bitte warten oder später erneut überprüfen! + No comment provided by engineer. + Connecting to desktop Mit dem Desktop verbinden @@ -1398,6 +1615,11 @@ Das ist Ihr eigener Einmal-Link! Verbindung No comment provided by engineer. + + Connection and servers status. + Verbindungs- und Server-Status. + No comment provided by engineer. + Connection error Verbindungsfehler @@ -1408,6 +1630,11 @@ Das ist Ihr eigener Einmal-Link! Verbindungsfehler (AUTH) No comment provided by engineer. + + Connection notifications + Verbindungsbenachrichtigungen + No comment provided by engineer. + Connection request sent! Verbindungsanfrage wurde gesendet! @@ -1423,6 +1650,16 @@ Das ist Ihr eigener Einmal-Link! Verbindungszeitüberschreitung No comment provided by engineer. + + Connection with desktop stopped + Die Verbindung mit dem Desktop wurde gestoppt + No comment provided by engineer. + + + Connections + Verbindungen + No comment provided by engineer. + Contact allows Der Kontakt erlaubt @@ -1433,6 +1670,11 @@ Das ist Ihr eigener Einmal-Link! Der Kontakt ist bereits vorhanden No comment provided by engineer. + + Contact deleted! + Kontakt gelöscht! + No comment provided by engineer. + Contact hidden: Kontakt verborgen: @@ -1443,9 +1685,9 @@ Das ist Ihr eigener Einmal-Link! Mit Ihrem Kontakt verbunden notification - - Contact is not connected yet! - Ihr Kontakt ist noch nicht verbunden! + + Contact is deleted. + Kontakt wurde gelöscht. No comment provided by engineer. @@ -1458,6 +1700,11 @@ Das ist Ihr eigener Einmal-Link! Kontakt-Präferenzen No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + Kontakt wird gelöscht. Dies kann nicht rückgängig gemacht werden! + No comment provided by engineer. + Contacts Kontakte @@ -1473,10 +1720,20 @@ Das ist Ihr eigener Einmal-Link! Weiter No comment provided by engineer. + + Conversation deleted! + Unterhaltung gelöscht! + No comment provided by engineer. + Copy Kopieren - chat item action + No comment provided by engineer. + + + Copy error + Fehlermeldung kopieren + No comment provided by engineer. Core version: v%@ @@ -1553,6 +1810,11 @@ Das ist Ihr eigener Einmal-Link! Erstellen Sie Ihr Profil No comment provided by engineer. + + Created + Erstellt + No comment provided by engineer. + Created at Erstellt um @@ -1588,6 +1850,11 @@ Das ist Ihr eigener Einmal-Link! Aktuelles Passwort… No comment provided by engineer. + + Current profile + Aktueller Profil + No comment provided by engineer. + Currently maximum supported file size is %@. Die derzeit maximal unterstützte Dateigröße beträgt %@. @@ -1598,11 +1865,21 @@ Das ist Ihr eigener Einmal-Link! Zeit anpassen No comment provided by engineer. + + Customize theme + Design anpassen + No comment provided by engineer. + Dark Dunkel No comment provided by engineer. + + Dark mode colors + Farben für die dunkle Variante + No comment provided by engineer. + Database ID Datenbank-ID @@ -1701,6 +1978,11 @@ Das ist Ihr eigener Einmal-Link! Die Datenbank wird beim nächsten Start der App migriert No comment provided by engineer. + + Debug delivery + Debugging-Zustellung + No comment provided by engineer. + Decentralized Dezentral @@ -1714,18 +1996,19 @@ Das ist Ihr eigener Einmal-Link! Delete Löschen - chat item action + chat item action + swipe action + + + Delete %lld messages of members? + %lld Nachrichten der Mitglieder löschen? + No comment provided by engineer. Delete %lld messages? %lld Nachrichten löschen? No comment provided by engineer. - - Delete Contact - Kontakt löschen - No comment provided by engineer. - Delete address Adresse löschen @@ -1781,11 +2064,9 @@ Das ist Ihr eigener Einmal-Link! Kontakt löschen No comment provided by engineer. - - Delete contact? -This cannot be undone! - Kontakt löschen? -Das kann nicht rückgängig gemacht werden! + + Delete contact? + Kontakt löschen? No comment provided by engineer. @@ -1878,14 +2159,9 @@ Das kann nicht rückgängig gemacht werden! Alte Datenbank löschen? No comment provided by engineer. - - Delete pending connection - Ausstehende Verbindung löschen - No comment provided by engineer. - Delete pending connection? - Die ausstehende Verbindung löschen? + Ausstehende Verbindung löschen? No comment provided by engineer. @@ -1898,11 +2174,26 @@ Das kann nicht rückgängig gemacht werden! Lösche Warteschlange server test step + + Delete up to 20 messages at once. + Löschen Sie bis zu 20 Nachrichten auf einmal. + No comment provided by engineer. + Delete user profile? Benutzerprofil löschen? No comment provided by engineer. + + Delete without notification + Ohne Benachrichtigung löschen + No comment provided by engineer. + + + Deleted + Gelöscht + No comment provided by engineer. + Deleted at Gelöscht um @@ -1913,6 +2204,11 @@ Das kann nicht rückgängig gemacht werden! Gelöscht um: %@ copied message info + + Deletion errors + Fehler beim Löschen + No comment provided by engineer. + Delivery Zustellung @@ -1948,11 +2244,41 @@ Das kann nicht rückgängig gemacht werden! Desktop-Geräte No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + Adresse des Zielservers von %@ ist nicht kompatibel mit den Einstellungen des Weiterleitungsservers %@. + No comment provided by engineer. + + + Destination server error: %@ + Zielserver-Fehler: %@ + snd error text + + + Destination server version of %@ is incompatible with forwarding server %@. + Die Version des Zielservers %@ ist nicht kompatibel mit dem Weiterleitungsserver %@. + No comment provided by engineer. + + + Detailed statistics + Detaillierte Statistiken + No comment provided by engineer. + + + Details + Details + No comment provided by engineer. + Develop Entwicklung No comment provided by engineer. + + Developer options + Optionen für Entwickler + No comment provided by engineer. + Developer tools Entwicklertools @@ -2003,6 +2329,11 @@ Das kann nicht rückgängig gemacht werden! Für Alle deaktivieren No comment provided by engineer. + + Disabled + Deaktiviert + No comment provided by engineer. + Disappearing message Verschwindende Nachricht @@ -2053,9 +2384,19 @@ Das kann nicht rückgängig gemacht werden! Lokales Netzwerk durchsuchen No comment provided by engineer. + + Do NOT send messages directly, even if your or destination server does not support private routing. + Nachrichten werden nicht direkt versendet, selbst wenn Ihr oder der Zielserver kein privates Routing unterstützt. + No comment provided by engineer. + Do NOT use SimpleX for emergency calls. - Nutzen Sie SimpleX nicht für Notrufe. + SimpleX NICHT für Notrufe nutzen. + No comment provided by engineer. + + + Do NOT use private routing. + Sie nutzen KEIN privates Routing. No comment provided by engineer. @@ -2093,6 +2434,11 @@ Das kann nicht rückgängig gemacht werden! Herunterladen chat item action + + Download errors + Fehler beim Herunterladen + No comment provided by engineer. + Download failed Herunterladen fehlgeschlagen @@ -2103,6 +2449,16 @@ Das kann nicht rückgängig gemacht werden! Datei herunterladen server test step + + Downloaded + Heruntergeladen + No comment provided by engineer. + + + Downloaded files + Heruntergeladene Dateien + No comment provided by engineer. + Downloading archive Archiv wird heruntergeladen @@ -2203,6 +2559,11 @@ Das kann nicht rückgängig gemacht werden! Selbstzerstörungs-Zugangscode aktivieren set passcode view + + Enabled + Aktiviert + No comment provided by engineer. + Enabled for Aktiviert für @@ -2340,7 +2701,7 @@ Das kann nicht rückgängig gemacht werden! Error aborting address change - Fehler beim Abbrechen des Adresswechsels + Fehler beim Beenden des Adresswechsels No comment provided by engineer. @@ -2373,6 +2734,11 @@ Das kann nicht rückgängig gemacht werden! Fehler beim Ändern der Einstellung No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + Fehler beim Verbinden mit dem Weiterleitungsserver %@. Bitte versuchen Sie es später erneut. + No comment provided by engineer. + Error creating address Fehler beim Erstellen der Adresse @@ -2423,11 +2789,6 @@ Das kann nicht rückgängig gemacht werden! Fehler beim Löschen der Verbindung No comment provided by engineer. - - Error deleting contact - Fehler beim Löschen des Kontakts - No comment provided by engineer. - Error deleting database Fehler beim Löschen der Datenbank @@ -2473,6 +2834,11 @@ Das kann nicht rückgängig gemacht werden! Fehler beim Exportieren der Chat-Datenbank No comment provided by engineer. + + Error exporting theme: %@ + Fehler beim Exportieren des Designs: %@ + No comment provided by engineer. + Error importing chat database Fehler beim Importieren der Chat-Datenbank @@ -2498,11 +2864,26 @@ Das kann nicht rückgängig gemacht werden! Fehler beim Empfangen der Datei No comment provided by engineer. + + Error reconnecting server + Fehler beim Wiederherstellen der Verbindung zum Server + No comment provided by engineer. + + + Error reconnecting servers + Fehler beim Wiederherstellen der Verbindungen zu den Servern + No comment provided by engineer. + Error removing member Fehler beim Entfernen des Mitglieds No comment provided by engineer. + + Error resetting statistics + Fehler beim Zurücksetzen der Statistiken + No comment provided by engineer. + Error saving %@ servers Fehler beim Speichern der %@-Server @@ -2621,7 +3002,8 @@ Das kann nicht rückgängig gemacht werden! Error: %@ Fehler: %@ - No comment provided by engineer. + file error text + snd error text Error: URL is invalid @@ -2633,6 +3015,11 @@ Das kann nicht rückgängig gemacht werden! Fehler: Keine Datenbankdatei No comment provided by engineer. + + Errors + Fehler + No comment provided by engineer. + Even when disabled in the conversation. Auch wenn sie im Chat deaktiviert sind. @@ -2658,6 +3045,11 @@ Das kann nicht rückgängig gemacht werden! Fehler beim Export: No comment provided by engineer. + + Export theme + Design exportieren + No comment provided by engineer. + Exported database archive. Exportiertes Datenbankarchiv. @@ -2691,8 +3083,33 @@ Das kann nicht rückgängig gemacht werden! Favorite Favorit + swipe action + + + File error + Datei-Fehler No comment provided by engineer. + + File not found - most likely file was deleted or cancelled. + Datei nicht gefunden - höchstwahrscheinlich wurde die Datei gelöscht oder der Transfer abgebrochen. + file error text + + + File server error: %@ + Datei-Server Fehler: %@ + file error text + + + File status + Datei-Status + No comment provided by engineer. + + + File status: %@ + Datei-Status: %@ + copied message info + File will be deleted from servers. Die Datei wird von den Servern gelöscht. @@ -2713,6 +3130,11 @@ Das kann nicht rückgängig gemacht werden! Datei: %@ No comment provided by engineer. + + Files + Dateien + No comment provided by engineer. + Files & media Dateien & Medien @@ -2818,6 +3240,35 @@ Das kann nicht rückgängig gemacht werden! Weitergeleitet aus No comment provided by engineer. + + Forwarding server %@ failed to connect to destination server %@. Please try later. + Weiterleitungsserver %@ konnte sich nicht mit dem Zielserver %@ verbinden. Bitte versuchen Sie es später erneut. + No comment provided by engineer. + + + Forwarding server address is incompatible with network settings: %@. + Adresse des Weiterleitungsservers ist nicht kompatibel mit den Netzwerkeinstellungen: %@. + No comment provided by engineer. + + + Forwarding server version is incompatible with network settings: %@. + Version des Weiterleitungsservers ist nicht kompatibel mit den Netzwerkeinstellungen: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + Weiterleitungsserver: %1$@ +Zielserver Fehler: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + Weiterleitungsserver: %1$@ +Fehler: %2$@ + snd error text + Found desktop Gefundener Desktop @@ -2863,6 +3314,16 @@ Das kann nicht rückgängig gemacht werden! GIFs und Sticker No comment provided by engineer. + + Good afternoon! + Guten Nachmittag! + message preview + + + Good morning! + Guten Morgen! + message preview + Group Gruppe @@ -2985,12 +3446,12 @@ Das kann nicht rückgängig gemacht werden! Group will be deleted for all members - this cannot be undone! - Die Gruppe wird für alle Mitglieder gelöscht - dies kann nicht rückgängig gemacht werden! + Die Gruppe wird für alle Mitglieder gelöscht. Dies kann nicht rückgängig gemacht werden! No comment provided by engineer. Group will be deleted for you - this cannot be undone! - Die Gruppe wird für Sie gelöscht - dies kann nicht rückgängig gemacht werden! + Die Gruppe wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden! No comment provided by engineer. @@ -3143,6 +3604,11 @@ Das kann nicht rückgängig gemacht werden! Import ist fehlgeschlagen No comment provided by engineer. + + Import theme + Design importieren + No comment provided by engineer. + Importing archive Archiv wird importiert @@ -3215,12 +3681,12 @@ Das kann nicht rückgängig gemacht werden! Incompatible database version - Inkompatible Datenbank-Version + Datenbank-Version nicht kompatibel No comment provided by engineer. Incompatible version - Inkompatible Version + Version nicht kompatibel No comment provided by engineer. @@ -3265,6 +3731,11 @@ Das kann nicht rückgängig gemacht werden! Schnittstelle No comment provided by engineer. + + Interface colors + Interface-Farben + No comment provided by engineer. + Invalid QR code Ungültiger QR-Code @@ -3366,6 +3837,11 @@ Das kann nicht rückgängig gemacht werden! 3. Die Verbindung wurde kompromittiert. No comment provided by engineer. + + It protects your IP address and connections. + Ihre IP-Adresse und Verbindungen werden geschützt. + No comment provided by engineer. + It seems like you are already connected via this link. If it is not the case, there was an error (%@). Es sieht so aus, als ob Sie bereits über diesen Link verbunden sind. Wenn das nicht der Fall ist, gab es einen Fehler (%@). @@ -3384,7 +3860,7 @@ Das kann nicht rückgängig gemacht werden! Join Beitreten - No comment provided by engineer. + swipe action Join group @@ -3428,6 +3904,11 @@ Das ist Ihr Link für die Gruppe %@! Behalten No comment provided by engineer. + + Keep conversation + Unterhaltung behalten + No comment provided by engineer. + Keep the app open to use it from desktop Die App muss geöffnet bleiben, um sie vom Desktop aus nutzen zu können @@ -3471,7 +3952,7 @@ Das ist Ihr Link für die Gruppe %@! Leave Verlassen - No comment provided by engineer. + swipe action Leave group @@ -3603,11 +4084,26 @@ Das ist Ihr Link für die Gruppe %@! Max. 30 Sekunden, sofort erhalten. No comment provided by engineer. + + Media & file servers + Medien- und Datei-Server + No comment provided by engineer. + + + Medium + Medium + blur media + Member Mitglied No comment provided by engineer. + + Member inactive + Mitglied inaktiv + item status text + Member role will be changed to "%@". All group members will be notified. Die Mitgliederrolle wird auf "%@" geändert. Alle Mitglieder der Gruppe werden benachrichtigt. @@ -3620,7 +4116,12 @@ Das ist Ihr Link für die Gruppe %@! Member will be removed from group - this cannot be undone! - Das Mitglied wird aus der Gruppe entfernt - dies kann nicht rückgängig gemacht werden! + Das Mitglied wird aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden! + No comment provided by engineer. + + + Menus + Menüs No comment provided by engineer. @@ -3633,11 +4134,31 @@ Das ist Ihr Link für die Gruppe %@! Empfangsbestätigungen für Nachrichten! No comment provided by engineer. + + Message delivery warning + Warnung bei der Nachrichtenzustellung + item status text + Message draft Nachrichtenentwurf No comment provided by engineer. + + Message forwarded + Nachricht weitergeleitet + item status text + + + Message may be delivered later if member becomes active. + Die Nachricht kann später zugestellt werden, wenn das Mitglied aktiv wird. + item status description + + + Message queue info + Nachrichten-Warteschlangen-Information + No comment provided by engineer. + Message reactions Reaktionen auf Nachrichten @@ -3653,11 +4174,31 @@ Das ist Ihr Link für die Gruppe %@! In dieser Gruppe sind Reaktionen auf Nachrichten nicht erlaubt. No comment provided by engineer. + + Message reception + Nachrichtenempfang + No comment provided by engineer. + + + Message servers + Nachrichten-Server + No comment provided by engineer. + Message source remains private. Die Nachrichtenquelle bleibt privat. No comment provided by engineer. + + Message status + Nachrichten-Status + No comment provided by engineer. + + + Message status: %@ + Nachrichten-Status: %@ + copied message info + Message text Nachrichtentext @@ -3683,6 +4224,16 @@ Das ist Ihr Link für die Gruppe %@! Die Nachrichten von %@ werden angezeigt! No comment provided by engineer. + + Messages received + Empfangene Nachrichten + No comment provided by engineer. + + + Messages sent + Gesendete Nachrichten + No comment provided by engineer. + Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. Nachrichten, Dateien und Anrufe sind durch **Ende-zu-Ende-Verschlüsselung** mit Perfect Forward Secrecy, Ablehnung und Einbruchs-Wiederherstellung geschützt. @@ -3783,11 +4334,6 @@ Das ist Ihr Link für die Gruppe %@! Wahrscheinlich ist diese Verbindung gelöscht worden. item status description - - Most likely this contact has deleted the connection with you. - Dieser Kontakt hat sehr wahrscheinlich die Verbindung mit Ihnen gelöscht. - No comment provided by engineer. - Multiple chat profiles Mehrere Chat-Profile @@ -3796,7 +4342,7 @@ Das ist Ihr Link für die Gruppe %@! Mute Stummschalten - No comment provided by engineer. + swipe action Muted when inactive! @@ -3806,7 +4352,7 @@ Das ist Ihr Link für die Gruppe %@! Name Name - No comment provided by engineer. + swipe action Network & servers @@ -3818,6 +4364,11 @@ Das ist Ihr Link für die Gruppe %@! Netzwerkverbindung No comment provided by engineer. + + Network issues - message expired after many attempts to send it. + Netzwerk-Fehler - die Nachricht ist nach vielen Sende-Versuchen abgelaufen. + snd error text + Network management Netzwerk-Verwaltung @@ -3843,6 +4394,11 @@ Das ist Ihr Link für die Gruppe %@! Neuer Chat No comment provided by engineer. + + New chat experience 🎉 + Neue Chat-Erfahrung 🎉 + No comment provided by engineer. + New contact request Neue Kontaktanfrage @@ -3873,6 +4429,11 @@ Das ist Ihr Link für die Gruppe %@! Neu in %@ No comment provided by engineer. + + New media options + Neue Medien-Optionen + No comment provided by engineer. + New member role Neue Mitgliedsrolle @@ -3918,6 +4479,11 @@ Das ist Ihr Link für die Gruppe %@! Kein Geräte-Token! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + Bisher keine direkte Verbindung. Nachricht wird von einem Admin weitergeleitet. + item status description + No filtered chats Keine gefilterten Chats @@ -3933,6 +4499,11 @@ Das ist Ihr Link für die Gruppe %@! Kein Nachrichtenverlauf No comment provided by engineer. + + No info, try to reload + Keine Information - es wird versucht neu zu laden + No comment provided by engineer. + No network connection Keine Netzwerkverbindung @@ -3953,6 +4524,11 @@ Das ist Ihr Link für die Gruppe %@! Nicht kompatibel! No comment provided by engineer. + + Nothing selected + Nichts ausgewählt + No comment provided by engineer. + Notifications Benachrichtigungen @@ -3980,7 +4556,7 @@ Das ist Ihr Link für die Gruppe %@! Off Aus - No comment provided by engineer. + blur media Ok @@ -4002,14 +4578,18 @@ Das ist Ihr Link für die Gruppe %@! Einmal-Einladungslink No comment provided by engineer. - - Onion hosts will be required for connection. Requires enabling VPN. - Für die Verbindung werden Onion-Hosts benötigt. Dies erfordert die Aktivierung eines VPNs. + + Onion hosts will be **required** for connection. +Requires compatible VPN. + Für diese Verbindung werden Onion-Hosts benötigt. +Dies erfordert die Aktivierung eines VPNs. No comment provided by engineer. - - Onion hosts will be used when available. Requires enabling VPN. - Onion-Hosts werden verwendet, sobald sie verfügbar sind. Dies erfordert die Aktivierung eines VPNs. + + Onion hosts will be used when available. +Requires compatible VPN. + Wenn Onion-Hosts verfügbar sind, werden sie verwendet. +Dies erfordert die Aktivierung eines VPNs. No comment provided by engineer. @@ -4022,6 +4602,11 @@ Das ist Ihr Link für die Gruppe %@! Nur die Endgeräte speichern die Benutzerprofile, Kontakte, Gruppen und Nachrichten, welche über eine **2-Schichten Ende-zu-Ende-Verschlüsselung** gesendet werden. No comment provided by engineer. + + Only delete conversation + Nur die Unterhaltung löschen + No comment provided by engineer. + Only group owners can change group preferences. Gruppen-Präferenzen können nur von Gruppen-Eigentümern geändert werden. @@ -4117,6 +4702,11 @@ Das ist Ihr Link für die Gruppe %@! Migration auf ein anderes Gerät öffnen authentication reason + + Open server settings + Server-Einstellungen öffnen + No comment provided by engineer. + Open user profiles Benutzerprofile öffnen @@ -4157,6 +4747,11 @@ Das ist Ihr Link für die Gruppe %@! Andere No comment provided by engineer. + + Other %@ servers + Andere %@ Server + No comment provided by engineer. + PING count PING-Zähler @@ -4222,6 +4817,11 @@ Das ist Ihr Link für die Gruppe %@! Fügen Sie den erhaltenen Link ein No comment provided by engineer. + + Pending + Ausstehend + No comment provided by engineer. + People can connect to you only via the links you share. Verbindungen mit Kontakten sind nur über Links möglich, die Sie oder Ihre Kontakte untereinander teilen. @@ -4242,11 +4842,28 @@ Das ist Ihr Link für die Gruppe %@! Bild-in-Bild-Anrufe No comment provided by engineer. + + Play from the chat list. + Direkt aus der Chat-Liste abspielen. + No comment provided by engineer. + + + Please ask your contact to enable calls. + Bitten Sie Ihren Kontakt darum, Anrufe zu aktivieren. + No comment provided by engineer. + Please ask your contact to enable sending voice messages. Bitten Sie Ihren Kontakt darum, das Senden von Sprachnachrichten zu aktivieren. No comment provided by engineer. + + Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers. + Bitte überprüfen Sie, ob sich das Mobiltelefon und die Desktop-App im gleichen lokalen Netzwerk befinden, und die Desktop-Firewall die Verbindung erlaubt. +Bitte teilen Sie weitere mögliche Probleme den Entwicklern mit. + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. Überprüfen Sie bitte, ob Sie den richtigen Link genutzt haben oder bitten Sie Ihren Kontakt nochmal darum, Ihnen einen Link zuzusenden. @@ -4344,6 +4961,11 @@ Fehler: %@ Vorschau No comment provided by engineer. + + Previously connected servers + Bisher verbundene Server + No comment provided by engineer. + Privacy & security Datenschutz & Sicherheit @@ -4359,11 +4981,31 @@ Fehler: %@ Neutrale Dateinamen No comment provided by engineer. + + Private message routing + Privates Nachrichten-Routing + No comment provided by engineer. + + + Private message routing 🚀 + Privates Nachrichten-Routing 🚀 + No comment provided by engineer. + Private notes Private Notizen name of notes to self + + Private routing + Privates Routing + No comment provided by engineer. + + + Private routing error + Fehler beim privaten Routing + No comment provided by engineer. + Profile and server connections Profil und Serververbindungen @@ -4394,6 +5036,11 @@ Fehler: %@ Passwort für Profil No comment provided by engineer. + + Profile theme + Profil-Design + No comment provided by engineer. + Profile update will be sent to your contacts. Profil-Aktualisierung wird an Ihre Kontakte gesendet. @@ -4444,11 +5091,23 @@ Fehler: %@ Das Senden von Sprachnachrichten nicht erlauben. No comment provided by engineer. + + Protect IP address + IP-Adresse schützen + No comment provided by engineer. + Protect app screen App-Bildschirm schützen No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + Schützen Sie Ihre IP-Adresse vor den Nachrichten-Relais, die Ihre Kontakte ausgewählt haben. +Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. + No comment provided by engineer. + Protect your chat profiles with a password! Ihre Chat-Profile mit einem Passwort schützen! @@ -4464,6 +5123,16 @@ Fehler: %@ Protokollzeitüberschreitung pro kB No comment provided by engineer. + + Proxied + Proxied + No comment provided by engineer. + + + Proxied servers + Proxy-Server + No comment provided by engineer. + Push notifications Push-Benachrichtigungen @@ -4484,6 +5153,11 @@ Fehler: %@ Bewerten Sie die App No comment provided by engineer. + + Reachable chat toolbar + Erreichbare Chat-Symbolleiste + No comment provided by engineer. + React… Reagiere… @@ -4492,7 +5166,7 @@ Fehler: %@ Read Gelesen - No comment provided by engineer. + swipe action Read more @@ -4529,6 +5203,11 @@ Fehler: %@ Bestätigungen sind deaktiviert No comment provided by engineer. + + Receive errors + Fehler beim Empfang + No comment provided by engineer. + Received at Empfangen um @@ -4549,16 +5228,26 @@ Fehler: %@ Empfangene Nachricht message info title + + Received messages + Empfangene Nachrichten + No comment provided by engineer. + + + Received reply + Empfangene Antwort + No comment provided by engineer. + + + Received total + Summe aller empfangenen Nachrichten + No comment provided by engineer. + Receiving address will be changed to a different server. Address change will complete after sender comes online. Die Empfängeradresse wird auf einen anderen Server geändert. Der Adresswechsel wird abgeschlossen, wenn der Absender wieder online ist. No comment provided by engineer. - - Receiving concurrency - Gleichzeitiger Empfang - No comment provided by engineer. - Receiving file will be stopped. Der Empfang der Datei wird beendet. @@ -4584,11 +5273,36 @@ Fehler: %@ Die Empfänger sehen Nachrichtenaktualisierungen, während Sie sie eingeben. No comment provided by engineer. + + Reconnect + Neu verbinden + No comment provided by engineer. + Reconnect all connected servers to force message delivery. It uses additional traffic. Alle verbundenen Server werden neu verbunden, um die Zustellung der Nachricht zu erzwingen. Dies verursacht zusätzlichen Datenverkehr. No comment provided by engineer. + + Reconnect all servers + Alle Server neu verbinden + No comment provided by engineer. + + + Reconnect all servers? + Alle Server neu verbinden? + No comment provided by engineer. + + + Reconnect server to force message delivery. It uses additional traffic. + Um die Auslieferung von Nachrichten zu erzwingen, wird der Server neu verbunden. Dafür wird weiterer Datenverkehr benötigt. + No comment provided by engineer. + + + Reconnect server? + Server neu verbinden? + No comment provided by engineer. + Reconnect servers? Die Server neu verbinden? @@ -4612,7 +5326,8 @@ Fehler: %@ Reject Ablehnen - reject incoming call via notification + reject incoming call via notification + swipe action Reject (sender NOT notified) @@ -4639,6 +5354,11 @@ Fehler: %@ Entfernen No comment provided by engineer. + + Remove image + Bild entfernen + No comment provided by engineer. + Remove member Mitglied entfernen @@ -4709,16 +5429,41 @@ Fehler: %@ Zurücksetzen No comment provided by engineer. + + Reset all hints + Alle Hinweise zurücksetzen + No comment provided by engineer. + + + Reset all statistics + Alle Statistiken zurücksetzen + No comment provided by engineer. + + + Reset all statistics? + Alle Statistiken zurücksetzen? + No comment provided by engineer. + Reset colors Farben zurücksetzen No comment provided by engineer. + + Reset to app theme + Auf das App-Design zurücksetzen + No comment provided by engineer. + Reset to defaults Auf Voreinstellungen zurücksetzen No comment provided by engineer. + + Reset to user theme + Auf das Benutzer-spezifische Design zurücksetzen + No comment provided by engineer. + Restart the app to create a new chat profile Um ein neues Chat-Profil zu erstellen, starten Sie die App neu @@ -4759,11 +5504,6 @@ Fehler: %@ Aufdecken chat item action - - Revert - Zurückkehren - No comment provided by engineer. - Revoke Widerrufen @@ -4789,11 +5529,16 @@ Fehler: %@ Chat starten No comment provided by engineer. - - SMP servers + + SMP server SMP-Server No comment provided by engineer. + + Safely receive files + Dateien sicher empfangen + No comment provided by engineer. + Safer groups Sicherere Gruppen @@ -4819,6 +5564,11 @@ Fehler: %@ Speichern und Gruppenmitglieder benachrichtigen No comment provided by engineer. + + Save and reconnect + Speichern und neu verbinden + No comment provided by engineer. + Save and update group profile Gruppen-Profil sichern und aktualisieren @@ -4899,6 +5649,16 @@ Fehler: %@ Gespeicherte Nachricht message info title + + Scale + Skalieren + No comment provided by engineer. + + + Scan / Paste link + Link scannen / einfügen + No comment provided by engineer. + Scan QR code QR-Code scannen @@ -4939,11 +5699,21 @@ Fehler: %@ Suchen oder fügen Sie den SimpleX-Link ein No comment provided by engineer. + + Secondary + Zweite Farbe + No comment provided by engineer. + Secure queue Sichere Warteschlange server test step + + Secured + Abgesichert + No comment provided by engineer. + Security assessment Sicherheits-Gutachten @@ -4957,6 +5727,16 @@ Fehler: %@ Select Auswählen + chat item action + + + Selected %lld + %lld ausgewählt + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + Diese Nachricht ist wegen der gewählten Chat-Einstellungen nicht erlaubt. No comment provided by engineer. @@ -4994,11 +5774,6 @@ Fehler: %@ Empfangsbestätigungen senden an No comment provided by engineer. - - Send direct message - Direktnachricht senden - No comment provided by engineer. - Send direct message to connect Eine Direktnachricht zum Verbinden senden @@ -5009,6 +5784,11 @@ Fehler: %@ Verschwindende Nachricht senden No comment provided by engineer. + + Send errors + Fehler beim Senden + No comment provided by engineer. + Send link previews Link-Vorschau senden @@ -5019,6 +5799,21 @@ Fehler: %@ Live Nachricht senden No comment provided by engineer. + + Send message to enable calls. + Nachricht senden, um Anrufe zu aktivieren. + No comment provided by engineer. + + + Send messages directly when IP address is protected and your or destination server does not support private routing. + Nachrichten werden direkt versendet, wenn die IP-Adresse geschützt ist, und Ihr oder der Zielserver kein privates Routing unterstützt. + No comment provided by engineer. + + + Send messages directly when your or destination server does not support private routing. + Nachrichten werden direkt versendet, wenn Ihr oder der Zielserver kein privates Routing unterstützt. + No comment provided by engineer. + Send notifications Benachrichtigungen senden @@ -5109,6 +5904,11 @@ Fehler: %@ Gesendet um: %@ copied message info + + Sent directly + Direkt gesendet + No comment provided by engineer. + Sent file event Datei-Ereignis wurde gesendet @@ -5119,11 +5919,46 @@ Fehler: %@ Gesendete Nachricht message info title + + Sent messages + Gesendete Nachrichten + No comment provided by engineer. + Sent messages will be deleted after set time. Gesendete Nachrichten werden nach der eingestellten Zeit gelöscht. No comment provided by engineer. + + Sent reply + Gesendete Antwort + No comment provided by engineer. + + + Sent total + Summe aller gesendeten Nachrichten + No comment provided by engineer. + + + Sent via proxy + Über einen Proxy gesendet + No comment provided by engineer. + + + Server address + Server-Adresse + No comment provided by engineer. + + + Server address is incompatible with network settings. + Die Server-Adresse ist nicht mit den Netzwerkeinstellungen kompatibel. + srv error text. + + + Server address is incompatible with network settings: %@. + Die Server-Adresse ist nicht mit den Netzwerkeinstellungen kompatibel: %@. + No comment provided by engineer. + Server requires authorization to create queues, check password Um Warteschlangen zu erzeugen benötigt der Server eine Authentifizierung. Bitte überprüfen Sie das Passwort @@ -5139,11 +5974,36 @@ Fehler: %@ Server Test ist fehlgeschlagen! No comment provided by engineer. + + Server type + Server-Typ + No comment provided by engineer. + + + Server version is incompatible with network settings. + Die Server-Version ist nicht mit den Netzwerkeinstellungen kompatibel. + srv error text + + + Server version is incompatible with your app: %@. + Die Server-Version ist nicht mit Ihrer App kompatibel: %@. + No comment provided by engineer. + Servers Server No comment provided by engineer. + + Servers info + Server-Informationen + No comment provided by engineer. + + + Servers statistics will be reset - this cannot be undone! + Die Serverstatistiken werden zurückgesetzt. Dies kann nicht rückgängig gemacht werden! + No comment provided by engineer. + Session code Sitzungscode @@ -5159,6 +6019,11 @@ Fehler: %@ Kontaktname festlegen… No comment provided by engineer. + + Set default theme + Default-Design einstellen + No comment provided by engineer. + Set group preferences Gruppen-Präferenzen einstellen @@ -5224,6 +6089,11 @@ Fehler: %@ Die Adresse mit Kontakten teilen? No comment provided by engineer. + + Share from other apps. + Aus anderen Apps heraus teilen. + No comment provided by engineer. + Share link Link teilen @@ -5234,6 +6104,11 @@ Fehler: %@ Teilen Sie diesen Einmal-Einladungslink No comment provided by engineer. + + Share to SimpleX + Mit SimpleX teilen + No comment provided by engineer. + Share with contacts Mit Kontakten teilen @@ -5259,16 +6134,36 @@ Fehler: %@ Letzte Nachrichten anzeigen No comment provided by engineer. + + Show message status + Nachrichtenstatus anzeigen + No comment provided by engineer. + + + Show percentage + Prozentualen Anteil anzeigen + No comment provided by engineer. + Show preview Vorschau anzeigen No comment provided by engineer. + + Show → on messages sent via private routing. + Bei Nachrichten, die über privates Routing versendet wurden, → anzeigen. + No comment provided by engineer. + Show: Anzeigen: No comment provided by engineer. + + SimpleX + SimpleX + No comment provided by engineer. + SimpleX Address SimpleX-Adresse @@ -5344,6 +6239,11 @@ Fehler: %@ Vereinfachter Inkognito-Modus No comment provided by engineer. + + Size + Größe + No comment provided by engineer. + Skip Überspringen @@ -5359,11 +6259,26 @@ Fehler: %@ Kleine Gruppen (max. 20) No comment provided by engineer. + + Soft + Weich + blur media + + + Some file(s) were not exported: + Einzelne Datei(en) wurde(n) nicht exportiert: + No comment provided by engineer. + Some non-fatal errors occurred during import - you may see Chat console for more details. Während des Imports sind einige nicht schwerwiegende Fehler aufgetreten - in der Chat-Konsole finden Sie weitere Einzelheiten. No comment provided by engineer. + + Some non-fatal errors occurred during import: + Während des Imports traten ein paar nicht schwerwiegende Fehler auf: + No comment provided by engineer. + Somebody Jemand @@ -5389,6 +6304,16 @@ Fehler: %@ Starten Sie die Migration No comment provided by engineer. + + Starting from %@. + Beginnend mit %@. + No comment provided by engineer. + + + Statistics + Statistiken + No comment provided by engineer. + Stop Beenden @@ -5449,11 +6374,31 @@ Fehler: %@ Chat wird beendet No comment provided by engineer. + + Strong + Hart + blur media + Submit Bestätigen No comment provided by engineer. + + Subscribed + Abonniert + No comment provided by engineer. + + + Subscription errors + Fehler beim Abonnieren + No comment provided by engineer. + + + Subscriptions ignored + Nicht beachtete Abonnements + No comment provided by engineer. + Support SimpleX Chat Unterstützung von SimpleX Chat @@ -5469,6 +6414,11 @@ Fehler: %@ System-Authentifizierung No comment provided by engineer. + + TCP connection + TCP-Verbindung + No comment provided by engineer. + TCP connection timeout Timeout der TCP-Verbindung @@ -5529,9 +6479,9 @@ Fehler: %@ Zum Scannen tippen No comment provided by engineer. - - Tap to start a new chat - Zum Starten eines neuen Chats tippen + + Temporary file error + Temporärer Datei-Fehler No comment provided by engineer. @@ -5586,6 +6536,11 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Wenn sie Nachrichten oder Kontaktanfragen empfangen, kann Sie die App benachrichtigen - Um dies zu aktivieren, öffnen Sie bitte die Einstellungen. No comment provided by engineer. + + The app will ask to confirm downloads from unknown file servers (except .onion). + Die App wird eine Bestätigung bei Downloads von unbekannten Datei-Servern anfordern (außer bei .onion). + No comment provided by engineer. + The attempt to change database passphrase was not completed. Die Änderung des Datenbank-Passworts konnte nicht abgeschlossen werden. @@ -5631,6 +6586,16 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Diese Nachricht wird für alle Mitglieder als moderiert gekennzeichnet. No comment provided by engineer. + + The messages will be deleted for all members. + Die Nachrichten werden für alle Mitglieder gelöscht werden. + No comment provided by engineer. + + + The messages will be marked as moderated for all members. + Die Nachrichten werden für alle Mitglieder als moderiert gekennzeichnet werden. + No comment provided by engineer. + The next generation of private messaging Die nächste Generation von privatem Messaging @@ -5666,8 +6631,8 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Der von Ihnen eingefügte Text ist kein SimpleX-Link. No comment provided by engineer. - - Theme + + Themes Design No comment provided by engineer. @@ -5683,12 +6648,12 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. - Diese Aktion kann nicht rückgängig gemacht werden! Alle empfangenen und gesendeten Dateien und Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten. + Diese Aktion kann nicht rückgängig gemacht werden! Es werden alle empfangenen und gesendeten Dateien und Medien gelöscht. Bilder mit niedriger Auflösung bleiben erhalten. No comment provided by engineer. This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes. - Diese Aktion kann nicht rückgängig gemacht werden! Alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, werden gelöscht. Dieser Vorgang kann mehrere Minuten dauern. + Diese Aktion kann nicht rückgängig gemacht werden! Es werden alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, gelöscht. Dieser Vorgang kann mehrere Minuten dauern. No comment provided by engineer. @@ -5736,11 +6701,21 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Das ist Ihr eigener Einmal-Link! No comment provided by engineer. + + This link was used with another mobile device, please create a new link on the desktop. + Dieser Link wurde schon mit einem anderen Mobiltelefon genutzt. Bitte erstellen sie einen neuen Link in der Desktop-App. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. Diese Einstellung gilt für Nachrichten in Ihrem aktuellen Chat-Profil **%@**. No comment provided by engineer. + + Title + Bezeichnung + No comment provided by engineer. + To ask any questions and to receive updates: Um Fragen zu stellen und aktuelle Informationen zu erhalten: @@ -5771,6 +6746,11 @@ Dies kann passieren, wenn es einen Fehler gegeben hat oder die Verbindung kompro Bild- und Sprachdateinamen enthalten UTC, um Informationen zur Zeitzone zu schützen. No comment provided by engineer. + + To protect your IP address, private routing uses your SMP servers to deliver messages. + Zum Schutz Ihrer IP-Adresse, wird für die Nachrichten-Auslieferung privates Routing über Ihre konfigurierten SMP-Server genutzt. + No comment provided by engineer. + To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5798,16 +6778,36 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Um die Ende-zu-Ende-Verschlüsselung mit Ihrem Kontakt zu überprüfen, müssen Sie den Sicherheitscode in Ihren Apps vergleichen oder scannen. No comment provided by engineer. + + Toggle chat list: + Chat-Liste umschalten: + No comment provided by engineer. + Toggle incognito when connecting. Inkognito beim Verbinden einschalten. No comment provided by engineer. + + Toolbar opacity + Deckkraft der Symbolleiste + No comment provided by engineer. + + + Total + Summe aller Abonnements + No comment provided by engineer. + Transport isolation Transport-Isolation No comment provided by engineer. + + Transport sessions + Transport-Sitzungen + No comment provided by engineer. + Trying to connect to the server used to receive messages from this contact (error: %@). Beim Versuch die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird, ist ein Fehler aufgetreten (Fehler: %@). @@ -5863,11 +6863,6 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Mitglied freigeben? No comment provided by engineer. - - Unexpected error: %@ - Unerwarteter Fehler: %@ - item status description - Unexpected migration state Unerwarteter Migrationsstatus @@ -5876,7 +6871,7 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Unfav. Fav. entf. - No comment provided by engineer. + swipe action Unhide @@ -5913,6 +6908,11 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Unbekannter Fehler No comment provided by engineer. + + Unknown servers! + Unbekannte Server! + No comment provided by engineer. + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. Aktivieren Sie den Modus "Bitte nicht stören", um Unterbrechungen zu vermeiden, es sei denn, Sie verwenden die iOS Anrufschnittstelle. @@ -5948,12 +6948,12 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Unmute Stummschaltung aufheben - No comment provided by engineer. + swipe action Unread Ungelesen - No comment provided by engineer. + swipe action Up to 100 last messages are sent to new members. @@ -5965,11 +6965,6 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Aktualisieren No comment provided by engineer. - - Update .onion hosts setting? - Einstellung für .onion-Hosts aktualisieren? - No comment provided by engineer. - Update database passphrase Datenbank-Passwort aktualisieren @@ -5980,9 +6975,9 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Netzwerkeinstellungen aktualisieren? No comment provided by engineer. - - Update transport isolation mode? - Transport-Isolations-Modus aktualisieren? + + Update settings? + Einstellungen aktualisieren? No comment provided by engineer. @@ -5990,16 +6985,16 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Die Aktualisierung der Einstellungen wird den Client wieder mit allen Servern verbinden. No comment provided by engineer. - - Updating this setting will re-connect the client to all servers. - Die Aktualisierung dieser Einstellung wird den Client wieder mit allen Servern verbinden. - No comment provided by engineer. - Upgrade and open chat Aktualisieren und den Chat öffnen No comment provided by engineer. + + Upload errors + Fehler beim Hochladen + No comment provided by engineer. + Upload failed Hochladen fehlgeschlagen @@ -6010,6 +7005,16 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Datei hochladen server test step + + Uploaded + Hochgeladen + No comment provided by engineer. + + + Uploaded files + Hochgeladene Dateien + No comment provided by engineer. + Uploading archive Archiv wird hochgeladen @@ -6032,7 +7037,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Use current profile - Das aktuelle Profil nutzen + Aktuelles Profil nutzen No comment provided by engineer. @@ -6052,7 +7057,7 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Use new incognito profile - Ein neues Inkognito-Profil nutzen + Neues Inkognito-Profil nutzen No comment provided by engineer. @@ -6060,6 +7065,16 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Nur lokale Benachrichtigungen nutzen? No comment provided by engineer. + + Use private routing with unknown servers when IP address is not protected. + Sie nutzen privates Routing mit unbekannten Servern, wenn Ihre IP-Adresse nicht geschützt ist. + No comment provided by engineer. + + + Use private routing with unknown servers. + Sie nutzen privates Routing mit unbekannten Servern. + No comment provided by engineer. + Use server Server nutzen @@ -6070,14 +7085,19 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Die App kann während eines Anrufs genutzt werden. No comment provided by engineer. + + Use the app with one hand. + Die App mit einer Hand nutzen. + No comment provided by engineer. + User profile Benutzerprofil No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - Für die Nutzung von .onion-Hosts sind kompatible VPN-Anbieter erforderlich. + + User selection + Benutzer-Auswahl No comment provided by engineer. @@ -6210,6 +7230,16 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Auf das Video warten No comment provided by engineer. + + Wallpaper accent + Wallpaper-Akzent + No comment provided by engineer. + + + Wallpaper background + Wallpaper-Hintergrund + No comment provided by engineer. + Warning: starting chat on multiple devices is not supported and will cause message delivery failures Warnung: Das Starten des Chats auf mehreren Geräten wird nicht unterstützt und wird zu Fehlern bei der Nachrichtenübermittlung führen @@ -6295,18 +7325,38 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Mit reduziertem Akkuverbrauch. No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + Ohne Tor- oder VPN-Nutzung wird Ihre IP-Adresse für Datei-Server sichtbar sein. + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + Ohne Tor- oder VPN-Nutzung wird Ihre IP-Adresse für diese XFTP-Relais sichtbar sein: %@. + No comment provided by engineer. + Wrong database passphrase Falsches Datenbank-Passwort No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + Falscher Schlüssel oder unbekannte Verbindung - höchstwahrscheinlich ist diese Verbindung gelöscht worden. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + Falscher Schlüssel oder unbekannte Daten-Paketadresse der Datei - höchstwahrscheinlich wurde die Datei gelöscht. + file error text + Wrong passphrase! Falsches Passwort! No comment provided by engineer. - - XFTP servers + + XFTP server XFTP-Server No comment provided by engineer. @@ -6387,11 +7437,21 @@ Verbindungsanfrage wiederholen? Sie sind zu der Gruppe eingeladen No comment provided by engineer. + + You are not connected to these servers. Private routing is used to deliver messages to them. + Sie sind nicht mit diesen Servern verbunden. Zur Auslieferung von Nachrichten an diese Server wird privates Routing genutzt. + No comment provided by engineer. + You can accept calls from lock screen, without device and app authentication. Sie können Anrufe ohne Geräte- und App-Authentifizierung vom Sperrbildschirm aus annehmen. No comment provided by engineer. + + You can change it in Appearance settings. + Kann von Ihnen in den Erscheinungsbild-Einstellungen geändert werden. + No comment provided by engineer. + You can create it later Sie können dies später erstellen @@ -6422,11 +7482,16 @@ Verbindungsanfrage wiederholen? Sie können sie über Einstellungen für Ihre SimpleX-Kontakte sichtbar machen. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Sie können nun Nachrichten an %@ versenden notification body + + You can send messages to %@ from Archived contacts. + Sie können aus den archivierten Kontakten heraus Nachrichten an %@ versenden. + No comment provided by engineer. + You can set lock screen notification preview via settings. Über die Geräte-Einstellungen können Sie die Benachrichtigungsvorschau im Sperrbildschirm erlauben. @@ -6452,6 +7517,11 @@ Verbindungsanfrage wiederholen? Sie können den Chat über die App-Einstellungen / Datenbank oder durch Neustart der App starten No comment provided by engineer. + + You can still view conversation with %@ in the list of chats. + Sie können in der Chatliste weiterhin die Unterhaltung mit %@ einsehen. + No comment provided by engineer. + You can turn on SimpleX Lock via Settings. Sie können die SimpleX-Sperre über die Einstellungen aktivieren. @@ -6494,11 +7564,6 @@ Repeat connection request? Verbindungsanfrage wiederholen? No comment provided by engineer. - - You have no chats - Sie haben keine Chats - No comment provided by engineer. - You have to enter passphrase every time the app starts - it is not stored on the device. Sie müssen das Passwort jedes Mal eingeben, wenn die App startet. Es wird nicht auf dem Gerät gespeichert. @@ -6519,11 +7584,26 @@ Verbindungsanfrage wiederholen? Sie sind dieser Gruppe beigetreten. Sie werden mit dem einladenden Gruppenmitglied verbunden. No comment provided by engineer. + + You may migrate the exported database. + Sie können die exportierte Datenbank migrieren. + No comment provided by engineer. + + + You may save the exported archive. + Sie können das exportierte Archiv speichern. + No comment provided by engineer. + You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. Sie dürfen die neueste Version Ihrer Chat-Datenbank NUR auf einem Gerät verwenden, andernfalls erhalten Sie möglicherweise keine Nachrichten mehr von einigen Ihrer Kontakte. No comment provided by engineer. + + You need to allow your contact to call to be able to call them. + Sie müssen Ihrem Kontakt Anrufe zu Ihnen erlauben, bevor Sie ihn selbst anrufen können. + No comment provided by engineer. + You need to allow your contact to send voice messages to be able to send them. Sie müssen Ihrem Kontakt das Senden von Sprachnachrichten erlauben, um diese senden zu können. @@ -6639,13 +7719,6 @@ Verbindungsanfrage wiederholen? Ihre Chat-Profile No comment provided by engineer. - - Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link). - Damit die Verbindung hergestellt werden kann, muss Ihr Kontakt online sein. -Sie können diese Verbindung abbrechen und den Kontakt entfernen (und es später nochmals mit einem neuen Link versuchen). - No comment provided by engineer. - Your contact sent a file that is larger than currently supported maximum size (%@). Ihr Kontakt hat eine Datei gesendet, die größer ist als die derzeit unterstützte maximale Größe (%@). @@ -6793,6 +7866,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. und %lld weitere Ereignisse No comment provided by engineer. + + attempts + Versuche + No comment provided by engineer. + audio call (not e2e encrypted) Audioanruf (nicht E2E verschlüsselt) @@ -6833,6 +7911,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. fett No comment provided by engineer. + + call + Anrufen + No comment provided by engineer. + call error Fehler bei Anruf @@ -6983,6 +8066,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. Tage time unit + + decryption errors + Entschlüsselungs-Fehler + No comment provided by engineer. + default (%@) Voreinstellung (%@) @@ -7033,6 +8121,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. Doppelte Nachricht integrity error chat item + + duplicates + Duplikate + No comment provided by engineer. + e2e encrypted E2E-verschlüsselt @@ -7113,6 +8206,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. event happened No comment provided by engineer. + + expired + abgelaufen + No comment provided by engineer. + forwarded weitergeleitet @@ -7143,6 +8241,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. Für die sichere Speicherung des Passworts nach dem Neustart der App und dem Wechsel des Passworts wird der iOS Schlüsselbund verwendet - dies erlaubt den Empfang von Push-Benachrichtigungen. No comment provided by engineer. + + inactive + Inaktiv + No comment provided by engineer. + incognito via contact address link Inkognito über einen Kontaktadressen-Link @@ -7183,6 +8286,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. Einladung zur Gruppe %@ group name + + invite + Einladen + No comment provided by engineer. + invited eingeladen @@ -7238,6 +8346,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. ist der Gruppe beigetreten rcv group event chat item + + message + Nachricht + No comment provided by engineer. + message received Nachricht empfangen @@ -7268,6 +8381,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. Monate time unit + + mute + Stummschalten + No comment provided by engineer. + never nie @@ -7320,6 +8438,16 @@ SimpleX-Server können Ihr Profil nicht einsehen. Ein group pref value + + other + andere + No comment provided by engineer. + + + other errors + Andere Fehler + No comment provided by engineer. + owner Eigentümer @@ -7390,6 +8518,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. abgespeichert von %@ No comment provided by engineer. + + search + Suchen + No comment provided by engineer. + sec sek @@ -7415,6 +8548,15 @@ SimpleX-Server können Ihr Profil nicht einsehen. Direktnachricht senden No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + Server-Warteschlangen-Information: %1$@ + +Zuletzt empfangene Nachricht: %2$@ + queue info + set new contact address Es wurde eine neue Kontaktadresse festgelegt @@ -7455,11 +8597,26 @@ SimpleX-Server können Ihr Profil nicht einsehen. Unbekannt connection info + + unknown servers + Unbekannte Relais + No comment provided by engineer. + unknown status unbekannter Gruppenmitglieds-Status No comment provided by engineer. + + unmute + Stummschaltung aufheben + No comment provided by engineer. + + + unprotected + Ungeschützt + No comment provided by engineer. + updated group profile Aktualisiertes Gruppenprofil @@ -7500,6 +8657,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. über Relais No comment provided by engineer. + + video + Video + No comment provided by engineer. + video call (not e2e encrypted) Videoanruf (nicht E2E verschlüsselt) @@ -7525,6 +8687,11 @@ SimpleX-Server können Ihr Profil nicht einsehen. Wochen time unit + + when IP hidden + Wenn die IP-Adresse versteckt ist + No comment provided by engineer. + yes Ja @@ -7609,7 +8776,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.
- +
@@ -7619,7 +8786,7 @@ SimpleX-Server können Ihr Profil nicht einsehen. SimpleX needs camera access to scan QR codes to connect to other users and for video calls. - SimpleX benötigt Zugriff auf die Kamera, um QR Codes für die Verbindung mit anderen Nutzern zu scannen und Videoanrufe durchzuführen. + SimpleX benötigt Zugriff auf die Kamera, um QR Codes für die Verbindung mit anderen Benutzern zu scannen und Videoanrufe durchzuführen. Privacy - Camera Usage Description @@ -7646,7 +8813,7 @@ SimpleX-Server können Ihr Profil nicht einsehen.
- +
@@ -7661,9 +8828,223 @@ SimpleX-Server können Ihr Profil nicht einsehen. Copyright © 2022 SimpleX Chat. All rights reserved. - Copyright © 2022 SimpleX Chat. All rights reserved. + Copyright © 2024 SimpleX Chat. All rights reserved. Copyright (human-readable)
+ +
+ +
+ + + SimpleX SE + SimpleX SE + Bundle display name + + + SimpleX SE + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright © 2024 SimpleX Chat. Alle Rechte vorbehalten. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + %@ + No comment provided by engineer. + + + App is locked! + Die App ist gesperrt! + No comment provided by engineer. + + + Cancel + Abbrechen + No comment provided by engineer. + + + Cannot access keychain to save database password + Es ist nicht möglich, auf den Schlüsselbund zuzugreifen, um das Datenbankpasswort zu speichern + No comment provided by engineer. + + + Cannot forward message + Nachricht kann nicht weitergeleitet werden + No comment provided by engineer. + + + Comment + Kommentieren + No comment provided by engineer. + + + Currently maximum supported file size is %@. + Die maximale erlaubte Dateigröße beträgt aktuell %@. + No comment provided by engineer. + + + Database downgrade required + Datenbank-Herabstufung erforderlich + No comment provided by engineer. + + + Database encrypted! + Datenbank verschlüsselt! + No comment provided by engineer. + + + Database error + Datenbankfehler + No comment provided by engineer. + + + Database passphrase is different from saved in the keychain. + Das Datenbank-Passwort unterscheidet sich vom im Schlüsselbund gespeicherten. + No comment provided by engineer. + + + Database passphrase is required to open chat. + Ein Datenbank-Passwort ist erforderlich, um den Chat zu öffnen. + No comment provided by engineer. + + + Database upgrade required + Datenbank-Aktualisierung erforderlich + No comment provided by engineer. + + + Error preparing file + Fehler beim Vorbereiten der Datei + No comment provided by engineer. + + + Error preparing message + Fehler beim Vorbereiten der Nachricht + No comment provided by engineer. + + + Error: %@ + Fehler: %@ + No comment provided by engineer. + + + File error + Dateifehler + No comment provided by engineer. + + + Incompatible database version + Datenbank-Version nicht kompatibel + No comment provided by engineer. + + + Invalid migration confirmation + Migrations-Bestätigung ungültig + No comment provided by engineer. + + + Keychain error + Schlüsselbund-Fehler + No comment provided by engineer. + + + Large file! + Große Datei! + No comment provided by engineer. + + + No active profile + Kein aktives Profil + No comment provided by engineer. + + + Ok + OK + No comment provided by engineer. + + + Open the app to downgrade the database. + Öffne die App, um die Datenbank herabzustufen. + No comment provided by engineer. + + + Open the app to upgrade the database. + Öffne die App, um die Datenbank zu aktualisieren. + No comment provided by engineer. + + + Passphrase + Passwort + No comment provided by engineer. + + + Please create a profile in the SimpleX app + Bitte erstelle ein Profil in der SimpleX-App + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + Diese Nachricht ist wegen der gewählten Chat-Einstellungen nicht erlaubt. + No comment provided by engineer. + + + Sending a message takes longer than expected. + Das Senden einer Nachricht dauert länger als erwartet. + No comment provided by engineer. + + + Sending message… + Nachricht wird gesendet… + No comment provided by engineer. + + + Share + Teilen + No comment provided by engineer. + + + Slow network? + Langsames Netzwerk? + No comment provided by engineer. + + + Unknown database error: %@ + Unbekannter Datenbankfehler: %@ + No comment provided by engineer. + + + Unsupported format + Nicht unterstütztes Format + No comment provided by engineer. + + + Wait + Warten + No comment provided by engineer. + + + Wrong database passphrase + Falsches Datenbank-Passwort + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + Du kannst das Teilen in den Einstellungen zu Datenschutz & Sicherheit - SimpleX-Sperre erlauben. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/de.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/de.xcloc/contents.json b/apps/ios/SimpleX Localizations/de.xcloc/contents.json index 11924b71f5..18b517d802 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/de.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "de", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "15F31d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "15.4" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff b/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff index 18051ae350..b8432a33b6 100644 --- a/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff +++ b/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff @@ -336,8 +336,8 @@ Available in v5.1 Add servers by scanning QR codes. No comment provided by engineer.
- - Add server… + + Add server No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 2b5677dd61..c217793f03 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -2,7 +2,7 @@
- +
@@ -127,11 +127,6 @@ %@ is verified No comment provided by engineer. - - %@ servers - %@ servers - No comment provided by engineer. - %@ uploaded %@ uploaded @@ -557,16 +552,17 @@ About SimpleX address No comment provided by engineer. - - Accent color - Accent color + + Accent + Accent No comment provided by engineer. Accept Accept accept contact request via notification - accept incoming call via notification + accept incoming call via notification + swipe action Accept connection request? @@ -581,7 +577,23 @@ Accept incognito Accept incognito - accept contact request via notification + accept contact request via notification + swipe action + + + Acknowledged + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + Acknowledgement errors + No comment provided by engineer. + + + Active connections + Active connections + No comment provided by engineer. Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. @@ -603,16 +615,16 @@ Add profile No comment provided by engineer. + + Add server + Add server + No comment provided by engineer. + Add servers by scanning QR codes. Add servers by scanning QR codes. No comment provided by engineer. - - Add server… - Add server… - No comment provided by engineer. - Add to another device Add to another device @@ -623,6 +635,21 @@ Add welcome message No comment provided by engineer. + + Additional accent + Additional accent + No comment provided by engineer. + + + Additional accent 2 + Additional accent 2 + No comment provided by engineer. + + + Additional secondary + Additional secondary + No comment provided by engineer. + Address Address @@ -648,6 +675,11 @@ Advanced network settings No comment provided by engineer. + + Advanced settings + Advanced settings + No comment provided by engineer. + All app data is deleted. All app data is deleted. @@ -663,6 +695,11 @@ All data is erased when it is entered. No comment provided by engineer. + + All data is private to your device. + All data is private to your device. + No comment provided by engineer. + All group members will remain connected. All group members will remain connected. @@ -683,6 +720,11 @@ All new messages from %@ will be hidden! No comment provided by engineer. + + All profiles + All profiles + No comment provided by engineer. + All your contacts will remain connected. All your contacts will remain connected. @@ -708,11 +750,21 @@ Allow calls only if your contact allows them. No comment provided by engineer. + + Allow calls? + Allow calls? + No comment provided by engineer. + Allow disappearing messages only if your contact allows it to you. Allow disappearing messages only if your contact allows it to you. No comment provided by engineer. + + Allow downgrade + Allow downgrade + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Allow irreversible message deletion only if your contact allows it to you. (24 hours) @@ -738,6 +790,11 @@ Allow sending disappearing messages. No comment provided by engineer. + + Allow sharing + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Allow to irreversibly delete sent messages. (24 hours) @@ -808,6 +865,11 @@ Already joining the group! No comment provided by engineer. + + Always use private routing. + Always use private routing. + No comment provided by engineer. + Always use relay Always use relay @@ -873,11 +935,26 @@ Apply No comment provided by engineer. + + Apply to + Apply to + No comment provided by engineer. + Archive and upload Archive and upload No comment provided by engineer. + + Archive contacts to chat later. + Archive contacts to chat later. + No comment provided by engineer. + + + Archived contacts + Archived contacts + No comment provided by engineer. + Archiving database Archiving database @@ -948,6 +1025,11 @@ Back No comment provided by engineer. + + Background + Background + No comment provided by engineer. + Bad desktop address Bad desktop address @@ -973,6 +1055,16 @@ Better messages No comment provided by engineer. + + Better networking + Better networking + No comment provided by engineer. + + + Black + Black + No comment provided by engineer. + Block Block @@ -1008,6 +1100,16 @@ Blocked by admin No comment provided by engineer. + + Blur for better privacy. + Blur for better privacy. + No comment provided by engineer. + + + Blur media + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. Both you and your contact can add message reactions. @@ -1053,11 +1155,26 @@ Calls No comment provided by engineer. + + Calls prohibited! + Calls prohibited! + No comment provided by engineer. + Camera not available Camera not available No comment provided by engineer. + + Can't call contact + Can't call contact + No comment provided by engineer. + + + Can't call member + Can't call member + No comment provided by engineer. + Can't invite contact! Can't invite contact! @@ -1068,6 +1185,11 @@ Can't invite contacts! No comment provided by engineer. + + Can't message member + Can't message member + No comment provided by engineer. + Cancel Cancel @@ -1083,11 +1205,21 @@ Cannot access keychain to save database password No comment provided by engineer. + + Cannot forward message + Cannot forward message + No comment provided by engineer. + Cannot receive file Cannot receive file No comment provided by engineer. + + Capacity exceeded - recipient did not receive previously sent messages. + Capacity exceeded - recipient did not receive previously sent messages. + snd error text + Cellular Cellular @@ -1149,6 +1281,11 @@ Chat archive No comment provided by engineer. + + Chat colors + Chat colors + No comment provided by engineer. + Chat console Chat console @@ -1164,6 +1301,11 @@ Chat database deleted No comment provided by engineer. + + Chat database exported + Chat database exported + No comment provided by engineer. + Chat database imported Chat database imported @@ -1184,6 +1326,11 @@ Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat. No comment provided by engineer. + + Chat list + Chat list + No comment provided by engineer. + Chat migrated! Chat migrated! @@ -1194,6 +1341,11 @@ Chat preferences No comment provided by engineer. + + Chat theme + Chat theme + No comment provided by engineer. + Chats Chats @@ -1224,10 +1376,25 @@ Choose from library No comment provided by engineer. + + Chunks deleted + Chunks deleted + No comment provided by engineer. + + + Chunks downloaded + Chunks downloaded + No comment provided by engineer. + + + Chunks uploaded + Chunks uploaded + No comment provided by engineer. + Clear Clear - No comment provided by engineer. + swipe action Clear conversation @@ -1249,9 +1416,14 @@ Clear verification No comment provided by engineer. - - Colors - Colors + + Color chats with the new themes. + Color chats with the new themes. + No comment provided by engineer. + + + Color mode + Color mode No comment provided by engineer. @@ -1264,11 +1436,21 @@ Compare security codes with your contacts. No comment provided by engineer. + + Completed + Completed + No comment provided by engineer. + Configure ICE servers Configure ICE servers No comment provided by engineer. + + Configured %@ servers + Configured %@ servers + No comment provided by engineer. + Confirm Confirm @@ -1279,11 +1461,21 @@ Confirm Passcode No comment provided by engineer. + + Confirm contact deletion? + Confirm contact deletion? + No comment provided by engineer. + Confirm database upgrades Confirm database upgrades No comment provided by engineer. + + Confirm files from unknown servers. + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings Confirm network settings @@ -1329,6 +1521,11 @@ Connect to desktop No comment provided by engineer. + + Connect to your friends faster. + Connect to your friends faster. + No comment provided by engineer. + Connect to yourself? Connect to yourself? @@ -1368,16 +1565,31 @@ This is your own one-time link! Connect with %@ No comment provided by engineer. + + Connected + Connected + No comment provided by engineer. + Connected desktop Connected desktop No comment provided by engineer. + + Connected servers + Connected servers + No comment provided by engineer. + Connected to desktop Connected to desktop No comment provided by engineer. + + Connecting + Connecting + No comment provided by engineer. + Connecting to server… Connecting to server… @@ -1388,6 +1600,11 @@ This is your own one-time link! Connecting to server… (error: %@) No comment provided by engineer. + + Connecting to contact, please wait or check later! + Connecting to contact, please wait or check later! + No comment provided by engineer. + Connecting to desktop Connecting to desktop @@ -1398,6 +1615,11 @@ This is your own one-time link! Connection No comment provided by engineer. + + Connection and servers status. + Connection and servers status. + No comment provided by engineer. + Connection error Connection error @@ -1408,6 +1630,11 @@ This is your own one-time link! Connection error (AUTH) No comment provided by engineer. + + Connection notifications + Connection notifications + No comment provided by engineer. + Connection request sent! Connection request sent! @@ -1423,6 +1650,16 @@ This is your own one-time link! Connection timeout No comment provided by engineer. + + Connection with desktop stopped + Connection with desktop stopped + No comment provided by engineer. + + + Connections + Connections + No comment provided by engineer. + Contact allows Contact allows @@ -1433,6 +1670,11 @@ This is your own one-time link! Contact already exists No comment provided by engineer. + + Contact deleted! + Contact deleted! + No comment provided by engineer. + Contact hidden: Contact hidden: @@ -1443,9 +1685,9 @@ This is your own one-time link! Contact is connected notification - - Contact is not connected yet! - Contact is not connected yet! + + Contact is deleted. + Contact is deleted. No comment provided by engineer. @@ -1458,6 +1700,11 @@ This is your own one-time link! Contact preferences No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + Contact will be deleted - this cannot be undone! + No comment provided by engineer. + Contacts Contacts @@ -1473,10 +1720,20 @@ This is your own one-time link! Continue No comment provided by engineer. + + Conversation deleted! + Conversation deleted! + No comment provided by engineer. + Copy Copy - chat item action + No comment provided by engineer. + + + Copy error + Copy error + No comment provided by engineer. Core version: v%@ @@ -1553,6 +1810,11 @@ This is your own one-time link! Create your profile No comment provided by engineer. + + Created + Created + No comment provided by engineer. + Created at Created at @@ -1588,6 +1850,11 @@ This is your own one-time link! Current passphrase… No comment provided by engineer. + + Current profile + Current profile + No comment provided by engineer. + Currently maximum supported file size is %@. Currently maximum supported file size is %@. @@ -1598,11 +1865,21 @@ This is your own one-time link! Custom time No comment provided by engineer. + + Customize theme + Customize theme + No comment provided by engineer. + Dark Dark No comment provided by engineer. + + Dark mode colors + Dark mode colors + No comment provided by engineer. + Database ID Database ID @@ -1701,6 +1978,11 @@ This is your own one-time link! Database will be migrated when the app restarts No comment provided by engineer. + + Debug delivery + Debug delivery + No comment provided by engineer. + Decentralized Decentralized @@ -1714,18 +1996,19 @@ This is your own one-time link! Delete Delete - chat item action + chat item action + swipe action + + + Delete %lld messages of members? + Delete %lld messages of members? + No comment provided by engineer. Delete %lld messages? Delete %lld messages? No comment provided by engineer. - - Delete Contact - Delete Contact - No comment provided by engineer. - Delete address Delete address @@ -1781,11 +2064,9 @@ This is your own one-time link! Delete contact No comment provided by engineer. - - Delete contact? -This cannot be undone! - Delete contact? -This cannot be undone! + + Delete contact? + Delete contact? No comment provided by engineer. @@ -1878,11 +2159,6 @@ This cannot be undone! Delete old database? No comment provided by engineer. - - Delete pending connection - Delete pending connection - No comment provided by engineer. - Delete pending connection? Delete pending connection? @@ -1898,11 +2174,26 @@ This cannot be undone! Delete queue server test step + + Delete up to 20 messages at once. + Delete up to 20 messages at once. + No comment provided by engineer. + Delete user profile? Delete user profile? No comment provided by engineer. + + Delete without notification + Delete without notification + No comment provided by engineer. + + + Deleted + Deleted + No comment provided by engineer. + Deleted at Deleted at @@ -1913,6 +2204,11 @@ This cannot be undone! Deleted at: %@ copied message info + + Deletion errors + Deletion errors + No comment provided by engineer. + Delivery Delivery @@ -1948,11 +2244,41 @@ This cannot be undone! Desktop devices No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + Destination server address of %@ is incompatible with forwarding server %@ settings. + No comment provided by engineer. + + + Destination server error: %@ + Destination server error: %@ + snd error text + + + Destination server version of %@ is incompatible with forwarding server %@. + Destination server version of %@ is incompatible with forwarding server %@. + No comment provided by engineer. + + + Detailed statistics + Detailed statistics + No comment provided by engineer. + + + Details + Details + No comment provided by engineer. + Develop Develop No comment provided by engineer. + + Developer options + Developer options + No comment provided by engineer. + Developer tools Developer tools @@ -2003,6 +2329,11 @@ This cannot be undone! Disable for all No comment provided by engineer. + + Disabled + Disabled + No comment provided by engineer. + Disappearing message Disappearing message @@ -2053,11 +2384,21 @@ This cannot be undone! Discover via local network No comment provided by engineer. + + Do NOT send messages directly, even if your or destination server does not support private routing. + Do NOT send messages directly, even if your or destination server does not support private routing. + No comment provided by engineer. + Do NOT use SimpleX for emergency calls. Do NOT use SimpleX for emergency calls. No comment provided by engineer. + + Do NOT use private routing. + Do NOT use private routing. + No comment provided by engineer. + Do it later Do it later @@ -2093,6 +2434,11 @@ This cannot be undone! Download chat item action + + Download errors + Download errors + No comment provided by engineer. + Download failed Download failed @@ -2103,6 +2449,16 @@ This cannot be undone! Download file server test step + + Downloaded + Downloaded + No comment provided by engineer. + + + Downloaded files + Downloaded files + No comment provided by engineer. + Downloading archive Downloading archive @@ -2203,6 +2559,11 @@ This cannot be undone! Enable self-destruct passcode set passcode view + + Enabled + Enabled + No comment provided by engineer. + Enabled for Enabled for @@ -2373,6 +2734,11 @@ This cannot be undone! Error changing setting No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address Error creating address @@ -2423,11 +2789,6 @@ This cannot be undone! Error deleting connection No comment provided by engineer. - - Error deleting contact - Error deleting contact - No comment provided by engineer. - Error deleting database Error deleting database @@ -2473,6 +2834,11 @@ This cannot be undone! Error exporting chat database No comment provided by engineer. + + Error exporting theme: %@ + Error exporting theme: %@ + No comment provided by engineer. + Error importing chat database Error importing chat database @@ -2498,11 +2864,26 @@ This cannot be undone! Error receiving file No comment provided by engineer. + + Error reconnecting server + Error reconnecting server + No comment provided by engineer. + + + Error reconnecting servers + Error reconnecting servers + No comment provided by engineer. + Error removing member Error removing member No comment provided by engineer. + + Error resetting statistics + Error resetting statistics + No comment provided by engineer. + Error saving %@ servers Error saving %@ servers @@ -2621,7 +3002,8 @@ This cannot be undone! Error: %@ Error: %@ - No comment provided by engineer. + file error text + snd error text Error: URL is invalid @@ -2633,6 +3015,11 @@ This cannot be undone! Error: no database file No comment provided by engineer. + + Errors + Errors + No comment provided by engineer. + Even when disabled in the conversation. Even when disabled in the conversation. @@ -2658,6 +3045,11 @@ This cannot be undone! Export error: No comment provided by engineer. + + Export theme + Export theme + No comment provided by engineer. + Exported database archive. Exported database archive. @@ -2691,8 +3083,33 @@ This cannot be undone! Favorite Favorite + swipe action + + + File error + File error No comment provided by engineer. + + File not found - most likely file was deleted or cancelled. + File not found - most likely file was deleted or cancelled. + file error text + + + File server error: %@ + File server error: %@ + file error text + + + File status + File status + No comment provided by engineer. + + + File status: %@ + File status: %@ + copied message info + File will be deleted from servers. File will be deleted from servers. @@ -2713,6 +3130,11 @@ This cannot be undone! File: %@ No comment provided by engineer. + + Files + Files + No comment provided by engineer. + Files & media Files & media @@ -2818,6 +3240,35 @@ This cannot be undone! Forwarded from No comment provided by engineer. + + Forwarding server %@ failed to connect to destination server %@. Please try later. + Forwarding server %@ failed to connect to destination server %@. Please try later. + No comment provided by engineer. + + + Forwarding server address is incompatible with network settings: %@. + Forwarding server address is incompatible with network settings: %@. + No comment provided by engineer. + + + Forwarding server version is incompatible with network settings: %@. + Forwarding server version is incompatible with network settings: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + Forwarding server: %1$@ +Destination server error: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + Forwarding server: %1$@ +Error: %2$@ + snd error text + Found desktop Found desktop @@ -2863,6 +3314,16 @@ This cannot be undone! GIFs and stickers No comment provided by engineer. + + Good afternoon! + Good afternoon! + message preview + + + Good morning! + Good morning! + message preview + Group Group @@ -3143,6 +3604,11 @@ This cannot be undone! Import failed No comment provided by engineer. + + Import theme + Import theme + No comment provided by engineer. + Importing archive Importing archive @@ -3265,6 +3731,11 @@ This cannot be undone! Interface No comment provided by engineer. + + Interface colors + Interface colors + No comment provided by engineer. + Invalid QR code Invalid QR code @@ -3366,6 +3837,11 @@ This cannot be undone! 3. The connection was compromised. No comment provided by engineer. + + It protects your IP address and connections. + It protects your IP address and connections. + No comment provided by engineer. + It seems like you are already connected via this link. If it is not the case, there was an error (%@). It seems like you are already connected via this link. If it is not the case, there was an error (%@). @@ -3384,7 +3860,7 @@ This cannot be undone! Join Join - No comment provided by engineer. + swipe action Join group @@ -3428,6 +3904,11 @@ This is your link for group %@! Keep No comment provided by engineer. + + Keep conversation + Keep conversation + No comment provided by engineer. + Keep the app open to use it from desktop Keep the app open to use it from desktop @@ -3471,7 +3952,7 @@ This is your link for group %@! Leave Leave - No comment provided by engineer. + swipe action Leave group @@ -3603,11 +4084,26 @@ This is your link for group %@! Max 30 seconds, received instantly. No comment provided by engineer. + + Media & file servers + Media & file servers + No comment provided by engineer. + + + Medium + Medium + blur media + Member Member No comment provided by engineer. + + Member inactive + Member inactive + item status text + Member role will be changed to "%@". All group members will be notified. Member role will be changed to "%@". All group members will be notified. @@ -3623,6 +4119,11 @@ This is your link for group %@! Member will be removed from group - this cannot be undone! No comment provided by engineer. + + Menus + Menus + No comment provided by engineer. + Message delivery error Message delivery error @@ -3633,11 +4134,31 @@ This is your link for group %@! Message delivery receipts! No comment provided by engineer. + + Message delivery warning + Message delivery warning + item status text + Message draft Message draft No comment provided by engineer. + + Message forwarded + Message forwarded + item status text + + + Message may be delivered later if member becomes active. + Message may be delivered later if member becomes active. + item status description + + + Message queue info + Message queue info + No comment provided by engineer. + Message reactions Message reactions @@ -3653,11 +4174,31 @@ This is your link for group %@! Message reactions are prohibited in this group. No comment provided by engineer. + + Message reception + Message reception + No comment provided by engineer. + + + Message servers + Message servers + No comment provided by engineer. + Message source remains private. Message source remains private. No comment provided by engineer. + + Message status + Message status + No comment provided by engineer. + + + Message status: %@ + Message status: %@ + copied message info + Message text Message text @@ -3683,6 +4224,16 @@ This is your link for group %@! Messages from %@ will be shown! No comment provided by engineer. + + Messages received + Messages received + No comment provided by engineer. + + + Messages sent + Messages sent + No comment provided by engineer. + Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. @@ -3783,11 +4334,6 @@ This is your link for group %@! Most likely this connection is deleted. item status description - - Most likely this contact has deleted the connection with you. - Most likely this contact has deleted the connection with you. - No comment provided by engineer. - Multiple chat profiles Multiple chat profiles @@ -3796,7 +4342,7 @@ This is your link for group %@! Mute Mute - No comment provided by engineer. + swipe action Muted when inactive! @@ -3806,7 +4352,7 @@ This is your link for group %@! Name Name - No comment provided by engineer. + swipe action Network & servers @@ -3818,6 +4364,11 @@ This is your link for group %@! Network connection No comment provided by engineer. + + Network issues - message expired after many attempts to send it. + Network issues - message expired after many attempts to send it. + snd error text + Network management Network management @@ -3843,6 +4394,11 @@ This is your link for group %@! New chat No comment provided by engineer. + + New chat experience 🎉 + New chat experience 🎉 + No comment provided by engineer. + New contact request New contact request @@ -3873,6 +4429,11 @@ This is your link for group %@! New in %@ No comment provided by engineer. + + New media options + New media options + No comment provided by engineer. + New member role New member role @@ -3918,6 +4479,11 @@ This is your link for group %@! No device token! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + No direct connection yet, message is forwarded by admin. + item status description + No filtered chats No filtered chats @@ -3933,6 +4499,11 @@ This is your link for group %@! No history No comment provided by engineer. + + No info, try to reload + No info, try to reload + No comment provided by engineer. + No network connection No network connection @@ -3953,6 +4524,11 @@ This is your link for group %@! Not compatible! No comment provided by engineer. + + Nothing selected + Nothing selected + No comment provided by engineer. + Notifications Notifications @@ -3980,7 +4556,7 @@ This is your link for group %@! Off Off - No comment provided by engineer. + blur media Ok @@ -4002,14 +4578,18 @@ This is your link for group %@! One-time invitation link No comment provided by engineer. - - Onion hosts will be required for connection. Requires enabling VPN. - Onion hosts will be required for connection. Requires enabling VPN. + + Onion hosts will be **required** for connection. +Requires compatible VPN. + Onion hosts will be **required** for connection. +Requires compatible VPN. No comment provided by engineer. - - Onion hosts will be used when available. Requires enabling VPN. - Onion hosts will be used when available. Requires enabling VPN. + + Onion hosts will be used when available. +Requires compatible VPN. + Onion hosts will be used when available. +Requires compatible VPN. No comment provided by engineer. @@ -4022,6 +4602,11 @@ This is your link for group %@! Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. No comment provided by engineer. + + Only delete conversation + Only delete conversation + No comment provided by engineer. + Only group owners can change group preferences. Only group owners can change group preferences. @@ -4117,6 +4702,11 @@ This is your link for group %@! Open migration to another device authentication reason + + Open server settings + Open server settings + No comment provided by engineer. + Open user profiles Open user profiles @@ -4157,6 +4747,11 @@ This is your link for group %@! Other No comment provided by engineer. + + Other %@ servers + Other %@ servers + No comment provided by engineer. + PING count PING count @@ -4222,6 +4817,11 @@ This is your link for group %@! Paste the link you received No comment provided by engineer. + + Pending + Pending + No comment provided by engineer. + People can connect to you only via the links you share. People can connect to you only via the links you share. @@ -4242,11 +4842,28 @@ This is your link for group %@! Picture-in-picture calls No comment provided by engineer. + + Play from the chat list. + Play from the chat list. + No comment provided by engineer. + + + Please ask your contact to enable calls. + Please ask your contact to enable calls. + No comment provided by engineer. + Please ask your contact to enable sending voice messages. Please ask your contact to enable sending voice messages. No comment provided by engineer. + + Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers. + Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers. + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. Please check that you used the correct link or ask your contact to send you another one. @@ -4344,6 +4961,11 @@ Error: %@ Preview No comment provided by engineer. + + Previously connected servers + Previously connected servers + No comment provided by engineer. + Privacy & security Privacy & security @@ -4359,11 +4981,31 @@ Error: %@ Private filenames No comment provided by engineer. + + Private message routing + Private message routing + No comment provided by engineer. + + + Private message routing 🚀 + Private message routing 🚀 + No comment provided by engineer. + Private notes Private notes name of notes to self + + Private routing + Private routing + No comment provided by engineer. + + + Private routing error + Private routing error + No comment provided by engineer. + Profile and server connections Profile and server connections @@ -4394,6 +5036,11 @@ Error: %@ Profile password No comment provided by engineer. + + Profile theme + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. Profile update will be sent to your contacts. @@ -4444,11 +5091,23 @@ Error: %@ Prohibit sending voice messages. No comment provided by engineer. + + Protect IP address + Protect IP address + No comment provided by engineer. + Protect app screen Protect app screen No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! Protect your chat profiles with a password! @@ -4464,6 +5123,16 @@ Error: %@ Protocol timeout per KB No comment provided by engineer. + + Proxied + Proxied + No comment provided by engineer. + + + Proxied servers + Proxied servers + No comment provided by engineer. + Push notifications Push notifications @@ -4484,6 +5153,11 @@ Error: %@ Rate the app No comment provided by engineer. + + Reachable chat toolbar + Reachable chat toolbar + No comment provided by engineer. + React… React… @@ -4492,7 +5166,7 @@ Error: %@ Read Read - No comment provided by engineer. + swipe action Read more @@ -4529,6 +5203,11 @@ Error: %@ Receipts are disabled No comment provided by engineer. + + Receive errors + Receive errors + No comment provided by engineer. + Received at Received at @@ -4549,16 +5228,26 @@ Error: %@ Received message message info title + + Received messages + Received messages + No comment provided by engineer. + + + Received reply + Received reply + No comment provided by engineer. + + + Received total + Received total + No comment provided by engineer. + Receiving address will be changed to a different server. Address change will complete after sender comes online. Receiving address will be changed to a different server. Address change will complete after sender comes online. No comment provided by engineer. - - Receiving concurrency - Receiving concurrency - No comment provided by engineer. - Receiving file will be stopped. Receiving file will be stopped. @@ -4584,11 +5273,36 @@ Error: %@ Recipients see updates as you type them. No comment provided by engineer. + + Reconnect + Reconnect + No comment provided by engineer. + Reconnect all connected servers to force message delivery. It uses additional traffic. Reconnect all connected servers to force message delivery. It uses additional traffic. No comment provided by engineer. + + Reconnect all servers + Reconnect all servers + No comment provided by engineer. + + + Reconnect all servers? + Reconnect all servers? + No comment provided by engineer. + + + Reconnect server to force message delivery. It uses additional traffic. + Reconnect server to force message delivery. It uses additional traffic. + No comment provided by engineer. + + + Reconnect server? + Reconnect server? + No comment provided by engineer. + Reconnect servers? Reconnect servers? @@ -4612,7 +5326,8 @@ Error: %@ Reject Reject - reject incoming call via notification + reject incoming call via notification + swipe action Reject (sender NOT notified) @@ -4639,6 +5354,11 @@ Error: %@ Remove No comment provided by engineer. + + Remove image + Remove image + No comment provided by engineer. + Remove member Remove member @@ -4709,16 +5429,41 @@ Error: %@ Reset No comment provided by engineer. + + Reset all hints + Reset all hints + No comment provided by engineer. + + + Reset all statistics + Reset all statistics + No comment provided by engineer. + + + Reset all statistics? + Reset all statistics? + No comment provided by engineer. + Reset colors Reset colors No comment provided by engineer. + + Reset to app theme + Reset to app theme + No comment provided by engineer. + Reset to defaults Reset to defaults No comment provided by engineer. + + Reset to user theme + Reset to user theme + No comment provided by engineer. + Restart the app to create a new chat profile Restart the app to create a new chat profile @@ -4759,11 +5504,6 @@ Error: %@ Reveal chat item action - - Revert - Revert - No comment provided by engineer. - Revoke Revoke @@ -4789,9 +5529,14 @@ Error: %@ Run chat No comment provided by engineer. - - SMP servers - SMP servers + + SMP server + SMP server + No comment provided by engineer. + + + Safely receive files + Safely receive files No comment provided by engineer. @@ -4819,6 +5564,11 @@ Error: %@ Save and notify group members No comment provided by engineer. + + Save and reconnect + Save and reconnect + No comment provided by engineer. + Save and update group profile Save and update group profile @@ -4899,6 +5649,16 @@ Error: %@ Saved message message info title + + Scale + Scale + No comment provided by engineer. + + + Scan / Paste link + Scan / Paste link + No comment provided by engineer. + Scan QR code Scan QR code @@ -4939,11 +5699,21 @@ Error: %@ Search or paste SimpleX link No comment provided by engineer. + + Secondary + Secondary + No comment provided by engineer. + Secure queue Secure queue server test step + + Secured + Secured + No comment provided by engineer. + Security assessment Security assessment @@ -4957,6 +5727,16 @@ Error: %@ Select Select + chat item action + + + Selected %lld + Selected %lld + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + Selected chat preferences prohibit this message. No comment provided by engineer. @@ -4994,11 +5774,6 @@ Error: %@ Send delivery receipts to No comment provided by engineer. - - Send direct message - Send direct message - No comment provided by engineer. - Send direct message to connect Send direct message to connect @@ -5009,6 +5784,11 @@ Error: %@ Send disappearing message No comment provided by engineer. + + Send errors + Send errors + No comment provided by engineer. + Send link previews Send link previews @@ -5019,6 +5799,21 @@ Error: %@ Send live message No comment provided by engineer. + + Send message to enable calls. + Send message to enable calls. + No comment provided by engineer. + + + Send messages directly when IP address is protected and your or destination server does not support private routing. + Send messages directly when IP address is protected and your or destination server does not support private routing. + No comment provided by engineer. + + + Send messages directly when your or destination server does not support private routing. + Send messages directly when your or destination server does not support private routing. + No comment provided by engineer. + Send notifications Send notifications @@ -5109,6 +5904,11 @@ Error: %@ Sent at: %@ copied message info + + Sent directly + Sent directly + No comment provided by engineer. + Sent file event Sent file event @@ -5119,11 +5919,46 @@ Error: %@ Sent message message info title + + Sent messages + Sent messages + No comment provided by engineer. + Sent messages will be deleted after set time. Sent messages will be deleted after set time. No comment provided by engineer. + + Sent reply + Sent reply + No comment provided by engineer. + + + Sent total + Sent total + No comment provided by engineer. + + + Sent via proxy + Sent via proxy + No comment provided by engineer. + + + Server address + Server address + No comment provided by engineer. + + + Server address is incompatible with network settings. + Server address is incompatible with network settings. + srv error text. + + + Server address is incompatible with network settings: %@. + Server address is incompatible with network settings: %@. + No comment provided by engineer. + Server requires authorization to create queues, check password Server requires authorization to create queues, check password @@ -5139,11 +5974,36 @@ Error: %@ Server test failed! No comment provided by engineer. + + Server type + Server type + No comment provided by engineer. + + + Server version is incompatible with network settings. + Server version is incompatible with network settings. + srv error text + + + Server version is incompatible with your app: %@. + Server version is incompatible with your app: %@. + No comment provided by engineer. + Servers Servers No comment provided by engineer. + + Servers info + Servers info + No comment provided by engineer. + + + Servers statistics will be reset - this cannot be undone! + Servers statistics will be reset - this cannot be undone! + No comment provided by engineer. + Session code Session code @@ -5159,6 +6019,11 @@ Error: %@ Set contact name… No comment provided by engineer. + + Set default theme + Set default theme + No comment provided by engineer. + Set group preferences Set group preferences @@ -5224,6 +6089,11 @@ Error: %@ Share address with contacts? No comment provided by engineer. + + Share from other apps. + Share from other apps. + No comment provided by engineer. + Share link Share link @@ -5234,6 +6104,11 @@ Error: %@ Share this 1-time invite link No comment provided by engineer. + + Share to SimpleX + Share to SimpleX + No comment provided by engineer. + Share with contacts Share with contacts @@ -5259,16 +6134,36 @@ Error: %@ Show last messages No comment provided by engineer. + + Show message status + Show message status + No comment provided by engineer. + + + Show percentage + Show percentage + No comment provided by engineer. + Show preview Show preview No comment provided by engineer. + + Show → on messages sent via private routing. + Show → on messages sent via private routing. + No comment provided by engineer. + Show: Show: No comment provided by engineer. + + SimpleX + SimpleX + No comment provided by engineer. + SimpleX Address SimpleX Address @@ -5344,6 +6239,11 @@ Error: %@ Simplified incognito mode No comment provided by engineer. + + Size + Size + No comment provided by engineer. + Skip Skip @@ -5359,11 +6259,26 @@ Error: %@ Small groups (max 20) No comment provided by engineer. + + Soft + Soft + blur media + + + Some file(s) were not exported: + Some file(s) were not exported: + No comment provided by engineer. + Some non-fatal errors occurred during import - you may see Chat console for more details. Some non-fatal errors occurred during import - you may see Chat console for more details. No comment provided by engineer. + + Some non-fatal errors occurred during import: + Some non-fatal errors occurred during import: + No comment provided by engineer. + Somebody Somebody @@ -5389,6 +6304,16 @@ Error: %@ Start migration No comment provided by engineer. + + Starting from %@. + Starting from %@. + No comment provided by engineer. + + + Statistics + Statistics + No comment provided by engineer. + Stop Stop @@ -5449,11 +6374,31 @@ Error: %@ Stopping chat No comment provided by engineer. + + Strong + Strong + blur media + Submit Submit No comment provided by engineer. + + Subscribed + Subscribed + No comment provided by engineer. + + + Subscription errors + Subscription errors + No comment provided by engineer. + + + Subscriptions ignored + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat Support SimpleX Chat @@ -5469,6 +6414,11 @@ Error: %@ System authentication No comment provided by engineer. + + TCP connection + TCP connection + No comment provided by engineer. + TCP connection timeout TCP connection timeout @@ -5529,9 +6479,9 @@ Error: %@ Tap to scan No comment provided by engineer. - - Tap to start a new chat - Tap to start a new chat + + Temporary file error + Temporary file error No comment provided by engineer. @@ -5586,6 +6536,11 @@ It can happen because of some bug or when the connection is compromised.The app can notify you when you receive messages or contact requests - please open settings to enable. No comment provided by engineer. + + The app will ask to confirm downloads from unknown file servers (except .onion). + The app will ask to confirm downloads from unknown file servers (except .onion). + No comment provided by engineer. + The attempt to change database passphrase was not completed. The attempt to change database passphrase was not completed. @@ -5631,6 +6586,16 @@ It can happen because of some bug or when the connection is compromised.The message will be marked as moderated for all members. No comment provided by engineer. + + The messages will be deleted for all members. + The messages will be deleted for all members. + No comment provided by engineer. + + + The messages will be marked as moderated for all members. + The messages will be marked as moderated for all members. + No comment provided by engineer. + The next generation of private messaging The next generation of private messaging @@ -5666,9 +6631,9 @@ It can happen because of some bug or when the connection is compromised.The text you pasted is not a SimpleX link. No comment provided by engineer. - - Theme - Theme + + Themes + Themes No comment provided by engineer. @@ -5736,11 +6701,21 @@ It can happen because of some bug or when the connection is compromised.This is your own one-time link! No comment provided by engineer. + + This link was used with another mobile device, please create a new link on the desktop. + This link was used with another mobile device, please create a new link on the desktop. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. This setting applies to messages in your current chat profile **%@**. No comment provided by engineer. + + Title + Title + No comment provided by engineer. + To ask any questions and to receive updates: To ask any questions and to receive updates: @@ -5771,6 +6746,11 @@ It can happen because of some bug or when the connection is compromised.To protect timezone, image/voice files use UTC. No comment provided by engineer. + + To protect your IP address, private routing uses your SMP servers to deliver messages. + To protect your IP address, private routing uses your SMP servers to deliver messages. + No comment provided by engineer. + To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5798,16 +6778,36 @@ You will be prompted to complete authentication before this feature is enabled.< To verify end-to-end encryption with your contact compare (or scan) the code on your devices. No comment provided by engineer. + + Toggle chat list: + Toggle chat list: + No comment provided by engineer. + Toggle incognito when connecting. Toggle incognito when connecting. No comment provided by engineer. + + Toolbar opacity + Toolbar opacity + No comment provided by engineer. + + + Total + Total + No comment provided by engineer. + Transport isolation Transport isolation No comment provided by engineer. + + Transport sessions + Transport sessions + No comment provided by engineer. + Trying to connect to the server used to receive messages from this contact (error: %@). Trying to connect to the server used to receive messages from this contact (error: %@). @@ -5863,11 +6863,6 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock member? No comment provided by engineer. - - Unexpected error: %@ - Unexpected error: %@ - item status description - Unexpected migration state Unexpected migration state @@ -5876,7 +6871,7 @@ You will be prompted to complete authentication before this feature is enabled.< Unfav. Unfav. - No comment provided by engineer. + swipe action Unhide @@ -5913,6 +6908,11 @@ You will be prompted to complete authentication before this feature is enabled.< Unknown error No comment provided by engineer. + + Unknown servers! + Unknown servers! + No comment provided by engineer. + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. @@ -5948,12 +6948,12 @@ To connect, please ask your contact to create another connection link and check Unmute Unmute - No comment provided by engineer. + swipe action Unread Unread - No comment provided by engineer. + swipe action Up to 100 last messages are sent to new members. @@ -5965,11 +6965,6 @@ To connect, please ask your contact to create another connection link and check Update No comment provided by engineer. - - Update .onion hosts setting? - Update .onion hosts setting? - No comment provided by engineer. - Update database passphrase Update database passphrase @@ -5980,9 +6975,9 @@ To connect, please ask your contact to create another connection link and check Update network settings? No comment provided by engineer. - - Update transport isolation mode? - Update transport isolation mode? + + Update settings? + Update settings? No comment provided by engineer. @@ -5990,16 +6985,16 @@ To connect, please ask your contact to create another connection link and check Updating settings will re-connect the client to all servers. No comment provided by engineer. - - Updating this setting will re-connect the client to all servers. - Updating this setting will re-connect the client to all servers. - No comment provided by engineer. - Upgrade and open chat Upgrade and open chat No comment provided by engineer. + + Upload errors + Upload errors + No comment provided by engineer. + Upload failed Upload failed @@ -6010,6 +7005,16 @@ To connect, please ask your contact to create another connection link and check Upload file server test step + + Uploaded + Uploaded + No comment provided by engineer. + + + Uploaded files + Uploaded files + No comment provided by engineer. + Uploading archive Uploading archive @@ -6060,6 +7065,16 @@ To connect, please ask your contact to create another connection link and check Use only local notifications? No comment provided by engineer. + + Use private routing with unknown servers when IP address is not protected. + Use private routing with unknown servers when IP address is not protected. + No comment provided by engineer. + + + Use private routing with unknown servers. + Use private routing with unknown servers. + No comment provided by engineer. + Use server Use server @@ -6070,14 +7085,19 @@ To connect, please ask your contact to create another connection link and check Use the app while in the call. No comment provided by engineer. + + Use the app with one hand. + Use the app with one hand. + No comment provided by engineer. + User profile User profile No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - Using .onion hosts requires compatible VPN provider. + + User selection + User selection No comment provided by engineer. @@ -6210,6 +7230,16 @@ To connect, please ask your contact to create another connection link and check Waiting for video No comment provided by engineer. + + Wallpaper accent + Wallpaper accent + No comment provided by engineer. + + + Wallpaper background + Wallpaper background + No comment provided by engineer. + Warning: starting chat on multiple devices is not supported and will cause message delivery failures Warning: starting chat on multiple devices is not supported and will cause message delivery failures @@ -6295,19 +7325,39 @@ To connect, please ask your contact to create another connection link and check With reduced battery usage. No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + Without Tor or VPN, your IP address will be visible to file servers. + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + No comment provided by engineer. + Wrong database passphrase Wrong database passphrase No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + Wrong key or unknown connection - most likely this connection is deleted. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + Wrong key or unknown file chunk address - most likely file is deleted. + file error text + Wrong passphrase! Wrong passphrase! No comment provided by engineer. - - XFTP servers - XFTP servers + + XFTP server + XFTP server No comment provided by engineer. @@ -6387,11 +7437,21 @@ Repeat join request? You are invited to group No comment provided by engineer. + + You are not connected to these servers. Private routing is used to deliver messages to them. + You are not connected to these servers. Private routing is used to deliver messages to them. + No comment provided by engineer. + You can accept calls from lock screen, without device and app authentication. You can accept calls from lock screen, without device and app authentication. No comment provided by engineer. + + You can change it in Appearance settings. + You can change it in Appearance settings. + No comment provided by engineer. + You can create it later You can create it later @@ -6422,11 +7482,16 @@ Repeat join request? You can make it visible to your SimpleX contacts via Settings. No comment provided by engineer. - - You can now send messages to %@ - You can now send messages to %@ + + You can now chat with %@ + You can now chat with %@ notification body + + You can send messages to %@ from Archived contacts. + You can send messages to %@ from Archived contacts. + No comment provided by engineer. + You can set lock screen notification preview via settings. You can set lock screen notification preview via settings. @@ -6452,6 +7517,11 @@ Repeat join request? You can start chat via app Settings / Database or by restarting the app No comment provided by engineer. + + You can still view conversation with %@ in the list of chats. + You can still view conversation with %@ in the list of chats. + No comment provided by engineer. + You can turn on SimpleX Lock via Settings. You can turn on SimpleX Lock via Settings. @@ -6494,11 +7564,6 @@ Repeat connection request? Repeat connection request? No comment provided by engineer. - - You have no chats - You have no chats - No comment provided by engineer. - You have to enter passphrase every time the app starts - it is not stored on the device. You have to enter passphrase every time the app starts - it is not stored on the device. @@ -6519,11 +7584,26 @@ Repeat connection request? You joined this group. Connecting to inviting group member. No comment provided by engineer. + + You may migrate the exported database. + You may migrate the exported database. + No comment provided by engineer. + + + You may save the exported archive. + You may save the exported archive. + No comment provided by engineer. + You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. No comment provided by engineer. + + You need to allow your contact to call to be able to call them. + You need to allow your contact to call to be able to call them. + No comment provided by engineer. + You need to allow your contact to send voice messages to be able to send them. You need to allow your contact to send voice messages to be able to send them. @@ -6639,13 +7719,6 @@ Repeat connection request? Your chat profiles No comment provided by engineer. - - Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link). - Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link). - No comment provided by engineer. - Your contact sent a file that is larger than currently supported maximum size (%@). Your contact sent a file that is larger than currently supported maximum size (%@). @@ -6793,6 +7866,11 @@ SimpleX servers cannot see your profile. and %lld other events No comment provided by engineer. + + attempts + attempts + No comment provided by engineer. + audio call (not e2e encrypted) audio call (not e2e encrypted) @@ -6833,6 +7911,11 @@ SimpleX servers cannot see your profile. bold No comment provided by engineer. + + call + call + No comment provided by engineer. + call error call error @@ -6983,6 +8066,11 @@ SimpleX servers cannot see your profile. days time unit + + decryption errors + decryption errors + No comment provided by engineer. + default (%@) default (%@) @@ -7033,6 +8121,11 @@ SimpleX servers cannot see your profile. duplicate message integrity error chat item + + duplicates + duplicates + No comment provided by engineer. + e2e encrypted e2e encrypted @@ -7113,6 +8206,11 @@ SimpleX servers cannot see your profile. event happened No comment provided by engineer. + + expired + expired + No comment provided by engineer. + forwarded forwarded @@ -7143,6 +8241,11 @@ SimpleX servers cannot see your profile. iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications. No comment provided by engineer. + + inactive + inactive + No comment provided by engineer. + incognito via contact address link incognito via contact address link @@ -7183,6 +8286,11 @@ SimpleX servers cannot see your profile. invitation to group %@ group name + + invite + invite + No comment provided by engineer. + invited invited @@ -7238,6 +8346,11 @@ SimpleX servers cannot see your profile. connected rcv group event chat item + + message + message + No comment provided by engineer. + message received message received @@ -7268,6 +8381,11 @@ SimpleX servers cannot see your profile. months time unit + + mute + mute + No comment provided by engineer. + never never @@ -7320,6 +8438,16 @@ SimpleX servers cannot see your profile. on group pref value + + other + other + No comment provided by engineer. + + + other errors + other errors + No comment provided by engineer. + owner owner @@ -7390,6 +8518,11 @@ SimpleX servers cannot see your profile. saved from %@ No comment provided by engineer. + + search + search + No comment provided by engineer. + sec sec @@ -7415,6 +8548,15 @@ SimpleX servers cannot see your profile. send direct message No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + server queue info: %1$@ + +last received msg: %2$@ + queue info + set new contact address set new contact address @@ -7455,11 +8597,26 @@ SimpleX servers cannot see your profile. unknown connection info + + unknown servers + unknown servers + No comment provided by engineer. + unknown status unknown status No comment provided by engineer. + + unmute + unmute + No comment provided by engineer. + + + unprotected + unprotected + No comment provided by engineer. + updated group profile updated group profile @@ -7500,6 +8657,11 @@ SimpleX servers cannot see your profile. via relay No comment provided by engineer. + + video + video + No comment provided by engineer. + video call (not e2e encrypted) video call (not e2e encrypted) @@ -7525,6 +8687,11 @@ SimpleX servers cannot see your profile. weeks time unit + + when IP hidden + when IP hidden + No comment provided by engineer. + yes yes @@ -7609,7 +8776,7 @@ SimpleX servers cannot see your profile.
- +
@@ -7646,7 +8813,7 @@ SimpleX servers cannot see your profile.
- +
@@ -7666,4 +8833,218 @@ SimpleX servers cannot see your profile.
+ +
+ +
+ + + SimpleX SE + SimpleX SE + Bundle display name + + + SimpleX SE + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + %@ + No comment provided by engineer. + + + App is locked! + App is locked! + No comment provided by engineer. + + + Cancel + Cancel + No comment provided by engineer. + + + Cannot access keychain to save database password + Cannot access keychain to save database password + No comment provided by engineer. + + + Cannot forward message + Cannot forward message + No comment provided by engineer. + + + Comment + Comment + No comment provided by engineer. + + + Currently maximum supported file size is %@. + Currently maximum supported file size is %@. + No comment provided by engineer. + + + Database downgrade required + Database downgrade required + No comment provided by engineer. + + + Database encrypted! + Database encrypted! + No comment provided by engineer. + + + Database error + Database error + No comment provided by engineer. + + + Database passphrase is different from saved in the keychain. + Database passphrase is different from saved in the keychain. + No comment provided by engineer. + + + Database passphrase is required to open chat. + Database passphrase is required to open chat. + No comment provided by engineer. + + + Database upgrade required + Database upgrade required + No comment provided by engineer. + + + Error preparing file + Error preparing file + No comment provided by engineer. + + + Error preparing message + Error preparing message + No comment provided by engineer. + + + Error: %@ + Error: %@ + No comment provided by engineer. + + + File error + File error + No comment provided by engineer. + + + Incompatible database version + Incompatible database version + No comment provided by engineer. + + + Invalid migration confirmation + Invalid migration confirmation + No comment provided by engineer. + + + Keychain error + Keychain error + No comment provided by engineer. + + + Large file! + Large file! + No comment provided by engineer. + + + No active profile + No active profile + No comment provided by engineer. + + + Ok + Ok + No comment provided by engineer. + + + Open the app to downgrade the database. + Open the app to downgrade the database. + No comment provided by engineer. + + + Open the app to upgrade the database. + Open the app to upgrade the database. + No comment provided by engineer. + + + Passphrase + Passphrase + No comment provided by engineer. + + + Please create a profile in the SimpleX app + Please create a profile in the SimpleX app + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending a message takes longer than expected. + Sending a message takes longer than expected. + No comment provided by engineer. + + + Sending message… + Sending message… + No comment provided by engineer. + + + Share + Share + No comment provided by engineer. + + + Slow network? + Slow network? + No comment provided by engineer. + + + Unknown database error: %@ + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + Unsupported format + No comment provided by engineer. + + + Wait + Wait + No comment provided by engineer. + + + Wrong database passphrase + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/en.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/en.xcloc/contents.json b/apps/ios/SimpleX Localizations/en.xcloc/contents.json index 7d429820ee..2f39a1f1ee 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/en.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "en", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "15F31d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "15.4" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index 5d2482ae27..6e5ec0e85a 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -2,7 +2,7 @@
- +
@@ -127,11 +127,6 @@ %@ está verificado No comment provided by engineer. - - %@ servers - Servidores %@ - No comment provided by engineer. - %@ uploaded %@ subido @@ -557,8 +552,8 @@ Acerca de la dirección SimpleX No comment provided by engineer. - - Accent color + + Accent Color No comment provided by engineer. @@ -566,7 +561,8 @@ Accept Aceptar accept contact request via notification - accept incoming call via notification + accept incoming call via notification + swipe action Accept connection request? @@ -581,7 +577,23 @@ Accept incognito Aceptar incógnito - accept contact request via notification + accept contact request via notification + swipe action + + + Acknowledged + Confirmaciones + No comment provided by engineer. + + + Acknowledgement errors + Errores de confirmación + No comment provided by engineer. + + + Active connections + Conexiones activas + No comment provided by engineer. Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. @@ -603,16 +615,16 @@ Añadir perfil No comment provided by engineer. + + Add server + Añadir servidor + No comment provided by engineer. + Add servers by scanning QR codes. Añadir servidores mediante el escaneo de códigos QR. No comment provided by engineer. - - Add server… - Añadir servidor… - No comment provided by engineer. - Add to another device Añadir a otro dispositivo @@ -623,6 +635,21 @@ Añadir mensaje de bienvenida No comment provided by engineer. + + Additional accent + Acento adicional + No comment provided by engineer. + + + Additional accent 2 + Color adicional 2 + No comment provided by engineer. + + + Additional secondary + Secundario adicional + No comment provided by engineer. + Address Dirección @@ -648,6 +675,11 @@ Configuración avanzada de red No comment provided by engineer. + + Advanced settings + Configuración avanzada + No comment provided by engineer. + All app data is deleted. Todos los datos de la aplicación se eliminarán. @@ -663,6 +695,11 @@ Al introducirlo todos los datos son eliminados. No comment provided by engineer. + + All data is private to your device. + Todos los datos son privados y están en tu dispositivo. + No comment provided by engineer. + All group members will remain connected. Todos los miembros del grupo permanecerán conectados. @@ -683,6 +720,11 @@ ¡Los mensajes nuevos de %@ estarán ocultos! No comment provided by engineer. + + All profiles + Todos los perfiles + No comment provided by engineer. + All your contacts will remain connected. Todos tus contactos permanecerán conectados. @@ -708,11 +750,21 @@ Se permiten las llamadas pero sólo si tu contacto también las permite. No comment provided by engineer. + + Allow calls? + ¿Permitir llamadas? + No comment provided by engineer. + Allow disappearing messages only if your contact allows it to you. Se permiten los mensajes temporales pero sólo si tu contacto también los permite para tí. No comment provided by engineer. + + Allow downgrade + Permitir versión anterior + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también la permite para tí. (24 horas) @@ -738,6 +790,11 @@ Permites el envío de mensajes temporales. No comment provided by engineer. + + Allow sharing + Permitir compartir + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Se permite la eliminación irreversible de mensajes. (24 horas) @@ -745,7 +802,7 @@ Allow to send SimpleX links. - Permitir enviar enlaces SimpleX. + Se permite enviar enlaces SimpleX. No comment provided by engineer. @@ -808,6 +865,11 @@ ¡Ya en proceso de unirte al grupo! No comment provided by engineer. + + Always use private routing. + Usar siempre enrutamiento privado. + No comment provided by engineer. + Always use relay Usar siempre retransmisor @@ -873,11 +935,26 @@ Aplicar No comment provided by engineer. + + Apply to + Aplicar a + No comment provided by engineer. + Archive and upload Archivar y subir No comment provided by engineer. + + Archive contacts to chat later. + Archiva contactos para charlar más tarde. + No comment provided by engineer. + + + Archived contacts + Contactos archivados + No comment provided by engineer. + Archiving database Archivando base de datos @@ -948,6 +1025,11 @@ Volver No comment provided by engineer. + + Background + Fondo + No comment provided by engineer. + Bad desktop address Dirección ordenador incorrecta @@ -973,6 +1055,16 @@ Mensajes mejorados No comment provided by engineer. + + Better networking + Uso de red mejorado + No comment provided by engineer. + + + Black + Negro + No comment provided by engineer. + Block Bloquear @@ -1008,6 +1100,16 @@ Bloqueado por administrador No comment provided by engineer. + + Blur for better privacy. + Difumina para mayor privacidad. + No comment provided by engineer. + + + Blur media + Difuminar multimedia + No comment provided by engineer. + Both you and your contact can add message reactions. Tanto tú como tu contacto podéis añadir reacciones a los mensajes. @@ -1040,7 +1142,7 @@ By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). - Mediante perfil (por defecto) o [por conexión](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). + Mediante perfil (predeterminado) o [por conexión](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). No comment provided by engineer. @@ -1053,11 +1155,26 @@ Llamadas No comment provided by engineer. + + Calls prohibited! + ¡Llamadas no permitidas! + No comment provided by engineer. + Camera not available Cámara no disponible No comment provided by engineer. + + Can't call contact + No se puede llamar al contacto + No comment provided by engineer. + + + Can't call member + No se puede llamar al miembro + No comment provided by engineer. + Can't invite contact! ¡No se puede invitar el contacto! @@ -1068,6 +1185,11 @@ ¡No se pueden invitar contactos! No comment provided by engineer. + + Can't message member + No se pueden enviar mensajes al miembro + No comment provided by engineer. + Cancel Cancelar @@ -1083,11 +1205,21 @@ Keychain inaccesible para guardar la contraseña de la base de datos No comment provided by engineer. + + Cannot forward message + No se puede reenviar el mensaje + No comment provided by engineer. + Cannot receive file No se puede recibir el archivo No comment provided by engineer. + + Capacity exceeded - recipient did not receive previously sent messages. + Capacidad excedida - el destinatario no ha recibido los mensajes previos. + snd error text + Cellular Móvil @@ -1149,6 +1281,11 @@ Archivo del chat No comment provided by engineer. + + Chat colors + Colores del chat + No comment provided by engineer. + Chat console Consola de Chat @@ -1156,7 +1293,7 @@ Chat database - Base de datos del chat + Base de datos de SimpleX No comment provided by engineer. @@ -1164,6 +1301,11 @@ Base de datos eliminada No comment provided by engineer. + + Chat database exported + Base de datos exportada + No comment provided by engineer. + Chat database imported Base de datos importada @@ -1171,17 +1313,22 @@ Chat is running - Chat está en ejecución + SimpleX está en ejecución No comment provided by engineer. Chat is stopped - Chat está parado + SimpleX está parado No comment provided by engineer. Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat. - Chat parado. Si has usado esta base de datos en otro dispositivo, debes transferirla de vuelta antes de iniciar Chat. + SimpleX está parado. Si has usado esta base de datos en otro dispositivo, debes transferirla de vuelta antes de iniciar SimpleX. + No comment provided by engineer. + + + Chat list + Lista de chats No comment provided by engineer. @@ -1194,6 +1341,11 @@ Preferencias de Chat No comment provided by engineer. + + Chat theme + Tema de chat + No comment provided by engineer. + Chats Chats @@ -1224,10 +1376,25 @@ Elige de la biblioteca No comment provided by engineer. + + Chunks deleted + Bloques eliminados + No comment provided by engineer. + + + Chunks downloaded + Bloques descargados + No comment provided by engineer. + + + Chunks uploaded + Bloques subidos + No comment provided by engineer. + Clear Vaciar - No comment provided by engineer. + swipe action Clear conversation @@ -1249,9 +1416,14 @@ Eliminar verificación No comment provided by engineer. - - Colors - Colores + + Color chats with the new themes. + Colorea los chats con los nuevos temas. + No comment provided by engineer. + + + Color mode + Modo de color No comment provided by engineer. @@ -1264,11 +1436,21 @@ Compara los códigos de seguridad con tus contactos. No comment provided by engineer. + + Completed + Completadas + No comment provided by engineer. + Configure ICE servers Configure servidores ICE No comment provided by engineer. + + Configured %@ servers + %@ servidores configurados + No comment provided by engineer. + Confirm Confirmar @@ -1279,11 +1461,21 @@ Confirma Código No comment provided by engineer. + + Confirm contact deletion? + ¿Confirmas la eliminación del contacto? + No comment provided by engineer. + Confirm database upgrades Confirmar actualizaciones de la bases de datos No comment provided by engineer. + + Confirm files from unknown servers. + Confirma archivos de servidores desconocidos. + No comment provided by engineer. + Confirm network settings Confirmar configuración de red @@ -1301,7 +1493,7 @@ Confirm that you remember database passphrase to migrate it. - Confirma que recuerdas la frase de contraseña de la base de datos para migrarla. + Para migrar confirma que recuerdas la frase de contraseña de la base de datos. No comment provided by engineer. @@ -1329,6 +1521,11 @@ Conectar con ordenador No comment provided by engineer. + + Connect to your friends faster. + Conecta más rápido con tus amigos. + No comment provided by engineer. + Connect to yourself? ¿Conectarte a tí mismo? @@ -1368,16 +1565,31 @@ This is your own one-time link! Conectar con %@ No comment provided by engineer. + + Connected + Conectadas + No comment provided by engineer. + Connected desktop Ordenador conectado No comment provided by engineer. + + Connected servers + Servidores conectados + No comment provided by engineer. + Connected to desktop Conectado con ordenador No comment provided by engineer. + + Connecting + Conectando + No comment provided by engineer. + Connecting to server… Conectando con el servidor… @@ -1388,6 +1600,11 @@ This is your own one-time link! Conectando con el servidor... (error: %@) No comment provided by engineer. + + Connecting to contact, please wait or check later! + Conectando con el contacto, por favor espera o revisa más tarde. + No comment provided by engineer. + Connecting to desktop Conectando con ordenador @@ -1398,6 +1615,11 @@ This is your own one-time link! Conexión No comment provided by engineer. + + Connection and servers status. + Estado de tu conexión y servidores. + No comment provided by engineer. + Connection error Error conexión @@ -1405,7 +1627,12 @@ This is your own one-time link! Connection error (AUTH) - Error conexión (Autenticación) + Error de conexión (Autenticación) + No comment provided by engineer. + + + Connection notifications + Notificaciones de conexión No comment provided by engineer. @@ -1420,7 +1647,17 @@ This is your own one-time link! Connection timeout - Tiempo de conexión expirado + Tiempo de conexión agotado + No comment provided by engineer. + + + Connection with desktop stopped + La conexión con el escritorio (desktop) se ha parado + No comment provided by engineer. + + + Connections + Conexiones No comment provided by engineer. @@ -1433,6 +1670,11 @@ This is your own one-time link! El contácto ya existe No comment provided by engineer. + + Contact deleted! + ¡Contacto eliminado! + No comment provided by engineer. + Contact hidden: Contacto oculto: @@ -1443,9 +1685,9 @@ This is your own one-time link! El contacto está en línea notification - - Contact is not connected yet! - ¡El contacto aun no se ha conectado! + + Contact is deleted. + El contacto está eliminado. No comment provided by engineer. @@ -1458,6 +1700,11 @@ This is your own one-time link! Preferencias de contacto No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + El contacto será eliminado. ¡No podrá deshacerse! + No comment provided by engineer. + Contacts Contactos @@ -1473,10 +1720,20 @@ This is your own one-time link! Continuar No comment provided by engineer. + + Conversation deleted! + ¡Conversación eliminada! + No comment provided by engineer. + Copy Copiar - chat item action + No comment provided by engineer. + + + Copy error + Copiar error + No comment provided by engineer. Core version: v%@ @@ -1553,6 +1810,11 @@ This is your own one-time link! Crea tu perfil No comment provided by engineer. + + Created + Creadas + No comment provided by engineer. + Created at Creado @@ -1588,6 +1850,11 @@ This is your own one-time link! Contraseña actual… No comment provided by engineer. + + Current profile + Perfil actual + No comment provided by engineer. + Currently maximum supported file size is %@. El tamaño máximo de archivo admitido es %@. @@ -1598,11 +1865,21 @@ This is your own one-time link! Tiempo personalizado No comment provided by engineer. + + Customize theme + Personalizar tema + No comment provided by engineer. + Dark Oscuro No comment provided by engineer. + + Dark mode colors + Colores en modo oscuro + No comment provided by engineer. + Database ID ID base de datos @@ -1701,6 +1978,11 @@ This is your own one-time link! La base de datos migrará cuando se reinicie la aplicación No comment provided by engineer. + + Debug delivery + Informe debug + No comment provided by engineer. + Decentralized Descentralizada @@ -1714,16 +1996,17 @@ This is your own one-time link! Delete Eliminar - chat item action + chat item action + swipe action + + + Delete %lld messages of members? + ¿Eliminar %lld mensajes de miembros? + No comment provided by engineer. Delete %lld messages? - ¿Elimina %lld mensajes? - No comment provided by engineer. - - - Delete Contact - Eliminar contacto + ¿Eliminar %lld mensajes? No comment provided by engineer. @@ -1781,11 +2064,9 @@ This is your own one-time link! Eliminar contacto No comment provided by engineer. - - Delete contact? -This cannot be undone! - ¿Eliminar contacto? -¡No podrá deshacerse! + + Delete contact? + ¿Eliminar contacto? No comment provided by engineer. @@ -1878,11 +2159,6 @@ This cannot be undone! ¿Eliminar base de datos antigua? No comment provided by engineer. - - Delete pending connection - Eliminar conexión pendiente - No comment provided by engineer. - Delete pending connection? ¿Eliminar conexión pendiente? @@ -1898,11 +2174,26 @@ This cannot be undone! Eliminar cola server test step + + Delete up to 20 messages at once. + Elimina hasta 20 mensajes a la vez. + No comment provided by engineer. + Delete user profile? ¿Eliminar perfil de usuario? No comment provided by engineer. + + Delete without notification + Elimina sin notificar + No comment provided by engineer. + + + Deleted + Eliminadas + No comment provided by engineer. + Deleted at Eliminado @@ -1913,6 +2204,11 @@ This cannot be undone! Eliminado: %@ copied message info + + Deletion errors + Errores de eliminación + No comment provided by engineer. + Delivery Entrega @@ -1948,11 +2244,41 @@ This cannot be undone! Ordenadores No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + La dirección del servidor de destino de %@ es incompatible con la configuración del servidor de reenvío %@. + No comment provided by engineer. + + + Destination server error: %@ + Error del servidor de destino: %@ + snd error text + + + Destination server version of %@ is incompatible with forwarding server %@. + La versión del servidor de destino de %@ es incompatible con el servidor de reenvío %@. + No comment provided by engineer. + + + Detailed statistics + Estadísticas detalladas + No comment provided by engineer. + + + Details + Detalles + No comment provided by engineer. + Develop Desarrollo No comment provided by engineer. + + Developer options + Opciones desarrollador + No comment provided by engineer. + Developer tools Herramientas desarrollo @@ -2003,6 +2329,11 @@ This cannot be undone! Desactivar para todos No comment provided by engineer. + + Disabled + Desactivado + No comment provided by engineer. + Disappearing message Mensaje temporal @@ -2053,11 +2384,21 @@ This cannot be undone! Descubrir en red local No comment provided by engineer. + + Do NOT send messages directly, even if your or destination server does not support private routing. + NO enviar mensajes directamente incluso si tu servidor o el de destino no soportan enrutamiento privado. + No comment provided by engineer. + Do NOT use SimpleX for emergency calls. NO uses SimpleX para llamadas de emergencia. No comment provided by engineer. + + Do NOT use private routing. + NO usar enrutamiento privado. + No comment provided by engineer. + Do it later Hacer más tarde @@ -2065,7 +2406,7 @@ This cannot be undone! Do not send history to new members. - No enviar historial a miembros nuevos. + No se envía el historial a los miembros nuevos. No comment provided by engineer. @@ -2093,6 +2434,11 @@ This cannot be undone! Descargar chat item action + + Download errors + Errores en la descarga + No comment provided by engineer. + Download failed Descarga fallida @@ -2103,6 +2449,16 @@ This cannot be undone! Descargar archivo server test step + + Downloaded + Descargado + No comment provided by engineer. + + + Downloaded files + Archivos descargados + No comment provided by engineer. + Downloading archive Descargando archivo @@ -2203,9 +2559,14 @@ This cannot be undone! Activar código de autodestrucción set passcode view + + Enabled + Activado + No comment provided by engineer. + Enabled for - Activar para + Activado para No comment provided by engineer. @@ -2373,6 +2734,11 @@ This cannot be undone! Error cambiando configuración No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + Error al conectar con el servidor de reenvío %@. Por favor, inténtalo más tarde. + No comment provided by engineer. + Error creating address Error al crear dirección @@ -2423,11 +2789,6 @@ This cannot be undone! Error al eliminar conexión No comment provided by engineer. - - Error deleting contact - Error al eliminar contacto - No comment provided by engineer. - Error deleting database Error al eliminar base de datos @@ -2473,6 +2834,11 @@ This cannot be undone! Error al exportar base de datos No comment provided by engineer. + + Error exporting theme: %@ + Error al exportar tema: %@ + No comment provided by engineer. + Error importing chat database Error al importar base de datos @@ -2498,11 +2864,26 @@ This cannot be undone! Error al recibir archivo No comment provided by engineer. + + Error reconnecting server + Error al reconectar con el servidor + No comment provided by engineer. + + + Error reconnecting servers + Error al reconectar con los servidores + No comment provided by engineer. + Error removing member Error al eliminar miembro No comment provided by engineer. + + Error resetting statistics + Error al restablecer las estadísticas + No comment provided by engineer. + Error saving %@ servers Error al guardar servidores %@ @@ -2570,7 +2951,7 @@ This cannot be undone! Error stopping chat - Error al parar Chat + Error al parar SimpleX No comment provided by engineer. @@ -2621,7 +3002,8 @@ This cannot be undone! Error: %@ Error: %@ - No comment provided by engineer. + file error text + snd error text Error: URL is invalid @@ -2633,6 +3015,11 @@ This cannot be undone! Error: sin archivo de base de datos No comment provided by engineer. + + Errors + Errores + No comment provided by engineer. + Even when disabled in the conversation. Incluso si está desactivado para la conversación. @@ -2658,6 +3045,11 @@ This cannot be undone! Error al exportar: No comment provided by engineer. + + Export theme + Exportar tema + No comment provided by engineer. + Exported database archive. Archivo de base de datos exportado. @@ -2691,8 +3083,33 @@ This cannot be undone! Favorite Favoritos + swipe action + + + File error + Error de archivo No comment provided by engineer. + + File not found - most likely file was deleted or cancelled. + Archivo no encontrado, probablemente haya sido borrado o cancelado. + file error text + + + File server error: %@ + Error del servidor de archivos: %@ + file error text + + + File status + Estado del archivo + No comment provided by engineer. + + + File status: %@ + Estado del archivo: %@ + copied message info + File will be deleted from servers. El archivo será eliminado de los servidores. @@ -2705,7 +3122,7 @@ This cannot be undone! File will be received when your contact is online, please wait or check later! - El archivo se recibirá cuando el contacto esté en línea, ¡por favor espera o compruébalo más tarde! + El archivo se recibirá cuando el contacto esté en línea, ¡por favor espera o revisa más tarde! No comment provided by engineer. @@ -2713,6 +3130,11 @@ This cannot be undone! Archivo: %@ No comment provided by engineer. + + Files + Archivos + No comment provided by engineer. + Files & media Archivos y multimedia @@ -2818,6 +3240,35 @@ This cannot be undone! Reenviado por No comment provided by engineer. + + Forwarding server %@ failed to connect to destination server %@. Please try later. + El servidor de reenvío %@ no ha podido conectarse al servidor de destino %@. Por favor, intentalo más tarde. + No comment provided by engineer. + + + Forwarding server address is incompatible with network settings: %@. + La dirección del servidor de reenvío es incompatible con la configuración de red: %@. + No comment provided by engineer. + + + Forwarding server version is incompatible with network settings: %@. + La versión del servidor de reenvío es incompatible con la configuración de red: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + Servidor de reenvío: %1$@ +Error del servidor de destino: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + Servidor de reenvío: %1$@ +Error: %2$@ + snd error text + Found desktop Ordenador encontrado @@ -2845,7 +3296,7 @@ This cannot be undone! Fully decentralized – visible only to members. - Completamente descentralizado: sólo visible a los miembros. + Completamente descentralizado y sólo visible para los miembros. No comment provided by engineer. @@ -2863,6 +3314,16 @@ This cannot be undone! GIFs y stickers No comment provided by engineer. + + Good afternoon! + ¡Buenas tardes! + message preview + + + Good morning! + ¡Buenos días! + message preview + Group Grupo @@ -3080,7 +3541,7 @@ This cannot be undone! If you can't meet in person, show QR code in a video call, or share the link. - Si no puedes reunirte en persona, muestra el código QR por videollamada, o comparte el enlace. + Si no puedes reunirte en persona, muestra el código QR por videollamada o comparte el enlace. No comment provided by engineer. @@ -3110,7 +3571,7 @@ This cannot be undone! Image will be received when your contact is online, please wait or check later! - La imagen se recibirá cuando el contacto esté en línea, ¡por favor espera o compruébalo más tarde! + La imagen se recibirá cuando el contacto esté en línea, ¡por favor espera o revisa más tarde! No comment provided by engineer. @@ -3143,6 +3604,11 @@ This cannot be undone! Error de importación No comment provided by engineer. + + Import theme + Importar tema + No comment provided by engineer. + Importing archive Importando archivo @@ -3165,7 +3631,7 @@ This cannot be undone! In order to continue, chat should be stopped. - Para continuar, Chat debe estar parado. + Para continuar, SimpleX debe estar parado. No comment provided by engineer. @@ -3265,6 +3731,11 @@ This cannot be undone! Interfaz No comment provided by engineer. + + Interface colors + Colores del interfaz + No comment provided by engineer. + Invalid QR code Código QR no válido @@ -3366,6 +3837,11 @@ This cannot be undone! 3. La conexión ha sido comprometida. No comment provided by engineer. + + It protects your IP address and connections. + Protege tu dirección IP y tus conexiones. + No comment provided by engineer. + It seems like you are already connected via this link. If it is not the case, there was an error (%@). Parece que ya estás conectado mediante este enlace. Si no es así ha habido un error (%@). @@ -3384,7 +3860,7 @@ This cannot be undone! Join Unirte - No comment provided by engineer. + swipe action Join group @@ -3428,6 +3904,11 @@ This is your link for group %@! Guardar No comment provided by engineer. + + Keep conversation + Conservar conversación + No comment provided by engineer. + Keep the app open to use it from desktop Mantén la aplicación abierta para usarla desde el ordenador @@ -3471,7 +3952,7 @@ This is your link for group %@! Leave Salir - No comment provided by engineer. + swipe action Leave group @@ -3603,11 +4084,26 @@ This is your link for group %@! Máximo 30 segundos, recibido al instante. No comment provided by engineer. + + Media & file servers + Servidores de archivos y multimedia + No comment provided by engineer. + + + Medium + Medio + blur media + Member Miembro No comment provided by engineer. + + Member inactive + Miembro inactivo + item status text + Member role will be changed to "%@". All group members will be notified. El rol del miembro cambiará a "%@" y se notificará al grupo. @@ -3623,6 +4119,11 @@ This is your link for group %@! El miembro será expulsado del grupo. ¡No podrá deshacerse! No comment provided by engineer. + + Menus + Menus + No comment provided by engineer. + Message delivery error Error en la entrega del mensaje @@ -3633,11 +4134,31 @@ This is your link for group %@! ¡Confirmación de entrega de mensajes! No comment provided by engineer. + + Message delivery warning + Aviso de entrega de mensaje + item status text + Message draft Borrador de mensaje No comment provided by engineer. + + Message forwarded + Mensaje reenviado + item status text + + + Message may be delivered later if member becomes active. + El mensaje podría ser entregado más tarde si el miembro vuelve a estar activo. + item status description + + + Message queue info + Información cola de mensajes + No comment provided by engineer. + Message reactions Reacciones a mensajes @@ -3653,11 +4174,31 @@ This is your link for group %@! Las reacciones a los mensajes no están permitidas en este grupo. No comment provided by engineer. + + Message reception + Recepción de mensaje + No comment provided by engineer. + + + Message servers + Servidores de mensajes + No comment provided by engineer. + Message source remains private. El autor del mensaje se mantiene privado. No comment provided by engineer. + + Message status + Estado del mensaje + No comment provided by engineer. + + + Message status: %@ + Estado del mensaje: %@ + copied message info + Message text Contacto y texto @@ -3683,6 +4224,16 @@ This is your link for group %@! ¡Los mensajes de %@ serán mostrados! No comment provided by engineer. + + Messages received + Mensajes recibidos + No comment provided by engineer. + + + Messages sent + Mensajes enviados + No comment provided by engineer. + Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. Los mensajes, archivos y llamadas están protegidos mediante **cifrado de extremo a extremo** con secreto perfecto hacía adelante, repudio y recuperación tras ataque. @@ -3783,11 +4334,6 @@ This is your link for group %@! Probablemente la conexión ha sido eliminada. item status description - - Most likely this contact has deleted the connection with you. - Lo más probable es que este contacto haya eliminado la conexión contigo. - No comment provided by engineer. - Multiple chat profiles Múltiples perfiles @@ -3796,7 +4342,7 @@ This is your link for group %@! Mute Silenciar - No comment provided by engineer. + swipe action Muted when inactive! @@ -3806,7 +4352,7 @@ This is your link for group %@! Name Nombre - No comment provided by engineer. + swipe action Network & servers @@ -3818,6 +4364,11 @@ This is your link for group %@! Conexión de red No comment provided by engineer. + + Network issues - message expired after many attempts to send it. + Problema en la red - el mensaje ha expirado tras muchos intentos de envío. + snd error text + Network management Gestión de la red @@ -3843,6 +4394,11 @@ This is your link for group %@! Nuevo chat No comment provided by engineer. + + New chat experience 🎉 + Nueva experiencia de chat 🎉 + No comment provided by engineer. + New contact request Nueva solicitud de contacto @@ -3873,6 +4429,11 @@ This is your link for group %@! Nuevo en %@ No comment provided by engineer. + + New media options + Nuevas opciones multimedia + No comment provided by engineer. + New member role Nuevo rol de miembro @@ -3918,6 +4479,11 @@ This is your link for group %@! ¡Sin dispositivo token! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + Aún no hay conexión directa con este miembro, el mensaje es reenviado por el administrador. + item status description + No filtered chats Sin chats filtrados @@ -3933,6 +4499,11 @@ This is your link for group %@! Sin historial No comment provided by engineer. + + No info, try to reload + No hay información, intenta recargar + No comment provided by engineer. + No network connection Sin conexión de red @@ -3953,6 +4524,11 @@ This is your link for group %@! ¡No compatible! No comment provided by engineer. + + Nothing selected + Nada seleccionado + No comment provided by engineer. + Notifications Notificaciones @@ -3980,7 +4556,7 @@ This is your link for group %@! Off Desactivado - No comment provided by engineer. + blur media Ok @@ -3999,17 +4575,21 @@ This is your link for group %@! One-time invitation link - Enlace único de invitación de un uso + Enlace de invitación de un solo uso No comment provided by engineer. - - Onion hosts will be required for connection. Requires enabling VPN. - Se requieren hosts .onion para la conexión. Requiere activación de la VPN. + + Onion hosts will be **required** for connection. +Requires compatible VPN. + Se **requieren** hosts .onion para la conexión. +Requiere activación de la VPN. No comment provided by engineer. - - Onion hosts will be used when available. Requires enabling VPN. - Se usarán hosts .onion si están disponibles. Requiere activación de la VPN. + + Onion hosts will be used when available. +Requires compatible VPN. + Se usarán hosts .onion si están disponibles. +Requiere activación de la VPN. No comment provided by engineer. @@ -4022,6 +4602,11 @@ This is your link for group %@! Sólo los dispositivos cliente almacenan perfiles de usuario, contactos, grupos y mensajes enviados con **cifrado de extremo a extremo de 2 capas**. No comment provided by engineer. + + Only delete conversation + Sólo borrar la conversación + No comment provided by engineer. + Only group owners can change group preferences. Sólo los propietarios pueden modificar las preferencias del grupo. @@ -4117,6 +4702,11 @@ This is your link for group %@! Abrir menú migración a otro dispositivo authentication reason + + Open server settings + Abrir configuración del servidor + No comment provided by engineer. + Open user profiles Abrir perfil de usuario @@ -4139,7 +4729,7 @@ This is your link for group %@! Or scan QR code - O escanear código QR + O escanea el código QR No comment provided by engineer. @@ -4149,7 +4739,7 @@ This is your link for group %@! Or show this code - O mostrar este código + O muestra este código QR No comment provided by engineer. @@ -4157,6 +4747,11 @@ This is your link for group %@! Otro No comment provided by engineer. + + Other %@ servers + Otros servidores %@ + No comment provided by engineer. + PING count Contador PING @@ -4219,7 +4814,12 @@ This is your link for group %@! Paste the link you received - Pegar el enlace recibido + Pega el enlace recibido + No comment provided by engineer. + + + Pending + Pendientes No comment provided by engineer. @@ -4229,7 +4829,7 @@ This is your link for group %@! Periodically - Periódico + Periódicamente No comment provided by engineer. @@ -4242,11 +4842,28 @@ This is your link for group %@! Llamadas picture-in-picture No comment provided by engineer. + + Play from the chat list. + Reproduce desde la lista de chats. + No comment provided by engineer. + + + Please ask your contact to enable calls. + Por favor, pide a tu contacto que active las llamadas. + No comment provided by engineer. + Please ask your contact to enable sending voice messages. Solicita que tu contacto habilite el envío de mensajes de voz. No comment provided by engineer. + + Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers. + Comprueba que el móvil y el ordenador están conectados a la misma red local y que el cortafuegos del ordenador permite la conexión. +Por favor, comparte cualquier otro problema con los desarrolladores. + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. Comprueba que has usado el enlace correcto o pide a tu contacto que te envíe otro. @@ -4344,9 +4961,14 @@ Error: %@ Vista previa No comment provided by engineer. + + Previously connected servers + Servidores conectados previamente + No comment provided by engineer. + Privacy & security - Privacidad y Seguridad + Seguridad y Privacidad No comment provided by engineer. @@ -4359,11 +4981,31 @@ Error: %@ Nombres de archivos privados No comment provided by engineer. + + Private message routing + Enrutamiento privado de mensajes + No comment provided by engineer. + + + Private message routing 🚀 + Enrutamiento privado de mensajes 🚀 + No comment provided by engineer. + Private notes Notas privadas name of notes to self + + Private routing + Enrutamiento privado + No comment provided by engineer. + + + Private routing error + Error de enrutamiento privado + No comment provided by engineer. + Profile and server connections Datos del perfil y conexiones @@ -4376,7 +5018,7 @@ Error: %@ Profile images - Imágenes del perfil + Forma de los perfiles No comment provided by engineer. @@ -4394,6 +5036,11 @@ Error: %@ Contraseña del perfil No comment provided by engineer. + + Profile theme + Tema del perfil + No comment provided by engineer. + Profile update will be sent to your contacts. La actualización del perfil se enviará a tus contactos. @@ -4421,7 +5068,7 @@ Error: %@ Prohibit sending SimpleX links. - No permitir el envío de enlaces SimpleX. + No se permite enviar enlaces SimpleX. No comment provided by engineer. @@ -4444,11 +5091,23 @@ Error: %@ No se permiten mensajes de voz. No comment provided by engineer. + + Protect IP address + Proteger dirección IP + No comment provided by engineer. + Protect app screen Proteger la pantalla de la aplicación No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + Protege tu dirección IP de los servidores de retransmisión elegidos por tus contactos. +Actívalo en ajustes de *Servidores y Redes*. + No comment provided by engineer. + Protect your chat profiles with a password! ¡Protege tus perfiles con contraseña! @@ -4456,12 +5115,22 @@ Error: %@ Protocol timeout - Tiempo de espera del protocolo + Timeout protocolo No comment provided by engineer. Protocol timeout per KB - Límite de espera del protocolo por KB + Timeout protocolo por KB + No comment provided by engineer. + + + Proxied + Como proxy + No comment provided by engineer. + + + Proxied servers + Servidores con proxy No comment provided by engineer. @@ -4484,6 +5153,11 @@ Error: %@ Valora la aplicación No comment provided by engineer. + + Reachable chat toolbar + Barra de herramientas accesible + No comment provided by engineer. + React… Reacciona… @@ -4492,36 +5166,36 @@ Error: %@ Read Leer - No comment provided by engineer. + swipe action Read more - Saber más + Conoce más No comment provided by engineer. Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - Saber más en el [Manual del Usuario](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). + Conoce más en el [Manual del Usuario](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). No comment provided by engineer. Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). - Saber más en [Guía de Usuario](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). + Conoce más en la [Guía del Usuario](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode). No comment provided by engineer. Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). - Saber más en el [Manual del Usuario](https://simplex.chat/docs/guide/readme.html#connect-to-friends). + Conoce más en el [Manual del Usuario](https://simplex.chat/docs/guide/readme.html#connect-to-friends). No comment provided by engineer. Read more in our GitHub repository. - Saber más en nuestro repositorio GitHub. + Conoce más en nuestro repositorio GitHub. No comment provided by engineer. Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). - Saber más en nuestro [repositorio GitHub](https://github.com/simplex-chat/simplex-chat#readme). + Conoce más en nuestro [repositorio GitHub](https://github.com/simplex-chat/simplex-chat#readme). No comment provided by engineer. @@ -4529,6 +5203,11 @@ Error: %@ Las confirmaciones están desactivadas No comment provided by engineer. + + Receive errors + Errores de recepción + No comment provided by engineer. + Received at Recibido a las @@ -4549,16 +5228,26 @@ Error: %@ Mensaje entrante message info title + + Received messages + Mensajes recibidos + No comment provided by engineer. + + + Received reply + Respuesta recibida + No comment provided by engineer. + + + Received total + Total recibidos + No comment provided by engineer. + Receiving address will be changed to a different server. Address change will complete after sender comes online. La dirección de recepción pasará a otro servidor. El cambio se completará cuando el remitente esté en línea. No comment provided by engineer. - - Receiving concurrency - Concurrencia en la recepción - No comment provided by engineer. - Receiving file will be stopped. Se detendrá la recepción del archivo. @@ -4584,11 +5273,36 @@ Error: %@ Los destinatarios ven actualizarse mientras escribes. No comment provided by engineer. + + Reconnect + Reconectar + No comment provided by engineer. + Reconnect all connected servers to force message delivery. It uses additional traffic. Reconectar todos los servidores conectados para forzar la entrega del mensaje. Se usa tráfico adicional. No comment provided by engineer. + + Reconnect all servers + Reconectar todos los servidores + No comment provided by engineer. + + + Reconnect all servers? + ¿Reconectar todos los servidores? + No comment provided by engineer. + + + Reconnect server to force message delivery. It uses additional traffic. + Reconectar el servidor para forzar la entrega de mensajes. Usa tráfico adicional. + No comment provided by engineer. + + + Reconnect server? + ¿Reconectar servidor? + No comment provided by engineer. + Reconnect servers? ¿Reconectar servidores? @@ -4612,7 +5326,8 @@ Error: %@ Reject Rechazar - reject incoming call via notification + reject incoming call via notification + swipe action Reject (sender NOT notified) @@ -4626,7 +5341,7 @@ Error: %@ Relay server is only used if necessary. Another party can observe your IP address. - El retransmisor sólo se usa en caso de necesidad. Un tercero podría ver tu IP. + El servidor de retransmisión sólo se usa en caso de necesidad. Un tercero podría ver tu IP. No comment provided by engineer. @@ -4639,6 +5354,11 @@ Error: %@ Eliminar No comment provided by engineer. + + Remove image + Eliminar imagen + No comment provided by engineer. + Remove member Expulsar miembro @@ -4709,14 +5429,39 @@ Error: %@ Restablecer No comment provided by engineer. + + Reset all hints + Reiniciar todas las pistas + No comment provided by engineer. + + + Reset all statistics + Restablecer todas las estadísticas + No comment provided by engineer. + + + Reset all statistics? + ¿Restablecer todas las estadísticas? + No comment provided by engineer. + Reset colors Restablecer colores No comment provided by engineer. + + Reset to app theme + Restablecer al tema de la aplicación + No comment provided by engineer. + Reset to defaults - Restablecer valores por defecto + Restablecer valores predetarminados + No comment provided by engineer. + + + Reset to user theme + Restablecer al tema del usuario No comment provided by engineer. @@ -4759,11 +5504,6 @@ Error: %@ Revelar chat item action - - Revert - Revertir - No comment provided by engineer. - Revoke Revocar @@ -4786,12 +5526,17 @@ Error: %@ Run chat - Ejecutar chat + Ejecutar SimpleX No comment provided by engineer. - - SMP servers - Servidores SMP + + SMP server + Servidor SMP + No comment provided by engineer. + + + Safely receive files + Recibe archivos de forma segura No comment provided by engineer. @@ -4819,6 +5564,11 @@ Error: %@ Guardar y notificar grupo No comment provided by engineer. + + Save and reconnect + Guardar y reconectar + No comment provided by engineer. + Save and update group profile Guardar y actualizar perfil del grupo @@ -4899,6 +5649,16 @@ Error: %@ Mensaje guardado message info title + + Scale + Escala + No comment provided by engineer. + + + Scan / Paste link + Escanear / Pegar enlace + No comment provided by engineer. + Scan QR code Escanear código QR @@ -4939,11 +5699,21 @@ Error: %@ Buscar o pegar enlace SimpleX No comment provided by engineer. + + Secondary + Secundario + No comment provided by engineer. + Secure queue Cola segura server test step + + Secured + Aseguradas + No comment provided by engineer. + Security assessment Evaluación de la seguridad @@ -4957,6 +5727,16 @@ Error: %@ Select Seleccionar + chat item action + + + Selected %lld + Seleccionados %lld + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + Las preferencias seleccionadas no permiten este mensaje. No comment provided by engineer. @@ -4994,14 +5774,9 @@ Error: %@ Enviar confirmaciones de entrega a No comment provided by engineer. - - Send direct message - Enviar mensaje directo - No comment provided by engineer. - Send direct message to connect - Enviar mensaje directo para conectar + Envia un mensaje para conectar No comment provided by engineer. @@ -5009,6 +5784,11 @@ Error: %@ Enviar mensaje temporal No comment provided by engineer. + + Send errors + Errores de envío + No comment provided by engineer. + Send link previews Enviar previsualizacion de enlaces @@ -5019,6 +5799,21 @@ Error: %@ Mensaje en vivo No comment provided by engineer. + + Send message to enable calls. + Enviar mensaje para activar llamadas. + No comment provided by engineer. + + + Send messages directly when IP address is protected and your or destination server does not support private routing. + Enviar mensajes directamente cuando tu dirección IP está protegida y tu servidor o el de destino no admitan enrutamiento privado. + No comment provided by engineer. + + + Send messages directly when your or destination server does not support private routing. + Enviar mensajes directamente cuando tu servidor o el de destino no admitan enrutamiento privado. + No comment provided by engineer. + Send notifications Enviar notificaciones @@ -5046,7 +5841,7 @@ Error: %@ Send up to 100 last messages to new members. - Enviar hasta 100 últimos mensajes a los miembros nuevos. + Se envían hasta 100 mensajes más recientes a los miembros nuevos. No comment provided by engineer. @@ -5109,6 +5904,11 @@ Error: %@ Enviado: %@ copied message info + + Sent directly + Directamente + No comment provided by engineer. + Sent file event Evento de archivo enviado @@ -5119,11 +5919,46 @@ Error: %@ Mensaje saliente message info title + + Sent messages + Mensajes enviados + No comment provided by engineer. + Sent messages will be deleted after set time. Los mensajes enviados se eliminarán una vez transcurrido el tiempo establecido. No comment provided by engineer. + + Sent reply + Respuesta enviada + No comment provided by engineer. + + + Sent total + Total enviados + No comment provided by engineer. + + + Sent via proxy + Mediante proxy + No comment provided by engineer. + + + Server address + Dirección del servidor + No comment provided by engineer. + + + Server address is incompatible with network settings. + La dirección del servidor es incompatible con la configuración de la red. + srv error text. + + + Server address is incompatible with network settings: %@. + La dirección del servidor es incompatible con la configuración de la red: %@. + No comment provided by engineer. + Server requires authorization to create queues, check password El servidor requiere autorización para crear colas, comprueba la contraseña @@ -5139,11 +5974,36 @@ Error: %@ ¡Error en prueba del servidor! No comment provided by engineer. + + Server type + Tipo de servidor + No comment provided by engineer. + + + Server version is incompatible with network settings. + La versión del servidor es incompatible con la configuración de red. + srv error text + + + Server version is incompatible with your app: %@. + La versión del servidor es incompatible con tu aplicación: %@. + No comment provided by engineer. + Servers Servidores No comment provided by engineer. + + Servers info + Info servidores + No comment provided by engineer. + + + Servers statistics will be reset - this cannot be undone! + Las estadísticas de los servidores serán restablecidas. ¡No podrá deshacerse! + No comment provided by engineer. + Session code Código de sesión @@ -5159,6 +6019,11 @@ Error: %@ Escribe el nombre del contacto… No comment provided by engineer. + + Set default theme + Establecer tema predeterminado + No comment provided by engineer. + Set group preferences Establecer preferencias de grupo @@ -5224,6 +6089,11 @@ Error: %@ ¿Compartir la dirección con los contactos? No comment provided by engineer. + + Share from other apps. + Comparte desde otras aplicaciones. + No comment provided by engineer. + Share link Compartir enlace @@ -5231,7 +6101,12 @@ Error: %@ Share this 1-time invite link - Compartir este enlace de un uso + Comparte este enlace de un solo uso + No comment provided by engineer. + + + Share to SimpleX + Compartir con Simplex No comment provided by engineer. @@ -5259,16 +6134,36 @@ Error: %@ Mostrar último mensaje No comment provided by engineer. + + Show message status + Estado del mensaje + No comment provided by engineer. + + + Show percentage + Mostrar porcentajes + No comment provided by engineer. + Show preview Mostrar vista previa No comment provided by engineer. + + Show → on messages sent via private routing. + Mostrar → en mensajes con enrutamiento privado. + No comment provided by engineer. + Show: Mostrar: No comment provided by engineer. + + SimpleX + SimpleX + No comment provided by engineer. + SimpleX Address Dirección SimpleX @@ -5344,6 +6239,11 @@ Error: %@ Modo incógnito simplificado No comment provided by engineer. + + Size + Tamaño + No comment provided by engineer. + Skip Omitir @@ -5359,11 +6259,26 @@ Error: %@ Grupos pequeños (máx. 20) No comment provided by engineer. + + Soft + Suave + blur media + + + Some file(s) were not exported: + Algunos archivos no han sido exportados: + No comment provided by engineer. + Some non-fatal errors occurred during import - you may see Chat console for more details. Algunos errores no críticos ocurrieron durante la importación - para más detalles puedes ver la consola de Chat. No comment provided by engineer. + + Some non-fatal errors occurred during import: + Han ocurrido algunos errores no críticos durante la importación: + No comment provided by engineer. + Somebody Alguien @@ -5389,6 +6304,16 @@ Error: %@ Iniciar migración No comment provided by engineer. + + Starting from %@. + Iniciado el %@. + No comment provided by engineer. + + + Statistics + Estadísticas + No comment provided by engineer. + Stop Parar @@ -5401,17 +6326,17 @@ Error: %@ Stop chat - Parar chat + Parar SimpleX No comment provided by engineer. Stop chat to enable database actions - Para habilitar las acciones sobre la base de datos, debes parar Chat + Para habilitar las acciones sobre la base de datos, debes parar SimpleX No comment provided by engineer. Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. - Para poder exportar, importar o eliminar la base de datos primero debes parar Chat. Mientras tanto no podrás recibir ni enviar mensajes. + Para poder exportar, importar o eliminar la base de datos primero debes parar SimpleX. Mientras tanto no podrás recibir ni enviar mensajes. No comment provided by engineer. @@ -5449,11 +6374,31 @@ Error: %@ Parando chat No comment provided by engineer. + + Strong + Fuerte + blur media + Submit Enviar No comment provided by engineer. + + Subscribed + Suscrito + No comment provided by engineer. + + + Subscription errors + Errores de suscripción + No comment provided by engineer. + + + Subscriptions ignored + Suscripciones ignoradas + No comment provided by engineer. + Support SimpleX Chat Soporte SimpleX Chat @@ -5469,9 +6414,14 @@ Error: %@ Autenticación del sistema No comment provided by engineer. + + TCP connection + Conexión TCP + No comment provided by engineer. + TCP connection timeout - Tiempo de espera de la conexión TCP agotado + Timeout de la conexión TCP No comment provided by engineer. @@ -5521,7 +6471,7 @@ Error: %@ Tap to paste link - Pulsa para pegar enlace + Pulsa para pegar el enlacePulsa para pegar enlace No comment provided by engineer. @@ -5529,9 +6479,9 @@ Error: %@ Pulsa para escanear No comment provided by engineer. - - Tap to start a new chat - Pulsa para iniciar chat nuevo + + Temporary file error + Error en archivo temporal No comment provided by engineer. @@ -5586,6 +6536,11 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. La aplicación puede notificarte cuando recibas mensajes o solicitudes de contacto: por favor, abre la configuración para activarlo. No comment provided by engineer. + + The app will ask to confirm downloads from unknown file servers (except .onion). + La aplicación pedirá que confirmes las descargas desde servidores de archivos desconocidos (excepto si son .onion). + No comment provided by engineer. + The attempt to change database passphrase was not completed. El intento de cambiar la contraseña de la base de datos no se ha completado. @@ -5631,6 +6586,16 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. El mensaje será marcado como moderado para todos los miembros. No comment provided by engineer. + + The messages will be deleted for all members. + Los mensajes serán eliminados para todos los miembros. + No comment provided by engineer. + + + The messages will be marked as moderated for all members. + Los mensajes serán marcados como moderados para todos los miembros. + No comment provided by engineer. + The next generation of private messaging La nueva generación de mensajería privada @@ -5666,9 +6631,9 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. El texto pegado no es un enlace SimpleX. No comment provided by engineer. - - Theme - Tema + + Themes + Temas No comment provided by engineer. @@ -5678,7 +6643,7 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. They can be overridden in contact and group settings. - Se pueden anular en la configuración de contactos. + Se puede modificar desde la configuración particular de cada grupo y contacto. No comment provided by engineer. @@ -5736,11 +6701,21 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. ¡Este es tu propio enlace de un solo uso! No comment provided by engineer. + + This link was used with another mobile device, please create a new link on the desktop. + Este enlace ha sido usado en otro dispositivo móvil, por favor crea un enlace nuevo en el ordenador. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. Esta configuración se aplica a los mensajes del perfil actual **%@**. No comment provided by engineer. + + Title + Título + No comment provided by engineer. + To ask any questions and to receive updates: Para consultar cualquier duda y recibir actualizaciones: @@ -5771,6 +6746,11 @@ Puede ocurrir por algún bug o cuando la conexión está comprometida. Para proteger la zona horaria, los archivos de imagen/voz usan la hora UTC. No comment provided by engineer. + + To protect your IP address, private routing uses your SMP servers to deliver messages. + Para proteger tu dirección IP, el enrutamiento privado usa tu lista de servidores SMP para enviar mensajes. + No comment provided by engineer. + To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5798,16 +6778,36 @@ Se te pedirá que completes la autenticación antes de activar esta función.
Para verificar el cifrado de extremo a extremo con tu contacto, compara (o escanea) el código en ambos dispositivos. No comment provided by engineer. + + Toggle chat list: + Alternar lista de chats: + No comment provided by engineer. + Toggle incognito when connecting. Activa incógnito al conectar. No comment provided by engineer. + + Toolbar opacity + Opacidad barra + No comment provided by engineer. + + + Total + Total + No comment provided by engineer. + Transport isolation Aislamiento de transporte No comment provided by engineer. + + Transport sessions + Sesiones de transporte + No comment provided by engineer. + Trying to connect to the server used to receive messages from this contact (error: %@). Intentando conectar con el servidor usado para recibir mensajes de este contacto (error: %@). @@ -5863,11 +6863,6 @@ Se te pedirá que completes la autenticación antes de activar esta función.
¿Desbloquear miembro? No comment provided by engineer. - - Unexpected error: %@ - Error inesperado: %@ - item status description - Unexpected migration state Estado de migración inesperado @@ -5876,7 +6871,7 @@ Se te pedirá que completes la autenticación antes de activar esta función. Unfav. No fav. - No comment provided by engineer. + swipe action Unhide @@ -5913,6 +6908,11 @@ Se te pedirá que completes la autenticación antes de activar esta función.Error desconocido No comment provided by engineer. + + Unknown servers! + ¡Servidores desconocidos! + No comment provided by engineer. + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. A menos que utilices la interfaz de llamadas de iOS, activa el modo No molestar para evitar interrupciones. @@ -5921,9 +6921,8 @@ Se te pedirá que completes la autenticación antes de activar esta función. Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. To connect, please ask your contact to create another connection link and check that you have a stable network connection. - A menos que tu contacto haya eliminado la conexión o -que este enlace ya se haya usado, podría ser un error. Por favor, notifícalo. -Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueba que tienes buena conexión de red. + A menos que tu contacto haya eliminado la conexión o el enlace haya sido usado, podría ser un error. Por favor, notifícalo. +Para conectarte pide a tu contacto que cree otro enlace y comprueba la conexión de red. No comment provided by engineer. @@ -5949,12 +6948,12 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Unmute Activar audio - No comment provided by engineer. + swipe action Unread No leído - No comment provided by engineer. + swipe action Up to 100 last messages are sent to new members. @@ -5966,11 +6965,6 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Actualizar No comment provided by engineer. - - Update .onion hosts setting? - ¿Actualizar la configuración de los hosts .onion? - No comment provided by engineer. - Update database passphrase Actualizar contraseña de la base de datos @@ -5981,9 +6975,9 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb ¿Actualizar la configuración de red? No comment provided by engineer. - - Update transport isolation mode? - ¿Actualizar el modo de aislamiento de transporte? + + Update settings? + ¿Actualizar configuración? No comment provided by engineer. @@ -5991,16 +6985,16 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Al actualizar la configuración el cliente se reconectará a todos los servidores. No comment provided by engineer. - - Updating this setting will re-connect the client to all servers. - Al actualizar esta configuración el cliente se reconectará a todos los servidores. - No comment provided by engineer. - Upgrade and open chat Actualizar y abrir Chat No comment provided by engineer. + + Upload errors + Errores en subida + No comment provided by engineer. + Upload failed Error de subida @@ -6011,6 +7005,16 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Subir archivo server test step + + Uploaded + Subido + No comment provided by engineer. + + + Uploaded files + Archivos subidos + No comment provided by engineer. + Uploading archive Subiendo archivo @@ -6061,6 +7065,16 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb ¿Usar sólo notificaciones locales? No comment provided by engineer. + + Use private routing with unknown servers when IP address is not protected. + Usar enrutamiento privado con servidores desconocidos cuando tu dirección IP no está protegida. + No comment provided by engineer. + + + Use private routing with unknown servers. + Usar enrutamiento privado con servidores de retransmisión desconocidos. + No comment provided by engineer. + Use server Usar servidor @@ -6071,14 +7085,19 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Usar la aplicación durante la llamada. No comment provided by engineer. + + Use the app with one hand. + Usa la aplicación con una sola mano. + No comment provided by engineer. + User profile Perfil de usuario No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - Usar hosts .onion requiere un proveedor VPN compatible. + + User selection + Selección de usuarios No comment provided by engineer. @@ -6143,7 +7162,7 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Video will be received when your contact is online, please wait or check later! - El vídeo se recibirá cuando el contacto esté en línea, por favor espera o compruébalo más tarde. + El vídeo se recibirá cuando el contacto esté en línea, por favor espera o revisa más tarde. No comment provided by engineer. @@ -6211,6 +7230,16 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Esperando el vídeo No comment provided by engineer. + + Wallpaper accent + Color imagen de fondo + No comment provided by engineer. + + + Wallpaper background + Color de fondo + No comment provided by engineer. + Warning: starting chat on multiple devices is not supported and will cause message delivery failures Atención: el inicio del chat en varios dispositivos es incompatible y provocará fallos en la entrega de mensajes @@ -6296,19 +7325,39 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Con uso reducido de batería. No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + Sin Tor o VPN, tu dirección IP será visible para los servidores de archivos. + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + Sin Tor o VPN, tu dirección IP será visible para estos servidores XFTP: %@. + No comment provided by engineer. + Wrong database passphrase Contraseña de base de datos incorrecta No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + Clave incorrecta o conexión desconocida - probablemente esta conexión fue eliminada. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + Clave incorrecta o dirección del bloque del archivo desconocida. Es probable que el archivo se haya eliminado. + file error text + Wrong passphrase! ¡Contraseña incorrecta! No comment provided by engineer. - - XFTP servers - Servidores XFTP + + XFTP server + Servidor XFTP No comment provided by engineer. @@ -6388,11 +7437,21 @@ Repeat join request? Has sido invitado a un grupo No comment provided by engineer. + + You are not connected to these servers. Private routing is used to deliver messages to them. + No estás conectado a estos servidores. Para enviarles mensajes se usa el enrutamiento privado. + No comment provided by engineer. + You can accept calls from lock screen, without device and app authentication. Puede aceptar llamadas desde la pantalla de bloqueo, sin autenticación de dispositivos y aplicaciones. No comment provided by engineer. + + You can change it in Appearance settings. + Puedes cambiar la posición de la barra desde el menú Apariencia. + No comment provided by engineer. + You can create it later Puedes crearla más tarde @@ -6423,11 +7482,16 @@ Repeat join request? Puedes hacerlo visible para tus contactos de SimpleX en Configuración. No comment provided by engineer. - - You can now send messages to %@ - Ya puedes enviar mensajes a %@ + + You can now chat with %@ + Ya puedes chatear con %@ notification body + + You can send messages to %@ from Archived contacts. + Puedes enviar mensajes a %@ desde Contactos archivados. + No comment provided by engineer. + You can set lock screen notification preview via settings. Puedes configurar las notificaciones de la pantalla de bloqueo desde Configuración. @@ -6453,6 +7517,11 @@ Repeat join request? Puede iniciar Chat a través de la Configuración / Base de datos de la aplicación o reiniciando la aplicación No comment provided by engineer. + + You can still view conversation with %@ in the list of chats. + Aún puedes ver la conversación con %@ en la lista de chats. + No comment provided by engineer. + You can turn on SimpleX Lock via Settings. Puedes activar el Bloqueo SimpleX a través de Configuración. @@ -6495,11 +7564,6 @@ Repeat connection request? ¿Repetir solicitud? No comment provided by engineer. - - You have no chats - No tienes chats - No comment provided by engineer. - You have to enter passphrase every time the app starts - it is not stored on the device. La contraseña no se almacena en el dispositivo, tienes que introducirla cada vez que inicies la aplicación. @@ -6520,11 +7584,26 @@ Repeat connection request? Te has unido a este grupo. Conectando con el emisor de la invitacíon. No comment provided by engineer. + + You may migrate the exported database. + Puedes migrar la base de datos exportada. + No comment provided by engineer. + + + You may save the exported archive. + Puedes guardar el archivo exportado. + No comment provided by engineer. + You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. Debes usar la versión más reciente de tu base de datos ÚNICAMENTE en un dispositivo, de lo contrario podrías dejar de recibir mensajes de algunos contactos. No comment provided by engineer. + + You need to allow your contact to call to be able to call them. + Necesitas permitir que tus contacto llamen para poder llamarles. + No comment provided by engineer. + You need to allow your contact to send voice messages to be able to send them. Para poder enviar mensajes de voz antes debes permitir que tu contacto pueda enviarlos. @@ -6542,27 +7621,27 @@ Repeat connection request? You will be connected to group when the group host's device is online, please wait or check later! - Te conectarás al grupo cuando el dispositivo del anfitrión esté en línea, por favor espera o compruébalo más tarde. + Te conectarás al grupo cuando el dispositivo del anfitrión esté en línea, por favor espera o revisa más tarde. No comment provided by engineer. You will be connected when group link host's device is online, please wait or check later! - Te conectarás cuando el dispositivo propietario del grupo esté en línea, por favor espera o compruébalo más tarde. + Te conectarás cuando el dispositivo propietario del grupo esté en línea, por favor espera o revisa más tarde. No comment provided by engineer. You will be connected when your connection request is accepted, please wait or check later! - Te conectarás cuando tu solicitud se acepte, por favor espera o compruébalo más tarde. + Te conectarás cuando tu solicitud se acepte, por favor espera o revisa más tarde. No comment provided by engineer. You will be connected when your contact's device is online, please wait or check later! - Te conectarás cuando el dispositivo del contacto esté en línea, por favor espera o compruébalo más tarde. + Te conectarás cuando el dispositivo del contacto esté en línea, por favor espera o revisa más tarde. No comment provided by engineer. You will be required to authenticate when you start or resume the app after 30 seconds in background. - Se te pedirá identificarte cuándo inicies o continues usando la aplicación tras 30 segundos en segundo plano. + Se te pedirá autenticarte cuando inicies la aplicación o sigas usándola tras 30 segundos en segundo plano. No comment provided by engineer. @@ -6640,13 +7719,6 @@ Repeat connection request? Mis perfiles No comment provided by engineer. - - Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link). - El contacto debe estar en línea para completar la conexión. -Puedes cancelarla y eliminar el contacto (e intentarlo más tarde con un enlace nuevo). - No comment provided by engineer. - Your contact sent a file that is larger than currently supported maximum size (%@). El contacto ha enviado un archivo mayor al máximo admitido (%@). @@ -6689,14 +7761,14 @@ Puedes cancelarla y eliminar el contacto (e intentarlo más tarde con un enlace Your profile **%@** will be shared. - Tu perfil **%@** será compartido. + El perfil **%@** será compartido. No comment provided by engineer. Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile. - Tu perfil se almacena en tu dispositivo y sólo se comparte con tus contactos. -Los servidores de SimpleX no pueden ver tu perfil. + Tu perfil es almacenado en tu dispositivo y solamente se comparte con tus contactos. +Los servidores SimpleX no pueden ver tu perfil. No comment provided by engineer. @@ -6794,6 +7866,11 @@ Los servidores de SimpleX no pueden ver tu perfil. y %lld evento(s) más No comment provided by engineer. + + attempts + intentos + No comment provided by engineer. + audio call (not e2e encrypted) llamada (sin cifrar) @@ -6834,6 +7911,11 @@ Los servidores de SimpleX no pueden ver tu perfil. negrita No comment provided by engineer. + + call + llamada + No comment provided by engineer. + call error error en llamada @@ -6984,19 +8066,24 @@ Los servidores de SimpleX no pueden ver tu perfil. días time unit + + decryption errors + errores de descifrado + No comment provided by engineer. + default (%@) - por defecto (%@) + predeterminado (%@) pref value default (no) - por defecto (no) + predeterminado (no) No comment provided by engineer. default (yes) - por defecto (sí) + predeterminado (sí) No comment provided by engineer. @@ -7034,6 +8121,11 @@ Los servidores de SimpleX no pueden ver tu perfil. mensaje duplicado integrity error chat item + + duplicates + duplicados + No comment provided by engineer. + e2e encrypted cifrado de extremo a extremo @@ -7114,6 +8206,11 @@ Los servidores de SimpleX no pueden ver tu perfil. evento ocurrido No comment provided by engineer. + + expired + expirados + No comment provided by engineer. + forwarded reenviado @@ -7144,6 +8241,11 @@ Los servidores de SimpleX no pueden ver tu perfil. iOS Keychain se usará para almacenar la contraseña de forma segura después de reiniciar la aplicación o cambiar la contraseña. Esto permitirá recibir notificaciones automáticas. No comment provided by engineer. + + inactive + inactivo + No comment provided by engineer. + incognito via contact address link en modo incógnito mediante enlace de dirección del contacto @@ -7184,6 +8286,11 @@ Los servidores de SimpleX no pueden ver tu perfil. invitación al grupo %@ group name + + invite + Invitar + No comment provided by engineer. + invited ha sido invitado @@ -7239,6 +8346,11 @@ Los servidores de SimpleX no pueden ver tu perfil. conectado rcv group event chat item + + message + mensaje + No comment provided by engineer. + message received mensaje recibido @@ -7269,6 +8381,11 @@ Los servidores de SimpleX no pueden ver tu perfil. meses time unit + + mute + silenciar + No comment provided by engineer. + never nunca @@ -7321,6 +8438,16 @@ Los servidores de SimpleX no pueden ver tu perfil. Activado group pref value + + other + otros + No comment provided by engineer. + + + other errors + otros errores + No comment provided by engineer. + owner propietario @@ -7391,6 +8518,11 @@ Los servidores de SimpleX no pueden ver tu perfil. Guardado desde %@ No comment provided by engineer. + + search + buscar + No comment provided by engineer. + sec seg @@ -7416,6 +8548,15 @@ Los servidores de SimpleX no pueden ver tu perfil. Enviar mensaje directo No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + información cola del servidor: %1$@ + +último mensaje recibido: %2$@ + queue info + set new contact address nueva dirección de contacto @@ -7456,11 +8597,26 @@ Los servidores de SimpleX no pueden ver tu perfil. desconocido connection info + + unknown servers + con servidores desconocidos + No comment provided by engineer. + unknown status estado desconocido No comment provided by engineer. + + unmute + activar sonido + No comment provided by engineer. + + + unprotected + con IP desprotegida + No comment provided by engineer. + updated group profile ha actualizado el perfil del grupo @@ -7501,6 +8657,11 @@ Los servidores de SimpleX no pueden ver tu perfil. mediante retransmisor No comment provided by engineer. + + video + video + No comment provided by engineer. + video call (not e2e encrypted) videollamada (sin cifrar) @@ -7526,6 +8687,11 @@ Los servidores de SimpleX no pueden ver tu perfil. semanas time unit + + when IP hidden + con IP oculta + No comment provided by engineer. + yes @@ -7610,7 +8776,7 @@ Los servidores de SimpleX no pueden ver tu perfil.
- +
@@ -7647,7 +8813,7 @@ Los servidores de SimpleX no pueden ver tu perfil.
- +
@@ -7667,4 +8833,218 @@ Los servidores de SimpleX no pueden ver tu perfil.
+ +
+ +
+ + + SimpleX SE + SimpleX SE + Bundle display name + + + SimpleX SE + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright © 2024 SimpleX Chat. Todos los derechos reservados. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + %@ + No comment provided by engineer. + + + App is locked! + ¡Aplicación bloqueada! + No comment provided by engineer. + + + Cancel + Cancelar + No comment provided by engineer. + + + Cannot access keychain to save database password + Keychain inaccesible para guardar la contraseña de la base de datos + No comment provided by engineer. + + + Cannot forward message + No se puede reenviar el mensaje + No comment provided by engineer. + + + Comment + Comentario + No comment provided by engineer. + + + Currently maximum supported file size is %@. + El tamaño máximo de archivo admitido es %@. + No comment provided by engineer. + + + Database downgrade required + Se requiere volver a versión anterior de la base de datos + No comment provided by engineer. + + + Database encrypted! + ¡Base de datos cifrada! + No comment provided by engineer. + + + Database error + Error en base de datos + No comment provided by engineer. + + + Database passphrase is different from saved in the keychain. + La contraseña de la base de datos es distinta a la almacenada en keychain. + No comment provided by engineer. + + + Database passphrase is required to open chat. + Se requiere la contraseña de la base de datos para abrir la aplicación. + No comment provided by engineer. + + + Database upgrade required + Se requiere actualizar la base de datos + No comment provided by engineer. + + + Error preparing file + Error al preparar el archivo + No comment provided by engineer. + + + Error preparing message + Error al preparar el mensaje + No comment provided by engineer. + + + Error: %@ + Error: %@ + No comment provided by engineer. + + + File error + Error de archivo + No comment provided by engineer. + + + Incompatible database version + Versión de base de datos incompatible + No comment provided by engineer. + + + Invalid migration confirmation + Confirmación de migración no válida + No comment provided by engineer. + + + Keychain error + Error en keychain + No comment provided by engineer. + + + Large file! + ¡Archivo grande! + No comment provided by engineer. + + + No active profile + Ningún perfil activo + No comment provided by engineer. + + + Ok + Ok + No comment provided by engineer. + + + Open the app to downgrade the database. + Abre la aplicación para volver a versión anterior de la base de datos. + No comment provided by engineer. + + + Open the app to upgrade the database. + Abre la aplicación para actualizar la base de datos. + No comment provided by engineer. + + + Passphrase + Frase de contraseña + No comment provided by engineer. + + + Please create a profile in the SimpleX app + Por favor, crea un perfil en SimpleX + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + Las preferencias seleccionadas no permiten este mensaje. + No comment provided by engineer. + + + Sending a message takes longer than expected. + Enviar el mensaje lleva más tiempo del esperado. + No comment provided by engineer. + + + Sending message… + Enviando mensaje… + No comment provided by engineer. + + + Share + Compartir + No comment provided by engineer. + + + Slow network? + ¿Red lenta? + No comment provided by engineer. + + + Unknown database error: %@ + Error desconocido en la base de datos: %@ + No comment provided by engineer. + + + Unsupported format + Formato sin soporte + No comment provided by engineer. + + + Wait + Espera + No comment provided by engineer. + + + Wrong database passphrase + Contraseña incorrecta de la base de datos + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + Puedes dar permiso para compartir en Privacidad y Seguridad / Bloque SimpleX. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/es.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/es.xcloc/contents.json b/apps/ios/SimpleX Localizations/es.xcloc/contents.json index c7d2c05ffa..340591e607 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/es.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "es", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "15F31d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "15.4" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff index a239bebbcf..211e512a1e 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Localized Contents/fi.xliff @@ -2,7 +2,7 @@
- +
@@ -124,11 +124,6 @@ %@ on vahvistettu No comment provided by engineer. - - %@ servers - %@ palvelimet - No comment provided by engineer. - %@ uploaded No comment provided by engineer. @@ -534,16 +529,16 @@ Tietoja SimpleX osoitteesta No comment provided by engineer. - - Accent color - Korostusväri + + Accent No comment provided by engineer. Accept Hyväksy accept contact request via notification - accept incoming call via notification + accept incoming call via notification + swipe action Accept connection request? @@ -558,7 +553,20 @@ Accept incognito Hyväksy tuntematon - accept contact request via notification + accept contact request via notification + swipe action + + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + No comment provided by engineer. + + + Active connections + No comment provided by engineer. Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. @@ -579,16 +587,16 @@ Lisää profiili No comment provided by engineer. + + Add server + Lisää palvelin + No comment provided by engineer. + Add servers by scanning QR codes. Lisää palvelimia skannaamalla QR-koodeja. No comment provided by engineer. - - Add server… - Lisää palvelin… - No comment provided by engineer. - Add to another device Lisää toiseen laitteeseen @@ -599,6 +607,18 @@ Lisää tervetuloviesti No comment provided by engineer. + + Additional accent + No comment provided by engineer. + + + Additional accent 2 + No comment provided by engineer. + + + Additional secondary + No comment provided by engineer. + Address Osoite @@ -623,6 +643,10 @@ Verkon lisäasetukset No comment provided by engineer. + + Advanced settings + No comment provided by engineer. + All app data is deleted. Kaikki sovelluksen tiedot poistetaan. @@ -638,6 +662,10 @@ Kaikki tiedot poistetaan, kun se syötetään. No comment provided by engineer. + + All data is private to your device. + No comment provided by engineer. + All group members will remain connected. Kaikki ryhmän jäsenet pysyvät yhteydessä. @@ -656,6 +684,10 @@ All new messages from %@ will be hidden! No comment provided by engineer. + + All profiles + No comment provided by engineer. + All your contacts will remain connected. Kaikki kontaktisi pysyvät yhteydessä. @@ -680,11 +712,19 @@ Salli puhelut vain, jos kontaktisi sallii ne. No comment provided by engineer. + + Allow calls? + No comment provided by engineer. + Allow disappearing messages only if your contact allows it to you. Salli katoavat viestit vain, jos kontaktisi sallii sen sinulle. No comment provided by engineer. + + Allow downgrade + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Salli peruuttamaton viestien poisto vain, jos kontaktisi sallii ne sinulle. (24 tuntia) @@ -710,6 +750,10 @@ Salli katoavien viestien lähettäminen. No comment provided by engineer. + + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Salli lähetettyjen viestien peruuttamaton poistaminen. (24 tuntia) @@ -777,6 +821,10 @@ Already joining the group! No comment provided by engineer. + + Always use private routing. + No comment provided by engineer. + Always use relay Käytä aina relettä @@ -839,10 +887,22 @@ Apply No comment provided by engineer. + + Apply to + No comment provided by engineer. + Archive and upload No comment provided by engineer. + + Archive contacts to chat later. + No comment provided by engineer. + + + Archived contacts + No comment provided by engineer. + Archiving database No comment provided by engineer. @@ -912,6 +972,10 @@ Takaisin No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address No comment provided by engineer. @@ -935,6 +999,14 @@ Parempia viestejä No comment provided by engineer. + + Better networking + No comment provided by engineer. + + + Black + No comment provided by engineer. + Block No comment provided by engineer. @@ -963,6 +1035,14 @@ Blocked by admin No comment provided by engineer. + + Blur for better privacy. + No comment provided by engineer. + + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. Sekä sinä että kontaktisi voivat käyttää viestireaktioita. @@ -1007,10 +1087,22 @@ Puhelut No comment provided by engineer. + + Calls prohibited! + No comment provided by engineer. + Camera not available No comment provided by engineer. + + Can't call contact + No comment provided by engineer. + + + Can't call member + No comment provided by engineer. + Can't invite contact! Kontaktia ei voi kutsua! @@ -1021,6 +1113,10 @@ Kontakteja ei voi kutsua! No comment provided by engineer. + + Can't message member + No comment provided by engineer. + Cancel Peruuta @@ -1035,11 +1131,19 @@ Ei pääsyä avainnippuun tietokannan salasanan tallentamiseksi No comment provided by engineer. + + Cannot forward message + No comment provided by engineer. + Cannot receive file Tiedostoa ei voi vastaanottaa No comment provided by engineer. + + Capacity exceeded - recipient did not receive previously sent messages. + snd error text + Cellular No comment provided by engineer. @@ -1100,6 +1204,10 @@ Chat-arkisto No comment provided by engineer. + + Chat colors + No comment provided by engineer. + Chat console Chat-konsoli @@ -1115,6 +1223,10 @@ Chat-tietokanta poistettu No comment provided by engineer. + + Chat database exported + No comment provided by engineer. + Chat database imported Chat-tietokanta tuotu @@ -1134,6 +1246,10 @@ Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat. No comment provided by engineer. + + Chat list + No comment provided by engineer. + Chat migrated! No comment provided by engineer. @@ -1143,6 +1259,10 @@ Chat-asetukset No comment provided by engineer. + + Chat theme + No comment provided by engineer. + Chats Keskustelut @@ -1172,10 +1292,22 @@ Valitse kirjastosta No comment provided by engineer. + + Chunks deleted + No comment provided by engineer. + + + Chunks downloaded + No comment provided by engineer. + + + Chunks uploaded + No comment provided by engineer. + Clear Tyhjennä - No comment provided by engineer. + swipe action Clear conversation @@ -1196,9 +1328,12 @@ Tyhjennä vahvistus No comment provided by engineer. - - Colors - Värit + + Color chats with the new themes. + No comment provided by engineer. + + + Color mode No comment provided by engineer. @@ -1211,11 +1346,19 @@ Vertaa turvakoodeja kontaktiesi kanssa. No comment provided by engineer. + + Completed + No comment provided by engineer. + Configure ICE servers Määritä ICE-palvelimet No comment provided by engineer. + + Configured %@ servers + No comment provided by engineer. + Confirm Vahvista @@ -1226,11 +1369,19 @@ Vahvista pääsykoodi No comment provided by engineer. + + Confirm contact deletion? + No comment provided by engineer. + Confirm database upgrades Vahvista tietokannan päivitykset No comment provided by engineer. + + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings No comment provided by engineer. @@ -1271,6 +1422,10 @@ Connect to desktop No comment provided by engineer. + + Connect to your friends faster. + No comment provided by engineer. + Connect to yourself? No comment provided by engineer. @@ -1303,14 +1458,26 @@ This is your own one-time link! Connect with %@ No comment provided by engineer. + + Connected + No comment provided by engineer. + Connected desktop No comment provided by engineer. + + Connected servers + No comment provided by engineer. + Connected to desktop No comment provided by engineer. + + Connecting + No comment provided by engineer. + Connecting to server… Yhteyden muodostaminen palvelimeen… @@ -1321,6 +1488,10 @@ This is your own one-time link! Yhteyden muodostaminen palvelimeen... (virhe: %@) No comment provided by engineer. + + Connecting to contact, please wait or check later! + No comment provided by engineer. + Connecting to desktop No comment provided by engineer. @@ -1330,6 +1501,10 @@ This is your own one-time link! Yhteys No comment provided by engineer. + + Connection and servers status. + No comment provided by engineer. + Connection error Yhteysvirhe @@ -1340,6 +1515,10 @@ This is your own one-time link! Yhteysvirhe (AUTH) No comment provided by engineer. + + Connection notifications + No comment provided by engineer. + Connection request sent! Yhteyspyyntö lähetetty! @@ -1354,6 +1533,14 @@ This is your own one-time link! Yhteyden aikakatkaisu No comment provided by engineer. + + Connection with desktop stopped + No comment provided by engineer. + + + Connections + No comment provided by engineer. + Contact allows Kontakti sallii @@ -1364,6 +1551,10 @@ This is your own one-time link! Kontakti on jo olemassa No comment provided by engineer. + + Contact deleted! + No comment provided by engineer. + Contact hidden: Kontakti piilotettu: @@ -1374,9 +1565,8 @@ This is your own one-time link! Kontakti on yhdistetty notification - - Contact is not connected yet! - Kontaktia ei ole vielä yhdistetty! + + Contact is deleted. No comment provided by engineer. @@ -1389,6 +1579,10 @@ This is your own one-time link! Kontaktin asetukset No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + No comment provided by engineer. + Contacts Kontaktit @@ -1404,10 +1598,18 @@ This is your own one-time link! Jatka No comment provided by engineer. + + Conversation deleted! + No comment provided by engineer. + Copy Kopioi - chat item action + No comment provided by engineer. + + + Copy error + No comment provided by engineer. Core version: v%@ @@ -1480,6 +1682,10 @@ This is your own one-time link! Luo profiilisi No comment provided by engineer. + + Created + No comment provided by engineer. + Created at No comment provided by engineer. @@ -1511,6 +1717,10 @@ This is your own one-time link! Nykyinen tunnuslause… No comment provided by engineer. + + Current profile + No comment provided by engineer. + Currently maximum supported file size is %@. Nykyinen tuettu enimmäistiedostokoko on %@. @@ -1521,11 +1731,19 @@ This is your own one-time link! Mukautettu aika No comment provided by engineer. + + Customize theme + No comment provided by engineer. + Dark Tumma No comment provided by engineer. + + Dark mode colors + No comment provided by engineer. + Database ID Tietokannan tunnus @@ -1624,6 +1842,10 @@ This is your own one-time link! Tietokanta siirretään, kun sovellus käynnistyy uudelleen No comment provided by engineer. + + Debug delivery + No comment provided by engineer. + Decentralized Hajautettu @@ -1637,17 +1859,17 @@ This is your own one-time link! Delete Poista - chat item action + chat item action + swipe action + + + Delete %lld messages of members? + No comment provided by engineer. Delete %lld messages? No comment provided by engineer. - - Delete Contact - Poista kontakti - No comment provided by engineer. - Delete address Poista osoite @@ -1702,9 +1924,8 @@ This is your own one-time link! Poista kontakti No comment provided by engineer. - - Delete contact? -This cannot be undone! + + Delete contact? No comment provided by engineer. @@ -1796,11 +2017,6 @@ This cannot be undone! Poista vanha tietokanta? No comment provided by engineer. - - Delete pending connection - Poista vireillä oleva yhteys - No comment provided by engineer. - Delete pending connection? Poistetaanko odottava yhteys? @@ -1816,11 +2032,23 @@ This cannot be undone! Poista jono server test step + + Delete up to 20 messages at once. + No comment provided by engineer. + Delete user profile? Poista käyttäjäprofiili? No comment provided by engineer. + + Delete without notification + No comment provided by engineer. + + + Deleted + No comment provided by engineer. + Deleted at Poistettu klo @@ -1831,6 +2059,10 @@ This cannot be undone! Poistettu klo: %@ copied message info + + Deletion errors + No comment provided by engineer. + Delivery Toimitus @@ -1863,11 +2095,35 @@ This cannot be undone! Desktop devices No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + No comment provided by engineer. + + + Destination server error: %@ + snd error text + + + Destination server version of %@ is incompatible with forwarding server %@. + No comment provided by engineer. + + + Detailed statistics + No comment provided by engineer. + + + Details + No comment provided by engineer. + Develop Kehitä No comment provided by engineer. + + Developer options + No comment provided by engineer. + Developer tools Kehittäjätyökalut @@ -1918,6 +2174,10 @@ This cannot be undone! Poista käytöstä kaikilta No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message Tuhoutuva viesti @@ -1966,11 +2226,19 @@ This cannot be undone! Discover via local network No comment provided by engineer. + + Do NOT send messages directly, even if your or destination server does not support private routing. + No comment provided by engineer. + Do NOT use SimpleX for emergency calls. Älä käytä SimpleX-sovellusta hätäpuheluihin. No comment provided by engineer. + + Do NOT use private routing. + No comment provided by engineer. + Do it later Tee myöhemmin @@ -2004,6 +2272,10 @@ This cannot be undone! Download chat item action + + Download errors + No comment provided by engineer. + Download failed No comment provided by engineer. @@ -2013,6 +2285,14 @@ This cannot be undone! Lataa tiedosto server test step + + Downloaded + No comment provided by engineer. + + + Downloaded files + No comment provided by engineer. + Downloading archive No comment provided by engineer. @@ -2109,6 +2389,10 @@ This cannot be undone! Ota itsetuhoava pääsykoodi käyttöön set passcode view + + Enabled + No comment provided by engineer. + Enabled for No comment provided by engineer. @@ -2270,6 +2554,10 @@ This cannot be undone! Virhe asetuksen muuttamisessa No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address Virhe osoitteen luomisessa @@ -2318,11 +2606,6 @@ This cannot be undone! Virhe yhteyden poistamisessa No comment provided by engineer. - - Error deleting contact - Virhe kontaktin poistamisessa - No comment provided by engineer. - Error deleting database Virhe tietokannan poistamisessa @@ -2367,6 +2650,10 @@ This cannot be undone! Virhe vietäessä keskustelujen tietokantaa No comment provided by engineer. + + Error exporting theme: %@ + No comment provided by engineer. + Error importing chat database Virhe keskustelujen tietokannan tuonnissa @@ -2391,11 +2678,23 @@ This cannot be undone! Virhe tiedoston vastaanottamisessa No comment provided by engineer. + + Error reconnecting server + No comment provided by engineer. + + + Error reconnecting servers + No comment provided by engineer. + Error removing member Virhe poistettaessa jäsentä No comment provided by engineer. + + Error resetting statistics + No comment provided by engineer. + Error saving %@ servers Virhe %@ palvelimien tallentamisessa @@ -2509,7 +2808,8 @@ This cannot be undone! Error: %@ Virhe: %@ - No comment provided by engineer. + file error text + snd error text Error: URL is invalid @@ -2521,6 +2821,10 @@ This cannot be undone! Virhe: ei tietokantatiedostoa No comment provided by engineer. + + Errors + No comment provided by engineer. + Even when disabled in the conversation. Jopa kun ei käytössä keskustelussa. @@ -2545,6 +2849,10 @@ This cannot be undone! Vientivirhe: No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. Viety tietokanta-arkisto. @@ -2576,8 +2884,28 @@ This cannot be undone! Favorite Suosikki + swipe action + + + File error No comment provided by engineer. + + File not found - most likely file was deleted or cancelled. + file error text + + + File server error: %@ + file error text + + + File status + No comment provided by engineer. + + + File status: %@ + copied message info + File will be deleted from servers. Tiedosto poistetaan palvelimilta. @@ -2598,6 +2926,10 @@ This cannot be undone! Tiedosto: %@ No comment provided by engineer. + + Files + No comment provided by engineer. + Files & media Tiedostot & media @@ -2696,6 +3028,28 @@ This cannot be undone! Forwarded from No comment provided by engineer. + + Forwarding server %@ failed to connect to destination server %@. Please try later. + No comment provided by engineer. + + + Forwarding server address is incompatible with network settings: %@. + No comment provided by engineer. + + + Forwarding server version is incompatible with network settings: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + snd error text + Found desktop No comment provided by engineer. @@ -2739,6 +3093,14 @@ This cannot be undone! GIFit ja tarrat No comment provided by engineer. + + Good afternoon! + message preview + + + Good morning! + message preview + Group Ryhmä @@ -3013,6 +3375,10 @@ This cannot be undone! Import failed No comment provided by engineer. + + Import theme + No comment provided by engineer. + Importing archive No comment provided by engineer. @@ -3129,6 +3495,10 @@ This cannot be undone! Käyttöliittymä No comment provided by engineer. + + Interface colors + No comment provided by engineer. + Invalid QR code No comment provided by engineer. @@ -3224,6 +3594,10 @@ This cannot be undone! 3. Yhteys vaarantui. No comment provided by engineer. + + It protects your IP address and connections. + No comment provided by engineer. + It seems like you are already connected via this link. If it is not the case, there was an error (%@). Näyttäisi, että olet jo yhteydessä tämän linkin kautta. Jos näin ei ole, tapahtui virhe (%@). @@ -3242,7 +3616,7 @@ This cannot be undone! Join Liity - No comment provided by engineer. + swipe action Join group @@ -3280,6 +3654,10 @@ This is your link for group %@! Keep No comment provided by engineer. + + Keep conversation + No comment provided by engineer. + Keep the app open to use it from desktop No comment provided by engineer. @@ -3321,7 +3699,7 @@ This is your link for group %@! Leave Poistu - No comment provided by engineer. + swipe action Leave group @@ -3450,11 +3828,23 @@ This is your link for group %@! Enintään 30 sekuntia, vastaanotetaan välittömästi. No comment provided by engineer. + + Media & file servers + No comment provided by engineer. + + + Medium + blur media + Member Jäsen No comment provided by engineer. + + Member inactive + item status text + Member role will be changed to "%@". All group members will be notified. Jäsenen rooli muuttuu muotoon "%@". Kaikille ryhmän jäsenille ilmoitetaan asiasta. @@ -3470,6 +3860,10 @@ This is your link for group %@! Jäsen poistetaan ryhmästä - tätä ei voi perua! No comment provided by engineer. + + Menus + No comment provided by engineer. + Message delivery error Viestin toimitusvirhe @@ -3480,11 +3874,27 @@ This is your link for group %@! Viestien toimituskuittaukset! No comment provided by engineer. + + Message delivery warning + item status text + Message draft Viestiluonnos No comment provided by engineer. + + Message forwarded + item status text + + + Message may be delivered later if member becomes active. + item status description + + + Message queue info + No comment provided by engineer. + Message reactions Viestireaktiot @@ -3500,10 +3910,26 @@ This is your link for group %@! Viestireaktiot ovat kiellettyjä tässä ryhmässä. No comment provided by engineer. + + Message reception + No comment provided by engineer. + + + Message servers + No comment provided by engineer. + Message source remains private. No comment provided by engineer. + + Message status + No comment provided by engineer. + + + Message status: %@ + copied message info + Message text Viestin teksti @@ -3527,6 +3953,14 @@ This is your link for group %@! Messages from %@ will be shown! No comment provided by engineer. + + Messages received + No comment provided by engineer. + + + Messages sent + No comment provided by engineer. + Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. No comment provided by engineer. @@ -3617,11 +4051,6 @@ This is your link for group %@! Todennäköisesti tämä yhteys on poistettu. item status description - - Most likely this contact has deleted the connection with you. - Todennäköisesti tämä kontakti on poistanut yhteyden sinuun. - No comment provided by engineer. - Multiple chat profiles Useita keskusteluprofiileja @@ -3630,7 +4059,7 @@ This is your link for group %@! Mute Mykistä - No comment provided by engineer. + swipe action Muted when inactive! @@ -3640,7 +4069,7 @@ This is your link for group %@! Name Nimi - No comment provided by engineer. + swipe action Network & servers @@ -3651,6 +4080,10 @@ This is your link for group %@! Network connection No comment provided by engineer. + + Network issues - message expired after many attempts to send it. + snd error text + Network management No comment provided by engineer. @@ -3674,6 +4107,10 @@ This is your link for group %@! New chat No comment provided by engineer. + + New chat experience 🎉 + No comment provided by engineer. + New contact request Uusi kontaktipyyntö @@ -3703,6 +4140,10 @@ This is your link for group %@! Uutta %@ No comment provided by engineer. + + New media options + No comment provided by engineer. + New member role Uusi jäsenrooli @@ -3748,6 +4189,10 @@ This is your link for group %@! Ei laitetunnusta! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + item status description + No filtered chats Ei suodatettuja keskusteluja @@ -3763,6 +4208,10 @@ This is your link for group %@! Ei historiaa No comment provided by engineer. + + No info, try to reload + No comment provided by engineer. + No network connection No comment provided by engineer. @@ -3781,6 +4230,10 @@ This is your link for group %@! Not compatible! No comment provided by engineer. + + Nothing selected + No comment provided by engineer. + Notifications Ilmoitukset @@ -3807,7 +4260,7 @@ This is your link for group %@! Off Pois - No comment provided by engineer. + blur media Ok @@ -3829,14 +4282,18 @@ This is your link for group %@! Kertakutsulinkki No comment provided by engineer. - - Onion hosts will be required for connection. Requires enabling VPN. - Yhteyden muodostamiseen tarvitaan Onion-isäntiä. Edellyttää VPN:n sallimista. + + Onion hosts will be **required** for connection. +Requires compatible VPN. + Yhteyden muodostamiseen tarvitaan Onion-isäntiä. +Edellyttää VPN:n sallimista. No comment provided by engineer. - - Onion hosts will be used when available. Requires enabling VPN. - Onion-isäntiä käytetään, kun niitä on saatavilla. Edellyttää VPN:n sallimista. + + Onion hosts will be used when available. +Requires compatible VPN. + Onion-isäntiä käytetään, kun niitä on saatavilla. +Edellyttää VPN:n sallimista. No comment provided by engineer. @@ -3849,6 +4306,10 @@ This is your link for group %@! Vain asiakaslaitteet tallentavat käyttäjäprofiileja, yhteystietoja, ryhmiä ja viestejä, jotka on lähetetty **kaksinkertaisella päästä päähän -salauksella**. No comment provided by engineer. + + Only delete conversation + No comment provided by engineer. + Only group owners can change group preferences. Vain ryhmän omistajat voivat muuttaa ryhmän asetuksia. @@ -3941,6 +4402,10 @@ This is your link for group %@! Open migration to another device authentication reason + + Open server settings + No comment provided by engineer. + Open user profiles Avaa käyttäjäprofiilit @@ -3975,6 +4440,10 @@ This is your link for group %@! Other No comment provided by engineer. + + Other %@ servers + No comment provided by engineer. + PING count PING-määrä @@ -4036,6 +4505,10 @@ This is your link for group %@! Paste the link you received No comment provided by engineer. + + Pending + No comment provided by engineer. + People can connect to you only via the links you share. Ihmiset voivat ottaa sinuun yhteyttä vain jakamiesi linkkien kautta. @@ -4055,11 +4528,24 @@ This is your link for group %@! Picture-in-picture calls No comment provided by engineer. + + Play from the chat list. + No comment provided by engineer. + + + Please ask your contact to enable calls. + No comment provided by engineer. + Please ask your contact to enable sending voice messages. Pyydä kontaktiasi sallimaan ääniviestien lähettäminen. No comment provided by engineer. + + Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers. + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. Tarkista, että käytit oikeaa linkkiä tai pyydä kontaktiasi lähettämään sinulle uusi linkki. @@ -4154,6 +4640,10 @@ Error: %@ Esikatselu No comment provided by engineer. + + Previously connected servers + No comment provided by engineer. + Privacy & security Yksityisyys ja turvallisuus @@ -4169,10 +4659,26 @@ Error: %@ Yksityiset tiedostonimet No comment provided by engineer. + + Private message routing + No comment provided by engineer. + + + Private message routing 🚀 + No comment provided by engineer. + Private notes name of notes to self + + Private routing + No comment provided by engineer. + + + Private routing error + No comment provided by engineer. + Profile and server connections Profiili- ja palvelinyhteydet @@ -4200,6 +4706,10 @@ Error: %@ Profiilin salasana No comment provided by engineer. + + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. Profiilipäivitys lähetetään kontakteillesi. @@ -4249,11 +4759,20 @@ Error: %@ Estä ääniviestien lähettäminen. No comment provided by engineer. + + Protect IP address + No comment provided by engineer. + Protect app screen Suojaa sovellusnäyttö No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! Suojaa keskusteluprofiilisi salasanalla! @@ -4269,6 +4788,14 @@ Error: %@ Protokollan aikakatkaisu per KB No comment provided by engineer. + + Proxied + No comment provided by engineer. + + + Proxied servers + No comment provided by engineer. + Push notifications Push-ilmoitukset @@ -4287,6 +4814,10 @@ Error: %@ Arvioi sovellus No comment provided by engineer. + + Reachable chat toolbar + No comment provided by engineer. + React… Reagoi… @@ -4295,7 +4826,7 @@ Error: %@ Read Lue - No comment provided by engineer. + swipe action Read more @@ -4331,6 +4862,10 @@ Error: %@ Kuittaukset pois käytöstä No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at Vastaanotettu klo @@ -4351,15 +4886,23 @@ Error: %@ Vastaanotettu viesti message info title + + Received messages + No comment provided by engineer. + + + Received reply + No comment provided by engineer. + + + Received total + No comment provided by engineer. + Receiving address will be changed to a different server. Address change will complete after sender comes online. Vastaanotto-osoite vaihdetaan toiseen palvelimeen. Osoitteenmuutos tehdään sen jälkeen, kun lähettäjä tulee verkkoon. No comment provided by engineer. - - Receiving concurrency - No comment provided by engineer. - Receiving file will be stopped. Tiedoston vastaanotto pysäytetään. @@ -4383,11 +4926,31 @@ Error: %@ Vastaanottajat näkevät päivitykset, kun kirjoitat niitä. No comment provided by engineer. + + Reconnect + No comment provided by engineer. + Reconnect all connected servers to force message delivery. It uses additional traffic. Yhdistä kaikki yhdistetyt palvelimet uudelleen pakottaaksesi viestin toimituksen. Tämä käyttää ylimääräistä liikennettä. No comment provided by engineer. + + Reconnect all servers + No comment provided by engineer. + + + Reconnect all servers? + No comment provided by engineer. + + + Reconnect server to force message delivery. It uses additional traffic. + No comment provided by engineer. + + + Reconnect server? + No comment provided by engineer. + Reconnect servers? Yhdistä palvelimet uudelleen? @@ -4411,7 +4974,8 @@ Error: %@ Reject Hylkää - reject incoming call via notification + reject incoming call via notification + swipe action Reject (sender NOT notified) @@ -4438,6 +5002,10 @@ Error: %@ Poista No comment provided by engineer. + + Remove image + No comment provided by engineer. + Remove member Poista jäsen @@ -4503,16 +5071,36 @@ Error: %@ Oletustilaan No comment provided by engineer. + + Reset all hints + No comment provided by engineer. + + + Reset all statistics + No comment provided by engineer. + + + Reset all statistics? + No comment provided by engineer. + Reset colors Oletusvärit No comment provided by engineer. + + Reset to app theme + No comment provided by engineer. + Reset to defaults Palauta oletusasetukset No comment provided by engineer. + + Reset to user theme + No comment provided by engineer. + Restart the app to create a new chat profile Käynnistä sovellus uudelleen uuden keskusteluprofiilin luomiseksi @@ -4552,11 +5140,6 @@ Error: %@ Paljasta chat item action - - Revert - Palauta - No comment provided by engineer. - Revoke Peruuta @@ -4582,9 +5165,12 @@ Error: %@ Käynnistä chat No comment provided by engineer. - - SMP servers - SMP-palvelimet + + SMP server + No comment provided by engineer. + + + Safely receive files No comment provided by engineer. @@ -4611,6 +5197,10 @@ Error: %@ Tallenna ja ilmoita ryhmän jäsenille No comment provided by engineer. + + Save and reconnect + No comment provided by engineer. + Save and update group profile Tallenna ja päivitä ryhmäprofiili @@ -4688,6 +5278,14 @@ Error: %@ Saved message message info title + + Scale + No comment provided by engineer. + + + Scan / Paste link + No comment provided by engineer. + Scan QR code Skannaa QR-koodi @@ -4725,11 +5323,19 @@ Error: %@ Search or paste SimpleX link No comment provided by engineer. + + Secondary + No comment provided by engineer. + Secure queue Turvallinen jono server test step + + Secured + No comment provided by engineer. + Security assessment Turvallisuusarviointi @@ -4743,6 +5349,14 @@ Error: %@ Select Valitse + chat item action + + + Selected %lld + No comment provided by engineer. + + + Selected chat preferences prohibit this message. No comment provided by engineer. @@ -4780,11 +5394,6 @@ Error: %@ Lähetä toimituskuittaukset vastaanottajalle No comment provided by engineer. - - Send direct message - Lähetä yksityisviesti - No comment provided by engineer. - Send direct message to connect No comment provided by engineer. @@ -4794,6 +5403,10 @@ Error: %@ Lähetä katoava viesti No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews Lähetä linkkien esikatselu @@ -4804,6 +5417,18 @@ Error: %@ Lähetä live-viesti No comment provided by engineer. + + Send message to enable calls. + No comment provided by engineer. + + + Send messages directly when IP address is protected and your or destination server does not support private routing. + No comment provided by engineer. + + + Send messages directly when your or destination server does not support private routing. + No comment provided by engineer. + Send notifications Lähetys ilmoitukset @@ -4893,6 +5518,10 @@ Error: %@ Lähetetty klo: %@ copied message info + + Sent directly + No comment provided by engineer. + Sent file event Lähetetty tiedosto tapahtuma @@ -4903,11 +5532,39 @@ Error: %@ Lähetetty viesti message info title + + Sent messages + No comment provided by engineer. + Sent messages will be deleted after set time. Lähetetyt viestit poistetaan asetetun ajan kuluttua. No comment provided by engineer. + + Sent reply + No comment provided by engineer. + + + Sent total + No comment provided by engineer. + + + Sent via proxy + No comment provided by engineer. + + + Server address + No comment provided by engineer. + + + Server address is incompatible with network settings. + srv error text. + + + Server address is incompatible with network settings: %@. + No comment provided by engineer. + Server requires authorization to create queues, check password Palvelin vaatii valtuutuksen jonojen luomiseen, tarkista salasana @@ -4923,11 +5580,31 @@ Error: %@ Palvelintesti epäonnistui! No comment provided by engineer. + + Server type + No comment provided by engineer. + + + Server version is incompatible with network settings. + srv error text + + + Server version is incompatible with your app: %@. + No comment provided by engineer. + Servers Palvelimet No comment provided by engineer. + + Servers info + No comment provided by engineer. + + + Servers statistics will be reset - this cannot be undone! + No comment provided by engineer. + Session code No comment provided by engineer. @@ -4942,6 +5619,10 @@ Error: %@ Aseta kontaktin nimi… No comment provided by engineer. + + Set default theme + No comment provided by engineer. + Set group preferences Aseta ryhmän asetukset @@ -5005,6 +5686,10 @@ Error: %@ Jaa osoite kontakteille? No comment provided by engineer. + + Share from other apps. + No comment provided by engineer. + Share link Jaa linkki @@ -5014,6 +5699,10 @@ Error: %@ Share this 1-time invite link No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts Jaa kontaktien kanssa @@ -5038,16 +5727,32 @@ Error: %@ Näytä viimeiset viestit No comment provided by engineer. + + Show message status + No comment provided by engineer. + + + Show percentage + No comment provided by engineer. + Show preview Näytä esikatselu No comment provided by engineer. + + Show → on messages sent via private routing. + No comment provided by engineer. + Show: Näytä: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address SimpleX-osoite @@ -5120,6 +5825,10 @@ Error: %@ Simplified incognito mode No comment provided by engineer. + + Size + No comment provided by engineer. + Skip Ohita @@ -5135,11 +5844,23 @@ Error: %@ Pienryhmät (max 20) No comment provided by engineer. + + Soft + blur media + + + Some file(s) were not exported: + No comment provided by engineer. + Some non-fatal errors occurred during import - you may see Chat console for more details. Tuonnin aikana tapahtui joitakin ei-vakavia virheitä – saatat nähdä Chat-konsolissa lisätietoja. No comment provided by engineer. + + Some non-fatal errors occurred during import: + No comment provided by engineer. + Somebody Joku @@ -5163,6 +5884,14 @@ Error: %@ Aloita siirto No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop Lopeta @@ -5221,11 +5950,27 @@ Error: %@ Stopping chat No comment provided by engineer. + + Strong + blur media + Submit Lähetä No comment provided by engineer. + + Subscribed + No comment provided by engineer. + + + Subscription errors + No comment provided by engineer. + + + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat SimpleX Chat tuki @@ -5241,6 +5986,10 @@ Error: %@ Järjestelmän todennus No comment provided by engineer. + + TCP connection + No comment provided by engineer. + TCP connection timeout TCP-yhteyden aikakatkaisu @@ -5298,9 +6047,8 @@ Error: %@ Tap to scan No comment provided by engineer. - - Tap to start a new chat - Aloita uusi keskustelu napauttamalla + + Temporary file error No comment provided by engineer. @@ -5355,6 +6103,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Sovellus voi ilmoittaa sinulle, kun saat viestejä tai yhteydenottopyyntöjä - avaa asetukset ottaaksesi ne käyttöön. No comment provided by engineer. + + The app will ask to confirm downloads from unknown file servers (except .onion). + No comment provided by engineer. + The attempt to change database passphrase was not completed. Tietokannan tunnuslauseen muuttamista ei suoritettu loppuun. @@ -5399,6 +6151,14 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Viesti merkitään moderoiduksi kaikille jäsenille. No comment provided by engineer. + + The messages will be deleted for all members. + No comment provided by engineer. + + + The messages will be marked as moderated for all members. + No comment provided by engineer. + The next generation of private messaging Seuraavan sukupolven yksityisviestit @@ -5433,9 +6193,8 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.The text you pasted is not a SimpleX link. No comment provided by engineer. - - Theme - Teema + + Themes No comment provided by engineer. @@ -5497,11 +6256,19 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.This is your own one-time link! No comment provided by engineer. + + This link was used with another mobile device, please create a new link on the desktop. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. Tämä asetus koskee nykyisen keskusteluprofiilisi viestejä *%@**. No comment provided by engineer. + + Title + No comment provided by engineer. + To ask any questions and to receive updates: Voit esittää kysymyksiä ja saada päivityksiä: @@ -5531,6 +6298,10 @@ Tämä voi johtua jostain virheestä tai siitä, että yhteys on vaarantunut.Aikavyöhykkeen suojaamiseksi kuva-/äänitiedostot käyttävät UTC:tä. No comment provided by engineer. + + To protect your IP address, private routing uses your SMP servers to deliver messages. + No comment provided by engineer. + To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5558,15 +6329,31 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote Voit tarkistaa päästä päähän -salauksen kontaktisi kanssa vertaamalla (tai skannaamalla) laitteidenne koodia. No comment provided by engineer. + + Toggle chat list: + No comment provided by engineer. + Toggle incognito when connecting. No comment provided by engineer. + + Toolbar opacity + No comment provided by engineer. + + + Total + No comment provided by engineer. + Transport isolation Kuljetuksen eristäminen No comment provided by engineer. + + Transport sessions + No comment provided by engineer. + Trying to connect to the server used to receive messages from this contact (error: %@). Yritetään muodostaa yhteyttä palvelimeen, jota käytetään tämän kontaktin viestien vastaanottamiseen (virhe: %@). @@ -5616,11 +6403,6 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote Unblock member? No comment provided by engineer. - - Unexpected error: %@ - Odottamaton virhe: %@ - item status description - Unexpected migration state Odottamaton siirtotila @@ -5629,7 +6411,7 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote Unfav. Epäsuotuisa. - No comment provided by engineer. + swipe action Unhide @@ -5666,6 +6448,10 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote Tuntematon virhe No comment provided by engineer. + + Unknown servers! + No comment provided by engineer. + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. Ellet käytä iOS:n puhelinkäyttöliittymää, ota Älä häiritse -tila käyttöön keskeytysten välttämiseksi. @@ -5699,12 +6485,12 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Unmute Poista mykistys - No comment provided by engineer. + swipe action Unread Lukematon - No comment provided by engineer. + swipe action Up to 100 last messages are sent to new members. @@ -5715,11 +6501,6 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Päivitä No comment provided by engineer. - - Update .onion hosts setting? - Päivitä .onion-isäntien asetus? - No comment provided by engineer. - Update database passphrase Päivitä tietokannan tunnuslause @@ -5730,9 +6511,8 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Päivitä verkkoasetukset? No comment provided by engineer. - - Update transport isolation mode? - Päivitä kuljetuksen eristystila? + + Update settings? No comment provided by engineer. @@ -5740,16 +6520,15 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Asetusten päivittäminen yhdistää asiakkaan uudelleen kaikkiin palvelimiin. No comment provided by engineer. - - Updating this setting will re-connect the client to all servers. - Tämän asetuksen päivittäminen yhdistää asiakkaan uudelleen kaikkiin palvelimiin. - No comment provided by engineer. - Upgrade and open chat Päivitä ja avaa keskustelu No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed No comment provided by engineer. @@ -5759,6 +6538,14 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Lataa tiedosto server test step + + Uploaded + No comment provided by engineer. + + + Uploaded files + No comment provided by engineer. + Uploading archive No comment provided by engineer. @@ -5806,6 +6593,14 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Use only local notifications? No comment provided by engineer. + + Use private routing with unknown servers when IP address is not protected. + No comment provided by engineer. + + + Use private routing with unknown servers. + No comment provided by engineer. + Use server Käytä palvelinta @@ -5815,14 +6610,17 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Use the app while in the call. No comment provided by engineer. + + Use the app with one hand. + No comment provided by engineer. + User profile Käyttäjäprofiili No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - .onion-isäntien käyttäminen vaatii yhteensopivan VPN-palveluntarjoajan. + + User selection No comment provided by engineer. @@ -5946,6 +6744,14 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Odottaa videota No comment provided by engineer. + + Wallpaper accent + No comment provided by engineer. + + + Wallpaper background + No comment provided by engineer. + Warning: starting chat on multiple devices is not supported and will cause message delivery failures No comment provided by engineer. @@ -6023,19 +6829,34 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja With reduced battery usage. No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + No comment provided by engineer. + Wrong database passphrase Väärä tietokannan tunnuslause No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + file error text + Wrong passphrase! Väärä tunnuslause! No comment provided by engineer. - - XFTP servers - XFTP-palvelimet + + XFTP server No comment provided by engineer. @@ -6106,11 +6927,19 @@ Repeat join request? Sinut on kutsuttu ryhmään No comment provided by engineer. + + You are not connected to these servers. Private routing is used to deliver messages to them. + No comment provided by engineer. + You can accept calls from lock screen, without device and app authentication. Voit vastaanottaa puheluita lukitusnäytöltä ilman laitteen ja sovelluksen todennusta. No comment provided by engineer. + + You can change it in Appearance settings. + No comment provided by engineer. + You can create it later Voit luoda sen myöhemmin @@ -6139,11 +6968,15 @@ Repeat join request? You can make it visible to your SimpleX contacts via Settings. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Voit nyt lähettää viestejä %@:lle notification body + + You can send messages to %@ from Archived contacts. + No comment provided by engineer. + You can set lock screen notification preview via settings. Voit määrittää lukitusnäytön ilmoituksen esikatselun asetuksista. @@ -6169,6 +7002,10 @@ Repeat join request? Voit aloittaa keskustelun sovelluksen Asetukset / Tietokanta kautta tai käynnistämällä sovelluksen uudelleen No comment provided by engineer. + + You can still view conversation with %@ in the list of chats. + No comment provided by engineer. + You can turn on SimpleX Lock via Settings. Voit ottaa SimpleX Lockin käyttöön Asetusten kautta. @@ -6207,11 +7044,6 @@ Repeat join request? Repeat connection request? No comment provided by engineer. - - You have no chats - Sinulla ei ole keskusteluja - No comment provided by engineer. - You have to enter passphrase every time the app starts - it is not stored on the device. Sinun on annettava tunnuslause aina, kun sovellus käynnistyy - sitä ei tallenneta laitteeseen. @@ -6232,11 +7064,23 @@ Repeat connection request? Liityit tähän ryhmään. Muodostetaan yhteyttä ryhmän jäsenten kutsumiseksi. No comment provided by engineer. + + You may migrate the exported database. + No comment provided by engineer. + + + You may save the exported archive. + No comment provided by engineer. + You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. Sinun tulee käyttää keskustelujen-tietokannan uusinta versiota AINOSTAAN yhdessä laitteessa, muuten saatat lakata vastaanottamasta viestejä joiltakin kontakteilta. No comment provided by engineer. + + You need to allow your contact to call to be able to call them. + No comment provided by engineer. + You need to allow your contact to send voice messages to be able to send them. Sinun on sallittava kontaktiesi lähettää ääniviestejä, jotta voit lähettää niitä. @@ -6350,13 +7194,6 @@ Repeat connection request? Keskusteluprofiilisi No comment provided by engineer. - - Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link). - Kontaktin tulee olla online-tilassa, jotta yhteys voidaan muodostaa. -Voit peruuttaa tämän yhteyden ja poistaa kontaktin (ja yrittää myöhemmin uudella linkillä). - No comment provided by engineer. - Your contact sent a file that is larger than currently supported maximum size (%@). Yhteyshenkilösi lähetti tiedoston, joka on suurempi kuin tällä hetkellä tuettu enimmäiskoko (%@). @@ -6500,6 +7337,10 @@ SimpleX-palvelimet eivät näe profiiliasi. and %lld other events No comment provided by engineer. + + attempts + No comment provided by engineer. + audio call (not e2e encrypted) äänipuhelu (ei e2e-salattu) @@ -6536,6 +7377,10 @@ SimpleX-palvelimet eivät näe profiiliasi. lihavoitu No comment provided by engineer. + + call + No comment provided by engineer. + call error soittovirhe @@ -6684,6 +7529,10 @@ SimpleX-palvelimet eivät näe profiiliasi. päivää time unit + + decryption errors + No comment provided by engineer. + default (%@) oletusarvo (%@) @@ -6733,6 +7582,10 @@ SimpleX-palvelimet eivät näe profiiliasi. päällekkäinen viesti integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted e2e-salattu @@ -6813,6 +7666,10 @@ SimpleX-palvelimet eivät näe profiiliasi. tapahtuma tapahtui No comment provided by engineer. + + expired + No comment provided by engineer. + forwarded No comment provided by engineer. @@ -6842,6 +7699,10 @@ SimpleX-palvelimet eivät näe profiiliasi. iOS-Avainnippua käytetään tunnuslauseen turvalliseen tallentamiseen sen muuttamisen tai sovelluksen uudelleen käynnistämisen jälkeen - se mahdollistaa push-ilmoitusten vastaanottamisen. No comment provided by engineer. + + inactive + No comment provided by engineer. + incognito via contact address link incognito kontaktilinkin kautta @@ -6882,6 +7743,10 @@ SimpleX-palvelimet eivät näe profiiliasi. kutsu ryhmään %@ group name + + invite + No comment provided by engineer. + invited kutsuttu @@ -6936,6 +7801,10 @@ SimpleX-palvelimet eivät näe profiiliasi. yhdistetty rcv group event chat item + + message + No comment provided by engineer. + message received viesti vastaanotettu @@ -6966,6 +7835,10 @@ SimpleX-palvelimet eivät näe profiiliasi. kuukautta time unit + + mute + No comment provided by engineer. + never ei koskaan @@ -7018,6 +7891,14 @@ SimpleX-palvelimet eivät näe profiiliasi. päällä group pref value + + other + No comment provided by engineer. + + + other errors + No comment provided by engineer. + owner omistaja @@ -7082,6 +7963,10 @@ SimpleX-palvelimet eivät näe profiiliasi. saved from %@ No comment provided by engineer. + + search + No comment provided by engineer. + sec sek @@ -7106,6 +7991,12 @@ SimpleX-palvelimet eivät näe profiiliasi. send direct message No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + queue info + set new contact address profile update event chat item @@ -7142,10 +8033,22 @@ SimpleX-palvelimet eivät näe profiiliasi. tuntematon connection info + + unknown servers + No comment provided by engineer. + unknown status No comment provided by engineer. + + unmute + No comment provided by engineer. + + + unprotected + No comment provided by engineer. + updated group profile päivitetty ryhmäprofiili @@ -7184,6 +8087,10 @@ SimpleX-palvelimet eivät näe profiiliasi. releellä No comment provided by engineer. + + video + No comment provided by engineer. + video call (not e2e encrypted) videopuhelu (ei e2e-salattu) @@ -7209,6 +8116,10 @@ SimpleX-palvelimet eivät näe profiiliasi. viikkoa time unit + + when IP hidden + No comment provided by engineer. + yes kyllä @@ -7290,7 +8201,7 @@ SimpleX-palvelimet eivät näe profiiliasi.
- +
@@ -7326,7 +8237,7 @@ SimpleX-palvelimet eivät näe profiiliasi.
- +
@@ -7346,4 +8257,178 @@ SimpleX-palvelimet eivät näe profiiliasi.
+ +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + No comment provided by engineer. + + + App is locked! + No comment provided by engineer. + + + Cancel + No comment provided by engineer. + + + Cannot access keychain to save database password + No comment provided by engineer. + + + Cannot forward message + No comment provided by engineer. + + + Comment + No comment provided by engineer. + + + Currently maximum supported file size is %@. + No comment provided by engineer. + + + Database downgrade required + No comment provided by engineer. + + + Database encrypted! + No comment provided by engineer. + + + Database error + No comment provided by engineer. + + + Database passphrase is different from saved in the keychain. + No comment provided by engineer. + + + Database passphrase is required to open chat. + No comment provided by engineer. + + + Database upgrade required + No comment provided by engineer. + + + Error preparing file + No comment provided by engineer. + + + Error preparing message + No comment provided by engineer. + + + Error: %@ + No comment provided by engineer. + + + File error + No comment provided by engineer. + + + Incompatible database version + No comment provided by engineer. + + + Invalid migration confirmation + No comment provided by engineer. + + + Keychain error + No comment provided by engineer. + + + Large file! + No comment provided by engineer. + + + No active profile + No comment provided by engineer. + + + Ok + No comment provided by engineer. + + + Open the app to downgrade the database. + No comment provided by engineer. + + + Open the app to upgrade the database. + No comment provided by engineer. + + + Passphrase + No comment provided by engineer. + + + Please create a profile in the SimpleX app + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending a message takes longer than expected. + No comment provided by engineer. + + + Sending message… + No comment provided by engineer. + + + Share + No comment provided by engineer. + + + Slow network? + No comment provided by engineer. + + + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + No comment provided by engineer. + + + Wait + No comment provided by engineer. + + + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/fi.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/fi.xcloc/contents.json b/apps/ios/SimpleX Localizations/fi.xcloc/contents.json index 0e3ae6dc56..78ce40cec5 100644 --- a/apps/ios/SimpleX Localizations/fi.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/fi.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "fi", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "15F31d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "15.4" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index ac7fc8cc8b..c05098980e 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -2,7 +2,7 @@
- +
@@ -127,11 +127,6 @@ %@ est vérifié·e No comment provided by engineer. - - %@ servers - Serveurs %@ - No comment provided by engineer. - %@ uploaded %@ envoyé @@ -529,7 +524,7 @@ Abort - Annuler + Abandonner No comment provided by engineer. @@ -557,16 +552,17 @@ À propos de l'adresse SimpleX No comment provided by engineer. - - Accent color - Couleur principale + + Accent + Principale No comment provided by engineer. Accept Accepter accept contact request via notification - accept incoming call via notification + accept incoming call via notification + swipe action Accept connection request? @@ -581,7 +577,23 @@ Accept incognito Accepter en incognito - accept contact request via notification + accept contact request via notification + swipe action + + + Acknowledged + Reçu avec accusé de réception + No comment provided by engineer. + + + Acknowledgement errors + Erreur d'accusé de réception + No comment provided by engineer. + + + Active connections + Connections actives + No comment provided by engineer. Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. @@ -603,16 +615,16 @@ Ajouter un profil No comment provided by engineer. + + Add server + Ajouter un serveur + No comment provided by engineer. + Add servers by scanning QR codes. Ajoutez des serveurs en scannant des codes QR. No comment provided by engineer. - - Add server… - Ajouter un serveur… - No comment provided by engineer. - Add to another device Ajouter à un autre appareil @@ -623,6 +635,21 @@ Ajouter un message d'accueil No comment provided by engineer. + + Additional accent + Accent additionnel + No comment provided by engineer. + + + Additional accent 2 + Accent additionnel 2 + No comment provided by engineer. + + + Additional secondary + Accent secondaire + No comment provided by engineer. + Address Adresse @@ -648,6 +675,11 @@ Paramètres réseau avancés No comment provided by engineer. + + Advanced settings + Paramètres avancés + No comment provided by engineer. + All app data is deleted. Toutes les données de l'application sont supprimées. @@ -663,6 +695,11 @@ Toutes les données sont effacées lorsqu'il est saisi. No comment provided by engineer. + + All data is private to your device. + Toutes les données restent confinées dans votre appareil. + No comment provided by engineer. + All group members will remain connected. Tous les membres du groupe resteront connectés. @@ -683,6 +720,11 @@ Tous les nouveaux messages de %@ seront cachés ! No comment provided by engineer. + + All profiles + Tous les profiles + No comment provided by engineer. + All your contacts will remain connected. Tous vos contacts resteront connectés. @@ -708,11 +750,21 @@ Autoriser les appels que si votre contact les autorise. No comment provided by engineer. + + Allow calls? + Autoriser les appels ? + No comment provided by engineer. + Allow disappearing messages only if your contact allows it to you. Autorise les messages éphémères seulement si votre contact vous l’autorise. No comment provided by engineer. + + Allow downgrade + Autoriser la rétrogradation + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Autoriser la suppression irréversible des messages uniquement si votre contact vous l'autorise. (24 heures) @@ -738,6 +790,11 @@ Autorise l’envoi de messages éphémères. No comment provided by engineer. + + Allow sharing + Autoriser le partage + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Autoriser la suppression irréversible de messages envoyés. (24 heures) @@ -808,6 +865,11 @@ Groupe déjà rejoint ! No comment provided by engineer. + + Always use private routing. + Toujours utiliser le routage privé. + No comment provided by engineer. + Always use relay Se connecter via relais @@ -873,9 +935,24 @@ Appliquer No comment provided by engineer. + + Apply to + Appliquer à + No comment provided by engineer. + Archive and upload - Archiver et transférer + Archiver et téléverser + No comment provided by engineer. + + + Archive contacts to chat later. + Archiver les contacts pour discuter plus tard. + No comment provided by engineer. + + + Archived contacts + Contacts archivés No comment provided by engineer. @@ -948,6 +1025,11 @@ Retour No comment provided by engineer. + + Background + Fond + No comment provided by engineer. + Bad desktop address Mauvaise adresse de bureau @@ -973,6 +1055,16 @@ Meilleurs messages No comment provided by engineer. + + Better networking + Meilleure gestion de réseau + No comment provided by engineer. + + + Black + Noir + No comment provided by engineer. + Block Bloquer @@ -1008,6 +1100,16 @@ Bloqué par l'administrateur No comment provided by engineer. + + Blur for better privacy. + Rendez les images floues et protégez-les contre les regards indiscrets. + No comment provided by engineer. + + + Blur media + Flouter les médias + No comment provided by engineer. + Both you and your contact can add message reactions. Vous et votre contact pouvez ajouter des réactions aux messages. @@ -1053,11 +1155,26 @@ Appels No comment provided by engineer. + + Calls prohibited! + Les appels ne sont pas autorisés ! + No comment provided by engineer. + Camera not available Caméra non disponible No comment provided by engineer. + + Can't call contact + Impossible d'appeler le contact + No comment provided by engineer. + + + Can't call member + Impossible d'appeler le membre + No comment provided by engineer. + Can't invite contact! Impossible d'inviter le contact ! @@ -1068,6 +1185,11 @@ Impossible d'inviter les contacts ! No comment provided by engineer. + + Can't message member + Impossible d'envoyer un message à ce membre + No comment provided by engineer. + Cancel Annuler @@ -1083,11 +1205,21 @@ Impossible d'accéder à la keychain pour enregistrer le mot de passe de la base de données No comment provided by engineer. + + Cannot forward message + Impossible de transférer le message + No comment provided by engineer. + Cannot receive file Impossible de recevoir le fichier No comment provided by engineer. + + Capacity exceeded - recipient did not receive previously sent messages. + Capacité dépassée - le destinataire n'a pas pu recevoir les messages envoyés précédemment. + snd error text + Cellular Cellulaire @@ -1149,6 +1281,11 @@ Archives du chat No comment provided by engineer. + + Chat colors + Couleurs de chat + No comment provided by engineer. + Chat console Console du chat @@ -1164,6 +1301,11 @@ Base de données du chat supprimée No comment provided by engineer. + + Chat database exported + Exportation de la base de données des discussions + No comment provided by engineer. + Chat database imported Base de données du chat importée @@ -1184,6 +1326,11 @@ Le chat est arrêté. Si vous avez déjà utilisé cette base de données sur un autre appareil, vous devez la transférer à nouveau avant de démarrer le chat. No comment provided by engineer. + + Chat list + Liste de discussion + No comment provided by engineer. + Chat migrated! Messagerie transférée ! @@ -1194,6 +1341,11 @@ Préférences de chat No comment provided by engineer. + + Chat theme + Thème de chat + No comment provided by engineer. + Chats Discussions @@ -1224,10 +1376,25 @@ Choisir dans la photothèque No comment provided by engineer. + + Chunks deleted + Chunks supprimés + No comment provided by engineer. + + + Chunks downloaded + Chunks téléchargés + No comment provided by engineer. + + + Chunks uploaded + Chunks téléversés + No comment provided by engineer. + Clear Effacer - No comment provided by engineer. + swipe action Clear conversation @@ -1249,9 +1416,14 @@ Retirer la vérification No comment provided by engineer. - - Colors - Couleurs + + Color chats with the new themes. + Colorez vos discussions avec les nouveaux thèmes. + No comment provided by engineer. + + + Color mode + Mode de couleur No comment provided by engineer. @@ -1264,11 +1436,21 @@ Comparez les codes de sécurité avec vos contacts. No comment provided by engineer. + + Completed + Complétées + No comment provided by engineer. + Configure ICE servers Configurer les serveurs ICE No comment provided by engineer. + + Configured %@ servers + %@ serveurs configurés + No comment provided by engineer. + Confirm Confirmer @@ -1279,11 +1461,21 @@ Confirmer le code d'accès No comment provided by engineer. + + Confirm contact deletion? + Confirmer la suppression du contact ? + No comment provided by engineer. + Confirm database upgrades Confirmer la mise à niveau de la base de données No comment provided by engineer. + + Confirm files from unknown servers. + Confirmer les fichiers provenant de serveurs inconnus. + No comment provided by engineer. + Confirm network settings Confirmer les paramètres réseau @@ -1329,6 +1521,11 @@ Connexion au bureau No comment provided by engineer. + + Connect to your friends faster. + Connectez-vous à vos amis plus rapidement. + No comment provided by engineer. + Connect to yourself? Se connecter à soi-même ? @@ -1368,16 +1565,31 @@ Il s'agit de votre propre lien unique ! Se connecter avec %@ No comment provided by engineer. + + Connected + Connecté + No comment provided by engineer. + Connected desktop Bureau connecté No comment provided by engineer. + + Connected servers + Serveurs connectés + No comment provided by engineer. + Connected to desktop Connecté au bureau No comment provided by engineer. + + Connecting + Connexion + No comment provided by engineer. + Connecting to server… Connexion au serveur… @@ -1388,6 +1600,11 @@ Il s'agit de votre propre lien unique ! Connexion au serveur… (erreur : %@) No comment provided by engineer. + + Connecting to contact, please wait or check later! + Connexion au contact, veuillez patienter ou vérifier plus tard ! + No comment provided by engineer. + Connecting to desktop Connexion au bureau @@ -1398,6 +1615,11 @@ Il s'agit de votre propre lien unique ! Connexion No comment provided by engineer. + + Connection and servers status. + État de la connexion et des serveurs. + No comment provided by engineer. + Connection error Erreur de connexion @@ -1408,6 +1630,11 @@ Il s'agit de votre propre lien unique ! Erreur de connexion (AUTH) No comment provided by engineer. + + Connection notifications + Notifications de connexion + No comment provided by engineer. + Connection request sent! Demande de connexion envoyée ! @@ -1423,6 +1650,16 @@ Il s'agit de votre propre lien unique ! Délai de connexion No comment provided by engineer. + + Connection with desktop stopped + La connexion avec le bureau s'est arrêtée + No comment provided by engineer. + + + Connections + Connexions + No comment provided by engineer. + Contact allows Votre contact autorise @@ -1433,6 +1670,11 @@ Il s'agit de votre propre lien unique ! Contact déjà existant No comment provided by engineer. + + Contact deleted! + Contact supprimé ! + No comment provided by engineer. + Contact hidden: Contact masqué : @@ -1443,9 +1685,9 @@ Il s'agit de votre propre lien unique ! Le contact est connecté notification - - Contact is not connected yet! - Le contact n'est pas encore connecté ! + + Contact is deleted. + Le contact est supprimé. No comment provided by engineer. @@ -1458,6 +1700,11 @@ Il s'agit de votre propre lien unique ! Préférences de contact No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + Le contact sera supprimé - il n'est pas possible de revenir en arrière ! + No comment provided by engineer. + Contacts Contacts @@ -1473,10 +1720,20 @@ Il s'agit de votre propre lien unique ! Continuer No comment provided by engineer. + + Conversation deleted! + Conversation supprimée ! + No comment provided by engineer. + Copy Copier - chat item action + No comment provided by engineer. + + + Copy error + Erreur de copie + No comment provided by engineer. Core version: v%@ @@ -1553,6 +1810,11 @@ Il s'agit de votre propre lien unique ! Créez votre profil No comment provided by engineer. + + Created + Créées + No comment provided by engineer. + Created at Créé à @@ -1588,6 +1850,11 @@ Il s'agit de votre propre lien unique ! Phrase secrète actuelle… No comment provided by engineer. + + Current profile + Profil actuel + No comment provided by engineer. + Currently maximum supported file size is %@. Actuellement, la taille maximale des fichiers supportés est de %@. @@ -1598,11 +1865,21 @@ Il s'agit de votre propre lien unique ! Délai personnalisé No comment provided by engineer. + + Customize theme + Personnaliser le thème + No comment provided by engineer. + Dark Sombre No comment provided by engineer. + + Dark mode colors + Couleurs en mode sombre + No comment provided by engineer. + Database ID ID de base de données @@ -1701,6 +1978,11 @@ Il s'agit de votre propre lien unique ! La base de données sera migrée lors du redémarrage de l'app No comment provided by engineer. + + Debug delivery + Livraison de débogage + No comment provided by engineer. + Decentralized Décentralisé @@ -1714,18 +1996,19 @@ Il s'agit de votre propre lien unique ! Delete Supprimer - chat item action + chat item action + swipe action + + + Delete %lld messages of members? + Supprimer %lld messages de membres ? + No comment provided by engineer. Delete %lld messages? Supprimer %lld messages ? No comment provided by engineer. - - Delete Contact - Supprimer le contact - No comment provided by engineer. - Delete address Supprimer l'adresse @@ -1781,11 +2064,9 @@ Il s'agit de votre propre lien unique ! Supprimer le contact No comment provided by engineer. - - Delete contact? -This cannot be undone! - Supprimer le contact ? -Cette opération ne peut être annulée ! + + Delete contact? + Supprimer le contact ? No comment provided by engineer. @@ -1878,11 +2159,6 @@ Cette opération ne peut être annulée ! Supprimer l'ancienne base de données ? No comment provided by engineer. - - Delete pending connection - Supprimer la connexion en attente - No comment provided by engineer. - Delete pending connection? Supprimer la connexion en attente ? @@ -1898,11 +2174,26 @@ Cette opération ne peut être annulée ! Supprimer la file d'attente server test step + + Delete up to 20 messages at once. + Supprimez jusqu'à 20 messages à la fois. + No comment provided by engineer. + Delete user profile? Supprimer le profil utilisateur ? No comment provided by engineer. + + Delete without notification + Supprimer sans notification + No comment provided by engineer. + + + Deleted + Supprimées + No comment provided by engineer. + Deleted at Supprimé à @@ -1913,6 +2204,11 @@ Cette opération ne peut être annulée ! Supprimé à : %@ copied message info + + Deletion errors + Erreurs de suppression + No comment provided by engineer. + Delivery Distribution @@ -1925,7 +2221,7 @@ Cette opération ne peut être annulée ! Delivery receipts! - Justificatifs de réception! + Justificatifs de réception ! No comment provided by engineer. @@ -1948,11 +2244,41 @@ Cette opération ne peut être annulée ! Appareils de bureau No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + L'adresse du serveur de destination %@ est incompatible avec les paramètres du serveur de redirection %@. + No comment provided by engineer. + + + Destination server error: %@ + Erreur du serveur de destination : %@ + snd error text + + + Destination server version of %@ is incompatible with forwarding server %@. + La version du serveur de destination %@ est incompatible avec le serveur de redirection %@. + No comment provided by engineer. + + + Detailed statistics + Statistiques détaillées + No comment provided by engineer. + + + Details + Détails + No comment provided by engineer. + Develop Développer No comment provided by engineer. + + Developer options + Options pour les développeurs + No comment provided by engineer. + Developer tools Outils du développeur @@ -2003,6 +2329,11 @@ Cette opération ne peut être annulée ! Désactiver pour tous No comment provided by engineer. + + Disabled + Désactivé + No comment provided by engineer. + Disappearing message Message éphémère @@ -2053,11 +2384,21 @@ Cette opération ne peut être annulée ! Rechercher sur le réseau No comment provided by engineer. + + Do NOT send messages directly, even if your or destination server does not support private routing. + Ne pas envoyer de messages directement, même si votre serveur ou le serveur de destination ne prend pas en charge le routage privé. + No comment provided by engineer. + Do NOT use SimpleX for emergency calls. N'utilisez PAS SimpleX pour les appels d'urgence. No comment provided by engineer. + + Do NOT use private routing. + Ne pas utiliser de routage privé. + No comment provided by engineer. + Do it later Faites-le plus tard @@ -2093,6 +2434,11 @@ Cette opération ne peut être annulée ! Télécharger chat item action + + Download errors + Erreurs de téléchargement + No comment provided by engineer. + Download failed Échec du téléchargement @@ -2103,6 +2449,16 @@ Cette opération ne peut être annulée ! Télécharger le fichier server test step + + Downloaded + Téléchargé + No comment provided by engineer. + + + Downloaded files + Fichiers téléchargés + No comment provided by engineer. + Downloading archive Téléchargement de l'archive @@ -2203,6 +2559,11 @@ Cette opération ne peut être annulée ! Activer le code d'autodestruction set passcode view + + Enabled + Activé + No comment provided by engineer. + Enabled for Activé pour @@ -2373,6 +2734,11 @@ Cette opération ne peut être annulée ! Erreur de changement de paramètre No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + Erreur de connexion au serveur de redirection %@. Veuillez réessayer plus tard. + No comment provided by engineer. + Error creating address Erreur lors de la création de l'adresse @@ -2423,11 +2789,6 @@ Cette opération ne peut être annulée ! Erreur lors de la suppression de la connexion No comment provided by engineer. - - Error deleting contact - Erreur lors de la suppression du contact - No comment provided by engineer. - Error deleting database Erreur lors de la suppression de la base de données @@ -2473,6 +2834,11 @@ Cette opération ne peut être annulée ! Erreur lors de l'exportation de la base de données du chat No comment provided by engineer. + + Error exporting theme: %@ + Erreur d'exportation du thème : %@ + No comment provided by engineer. + Error importing chat database Erreur lors de l'importation de la base de données du chat @@ -2498,11 +2864,26 @@ Cette opération ne peut être annulée ! Erreur lors de la réception du fichier No comment provided by engineer. + + Error reconnecting server + Erreur de reconnexion du serveur + No comment provided by engineer. + + + Error reconnecting servers + Erreur de reconnexion des serveurs + No comment provided by engineer. + Error removing member Erreur lors de la suppression d'un membre No comment provided by engineer. + + Error resetting statistics + Erreur de réinitialisation des statistiques + No comment provided by engineer. + Error saving %@ servers Erreur lors de la sauvegarde des serveurs %@ @@ -2621,7 +3002,8 @@ Cette opération ne peut être annulée ! Error: %@ Erreur : %@ - No comment provided by engineer. + file error text + snd error text Error: URL is invalid @@ -2633,6 +3015,11 @@ Cette opération ne peut être annulée ! Erreur : pas de fichier de base de données No comment provided by engineer. + + Errors + Erreurs + No comment provided by engineer. + Even when disabled in the conversation. Même s'il est désactivé dans la conversation. @@ -2658,6 +3045,11 @@ Cette opération ne peut être annulée ! Erreur lors de l'exportation : No comment provided by engineer. + + Export theme + Exporter le thème + No comment provided by engineer. + Exported database archive. Archive de la base de données exportée. @@ -2691,8 +3083,33 @@ Cette opération ne peut être annulée ! Favorite Favoris + swipe action + + + File error + Erreur de fichier No comment provided by engineer. + + File not found - most likely file was deleted or cancelled. + Fichier introuvable - le fichier a probablement été supprimé ou annulé. + file error text + + + File server error: %@ + Erreur de serveur de fichiers : %@ + file error text + + + File status + Statut du fichier + No comment provided by engineer. + + + File status: %@ + Statut du fichier : %@ + copied message info + File will be deleted from servers. Le fichier sera supprimé des serveurs. @@ -2713,6 +3130,11 @@ Cette opération ne peut être annulée ! Fichier : %@ No comment provided by engineer. + + Files + Fichiers + No comment provided by engineer. + Files & media Fichiers & médias @@ -2775,7 +3197,7 @@ Cette opération ne peut être annulée ! Fix connection? - Réparer la connexion? + Réparer la connexion ? No comment provided by engineer. @@ -2818,6 +3240,35 @@ Cette opération ne peut être annulée ! Transféré depuis No comment provided by engineer. + + Forwarding server %@ failed to connect to destination server %@. Please try later. + Le serveur de redirection %@ n'a pas réussi à se connecter au serveur de destination %@. Veuillez réessayer plus tard. + No comment provided by engineer. + + + Forwarding server address is incompatible with network settings: %@. + L'adresse du serveur de redirection est incompatible avec les paramètres du réseau : %@. + No comment provided by engineer. + + + Forwarding server version is incompatible with network settings: %@. + La version du serveur de redirection est incompatible avec les paramètres du réseau : %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + Serveur de transfert : %1$@ +Erreur du serveur de destination : %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + Serveur de transfert : %1$@ +Erreur : %2$@ + snd error text + Found desktop Bureau trouvé @@ -2863,6 +3314,16 @@ Cette opération ne peut être annulée ! GIFs et stickers No comment provided by engineer. + + Good afternoon! + Bonjour ! + message preview + + + Good morning! + Bonjour ! + message preview + Group Groupe @@ -3143,6 +3604,11 @@ Cette opération ne peut être annulée ! Échec de l'importation No comment provided by engineer. + + Import theme + Importer un thème + No comment provided by engineer. + Importing archive Importation de l'archive @@ -3265,6 +3731,11 @@ Cette opération ne peut être annulée ! Interface No comment provided by engineer. + + Interface colors + Couleurs d'interface + No comment provided by engineer. + Invalid QR code Code QR invalide @@ -3366,6 +3837,11 @@ Cette opération ne peut être annulée ! 3. La connexion a été compromise. No comment provided by engineer. + + It protects your IP address and connections. + Il protège votre adresse IP et vos connexions. + No comment provided by engineer. + It seems like you are already connected via this link. If it is not the case, there was an error (%@). Il semblerait que vous êtes déjà connecté via ce lien. Si ce n'est pas le cas, il y a eu une erreur (%@). @@ -3384,7 +3860,7 @@ Cette opération ne peut être annulée ! Join Rejoindre - No comment provided by engineer. + swipe action Join group @@ -3428,6 +3904,11 @@ Voici votre lien pour le groupe %@ ! Conserver No comment provided by engineer. + + Keep conversation + Garder la conversation + No comment provided by engineer. + Keep the app open to use it from desktop Garder l'application ouverte pour l'utiliser depuis le bureau @@ -3471,7 +3952,7 @@ Voici votre lien pour le groupe %@ ! Leave Quitter - No comment provided by engineer. + swipe action Leave group @@ -3603,11 +4084,26 @@ Voici votre lien pour le groupe %@ ! Max 30 secondes, réception immédiate. No comment provided by engineer. + + Media & file servers + Serveurs de fichiers et de médias + No comment provided by engineer. + + + Medium + Modéré + blur media + Member Membre No comment provided by engineer. + + Member inactive + Membre inactif + item status text + Member role will be changed to "%@". All group members will be notified. Le rôle du membre sera changé pour "%@". Tous les membres du groupe en seront informés. @@ -3623,6 +4119,11 @@ Voici votre lien pour le groupe %@ ! Ce membre sera retiré du groupe - impossible de revenir en arrière ! No comment provided by engineer. + + Menus + Menus + No comment provided by engineer. + Message delivery error Erreur de distribution du message @@ -3633,11 +4134,31 @@ Voici votre lien pour le groupe %@ ! Accusés de réception des messages ! No comment provided by engineer. + + Message delivery warning + Avertissement sur la distribution des messages + item status text + Message draft Brouillon de message No comment provided by engineer. + + Message forwarded + Message transféré + item status text + + + Message may be delivered later if member becomes active. + Le message peut être transmis plus tard si le membre devient actif. + item status description + + + Message queue info + Informations sur la file d'attente des messages + No comment provided by engineer. + Message reactions Réactions aux messages @@ -3653,11 +4174,31 @@ Voici votre lien pour le groupe %@ ! Les réactions aux messages sont interdites dans ce groupe. No comment provided by engineer. + + Message reception + Réception de message + No comment provided by engineer. + + + Message servers + Serveurs de messages + No comment provided by engineer. + Message source remains private. La source du message reste privée. No comment provided by engineer. + + Message status + Statut du message + No comment provided by engineer. + + + Message status: %@ + Statut du message : %@ + copied message info + Message text Texte du message @@ -3683,6 +4224,16 @@ Voici votre lien pour le groupe %@ ! Les messages de %@ seront affichés ! No comment provided by engineer. + + Messages received + Messages reçus + No comment provided by engineer. + + + Messages sent + Messages envoyés + No comment provided by engineer. + Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. Les messages, fichiers et appels sont protégés par un chiffrement **de bout en bout** avec une confidentialité persistante, une répudiation et une récupération en cas d'effraction. @@ -3783,11 +4334,6 @@ Voici votre lien pour le groupe %@ ! Connexion probablement supprimée. item status description - - Most likely this contact has deleted the connection with you. - Il est fort probable que ce contact ait supprimé la connexion avec vous. - No comment provided by engineer. - Multiple chat profiles Différents profils de chat @@ -3796,7 +4342,7 @@ Voici votre lien pour le groupe %@ ! Mute Muet - No comment provided by engineer. + swipe action Muted when inactive! @@ -3806,7 +4352,7 @@ Voici votre lien pour le groupe %@ ! Name Nom - No comment provided by engineer. + swipe action Network & servers @@ -3818,6 +4364,11 @@ Voici votre lien pour le groupe %@ ! Connexion au réseau No comment provided by engineer. + + Network issues - message expired after many attempts to send it. + Problèmes de réseau - le message a expiré après plusieurs tentatives d'envoi. + snd error text + Network management Gestion du réseau @@ -3843,6 +4394,11 @@ Voici votre lien pour le groupe %@ ! Nouveau chat No comment provided by engineer. + + New chat experience 🎉 + Nouvelle expérience de discussion 🎉 + No comment provided by engineer. + New contact request Nouvelle demande de contact @@ -3873,6 +4429,11 @@ Voici votre lien pour le groupe %@ ! Nouveautés de la %@ No comment provided by engineer. + + New media options + Nouvelles options de médias + No comment provided by engineer. + New member role Nouveau rôle @@ -3918,6 +4479,11 @@ Voici votre lien pour le groupe %@ ! Pas de token d'appareil ! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + Pas de connexion directe pour l'instant, le message est transmis par l'administrateur. + item status description + No filtered chats Aucune discussion filtrés @@ -3933,6 +4499,11 @@ Voici votre lien pour le groupe %@ ! Aucun historique No comment provided by engineer. + + No info, try to reload + Pas d'info, essayez de recharger + No comment provided by engineer. + No network connection Pas de connexion au réseau @@ -3953,6 +4524,11 @@ Voici votre lien pour le groupe %@ ! Non compatible ! No comment provided by engineer. + + Nothing selected + Aucune sélection + No comment provided by engineer. + Notifications Notifications @@ -3980,7 +4556,7 @@ Voici votre lien pour le groupe %@ ! Off Off - No comment provided by engineer. + blur media Ok @@ -4002,14 +4578,18 @@ Voici votre lien pour le groupe %@ ! Lien d'invitation unique No comment provided by engineer. - - Onion hosts will be required for connection. Requires enabling VPN. - Les hôtes .onion seront nécessaires pour la connexion. Nécessite l'activation d'un VPN. + + Onion hosts will be **required** for connection. +Requires compatible VPN. + Les hôtes .onion seront **nécessaires** pour la connexion. +Nécessite l'activation d'un VPN. No comment provided by engineer. - - Onion hosts will be used when available. Requires enabling VPN. - Les hôtes .onion seront utilisés dès que possible. Nécessite l'activation d'un VPN. + + Onion hosts will be used when available. +Requires compatible VPN. + Les hôtes .onion seront utilisés dès que possible. +Nécessite l'activation d'un VPN. No comment provided by engineer. @@ -4022,6 +4602,11 @@ Voici votre lien pour le groupe %@ ! Seuls les appareils clients stockent les profils des utilisateurs, les contacts, les groupes et les messages envoyés avec un **chiffrement de bout en bout à deux couches**. No comment provided by engineer. + + Only delete conversation + Ne supprimer que la conversation + No comment provided by engineer. + Only group owners can change group preferences. Seuls les propriétaires du groupe peuvent modifier les préférences du groupe. @@ -4117,6 +4702,11 @@ Voici votre lien pour le groupe %@ ! Ouvrir le transfert vers un autre appareil authentication reason + + Open server settings + Ouvrir les paramètres du serveur + No comment provided by engineer. + Open user profiles Ouvrir les profils d'utilisateurs @@ -4157,6 +4747,11 @@ Voici votre lien pour le groupe %@ ! Autres No comment provided by engineer. + + Other %@ servers + Autres serveurs %@ + No comment provided by engineer. + PING count Nombre de PING @@ -4222,6 +4817,11 @@ Voici votre lien pour le groupe %@ ! Collez le lien que vous avez reçu No comment provided by engineer. + + Pending + En attente + No comment provided by engineer. + People can connect to you only via the links you share. On ne peut se connecter à vous qu’avec les liens que vous partagez. @@ -4242,11 +4842,28 @@ Voici votre lien pour le groupe %@ ! Appels picture-in-picture No comment provided by engineer. + + Play from the chat list. + Aperçu depuis la liste de conversation. + No comment provided by engineer. + + + Please ask your contact to enable calls. + Veuillez demander à votre contact d'autoriser les appels. + No comment provided by engineer. + Please ask your contact to enable sending voice messages. Veuillez demander à votre contact de permettre l'envoi de messages vocaux. No comment provided by engineer. + + Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers. + Veuillez vérifier que le téléphone portable et l'ordinateur de bureau sont connectés au même réseau local et que le pare-feu de l'ordinateur de bureau autorise la connexion. +Veuillez faire part de tout autre problème aux développeurs. + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. Veuillez vérifier que vous avez utilisé le bon lien ou demandez à votre contact de vous en envoyer un autre. @@ -4344,6 +4961,11 @@ Erreur : %@ Aperçu No comment provided by engineer. + + Previously connected servers + Serveurs précédemment connectés + No comment provided by engineer. + Privacy & security Vie privée et sécurité @@ -4359,11 +4981,31 @@ Erreur : %@ Noms de fichiers privés No comment provided by engineer. + + Private message routing + Routage privé des messages + No comment provided by engineer. + + + Private message routing 🚀 + Routage privé des messages 🚀 + No comment provided by engineer. + Private notes Notes privées name of notes to self + + Private routing + Routage privé + No comment provided by engineer. + + + Private routing error + Erreur de routage privé + No comment provided by engineer. + Profile and server connections Profil et connexions au serveur @@ -4394,6 +5036,11 @@ Erreur : %@ Mot de passe de profil No comment provided by engineer. + + Profile theme + Thème de profil + No comment provided by engineer. + Profile update will be sent to your contacts. La mise à jour du profil sera envoyée à vos contacts. @@ -4444,11 +5091,23 @@ Erreur : %@ Interdire l'envoi de messages vocaux. No comment provided by engineer. + + Protect IP address + Protéger l'adresse IP + No comment provided by engineer. + Protect app screen Protéger l'écran de l'app No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + Protégez votre adresse IP des relais de messagerie choisis par vos contacts. +Activez-le dans les paramètres *Réseau et serveurs*. + No comment provided by engineer. + Protect your chat profiles with a password! Protégez vos profils de chat par un mot de passe ! @@ -4464,6 +5123,16 @@ Erreur : %@ Délai d'attente du protocole par KB No comment provided by engineer. + + Proxied + Routé via un proxy + No comment provided by engineer. + + + Proxied servers + Serveurs routés via des proxy + No comment provided by engineer. + Push notifications Notifications push @@ -4484,6 +5153,11 @@ Erreur : %@ Évaluer l'app No comment provided by engineer. + + Reachable chat toolbar + Barre d'outils accessible + No comment provided by engineer. + React… Réagissez… @@ -4492,7 +5166,7 @@ Erreur : %@ Read Lire - No comment provided by engineer. + swipe action Read more @@ -4529,6 +5203,11 @@ Erreur : %@ Les accusés de réception sont désactivés No comment provided by engineer. + + Receive errors + Erreurs reçues + No comment provided by engineer. + Received at Reçu à @@ -4549,16 +5228,26 @@ Erreur : %@ Message reçu message info title + + Received messages + Messages reçus + No comment provided by engineer. + + + Received reply + Réponse reçue + No comment provided by engineer. + + + Received total + Total reçu + No comment provided by engineer. + Receiving address will be changed to a different server. Address change will complete after sender comes online. L'adresse de réception sera changée pour un autre serveur. Le changement d'adresse sera terminé lorsque l'expéditeur sera en ligne. No comment provided by engineer. - - Receiving concurrency - Réception simultanée - No comment provided by engineer. - Receiving file will be stopped. La réception du fichier sera interrompue. @@ -4584,14 +5273,39 @@ Erreur : %@ Les destinataires voient les mises à jour au fur et à mesure que vous leur écrivez. No comment provided by engineer. + + Reconnect + Reconnecter + No comment provided by engineer. + Reconnect all connected servers to force message delivery. It uses additional traffic. Reconnecter tous les serveurs connectés pour forcer la livraison des messages. Cette méthode utilise du trafic supplémentaire. No comment provided by engineer. + + Reconnect all servers + Reconnecter tous les serveurs + No comment provided by engineer. + + + Reconnect all servers? + Reconnecter tous les serveurs ? + No comment provided by engineer. + + + Reconnect server to force message delivery. It uses additional traffic. + Reconnecter le serveur pour forcer la livraison des messages. Utilise du trafic supplémentaire. + No comment provided by engineer. + + + Reconnect server? + Reconnecter le serveur ? + No comment provided by engineer. + Reconnect servers? - Reconnecter les serveurs? + Reconnecter les serveurs ? No comment provided by engineer. @@ -4612,7 +5326,8 @@ Erreur : %@ Reject Rejeter - reject incoming call via notification + reject incoming call via notification + swipe action Reject (sender NOT notified) @@ -4639,6 +5354,11 @@ Erreur : %@ Supprimer No comment provided by engineer. + + Remove image + Enlever l'image + No comment provided by engineer. + Remove member Retirer le membre @@ -4666,7 +5386,7 @@ Erreur : %@ Renegotiate encryption? - Renégocier le chiffrement? + Renégocier le chiffrement ? No comment provided by engineer. @@ -4709,16 +5429,41 @@ Erreur : %@ Réinitialisation No comment provided by engineer. + + Reset all hints + Rétablir tous les conseils + No comment provided by engineer. + + + Reset all statistics + Réinitialiser toutes les statistiques + No comment provided by engineer. + + + Reset all statistics? + Réinitialiser toutes les statistiques ? + No comment provided by engineer. + Reset colors Réinitialisation des couleurs No comment provided by engineer. + + Reset to app theme + Réinitialisation au thème de l'appli + No comment provided by engineer. + Reset to defaults Réinitialisation des valeurs par défaut No comment provided by engineer. + + Reset to user theme + Réinitialisation au thème de l'utilisateur + No comment provided by engineer. + Restart the app to create a new chat profile Redémarrez l'application pour créer un nouveau profil de chat @@ -4759,11 +5504,6 @@ Erreur : %@ Révéler chat item action - - Revert - Revenir en arrière - No comment provided by engineer. - Revoke Révoquer @@ -4789,9 +5529,14 @@ Erreur : %@ Exécuter le chat No comment provided by engineer. - - SMP servers - Serveurs SMP + + SMP server + Serveur SMP + No comment provided by engineer. + + + Safely receive files + Réception de fichiers en toute sécurité No comment provided by engineer. @@ -4819,6 +5564,11 @@ Erreur : %@ Enregistrer et en informer les membres du groupe No comment provided by engineer. + + Save and reconnect + Sauvegarder et se reconnecter + No comment provided by engineer. + Save and update group profile Enregistrer et mettre à jour le profil du groupe @@ -4899,6 +5649,16 @@ Erreur : %@ Message enregistré message info title + + Scale + Échelle + No comment provided by engineer. + + + Scan / Paste link + Scanner / Coller le lien + No comment provided by engineer. + Scan QR code Scanner un code QR @@ -4939,11 +5699,21 @@ Erreur : %@ Rechercher ou coller un lien SimpleX No comment provided by engineer. + + Secondary + Secondaire + No comment provided by engineer. + Secure queue File d'attente sécurisée server test step + + Secured + Sécurisées + No comment provided by engineer. + Security assessment Évaluation de sécurité @@ -4957,6 +5727,16 @@ Erreur : %@ Select Choisir + chat item action + + + Selected %lld + %lld sélectionné(s) + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + Les préférences de chat sélectionnées interdisent ce message. No comment provided by engineer. @@ -4994,11 +5774,6 @@ Erreur : %@ Envoyer les accusés de réception à No comment provided by engineer. - - Send direct message - Envoyer un message direct - No comment provided by engineer. - Send direct message to connect Envoyer un message direct pour vous connecter @@ -5009,9 +5784,14 @@ Erreur : %@ Envoyer un message éphémère No comment provided by engineer. + + Send errors + Erreurs d'envoi + No comment provided by engineer. + Send link previews - Envoi d'aperçus de liens + Aperçu des liens No comment provided by engineer. @@ -5019,6 +5799,21 @@ Erreur : %@ Envoyer un message dynamique No comment provided by engineer. + + Send message to enable calls. + Envoyer un message pour activer les appels. + No comment provided by engineer. + + + Send messages directly when IP address is protected and your or destination server does not support private routing. + Envoyer les messages de manière directe lorsque l'adresse IP est protégée et que votre serveur ou le serveur de destination ne prend pas en charge le routage privé. + No comment provided by engineer. + + + Send messages directly when your or destination server does not support private routing. + Envoyez les messages de manière directe lorsque votre serveur ou le serveur de destination ne prend pas en charge le routage privé. + No comment provided by engineer. + Send notifications Envoi de notifications @@ -5109,6 +5904,11 @@ Erreur : %@ Envoyé le : %@ copied message info + + Sent directly + Envoyé directement + No comment provided by engineer. + Sent file event Événement de fichier envoyé @@ -5119,11 +5919,46 @@ Erreur : %@ Message envoyé message info title + + Sent messages + Messages envoyés + No comment provided by engineer. + Sent messages will be deleted after set time. Les messages envoyés seront supprimés après une durée déterminée. No comment provided by engineer. + + Sent reply + Réponse envoyée + No comment provided by engineer. + + + Sent total + Total envoyé + No comment provided by engineer. + + + Sent via proxy + Envoyé via le proxy + No comment provided by engineer. + + + Server address + Adresse du serveur + No comment provided by engineer. + + + Server address is incompatible with network settings. + L'adresse du serveur est incompatible avec les paramètres du réseau. + srv error text. + + + Server address is incompatible with network settings: %@. + L'adresse du serveur est incompatible avec les paramètres réseau : %@. + No comment provided by engineer. + Server requires authorization to create queues, check password Le serveur requiert une autorisation pour créer des files d'attente, vérifiez le mot de passe @@ -5131,7 +5966,7 @@ Erreur : %@ Server requires authorization to upload, check password - Le serveur requiert une autorisation pour uploader, vérifiez le mot de passe + Le serveur requiert une autorisation pour téléverser, vérifiez le mot de passe server test error @@ -5139,11 +5974,36 @@ Erreur : %@ Échec du test du serveur ! No comment provided by engineer. + + Server type + Type de serveur + No comment provided by engineer. + + + Server version is incompatible with network settings. + La version du serveur est incompatible avec les paramètres du réseau. + srv error text + + + Server version is incompatible with your app: %@. + La version du serveur est incompatible avec votre appli : %@. + No comment provided by engineer. + Servers Serveurs No comment provided by engineer. + + Servers info + Infos serveurs + No comment provided by engineer. + + + Servers statistics will be reset - this cannot be undone! + Les statistiques des serveurs seront réinitialisées - il n'est pas possible de revenir en arrière ! + No comment provided by engineer. + Session code Code de session @@ -5159,6 +6019,11 @@ Erreur : %@ Définir le nom du contact… No comment provided by engineer. + + Set default theme + Définir le thème par défaut + No comment provided by engineer. + Set group preferences Définir les préférences du groupe @@ -5224,6 +6089,11 @@ Erreur : %@ Partager l'adresse avec vos contacts ? No comment provided by engineer. + + Share from other apps. + Partager depuis d'autres applications. + No comment provided by engineer. + Share link Partager le lien @@ -5234,6 +6104,11 @@ Erreur : %@ Partager ce lien d'invitation unique No comment provided by engineer. + + Share to SimpleX + Partager sur SimpleX + No comment provided by engineer. + Share with contacts Partager avec vos contacts @@ -5256,12 +6131,27 @@ Erreur : %@ Show last messages - Voir les derniers messages + Aperçu des derniers messages + No comment provided by engineer. + + + Show message status + Afficher le statut du message + No comment provided by engineer. + + + Show percentage + Afficher le pourcentage No comment provided by engineer. Show preview - Afficher l'aperçu + Aperçu affiché + No comment provided by engineer. + + + Show → on messages sent via private routing. + Afficher → sur les messages envoyés via le routage privé. No comment provided by engineer. @@ -5269,6 +6159,11 @@ Erreur : %@ Afficher : No comment provided by engineer. + + SimpleX + SimpleX + No comment provided by engineer. + SimpleX Address Adresse SimpleX @@ -5344,6 +6239,11 @@ Erreur : %@ Mode incognito simplifié No comment provided by engineer. + + Size + Taille + No comment provided by engineer. + Skip Passer @@ -5359,11 +6259,26 @@ Erreur : %@ Petits groupes (max 20) No comment provided by engineer. + + Soft + Léger + blur media + + + Some file(s) were not exported: + Certains fichiers n'ont pas été exportés : + No comment provided by engineer. + Some non-fatal errors occurred during import - you may see Chat console for more details. Des erreurs non fatales se sont produites lors de l'importation - vous pouvez consulter la console de chat pour plus de détails. No comment provided by engineer. + + Some non-fatal errors occurred during import: + L'importation a entraîné des erreurs non fatales : + No comment provided by engineer. + Somebody Quelqu'un @@ -5389,6 +6304,16 @@ Erreur : %@ Démarrer la migration No comment provided by engineer. + + Starting from %@. + À partir de %@. + No comment provided by engineer. + + + Statistics + Statistiques + No comment provided by engineer. + Stop Arrêter @@ -5449,11 +6374,31 @@ Erreur : %@ Arrêt du chat No comment provided by engineer. + + Strong + Fort + blur media + Submit Soumettre No comment provided by engineer. + + Subscribed + Inscriptions + No comment provided by engineer. + + + Subscription errors + Erreurs d'inscription + No comment provided by engineer. + + + Subscriptions ignored + Inscriptions ignorées + No comment provided by engineer. + Support SimpleX Chat Supporter SimpleX Chat @@ -5469,6 +6414,11 @@ Erreur : %@ Authentification du système No comment provided by engineer. + + TCP connection + Connexion TCP + No comment provided by engineer. + TCP connection timeout Délai de connexion TCP @@ -5529,9 +6479,9 @@ Erreur : %@ Appuyez pour scanner No comment provided by engineer. - - Tap to start a new chat - Appuyez ici pour démarrer une nouvelle discussion + + Temporary file error + Erreur de fichier temporaire No comment provided by engineer. @@ -5586,6 +6536,11 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. L'application peut vous avertir lorsque vous recevez des messages ou des demandes de contact - veuillez ouvrir les paramètres pour les activer. No comment provided by engineer. + + The app will ask to confirm downloads from unknown file servers (except .onion). + L'application demandera de confirmer les téléchargements à partir de serveurs de fichiers inconnus (sauf .onion). + No comment provided by engineer. + The attempt to change database passphrase was not completed. La tentative de modification de la phrase secrète de la base de données n'a pas abouti. @@ -5631,6 +6586,16 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Le message sera marqué comme modéré pour tous les membres. No comment provided by engineer. + + The messages will be deleted for all members. + Les messages seront supprimés pour tous les membres. + No comment provided by engineer. + + + The messages will be marked as moderated for all members. + Les messages seront marqués comme modérés pour tous les membres. + No comment provided by engineer. + The next generation of private messaging La nouvelle génération de messagerie privée @@ -5666,9 +6631,9 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Le texte collé n'est pas un lien SimpleX. No comment provided by engineer. - - Theme - Thème + + Themes + Thèmes No comment provided by engineer. @@ -5736,11 +6701,21 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Voici votre propre lien unique ! No comment provided by engineer. + + This link was used with another mobile device, please create a new link on the desktop. + Ce lien a été utilisé avec un autre appareil mobile, veuillez créer un nouveau lien sur le bureau. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. Ce paramètre s'applique aux messages de votre profil de chat actuel **%@**. No comment provided by engineer. + + Title + Titre + No comment provided by engineer. + To ask any questions and to receive updates: Si vous avez des questions et que vous souhaitez des réponses : @@ -5771,6 +6746,11 @@ Cela peut se produire en raison d'un bug ou lorsque la connexion est compromise. Pour préserver le fuseau horaire, les fichiers image/voix utilisent le système UTC. No comment provided by engineer. + + To protect your IP address, private routing uses your SMP servers to deliver messages. + Pour protéger votre adresse IP, le routage privé utilise vos serveurs SMP pour délivrer les messages. + No comment provided by engineer. + To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5798,16 +6778,36 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s Pour vérifier le chiffrement de bout en bout avec votre contact, comparez (ou scannez) le code sur vos appareils. No comment provided by engineer. + + Toggle chat list: + Afficher la liste des conversations : + No comment provided by engineer. + Toggle incognito when connecting. Basculer en mode incognito lors de la connexion. No comment provided by engineer. + + Toolbar opacity + Opacité de la barre d'outils + No comment provided by engineer. + + + Total + Total + No comment provided by engineer. + Transport isolation Transport isolé No comment provided by engineer. + + Transport sessions + Sessions de transport + No comment provided by engineer. + Trying to connect to the server used to receive messages from this contact (error: %@). Tentative de connexion au serveur utilisé pour recevoir les messages de ce contact (erreur : %@). @@ -5863,11 +6863,6 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s Débloquer ce membre ? No comment provided by engineer. - - Unexpected error: %@ - Erreur inattendue : %@ - item status description - Unexpected migration state État de la migration inattendu @@ -5876,7 +6871,7 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s Unfav. Unfav. - No comment provided by engineer. + swipe action Unhide @@ -5913,6 +6908,11 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s Erreur inconnue No comment provided by engineer. + + Unknown servers! + Serveurs inconnus ! + No comment provided by engineer. + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. À moins que vous utilisiez l'interface d'appel d'iOS, activez le mode "Ne pas déranger" pour éviter les interruptions. @@ -5948,12 +6948,12 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Unmute Démute - No comment provided by engineer. + swipe action Unread Non lu - No comment provided by engineer. + swipe action Up to 100 last messages are sent to new members. @@ -5965,11 +6965,6 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Mise à jour No comment provided by engineer. - - Update .onion hosts setting? - Mettre à jour le paramètre des hôtes .onion ? - No comment provided by engineer. - Update database passphrase Mise à jour de la phrase secrète de la base de données @@ -5980,9 +6975,9 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Mettre à jour les paramètres réseau ? No comment provided by engineer. - - Update transport isolation mode? - Mettre à jour le mode d'isolement du transport ? + + Update settings? + Mettre à jour les paramètres ? No comment provided by engineer. @@ -5990,16 +6985,16 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien La mise à jour des ces paramètres reconnectera le client à tous les serveurs. No comment provided by engineer. - - Updating this setting will re-connect the client to all servers. - La mise à jour de ce paramètre reconnectera le client à tous les serveurs. - No comment provided by engineer. - Upgrade and open chat Mettre à niveau et ouvrir le chat No comment provided by engineer. + + Upload errors + Erreurs de téléversement + No comment provided by engineer. + Upload failed Échec de l'envoi @@ -6007,9 +7002,19 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Upload file - Transférer le fichier + Téléverser le fichier server test step + + Uploaded + Téléversé + No comment provided by engineer. + + + Uploaded files + Fichiers téléversés + No comment provided by engineer. + Uploading archive Envoi de l'archive @@ -6060,6 +7065,16 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Utilisation de notifications locales uniquement ? No comment provided by engineer. + + Use private routing with unknown servers when IP address is not protected. + Utiliser le routage privé avec des serveurs inconnus lorsque l'adresse IP n'est pas protégée. + No comment provided by engineer. + + + Use private routing with unknown servers. + Utiliser le routage privé avec des serveurs inconnus. + No comment provided by engineer. + Use server Utiliser ce serveur @@ -6070,14 +7085,19 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Utiliser l'application pendant l'appel. No comment provided by engineer. + + Use the app with one hand. + Utiliser l'application d'une main. + No comment provided by engineer. + User profile Profil d'utilisateur No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - L'utilisation des hôtes .onion nécessite un fournisseur VPN compatible. + + User selection + Sélection de l'utilisateur No comment provided by engineer. @@ -6137,7 +7157,7 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Video will be received when your contact completes uploading it. - La vidéo ne sera reçue que lorsque votre contact aura fini de la transférer. + La vidéo ne sera reçue que lorsque votre contact aura fini la mettre en ligne. No comment provided by engineer. @@ -6210,9 +7230,19 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien En attente de la vidéo No comment provided by engineer. + + Wallpaper accent + Accentuation du papier-peint + No comment provided by engineer. + + + Wallpaper background + Fond d'écran + No comment provided by engineer. + Warning: starting chat on multiple devices is not supported and will cause message delivery failures - Attention: démarrer une session de chat sur plusieurs appareils n'est pas pris en charge et entraînera des dysfonctionnements au niveau de la transmission des messages + Attention : démarrer une session de chat sur plusieurs appareils n'est pas pris en charge et entraînera des dysfonctionnements au niveau de la transmission des messages No comment provided by engineer. @@ -6295,19 +7325,39 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Consommation réduite de la batterie. No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + Sans Tor ou un VPN, votre adresse IP sera visible par les serveurs de fichiers. + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + Sans Tor ni VPN, votre adresse IP sera visible par ces relais XFTP : %@. + No comment provided by engineer. + Wrong database passphrase Mauvaise phrase secrète pour la base de données No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + Clé erronée ou connexion non identifiée - il est très probable que cette connexion soit supprimée. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + Mauvaise clé ou adresse inconnue du bloc de données du fichier - le fichier est probablement supprimé. + file error text + Wrong passphrase! Mauvaise phrase secrète ! No comment provided by engineer. - - XFTP servers - Serveurs XFTP + + XFTP server + Serveur XFTP No comment provided by engineer. @@ -6387,11 +7437,21 @@ Répéter la demande d'adhésion ? Vous êtes invité·e au groupe No comment provided by engineer. + + You are not connected to these servers. Private routing is used to deliver messages to them. + Vous n'êtes pas connecté à ces serveurs. Le routage privé est utilisé pour leur délivrer des messages. + No comment provided by engineer. + You can accept calls from lock screen, without device and app authentication. Vous pouvez accepter des appels à partir de l'écran de verrouillage, sans authentification de l'appareil ou de l'application. No comment provided by engineer. + + You can change it in Appearance settings. + Vous pouvez choisir de le modifier dans les paramètres d'apparence. + No comment provided by engineer. + You can create it later Vous pouvez la créer plus tard @@ -6422,11 +7482,16 @@ Répéter la demande d'adhésion ? Vous pouvez le rendre visible à vos contacts SimpleX via Paramètres. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Vous pouvez maintenant envoyer des messages à %@ notification body + + You can send messages to %@ from Archived contacts. + Vous pouvez envoyer des messages à %@ à partir des contacts archivés. + No comment provided by engineer. + You can set lock screen notification preview via settings. Vous pouvez configurer l'aperçu des notifications sur l'écran de verrouillage via les paramètres. @@ -6452,6 +7517,11 @@ Répéter la demande d'adhésion ? Vous pouvez lancer le chat via Paramètres / Base de données ou en redémarrant l'app No comment provided by engineer. + + You can still view conversation with %@ in the list of chats. + Vous pouvez toujours voir la conversation avec %@ dans la liste des discussions. + No comment provided by engineer. + You can turn on SimpleX Lock via Settings. Vous pouvez activer SimpleX Lock dans les Paramètres. @@ -6494,11 +7564,6 @@ Repeat connection request? Répéter la demande de connexion ? No comment provided by engineer. - - You have no chats - Vous n'avez aucune discussion - No comment provided by engineer. - You have to enter passphrase every time the app starts - it is not stored on the device. Vous devez saisir la phrase secrète à chaque fois que l'application démarre - elle n'est pas stockée sur l'appareil. @@ -6519,11 +7584,26 @@ Répéter la demande de connexion ? Vous avez rejoint ce groupe. Connexion à l'invitation d'un membre du groupe. No comment provided by engineer. + + You may migrate the exported database. + Vous pouvez migrer la base de données exportée. + No comment provided by engineer. + + + You may save the exported archive. + Vous pouvez enregistrer l'archive exportée. + No comment provided by engineer. + You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. Vous devez utiliser la version la plus récente de votre base de données de chat sur un seul appareil UNIQUEMENT, sinon vous risquez de ne plus recevoir les messages de certains contacts. No comment provided by engineer. + + You need to allow your contact to call to be able to call them. + Vous devez autoriser votre contact à appeler pour pouvoir l'appeler. + No comment provided by engineer. + You need to allow your contact to send voice messages to be able to send them. Vous devez autoriser votre contact à envoyer des messages vocaux pour pouvoir en envoyer. @@ -6639,13 +7719,6 @@ Répéter la demande de connexion ? Vos profils de chat No comment provided by engineer. - - Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link). - Votre contact a besoin d'être en ligne pour completer la connexion. -Vous pouvez annuler la connexion et supprimer le contact (et réessayer plus tard avec un autre lien). - No comment provided by engineer. - Your contact sent a file that is larger than currently supported maximum size (%@). Votre contact a envoyé un fichier plus grand que la taille maximale supportée actuellement(%@). @@ -6793,6 +7866,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. et %lld autres événements No comment provided by engineer. + + attempts + tentatives + No comment provided by engineer. + audio call (not e2e encrypted) appel audio (sans chiffrement) @@ -6833,6 +7911,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. gras No comment provided by engineer. + + call + appeler + No comment provided by engineer. + call error erreur d'appel @@ -6983,6 +8066,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. jours time unit + + decryption errors + Erreurs de déchiffrement + No comment provided by engineer. + default (%@) défaut (%@) @@ -7033,6 +8121,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. message dupliqué integrity error chat item + + duplicates + doublons + No comment provided by engineer. + e2e encrypted chiffré de bout en bout @@ -7113,6 +8206,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. event happened No comment provided by engineer. + + expired + expiré + No comment provided by engineer. + forwarded transféré @@ -7143,6 +8241,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. La keychain d'iOS sera utilisée pour stocker en toute sécurité la phrase secrète après le redémarrage de l'app ou la modification de la phrase secrète - il permettra de recevoir les notifications push. No comment provided by engineer. + + inactive + inactif + No comment provided by engineer. + incognito via contact address link mode incognito via le lien d'adresse du contact @@ -7183,6 +8286,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. invitation au groupe %@ group name + + invite + inviter + No comment provided by engineer. + invited invité·e @@ -7238,6 +8346,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. est connecté·e rcv group event chat item + + message + message + No comment provided by engineer. + message received message reçu @@ -7268,6 +8381,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. mois time unit + + mute + muet + No comment provided by engineer. + never jamais @@ -7320,6 +8438,16 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. on group pref value + + other + autre + No comment provided by engineer. + + + other errors + autres erreurs + No comment provided by engineer. + owner propriétaire @@ -7390,6 +8518,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. enregistré à partir de %@ No comment provided by engineer. + + search + rechercher + No comment provided by engineer. + sec sec @@ -7415,6 +8548,15 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. envoyer un message direct No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + info sur la file d'attente du serveur : %1$@ + +dernier message reçu : %2$@ + queue info + set new contact address a changé d'adresse de contact @@ -7455,11 +8597,26 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. inconnu connection info + + unknown servers + relais inconnus + No comment provided by engineer. + unknown status statut inconnu No comment provided by engineer. + + unmute + démuter + No comment provided by engineer. + + + unprotected + non protégé + No comment provided by engineer. + updated group profile mise à jour du profil de groupe @@ -7500,6 +8657,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. via relais No comment provided by engineer. + + video + vidéo + No comment provided by engineer. + video call (not e2e encrypted) appel vidéo (sans chiffrement) @@ -7525,6 +8687,11 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. semaines time unit + + when IP hidden + lorsque l'IP est masquée + No comment provided by engineer. + yes oui @@ -7609,7 +8776,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.
- +
@@ -7646,7 +8813,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.
- +
@@ -7666,4 +8833,218 @@ Les serveurs SimpleX ne peuvent pas voir votre profil.
+ +
+ +
+ + + SimpleX SE + SimpleX SE + Bundle display name + + + SimpleX SE + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright © 2024 SimpleX Chat. Tous droits réservés. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + %@ + No comment provided by engineer. + + + App is locked! + L'app est verrouillée ! + No comment provided by engineer. + + + Cancel + Annuler + No comment provided by engineer. + + + Cannot access keychain to save database password + Impossible d'accéder à la keychain pour enregistrer le mot de passe de la base de données + No comment provided by engineer. + + + Cannot forward message + Impossible de transférer le message + No comment provided by engineer. + + + Comment + Commenter + No comment provided by engineer. + + + Currently maximum supported file size is %@. + Actuellement, la taille maximale des fichiers supportés est de %@. + No comment provided by engineer. + + + Database downgrade required + Mise à jour de la base de données nécessaire + No comment provided by engineer. + + + Database encrypted! + Base de données chiffrée ! + No comment provided by engineer. + + + Database error + Erreur de base de données + No comment provided by engineer. + + + Database passphrase is different from saved in the keychain. + La phrase secrète de la base de données est différente de celle enregistrée dans la keychain. + No comment provided by engineer. + + + Database passphrase is required to open chat. + La phrase secrète de la base de données est nécessaire pour ouvrir le chat. + No comment provided by engineer. + + + Database upgrade required + Mise à niveau de la base de données nécessaire + No comment provided by engineer. + + + Error preparing file + Erreur lors de la préparation du fichier + No comment provided by engineer. + + + Error preparing message + Erreur lors de la préparation du message + No comment provided by engineer. + + + Error: %@ + Erreur : %@ + No comment provided by engineer. + + + File error + Erreur de fichier + No comment provided by engineer. + + + Incompatible database version + Version de la base de données incompatible + No comment provided by engineer. + + + Invalid migration confirmation + Confirmation de migration invalide + No comment provided by engineer. + + + Keychain error + Erreur de la keychain + No comment provided by engineer. + + + Large file! + Fichier trop lourd ! + No comment provided by engineer. + + + No active profile + Pas de profil actif + No comment provided by engineer. + + + Ok + Ok + No comment provided by engineer. + + + Open the app to downgrade the database. + Ouvrez l'app pour rétrograder la base de données. + No comment provided by engineer. + + + Open the app to upgrade the database. + Ouvrez l'app pour mettre à jour la base de données. + No comment provided by engineer. + + + Passphrase + Phrase secrète + No comment provided by engineer. + + + Please create a profile in the SimpleX app + Veuillez créer un profil dans l'app SimpleX + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + Les paramètres de chat sélectionnés ne permettent pas l'envoi de ce message. + No comment provided by engineer. + + + Sending a message takes longer than expected. + L'envoi d'un message prend plus de temps que prévu. + No comment provided by engineer. + + + Sending message… + Envoi du message… + No comment provided by engineer. + + + Share + Partager + No comment provided by engineer. + + + Slow network? + Réseau lent ? + No comment provided by engineer. + + + Unknown database error: %@ + Erreur inconnue de la base de données : %@ + No comment provided by engineer. + + + Unsupported format + Format non pris en charge + No comment provided by engineer. + + + Wait + Attendez + No comment provided by engineer. + + + Wrong database passphrase + Mauvaise phrase secrète pour la base de données + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + Vous pouvez autoriser le partage dans les paramètres Confidentialité et sécurité / SimpleX Lock. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/contents.json b/apps/ios/SimpleX Localizations/fr.xcloc/contents.json index 7df7c8ed26..22d271b92e 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/fr.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "fr", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "15F31d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "15.4" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff b/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff index 45cfe0c468..928a01dead 100644 --- a/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff +++ b/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff @@ -403,9 +403,9 @@ Available in v5.1 הוספת שרתים על ידי סריקת קוד QR. No comment provided by engineer.
- - Add server… - הוסף שרת… + + Add server + הוסף שרת No comment provided by engineer. @@ -5316,6 +5316,278 @@ SimpleX servers cannot see your profile. %@ ו-%@ No comment provided by engineer. + + Connect automatically + התבר אוטומטי + + + Create profile + צור פרופיל + + + Created at: %@ + נוצר ב:%@ + + + Desktop devices + מכשירי מחשב + + + Discover via local network + גלה באמצעות הרשת המקומית + + + Forward + העבר + + + Group already exists + קבוצה כבר קיימת + + + Connected to desktop + מחובר למחשב + + + Group already exists! + קבוצה כבר קיימת! + + + Confirm upload + אשר ההעלאה + + + Block for all + חסום לכולם + + + Blocked by admin + נחסם ע"י מנהל + + + Block member for all? + לחסום את החבר לכולם? + + + Camera not available + מצלמה לא זמינה + + + Connect to desktop + חבר למחשב + + + Created at + נוצר ב + + + (new) + (חדש) + + + Block member + חבר חסום + + + Block member? + לחסום חבר? + + + Creating link… + יוצר קישור… + + + Files + קבצים + + + Disabled + מושבת + + + Enter passphrase + הכנס סיסמא + + + Apply + החל + + + Apply to + החל ל + + + Background + ברקע + + + Black + שחור + + + Blur media + טשטש מדיה + + + Chat theme + צבע ערכת נושא + + + Completed + הושלם + + + Connected + מחובר + + + Connection notifications + התראות חיבור + + + Connections + חיבורים + + + Current profile + פרופיל נוכחי + + + Disconnect desktop? + להתנתק מהמחשב? + + + Discover and join groups + גלה והצטרף לקבוצות + + + Enabled + מופעל + + + Error opening chat + שגיאה בפתיחת הצ'אט + + + Good morning! + בוקר טוב! + + + Connect to yourself? +This is your own SimpleX address! + להתחבר אליך? +זו כתובת הSimpleX שלך! + + + Connect to yourself? + להתחבר אליך? + + + Connect to yourself? +This is your own one-time link! + להתחבר אליך? +זו כתובת ההזמנה החד-פעמי שלך! + + + Connected desktop + מחשב מחובר + + + Connected servers + שרתים מחוברים + + + Enter group name… + הכנס שם לקבוצה… + + + Enter this device name… + הכנס שם למכשיר הזה… + + + Enter your name… + הכנס את השם שלך… + + + Error decrypting file + שגיאה בפענוח הקובץ + + + Errors + שגיאות + + + File status + מצב הקובץ + + + Connecting + מתחבר + + + Connecting to desktop + מתחבר למחשב + + + Deleted + נמחק + + + Deletion errors + שגיאות במחיקה + + + Details + פרטים + + + Forwarded + הועבר + + + Found desktop + נמצא מחשב + + + Good afternoon! + אחר צהריים טובים! + + + Desktop address + כתובת מחשב + + + Forwarded from + הועבר מ + + + History is not sent to new members. + היסטוריה לא נשלחת לחברים חדשים. + + + Created + נוצר + + + Copy error + שגיאת העתקה + + + Create group + צור קבוצה + + + Enabled for + מופעל עבור + + + Error creating message + שגיאה ביצירת הודעה + + + File error + שגיאה בקובץ + diff --git a/apps/ios/SimpleX Localizations/hi.xcloc/Localized Contents/hi.xliff b/apps/ios/SimpleX Localizations/hi.xcloc/Localized Contents/hi.xliff deleted file mode 100644 index 31746eccd9..0000000000 --- a/apps/ios/SimpleX Localizations/hi.xcloc/Localized Contents/hi.xliff +++ /dev/null @@ -1,3554 +0,0 @@ - - - -
- -
- - - - - No comment provided by engineer. - - - - No comment provided by engineer. - - - - No comment provided by engineer. - - - - No comment provided by engineer. - - - ( - No comment provided by engineer. - - - (can be copied) - No comment provided by engineer. - - - !1 colored! - No comment provided by engineer. - - - #secret# - No comment provided by engineer. - - - %@ - No comment provided by engineer. - - - %@ %@ - No comment provided by engineer. - - - %@ / %@ - No comment provided by engineer. - - - %@ is connected! - notification title - - - %@ is not verified - No comment provided by engineer. - - - %@ is verified - No comment provided by engineer. - - - %@ wants to connect! - notification title - - - %d days - message ttl - - - %d hours - message ttl - - - %d min - message ttl - - - %d months - message ttl - - - %d sec - message ttl - - - %d skipped message(s) - integrity error chat item - - - %lld - No comment provided by engineer. - - - %lld %@ - No comment provided by engineer. - - - %lld contact(s) selected - No comment provided by engineer. - - - %lld file(s) with total size of %@ - No comment provided by engineer. - - - %lld members - No comment provided by engineer. - - - %lld second(s) - No comment provided by engineer. - - - %lldd - No comment provided by engineer. - - - %lldh - No comment provided by engineer. - - - %lldk - No comment provided by engineer. - - - %lldm - No comment provided by engineer. - - - %lldmth - No comment provided by engineer. - - - %llds - No comment provided by engineer. - - - %lldw - No comment provided by engineer. - - - ( - No comment provided by engineer. - - - ) - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - No comment provided by engineer. - - - **Create link / QR code** for your contact to use. - No comment provided by engineer. - - - **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. - No comment provided by engineer. - - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). - No comment provided by engineer. - - - **Paste received link** or open it in the browser and tap **Open in mobile app**. - No comment provided by engineer. - - - **Please note**: you will NOT be able to recover or change passphrase if you lose it. - No comment provided by engineer. - - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. - No comment provided by engineer. - - - **Scan QR code**: to connect to your contact in person or via video call. - No comment provided by engineer. - - - **Warning**: Instant push notifications require passphrase saved in Keychain. - No comment provided by engineer. - - - **e2e encrypted** audio call - No comment provided by engineer. - - - **e2e encrypted** video call - No comment provided by engineer. - - - \*bold* - No comment provided by engineer. - - - , - No comment provided by engineer. - - - . - No comment provided by engineer. - - - 1 day - message ttl - - - 1 hour - message ttl - - - 1 month - message ttl - - - 1 week - message ttl - - - 2 weeks - message ttl - - - 6 - No comment provided by engineer. - - - : - No comment provided by engineer. - - - A new contact - notification title - - - A random profile will be sent to the contact that you received this link from - No comment provided by engineer. - - - A random profile will be sent to your contact - No comment provided by engineer. - - - A separate TCP connection will be used **for each chat profile you have in the app**. - No comment provided by engineer. - - - A separate TCP connection will be used **for each contact and group member**. -**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail. - No comment provided by engineer. - - - About SimpleX - No comment provided by engineer. - - - About SimpleX Chat - No comment provided by engineer. - - - Accent color - No comment provided by engineer. - - - Accept - accept contact request via notification - accept incoming call via notification - - - Accept contact - No comment provided by engineer. - - - Accept contact request from %@? - notification body - - - Accept incognito - No comment provided by engineer. - - - Accept requests - No comment provided by engineer. - - - Add preset servers - No comment provided by engineer. - - - Add profile - No comment provided by engineer. - - - Add servers by scanning QR codes. - No comment provided by engineer. - - - Add server… - No comment provided by engineer. - - - Add to another device - No comment provided by engineer. - - - Admins can create the links to join groups. - No comment provided by engineer. - - - Advanced network settings - No comment provided by engineer. - - - All chats and messages will be deleted - this cannot be undone! - No comment provided by engineer. - - - All group members will remain connected. - No comment provided by engineer. - - - All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you. - No comment provided by engineer. - - - All your contacts will remain connected - No comment provided by engineer. - - - Allow - No comment provided by engineer. - - - Allow disappearing messages only if your contact allows it to you. - No comment provided by engineer. - - - Allow irreversible message deletion only if your contact allows it to you. - No comment provided by engineer. - - - Allow sending direct messages to members. - No comment provided by engineer. - - - Allow sending disappearing messages. - No comment provided by engineer. - - - Allow to irreversibly delete sent messages. - No comment provided by engineer. - - - Allow to send voice messages. - No comment provided by engineer. - - - Allow voice messages only if your contact allows them. - No comment provided by engineer. - - - Allow voice messages? - No comment provided by engineer. - - - Allow your contacts to irreversibly delete sent messages. - No comment provided by engineer. - - - Allow your contacts to send disappearing messages. - No comment provided by engineer. - - - Allow your contacts to send voice messages. - No comment provided by engineer. - - - Already connected? - No comment provided by engineer. - - - Answer call - No comment provided by engineer. - - - App build: %@ - No comment provided by engineer. - - - App icon - No comment provided by engineer. - - - App version - No comment provided by engineer. - - - App version: v%@ - No comment provided by engineer. - - - Appearance - No comment provided by engineer. - - - Attach - No comment provided by engineer. - - - Audio & video calls - No comment provided by engineer. - - - Authentication failed - No comment provided by engineer. - - - Authentication unavailable - No comment provided by engineer. - - - Auto-accept contact requests - No comment provided by engineer. - - - Auto-accept images - No comment provided by engineer. - - - Automatically - No comment provided by engineer. - - - Back - No comment provided by engineer. - - - Both you and your contact can irreversibly delete sent messages. - No comment provided by engineer. - - - Both you and your contact can send disappearing messages. - No comment provided by engineer. - - - Both you and your contact can send voice messages. - No comment provided by engineer. - - - Call already ended! - No comment provided by engineer. - - - Calls - No comment provided by engineer. - - - Can't invite contact! - No comment provided by engineer. - - - Can't invite contacts! - No comment provided by engineer. - - - Cancel - No comment provided by engineer. - - - Cannot access keychain to save database password - No comment provided by engineer. - - - Cannot receive file - No comment provided by engineer. - - - Change - No comment provided by engineer. - - - Change database passphrase? - No comment provided by engineer. - - - Change member role? - No comment provided by engineer. - - - Change receiving address - No comment provided by engineer. - - - Change receiving address? - No comment provided by engineer. - - - Change role - No comment provided by engineer. - - - Chat archive - No comment provided by engineer. - - - Chat console - No comment provided by engineer. - - - Chat database - No comment provided by engineer. - - - Chat database deleted - No comment provided by engineer. - - - Chat database imported - No comment provided by engineer. - - - Chat is running - No comment provided by engineer. - - - Chat is stopped - No comment provided by engineer. - - - Chat preferences - No comment provided by engineer. - - - Chats - No comment provided by engineer. - - - Check server address and try again. - No comment provided by engineer. - - - Choose file - No comment provided by engineer. - - - Choose from library - No comment provided by engineer. - - - Clear - No comment provided by engineer. - - - Clear conversation - No comment provided by engineer. - - - Clear conversation? - No comment provided by engineer. - - - Clear verification - No comment provided by engineer. - - - Colors - No comment provided by engineer. - - - Compare security codes with your contacts. - No comment provided by engineer. - - - Configure ICE servers - No comment provided by engineer. - - - Confirm - No comment provided by engineer. - - - Confirm new passphrase… - No comment provided by engineer. - - - Connect - server test step - - - Connect via contact link? - No comment provided by engineer. - - - Connect via group link? - No comment provided by engineer. - - - Connect via link - No comment provided by engineer. - - - Connect via link / QR code - No comment provided by engineer. - - - Connect via one-time link? - No comment provided by engineer. - - - Connect via relay - No comment provided by engineer. - - - Connecting to server… - No comment provided by engineer. - - - Connecting to server… (error: %@) - No comment provided by engineer. - - - Connection - No comment provided by engineer. - - - Connection error - No comment provided by engineer. - - - Connection error (AUTH) - No comment provided by engineer. - - - Connection request - No comment provided by engineer. - - - Connection request sent! - No comment provided by engineer. - - - Connection timeout - No comment provided by engineer. - - - Contact allows - No comment provided by engineer. - - - Contact already exists - No comment provided by engineer. - - - Contact and all messages will be deleted - this cannot be undone! - No comment provided by engineer. - - - Contact hidden: - notification - - - Contact is connected - notification - - - Contact is not connected yet! - No comment provided by engineer. - - - Contact name - No comment provided by engineer. - - - Contact preferences - No comment provided by engineer. - - - Contact requests - No comment provided by engineer. - - - Contacts can mark messages for deletion; you will be able to view them. - No comment provided by engineer. - - - Copy - chat item action - - - Core built at: %@ - No comment provided by engineer. - - - Core version: v%@ - No comment provided by engineer. - - - Create - No comment provided by engineer. - - - Create address - No comment provided by engineer. - - - Create group link - No comment provided by engineer. - - - Create link - No comment provided by engineer. - - - Create one-time invitation link - No comment provided by engineer. - - - Create queue - server test step - - - Create secret group - No comment provided by engineer. - - - Create your profile - No comment provided by engineer. - - - Created on %@ - No comment provided by engineer. - - - Current passphrase… - No comment provided by engineer. - - - Currently maximum supported file size is %@. - No comment provided by engineer. - - - Dark - No comment provided by engineer. - - - Data - No comment provided by engineer. - - - Database ID - No comment provided by engineer. - - - Database encrypted! - No comment provided by engineer. - - - Database encryption passphrase will be updated and stored in the keychain. - - No comment provided by engineer. - - - Database encryption passphrase will be updated. - - No comment provided by engineer. - - - Database error - No comment provided by engineer. - - - Database is encrypted using a random passphrase, you can change it. - No comment provided by engineer. - - - Database is encrypted using a random passphrase. Please change it before exporting. - No comment provided by engineer. - - - Database passphrase - No comment provided by engineer. - - - Database passphrase & export - No comment provided by engineer. - - - Database passphrase is different from saved in the keychain. - No comment provided by engineer. - - - Database passphrase is required to open chat. - No comment provided by engineer. - - - Database will be encrypted and the passphrase stored in the keychain. - - No comment provided by engineer. - - - Database will be encrypted. - - No comment provided by engineer. - - - Database will be migrated when the app restarts - No comment provided by engineer. - - - Decentralized - No comment provided by engineer. - - - Delete - chat item action - - - Delete Contact - No comment provided by engineer. - - - Delete address - No comment provided by engineer. - - - Delete address? - No comment provided by engineer. - - - Delete after - No comment provided by engineer. - - - Delete all files - No comment provided by engineer. - - - Delete archive - No comment provided by engineer. - - - Delete chat archive? - No comment provided by engineer. - - - Delete chat profile? - No comment provided by engineer. - - - Delete connection - No comment provided by engineer. - - - Delete contact - No comment provided by engineer. - - - Delete contact? - No comment provided by engineer. - - - Delete database - No comment provided by engineer. - - - Delete files & media - No comment provided by engineer. - - - Delete files and media? - No comment provided by engineer. - - - Delete files for all chat profiles - No comment provided by engineer. - - - Delete for everyone - chat feature - - - Delete for me - No comment provided by engineer. - - - Delete group - No comment provided by engineer. - - - Delete group? - No comment provided by engineer. - - - Delete invitation - No comment provided by engineer. - - - Delete link - No comment provided by engineer. - - - Delete link? - No comment provided by engineer. - - - Delete message? - No comment provided by engineer. - - - Delete messages - No comment provided by engineer. - - - Delete messages after - No comment provided by engineer. - - - Delete old database - No comment provided by engineer. - - - Delete old database? - No comment provided by engineer. - - - Delete pending connection - No comment provided by engineer. - - - Delete pending connection? - No comment provided by engineer. - - - Delete queue - server test step - - - Delete user profile? - No comment provided by engineer. - - - Description - No comment provided by engineer. - - - Develop - No comment provided by engineer. - - - Developer tools - No comment provided by engineer. - - - Device - No comment provided by engineer. - - - Device authentication is disabled. Turning off SimpleX Lock. - No comment provided by engineer. - - - Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication. - No comment provided by engineer. - - - Direct messages - chat feature - - - Direct messages between members are prohibited in this group. - No comment provided by engineer. - - - Disable SimpleX Lock - authentication reason - - - Disappearing messages - chat feature - - - Disappearing messages are prohibited in this chat. - No comment provided by engineer. - - - Disappearing messages are prohibited in this group. - No comment provided by engineer. - - - Disconnect - server test step - - - Display name - No comment provided by engineer. - - - Display name: - No comment provided by engineer. - - - Do NOT use SimpleX for emergency calls. - No comment provided by engineer. - - - Do it later - No comment provided by engineer. - - - Edit - chat item action - - - Edit group profile - No comment provided by engineer. - - - Enable - No comment provided by engineer. - - - Enable SimpleX Lock - authentication reason - - - Enable TCP keep-alive - No comment provided by engineer. - - - Enable automatic message deletion? - No comment provided by engineer. - - - Enable instant notifications? - No comment provided by engineer. - - - Enable notifications - No comment provided by engineer. - - - Enable periodic notifications? - No comment provided by engineer. - - - Encrypt - No comment provided by engineer. - - - Encrypt database? - No comment provided by engineer. - - - Encrypted database - No comment provided by engineer. - - - Encrypted message or another event - notification - - - Encrypted message: database error - notification - - - Encrypted message: keychain error - notification - - - Encrypted message: no passphrase - notification - - - Encrypted message: unexpected error - notification - - - Enter correct passphrase. - No comment provided by engineer. - - - Enter passphrase… - No comment provided by engineer. - - - Enter server manually - No comment provided by engineer. - - - Error - No comment provided by engineer. - - - Error accepting contact request - No comment provided by engineer. - - - Error accessing database file - No comment provided by engineer. - - - Error adding member(s) - No comment provided by engineer. - - - Error changing address - No comment provided by engineer. - - - Error changing role - No comment provided by engineer. - - - Error changing setting - No comment provided by engineer. - - - Error creating address - No comment provided by engineer. - - - Error creating group - No comment provided by engineer. - - - Error creating group link - No comment provided by engineer. - - - Error deleting chat database - No comment provided by engineer. - - - Error deleting chat! - No comment provided by engineer. - - - Error deleting connection - No comment provided by engineer. - - - Error deleting contact - No comment provided by engineer. - - - Error deleting database - No comment provided by engineer. - - - Error deleting old database - No comment provided by engineer. - - - Error deleting token - No comment provided by engineer. - - - Error deleting user profile - No comment provided by engineer. - - - Error enabling notifications - No comment provided by engineer. - - - Error encrypting database - No comment provided by engineer. - - - Error exporting chat database - No comment provided by engineer. - - - Error importing chat database - No comment provided by engineer. - - - Error joining group - No comment provided by engineer. - - - Error receiving file - No comment provided by engineer. - - - Error removing member - No comment provided by engineer. - - - Error saving ICE servers - No comment provided by engineer. - - - Error saving SMP servers - No comment provided by engineer. - - - Error saving group profile - No comment provided by engineer. - - - Error saving passphrase to keychain - No comment provided by engineer. - - - Error sending message - No comment provided by engineer. - - - Error starting chat - No comment provided by engineer. - - - Error stopping chat - No comment provided by engineer. - - - Error updating message - No comment provided by engineer. - - - Error updating settings - No comment provided by engineer. - - - Error: %@ - No comment provided by engineer. - - - Error: URL is invalid - No comment provided by engineer. - - - Error: no database file - No comment provided by engineer. - - - Exit without saving - No comment provided by engineer. - - - Export database - No comment provided by engineer. - - - Export error: - No comment provided by engineer. - - - Exported database archive. - No comment provided by engineer. - - - Exporting database archive... - No comment provided by engineer. - - - Failed to remove passphrase - No comment provided by engineer. - - - File will be received when your contact is online, please wait or check later! - No comment provided by engineer. - - - File: %@ - No comment provided by engineer. - - - Files & media - No comment provided by engineer. - - - For console - No comment provided by engineer. - - - Full link - No comment provided by engineer. - - - Full name (optional) - No comment provided by engineer. - - - Full name: - No comment provided by engineer. - - - GIFs and stickers - No comment provided by engineer. - - - Group - No comment provided by engineer. - - - Group display name - No comment provided by engineer. - - - Group full name (optional) - No comment provided by engineer. - - - Group image - No comment provided by engineer. - - - Group invitation - No comment provided by engineer. - - - Group invitation expired - No comment provided by engineer. - - - Group invitation is no longer valid, it was removed by sender. - No comment provided by engineer. - - - Group link - No comment provided by engineer. - - - Group links - No comment provided by engineer. - - - Group members can irreversibly delete sent messages. - No comment provided by engineer. - - - Group members can send direct messages. - No comment provided by engineer. - - - Group members can send disappearing messages. - No comment provided by engineer. - - - Group members can send voice messages. - No comment provided by engineer. - - - Group message: - notification - - - Group preferences - No comment provided by engineer. - - - Group profile - No comment provided by engineer. - - - Group profile is stored on members' devices, not on the servers. - No comment provided by engineer. - - - Group will be deleted for all members - this cannot be undone! - No comment provided by engineer. - - - Group will be deleted for you - this cannot be undone! - No comment provided by engineer. - - - Help - No comment provided by engineer. - - - Hidden - No comment provided by engineer. - - - Hide - chat item action - - - Hide app screen in the recent apps. - No comment provided by engineer. - - - How SimpleX works - No comment provided by engineer. - - - How it works - No comment provided by engineer. - - - How to - No comment provided by engineer. - - - How to use it - No comment provided by engineer. - - - How to use your servers - No comment provided by engineer. - - - ICE servers (one per line) - No comment provided by engineer. - - - If the video fails to connect, flip the camera to resolve it. - No comment provided by engineer. - - - If you can't meet in person, **show QR code in the video call**, or share the link. - No comment provided by engineer. - - - If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link. - No comment provided by engineer. - - - If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app). - No comment provided by engineer. - - - Ignore - No comment provided by engineer. - - - Image will be received when your contact is online, please wait or check later! - No comment provided by engineer. - - - Immune to spam and abuse - No comment provided by engineer. - - - Import - No comment provided by engineer. - - - Import chat database? - No comment provided by engineer. - - - Import database - No comment provided by engineer. - - - Improved privacy and security - No comment provided by engineer. - - - Improved server configuration - No comment provided by engineer. - - - Incognito - No comment provided by engineer. - - - Incognito mode - No comment provided by engineer. - - - Incognito mode is not supported here - your main profile will be sent to group members - No comment provided by engineer. - - - Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created. - No comment provided by engineer. - - - Incoming audio call - notification - - - Incoming call - notification - - - Incoming video call - notification - - - Incorrect security code! - No comment provided by engineer. - - - Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) - No comment provided by engineer. - - - Instant push notifications will be hidden! - - No comment provided by engineer. - - - Instantly - No comment provided by engineer. - - - Invalid connection link - No comment provided by engineer. - - - Invalid server address! - No comment provided by engineer. - - - Invitation expired! - No comment provided by engineer. - - - Invite members - No comment provided by engineer. - - - Invite to group - No comment provided by engineer. - - - Irreversible message deletion - No comment provided by engineer. - - - Irreversible message deletion is prohibited in this chat. - No comment provided by engineer. - - - Irreversible message deletion is prohibited in this group. - No comment provided by engineer. - - - It allows having many anonymous connections without any shared data between them in a single chat profile. - No comment provided by engineer. - - - It can happen when: -1. The messages expire on the server if they were not received for 30 days, -2. The server you use to receive the messages from this contact was updated and restarted. -3. The connection is compromised. -Please connect to the developers via Settings to receive the updates about the servers. -We will be adding server redundancy to prevent lost messages. - No comment provided by engineer. - - - It seems like you are already connected via this link. If it is not the case, there was an error (%@). - No comment provided by engineer. - - - Join - No comment provided by engineer. - - - Join group - No comment provided by engineer. - - - Join incognito - No comment provided by engineer. - - - Joining group - No comment provided by engineer. - - - Keychain error - No comment provided by engineer. - - - LIVE - No comment provided by engineer. - - - Large file! - No comment provided by engineer. - - - Leave - No comment provided by engineer. - - - Leave group - No comment provided by engineer. - - - Leave group? - No comment provided by engineer. - - - Light - No comment provided by engineer. - - - Limitations - No comment provided by engineer. - - - Live message! - No comment provided by engineer. - - - Live messages - No comment provided by engineer. - - - Local name - No comment provided by engineer. - - - Local profile data only - No comment provided by engineer. - - - Make a private connection - No comment provided by engineer. - - - Make sure SMP server addresses are in correct format, line separated and are not duplicated (%@). - No comment provided by engineer. - - - Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated. - No comment provided by engineer. - - - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - No comment provided by engineer. - - - Mark deleted for everyone - No comment provided by engineer. - - - Mark read - No comment provided by engineer. - - - Mark verified - No comment provided by engineer. - - - Markdown in messages - No comment provided by engineer. - - - Max 30 seconds, received instantly. - No comment provided by engineer. - - - Member - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - No comment provided by engineer. - - - Member will be removed from group - this cannot be undone! - No comment provided by engineer. - - - Message delivery error - No comment provided by engineer. - - - Message text - No comment provided by engineer. - - - Messages - No comment provided by engineer. - - - Migrating database archive... - No comment provided by engineer. - - - Migration error: - No comment provided by engineer. - - - Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat). - No comment provided by engineer. - - - Migration is completed - No comment provided by engineer. - - - Most likely this contact has deleted the connection with you. - No comment provided by engineer. - - - Mute - No comment provided by engineer. - - - Name - No comment provided by engineer. - - - Network & servers - No comment provided by engineer. - - - Network settings - No comment provided by engineer. - - - Network status - No comment provided by engineer. - - - New contact request - notification - - - New contact: - notification - - - New database archive - No comment provided by engineer. - - - New in %@ - No comment provided by engineer. - - - New member role - No comment provided by engineer. - - - New message - notification - - - New passphrase… - No comment provided by engineer. - - - No - No comment provided by engineer. - - - No contacts selected - No comment provided by engineer. - - - No contacts to add - No comment provided by engineer. - - - No device token! - No comment provided by engineer. - - - Group not found! - No comment provided by engineer. - - - No permission to record voice message - No comment provided by engineer. - - - No received or sent files - No comment provided by engineer. - - - Notifications - No comment provided by engineer. - - - Notifications are disabled! - No comment provided by engineer. - - - Off (Local) - No comment provided by engineer. - - - Ok - No comment provided by engineer. - - - Old database - No comment provided by engineer. - - - Old database archive - No comment provided by engineer. - - - One-time invitation link - No comment provided by engineer. - - - Onion hosts will be required for connection. Requires enabling VPN. - No comment provided by engineer. - - - Onion hosts will be used when available. Requires enabling VPN. - No comment provided by engineer. - - - Onion hosts will not be used. - No comment provided by engineer. - - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. - No comment provided by engineer. - - - Only group owners can change group preferences. - No comment provided by engineer. - - - Only group owners can enable voice messages. - No comment provided by engineer. - - - Only you can irreversibly delete messages (your contact can mark them for deletion). - No comment provided by engineer. - - - Only you can send disappearing messages. - No comment provided by engineer. - - - Only you can send voice messages. - No comment provided by engineer. - - - Only your contact can irreversibly delete messages (you can mark them for deletion). - No comment provided by engineer. - - - Only your contact can send disappearing messages. - No comment provided by engineer. - - - Only your contact can send voice messages. - No comment provided by engineer. - - - Open Settings - No comment provided by engineer. - - - Open chat - No comment provided by engineer. - - - Open chat console - authentication reason - - - Open-source protocol and code – anybody can run the servers. - No comment provided by engineer. - - - Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red. - No comment provided by engineer. - - - PING count - No comment provided by engineer. - - - PING interval - No comment provided by engineer. - - - Paste - No comment provided by engineer. - - - Paste image - No comment provided by engineer. - - - Paste received link - No comment provided by engineer. - - - Paste the link you received into the box below to connect with your contact. - No comment provided by engineer. - - - People can connect to you only via the links you share. - No comment provided by engineer. - - - Periodically - No comment provided by engineer. - - - Please ask your contact to enable sending voice messages. - No comment provided by engineer. - - - Please check that you used the correct link or ask your contact to send you another one. - No comment provided by engineer. - - - Please check your network connection with %@ and try again. - No comment provided by engineer. - - - Please check yours and your contact preferences. - No comment provided by engineer. - - - Please enter correct current passphrase. - No comment provided by engineer. - - - Please enter the previous password after restoring database backup. This action can not be undone. - No comment provided by engineer. - - - Please restart the app and migrate the database to enable push notifications. - No comment provided by engineer. - - - Please store passphrase securely, you will NOT be able to access chat if you lose it. - No comment provided by engineer. - - - Please store passphrase securely, you will NOT be able to change it if you lose it. - No comment provided by engineer. - - - Possibly, certificate fingerprint in server address is incorrect - server test error - - - Preset server - No comment provided by engineer. - - - Preset server address - No comment provided by engineer. - - - Privacy & security - No comment provided by engineer. - - - Privacy redefined - No comment provided by engineer. - - - Profile and server connections - No comment provided by engineer. - - - Profile image - No comment provided by engineer. - - - Prohibit irreversible message deletion. - No comment provided by engineer. - - - Prohibit sending direct messages to members. - No comment provided by engineer. - - - Prohibit sending disappearing messages. - No comment provided by engineer. - - - Prohibit sending voice messages. - No comment provided by engineer. - - - Protect app screen - No comment provided by engineer. - - - Protocol timeout - No comment provided by engineer. - - - Push notifications - No comment provided by engineer. - - - Rate the app - No comment provided by engineer. - - - Read - No comment provided by engineer. - - - Read more in our GitHub repository. - No comment provided by engineer. - - - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). - No comment provided by engineer. - - - Received file event - notification - - - Receiving via - No comment provided by engineer. - - - Recipients see updates as you type them. - No comment provided by engineer. - - - Reject - reject incoming call via notification - - - Reject contact (sender NOT notified) - No comment provided by engineer. - - - Reject contact request - No comment provided by engineer. - - - Relay server is only used if necessary. Another party can observe your IP address. - No comment provided by engineer. - - - Relay server protects your IP address, but it can observe the duration of the call. - No comment provided by engineer. - - - Remove - No comment provided by engineer. - - - Remove member - No comment provided by engineer. - - - Remove member? - No comment provided by engineer. - - - Remove passphrase from keychain? - No comment provided by engineer. - - - Reply - chat item action - - - Required - No comment provided by engineer. - - - Reset - No comment provided by engineer. - - - Reset colors - No comment provided by engineer. - - - Reset to defaults - No comment provided by engineer. - - - Restart the app to create a new chat profile - No comment provided by engineer. - - - Restart the app to use imported chat database - No comment provided by engineer. - - - Restore - No comment provided by engineer. - - - Restore database backup - No comment provided by engineer. - - - Restore database backup? - No comment provided by engineer. - - - Restore database error - No comment provided by engineer. - - - Reveal - chat item action - - - Revert - No comment provided by engineer. - - - Role - No comment provided by engineer. - - - Run chat - No comment provided by engineer. - - - SMP servers - No comment provided by engineer. - - - Save - chat item action - - - Save (and notify contacts) - No comment provided by engineer. - - - Save and notify contact - No comment provided by engineer. - - - Save and notify group members - No comment provided by engineer. - - - Save archive - No comment provided by engineer. - - - Save group profile - No comment provided by engineer. - - - Save passphrase and open chat - No comment provided by engineer. - - - Save passphrase in Keychain - No comment provided by engineer. - - - Save preferences? - No comment provided by engineer. - - - Save servers - No comment provided by engineer. - - - Saved WebRTC ICE servers will be removed - No comment provided by engineer. - - - Scan QR code - No comment provided by engineer. - - - Scan code - No comment provided by engineer. - - - Scan security code from your contact's app. - No comment provided by engineer. - - - Scan server QR code - No comment provided by engineer. - - - Search - No comment provided by engineer. - - - Secure queue - server test step - - - Security assessment - No comment provided by engineer. - - - Security code - No comment provided by engineer. - - - Send - No comment provided by engineer. - - - Send a live message - it will update for the recipient(s) as you type it - No comment provided by engineer. - - - Send direct message - No comment provided by engineer. - - - Send link previews - No comment provided by engineer. - - - Send live message - No comment provided by engineer. - - - Send notifications - No comment provided by engineer. - - - Send notifications: - No comment provided by engineer. - - - Send questions and ideas - No comment provided by engineer. - - - Send them from gallery or custom keyboards. - No comment provided by engineer. - - - Sender cancelled file transfer. - No comment provided by engineer. - - - Sender may have deleted the connection request. - No comment provided by engineer. - - - Sending via - No comment provided by engineer. - - - Sent file event - notification - - - Sent messages will be deleted after set time. - No comment provided by engineer. - - - Server requires authorization to create queues, check password - server test error - - - Server test failed! - No comment provided by engineer. - - - Servers - No comment provided by engineer. - - - Set 1 day - No comment provided by engineer. - - - Set contact name… - No comment provided by engineer. - - - Set group preferences - No comment provided by engineer. - - - Set passphrase to export - No comment provided by engineer. - - - Set timeouts for proxy/VPN - No comment provided by engineer. - - - Settings - No comment provided by engineer. - - - Share - chat item action - - - Share invitation link - No comment provided by engineer. - - - Share link - No comment provided by engineer. - - - Share one-time invitation link - No comment provided by engineer. - - - Show QR code - No comment provided by engineer. - - - Show preview - No comment provided by engineer. - - - SimpleX Chat security was audited by Trail of Bits. - No comment provided by engineer. - - - SimpleX Lock - No comment provided by engineer. - - - SimpleX Lock turned on - No comment provided by engineer. - - - SimpleX contact address - simplex link type - - - SimpleX encrypted message or connection event - notification - - - SimpleX group link - simplex link type - - - SimpleX links - No comment provided by engineer. - - - SimpleX one-time invitation - simplex link type - - - Skip - No comment provided by engineer. - - - Skipped messages - No comment provided by engineer. - - - Somebody - notification title - - - Start a new chat - No comment provided by engineer. - - - Start chat - No comment provided by engineer. - - - Start migration - No comment provided by engineer. - - - Stop - No comment provided by engineer. - - - Stop SimpleX - authentication reason - - - Stop chat to enable database actions - No comment provided by engineer. - - - Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. - No comment provided by engineer. - - - Stop chat? - No comment provided by engineer. - - - Support SimpleX Chat - No comment provided by engineer. - - - System - No comment provided by engineer. - - - TCP connection timeout - No comment provided by engineer. - - - TCP_KEEPCNT - No comment provided by engineer. - - - TCP_KEEPIDLE - No comment provided by engineer. - - - TCP_KEEPINTVL - No comment provided by engineer. - - - Take picture - No comment provided by engineer. - - - Tap button - No comment provided by engineer. - - - Tap to join - No comment provided by engineer. - - - Tap to join incognito - No comment provided by engineer. - - - Tap to start a new chat - No comment provided by engineer. - - - Test failed at step %@. - server test failure - - - Test server - No comment provided by engineer. - - - Test servers - No comment provided by engineer. - - - Tests failed! - No comment provided by engineer. - - - Thank you for installing SimpleX Chat! - No comment provided by engineer. - - - The 1st platform without any user identifiers – private by design. - No comment provided by engineer. - - - The app can notify you when you receive messages or contact requests - please open settings to enable. - No comment provided by engineer. - - - The attempt to change database passphrase was not completed. - No comment provided by engineer. - - - The connection you accepted will be cancelled! - No comment provided by engineer. - - - The contact you shared this link with will NOT be able to connect! - No comment provided by engineer. - - - The created archive is available via app Settings / Database / Old database archive. - No comment provided by engineer. - - - The group is fully decentralized – it is visible only to the members. - No comment provided by engineer. - - - The microphone does not work when the app is in the background. - No comment provided by engineer. - - - The next generation of private messaging - No comment provided by engineer. - - - The old database was not removed during the migration, it can be deleted. - No comment provided by engineer. - - - The profile is only shared with your contacts. - No comment provided by engineer. - - - The sender will NOT be notified - No comment provided by engineer. - - - The servers for new connections of your current chat profile **%@**. - No comment provided by engineer. - - - Theme - No comment provided by engineer. - - - This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. - No comment provided by engineer. - - - This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes. - No comment provided by engineer. - - - This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. - No comment provided by engineer. - - - This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member). - No comment provided by engineer. - - - This group no longer exists. - No comment provided by engineer. - - - This setting applies to messages in your current chat profile **%@**. - No comment provided by engineer. - - - To ask any questions and to receive updates: - No comment provided by engineer. - - - To find the profile used for an incognito connection, tap the contact or group name on top of the chat. - No comment provided by engineer. - - - To make a new connection - No comment provided by engineer. - - - To prevent the call interruption, enable Do Not Disturb mode. - No comment provided by engineer. - - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. - No comment provided by engineer. - - - To protect your information, turn on SimpleX Lock. -You will be prompted to complete authentication before this feature is enabled. - No comment provided by engineer. - - - To record voice message please grant permission to use Microphone. - No comment provided by engineer. - - - To support instant push notifications the chat database has to be migrated. - No comment provided by engineer. - - - To verify end-to-end encryption with your contact compare (or scan) the code on your devices. - No comment provided by engineer. - - - Transfer images faster - No comment provided by engineer. - - - Transport isolation - No comment provided by engineer. - - - Trying to connect to the server used to receive messages from this contact (error: %@). - No comment provided by engineer. - - - Trying to connect to the server used to receive messages from this contact. - No comment provided by engineer. - - - Turn off - No comment provided by engineer. - - - Turn off notifications? - No comment provided by engineer. - - - Turn on - No comment provided by engineer. - - - Unable to record voice message - No comment provided by engineer. - - - Unexpected error: %@ - No comment provided by engineer. - - - Unexpected migration state - No comment provided by engineer. - - - Unknown database error: %@ - No comment provided by engineer. - - - Unknown error - No comment provided by engineer. - - - Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. -To connect, please ask your contact to create another connection link and check that you have a stable network connection. - No comment provided by engineer. - - - Unlock - authentication reason - - - Unmute - No comment provided by engineer. - - - Unread - No comment provided by engineer. - - - Update - No comment provided by engineer. - - - Update .onion hosts setting? - No comment provided by engineer. - - - Update database passphrase - No comment provided by engineer. - - - Update network settings? - No comment provided by engineer. - - - Update transport isolation mode? - No comment provided by engineer. - - - Updating settings will re-connect the client to all servers. - No comment provided by engineer. - - - Updating this setting will re-connect the client to all servers. - No comment provided by engineer. - - - Use .onion hosts - No comment provided by engineer. - - - Use SimpleX Chat servers? - No comment provided by engineer. - - - Use chat - No comment provided by engineer. - - - Use for new connections - No comment provided by engineer. - - - Use server - No comment provided by engineer. - - - User profile - No comment provided by engineer. - - - Using .onion hosts requires compatible VPN provider. - No comment provided by engineer. - - - Using SimpleX Chat servers. - No comment provided by engineer. - - - Verify connection security - No comment provided by engineer. - - - Verify security code - No comment provided by engineer. - - - Via browser - No comment provided by engineer. - - - Video call - No comment provided by engineer. - - - View security code - No comment provided by engineer. - - - Voice messages - chat feature - - - Voice messages are prohibited in this chat. - No comment provided by engineer. - - - Voice messages are prohibited in this group. - No comment provided by engineer. - - - Voice messages prohibited! - No comment provided by engineer. - - - Voice message… - No comment provided by engineer. - - - Waiting for file - No comment provided by engineer. - - - Waiting for image - No comment provided by engineer. - - - WebRTC ICE servers - No comment provided by engineer. - - - Welcome %@! - No comment provided by engineer. - - - Welcome message - No comment provided by engineer. - - - What's new - No comment provided by engineer. - - - When available - No comment provided by engineer. - - - When you share an incognito profile with somebody, this profile will be used for the groups they invite you to. - No comment provided by engineer. - - - With optional welcome message. - No comment provided by engineer. - - - Wrong database passphrase - No comment provided by engineer. - - - Wrong passphrase! - No comment provided by engineer. - - - You - No comment provided by engineer. - - - You accepted connection - No comment provided by engineer. - - - You allow - No comment provided by engineer. - - - You are already connected to %@. - No comment provided by engineer. - - - You are connected to the server used to receive messages from this contact. - No comment provided by engineer. - - - You are invited to group - No comment provided by engineer. - - - You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button. - No comment provided by engineer. - - - You can now send messages to %@ - notification body - - - You can set lock screen notification preview via settings. - No comment provided by engineer. - - - You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it. - No comment provided by engineer. - - - You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it. - No comment provided by engineer. - - - You can start chat via app Settings / Database or by restarting the app - No comment provided by engineer. - - - You can use markdown to format messages: - No comment provided by engineer. - - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - No comment provided by engineer. - - - You could not be verified; please try again. - No comment provided by engineer. - - - You have no chats - No comment provided by engineer. - - - You have to enter passphrase every time the app starts - it is not stored on the device. - No comment provided by engineer. - - - You invited your contact - No comment provided by engineer. - - - You joined this group - No comment provided by engineer. - - - You joined this group. Connecting to inviting group member. - No comment provided by engineer. - - - You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. - No comment provided by engineer. - - - You need to allow your contact to send voice messages to be able to send them. - No comment provided by engineer. - - - You rejected group invitation - No comment provided by engineer. - - - You sent group invitation - No comment provided by engineer. - - - You will be connected to group when the group host's device is online, please wait or check later! - No comment provided by engineer. - - - You will be connected when your connection request is accepted, please wait or check later! - No comment provided by engineer. - - - You will be connected when your contact's device is online, please wait or check later! - No comment provided by engineer. - - - You will be required to authenticate when you start or resume the app after 30 seconds in background. - No comment provided by engineer. - - - You will join a group this link refers to and connect to its group members. - No comment provided by engineer. - - - You will stop receiving messages from this group. Chat history will be preserved. - No comment provided by engineer. - - - You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile - No comment provided by engineer. - - - You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed - No comment provided by engineer. - - - Your ICE servers - No comment provided by engineer. - - - Your SMP servers - No comment provided by engineer. - - - Your SimpleX contact address - No comment provided by engineer. - - - Your calls - No comment provided by engineer. - - - Your chat database - No comment provided by engineer. - - - Your chat database is not encrypted - set passphrase to encrypt it. - No comment provided by engineer. - - - Your chat profile - No comment provided by engineer. - - - Your chat profile will be sent to group members - No comment provided by engineer. - - - Your chat profile will be sent to your contact - No comment provided by engineer. - - - Your chat profiles - No comment provided by engineer. - - - Your chat profiles are stored locally, only on your device. - No comment provided by engineer. - - - Your chats - No comment provided by engineer. - - - Your contact address - No comment provided by engineer. - - - Your contact can scan it from the app. - No comment provided by engineer. - - - Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link). - No comment provided by engineer. - - - Your contact sent a file that is larger than currently supported maximum size (%@). - No comment provided by engineer. - - - Your contacts can allow full message deletion. - No comment provided by engineer. - - - Your current chat database will be DELETED and REPLACED with the imported one. - No comment provided by engineer. - - - Your current profile - No comment provided by engineer. - - - Your preferences - No comment provided by engineer. - - - Your privacy - No comment provided by engineer. - - - Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile. - No comment provided by engineer. - - - Your profile will be sent to the contact that you received this link from - No comment provided by engineer. - - - Your profile, contacts and delivered messages are stored on your device. - No comment provided by engineer. - - - Your random profile - No comment provided by engineer. - - - Your server - No comment provided by engineer. - - - Your server address - No comment provided by engineer. - - - Your settings - No comment provided by engineer. - - - [Contribute](https://github.com/simplex-chat/simplex-chat#contribute) - No comment provided by engineer. - - - [Send us email](mailto:chat@simplex.chat) - No comment provided by engineer. - - - [Star on GitHub](https://github.com/simplex-chat/simplex-chat) - No comment provided by engineer. - - - \_italic_ - No comment provided by engineer. - - - \`a + b` - No comment provided by engineer. - - - above, then choose: - No comment provided by engineer. - - - accepted call - call status - - - admin - member role - - - always - pref value - - - audio call (not e2e encrypted) - No comment provided by engineer. - - - bad message ID - integrity error chat item - - - bad message hash - integrity error chat item - - - bold - No comment provided by engineer. - - - call error - call status - - - call in progress - call status - - - calling… - call status - - - cancelled %@ - feature offered item - - - changed address for you - chat item text - - - changed role of %1$@ to %2$@ - rcv group event chat item - - - changed your role to %@ - rcv group event chat item - - - changing address for %@... - chat item text - - - changing address... - chat item text - - - colored - No comment provided by engineer. - - - complete - No comment provided by engineer. - - - connect to SimpleX Chat developers. - No comment provided by engineer. - - - connected - No comment provided by engineer. - - - connecting - No comment provided by engineer. - - - connecting (accepted) - No comment provided by engineer. - - - connecting (announced) - No comment provided by engineer. - - - connecting (introduced) - No comment provided by engineer. - - - connecting (introduction invitation) - No comment provided by engineer. - - - connecting call… - call status - - - connecting… - chat list item title - - - connection established - chat list item title (it should not be shown - - - connection:%@ - connection information - - - contact has e2e encryption - No comment provided by engineer. - - - contact has no e2e encryption - No comment provided by engineer. - - - creator - No comment provided by engineer. - - - default (%@) - pref value - - - deleted - deleted chat item - - - deleted group - rcv group event chat item - - - direct - connection level description - - - duplicate message - integrity error chat item - - - e2e encrypted - No comment provided by engineer. - - - enabled - enabled status - - - enabled for contact - enabled status - - - enabled for you - enabled status - - - ended - No comment provided by engineer. - - - ended call %@ - call status - - - error - No comment provided by engineer. - - - group deleted - No comment provided by engineer. - - - group profile updated - snd group event chat item - - - iOS Keychain is used to securely store passphrase - it allows receiving push notifications. - No comment provided by engineer. - - - iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications. - No comment provided by engineer. - - - incognito via contact address link - chat list item description - - - incognito via group link - chat list item description - - - incognito via one-time link - chat list item description - - - indirect (%d) - connection level description - - - invalid chat - invalid chat data - - - invalid chat data - No comment provided by engineer. - - - invalid data - invalid chat item - - - invitation to group %@ - group name - - - invited - No comment provided by engineer. - - - invited %@ - rcv group event chat item - - - invited to connect - chat list item title - - - invited via your group link - rcv group event chat item - - - italic - No comment provided by engineer. - - - join as %@ - No comment provided by engineer. - - - left - rcv group event chat item - - - marked deleted - marked deleted chat item preview text - - - member - member role - - - connected - rcv group event chat item - - - message received - notification - - - missed call - call status - - - never - No comment provided by engineer. - - - new message - notification - - - no - pref value - - - no e2e encryption - No comment provided by engineer. - - - off - enabled status - group pref value - - - offered %@ - feature offered item - - - offered %1$@: %2$@ - feature offered item - - - on - group pref value - - - or chat with the developers - No comment provided by engineer. - - - owner - member role - - - peer-to-peer - No comment provided by engineer. - - - received answer… - No comment provided by engineer. - - - received confirmation… - No comment provided by engineer. - - - rejected call - call status - - - removed - No comment provided by engineer. - - - removed %@ - rcv group event chat item - - - removed you - rcv group event chat item - - - sec - network option - - - secret - No comment provided by engineer. - - - starting… - No comment provided by engineer. - - - strike - No comment provided by engineer. - - - this contact - notification title - - - unknown - connection info - - - updated group profile - rcv group event chat item - - - v%@ (%@) - No comment provided by engineer. - - - via contact address link - chat list item description - - - via group link - chat list item description - - - via one-time link - chat list item description - - - via relay - No comment provided by engineer. - - - video call (not e2e encrypted) - No comment provided by engineer. - - - waiting for answer… - No comment provided by engineer. - - - waiting for confirmation… - No comment provided by engineer. - - - wants to connect to you! - No comment provided by engineer. - - - yes - pref value - - - you are invited to group - No comment provided by engineer. - - - you changed address - chat item text - - - you changed address for %@ - chat item text - - - you changed role for yourself to %@ - snd group event chat item - - - you changed role of %1$@ to %2$@ - snd group event chat item - - - you left - snd group event chat item - - - you removed %@ - snd group event chat item - - - you shared one-time link - chat list item description - - - you shared one-time link incognito - chat list item description - - - you: - No comment provided by engineer. - - - \~strike~ - No comment provided by engineer. - - -
- -
- -
- - - SimpleX - Bundle name - - - SimpleX needs camera access to scan QR codes to connect to other users and for video calls. - Privacy - Camera Usage Description - - - SimpleX uses Face ID for local authentication - Privacy - Face ID Usage Description - - - SimpleX needs microphone access for audio and video calls, and to record voice messages. - Privacy - Microphone Usage Description - - - SimpleX needs access to Photo Library for saving captured and received media - Privacy - Photo Library Additions Usage Description - - -
- -
- -
- - - SimpleX NSE - Bundle display name - - - SimpleX NSE - Bundle name - - - Copyright © 2022 SimpleX Chat. All rights reserved. - Copyright (human-readable) - - -
-
diff --git a/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff b/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff index abf15ee42d..50f5536e5e 100644 --- a/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff +++ b/apps/ios/SimpleX Localizations/hr.xcloc/Localized Contents/hr.xliff @@ -367,8 +367,8 @@ Add servers by scanning QR codes. No comment provided by engineer.
- - Add server… + + Add server No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index ad3148d891..f7328eed91 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff @@ -2,7 +2,7 @@
- +
@@ -127,11 +127,6 @@ %@ ellenőrizve No comment provided by engineer. - - %@ servers - %@ kiszolgáló - No comment provided by engineer. - %@ uploaded %@ feltöltve @@ -224,12 +219,12 @@ %lld messages blocked - %lld üzenet blokkolva + %lld üzenet letiltva No comment provided by engineer. %lld messages blocked by admin - %lld üzenet blokkolva az admin által + %lld üzenet letiltva az admin által No comment provided by engineer. @@ -334,7 +329,7 @@ **Add new contact**: to create your one-time QR Code or link for your contact. - **Új ismerős hozzáadása**: egyszer használatos QR-kód vagy hivatkozás létrehozása a kapcsolattartóhoz. + **Új ismerős hozzáadása**: egyszer használatos QR-kód vagy hivatkozás létrehozása az ismerőse számára. No comment provided by engineer. @@ -349,12 +344,12 @@ **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). - **Legprivátabb**: ne használja a SimpleX Chat értesítési szervert, rendszeresen ellenőrizze az üzeneteket a háttérben (attól függően, hogy milyen gyakran használja az alkalmazást). + **Legprivátabb**: ne használja a SimpleX Chat értesítési kiszolgálót, rendszeresen ellenőrizze az üzeneteket a háttérben (attól függően, hogy milyen gyakran használja az alkalmazást). No comment provided by engineer. **Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection. - **Megjegyzés**: ha két eszközön is ugyanazt az adatbázist használja, akkor biztonsági védelemként megszakítja a kapcsolataiból érkező üzenetek visszafejtését. + **Megjegyzés**: ha két eszközön is ugyanazt az adatbázist használja, akkor biztonsági védelemként megszakítja az ismerőseitől érkező üzenetek visszafejtését. No comment provided by engineer. @@ -364,12 +359,12 @@ **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. - **Javasolt**: az eszköztoken és az értesítések elküldésre kerülnek a SimpleX Chat értesítési szerverre, kivéve az üzenet tartalma, mérete vagy az, hogy kitől származik. + **Javasolt**: az eszköztoken és az értesítések elküldésre kerülnek a SimpleX Chat értesítési kiszolgálóra, kivéve az üzenet tartalma, mérete vagy az, hogy kitől származik. No comment provided by engineer. **Warning**: Instant push notifications require passphrase saved in Keychain. - **Figyelmeztetés**: Az azonnali push-értesítésekhez a kulcstárolóban tárolt jelmondat megadása szükséges. + **Figyelmeztetés**: Az azonnali push-értesítésekhez a kulcstartóban tárolt jelmondat megadása szükséges. No comment provided by engineer. @@ -497,7 +492,7 @@ <p>Hi!</p> <p><a href="%@">Connect to me via SimpleX Chat</a></p> <p>Üdvözlöm!</p> -<p><a href="%@">Csatlakozzon hozzám a SimpleX Chaten</a></p> +<p><a href=„%@”>Csatlakozzon hozzám a SimpleX Chaten</a></p> email text @@ -544,33 +539,34 @@ About SimpleX - A SimpleX névjegye + A SimpleX-ről No comment provided by engineer. About SimpleX Chat - A SimpleX Chat névjegye + A SimpleX Chat-ről No comment provided by engineer. About SimpleX address - A SimpleX azonosítóról + A SimpleX címről No comment provided by engineer. - - Accent color - Kiemelő szín + + Accent + Kiemelés No comment provided by engineer. Accept Elfogadás accept contact request via notification - accept incoming call via notification + accept incoming call via notification + swipe action Accept connection request? - Kapcsolatfelvétel elfogadása? + Kapcsolódási kérelem elfogadása? No comment provided by engineer. @@ -581,11 +577,27 @@ Accept incognito Fogadás inkognítóban - accept contact request via notification + accept contact request via notification + swipe action + + + Acknowledged + Nyugtázva + No comment provided by engineer. + + + Acknowledgement errors + Nyugtázott hibák + No comment provided by engineer. + + + Active connections + Aktív kapcsolatok száma + No comment provided by engineer. Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. - Azonosító hozzáadása a profilhoz, hogy az ismerősei megoszthassák másokkal. A profilfrissítés elküldésre kerül az ismerősei számára. + Cím hozzáadása a profilhoz, hogy az ismerősei megoszthassák másokkal. A profilfrissítés elküldésre kerül az ismerősei számára. No comment provided by engineer. @@ -603,16 +615,16 @@ Profil hozzáadása No comment provided by engineer. + + Add server + Kiszolgáló hozzáadása + No comment provided by engineer. + Add servers by scanning QR codes. Kiszolgáló hozzáadása QR-kód beolvasásával. No comment provided by engineer. - - Add server… - Kiszolgáló hozzáadása… - No comment provided by engineer. - Add to another device Hozzáadás egy másik eszközhöz @@ -623,6 +635,21 @@ Üdvözlő üzenet hozzáadása No comment provided by engineer. + + Additional accent + További kiemelés + No comment provided by engineer. + + + Additional accent 2 + További kiemelés 2 + No comment provided by engineer. + + + Additional secondary + További másodlagos + No comment provided by engineer. + Address Cím @@ -648,6 +675,11 @@ Speciális hálózati beállítások No comment provided by engineer. + + Advanced settings + Haladó beállítások + No comment provided by engineer. + All app data is deleted. Minden alkalmazásadat törölve. @@ -663,6 +695,11 @@ A jelkód megadása után minden adat törlésre kerül. No comment provided by engineer. + + All data is private to your device. + Minden adat biztonságban van a készülékén. + No comment provided by engineer. + All group members will remain connected. Minden csoporttag kapcsolódva marad. @@ -683,6 +720,11 @@ Minden új üzenet elrejtésre kerül tőle: %@! No comment provided by engineer. + + All profiles + Minden profil + No comment provided by engineer. + All your contacts will remain connected. Minden ismerős kapcsolódva marad. @@ -705,22 +747,32 @@ Allow calls only if your contact allows them. - Hívások engedélyezése kizárólag abban az esetben, ha ismerőse is engedélyezi. + A hívások kezdeményezése kizárólag abban az esetben van engedélyezve, ha az ismerőse is engedélyezi. + No comment provided by engineer. + + + Allow calls? + Hívások engedélyezése? No comment provided by engineer. Allow disappearing messages only if your contact allows it to you. - Eltűnő üzenetek engedélyezése kizárólag abban az esetben, ha ismerőse is engedélyezi az ön számára. + Az eltűnő üzenetek küldése kizárólag abban az esetben van engedélyezve, ha az ismerőse is engedélyezi az ön számára. + No comment provided by engineer. + + + Allow downgrade + Visszafejlesztés engedélyezése No comment provided by engineer. Allow irreversible message deletion only if your contact allows it to you. (24 hours) - Üzenet végleges törlésének engedélyezése kizárólag abban az esetben, ha ismerőse is engedélyezi. (24 óra) + Az üzenetek végleges törlése kizárólag abban az esetben van engedélyezve, ha az ismerőse is engedélyezi. (24 óra) No comment provided by engineer. Allow message reactions only if your contact allows them. - Üzenetreakciók engedélyezése kizárólag abban az esetben, ha ismerőse is engedélyezi. + Az üzenetreakciók küldése kizárólag abban az esetben van engedélyezve, ha az ismerőse is engedélyezi. No comment provided by engineer. @@ -730,12 +782,17 @@ Allow sending direct messages to members. - Közvetlen üzenetek küldésének engedélyezése a tagok számára. + A közvetlen üzenetek küldése a tagok között engedélyezve van. No comment provided by engineer. Allow sending disappearing messages. - Eltűnő üzenetek küldésének engedélyezése. + Az eltűnő üzenetek küldése engedélyezve van. + No comment provided by engineer. + + + Allow sharing + Megosztás engedélyezése No comment provided by engineer. @@ -760,7 +817,7 @@ Allow voice messages only if your contact allows them. - Hangüzenetek küldésének engedélyezése kizárólag abban az esetben, ha ismerőse is engedélyezi. + A hangüzenetek küldése kizárólag abban az esetben van engedélyezve, ha az ismerőse is engedélyezi. No comment provided by engineer. @@ -770,27 +827,27 @@ Allow your contacts adding message reactions. - Ismerősök általi üzenetreakciók hozzáadásának engedélyezése. + Az üzenetreakciók küldése engedélyezve van az ismerősei számára. No comment provided by engineer. Allow your contacts to call you. - Hívások engedélyezése ismerősök számára. + A hívások kezdeményezése engedélyezve van az ismerősei számára. No comment provided by engineer. Allow your contacts to irreversibly delete sent messages. (24 hours) - Elküldött üzenetek végleges törlésének engedélyezése az ismerősök számára. (24 óra) + Az elküldött üzenetek végleges törlése engedélyezve van az ismerősei számára. (24 óra) No comment provided by engineer. Allow your contacts to send disappearing messages. - Eltűnő üzenetek engedélyezése ismerősök számára. + Az eltűnő üzenetek küldésének engedélyezése az ismerősei számára. No comment provided by engineer. Allow your contacts to send voice messages. - Hangüzenetek küldésének engedélyezése ismerősök számára. + A hangüzenetek küldése engedélyezve van az ismerősei számára. No comment provided by engineer. @@ -808,6 +865,11 @@ A csatlakozás folyamatban van a csoporthoz! No comment provided by engineer. + + Always use private routing. + Mindig használjon privát útválasztást. + No comment provided by engineer. + Always use relay Mindig használjon átjátszó kiszolgálót @@ -873,11 +935,26 @@ Alkalmaz No comment provided by engineer. + + Apply to + Alkalmazás erre + No comment provided by engineer. + Archive and upload Archiválás és feltöltés No comment provided by engineer. + + Archive contacts to chat later. + Ismerősök archiválása a későbbi csevegéshez. + No comment provided by engineer. + + + Archived contacts + Archivált ismerősök + No comment provided by engineer. + Archiving database Adatbázis archiválása @@ -905,12 +982,12 @@ Audio/video calls are prohibited. - A hang- és videóhívások le vannak tiltva. + A hívások kezdeményezése le van tiltva ebben a csevegésben. No comment provided by engineer. Authentication cancelled - Hitelesítés megszakítva + Hitelesítés visszavonva PIN entry @@ -935,12 +1012,12 @@ Auto-accept contact requests - Ismerős jelölések automatikus elfogadása + Kapcsolódási kérelmek automatikus elfogadása No comment provided by engineer. Auto-accept images - Fotók automatikus elfogadása + Képek automatikus elfogadása No comment provided by engineer. @@ -948,9 +1025,14 @@ Vissza No comment provided by engineer. + + Background + Háttér + No comment provided by engineer. + Bad desktop address - Hibás számítógép azonosító + Hibás számítógép cím No comment provided by engineer. @@ -960,7 +1042,7 @@ Bad message hash - Téves üzenet hash + Hibás az üzenet ellenőrzőösszege No comment provided by engineer. @@ -973,34 +1055,44 @@ Jobb üzenetek No comment provided by engineer. + + Better networking + Jobb hálózatkezelés + No comment provided by engineer. + + + Black + Fekete + No comment provided by engineer. + Block - Blokkolás + Letiltás No comment provided by engineer. Block for all - Mindenki számára letiltva + Letiltás mindenki számára No comment provided by engineer. Block group members - Csoporttagok blokkolása + Csoporttagok letiltása No comment provided by engineer. Block member - Tag blokkolása + Tag letiltása No comment provided by engineer. Block member for all? - Tag letiltása mindenki számára? + Mindenki számára letiltja ezt a tagot? No comment provided by engineer. Block member? - Tag blokkolása? + Tag letiltása? No comment provided by engineer. @@ -1008,6 +1100,16 @@ Letiltva az admin által No comment provided by engineer. + + Blur for better privacy. + Elhomályosítás a jobb adatvédelemért. + No comment provided by engineer. + + + Blur media + Média elhomályosítása + No comment provided by engineer. + Both you and your contact can add message reactions. Mindkét fél is hozzáadhat üzenetreakciókat. @@ -1020,7 +1122,7 @@ Both you and your contact can make calls. - Mindkét fél tud hívásokat indítani. + Mindkét fél tud hívásokat kezdeményezni. No comment provided by engineer. @@ -1053,9 +1155,24 @@ Hívások No comment provided by engineer. + + Calls prohibited! + A hívások le vannak tiltva! + No comment provided by engineer. + Camera not available - A fényképező nem elérhető + A kamera nem elérhető + No comment provided by engineer. + + + Can't call contact + Nem lehet felhívni az ismerőst + No comment provided by engineer. + + + Can't call member + Nem lehet felhívni a tagot No comment provided by engineer. @@ -1068,6 +1185,11 @@ Ismerősök meghívása nem lehetséges! No comment provided by engineer. + + Can't message member + Nem lehet üzenetet küldeni a tagnak + No comment provided by engineer. + Cancel Mégse @@ -1083,11 +1205,21 @@ Nem lehet hozzáférni a kulcstartóhoz az adatbázis jelszavának mentéséhez No comment provided by engineer. + + Cannot forward message + Nem lehet továbbítani az üzenetet + No comment provided by engineer. + Cannot receive file Nem lehet fogadni a fájlt No comment provided by engineer. + + Capacity exceeded - recipient did not receive previously sent messages. + Kapacitás túllépés - a címzett nem kapta meg a korábban elküldött üzeneteket. + snd error text + Cellular Mobilhálózat @@ -1149,6 +1281,11 @@ Csevegési archívum No comment provided by engineer. + + Chat colors + Csevegés színei + No comment provided by engineer. + Chat console Csevegési konzol @@ -1164,6 +1301,11 @@ Csevegési adatbázis törölve No comment provided by engineer. + + Chat database exported + Csevegési adatbázis exportálva + No comment provided by engineer. + Chat database imported Csevegési adatbázis importálva @@ -1184,6 +1326,11 @@ A csevegés leállt. Ha már használta ezt az adatbázist egy másik eszközön, úgy visszaállítás szükséges a csevegés megkezdése előtt. No comment provided by engineer. + + Chat list + Csevegőlista + No comment provided by engineer. + Chat migrated! A csevegés átköltöztetve! @@ -1194,6 +1341,11 @@ Csevegési beállítások No comment provided by engineer. + + Chat theme + Csevegés témája + No comment provided by engineer. + Chats Csevegések @@ -1211,7 +1363,7 @@ Choose _Migrate from another device_ on the new device and scan QR code. - Válassza az _Átköltöztetés egy másik eszközről_ opciót az új eszközön és szkennelje be a QR-kódot. + Válassza az _Átköltöztetés egy másik eszközről_ opciót az új eszközön és olvassa be a QR-kódot. No comment provided by engineer. @@ -1224,24 +1376,39 @@ Választás a könyvtárból No comment provided by engineer. + + Chunks deleted + Törölt fájltöredékek + No comment provided by engineer. + + + Chunks downloaded + Letöltött fájltöredékek + No comment provided by engineer. + + + Chunks uploaded + Feltöltött fájltöredékek + No comment provided by engineer. + Clear Kiürítés - No comment provided by engineer. + swipe action Clear conversation - Beszélgetés kiürítése + Üzenetek kiürítése No comment provided by engineer. Clear conversation? - Beszélgetés kiürítése? + Üzenetek kiürítése? No comment provided by engineer. Clear private notes? - Privát jegyzetek törlése? + Privát jegyzetek kiürítése? No comment provided by engineer. @@ -1249,9 +1416,14 @@ Hitelesítés törlése No comment provided by engineer. - - Colors - Színek + + Color chats with the new themes. + Csevegések színezése új témákkal. + No comment provided by engineer. + + + Color mode + Színmód No comment provided by engineer. @@ -1264,11 +1436,21 @@ Biztonsági kódok összehasonlítása az ismerősökkel. No comment provided by engineer. + + Completed + Elkészült + No comment provided by engineer. + Configure ICE servers ICE kiszolgálók beállítása No comment provided by engineer. + + Configured %@ servers + Beállított %@ kiszolgálók + No comment provided by engineer. + Confirm Megerősítés @@ -1279,9 +1461,19 @@ Jelkód megerősítése No comment provided by engineer. + + Confirm contact deletion? + Biztosan törli az ismerőst? + No comment provided by engineer. + Confirm database upgrades - Adatbázis frissítés megerősítése + Adatbázis fejlesztésének megerősítése + No comment provided by engineer. + + + Confirm files from unknown servers. + Ismeretlen kiszolgálókról származó fájlok jóváhagyása. No comment provided by engineer. @@ -1329,6 +1521,11 @@ Kapcsolódás számítógéphez No comment provided by engineer. + + Connect to your friends faster. + Kapcsolódjon gyorsabban az ismerőseihez. + No comment provided by engineer. + Connect to yourself? Kapcsolódás saját magához? @@ -1338,7 +1535,7 @@ Connect to yourself? This is your own SimpleX address! Kapcsolódás saját magához? -Ez a SimpleX azonosítója! +Ez az ön SimpleX címe! No comment provided by engineer. @@ -1350,7 +1547,7 @@ Ez az egyszer használatos hivatkozása! Connect via contact address - Kapcsolódás a kapcsolattartási azonosítón keresztül + Kapcsolódás a kapcsolattartási címen keresztül No comment provided by engineer. @@ -1368,16 +1565,31 @@ Ez az egyszer használatos hivatkozása! Kapcsolódás ezzel: %@ No comment provided by engineer. + + Connected + Kapcsolódva + No comment provided by engineer. + Connected desktop Csatlakoztatott számítógép No comment provided by engineer. + + Connected servers + Kapcsolódott kiszolgálók + No comment provided by engineer. + Connected to desktop Kapcsolódva a számítógéphez No comment provided by engineer. + + Connecting + Kapcsolódás + No comment provided by engineer. + Connecting to server… Kapcsolódás a kiszolgálóhoz… @@ -1388,6 +1600,11 @@ Ez az egyszer használatos hivatkozása! Kapcsolódás a kiszolgálóhoz... (hiba: %@) No comment provided by engineer. + + Connecting to contact, please wait or check later! + Kapcsolódás az ismerőshöz, várjon vagy ellenőrizze később! + No comment provided by engineer. + Connecting to desktop Kapcsolódás a számítógéphez @@ -1398,6 +1615,11 @@ Ez az egyszer használatos hivatkozása! Kapcsolat No comment provided by engineer. + + Connection and servers status. + Kapcsolatok- és kiszolgálók állapotának megjelenítése. + No comment provided by engineer. + Connection error Kapcsolódási hiba @@ -1408,6 +1630,11 @@ Ez az egyszer használatos hivatkozása! Kapcsolódási hiba (AUTH) No comment provided by engineer. + + Connection notifications + Kapcsolódási értesítések + No comment provided by engineer. + Connection request sent! Kapcsolódási kérés elküldve! @@ -1423,6 +1650,16 @@ Ez az egyszer használatos hivatkozása! Kapcsolat időtúllépés No comment provided by engineer. + + Connection with desktop stopped + A kapcsolat a számítógéppel megszakadt + No comment provided by engineer. + + + Connections + Kapcsolatok + No comment provided by engineer. + Contact allows Ismerős engedélyezi @@ -1433,6 +1670,11 @@ Ez az egyszer használatos hivatkozása! Létező ismerős No comment provided by engineer. + + Contact deleted! + Ismerős törölve! + No comment provided by engineer. + Contact hidden: Ismerős elrejtve: @@ -1443,9 +1685,9 @@ Ez az egyszer használatos hivatkozása! Ismerőse kapcsolódott notification - - Contact is not connected yet! - Az ismerőse még nem kapcsolódott! + + Contact is deleted. + Törölt ismerős. No comment provided by engineer. @@ -1458,6 +1700,11 @@ Ez az egyszer használatos hivatkozása! Ismerős beállításai No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + Az ismerős törlésre fog kerülni - ez a művelet nem vonható vissza! + No comment provided by engineer. + Contacts Ismerősök @@ -1465,7 +1712,7 @@ Ez az egyszer használatos hivatkozása! Contacts can mark messages for deletion; you will be able to view them. - Az ismerősök törlésre jelölhetnek üzeneteket ; megtekintheti őket. + Az ismerősei törlésre jelölhetnek üzeneteket; ön majd meg tudja nézni azokat. No comment provided by engineer. @@ -1473,10 +1720,20 @@ Ez az egyszer használatos hivatkozása! Folytatás No comment provided by engineer. + + Conversation deleted! + Beszélgetés törölve! + No comment provided by engineer. + Copy Másolás - chat item action + No comment provided by engineer. + + + Copy error + Másolási hiba + No comment provided by engineer. Core version: v%@ @@ -1495,7 +1752,7 @@ Ez az egyszer használatos hivatkozása! Create SimpleX address - SimpleX azonosító létrehozása + SimpleX cím létrehozása No comment provided by engineer. @@ -1505,7 +1762,7 @@ Ez az egyszer használatos hivatkozása! Create an address to let people connect with you. - Azonosító létrehozása, hogy az emberek kapcsolatba léphessenek önnel. + Cím létrehozása, hogy az emberek kapcsolatba léphessenek önnel. No comment provided by engineer. @@ -1553,6 +1810,11 @@ Ez az egyszer használatos hivatkozása! Saját profil létrehozása No comment provided by engineer. + + Created + Létrehozva + No comment provided by engineer. + Created at Létrehozva ekkor: @@ -1588,6 +1850,11 @@ Ez az egyszer használatos hivatkozása! Jelenlegi jelmondat… No comment provided by engineer. + + Current profile + Jelenlegi profil + No comment provided by engineer. + Currently maximum supported file size is %@. Jelenleg a maximális támogatott fájlméret %@. @@ -1598,11 +1865,21 @@ Ez az egyszer használatos hivatkozása! Személyreszabott idő No comment provided by engineer. + + Customize theme + Téma személyre szabása + No comment provided by engineer. + Dark Sötét No comment provided by engineer. + + Dark mode colors + Sötét mód színei + No comment provided by engineer. + Database ID Adatbázis ID @@ -1620,7 +1897,7 @@ Ez az egyszer használatos hivatkozása! Database downgrade - Visszatérés a korábbi adatbázis verzióra + Adatbázis visszafejlesztése No comment provided by engineer. @@ -1631,7 +1908,7 @@ Ez az egyszer használatos hivatkozása! Database encryption passphrase will be updated and stored in the keychain. - Az adatbázis titkosítási jelmondata frissül és tárolódik a kulcstárolóban. + Az adatbázis titkosítási jelmondata frissül és tárolódik a kulcstartóban. No comment provided by engineer. @@ -1669,12 +1946,12 @@ Ez az egyszer használatos hivatkozása! Database passphrase is different from saved in the keychain. - Az adatbázis jelmondata eltér a kulcstárlóban mentettől. + Az adatbázis jelmondata eltér a kulcstartóban mentettől. No comment provided by engineer. Database passphrase is required to open chat. - Adatbázis jelmondat szükséges a csevegés megnyitásához. + A csevegés megnyitásához adja meg az adatbázis jelmondatát. No comment provided by engineer. @@ -1685,7 +1962,7 @@ Ez az egyszer használatos hivatkozása! Database will be encrypted and the passphrase stored in the keychain. - Az adatbázis titkosítva lesz, a jelmondat pedig a kulcstárolóban lesz tárolva. + Az adatbázis titkosítva lesz, a jelmondat pedig a kulcstartóban lesz tárolva. No comment provided by engineer. @@ -1701,6 +1978,11 @@ Ez az egyszer használatos hivatkozása! Az adatbázis az alkalmazás újraindításakor migrálásra kerül No comment provided by engineer. + + Debug delivery + Kézbesítési hibák felderítése + No comment provided by engineer. + Decentralized Decentralizált @@ -1714,31 +1996,32 @@ Ez az egyszer használatos hivatkozása! Delete Törlés - chat item action + chat item action + swipe action + + + Delete %lld messages of members? + Tagok %lld üzenetének törlése? + No comment provided by engineer. Delete %lld messages? Töröl %lld üzenetet? No comment provided by engineer. - - Delete Contact - Ismerős törlése - No comment provided by engineer. - Delete address - Azonosító törlése + Cím törlése No comment provided by engineer. Delete address? - Azonosító törlése? + Cím törlése? No comment provided by engineer. Delete after - Törlés miután + Törlés ennyi idő után No comment provided by engineer. @@ -1748,7 +2031,7 @@ Ez az egyszer használatos hivatkozása! Delete and notify contact - Törlés és ismerős értesítése + Törlés, és az ismerős értesítése No comment provided by engineer. @@ -1781,11 +2064,9 @@ Ez az egyszer használatos hivatkozása! Ismerős törlése No comment provided by engineer. - - Delete contact? -This cannot be undone! - Ismerős törlése? -Ez a művelet nem vonható vissza! + + Delete contact? + Ismerős törlése? No comment provided by engineer. @@ -1820,7 +2101,7 @@ Ez a művelet nem vonható vissza! Delete for me - Törlés nálam + Csak nálam No comment provided by engineer. @@ -1850,7 +2131,7 @@ Ez a művelet nem vonható vissza! Delete member message? - Csoporttag üzenet törlése? + Csoporttag üzenetének törlése? No comment provided by engineer. @@ -1865,7 +2146,7 @@ Ez a művelet nem vonható vissza! Delete messages after - Üzenetek törlése miután + Üzenetek törlése ennyi idő után No comment provided by engineer. @@ -1878,11 +2159,6 @@ Ez a művelet nem vonható vissza! Régi adatbázis törlése? No comment provided by engineer. - - Delete pending connection - Függőben lévő kapcsolat törlése - No comment provided by engineer. - Delete pending connection? Függő kapcsolatfelvételi kérések törlése? @@ -1898,11 +2174,26 @@ Ez a művelet nem vonható vissza! Várólista törlése server test step + + Delete up to 20 messages at once. + Legfeljebb 20 üzenet törlése egyszerre. + No comment provided by engineer. + Delete user profile? Felhasználói profil törlése? No comment provided by engineer. + + Delete without notification + Törlés értesítés nélkül + No comment provided by engineer. + + + Deleted + Törölve + No comment provided by engineer. + Deleted at Törölve ekkor: @@ -1913,6 +2204,11 @@ Ez a művelet nem vonható vissza! Törölve ekkor: %@ copied message info + + Deletion errors + Törlési hibák + No comment provided by engineer. + Delivery Kézbesítés @@ -1920,12 +2216,12 @@ Ez a művelet nem vonható vissza! Delivery receipts are disabled! - Kézbesítési igazolások kikapcsolva! + A kézbesítési jelentések ki vannak kapcsolva! No comment provided by engineer. Delivery receipts! - Kézbesítési igazolások! + Üzenet kézbesítési jelentések! No comment provided by engineer. @@ -1935,7 +2231,7 @@ Ez a művelet nem vonható vissza! Desktop address - Számítógép azonosítója + Számítógép címe No comment provided by engineer. @@ -1948,11 +2244,41 @@ Ez a művelet nem vonható vissza! Számítógépek No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + A(z) %@ célkiszolgáló címe nem kompatibilis a(z) %@ továbbító kiszolgáló beállításaival. + No comment provided by engineer. + + + Destination server error: %@ + Célkiszolgáló hiba: %@ + snd error text + + + Destination server version of %@ is incompatible with forwarding server %@. + A(z) %@ célkiszolgáló verziója nem kompatibilis a(z) %@ továbbító kiszolgálóval. + No comment provided by engineer. + + + Detailed statistics + Részletes statisztikák + No comment provided by engineer. + + + Details + Részletek + No comment provided by engineer. + Develop Fejlesztés No comment provided by engineer. + + Developer options + Fejlesztői beállítások + No comment provided by engineer. + Developer tools Fejlesztői eszközök @@ -1965,12 +2291,12 @@ Ez a művelet nem vonható vissza! Device authentication is disabled. Turning off SimpleX Lock. - Eszközhitelesítés kikapcsolva. SimpleX zárolás kikapcsolása. + A készüléken nincs beállítva a képernyőzár. A SimpleX zár ki van kapcsolva. No comment provided by engineer. Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication. - Eszközhitelesítés nem engedélyezett.A SimpleX zárolás bekapcsolható a Beállításokon keresztül, miután az eszköz hitelesítés engedélyezésre került. + A készüléken nincs beállítva a képernyőzár. A SimpleX zár az „Adatvédelem és biztonság” menüben kapcsolható be, miután beállította a képernyőzárat az eszközén. No comment provided by engineer. @@ -1985,7 +2311,7 @@ Ez a művelet nem vonható vissza! Direct messages between members are prohibited in this group. - Ebben a csoportban tiltott a tagok közötti közvetlen üzenetek küldése. + A közvetlen üzenetek küldése a tagok között le van tiltva ebben a csoportban. No comment provided by engineer. @@ -1995,7 +2321,7 @@ Ez a művelet nem vonható vissza! Disable SimpleX Lock - SimpleX zárolás kikapcsolása + SimpleX zár kikapcsolása authentication reason @@ -2003,6 +2329,11 @@ Ez a művelet nem vonható vissza! Letiltás mindenki számára No comment provided by engineer. + + Disabled + Letiltva + No comment provided by engineer. + Disappearing message Eltűnő üzenet @@ -2015,7 +2346,7 @@ Ez a művelet nem vonható vissza! Disappearing messages are prohibited in this chat. - Az eltűnő üzenetek le vannak tiltva ebben a csevegésben. + Az eltűnő üzenetek küldése le van tiltva ebben a csevegésben. No comment provided by engineer. @@ -2053,11 +2384,21 @@ Ez a művelet nem vonható vissza! Felfedezés helyi hálózaton keresztül No comment provided by engineer. + + Do NOT send messages directly, even if your or destination server does not support private routing. + Ne küldjön üzeneteket közvetlenül, még akkor sem, ha az ön kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást. + No comment provided by engineer. + Do NOT use SimpleX for emergency calls. NE használja a SimpleX-et segélyhívásokhoz. No comment provided by engineer. + + Do NOT use private routing. + Ne használjon privát útválasztást. + No comment provided by engineer. + Do it later Későbbre halaszt @@ -2070,7 +2411,7 @@ Ez a művelet nem vonható vissza! Don't create address - Ne hozzon létre azonosítót + Ne hozzon létre címet No comment provided by engineer. @@ -2085,7 +2426,7 @@ Ez a művelet nem vonható vissza! Downgrade and open chat - Visszatérés a korábbi verzióra és a csevegés megnyitása + Visszafejlesztés és a csevegés megnyitása No comment provided by engineer. @@ -2093,6 +2434,11 @@ Ez a művelet nem vonható vissza! Letöltés chat item action + + Download errors + Letöltési hibák + No comment provided by engineer. + Download failed Sikertelen letöltés @@ -2103,6 +2449,16 @@ Ez a művelet nem vonható vissza! Fájl letöltése server test step + + Downloaded + Letöltve + No comment provided by engineer. + + + Downloaded files + Letöltött fájlok + No comment provided by engineer. + Downloading archive Archívum letöltése @@ -2145,12 +2501,12 @@ Ez a művelet nem vonható vissza! Enable SimpleX Lock - SimpleX zárolás engedélyezése + SimpleX zár bekapcsolása authentication reason Enable TCP keep-alive - TCP életben tartásának engedélyezése + TCP életben tartása No comment provided by engineer. @@ -2203,6 +2559,11 @@ Ez a művelet nem vonható vissza! Önmegsemmisítő jelkód engedélyezése set passcode view + + Enabled + Engedélyezve + No comment provided by engineer. + Enabled for Engedélyezve @@ -2255,7 +2616,7 @@ Ez a művelet nem vonható vissza! Encrypted message: keychain error - Titkosított üzenet: kulcstároló hiba + Titkosított üzenet: kulcstartó hiba notification @@ -2320,12 +2681,12 @@ Ez a művelet nem vonható vissza! Enter welcome message… - Üdvözlő üzenetet megadása… + Üdvözlő üzenet megadása… placeholder Enter welcome message… (optional) - Üdvözlő üzenetet megadása… (opcionális) + Üdvözlő üzenet megadása… (opcionális) placeholder @@ -2340,7 +2701,7 @@ Ez a művelet nem vonható vissza! Error aborting address change - Hiba az azonosító megváltoztatásának megszakításakor + Hiba a cím megváltoztatásának megszakításakor No comment provided by engineer. @@ -2360,7 +2721,7 @@ Ez a művelet nem vonható vissza! Error changing address - Hiba az azonosító megváltoztatásakor + Hiba a cím megváltoztatásakor No comment provided by engineer. @@ -2373,9 +2734,14 @@ Ez a művelet nem vonható vissza! Hiba a beállítás megváltoztatásakor No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + Hiba a(z) %@ továbbító kiszolgálóhoz való kapcsolódáskor. Próbálja meg később. + No comment provided by engineer. + Error creating address - Hiba az azonosító létrehozásakor + Hiba a cím létrehozásakor No comment provided by engineer. @@ -2423,11 +2789,6 @@ Ez a művelet nem vonható vissza! Hiba a kapcsolat törlésekor No comment provided by engineer. - - Error deleting contact - Hiba az ismerős törlésekor - No comment provided by engineer. - Error deleting database Hiba az adatbázis törlésekor @@ -2473,6 +2834,11 @@ Ez a művelet nem vonható vissza! Hiba a csevegési adatbázis exportálásakor No comment provided by engineer. + + Error exporting theme: %@ + Hiba a téma exportálásakor: %@ + No comment provided by engineer. + Error importing chat database Hiba a csevegési adatbázis importálásakor @@ -2498,11 +2864,26 @@ Ez a művelet nem vonható vissza! Hiba a fájl fogadásakor No comment provided by engineer. + + Error reconnecting server + Hiba a kiszolgálóhoz való újrakapcsolódáskor + No comment provided by engineer. + + + Error reconnecting servers + Hiba a kiszolgálókhoz való újrakapcsolódáskor + No comment provided by engineer. + Error removing member Hiba a tag eltávolításakor No comment provided by engineer. + + Error resetting statistics + Hiba a statisztikák visszaállításakor + No comment provided by engineer. + Error saving %@ servers Hiba történt a %@ kiszolgálók mentése közben @@ -2515,7 +2896,7 @@ Ez a művelet nem vonható vissza! Error saving group profile - Hiba a csoport profil mentésekor + Hiba a csoportprofil mentésekor No comment provided by engineer. @@ -2525,7 +2906,7 @@ Ez a művelet nem vonható vissza! Error saving passphrase to keychain - Hiba a jelmondat kulcstárolóba történő mentésekor + Hiba a jelmondat kulcstartóba történő mentésekor No comment provided by engineer. @@ -2580,7 +2961,7 @@ Ez a művelet nem vonható vissza! Error synchronizing connection - Hiba a kapcsolat szinkronizálása során + Hiba a kapcsolat szinkronizálása közben No comment provided by engineer. @@ -2621,7 +3002,8 @@ Ez a művelet nem vonható vissza! Error: %@ Hiba: %@ - No comment provided by engineer. + file error text + snd error text Error: URL is invalid @@ -2633,6 +3015,11 @@ Ez a művelet nem vonható vissza! Hiba: nincs adatbázis fájl No comment provided by engineer. + + Errors + Hibák + No comment provided by engineer. + Even when disabled in the conversation. Akkor is, ha le van tiltva a beszélgetésben. @@ -2658,6 +3045,11 @@ Ez a művelet nem vonható vissza! Exportálási hiba: No comment provided by engineer. + + Export theme + Téma exportálása + No comment provided by engineer. + Exported database archive. Exportált adatbázis-archívum. @@ -2691,8 +3083,33 @@ Ez a művelet nem vonható vissza! Favorite Kedvenc + swipe action + + + File error + Fájlhiba No comment provided by engineer. + + File not found - most likely file was deleted or cancelled. + A fájl nem található - valószínűleg a fájlt törölték vagy visszavonták. + file error text + + + File server error: %@ + Fájlkiszolgáló hiba: %@ + file error text + + + File status + Fájlállapot + No comment provided by engineer. + + + File status: %@ + Fájlállapot: %@ + copied message info + File will be deleted from servers. A fájl törölve lesz a kiszolgálóról. @@ -2713,6 +3130,11 @@ Ez a művelet nem vonható vissza! Fájl: %@ No comment provided by engineer. + + Files + Fájlok + No comment provided by engineer. + Files & media Fájlok és média @@ -2818,6 +3240,35 @@ Ez a művelet nem vonható vissza! Továbbítva innen: No comment provided by engineer. + + Forwarding server %@ failed to connect to destination server %@. Please try later. + A(z) %@ továbbító kiszolgáló nem tudott csatlakozni a(z) %@ célkiszolgálóhoz. Próbálja meg később. + No comment provided by engineer. + + + Forwarding server address is incompatible with network settings: %@. + A továbbító kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %@. + No comment provided by engineer. + + + Forwarding server version is incompatible with network settings: %@. + A továbbító kiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + Továbbító kiszolgáló: %1$@ +Célkiszolgáló hiba:%2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + Továbbító kiszolgáló: %1$@ +Hiba: %2$@ + snd error text + Found desktop Megtalált számítógép @@ -2863,6 +3314,16 @@ Ez a művelet nem vonható vissza! GIF-ek és matricák No comment provided by engineer. + + Good afternoon! + Jó napot! + message preview + + + Good morning! + Jó reggelt! + message preview + Group Csoport @@ -2970,7 +3431,7 @@ Ez a művelet nem vonható vissza! Group profile - Csoport profil + Csoportprofil No comment provided by engineer. @@ -3080,7 +3541,7 @@ Ez a művelet nem vonható vissza! If you can't meet in person, show QR code in a video call, or share the link. - Ha nem tud személyesen találkozni, mutassa meg a QR-kódot egy videohívás során, vagy ossza meg a hivatkozást. + Ha nem tud személyesen találkozni, mutassa meg a QR-kódot egy videohívás közben, vagy ossza meg a hivatkozást. No comment provided by engineer. @@ -3090,7 +3551,7 @@ Ez a művelet nem vonható vissza! If you enter your self-destruct passcode while opening the app: - Ha az alkalmazás megnyitásakor az önmegsemmisítő jelkódot megadásra kerül: + Ha az alkalmazás megnyitásakor megadja az önmegsemmisítő jelkódot: No comment provided by engineer. @@ -3143,6 +3604,11 @@ Ez a művelet nem vonható vissza! Sikertelen importálás No comment provided by engineer. + + Import theme + Téma importálása + No comment provided by engineer. + Importing archive Archívum importálása @@ -3265,6 +3731,11 @@ Ez a művelet nem vonható vissza! Felület No comment provided by engineer. + + Interface colors + Kezelőfelület színei + No comment provided by engineer. + Invalid QR code Érvénytelen QR-kód @@ -3337,12 +3808,12 @@ Ez a művelet nem vonható vissza! Irreversible message deletion is prohibited in this chat. - Ebben a csevegésben az üzenetek végleges törlése le van tiltva. + Az üzenetek végleges törlése le van tiltva ebben a csevegésben. No comment provided by engineer. Irreversible message deletion is prohibited in this group. - Ebben a csoportban az üzenetek végleges törlése le van tiltva. + Az üzenetek végleges törlése le van tiltva ebben a csoportban. No comment provided by engineer. @@ -3352,7 +3823,7 @@ Ez a művelet nem vonható vissza! It can happen when you or your connection used the old database backup. - Ez akkor fordulhat elő, ha ön vagy a kapcsolata régi adatbázis biztonsági mentést használt. + Ez akkor fordulhat elő, ha ön vagy az ismerőse régi adatbázis biztonsági mentést használt. No comment provided by engineer. @@ -3366,6 +3837,11 @@ Ez a művelet nem vonható vissza! 3. A kapcsolat sérült. No comment provided by engineer. + + It protects your IP address and connections. + Védi az IP-címét és a kapcsolatokat. + No comment provided by engineer. + It seems like you are already connected via this link. If it is not the case, there was an error (%@). Úgy tűnik, már kapcsolódott ezen a hivatkozáson keresztül. Ha ez nem így van, akkor hiba történt (%@). @@ -3384,7 +3860,7 @@ Ez a művelet nem vonható vissza! Join Csatlakozás - No comment provided by engineer. + swipe action Join group @@ -3428,6 +3904,11 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Megtart No comment provided by engineer. + + Keep conversation + Beszélgetés megtartása + No comment provided by engineer. + Keep the app open to use it from desktop A számítógépről való használathoz tartsd nyitva az alkalmazást @@ -3445,12 +3926,12 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! KeyChain error - Kulcstároló hiba + Kulcstartó hiba No comment provided by engineer. Keychain error - Kulcstároló hiba + Kulcstartó hiba No comment provided by engineer. @@ -3470,8 +3951,8 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Leave - Elhagy - No comment provided by engineer. + Elhagyás + swipe action Leave group @@ -3565,12 +4046,12 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@). - Győződjön meg arról, hogy a %@ szervercímek megfelelő formátumúak, sorszeparáltak és nem duplikáltak (%@). + Győződjön meg arról, hogy a %@ kiszolgálócímek megfelelő formátumúak, sorszeparáltak és nincsenek duplikálva (%@). No comment provided by engineer. Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated. - Győződjön meg arról, hogy a WebRTC ICE-kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nem duplikáltak. + Győződjön meg arról, hogy a WebRTC ICE-kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nincsenek duplikálva. No comment provided by engineer. @@ -3585,12 +4066,12 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Mark read - Megjelölés olvasottként + Olvasottnak jelölés No comment provided by engineer. Mark verified - Ellenőrzöttként jelölve + Hitelesítés No comment provided by engineer. @@ -3603,19 +4084,34 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Max. 30 másodperc, azonnal érkezett. No comment provided by engineer. + + Media & file servers + Média és fájlkiszolgálók + No comment provided by engineer. + + + Medium + Közepes + blur media + Member Tag No comment provided by engineer. + + Member inactive + Inaktív tag + item status text + Member role will be changed to "%@". All group members will be notified. - A tag szerepköre meg fog változni erre: "%@". A csoport minden tagja értesítést kap róla. + A tag szerepköre meg fog változni erre: „%@”. A csoport minden tagja értesítést kap róla. No comment provided by engineer. Member role will be changed to "%@". The member will receive a new invitation. - A tag szerepköre meg fog változni erre: "%@". A tag új meghívást fog kapni. + A tag szerepköre meg fog változni erre: „%@”. A tag új meghívást fog kapni. No comment provided by engineer. @@ -3623,6 +4119,11 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! A tag eltávolítása a csoportból - ez a művelet nem vonható vissza! No comment provided by engineer. + + Menus + Menük + No comment provided by engineer. + Message delivery error Üzenetkézbesítési hiba @@ -3630,14 +4131,34 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Message delivery receipts! - Üzenetkézbesítési bizonylatok! + Üzenet kézbesítési jelentések! No comment provided by engineer. + + Message delivery warning + Üzenet kézbesítési figyelmeztetés + item status text + Message draft Üzenetvázlat No comment provided by engineer. + + Message forwarded + Továbbított üzenet + item status text + + + Message may be delivered later if member becomes active. + Az üzenet később is kézbesíthető, ha a tag aktívvá válik. + item status description + + + Message queue info + Üzenet-várakoztatási információ + No comment provided by engineer. + Message reactions Üzenetreakciók @@ -3645,12 +4166,22 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Message reactions are prohibited in this chat. - Az üzenetreakciók ebben a csevegésben le vannak tiltva. + Az üzenetreakciók küldése le van tiltva ebben a csevegésben. No comment provided by engineer. Message reactions are prohibited in this group. - Ebben a csoportban az üzenetreakciók le vannak tiltva. + Az üzenetreakciók küldése le van tiltva ebben a csoportban. + No comment provided by engineer. + + + Message reception + Üzenet kézbesítési jelentés + No comment provided by engineer. + + + Message servers + Üzenetkiszolgálók No comment provided by engineer. @@ -3658,6 +4189,16 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Az üzenet forrása titokban marad. No comment provided by engineer. + + Message status + Üzenetállapot + No comment provided by engineer. + + + Message status: %@ + Üzenetállapot: %@ + copied message info + Message text Üzenet szövege @@ -3683,14 +4224,24 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! A(z) %@ által írt üzenetek megjelennek! No comment provided by engineer. + + Messages received + Fogadott üzenetek + No comment provided by engineer. + + + Messages sent + Elküldött üzenetek + No comment provided by engineer. + Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. - Az üzeneteket, fájlokat és hívásokat **végpontok közötti titkosítással** és sérülés utáni titkosságvédelemmel, visszautasítással és sérülés utáni helyreállítással védi. + Az üzeneteket, fájlokat és hívásokat **végpontok közötti titkosítással**, sérülés utáni titkosság-védelemmel és -helyreállítással, továbbá visszautasítással védi. No comment provided by engineer. Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery. - Az üzeneteket, fájlokat és hívásokat **végpontok közötti kvantumrezisztens titkosítással** és sérülés utáni titkosságvédelemmel, visszautasítással és sérülés utáni helyreállítással védi. + Az üzeneteket, fájlokat és hívásokat **végpontok közötti kvantumrezisztens titkosítással**, sérülés utáni titkosság-védelemmel és -helyreállítással, továbbá visszautasítással védi. No comment provided by engineer. @@ -3783,11 +4334,6 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Valószínűleg ez a kapcsolat törlésre került. item status description - - Most likely this contact has deleted the connection with you. - Valószínűleg ez az ismerős törölte önnel a kapcsolatot. - No comment provided by engineer. - Multiple chat profiles Több csevegőprofil @@ -3795,8 +4341,8 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Mute - Elnémítás - No comment provided by engineer. + Némítás + swipe action Muted when inactive! @@ -3806,7 +4352,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Name Név - No comment provided by engineer. + swipe action Network & servers @@ -3818,6 +4364,11 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Internetkapcsolat No comment provided by engineer. + + Network issues - message expired after many attempts to send it. + Hálózati problémák - az üzenet többszöri elküldési kísérlet után lejárt. + snd error text + Network management Hálózatkezelés @@ -3843,6 +4394,11 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Új beszélgetés No comment provided by engineer. + + New chat experience 🎉 + Új csevegési élmény 🎉 + No comment provided by engineer. + New contact request Új kapcsolattartási kérelem @@ -3873,6 +4429,11 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Újdonságok a(z) %@ verzióban No comment provided by engineer. + + New media options + Új médiabeállítások + No comment provided by engineer. + New member role Új tag szerepköre @@ -3918,6 +4479,11 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Nincs eszköztoken! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + Még nincs közvetlen kapcsolat, az üzenetet az admin továbbítja. + item status description + No filtered chats Nincsenek szűrt csevegések @@ -3933,6 +4499,11 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Nincsenek előzmények No comment provided by engineer. + + No info, try to reload + Nincs információ, próbálja meg újratölteni + No comment provided by engineer. + No network connection Nincs hálózati kapcsolat @@ -3953,6 +4524,11 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Nem kompatibilis! No comment provided by engineer. + + Nothing selected + Semmi sincs kiválasztva + No comment provided by engineer. + Notifications Értesítések @@ -3969,7 +4545,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! - disable members ("observer" role) Most már az adminok is: - törölhetik a tagok üzeneteit. -- letilthatnak tagokat ("megfigyelő" szerepkör) +- letilthatnak tagokat („megfigyelő” szerepkör) No comment provided by engineer. @@ -3979,8 +4555,8 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Off - Ki - No comment provided by engineer. + Kikapcsolva + blur media Ok @@ -4002,14 +4578,18 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Egyszer használatos meghívó hivatkozás No comment provided by engineer. - - Onion hosts will be required for connection. Requires enabling VPN. - A kapcsolódáshoz Onion kiszolgálókra lesz szükség. VPN engedélyezése szükséges. + + Onion hosts will be **required** for connection. +Requires compatible VPN. + A kapcsolódáshoz Onion kiszolgálókra lesz szükség. +VPN engedélyezése szükséges. No comment provided by engineer. - - Onion hosts will be used when available. Requires enabling VPN. - Onion kiszolgálók használata, ha azok rendelkezésre állnak. VPN engedélyezése szükséges. + + Onion hosts will be used when available. +Requires compatible VPN. + Onion kiszolgálók használata, ha azok rendelkezésre állnak. +VPN engedélyezése szükséges. No comment provided by engineer. @@ -4022,6 +4602,11 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Csak a klienseszközök tárolják a felhasználói profilokat, névjegyeket, csoportokat és a **2 rétegű végponttól-végpontig titkosítással** küldött üzeneteket. No comment provided by engineer. + + Only delete conversation + Csak a beszélgetés törlése + No comment provided by engineer. + Only group owners can change group preferences. Csak a csoporttulajdonosok módosíthatják a csoportbeállításokat. @@ -4079,7 +4664,7 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Only your contact can send disappearing messages. - Csak az ismerős tud eltűnő üzeneteket küldeni. + Csak az ismerőse tud eltűnő üzeneteket küldeni. No comment provided by engineer. @@ -4117,6 +4702,11 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Átköltöztetés megkezdése egy másik eszközre authentication reason + + Open server settings + Kiszolgáló beállításainak megnyitása + No comment provided by engineer. + Open user profiles Felhasználói profilok megnyitása @@ -4157,6 +4747,11 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! További No comment provided by engineer. + + Other %@ servers + További %@ kiszolgálók + No comment provided by engineer. + PING count PING számláló @@ -4199,12 +4794,12 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Past member %@ - Korábbi csoport tag %@ + Már nem tag - %@ past/unknown group member Paste desktop address - Számítógép azonosítójának beillesztése + Számítógép címének beillesztése No comment provided by engineer. @@ -4222,6 +4817,11 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Fogadott hivatkozás beillesztése No comment provided by engineer. + + Pending + Függő + No comment provided by engineer. + People can connect to you only via the links you share. Az emberek csak az ön által megosztott hivatkozáson keresztül kapcsolódhatnak. @@ -4242,11 +4842,28 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Kép a képben hívások No comment provided by engineer. + + Play from the chat list. + Lejátszás a csevegési listából. + No comment provided by engineer. + + + Please ask your contact to enable calls. + Kérje meg az ismerősét, hogy engedélyezze a hívásokat. + No comment provided by engineer. + Please ask your contact to enable sending voice messages. Ismerős felkérése, hogy engedélyezze a hangüzenetek küldését. No comment provided by engineer. + + Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers. + Ellenőrizze, hogy a mobil és az asztali számítógép ugyanahhoz a helyi hálózathoz csatlakozik-e, valamint az asztali számítógép tűzfalában engedélyezve van-e a kapcsolat. +Minden további problémát osszon meg a fejlesztőkkel. + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. Ellenőrizze, hogy a megfelelő hivatkozást használta-e, vagy kérje meg ismerősét, hogy küldjön egy másikat. @@ -4344,6 +4961,11 @@ Hiba: %@ Előnézet No comment provided by engineer. + + Previously connected servers + Korábban kapcsolódott kiszolgálók + No comment provided by engineer. + Privacy & security Adatvédelem és biztonság @@ -4359,11 +4981,31 @@ Hiba: %@ Privát fájl nevek No comment provided by engineer. + + Private message routing + Privát üzenet útválasztás + No comment provided by engineer. + + + Private message routing 🚀 + Privát üzenet útválasztás 🚀 + No comment provided by engineer. + Private notes Privát jegyzetek name of notes to self + + Private routing + Privát útválasztás + No comment provided by engineer. + + + Private routing error + Privát útválasztási hiba + No comment provided by engineer. + Profile and server connections Profil és kiszolgálókapcsolatok @@ -4394,6 +5036,11 @@ Hiba: %@ Profiljelszó No comment provided by engineer. + + Profile theme + Profiltéma + No comment provided by engineer. + Profile update will be sent to your contacts. A profilfrissítés elküldésre került az ismerősök számára. @@ -4401,7 +5048,7 @@ Hiba: %@ Prohibit audio/video calls. - Hang- és videóhívások tiltása. + A hívások kezdeményezése le van tiltva. No comment provided by engineer. @@ -4411,7 +5058,7 @@ Hiba: %@ Prohibit message reactions. - Üzenetreakciók tiltása. + Az üzenetreakciók küldése le van tiltva. No comment provided by engineer. @@ -4426,12 +5073,12 @@ Hiba: %@ Prohibit sending direct messages to members. - Közvetlen üzenetek küldésének letiltása a tagok számára. + A közvetlen üzenetek küldése a tagok között le van tiltva. No comment provided by engineer. Prohibit sending disappearing messages. - Eltűnő üzenetek küldésének letiltása. + Az eltűnő üzenetek küldése le van tiltva. No comment provided by engineer. @@ -4441,7 +5088,12 @@ Hiba: %@ Prohibit sending voice messages. - Hangüzenetek küldésének letiltása. + A hangüzenetek küldése le van tiltva. + No comment provided by engineer. + + + Protect IP address + IP-cím védelem No comment provided by engineer. @@ -4449,6 +5101,13 @@ Hiba: %@ Alkalmazás képernyőjének védelme No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + Védje IP-címét az ismerősei által kiválasztott üzenetküldő átjátszókkal szemben. +Engedélyezze a Beállítások / Hálózat és kiszolgálók menüben. + No comment provided by engineer. + Protect your chat profiles with a password! Csevegési profiljok védelme jelszóval! @@ -4464,6 +5123,16 @@ Hiba: %@ Protokoll időkorlát KB-onként No comment provided by engineer. + + Proxied + Proxyzott + No comment provided by engineer. + + + Proxied servers + Proxyzott kiszolgálók + No comment provided by engineer. + Push notifications Push értesítések @@ -4484,6 +5153,11 @@ Hiba: %@ Értékelje az alkalmazást No comment provided by engineer. + + Reachable chat toolbar + Könnyen elérhető eszköztár + No comment provided by engineer. + React… Reagálj… @@ -4492,7 +5166,7 @@ Hiba: %@ Read Olvasd el - No comment provided by engineer. + swipe action Read more @@ -4529,6 +5203,11 @@ Hiba: %@ Üzenet kézbesítési jelentés letiltva No comment provided by engineer. + + Receive errors + Üzenetfogadási hibák + No comment provided by engineer. + Received at Fogadva ekkor: @@ -4549,16 +5228,26 @@ Hiba: %@ Fogadott üzenet message info title + + Received messages + Fogadott üzenetek + No comment provided by engineer. + + + Received reply + Fogadott válasz + No comment provided by engineer. + + + Received total + Összes fogadott + No comment provided by engineer. + Receiving address will be changed to a different server. Address change will complete after sender comes online. A fogadó cím egy másik kiszolgálóra változik. A címváltoztatás a feladó online állapotba kerülése után fejeződik be. No comment provided by engineer. - - Receiving concurrency - Egyidejű fogadás - No comment provided by engineer. - Receiving file will be stopped. A fájl fogadása leállt. @@ -4584,11 +5273,36 @@ Hiba: %@ A címzettek a beírás közben látják a frissítéseket. No comment provided by engineer. + + Reconnect + Újrakapcsolás + No comment provided by engineer. + Reconnect all connected servers to force message delivery. It uses additional traffic. Újrakapcsolódás az összes kiszolgálóhoz az üzenetek kézbesítésének kikényszerítéséhez. Ez további forgalmat használ. No comment provided by engineer. + + Reconnect all servers + Újrakapcsolódás minden kiszolgálóhoz + No comment provided by engineer. + + + Reconnect all servers? + Újrakapcsolódás minden kiszolgálóhoz? + No comment provided by engineer. + + + Reconnect server to force message delivery. It uses additional traffic. + A kiszolgálóhoz való újrakapcsolódás az üzenet kézbesítésének kikényszerítéséhez. Ez további adatforgalmat használ. + No comment provided by engineer. + + + Reconnect server? + Újrakapcsolódás a kiszolgálóhoz? + No comment provided by engineer. + Reconnect servers? Újrakapcsolódás a kiszolgálókhoz? @@ -4612,7 +5326,8 @@ Hiba: %@ Reject Elutasítás - reject incoming call via notification + reject incoming call via notification + swipe action Reject (sender NOT notified) @@ -4626,12 +5341,12 @@ Hiba: %@ Relay server is only used if necessary. Another party can observe your IP address. - Az átjátszó kiszolgáló csak szükség esetén kerül használatra. Egy másik fél megfigyelheti az IP-címét. + Az átjátszó kiszolgáló csak szükség esetén kerül használatra. Egy másik fél megfigyelheti az IP-címet. No comment provided by engineer. Relay server protects your IP address, but it can observe the duration of the call. - Az átjátszó kiszolgáló megvédi IP-címét, de megfigyelheti a hívás időtartamát. + Az átjátszó kiszolgáló megvédi az IP-címet, de megfigyelheti a hívás időtartamát. No comment provided by engineer. @@ -4639,19 +5354,24 @@ Hiba: %@ Eltávolítás No comment provided by engineer. + + Remove image + Kép eltávolítása + No comment provided by engineer. + Remove member - Tag eltávolítása + Eltávolítás No comment provided by engineer. Remove member? - Tag eltávolítása? + Biztosan eltávolítja? No comment provided by engineer. Remove passphrase from keychain? - Jelmondat eltávolítása a kulcstárolóból? + Jelmondat eltávolítása a kulcstartóból? No comment provided by engineer. @@ -4701,7 +5421,7 @@ Hiba: %@ Required - Megkövetelt + Szükséges No comment provided by engineer. @@ -4709,16 +5429,41 @@ Hiba: %@ Alaphelyzetbe állítás No comment provided by engineer. + + Reset all hints + Tippek visszaállítása + No comment provided by engineer. + + + Reset all statistics + Minden statisztika visszaállítása + No comment provided by engineer. + + + Reset all statistics? + Minden statisztika visszaállítása? + No comment provided by engineer. + Reset colors Színek alaphelyzetbe állítása No comment provided by engineer. + + Reset to app theme + Alkalmazás témájának visszaállítása + No comment provided by engineer. + Reset to defaults Alaphelyzetbe állítás No comment provided by engineer. + + Reset to user theme + Felhasználó által létrehozott téma visszaállítása + No comment provided by engineer. + Restart the app to create a new chat profile Új csevegési profil létrehozásához indítsa újra az alkalmazást @@ -4759,11 +5504,6 @@ Hiba: %@ Felfedés chat item action - - Revert - Visszaállít - No comment provided by engineer. - Revoke Visszavonás @@ -4789,9 +5529,14 @@ Hiba: %@ Csevegési szolgáltatás indítása No comment provided by engineer. - - SMP servers - Üzenetküldő (SMP) kiszolgálók + + SMP server + SMP-kiszolgáló + No comment provided by engineer. + + + Safely receive files + Fájlok biztonságos fogadása No comment provided by engineer. @@ -4819,9 +5564,14 @@ Hiba: %@ Mentés és a csoporttagok értesítése No comment provided by engineer. + + Save and reconnect + Mentés és újrakapcsolódás + No comment provided by engineer. + Save and update group profile - Mentés és a csoport profil frissítése + Mentés és csoportprofil frissítése No comment provided by engineer. @@ -4836,7 +5586,7 @@ Hiba: %@ Save group profile - Csoport profil elmentése + Csoportprofil elmentése No comment provided by engineer. @@ -4846,7 +5596,7 @@ Hiba: %@ Save passphrase in Keychain - Jelmondat mentése a kulcstárban + Jelmondat mentése a kulcstartóba No comment provided by engineer. @@ -4899,6 +5649,16 @@ Hiba: %@ Mentett üzenet message info title + + Scale + Méretezés + No comment provided by engineer. + + + Scan / Paste link + Hivatkozás beolvasása / beillesztése + No comment provided by engineer. + Scan QR code QR-kód beolvasása @@ -4911,7 +5671,7 @@ Hiba: %@ Scan code - Kód beolvasása + Beolvasás No comment provided by engineer. @@ -4939,11 +5699,21 @@ Hiba: %@ Keresés, vagy SimpleX hivatkozás beillesztése No comment provided by engineer. + + Secondary + Másodlagos + No comment provided by engineer. + Secure queue Biztonságos várólista server test step + + Secured + Biztosítva + No comment provided by engineer. + Security assessment Biztonsági kiértékelés @@ -4957,6 +5727,16 @@ Hiba: %@ Select Választás + chat item action + + + Selected %lld + %lld kiválasztva + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + A kiválasztott csevegési beállítások tiltják ezt az üzenetet. No comment provided by engineer. @@ -4994,11 +5774,6 @@ Hiba: %@ A kézbesítési jelentéseket a következő címre kell küldeni No comment provided by engineer. - - Send direct message - Közvetlen üzenet küldése - No comment provided by engineer. - Send direct message to connect Közvetlen üzenet küldése a kapcsolódáshoz @@ -5009,6 +5784,11 @@ Hiba: %@ Eltűnő üzenet küldése No comment provided by engineer. + + Send errors + Üzenetküldési hibák + No comment provided by engineer. + Send link previews Hivatkozás előnézetek küldése @@ -5019,6 +5799,21 @@ Hiba: %@ Élő üzenet küldése No comment provided by engineer. + + Send message to enable calls. + Üzenet küldése a hívások engedélyezéséhez. + No comment provided by engineer. + + + Send messages directly when IP address is protected and your or destination server does not support private routing. + Közvetlen üzenetküldés, ha az IP-cím védett és az ön kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást. + No comment provided by engineer. + + + Send messages directly when your or destination server does not support private routing. + Közvetlen üzenetküldés, ha az ön kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást. + No comment provided by engineer. + Send notifications Értesítések küldése @@ -5051,7 +5846,7 @@ Hiba: %@ Sender cancelled file transfer. - A küldő megszakította a fájl átvitelt. + A fájl küldője visszavonta az átvitelt. No comment provided by engineer. @@ -5109,6 +5904,11 @@ Hiba: %@ Elküldve ekkor: %@ copied message info + + Sent directly + Közvetlenül küldött + No comment provided by engineer. + Sent file event Elküldött fájl esemény @@ -5119,11 +5919,46 @@ Hiba: %@ Elküldött üzenet message info title + + Sent messages + Elküldött üzenetek + No comment provided by engineer. + Sent messages will be deleted after set time. Az elküldött üzenetek törlésre kerülnek a beállított idő után. No comment provided by engineer. + + Sent reply + Elküldött válasz + No comment provided by engineer. + + + Sent total + Összes elküldött + No comment provided by engineer. + + + Sent via proxy + Proxyn keresztül küldve + No comment provided by engineer. + + + Server address + Kiszolgáló címe + No comment provided by engineer. + + + Server address is incompatible with network settings. + A kiszolgáló címe nem kompatibilis a hálózati beállításokkal. + srv error text. + + + Server address is incompatible with network settings: %@. + A kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %@. + No comment provided by engineer. + Server requires authorization to create queues, check password A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze jelszavát @@ -5136,7 +5971,22 @@ Hiba: %@ Server test failed! - Sikertelen kiszolgáló-teszt! + Sikertelen kiszolgáló teszt! + No comment provided by engineer. + + + Server type + Kiszolgáló típusa + No comment provided by engineer. + + + Server version is incompatible with network settings. + A kiszolgáló verziója nem kompatibilis a hálózati beállításokkal. + srv error text + + + Server version is incompatible with your app: %@. + A kiszolgáló verziója nem kompatibilis az alkalmazással: %@. No comment provided by engineer. @@ -5144,6 +5994,16 @@ Hiba: %@ Kiszolgálók No comment provided by engineer. + + Servers info + információk a kiszolgálókról + No comment provided by engineer. + + + Servers statistics will be reset - this cannot be undone! + A kiszolgálók statisztikái visszaállnak - ez nem vonható vissza! + No comment provided by engineer. + Session code Munkamenet kód @@ -5159,6 +6019,11 @@ Hiba: %@ Ismerős nevének beállítása… No comment provided by engineer. + + Set default theme + Alapértelmezett téma beállítása + No comment provided by engineer. + Set group preferences Csoportbeállítások megadása @@ -5216,12 +6081,17 @@ Hiba: %@ Share address - Azonosító megosztása + Cím megosztása No comment provided by engineer. Share address with contacts? - Megosztja az azonosítót az ismerőseivel? + Megosztja a címet az ismerőseivel? + No comment provided by engineer. + + + Share from other apps. + Megosztás más alkalmazásokból. No comment provided by engineer. @@ -5234,9 +6104,14 @@ Hiba: %@ Egyszer használatos meghívó hivatkozás megosztása No comment provided by engineer. + + Share to SimpleX + Megosztás a SimpleX-ben + No comment provided by engineer. + Share with contacts - Megosztás ismerősökkel + Megosztás az ismerősökkel No comment provided by engineer. @@ -5259,19 +6134,39 @@ Hiba: %@ Utolsó üzenetek megjelenítése No comment provided by engineer. + + Show message status + Üzenet állapot megjelenítése + No comment provided by engineer. + + + Show percentage + Százalék megjelenítése + No comment provided by engineer. + Show preview Előnézet megjelenítése No comment provided by engineer. + + Show → on messages sent via private routing. + Egy „→” jel megjelenítése a privát útválasztáson keresztül küldött üzeneteknél. + No comment provided by engineer. + Show: Megjelenítés: No comment provided by engineer. + + SimpleX + SimpleX + No comment provided by engineer. + SimpleX Address - SimpleX azonosító + SimpleX cím No comment provided by engineer. @@ -5281,32 +6176,32 @@ Hiba: %@ SimpleX Lock - SimpleX zárolás + SimpleX zár No comment provided by engineer. SimpleX Lock mode - SimpleX zárolási mód + Zárolási mód No comment provided by engineer. SimpleX Lock not enabled! - SimpleX zárolás nincs engedélyezve! + A SimpleX zár nincs bekapcsolva! No comment provided by engineer. SimpleX Lock turned on - SimpleX zárolás bekapcsolva + SimpleX zár bekapcsolva No comment provided by engineer. SimpleX address - SimpleX azonosító + SimpleX cím No comment provided by engineer. SimpleX contact address - SimpleX kapcsolattartási azonosító + SimpleX kapcsolattartási cím simplex link type @@ -5326,7 +6221,7 @@ Hiba: %@ SimpleX links are prohibited in this group. - A SimpleX hivatkozások küldése ebben a csoportban le van tiltva. + A SimpleX hivatkozások küldése le van tiltva ebben a csoportban. No comment provided by engineer. @@ -5344,6 +6239,11 @@ Hiba: %@ Egyszerűsített inkognító mód No comment provided by engineer. + + Size + Méret + No comment provided by engineer. + Skip Kihagyás @@ -5359,9 +6259,24 @@ Hiba: %@ Kis csoportok (max. 20 tag) No comment provided by engineer. + + Soft + Enyhe + blur media + + + Some file(s) were not exported: + Néhány fájl nem került exportálásra: + No comment provided by engineer. + Some non-fatal errors occurred during import - you may see Chat console for more details. - Néhány nem végzetes hiba történt az importálás során – további részletekért a csevegési konzolban olvashat. + Néhány nem végzetes hiba történt az importálás közben – további részletekért a csevegési konzolban olvashat. + No comment provided by engineer. + + + Some non-fatal errors occurred during import: + Néhány nem végzetes hiba történt az importálás közben: No comment provided by engineer. @@ -5389,6 +6304,16 @@ Hiba: %@ Átköltöztetés indítása No comment provided by engineer. + + Starting from %@. + Kezdve ettől %@. + No comment provided by engineer. + + + Statistics + Statisztikák + No comment provided by engineer. + Stop Megállítás @@ -5449,11 +6374,31 @@ Hiba: %@ Csevegés megállítása folyamatban No comment provided by engineer. + + Strong + Erős + blur media + Submit Elküldés No comment provided by engineer. + + Subscribed + Feliratkozva + No comment provided by engineer. + + + Subscription errors + Feliratkozási hibák + No comment provided by engineer. + + + Subscriptions ignored + Elutasított feliratkozások + No comment provided by engineer. + Support SimpleX Chat Támogassa a SimpleX Chatet @@ -5469,6 +6414,11 @@ Hiba: %@ Rendszerhitelesítés No comment provided by engineer. + + TCP connection + TCP kapcsolat + No comment provided by engineer. + TCP connection timeout TCP kapcsolat időtúllépés @@ -5491,7 +6441,7 @@ Hiba: %@ Take picture - Fotó készítése + Kép készítése No comment provided by engineer. @@ -5529,9 +6479,9 @@ Hiba: %@ Koppintson a beolvasáshoz No comment provided by engineer. - - Tap to start a new chat - Koppintson az új csevegés indításához + + Temporary file error + Ideiglenes fájlhiba No comment provided by engineer. @@ -5586,6 +6536,11 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. Az alkalmazás értesíteni fogja, amikor üzeneteket vagy kapcsolatfelvételi kéréseket kap – beállítások megnyitása az engedélyezéshez. No comment provided by engineer. + + The app will ask to confirm downloads from unknown file servers (except .onion). + Az alkalmazás kérni fogja az ismeretlen fájlkiszolgálókról (kivéve .onion) történő letöltések megerősítését. + No comment provided by engineer. + The attempt to change database passphrase was not completed. Az adatbázis jelmondatának megváltoztatására tett kísérlet nem fejeződött be. @@ -5598,7 +6553,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. The connection you accepted will be cancelled! - Az ön által elfogadott kapcsolat megszakad! + Az ön által elfogadott kapcsolat vissza lesz vonva! No comment provided by engineer. @@ -5608,7 +6563,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. The created archive is available via app Settings / Database / Old database archive. - A létrehozott archívum a Beállítások / Adatbázis / Régi adatbázis-archívum menüpontban érhető el. + A létrehozott archívum a Beállítások / Adatbázis / Régi adatbázis-archívum menüben érhető el. No comment provided by engineer. @@ -5618,7 +6573,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. The hash of the previous message is different. - Az előző üzenet hash-e más. + Az előző üzenet ellenőrzőösszege különbözik. No comment provided by engineer. @@ -5631,6 +6586,16 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. Az üzenet minden tag számára moderáltként lesz megjelölve. No comment provided by engineer. + + The messages will be deleted for all members. + Az üzenetek minden tag számára törlésre kerülnek. + No comment provided by engineer. + + + The messages will be marked as moderated for all members. + Az üzenetek moderáltként lesznek megjelölve minden tag számára. + No comment provided by engineer. + The next generation of private messaging A privát üzenetküldés következő generációja @@ -5638,7 +6603,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. The old database was not removed during the migration, it can be deleted. - A régi adatbázis nem került eltávolításra az átköltöztetés során, így törölhető. + A régi adatbázis nem került eltávolításra az átköltöztetés közben, így törölhető. No comment provided by engineer. @@ -5666,9 +6631,9 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. A beillesztett szöveg nem egy SimpleX hivatkozás. No comment provided by engineer. - - Theme - Téma + + Themes + Témák No comment provided by engineer. @@ -5683,7 +6648,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. - Ez a művelet nem vonható vissza - az összes fogadott és küldött fájl a médiatartalommal együtt törlésre kerülnek. Az alacsony felbontású fotók viszont megmaradnak. + Ez a művelet nem vonható vissza - az összes fogadott és küldött fájl a médiatartalommal együtt törlésre kerülnek. Az alacsony felbontású képek viszont megmaradnak. No comment provided by engineer. @@ -5728,7 +6693,7 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. This is your own SimpleX address! - Ez a SimpleX azonosítója! + Ez az ön SimpleX címe! No comment provided by engineer. @@ -5736,11 +6701,21 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. Ez az egyszer használatos hivatkozása! No comment provided by engineer. + + This link was used with another mobile device, please create a new link on the desktop. + Ezt a hivatkozást egy másik mobilleszközön már használták, hozzon létre egy új hivatkozást az asztali számítógépén. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. Ez a beállítás a jelenlegi **%@** profiljában lévő üzenetekre érvényes. No comment provided by engineer. + + Title + Cím + No comment provided by engineer. + To ask any questions and to receive updates: Bármilyen kérdés feltevéséhez és a frissítésekért: @@ -5771,11 +6746,16 @@ Ez valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő. Az időzóna védelme érdekében a kép-/hangfájlok UTC-t használnak. No comment provided by engineer. + + To protect your IP address, private routing uses your SMP servers to deliver messages. + Az IP-cím védelmének érdekében a privát útválasztás az SMP kiszolgálókat használja az üzenetek kézbesítéséhez. + No comment provided by engineer. + To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. - Az adatavédelem érdekében kapcsolja be a SimpleX zárolás funkciót. -A funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befejezésére. + A biztonsága érdekében kapcsolja be a SimpleX zár funkciót. +A funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beállítására az eszközén. No comment provided by engineer. @@ -5785,7 +6765,7 @@ A funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befej To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. - Rejtett profilja feltárásához írja be a teljes jelszót a keresőmezőbe a **Csevegési profiljai** oldalon. + Rejtett profilja megjelenítéséhez írja be a teljes jelszavát a keresőmezőbe a **Csevegési profilok** menüben. No comment provided by engineer. @@ -5795,7 +6775,12 @@ A funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befej To verify end-to-end encryption with your contact compare (or scan) the code on your devices. - A végpontok közötti titkosítás ellenőrzéséhez ismerősével hasonlítsa össze (vagy szkennelje be) az eszközén lévő kódot. + A végpontok közötti titkosítás ellenőrzéséhez hasonlítsa össze (vagy olvassa be a QR-kódot) az ismerőse eszközén lévő kóddal. + No comment provided by engineer. + + + Toggle chat list: + Csevegőlista átváltása: No comment provided by engineer. @@ -5803,11 +6788,26 @@ A funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befej Inkognitó mód kapcsolódáskor. No comment provided by engineer. + + Toolbar opacity + Eszköztár átlátszatlansága + No comment provided by engineer. + + + Total + Összesen + No comment provided by engineer. + Transport isolation Kapcsolat izolációs mód No comment provided by engineer. + + Transport sessions + Munkamenetek átvitele + No comment provided by engineer. + Trying to connect to the server used to receive messages from this contact (error: %@). Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott ismerőstől érkező üzenetek fogadására szolgál (hiba: %@). @@ -5863,11 +6863,6 @@ A funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befej Tag feloldása? No comment provided by engineer. - - Unexpected error: %@ - Váratlan hiba: %@ - item status description - Unexpected migration state Váratlan átköltöztetési állapot @@ -5876,7 +6871,7 @@ A funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befej Unfav. Nem kedvelt. - No comment provided by engineer. + swipe action Unhide @@ -5913,6 +6908,11 @@ A funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befej Ismeretlen hiba No comment provided by engineer. + + Unknown servers! + Ismeretlen kiszolgálók! + No comment provided by engineer. + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. Hacsak nem az iOS hívási felületét használja, engedélyezze a Ne zavarjanak módot a megszakítások elkerülése érdekében. @@ -5948,12 +6948,12 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Unmute Némítás feloldása - No comment provided by engineer. + swipe action Unread Olvasatlan - No comment provided by engineer. + swipe action Up to 100 last messages are sent to new members. @@ -5965,11 +6965,6 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Frissítés No comment provided by engineer. - - Update .onion hosts setting? - Tor .onion kiszolgálók beállításainak frissítése? - No comment provided by engineer. - Update database passphrase Adatbázis jelmondat megváltoztatása @@ -5980,24 +6975,24 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Hálózati beállítások megváltoztatása? No comment provided by engineer. - - Update transport isolation mode? - Kapcsolat izolációs mód frissítése? + + Update settings? + Beállítások frissítése? No comment provided by engineer. Updating settings will re-connect the client to all servers. - A beállítások frissítése a szerverekhez újra kapcsolódással jár. - No comment provided by engineer. - - - Updating this setting will re-connect the client to all servers. - A beállítás frissítésével a kliens újrakapcsolódik az összes kiszolgálóhoz. + A beállítások frissítése a kiszolgálókhoz való újra kapcsolódással jár. No comment provided by engineer. Upgrade and open chat - A csevegés frissítése és megnyitása + Fejlesztés és a csevegés megnyitása + No comment provided by engineer. + + + Upload errors + Feltöltési hibák No comment provided by engineer. @@ -6010,6 +7005,16 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Fájl feltöltése server test step + + Uploaded + Feltöltve + No comment provided by engineer. + + + Uploaded files + Feltöltött fájlok + No comment provided by engineer. + Uploading archive Archívum feltöltése @@ -6060,6 +7065,16 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Csak helyi értesítések használata? No comment provided by engineer. + + Use private routing with unknown servers when IP address is not protected. + Privát útválasztás használata ismeretlen kiszolgálókkal, ha az IP-cím nem védett. + No comment provided by engineer. + + + Use private routing with unknown servers. + Használjon privát útválasztást ismeretlen kiszolgálókkal. + No comment provided by engineer. + Use server Kiszolgáló használata @@ -6070,14 +7085,19 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Használja az alkalmazást hívás közben. No comment provided by engineer. + + Use the app with one hand. + Használja az alkalmazást egy kézzel. + No comment provided by engineer. + User profile Felhasználói profil No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - A .onion kiszolgálók használatához kompatibilis VPN szolgáltatóra van szükség. + + User selection + Felhasználó kiválasztása No comment provided by engineer. @@ -6167,7 +7187,7 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Voice messages are prohibited in this chat. - A hangüzenetek le vannak tiltva ebben a csevegésben. + A hangüzenetek küldése le van tiltva ebben a csevegésben. No comment provided by engineer. @@ -6210,6 +7230,16 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Videóra várakozás No comment provided by engineer. + + Wallpaper accent + Háttérkép kiemelés + No comment provided by engineer. + + + Wallpaper background + Háttérkép háttérszíne + No comment provided by engineer. + Warning: starting chat on multiple devices is not supported and will cause message delivery failures Figyelmeztetés: a csevegés elindítása egyszerre több eszközön nem támogatott, továbbá üzenetkézbesítési hibákat okozhat @@ -6295,19 +7325,39 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Csökkentett akkumulátorhasználattal. No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + Tor vagy VPN nélkül az IP-címe látható lesz a fájlkiszolgálók számára. + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + Tor vagy VPN nélkül az IP-címe látható lesz ezen XFTP átjátszók számára: %@. + No comment provided by engineer. + Wrong database passphrase - Téves adatbázis jelmondat + Hibás adatbázis jelmondat No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + Hibás kulcs vagy ismeretlen kapcsolat - valószínűleg ez a kapcsolat törlődött. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + Hibás kulcs vagy ismeretlen fájltöredék cím - valószínűleg a fájl törlődött. + file error text + Wrong passphrase! - Téves jelmondat! + Hibás jelmondat! No comment provided by engineer. - - XFTP servers - XFTP kiszolgálók + + XFTP server + XFTP-kiszolgáló No comment provided by engineer. @@ -6322,7 +7372,7 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol You accepted connection - Kapcsolódás elfogadva + Kapcsolat létrehozása No comment provided by engineer. @@ -6387,11 +7437,21 @@ Csatlakozási kérés megismétlése? Meghívást kapott a csoportba No comment provided by engineer. + + You are not connected to these servers. Private routing is used to deliver messages to them. + Ön nem kapcsolódik ezekhez a kiszolgálókhoz. A privát útválasztás az üzenetek kézbesítésére szolgál. + No comment provided by engineer. + You can accept calls from lock screen, without device and app authentication. Hívásokat fogadhat a lezárási képernyőről, eszköz- és alkalmazáshitelesítés nélkül. No comment provided by engineer. + + You can change it in Appearance settings. + Ezt a „Megjelenés” menüben módosíthatja. + No comment provided by engineer. + You can create it later Létrehozás később @@ -6404,7 +7464,7 @@ Csatlakozási kérés megismétlése? You can enable them later via app Privacy & Security settings. - Később engedélyezheti őket az alkalmazás Adatvédelem és biztonság menüpontban. + Később engedélyezheti őket az alkalmazás „Adatvédelem és biztonság” menüben. No comment provided by engineer. @@ -6414,7 +7474,7 @@ Csatlakozási kérés megismétlése? You can hide or mute a user profile - swipe it to the right. - Elrejthet vagy némíthat egy felhasználói profilt – csúsztasson jobbra. + Elrejtheti vagy lenémíthatja a felhasználó profiljait - csúsztassa jobbra a profilt. No comment provided by engineer. @@ -6422,11 +7482,16 @@ Csatlakozási kérés megismétlése? Láthatóvá teheti SimpleX ismerősök számára a Beállításokban. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Mostantól küldhet üzeneteket %@ számára notification body + + You can send messages to %@ from Archived contacts. + Az „Archivált ismerősökből” továbbra is küldhet üzeneteket neki: %@. + No comment provided by engineer. + You can set lock screen notification preview via settings. A beállításokon keresztül beállíthatja a lezárási képernyő értesítési előnézetét. @@ -6439,22 +7504,27 @@ Csatlakozási kérés megismétlése? You can share this address with your contacts to let them connect with **%@**. - Megoszthatja ezt az azonosítót az ismerőseivel, hogy kapcsolatba léphessenek önnel a **%@** nevű profilján keresztül. + Megoszthatja ezt a címet az ismerőseivel, hogy kapcsolatba léphessenek önnel a(z) **%@** nevű profilján keresztül. No comment provided by engineer. You can share your address as a link or QR code - anybody can connect to you. - Megoszthatja azonosítóját hivatkozásként vagy QR-kódként – így bárki kapcsolódhat önhöz. + Megoszthatja a címét egy hivatkozásként vagy QR-kódként – így bárki kapcsolódhat önhöz. No comment provided by engineer. You can start chat via app Settings / Database or by restarting the app - A csevegést az alkalmazás Beállítások / Adatbázis menü segítségével vagy az alkalmazás újraindításával indíthatja el + A csevegést az alkalmazás Beállítások / Adatbázis menüben vagy az alkalmazás újraindításával indíthatja el + No comment provided by engineer. + + + You can still view conversation with %@ in the list of chats. + A(z) %@ nevű ismerősével folytatott beszélgetéseit továbbra is megtekintheti a csevegések listájában. No comment provided by engineer. You can turn on SimpleX Lock via Settings. - A SimpleX zárolás a Beállításokon keresztül kapcsolható be. + A SimpleX zár az „Adatvédelem és biztonság” menüben kapcsolható be. No comment provided by engineer. @@ -6474,7 +7544,7 @@ Csatlakozási kérés megismétlése? You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - Ön szabályozhatja, hogy mely kiszogál(ók)ón keresztül **kapja** az üzeneteket, az ismerősöket - az üzenetküldéshez használt szervereken. + Ön szabályozhatja, hogy mely kiszogál(ók)ón keresztül **kapja** az üzeneteket, az ismerősöket - az üzenetküldéshez használt kiszolgálókon. No comment provided by engineer. @@ -6484,7 +7554,7 @@ Csatlakozási kérés megismétlése? You have already requested connection via this address! - Már kért egy kapcsolódási kérelmet ezen az azonosítón keresztül! + Már kért egy kapcsolódási kérelmet ezen a címen keresztül! No comment provided by engineer. @@ -6494,11 +7564,6 @@ Repeat connection request? Kapcsolódási kérés megismétlése? No comment provided by engineer. - - You have no chats - Nincsenek csevegési üzenetek - No comment provided by engineer. - You have to enter passphrase every time the app starts - it is not stored on the device. A jelmondatot minden alkalommal meg kell adnia, amikor az alkalmazás elindul - nem az eszközön kerül tárolásra. @@ -6519,11 +7584,26 @@ Kapcsolódási kérés megismétlése? Csatlakozott ehhez a csoporthoz. Kapcsolódás a meghívó csoporttaghoz. No comment provided by engineer. + + You may migrate the exported database. + Az exportált adatbázist átköltöztetheti. + No comment provided by engineer. + + + You may save the exported archive. + Az exportált archívumot elmentheti. + No comment provided by engineer. + You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. A csevegési adatbázis legfrissebb verzióját CSAK egy eszközön kell használnia, ellenkező esetben előfordulhat, hogy az üzeneteket nem fogja megkapni valamennyi ismerőstől. No comment provided by engineer. + + You need to allow your contact to call to be able to call them. + Engedélyeznie kell a hívásokat az ismerőse számára, hogy fel tudják hívni egymást. + No comment provided by engineer. + You need to allow your contact to send voice messages to be able to send them. Hangüzeneteket küldéséhez engedélyeznie kell azok küldését az ismerősök számára. @@ -6581,7 +7661,7 @@ Kapcsolódási kérés megismétlése? You won't lose your contacts if you later delete your address. - Nem veszíti el az ismerőseit, ha később törli az azonosítóját. + Nem veszíti el az ismerőseit, ha később törli a címét. No comment provided by engineer. @@ -6611,7 +7691,7 @@ Kapcsolódási kérés megismétlése? Your SimpleX address - SimpleX azonosítója + Az ön SimpleX címe No comment provided by engineer. @@ -6636,14 +7716,7 @@ Kapcsolódási kérés megismétlése? Your chat profiles - Csevegési profiljai - No comment provided by engineer. - - - Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link). - Az ismerősnek online kell lennie ahhoz, hogy a kapcsolat létrejöjjön. -Megszakíthatja ezt a kapcsolatfelvételt és törölheti az ismerőst (ezt később ismét megpróbálhatja egy új hivatkozással). + Csevegési profilok No comment provided by engineer. @@ -6790,7 +7863,12 @@ A SimpleX kiszolgálók nem látjhatják profilját. and %lld other events - és %lld további esemény + és további %lld esemény + No comment provided by engineer. + + + attempts + próbálkozások No comment provided by engineer. @@ -6810,17 +7888,17 @@ A SimpleX kiszolgálók nem látjhatják profilját. bad message hash - téves üzenet hash + hibás az üzenet ellenőrzőösszege integrity error chat item blocked - blokkolva + letiltva marked deleted chat item preview text blocked %@ - %@ letiltva + letiltotta %@-t rcv group event chat item @@ -6833,6 +7911,11 @@ A SimpleX kiszolgálók nem látjhatják profilját. félkövér No comment provided by engineer. + + call + hívás + No comment provided by engineer. + call error hiba a hívásban @@ -6850,7 +7933,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. cancelled %@ - %@ törölve + %@ visszavonva feature offered item @@ -6870,12 +7953,12 @@ A SimpleX kiszolgálók nem látjhatják profilját. changing address for %@… - cím módosítása %@ számára… + cím megváltoztatása nála: %@… chat item text changing address… - azonosító megváltoztatása… + cím megváltoztatása… chat item text @@ -6965,7 +8048,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. creator - szerző + készítő No comment provided by engineer. @@ -6983,6 +8066,11 @@ A SimpleX kiszolgálók nem látjhatják profilját. nap time unit + + decryption errors + visszafejtési hibák + No comment provided by engineer. + default (%@) alapértelmezett (%@) @@ -7030,9 +8118,14 @@ A SimpleX kiszolgálók nem látjhatják profilját. duplicate message - duplikálódott üzenet + duplikált üzenet integrity error chat item + + duplicates + duplikációk + No comment provided by engineer. + e2e encrypted e2e titkosított @@ -7113,6 +8206,11 @@ A SimpleX kiszolgálók nem látjhatják profilját. esemény történt No comment provided by engineer. + + expired + lejárt + No comment provided by engineer. + forwarded továbbított @@ -7125,7 +8223,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. group profile updated - csoport profil frissítve + csoportprofil frissítve snd group event chat item @@ -7135,12 +8233,17 @@ A SimpleX kiszolgálók nem látjhatják profilját. iOS Keychain is used to securely store passphrase - it allows receiving push notifications. - Az iOS kulcstár a jelmondat biztonságos tárolására szolgál - lehetővé teszi a push-értesítések fogadását. + Az iOS kulcstartó a jelmondat biztonságos tárolására szolgál - lehetővé teszi a push-értesítések fogadását. No comment provided by engineer. iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications. - Az iOS kulcstár az alkalmazás újraindítása, vagy a jelmondat módosítása után a jelmondat biztonságos tárolására szolgál - lehetővé teszi a push-értesítések fogadását. + Az iOS kulcstartó az alkalmazás újraindítása, vagy a jelmondat módosítása után a jelmondat biztonságos tárolására szolgál - lehetővé teszi a push-értesítések fogadását. + No comment provided by engineer. + + + inactive + inaktív No comment provided by engineer. @@ -7183,14 +8286,19 @@ A SimpleX kiszolgálók nem látjhatják profilját. meghívás a(z) %@ csoportba group name + + invite + meghívás + No comment provided by engineer. + invited - meghívta + meghíva No comment provided by engineer. invited %@ - meghívta %@-t + meghívta őt: %@ rcv group event chat item @@ -7200,7 +8308,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. invited via your group link - meghívta a csoport hivatkozásán keresztül + meghíva az ön csoport hivatkozásán keresztül rcv group event chat item @@ -7238,6 +8346,11 @@ A SimpleX kiszolgálók nem látjhatják profilját. kapcsolódott rcv group event chat item + + message + üzenet + No comment provided by engineer. + message received üzenet érkezett @@ -7268,6 +8381,11 @@ A SimpleX kiszolgálók nem látjhatják profilját. hónap time unit + + mute + némítás + No comment provided by engineer. + never soha @@ -7300,7 +8418,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. off - ki + kikapcsolva enabled status group pref value time to disappear @@ -7317,9 +8435,19 @@ A SimpleX kiszolgálók nem látjhatják profilját. on - be + bekapcsolva group pref value + + other + egyéb + No comment provided by engineer. + + + other errors + egyéb hibák + No comment provided by engineer. + owner tulajdonos @@ -7362,17 +8490,17 @@ A SimpleX kiszolgálók nem látjhatják profilját. removed %@ - %@ eltávolítva + eltávolította őt: %@ rcv group event chat item removed contact address - törölt kapcsolattartási azonosító + törölt kapcsolattartási cím profile update event chat item removed profile picture - törölt profilkép + törölte a profilképét profile update event chat item @@ -7390,6 +8518,11 @@ A SimpleX kiszolgálók nem látjhatják profilját. mentve innen: %@ No comment provided by engineer. + + search + keresés + No comment provided by engineer. + sec mp @@ -7415,14 +8548,23 @@ A SimpleX kiszolgálók nem látjhatják profilját. közvetlen üzenet küldése No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + kiszolgáló üzenet-várakotatási információ: %1$@ + +utoljára fogadott üzenet: %2$@ + queue info + set new contact address - új kapcsolattartási azonosító beállítása + új kapcsolattartási cím beállítása profile update event chat item set new profile picture - új profilkép beállítása + új profilképet állított be profile update event chat item @@ -7447,7 +8589,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. unblocked %@ - %@ feloldva + feloldotta %@ letiltását rcv group event chat item @@ -7455,14 +8597,29 @@ A SimpleX kiszolgálók nem látjhatják profilját. ismeretlen connection info + + unknown servers + ismeretlen átjátszók + No comment provided by engineer. + unknown status ismeretlen státusz No comment provided by engineer. + + unmute + némítás feloldása + No comment provided by engineer. + + + unprotected + nem védett + No comment provided by engineer. + updated group profile - módosított csoport profil + frissítette a csoport profilját rcv group event chat item @@ -7482,7 +8639,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. via contact address link - kapcsolattartási azonosító-hivatkozáson keresztül + kapcsolattartási cím-hivatkozáson keresztül chat list item description @@ -7500,6 +8657,11 @@ A SimpleX kiszolgálók nem látjhatják profilját. átjátszón keresztül No comment provided by engineer. + + video + videó + No comment provided by engineer. + video call (not e2e encrypted) videóhívás (nem e2e titkosított) @@ -7507,7 +8669,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. waiting for answer… - várakozás válaszra… + várakozás a válaszra… No comment provided by engineer. @@ -7525,6 +8687,11 @@ A SimpleX kiszolgálók nem látjhatják profilját. hét time unit + + when IP hidden + ha az IP-cím rejtett + No comment provided by engineer. + yes igen @@ -7547,17 +8714,17 @@ A SimpleX kiszolgálók nem látjhatják profilját. you blocked %@ - blokkolta őt: %@ + ön letiltotta %@-t snd group event chat item you changed address - azonosítója megváltoztatva + cím megváltoztatva chat item text you changed address for %@ - %@ azonosítója megváltoztatva + cím megváltoztatva nála: %@ chat item text @@ -7592,7 +8759,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. you unblocked %@ - feloldotta %@ blokkolását + ön feloldotta %@ letiltását snd group event chat item @@ -7609,7 +8776,7 @@ A SimpleX kiszolgálók nem látjhatják profilját.
- +
@@ -7646,7 +8813,7 @@ A SimpleX kiszolgálók nem látjhatják profilját.
- +
@@ -7666,4 +8833,218 @@ A SimpleX kiszolgálók nem látjhatják profilját.
+ +
+ +
+ + + SimpleX SE + SimpleX SE + Bundle display name + + + SimpleX SE + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright © 2024 SimpleX Chat. Minden jog fenntartva. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + %@ + No comment provided by engineer. + + + App is locked! + Az alkalmazás zárolva! + No comment provided by engineer. + + + Cancel + Mégse + No comment provided by engineer. + + + Cannot access keychain to save database password + Nem lehet hozzáférni a kulcstartóhoz az adatbázis jelszavának mentéséhez + No comment provided by engineer. + + + Cannot forward message + Nem lehet továbbítani az üzenetet + No comment provided by engineer. + + + Comment + Hozzászólás + No comment provided by engineer. + + + Currently maximum supported file size is %@. + Jelenleg a maximális támogatott fájlméret %@. + No comment provided by engineer. + + + Database downgrade required + Adatbázis visszafejlesztése szükséges + No comment provided by engineer. + + + Database encrypted! + Adatbázis titkosítva! + No comment provided by engineer. + + + Database error + Adatbázis hiba + No comment provided by engineer. + + + Database passphrase is different from saved in the keychain. + Az adatbázis jelmondata eltér a kulcstartóban lévőtől. + No comment provided by engineer. + + + Database passphrase is required to open chat. + Adatbázis jelmondat szükséges a csevegés megnyitásához. + No comment provided by engineer. + + + Database upgrade required + Adatbázis fejlesztése szükséges + No comment provided by engineer. + + + Error preparing file + Hiba a fájl előkészítésekor + No comment provided by engineer. + + + Error preparing message + Hiba az üzenet előkészítésekor + No comment provided by engineer. + + + Error: %@ + Hiba: %@ + No comment provided by engineer. + + + File error + Fájlhiba + No comment provided by engineer. + + + Incompatible database version + Nem kompatibilis adatbázis verzió + No comment provided by engineer. + + + Invalid migration confirmation + Érvénytelen átköltöztetési visszaigazolás + No comment provided by engineer. + + + Keychain error + Kulcstartó hiba + No comment provided by engineer. + + + Large file! + Nagy fájl! + No comment provided by engineer. + + + No active profile + Nincs aktív profil + No comment provided by engineer. + + + Ok + Rendben + No comment provided by engineer. + + + Open the app to downgrade the database. + Nyissa meg az alkalmazást az adatbázis visszafejlesztéséhez. + No comment provided by engineer. + + + Open the app to upgrade the database. + Nyissa meg az alkalmazást az adatbázis fejlesztéséhez. + No comment provided by engineer. + + + Passphrase + Jelmondat + No comment provided by engineer. + + + Please create a profile in the SimpleX app + Hozzon létre egy profilt a SimpleX alkalmazásban + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + A kiválasztott csevegési beállítások tiltják ezt az üzenetet. + No comment provided by engineer. + + + Sending a message takes longer than expected. + Az üzenet elküldése a vártnál tovább tart. + No comment provided by engineer. + + + Sending message… + Üzenet küldése… + No comment provided by engineer. + + + Share + Megosztás + No comment provided by engineer. + + + Slow network? + Lassú internetkapcsolat? + No comment provided by engineer. + + + Unknown database error: %@ + Ismeretlen adatbázis hiba: %@ + No comment provided by engineer. + + + Unsupported format + Nem támogatott formátum + No comment provided by engineer. + + + Wait + Várjon + No comment provided by engineer. + + + Wrong database passphrase + Hibás adatbázis jelmondat + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + A megosztást az Adatvédelem és biztonság / SimpleX zár menüben engedélyezheti. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/hu.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/hu.xcloc/contents.json b/apps/ios/SimpleX Localizations/hu.xcloc/contents.json index bc788c3c10..0b16198498 100644 --- a/apps/ios/SimpleX Localizations/hu.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/hu.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "hu", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "15F31d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "15.4" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index a5fe0ec830..72eb3561e3 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -2,7 +2,7 @@
- +
@@ -127,11 +127,6 @@ %@ è verificato/a No comment provided by engineer. - - %@ servers - Server %@ - No comment provided by engineer. - %@ uploaded %@ caricati @@ -557,16 +552,17 @@ Info sull'indirizzo SimpleX No comment provided by engineer. - - Accent color - Colore principale + + Accent + Principale No comment provided by engineer. Accept Accetta accept contact request via notification - accept incoming call via notification + accept incoming call via notification + swipe action Accept connection request? @@ -581,7 +577,23 @@ Accept incognito Accetta in incognito - accept contact request via notification + accept contact request via notification + swipe action + + + Acknowledged + Riconosciuto + No comment provided by engineer. + + + Acknowledgement errors + Errori di riconoscimento + No comment provided by engineer. + + + Active connections + Connessioni attive + No comment provided by engineer. Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. @@ -603,16 +615,16 @@ Aggiungi profilo No comment provided by engineer. + + Add server + Aggiungi server + No comment provided by engineer. + Add servers by scanning QR codes. Aggiungi server scansionando codici QR. No comment provided by engineer. - - Add server… - Aggiungi server… - No comment provided by engineer. - Add to another device Aggiungi ad un altro dispositivo @@ -623,6 +635,21 @@ Aggiungi messaggio di benvenuto No comment provided by engineer. + + Additional accent + Principale aggiuntivo + No comment provided by engineer. + + + Additional accent 2 + Principale aggiuntivo 2 + No comment provided by engineer. + + + Additional secondary + Secondario aggiuntivo + No comment provided by engineer. + Address Indirizzo @@ -648,6 +675,11 @@ Impostazioni di rete avanzate No comment provided by engineer. + + Advanced settings + Impostazioni avanzate + No comment provided by engineer. + All app data is deleted. Tutti i dati dell'app vengono eliminati. @@ -663,6 +695,11 @@ Tutti i dati vengono cancellati quando inserito. No comment provided by engineer. + + All data is private to your device. + Tutti i dati sono privati, nel tuo dispositivo. + No comment provided by engineer. + All group members will remain connected. Tutti i membri del gruppo resteranno connessi. @@ -683,6 +720,11 @@ Tutti i nuovi messaggi da %@ verrranno nascosti! No comment provided by engineer. + + All profiles + Tutti gli profili + No comment provided by engineer. + All your contacts will remain connected. Tutti i tuoi contatti resteranno connessi. @@ -708,11 +750,21 @@ Consenti le chiamate solo se il tuo contatto le consente. No comment provided by engineer. + + Allow calls? + Consentire le chiamate? + No comment provided by engineer. + Allow disappearing messages only if your contact allows it to you. Consenti i messaggi a tempo solo se il contatto li consente a te. No comment provided by engineer. + + Allow downgrade + Consenti downgrade + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Consenti l'eliminazione irreversibile dei messaggi solo se il contatto la consente a te. (24 ore) @@ -738,6 +790,11 @@ Permetti l'invio di messaggi a tempo. No comment provided by engineer. + + Allow sharing + Consenti la condivisione + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Permetti di eliminare irreversibilmente i messaggi inviati. (24 ore) @@ -808,6 +865,11 @@ Già in ingresso nel gruppo! No comment provided by engineer. + + Always use private routing. + Usa sempre l'instradamento privato. + No comment provided by engineer. + Always use relay Connetti via relay @@ -873,11 +935,26 @@ Applica No comment provided by engineer. + + Apply to + Applica a + No comment provided by engineer. + Archive and upload Archivia e carica No comment provided by engineer. + + Archive contacts to chat later. + Archivia contatti per chattare più tardi. + No comment provided by engineer. + + + Archived contacts + Contatti archiviati + No comment provided by engineer. + Archiving database Archiviazione del database @@ -948,6 +1025,11 @@ Indietro No comment provided by engineer. + + Background + Sfondo + No comment provided by engineer. + Bad desktop address Indirizzo desktop errato @@ -973,6 +1055,16 @@ Messaggi migliorati No comment provided by engineer. + + Better networking + Rete migliorata + No comment provided by engineer. + + + Black + Nero + No comment provided by engineer. + Block Blocca @@ -1008,6 +1100,16 @@ Bloccato dall'amministratore No comment provided by engineer. + + Blur for better privacy. + Sfoca per una privacy maggiore. + No comment provided by engineer. + + + Blur media + Sfocatura file multimediali + No comment provided by engineer. + Both you and your contact can add message reactions. Sia tu che il tuo contatto potete aggiungere reazioni ai messaggi. @@ -1053,11 +1155,26 @@ Chiamate No comment provided by engineer. + + Calls prohibited! + Chiamate proibite! + No comment provided by engineer. + Camera not available Fotocamera non disponibile No comment provided by engineer. + + Can't call contact + Impossibile chiamare il contatto + No comment provided by engineer. + + + Can't call member + Impossibile chiamare il membro + No comment provided by engineer. + Can't invite contact! Impossibile invitare il contatto! @@ -1068,6 +1185,11 @@ Impossibile invitare i contatti! No comment provided by engineer. + + Can't message member + Impossibile inviare un messaggio al membro + No comment provided by engineer. + Cancel Annulla @@ -1083,11 +1205,21 @@ Impossibile accedere al portachiavi per salvare la password del database No comment provided by engineer. + + Cannot forward message + Impossibile inoltrare il messaggio + No comment provided by engineer. + Cannot receive file Impossibile ricevere il file No comment provided by engineer. + + Capacity exceeded - recipient did not receive previously sent messages. + Quota superata - il destinatario non ha ricevuto i messaggi precedentemente inviati. + snd error text + Cellular Mobile @@ -1149,6 +1281,11 @@ Archivio chat No comment provided by engineer. + + Chat colors + Colori della chat + No comment provided by engineer. + Chat console Console della chat @@ -1164,6 +1301,11 @@ Database della chat eliminato No comment provided by engineer. + + Chat database exported + Database della chat esportato + No comment provided by engineer. + Chat database imported Database della chat importato @@ -1184,6 +1326,11 @@ La chat è ferma. Se hai già usato questo database su un altro dispositivo, dovresti trasferirlo prima di avviare la chat. No comment provided by engineer. + + Chat list + Elenco delle chat + No comment provided by engineer. + Chat migrated! Chat migrata! @@ -1194,6 +1341,11 @@ Preferenze della chat No comment provided by engineer. + + Chat theme + Tema della chat + No comment provided by engineer. + Chats Chat @@ -1224,10 +1376,25 @@ Scegli dalla libreria No comment provided by engineer. + + Chunks deleted + Blocchi eliminati + No comment provided by engineer. + + + Chunks downloaded + Blocchi scaricati + No comment provided by engineer. + + + Chunks uploaded + Blocchi inviati + No comment provided by engineer. + Clear Svuota - No comment provided by engineer. + swipe action Clear conversation @@ -1249,9 +1416,14 @@ Annulla la verifica No comment provided by engineer. - - Colors - Colori + + Color chats with the new themes. + Colora le chat con i nuovi temi. + No comment provided by engineer. + + + Color mode + Modalità di colore No comment provided by engineer. @@ -1264,11 +1436,21 @@ Confronta i codici di sicurezza con i tuoi contatti. No comment provided by engineer. + + Completed + Completato + No comment provided by engineer. + Configure ICE servers Configura server ICE No comment provided by engineer. + + Configured %@ servers + Configurati %@ server + No comment provided by engineer. + Confirm Conferma @@ -1279,11 +1461,21 @@ Conferma il codice di accesso No comment provided by engineer. + + Confirm contact deletion? + Confermare l'eliminazione del contatto? + No comment provided by engineer. + Confirm database upgrades Conferma aggiornamenti database No comment provided by engineer. + + Confirm files from unknown servers. + Conferma i file da server sconosciuti. + No comment provided by engineer. + Confirm network settings Conferma le impostazioni di rete @@ -1329,6 +1521,11 @@ Connetti al desktop No comment provided by engineer. + + Connect to your friends faster. + Connettiti più velocemente ai tuoi amici. + No comment provided by engineer. + Connect to yourself? Connettersi a te stesso? @@ -1368,16 +1565,31 @@ Questo è il tuo link una tantum! Connettersi con %@ No comment provided by engineer. + + Connected + Connesso + No comment provided by engineer. + Connected desktop Desktop connesso No comment provided by engineer. + + Connected servers + Server connessi + No comment provided by engineer. + Connected to desktop Connesso al desktop No comment provided by engineer. + + Connecting + In connessione + No comment provided by engineer. + Connecting to server… Connessione al server… @@ -1388,6 +1600,11 @@ Questo è il tuo link una tantum! Connessione al server… (errore: %@) No comment provided by engineer. + + Connecting to contact, please wait or check later! + In collegamento con il contatto, attendi o controlla più tardi! + No comment provided by engineer. + Connecting to desktop Connessione al desktop @@ -1398,6 +1615,11 @@ Questo è il tuo link una tantum! Connessione No comment provided by engineer. + + Connection and servers status. + Stato della connessione e dei server. + No comment provided by engineer. + Connection error Errore di connessione @@ -1408,6 +1630,11 @@ Questo è il tuo link una tantum! Errore di connessione (AUTH) No comment provided by engineer. + + Connection notifications + Notifiche di connessione + No comment provided by engineer. + Connection request sent! Richiesta di connessione inviata! @@ -1423,6 +1650,16 @@ Questo è il tuo link una tantum! Connessione scaduta No comment provided by engineer. + + Connection with desktop stopped + Connessione con il desktop fermata + No comment provided by engineer. + + + Connections + Connessioni + No comment provided by engineer. + Contact allows Il contatto lo consente @@ -1433,6 +1670,11 @@ Questo è il tuo link una tantum! Il contatto esiste già No comment provided by engineer. + + Contact deleted! + Contatto eliminato! + No comment provided by engineer. + Contact hidden: Contatto nascosto: @@ -1443,9 +1685,9 @@ Questo è il tuo link una tantum! Il contatto è connesso notification - - Contact is not connected yet! - Il contatto non è ancora connesso! + + Contact is deleted. + Il contatto è stato eliminato. No comment provided by engineer. @@ -1458,6 +1700,11 @@ Questo è il tuo link una tantum! Preferenze del contatto No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + Il contatto verrà eliminato - non è reversibile! + No comment provided by engineer. + Contacts Contatti @@ -1473,10 +1720,20 @@ Questo è il tuo link una tantum! Continua No comment provided by engineer. + + Conversation deleted! + Conversazione eliminata! + No comment provided by engineer. + Copy Copia - chat item action + No comment provided by engineer. + + + Copy error + Copia errore + No comment provided by engineer. Core version: v%@ @@ -1553,6 +1810,11 @@ Questo è il tuo link una tantum! Crea il tuo profilo No comment provided by engineer. + + Created + Creato + No comment provided by engineer. + Created at Creato il @@ -1588,6 +1850,11 @@ Questo è il tuo link una tantum! Password attuale… No comment provided by engineer. + + Current profile + Profilo attuale + No comment provided by engineer. + Currently maximum supported file size is %@. Attualmente la dimensione massima supportata è di %@. @@ -1598,11 +1865,21 @@ Questo è il tuo link una tantum! Tempo personalizzato No comment provided by engineer. + + Customize theme + Personalizza il tema + No comment provided by engineer. + Dark Scuro No comment provided by engineer. + + Dark mode colors + Colori modalità scura + No comment provided by engineer. + Database ID ID database @@ -1701,6 +1978,11 @@ Questo è il tuo link una tantum! Il database verrà migrato al riavvio dell'app No comment provided by engineer. + + Debug delivery + Debug della consegna + No comment provided by engineer. + Decentralized Decentralizzato @@ -1714,18 +1996,19 @@ Questo è il tuo link una tantum! Delete Elimina - chat item action + chat item action + swipe action + + + Delete %lld messages of members? + Eliminare %lld messaggi dei membri? + No comment provided by engineer. Delete %lld messages? Eliminare %lld messaggi? No comment provided by engineer. - - Delete Contact - Elimina contatto - No comment provided by engineer. - Delete address Elimina indirizzo @@ -1781,11 +2064,9 @@ Questo è il tuo link una tantum! Elimina contatto No comment provided by engineer. - - Delete contact? -This cannot be undone! - Eliminare il contatto? -Non è reversibile! + + Delete contact? + Eliminare il contatto? No comment provided by engineer. @@ -1878,11 +2159,6 @@ Non è reversibile! Eliminare il database vecchio? No comment provided by engineer. - - Delete pending connection - Elimina connessione in attesa - No comment provided by engineer. - Delete pending connection? Eliminare la connessione in attesa? @@ -1898,11 +2174,26 @@ Non è reversibile! Elimina coda server test step + + Delete up to 20 messages at once. + Elimina fino a 20 messaggi contemporaneamente. + No comment provided by engineer. + Delete user profile? Eliminare il profilo utente? No comment provided by engineer. + + Delete without notification + Elimina senza avvisare + No comment provided by engineer. + + + Deleted + Eliminato + No comment provided by engineer. + Deleted at Eliminato il @@ -1913,6 +2204,11 @@ Non è reversibile! Eliminato il: %@ copied message info + + Deletion errors + Errori di eliminazione + No comment provided by engineer. + Delivery Consegna @@ -1948,11 +2244,41 @@ Non è reversibile! Dispositivi desktop No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + L'indirizzo del server di destinazione di %@ è incompatibile con le impostazioni del server di inoltro %@. + No comment provided by engineer. + + + Destination server error: %@ + Errore del server di destinazione: %@ + snd error text + + + Destination server version of %@ is incompatible with forwarding server %@. + La versione del server di destinazione di %@ è incompatibile con il server di inoltro %@. + No comment provided by engineer. + + + Detailed statistics + Statistiche dettagliate + No comment provided by engineer. + + + Details + Dettagli + No comment provided by engineer. + Develop Sviluppa No comment provided by engineer. + + Developer options + Opzioni sviluppatore + No comment provided by engineer. + Developer tools Strumenti di sviluppo @@ -2003,6 +2329,11 @@ Non è reversibile! Disattiva per tutti No comment provided by engineer. + + Disabled + Disattivato + No comment provided by engineer. + Disappearing message Messaggio a tempo @@ -2053,11 +2384,21 @@ Non è reversibile! Individua via rete locale No comment provided by engineer. + + Do NOT send messages directly, even if your or destination server does not support private routing. + NON inviare messaggi direttamente, anche se il tuo server o quello di destinazione non supporta l'instradamento privato. + No comment provided by engineer. + Do NOT use SimpleX for emergency calls. NON usare SimpleX per chiamate di emergenza. No comment provided by engineer. + + Do NOT use private routing. + NON usare l'instradamento privato. + No comment provided by engineer. + Do it later Fallo dopo @@ -2093,6 +2434,11 @@ Non è reversibile! Scarica chat item action + + Download errors + Errori di scaricamento + No comment provided by engineer. + Download failed Scaricamento fallito @@ -2103,6 +2449,16 @@ Non è reversibile! Scarica file server test step + + Downloaded + Scaricato + No comment provided by engineer. + + + Downloaded files + File scaricati + No comment provided by engineer. + Downloading archive Scaricamento archivio @@ -2203,6 +2559,11 @@ Non è reversibile! Attiva il codice di autodistruzione set passcode view + + Enabled + Attivato + No comment provided by engineer. + Enabled for Attivo per @@ -2373,6 +2734,11 @@ Non è reversibile! Errore nella modifica dell'impostazione No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + Errore di connessione al server di inoltro %@. Riprova più tardi. + No comment provided by engineer. + Error creating address Errore nella creazione dell'indirizzo @@ -2423,11 +2789,6 @@ Non è reversibile! Errore nell'eliminazione della connessione No comment provided by engineer. - - Error deleting contact - Errore nell'eliminazione del contatto - No comment provided by engineer. - Error deleting database Errore nell'eliminazione del database @@ -2473,6 +2834,11 @@ Non è reversibile! Errore nell'esportazione del database della chat No comment provided by engineer. + + Error exporting theme: %@ + Errore di esportazione del tema: %@ + No comment provided by engineer. + Error importing chat database Errore nell'importazione del database della chat @@ -2498,11 +2864,26 @@ Non è reversibile! Errore nella ricezione del file No comment provided by engineer. + + Error reconnecting server + Errore di riconnessione al server + No comment provided by engineer. + + + Error reconnecting servers + Errore di riconnessione ai server + No comment provided by engineer. + Error removing member Errore nella rimozione del membro No comment provided by engineer. + + Error resetting statistics + Errore di azzeramento statistiche + No comment provided by engineer. + Error saving %@ servers Errore nel salvataggio dei server %@ @@ -2621,7 +3002,8 @@ Non è reversibile! Error: %@ Errore: %@ - No comment provided by engineer. + file error text + snd error text Error: URL is invalid @@ -2633,6 +3015,11 @@ Non è reversibile! Errore: nessun file di database No comment provided by engineer. + + Errors + Errori + No comment provided by engineer. + Even when disabled in the conversation. Anche quando disattivato nella conversazione. @@ -2658,6 +3045,11 @@ Non è reversibile! Errore di esportazione: No comment provided by engineer. + + Export theme + Esporta tema + No comment provided by engineer. + Exported database archive. Archivio database esportato. @@ -2691,8 +3083,33 @@ Non è reversibile! Favorite Preferito + swipe action + + + File error + Errore del file No comment provided by engineer. + + File not found - most likely file was deleted or cancelled. + File non trovato - probabilmente è stato eliminato o annullato. + file error text + + + File server error: %@ + Errore del server dei file: %@ + file error text + + + File status + Stato del file + No comment provided by engineer. + + + File status: %@ + Stato del file: %@ + copied message info + File will be deleted from servers. Il file verrà eliminato dai server. @@ -2713,6 +3130,11 @@ Non è reversibile! File: %@ No comment provided by engineer. + + Files + File + No comment provided by engineer. + Files & media File e multimediali @@ -2818,6 +3240,35 @@ Non è reversibile! Inoltrato da No comment provided by engineer. + + Forwarding server %@ failed to connect to destination server %@. Please try later. + Il server di inoltro %@ non è riuscito a connettersi al server di destinazione %@. Riprova più tardi. + No comment provided by engineer. + + + Forwarding server address is incompatible with network settings: %@. + L'indirizzo del server di inoltro è incompatibile con le impostazioni di rete: %@. + No comment provided by engineer. + + + Forwarding server version is incompatible with network settings: %@. + La versione del server di inoltro è incompatibile con le impostazioni di rete: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + Server di inoltro: %1$@ +Errore del server di destinazione: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + Server di inoltro: %1$@ +Errore: %2$@ + snd error text + Found desktop Desktop trovato @@ -2863,6 +3314,16 @@ Non è reversibile! GIF e adesivi No comment provided by engineer. + + Good afternoon! + Buon pomeriggio! + message preview + + + Good morning! + Buongiorno! + message preview + Group Gruppo @@ -3143,6 +3604,11 @@ Non è reversibile! Importazione fallita No comment provided by engineer. + + Import theme + Importa tema + No comment provided by engineer. + Importing archive Importazione archivio @@ -3265,6 +3731,11 @@ Non è reversibile! Interfaccia No comment provided by engineer. + + Interface colors + Colori dell'interfaccia + No comment provided by engineer. + Invalid QR code Codice QR non valido @@ -3366,6 +3837,11 @@ Non è reversibile! 3. La connessione è stata compromessa. No comment provided by engineer. + + It protects your IP address and connections. + Protegge il tuo indirizzo IP e le connessioni. + No comment provided by engineer. + It seems like you are already connected via this link. If it is not the case, there was an error (%@). Sembra che tu sia già connesso tramite questo link. In caso contrario, c'è stato un errore (%@). @@ -3384,7 +3860,7 @@ Non è reversibile! Join Entra - No comment provided by engineer. + swipe action Join group @@ -3428,6 +3904,11 @@ Questo è il tuo link per il gruppo %@! Tieni No comment provided by engineer. + + Keep conversation + Tieni la conversazione + No comment provided by engineer. + Keep the app open to use it from desktop Tieni aperta l'app per usarla dal desktop @@ -3471,7 +3952,7 @@ Questo è il tuo link per il gruppo %@! Leave Esci - No comment provided by engineer. + swipe action Leave group @@ -3603,11 +4084,26 @@ Questo è il tuo link per il gruppo %@! Max 30 secondi, ricevuto istantaneamente. No comment provided by engineer. + + Media & file servers + Server di multimediali e file + No comment provided by engineer. + + + Medium + Media + blur media + Member Membro No comment provided by engineer. + + Member inactive + Membro inattivo + item status text + Member role will be changed to "%@". All group members will be notified. Il ruolo del membro verrà cambiato in "%@". Tutti i membri del gruppo verranno avvisati. @@ -3623,6 +4119,11 @@ Questo è il tuo link per il gruppo %@! Il membro verrà rimosso dal gruppo, non è reversibile! No comment provided by engineer. + + Menus + Menu + No comment provided by engineer. + Message delivery error Errore di recapito del messaggio @@ -3633,11 +4134,31 @@ Questo è il tuo link per il gruppo %@! Ricevute di consegna dei messaggi! No comment provided by engineer. + + Message delivery warning + Avviso di consegna del messaggio + item status text + Message draft Bozza dei messaggi No comment provided by engineer. + + Message forwarded + Messaggio inoltrato + item status text + + + Message may be delivered later if member becomes active. + Il messaggio può essere consegnato più tardi se il membro diventa attivo. + item status description + + + Message queue info + Info coda messaggi + No comment provided by engineer. + Message reactions Reazioni ai messaggi @@ -3653,11 +4174,31 @@ Questo è il tuo link per il gruppo %@! Le reazioni ai messaggi sono vietate in questo gruppo. No comment provided by engineer. + + Message reception + Ricezione messaggi + No comment provided by engineer. + + + Message servers + Server dei messaggi + No comment provided by engineer. + Message source remains private. La fonte del messaggio resta privata. No comment provided by engineer. + + Message status + Stato del messaggio + No comment provided by engineer. + + + Message status: %@ + Stato del messaggio: %@ + copied message info + Message text Testo del messaggio @@ -3683,6 +4224,16 @@ Questo è il tuo link per il gruppo %@! I messaggi da %@ verranno mostrati! No comment provided by engineer. + + Messages received + Messaggi ricevuti + No comment provided by engineer. + + + Messages sent + Messaggi inviati + No comment provided by engineer. + Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. I messaggi, i file e le chiamate sono protetti da **crittografia end-to-end** con perfect forward secrecy, ripudio e recupero da intrusione. @@ -3783,11 +4334,6 @@ Questo è il tuo link per il gruppo %@! Probabilmente questa connessione è stata eliminata. item status description - - Most likely this contact has deleted the connection with you. - Probabilmente questo contatto ha eliminato la connessione con te. - No comment provided by engineer. - Multiple chat profiles Profili di chat multipli @@ -3796,7 +4342,7 @@ Questo è il tuo link per il gruppo %@! Mute Silenzia - No comment provided by engineer. + swipe action Muted when inactive! @@ -3806,7 +4352,7 @@ Questo è il tuo link per il gruppo %@! Name Nome - No comment provided by engineer. + swipe action Network & servers @@ -3818,6 +4364,11 @@ Questo è il tuo link per il gruppo %@! Connessione di rete No comment provided by engineer. + + Network issues - message expired after many attempts to send it. + Problemi di rete - messaggio scaduto dopo molti tentativi di inviarlo. + snd error text + Network management Gestione della rete @@ -3843,6 +4394,11 @@ Questo è il tuo link per il gruppo %@! Nuova chat No comment provided by engineer. + + New chat experience 🎉 + Una nuova esperienza di chat 🎉 + No comment provided by engineer. + New contact request Nuova richiesta di contatto @@ -3873,6 +4429,11 @@ Questo è il tuo link per il gruppo %@! Novità nella %@ No comment provided by engineer. + + New media options + Nuove opzioni multimediali + No comment provided by engineer. + New member role Nuovo ruolo del membro @@ -3918,6 +4479,11 @@ Questo è il tuo link per il gruppo %@! Nessun token del dispositivo! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + Ancora nessuna connessione diretta, il messaggio viene inoltrato dall'amministratore. + item status description + No filtered chats Nessuna chat filtrata @@ -3933,6 +4499,11 @@ Questo è il tuo link per il gruppo %@! Nessuna cronologia No comment provided by engineer. + + No info, try to reload + Nessuna informazione, prova a ricaricare + No comment provided by engineer. + No network connection Nessuna connessione di rete @@ -3953,6 +4524,11 @@ Questo è il tuo link per il gruppo %@! Non compatibile! No comment provided by engineer. + + Nothing selected + Nessuna selezione + No comment provided by engineer. + Notifications Notifiche @@ -3980,7 +4556,7 @@ Questo è il tuo link per il gruppo %@! Off Off - No comment provided by engineer. + blur media Ok @@ -4002,14 +4578,18 @@ Questo è il tuo link per il gruppo %@! Link di invito una tantum No comment provided by engineer. - - Onion hosts will be required for connection. Requires enabling VPN. - Gli host Onion saranno necessari per la connessione. Richiede l'attivazione della VPN. + + Onion hosts will be **required** for connection. +Requires compatible VPN. + Gli host Onion saranno **necessari** per la connessione. +Richiede l'attivazione della VPN. No comment provided by engineer. - - Onion hosts will be used when available. Requires enabling VPN. - Gli host Onion verranno usati quando disponibili. Richiede l'attivazione della VPN. + + Onion hosts will be used when available. +Requires compatible VPN. + Gli host Onion verranno usati quando disponibili. +Richiede l'attivazione della VPN. No comment provided by engineer. @@ -4022,6 +4602,11 @@ Questo è il tuo link per il gruppo %@! Solo i dispositivi client archiviano profili utente, i contatti, i gruppi e i messaggi inviati con la **crittografia end-to-end a 2 livelli**. No comment provided by engineer. + + Only delete conversation + Elimina solo la conversazione + No comment provided by engineer. + Only group owners can change group preferences. Solo i proprietari del gruppo possono modificarne le preferenze. @@ -4117,6 +4702,11 @@ Questo è il tuo link per il gruppo %@! Apri migrazione ad un altro dispositivo authentication reason + + Open server settings + Apri impostazioni server + No comment provided by engineer. + Open user profiles Apri i profili utente @@ -4157,6 +4747,11 @@ Questo è il tuo link per il gruppo %@! Altro No comment provided by engineer. + + Other %@ servers + Altri %@ server + No comment provided by engineer. + PING count Conteggio PING @@ -4222,6 +4817,11 @@ Questo è il tuo link per il gruppo %@! Incolla il link che hai ricevuto No comment provided by engineer. + + Pending + In attesa + No comment provided by engineer. + People can connect to you only via the links you share. Le persone possono connettersi a te solo tramite i link che condividi. @@ -4242,11 +4842,28 @@ Questo è il tuo link per il gruppo %@! Chiamate picture-in-picture No comment provided by engineer. + + Play from the chat list. + Riproduci dall'elenco delle chat. + No comment provided by engineer. + + + Please ask your contact to enable calls. + Chiedi al contatto di attivare le chiamate. + No comment provided by engineer. + Please ask your contact to enable sending voice messages. Chiedi al tuo contatto di attivare l'invio dei messaggi vocali. No comment provided by engineer. + + Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers. + Controlla che mobile e desktop siano collegati alla stessa rete locale e che il firewall del desktop consenta la connessione. +Si prega di condividere qualsiasi altro problema con gli sviluppatori. + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. Controlla di aver usato il link giusto o chiedi al tuo contatto di inviartene un altro. @@ -4344,6 +4961,11 @@ Errore: %@ Anteprima No comment provided by engineer. + + Previously connected servers + Server precedentemente connessi + No comment provided by engineer. + Privacy & security Privacy e sicurezza @@ -4359,11 +4981,31 @@ Errore: %@ Nomi di file privati No comment provided by engineer. + + Private message routing + Instradamento privato dei messaggi + No comment provided by engineer. + + + Private message routing 🚀 + Instradamento privato dei messaggi 🚀 + No comment provided by engineer. + Private notes Note private name of notes to self + + Private routing + Instradamento privato + No comment provided by engineer. + + + Private routing error + Errore di instradamento privato + No comment provided by engineer. + Profile and server connections Profilo e connessioni al server @@ -4394,6 +5036,11 @@ Errore: %@ Password del profilo No comment provided by engineer. + + Profile theme + Tema del profilo + No comment provided by engineer. + Profile update will be sent to your contacts. L'aggiornamento del profilo verrà inviato ai tuoi contatti. @@ -4444,11 +5091,23 @@ Errore: %@ Proibisci l'invio di messaggi vocali. No comment provided by engineer. + + Protect IP address + Proteggi l'indirizzo IP + No comment provided by engineer. + Protect app screen Proteggi la schermata dell'app No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + Proteggi il tuo indirizzo IP dai relay di messaggistica scelti dai tuoi contatti. +Attivalo nelle impostazioni *Rete e server*. + No comment provided by engineer. + Protect your chat profiles with a password! Proteggi i tuoi profili di chat con una password! @@ -4464,6 +5123,16 @@ Errore: %@ Scadenza del protocollo per KB No comment provided by engineer. + + Proxied + Via proxy + No comment provided by engineer. + + + Proxied servers + Server via proxy + No comment provided by engineer. + Push notifications Notifiche push @@ -4484,6 +5153,11 @@ Errore: %@ Valuta l'app No comment provided by engineer. + + Reachable chat toolbar + Barra degli strumenti di chat accessibile + No comment provided by engineer. + React… Reagisci… @@ -4492,7 +5166,7 @@ Errore: %@ Read Leggi - No comment provided by engineer. + swipe action Read more @@ -4529,6 +5203,11 @@ Errore: %@ Le ricevute sono disattivate No comment provided by engineer. + + Receive errors + Errori di ricezione + No comment provided by engineer. + Received at Ricevuto il @@ -4549,16 +5228,26 @@ Errore: %@ Messaggio ricevuto message info title + + Received messages + Messaggi ricevuti + No comment provided by engineer. + + + Received reply + Risposta ricevuta + No comment provided by engineer. + + + Received total + Totale ricevuto + No comment provided by engineer. + Receiving address will be changed to a different server. Address change will complete after sender comes online. L'indirizzo di ricezione verrà cambiato in un server diverso. La modifica dell'indirizzo verrà completata dopo che il mittente sarà in linea. No comment provided by engineer. - - Receiving concurrency - Ricezione concomitanza - No comment provided by engineer. - Receiving file will be stopped. La ricezione del file verrà interrotta. @@ -4584,11 +5273,36 @@ Errore: %@ I destinatari vedono gli aggiornamenti mentre li digiti. No comment provided by engineer. + + Reconnect + Riconnetti + No comment provided by engineer. + Reconnect all connected servers to force message delivery. It uses additional traffic. Riconnetti tutti i server connessi per imporre il recapito dei messaggi. Utilizza traffico aggiuntivo. No comment provided by engineer. + + Reconnect all servers + Riconnetti tutti i server + No comment provided by engineer. + + + Reconnect all servers? + Riconnettere tutti i server? + No comment provided by engineer. + + + Reconnect server to force message delivery. It uses additional traffic. + Riconnetti il server per forzare la consegna dei messaggi. Usa traffico aggiuntivo. + No comment provided by engineer. + + + Reconnect server? + Riconnettere il server? + No comment provided by engineer. + Reconnect servers? Riconnettere i server? @@ -4612,7 +5326,8 @@ Errore: %@ Reject Rifiuta - reject incoming call via notification + reject incoming call via notification + swipe action Reject (sender NOT notified) @@ -4639,6 +5354,11 @@ Errore: %@ Rimuovi No comment provided by engineer. + + Remove image + Rimuovi immagine + No comment provided by engineer. + Remove member Rimuovi membro @@ -4709,16 +5429,41 @@ Errore: %@ Ripristina No comment provided by engineer. + + Reset all hints + Ripristina tutti i suggerimenti + No comment provided by engineer. + + + Reset all statistics + Azzera tutte le statistiche + No comment provided by engineer. + + + Reset all statistics? + Azzerare tutte le statistiche? + No comment provided by engineer. + Reset colors Ripristina i colori No comment provided by engineer. + + Reset to app theme + Ripristina al tema dell'app + No comment provided by engineer. + Reset to defaults Ripristina i predefiniti No comment provided by engineer. + + Reset to user theme + Ripristina al tema dell'utente + No comment provided by engineer. + Restart the app to create a new chat profile Riavvia l'app per creare un nuovo profilo di chat @@ -4759,11 +5504,6 @@ Errore: %@ Rivela chat item action - - Revert - Ripristina - No comment provided by engineer. - Revoke Revoca @@ -4789,11 +5529,16 @@ Errore: %@ Avvia chat No comment provided by engineer. - - SMP servers + + SMP server Server SMP No comment provided by engineer. + + Safely receive files + Ricevi i file in sicurezza + No comment provided by engineer. + Safer groups Gruppi più sicuri @@ -4819,6 +5564,11 @@ Errore: %@ Salva e avvisa i membri del gruppo No comment provided by engineer. + + Save and reconnect + Salva e riconnetti + No comment provided by engineer. + Save and update group profile Salva e aggiorna il profilo del gruppo @@ -4899,6 +5649,16 @@ Errore: %@ Messaggio salvato message info title + + Scale + Scala + No comment provided by engineer. + + + Scan / Paste link + Scansiona / Incolla link + No comment provided by engineer. + Scan QR code Scansiona codice QR @@ -4939,11 +5699,21 @@ Errore: %@ Cerca o incolla un link SimpleX No comment provided by engineer. + + Secondary + Secondario + No comment provided by engineer. + Secure queue Coda sicura server test step + + Secured + Protetto + No comment provided by engineer. + Security assessment Valutazione della sicurezza @@ -4957,6 +5727,16 @@ Errore: %@ Select Seleziona + chat item action + + + Selected %lld + %lld selezionato + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + Le preferenze della chat selezionata vietano questo messaggio. No comment provided by engineer. @@ -4994,11 +5774,6 @@ Errore: %@ Invia ricevute di consegna a No comment provided by engineer. - - Send direct message - Invia messaggio diretto - No comment provided by engineer. - Send direct message to connect Invia messaggio diretto per connetterti @@ -5009,6 +5784,11 @@ Errore: %@ Invia messaggio a tempo No comment provided by engineer. + + Send errors + Errori di invio + No comment provided by engineer. + Send link previews Invia anteprime dei link @@ -5019,6 +5799,21 @@ Errore: %@ Invia messaggio in diretta No comment provided by engineer. + + Send message to enable calls. + Invia un messaggio per attivare le chiamate. + No comment provided by engineer. + + + Send messages directly when IP address is protected and your or destination server does not support private routing. + Invia messaggi direttamente quando l'indirizzo IP è protetto e il tuo server o quello di destinazione non supporta l'instradamento privato. + No comment provided by engineer. + + + Send messages directly when your or destination server does not support private routing. + Invia messaggi direttamente quando il tuo server o quello di destinazione non supporta l'instradamento privato. + No comment provided by engineer. + Send notifications Invia notifiche @@ -5109,6 +5904,11 @@ Errore: %@ Inviato il: %@ copied message info + + Sent directly + Inviato direttamente + No comment provided by engineer. + Sent file event Evento file inviato @@ -5119,11 +5919,46 @@ Errore: %@ Messaggio inviato message info title + + Sent messages + Messaggi inviati + No comment provided by engineer. + Sent messages will be deleted after set time. I messaggi inviati verranno eliminati dopo il tempo impostato. No comment provided by engineer. + + Sent reply + Risposta inviata + No comment provided by engineer. + + + Sent total + Totale inviato + No comment provided by engineer. + + + Sent via proxy + Inviato via proxy + No comment provided by engineer. + + + Server address + Indirizzo server + No comment provided by engineer. + + + Server address is incompatible with network settings. + L'indirizzo del server non è compatibile con le impostazioni di rete. + srv error text. + + + Server address is incompatible with network settings: %@. + L'indirizzo del server è incompatibile con le impostazioni di rete: %@. + No comment provided by engineer. + Server requires authorization to create queues, check password Il server richiede l'autorizzazione di creare code, controlla la password @@ -5139,11 +5974,36 @@ Errore: %@ Test del server fallito! No comment provided by engineer. + + Server type + Tipo server + No comment provided by engineer. + + + Server version is incompatible with network settings. + La versione del server non è compatibile con le impostazioni di rete. + srv error text + + + Server version is incompatible with your app: %@. + La versione del server è incompatibile con la tua app: %@. + No comment provided by engineer. + Servers Server No comment provided by engineer. + + Servers info + Info dei server + No comment provided by engineer. + + + Servers statistics will be reset - this cannot be undone! + Le statistiche dei server verranno azzerate - è irreversibile! + No comment provided by engineer. + Session code Codice di sessione @@ -5159,6 +6019,11 @@ Errore: %@ Imposta nome del contatto… No comment provided by engineer. + + Set default theme + Imposta tema predefinito + No comment provided by engineer. + Set group preferences Imposta le preferenze del gruppo @@ -5224,6 +6089,11 @@ Errore: %@ Condividere l'indirizzo con i contatti? No comment provided by engineer. + + Share from other apps. + Condividi da altre app. + No comment provided by engineer. + Share link Condividi link @@ -5234,6 +6104,11 @@ Errore: %@ Condividi questo link di invito una tantum No comment provided by engineer. + + Share to SimpleX + Condividi in SimpleX + No comment provided by engineer. + Share with contacts Condividi con i contatti @@ -5259,16 +6134,36 @@ Errore: %@ Mostra ultimi messaggi No comment provided by engineer. + + Show message status + Mostra stato del messaggio + No comment provided by engineer. + + + Show percentage + Mostra percentuale + No comment provided by engineer. + Show preview Mostra anteprima No comment provided by engineer. + + Show → on messages sent via private routing. + Mostra → nei messaggi inviati via instradamento privato. + No comment provided by engineer. + Show: Mostra: No comment provided by engineer. + + SimpleX + SimpleX + No comment provided by engineer. + SimpleX Address Indirizzo SimpleX @@ -5344,6 +6239,11 @@ Errore: %@ Modalità incognito semplificata No comment provided by engineer. + + Size + Dimensione + No comment provided by engineer. + Skip Salta @@ -5359,11 +6259,26 @@ Errore: %@ Piccoli gruppi (max 20) No comment provided by engineer. + + Soft + Leggera + blur media + + + Some file(s) were not exported: + Alcuni file non sono stati esportati: + No comment provided by engineer. + Some non-fatal errors occurred during import - you may see Chat console for more details. Si sono verificati alcuni errori non gravi durante l'importazione: vedi la console della chat per i dettagli. No comment provided by engineer. + + Some non-fatal errors occurred during import: + Si sono verificati alcuni errori non fatali durante l'importazione: + No comment provided by engineer. + Somebody Qualcuno @@ -5389,6 +6304,16 @@ Errore: %@ Avvia la migrazione No comment provided by engineer. + + Starting from %@. + Inizio da %@. + No comment provided by engineer. + + + Statistics + Statistiche + No comment provided by engineer. + Stop Ferma @@ -5449,11 +6374,31 @@ Errore: %@ Arresto della chat No comment provided by engineer. + + Strong + Forte + blur media + Submit Invia No comment provided by engineer. + + Subscribed + Iscritto + No comment provided by engineer. + + + Subscription errors + Errori di iscrizione + No comment provided by engineer. + + + Subscriptions ignored + Iscrizioni ignorate + No comment provided by engineer. + Support SimpleX Chat Supporta SimpleX Chat @@ -5469,6 +6414,11 @@ Errore: %@ Autenticazione di sistema No comment provided by engineer. + + TCP connection + Connessione TCP + No comment provided by engineer. + TCP connection timeout Scadenza connessione TCP @@ -5529,9 +6479,9 @@ Errore: %@ Tocca per scansionare No comment provided by engineer. - - Tap to start a new chat - Tocca per iniziare una chat + + Temporary file error + Errore del file temporaneo No comment provided by engineer. @@ -5586,6 +6536,11 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.L'app può avvisarti quando ricevi messaggi o richieste di contatto: apri le impostazioni per attivare. No comment provided by engineer. + + The app will ask to confirm downloads from unknown file servers (except .onion). + L'app chiederà di confermare i download da server di file sconosciuti (eccetto .onion). + No comment provided by engineer. + The attempt to change database passphrase was not completed. Il tentativo di cambiare la password del database non è stato completato. @@ -5631,6 +6586,16 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Il messaggio sarà segnato come moderato per tutti i membri. No comment provided by engineer. + + The messages will be deleted for all members. + I messaggi verranno eliminati per tutti i membri. + No comment provided by engineer. + + + The messages will be marked as moderated for all members. + I messaggi verranno contrassegnati come moderati per tutti i membri. + No comment provided by engineer. + The next generation of private messaging La nuova generazione di messaggistica privata @@ -5666,9 +6631,9 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Il testo che hai incollato non è un link SimpleX. No comment provided by engineer. - - Theme - Tema + + Themes + Temi No comment provided by engineer. @@ -5736,11 +6701,21 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Questo è il tuo link una tantum! No comment provided by engineer. + + This link was used with another mobile device, please create a new link on the desktop. + Questo link è stato usato con un altro dispositivo mobile, creane uno nuovo sul desktop. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. Questa impostazione si applica ai messaggi del profilo di chat attuale **%@**. No comment provided by engineer. + + Title + Titoli + No comment provided by engineer. + To ask any questions and to receive updates: Per porre domande e ricevere aggiornamenti: @@ -5771,6 +6746,11 @@ Può accadere a causa di qualche bug o quando la connessione è compromessa.Per proteggere il fuso orario, i file immagine/vocali usano UTC. No comment provided by engineer. + + To protect your IP address, private routing uses your SMP servers to deliver messages. + Per proteggere il tuo indirizzo IP, l'instradamento privato usa i tuoi server SMP per consegnare i messaggi. + No comment provided by engineer. + To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5798,16 +6778,36 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Per verificare la crittografia end-to-end con il tuo contatto, confrontate (o scansionate) il codice sui vostri dispositivi. No comment provided by engineer. + + Toggle chat list: + Cambia l'elenco delle chat: + No comment provided by engineer. + Toggle incognito when connecting. Attiva/disattiva l'incognito quando ti colleghi. No comment provided by engineer. + + Toolbar opacity + Opacità barra degli strumenti + No comment provided by engineer. + + + Total + Totale + No comment provided by engineer. + Transport isolation Isolamento del trasporto No comment provided by engineer. + + Transport sessions + Sessioni di trasporto + No comment provided by engineer. + Trying to connect to the server used to receive messages from this contact (error: %@). Tentativo di connessione al server usato per ricevere messaggi da questo contatto (errore: %@). @@ -5863,11 +6863,6 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Sbloccare il membro? No comment provided by engineer. - - Unexpected error: %@ - Errore imprevisto: % @ - item status description - Unexpected migration state Stato di migrazione imprevisto @@ -5876,7 +6871,7 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Unfav. Non pref. - No comment provided by engineer. + swipe action Unhide @@ -5913,6 +6908,11 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Errore sconosciuto No comment provided by engineer. + + Unknown servers! + Server sconosciuti! + No comment provided by engineer. + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. A meno che non utilizzi l'interfaccia di chiamata iOS, attiva la modalità Non disturbare per evitare interruzioni. @@ -5948,12 +6948,12 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Unmute Riattiva notifiche - No comment provided by engineer. + swipe action Unread Non letto - No comment provided by engineer. + swipe action Up to 100 last messages are sent to new members. @@ -5965,11 +6965,6 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Aggiorna No comment provided by engineer. - - Update .onion hosts setting? - Aggiornare l'impostazione degli host .onion? - No comment provided by engineer. - Update database passphrase Aggiorna la password del database @@ -5980,9 +6975,9 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Aggiornare le impostazioni di rete? No comment provided by engineer. - - Update transport isolation mode? - Aggiornare la modalità di isolamento del trasporto? + + Update settings? + Aggiornare le impostazioni? No comment provided by engineer. @@ -5990,16 +6985,16 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e L'aggiornamento delle impostazioni riconnetterà il client a tutti i server. No comment provided by engineer. - - Updating this setting will re-connect the client to all servers. - L'aggiornamento di questa impostazione riconnetterà il client a tutti i server. - No comment provided by engineer. - Upgrade and open chat Aggiorna e apri chat No comment provided by engineer. + + Upload errors + Errori di invio + No comment provided by engineer. + Upload failed Invio fallito @@ -6010,6 +7005,16 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Invia file server test step + + Uploaded + Inviato + No comment provided by engineer. + + + Uploaded files + File inviati + No comment provided by engineer. + Uploading archive Invio dell'archivio @@ -6060,6 +7065,16 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Usare solo notifiche locali? No comment provided by engineer. + + Use private routing with unknown servers when IP address is not protected. + Usa l'instradamento privato con server sconosciuti quando l'indirizzo IP non è protetto. + No comment provided by engineer. + + + Use private routing with unknown servers. + Usa l'instradamento privato con server sconosciuti. + No comment provided by engineer. + Use server Usa il server @@ -6070,14 +7085,19 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Usa l'app mentre sei in chiamata. No comment provided by engineer. + + Use the app with one hand. + Usa l'app con una mano sola. + No comment provided by engineer. + User profile Profilo utente No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - L'uso di host .onion richiede un fornitore di VPN compatibile. + + User selection + Selezione utente No comment provided by engineer. @@ -6210,6 +7230,16 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e In attesa del video No comment provided by engineer. + + Wallpaper accent + Tinta dello sfondo + No comment provided by engineer. + + + Wallpaper background + Retro dello sfondo + No comment provided by engineer. + Warning: starting chat on multiple devices is not supported and will cause message delivery failures Attenzione: avviare la chat su più dispositivi non è supportato e provocherà problemi di recapito dei messaggi @@ -6295,18 +7325,38 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Con consumo di batteria ridotto. No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + Senza Tor o VPN, il tuo indirizzo IP sarà visibile ai server di file. + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + Senza Tor o VPN, il tuo indirizzo IP sarà visibile a questi relay XFTP: %@. + No comment provided by engineer. + Wrong database passphrase Password del database sbagliata No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + Chiave sbagliata o connessione sconosciuta - molto probabilmente questa connessione è stata eliminata. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + Chiave sbagliata o indirizzo sconosciuto per frammento del file - probabilmente il file è stato eliminato. + file error text + Wrong passphrase! Password sbagliata! No comment provided by engineer. - - XFTP servers + + XFTP server Server XFTP No comment provided by engineer. @@ -6387,11 +7437,21 @@ Ripetere la richiesta di ingresso? Sei stato/a invitato/a al gruppo No comment provided by engineer. + + You are not connected to these servers. Private routing is used to deliver messages to them. + Non sei connesso/a a questi server. L'instradamento privato è usato per consegnare loro i messaggi. + No comment provided by engineer. + You can accept calls from lock screen, without device and app authentication. Puoi accettare chiamate dalla schermata di blocco, senza l'autenticazione del dispositivo e dell'app. No comment provided by engineer. + + You can change it in Appearance settings. + Puoi cambiarlo nelle impostazioni dell'aspetto. + No comment provided by engineer. + You can create it later Puoi crearlo più tardi @@ -6422,11 +7482,16 @@ Ripetere la richiesta di ingresso? Puoi renderlo visibile ai tuoi contatti SimpleX nelle impostazioni. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Ora puoi inviare messaggi a %@ notification body + + You can send messages to %@ from Archived contacts. + Puoi inviare messaggi a %@ dai contatti archiviati. + No comment provided by engineer. + You can set lock screen notification preview via settings. Puoi impostare l'anteprima della notifica nella schermata di blocco tramite le impostazioni. @@ -6452,6 +7517,11 @@ Ripetere la richiesta di ingresso? Puoi avviare la chat via Impostazioni / Database o riavviando l'app No comment provided by engineer. + + You can still view conversation with %@ in the list of chats. + Puoi ancora vedere la conversazione con %@ nell'elenco delle chat. + No comment provided by engineer. + You can turn on SimpleX Lock via Settings. Puoi attivare SimpleX Lock tramite le impostazioni. @@ -6494,11 +7564,6 @@ Repeat connection request? Ripetere la richiesta di connessione? No comment provided by engineer. - - You have no chats - Non hai chat - No comment provided by engineer. - You have to enter passphrase every time the app starts - it is not stored on the device. Devi inserire la password ogni volta che si avvia l'app: non viene memorizzata sul dispositivo. @@ -6519,11 +7584,26 @@ Ripetere la richiesta di connessione? Sei entrato/a in questo gruppo. Connessione al membro del gruppo invitante. No comment provided by engineer. + + You may migrate the exported database. + Puoi migrare il database esportato. + No comment provided by engineer. + + + You may save the exported archive. + Puoi salvare l'archivio esportato. + No comment provided by engineer. + You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. Devi usare la versione più recente del tuo database della chat SOLO su un dispositivo, altrimenti potresti non ricevere più i messaggi da alcuni contatti. No comment provided by engineer. + + You need to allow your contact to call to be able to call them. + Devi consentire le chiamate al tuo contatto per poterlo chiamare. + No comment provided by engineer. + You need to allow your contact to send voice messages to be able to send them. Devi consentire al tuo contatto di inviare messaggi vocali per poterli inviare anche tu. @@ -6639,13 +7719,6 @@ Ripetere la richiesta di connessione? I tuoi profili di chat No comment provided by engineer. - - Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link). - Il tuo contatto deve essere in linea per completare la connessione. -Puoi annullare questa connessione e rimuovere il contatto (e riprovare più tardi con un link nuovo). - No comment provided by engineer. - Your contact sent a file that is larger than currently supported maximum size (%@). Il tuo contatto ha inviato un file più grande della dimensione massima attualmente supportata (%@). @@ -6793,6 +7866,11 @@ I server di SimpleX non possono vedere il tuo profilo. e altri %lld eventi No comment provided by engineer. + + attempts + tentativi + No comment provided by engineer. + audio call (not e2e encrypted) chiamata audio (non crittografata e2e) @@ -6833,6 +7911,11 @@ I server di SimpleX non possono vedere il tuo profilo. grassetto No comment provided by engineer. + + call + chiama + No comment provided by engineer. + call error errore di chiamata @@ -6983,6 +8066,11 @@ I server di SimpleX non possono vedere il tuo profilo. giorni time unit + + decryption errors + errori di decifrazione + No comment provided by engineer. + default (%@) predefinito (%@) @@ -7033,6 +8121,11 @@ I server di SimpleX non possono vedere il tuo profilo. messaggio duplicato integrity error chat item + + duplicates + doppi + No comment provided by engineer. + e2e encrypted crittografato e2e @@ -7113,6 +8206,11 @@ I server di SimpleX non possono vedere il tuo profilo. evento accaduto No comment provided by engineer. + + expired + scaduto + No comment provided by engineer. + forwarded inoltrato @@ -7143,6 +8241,11 @@ I server di SimpleX non possono vedere il tuo profilo. Il portachiavi di iOS verrà usato per archiviare in modo sicuro la password dopo il riavvio dell'app o la modifica della password; consentirà di ricevere notifiche push. No comment provided by engineer. + + inactive + inattivo + No comment provided by engineer. + incognito via contact address link incognito via link indirizzo del contatto @@ -7183,6 +8286,11 @@ I server di SimpleX non possono vedere il tuo profilo. invito al gruppo %@ group name + + invite + invita + No comment provided by engineer. + invited ha invitato @@ -7238,6 +8346,11 @@ I server di SimpleX non possono vedere il tuo profilo. si è connesso/a rcv group event chat item + + message + messaggio + No comment provided by engineer. + message received messaggio ricevuto @@ -7268,6 +8381,11 @@ I server di SimpleX non possono vedere il tuo profilo. mesi time unit + + mute + silenzia + No comment provided by engineer. + never mai @@ -7320,6 +8438,16 @@ I server di SimpleX non possono vedere il tuo profilo. on group pref value + + other + altro + No comment provided by engineer. + + + other errors + altri errori + No comment provided by engineer. + owner proprietario @@ -7390,6 +8518,11 @@ I server di SimpleX non possono vedere il tuo profilo. salvato da %@ No comment provided by engineer. + + search + cerca + No comment provided by engineer. + sec sec @@ -7415,6 +8548,15 @@ I server di SimpleX non possono vedere il tuo profilo. invia messaggio diretto No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + info coda server: %1$@ + +ultimo msg ricevuto: %2$@ + queue info + set new contact address impostato nuovo indirizzo di contatto @@ -7455,11 +8597,26 @@ I server di SimpleX non possono vedere il tuo profilo. sconosciuto connection info + + unknown servers + relay sconosciuti + No comment provided by engineer. + unknown status stato sconosciuto No comment provided by engineer. + + unmute + riattiva notifiche + No comment provided by engineer. + + + unprotected + non protetto + No comment provided by engineer. + updated group profile ha aggiornato il profilo del gruppo @@ -7500,6 +8657,11 @@ I server di SimpleX non possono vedere il tuo profilo. via relay No comment provided by engineer. + + video + video + No comment provided by engineer. + video call (not e2e encrypted) videochiamata (non crittografata e2e) @@ -7525,6 +8687,11 @@ I server di SimpleX non possono vedere il tuo profilo. settimane time unit + + when IP hidden + quando l'IP è nascosto + No comment provided by engineer. + yes @@ -7609,7 +8776,7 @@ I server di SimpleX non possono vedere il tuo profilo.
- +
@@ -7646,7 +8813,7 @@ I server di SimpleX non possono vedere il tuo profilo.
- +
@@ -7666,4 +8833,218 @@ I server di SimpleX non possono vedere il tuo profilo.
+ +
+ +
+ + + SimpleX SE + SimpleX SE + Bundle display name + + + SimpleX SE + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright © 2024 SimpleX Chat. Tutti i diritti riservati. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + %@ + No comment provided by engineer. + + + App is locked! + L'app è bloccata! + No comment provided by engineer. + + + Cancel + Annulla + No comment provided by engineer. + + + Cannot access keychain to save database password + Impossibile accedere al portachiavi per salvare la password del database + No comment provided by engineer. + + + Cannot forward message + Impossibile inoltrare il messaggio + No comment provided by engineer. + + + Comment + Commento + No comment provided by engineer. + + + Currently maximum supported file size is %@. + Attualmente la dimensione massima supportata è di %@. + No comment provided by engineer. + + + Database downgrade required + Downgrade del database necessario + No comment provided by engineer. + + + Database encrypted! + Database crittografato! + No comment provided by engineer. + + + Database error + Errore del database + No comment provided by engineer. + + + Database passphrase is different from saved in the keychain. + La password del database è diversa da quella salvata nel portachiavi. + No comment provided by engineer. + + + Database passphrase is required to open chat. + La password del database è necessaria per aprire la chat. + No comment provided by engineer. + + + Database upgrade required + Aggiornamento del database necessario + No comment provided by engineer. + + + Error preparing file + Errore nella preparazione del file + No comment provided by engineer. + + + Error preparing message + Errore nella preparazione del messaggio + No comment provided by engineer. + + + Error: %@ + Errore: %@ + No comment provided by engineer. + + + File error + Errore del file + No comment provided by engineer. + + + Incompatible database version + Versione del database incompatibile + No comment provided by engineer. + + + Invalid migration confirmation + Conferma di migrazione non valida + No comment provided by engineer. + + + Keychain error + Errore del portachiavi + No comment provided by engineer. + + + Large file! + File grande! + No comment provided by engineer. + + + No active profile + Nessun profilo attivo + No comment provided by engineer. + + + Ok + Ok + No comment provided by engineer. + + + Open the app to downgrade the database. + Apri l'app per eseguire il downgrade del database. + No comment provided by engineer. + + + Open the app to upgrade the database. + Apri l'app per aggiornare il database. + No comment provided by engineer. + + + Passphrase + Password + No comment provided by engineer. + + + Please create a profile in the SimpleX app + Crea un profilo nell'app SimpleX + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + Le preferenze della chat selezionata vietano questo messaggio. + No comment provided by engineer. + + + Sending a message takes longer than expected. + L'invio di un messaggio richiede più tempo del previsto. + No comment provided by engineer. + + + Sending message… + Invio messaggio… + No comment provided by engineer. + + + Share + Condividi + No comment provided by engineer. + + + Slow network? + Rete lenta? + No comment provided by engineer. + + + Unknown database error: %@ + Errore del database sconosciuto: %@ + No comment provided by engineer. + + + Unsupported format + Formato non supportato + No comment provided by engineer. + + + Wait + Attendi + No comment provided by engineer. + + + Wrong database passphrase + Password del database sbagliata + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + Puoi consentire la condivisione in Privacy e sicurezza / impostazioni di SimpleX Lock. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/it.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/it.xcloc/contents.json b/apps/ios/SimpleX Localizations/it.xcloc/contents.json index 2ad653d36f..13870ab8dd 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/it.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "it", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "15F31d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "15.4" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff index 3f32998707..a545f3ba05 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Localized Contents/ja.xliff @@ -2,7 +2,7 @@
- +
@@ -127,11 +127,6 @@ %@ は検証されています No comment provided by engineer. - - %@ servers - %@ サーバー - No comment provided by engineer. - %@ uploaded %@ アップロード済 @@ -551,16 +546,16 @@ SimpleXアドレスについて No comment provided by engineer. - - Accent color - アクセントカラー + + Accent No comment provided by engineer. Accept 承諾 accept contact request via notification - accept incoming call via notification + accept incoming call via notification + swipe action Accept connection request? @@ -575,7 +570,20 @@ Accept incognito シークレットモードで承諾 - accept contact request via notification + accept contact request via notification + swipe action + + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + No comment provided by engineer. + + + Active connections + No comment provided by engineer. Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. @@ -596,16 +604,16 @@ プロフィールを追加 No comment provided by engineer. + + Add server + サーバを追加 + No comment provided by engineer. + Add servers by scanning QR codes. QRコードでサーバを追加する。 No comment provided by engineer. - - Add server… - サーバを追加… - No comment provided by engineer. - Add to another device 別の端末に追加 @@ -616,6 +624,18 @@ ウェルカムメッセージを追加 No comment provided by engineer. + + Additional accent + No comment provided by engineer. + + + Additional accent 2 + No comment provided by engineer. + + + Additional secondary + No comment provided by engineer. + Address アドレス @@ -640,6 +660,10 @@ ネットワーク詳細設定 No comment provided by engineer. + + Advanced settings + No comment provided by engineer. + All app data is deleted. すべてのアプリデータが削除されます。 @@ -655,6 +679,10 @@ 入力するとすべてのデータが消去されます。 No comment provided by engineer. + + All data is private to your device. + No comment provided by engineer. + All group members will remain connected. グループ全員の接続が継続します。 @@ -673,6 +701,10 @@ All new messages from %@ will be hidden! No comment provided by engineer. + + All profiles + No comment provided by engineer. + All your contacts will remain connected. あなたの連絡先が繋がったまま継続します。 @@ -698,11 +730,19 @@ 連絡先が通話を許可している場合のみ通話を許可する。 No comment provided by engineer. + + Allow calls? + No comment provided by engineer. + Allow disappearing messages only if your contact allows it to you. 連絡先が許可している場合のみ消えるメッセージを許可する。 No comment provided by engineer. + + Allow downgrade + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) 送信相手も永久メッセージ削除を許可する時のみに許可する。 @@ -728,6 +768,10 @@ 消えるメッセージの送信を許可する。 No comment provided by engineer. + + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) 送信済みメッセージの永久削除を許可する。(24時間) @@ -798,6 +842,10 @@ すでにグループに参加しています! No comment provided by engineer. + + Always use private routing. + No comment provided by engineer. + Always use relay 常にリレーを経由する @@ -862,10 +910,22 @@ Apply No comment provided by engineer. + + Apply to + No comment provided by engineer. + Archive and upload No comment provided by engineer. + + Archive contacts to chat later. + No comment provided by engineer. + + + Archived contacts + No comment provided by engineer. + Archiving database No comment provided by engineer. @@ -935,6 +995,10 @@ 戻る No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address No comment provided by engineer. @@ -958,6 +1022,14 @@ より良いメッセージ No comment provided by engineer. + + Better networking + No comment provided by engineer. + + + Black + No comment provided by engineer. + Block No comment provided by engineer. @@ -986,6 +1058,14 @@ Blocked by admin No comment provided by engineer. + + Blur for better privacy. + No comment provided by engineer. + + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. 自分も相手もメッセージへのリアクションを追加できます。 @@ -1031,10 +1111,22 @@ 通話 No comment provided by engineer. + + Calls prohibited! + No comment provided by engineer. + Camera not available No comment provided by engineer. + + Can't call contact + No comment provided by engineer. + + + Can't call member + No comment provided by engineer. + Can't invite contact! 連絡先を招待できません! @@ -1045,6 +1137,10 @@ 連絡先を招待できません! No comment provided by engineer. + + Can't message member + No comment provided by engineer. + Cancel 中止 @@ -1059,11 +1155,19 @@ データベースのパスワードを保存するためのキーチェーンにアクセスできません No comment provided by engineer. + + Cannot forward message + No comment provided by engineer. + Cannot receive file ファイル受信ができません No comment provided by engineer. + + Capacity exceeded - recipient did not receive previously sent messages. + snd error text + Cellular No comment provided by engineer. @@ -1124,6 +1228,10 @@ チャットのアーカイブ No comment provided by engineer. + + Chat colors + No comment provided by engineer. + Chat console チャットのコンソール @@ -1139,6 +1247,10 @@ チャットのデータベースが削除されました No comment provided by engineer. + + Chat database exported + No comment provided by engineer. + Chat database imported チャットのデータベースが読み込まれました @@ -1158,6 +1270,10 @@ Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat. No comment provided by engineer. + + Chat list + No comment provided by engineer. + Chat migrated! No comment provided by engineer. @@ -1167,6 +1283,10 @@ チャット設定 No comment provided by engineer. + + Chat theme + No comment provided by engineer. + Chats チャット @@ -1196,10 +1316,22 @@ ライブラリから選択 No comment provided by engineer. + + Chunks deleted + No comment provided by engineer. + + + Chunks downloaded + No comment provided by engineer. + + + Chunks uploaded + No comment provided by engineer. + Clear 消す - No comment provided by engineer. + swipe action Clear conversation @@ -1220,9 +1352,12 @@ 検証を消す No comment provided by engineer. - - Colors - + + Color chats with the new themes. + No comment provided by engineer. + + + Color mode No comment provided by engineer. @@ -1235,11 +1370,19 @@ 連絡先とセキュリティコードを確認する。 No comment provided by engineer. + + Completed + No comment provided by engineer. + Configure ICE servers ICEサーバを設定 No comment provided by engineer. + + Configured %@ servers + No comment provided by engineer. + Confirm 確認 @@ -1250,11 +1393,19 @@ パスコードを確認 No comment provided by engineer. + + Confirm contact deletion? + No comment provided by engineer. + Confirm database upgrades データベースのアップグレードを確認 No comment provided by engineer. + + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings No comment provided by engineer. @@ -1295,6 +1446,10 @@ Connect to desktop No comment provided by engineer. + + Connect to your friends faster. + No comment provided by engineer. + Connect to yourself? No comment provided by engineer. @@ -1327,14 +1482,26 @@ This is your own one-time link! Connect with %@ No comment provided by engineer. + + Connected + No comment provided by engineer. + Connected desktop No comment provided by engineer. + + Connected servers + No comment provided by engineer. + Connected to desktop No comment provided by engineer. + + Connecting + No comment provided by engineer. + Connecting to server… サーバーに接続中… @@ -1345,6 +1512,10 @@ This is your own one-time link! サーバーに接続中… (エラー: %@) No comment provided by engineer. + + Connecting to contact, please wait or check later! + No comment provided by engineer. + Connecting to desktop No comment provided by engineer. @@ -1354,6 +1525,10 @@ This is your own one-time link! 接続 No comment provided by engineer. + + Connection and servers status. + No comment provided by engineer. + Connection error 接続エラー @@ -1364,6 +1539,10 @@ This is your own one-time link! 接続エラー (AUTH) No comment provided by engineer. + + Connection notifications + No comment provided by engineer. + Connection request sent! 接続リクエストを送信しました! @@ -1378,6 +1557,14 @@ This is your own one-time link! 接続タイムアウト No comment provided by engineer. + + Connection with desktop stopped + No comment provided by engineer. + + + Connections + No comment provided by engineer. + Contact allows 連絡先の許可 @@ -1388,6 +1575,10 @@ This is your own one-time link! 連絡先に既に存在します No comment provided by engineer. + + Contact deleted! + No comment provided by engineer. + Contact hidden: 連絡先が非表示: @@ -1398,9 +1589,8 @@ This is your own one-time link! 連絡先は接続中 notification - - Contact is not connected yet! - 連絡先がまだ繋がってません! + + Contact is deleted. No comment provided by engineer. @@ -1413,6 +1603,10 @@ This is your own one-time link! 連絡先の設定 No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + No comment provided by engineer. + Contacts 連絡先 @@ -1428,10 +1622,18 @@ This is your own one-time link! 続ける No comment provided by engineer. + + Conversation deleted! + No comment provided by engineer. + Copy コピー - chat item action + No comment provided by engineer. + + + Copy error + No comment provided by engineer. Core version: v%@ @@ -1504,6 +1706,10 @@ This is your own one-time link! プロフィールを作成する No comment provided by engineer. + + Created + No comment provided by engineer. + Created at No comment provided by engineer. @@ -1535,6 +1741,10 @@ This is your own one-time link! 現在の暗証フレーズ… No comment provided by engineer. + + Current profile + No comment provided by engineer. + Currently maximum supported file size is %@. 現在サポートされている最大ファイルサイズは %@. @@ -1545,11 +1755,19 @@ This is your own one-time link! カスタム時間 No comment provided by engineer. + + Customize theme + No comment provided by engineer. + Dark ダークモード No comment provided by engineer. + + Dark mode colors + No comment provided by engineer. + Database ID データベースID @@ -1648,6 +1866,10 @@ This is your own one-time link! データベースはアプリ再起動時に移行されます No comment provided by engineer. + + Debug delivery + No comment provided by engineer. + Decentralized 分散型 @@ -1661,17 +1883,17 @@ This is your own one-time link! Delete 削除 - chat item action + chat item action + swipe action + + + Delete %lld messages of members? + No comment provided by engineer. Delete %lld messages? No comment provided by engineer. - - Delete Contact - 連絡先を削除 - No comment provided by engineer. - Delete address アドレスを削除 @@ -1726,9 +1948,8 @@ This is your own one-time link! 連絡先を削除 No comment provided by engineer. - - Delete contact? -This cannot be undone! + + Delete contact? No comment provided by engineer. @@ -1820,11 +2041,6 @@ This cannot be undone! 古いデータベースを削除しますか? No comment provided by engineer. - - Delete pending connection - 確認待ちの接続を削除 - No comment provided by engineer. - Delete pending connection? 接続待ちの接続を削除しますか? @@ -1840,11 +2056,23 @@ This cannot be undone! 待ち行列を削除 server test step + + Delete up to 20 messages at once. + No comment provided by engineer. + Delete user profile? ユーザープロフィールを削除しますか? No comment provided by engineer. + + Delete without notification + No comment provided by engineer. + + + Deleted + No comment provided by engineer. + Deleted at 削除完了 @@ -1855,6 +2083,10 @@ This cannot be undone! 削除完了: %@ copied message info + + Deletion errors + No comment provided by engineer. + Delivery 配信 @@ -1887,11 +2119,35 @@ This cannot be undone! Desktop devices No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + No comment provided by engineer. + + + Destination server error: %@ + snd error text + + + Destination server version of %@ is incompatible with forwarding server %@. + No comment provided by engineer. + + + Detailed statistics + No comment provided by engineer. + + + Details + No comment provided by engineer. + Develop 開発 No comment provided by engineer. + + Developer options + No comment provided by engineer. + Developer tools 開発ツール @@ -1942,6 +2198,10 @@ This cannot be undone! すべて無効 No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message 消えるメッセージ @@ -1990,11 +2250,19 @@ This cannot be undone! Discover via local network No comment provided by engineer. + + Do NOT send messages directly, even if your or destination server does not support private routing. + No comment provided by engineer. + Do NOT use SimpleX for emergency calls. 緊急通報にSimpleXを使用しないでください。 No comment provided by engineer. + + Do NOT use private routing. + No comment provided by engineer. + Do it later 後で行う @@ -2028,6 +2296,10 @@ This cannot be undone! Download chat item action + + Download errors + No comment provided by engineer. + Download failed No comment provided by engineer. @@ -2037,6 +2309,14 @@ This cannot be undone! ファイルをダウンロード server test step + + Downloaded + No comment provided by engineer. + + + Downloaded files + No comment provided by engineer. + Downloading archive No comment provided by engineer. @@ -2133,6 +2413,10 @@ This cannot be undone! 自己破壊パスコードを有効にする set passcode view + + Enabled + No comment provided by engineer. + Enabled for No comment provided by engineer. @@ -2295,6 +2579,10 @@ This cannot be undone! 設定変更にエラー発生 No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address アドレス作成にエラー発生 @@ -2344,11 +2632,6 @@ This cannot be undone! 接続の削除エラー No comment provided by engineer. - - Error deleting contact - 連絡先の削除にエラー発生 - No comment provided by engineer. - Error deleting database データベースの削除にエラー発生 @@ -2392,6 +2675,10 @@ This cannot be undone! チャットデータベースのエキスポートにエラー発生 No comment provided by engineer. + + Error exporting theme: %@ + No comment provided by engineer. + Error importing chat database チャットデータベースのインポートにエラー発生 @@ -2416,11 +2703,23 @@ This cannot be undone! ファイル受信にエラー発生 No comment provided by engineer. + + Error reconnecting server + No comment provided by engineer. + + + Error reconnecting servers + No comment provided by engineer. + Error removing member メンバー除名にエラー発生 No comment provided by engineer. + + Error resetting statistics + No comment provided by engineer. + Error saving %@ servers %@ サーバの保存エラー @@ -2534,7 +2833,8 @@ This cannot be undone! Error: %@ エラー : %@ - No comment provided by engineer. + file error text + snd error text Error: URL is invalid @@ -2546,6 +2846,10 @@ This cannot be undone! エラー: データベースが存在しません No comment provided by engineer. + + Errors + No comment provided by engineer. + Even when disabled in the conversation. 会話中に無効になっている場合でも。 @@ -2570,6 +2874,10 @@ This cannot be undone! エクスポートエラー: No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. データベースのアーカイブをエクスポートします。 @@ -2601,8 +2909,28 @@ This cannot be undone! Favorite お気に入り + swipe action + + + File error No comment provided by engineer. + + File not found - most likely file was deleted or cancelled. + file error text + + + File server error: %@ + file error text + + + File status + No comment provided by engineer. + + + File status: %@ + copied message info + File will be deleted from servers. ファイルはサーバーから削除されます。 @@ -2623,6 +2951,10 @@ This cannot be undone! ファイル: %@ No comment provided by engineer. + + Files + No comment provided by engineer. + Files & media ファイルとメディア @@ -2721,6 +3053,28 @@ This cannot be undone! Forwarded from No comment provided by engineer. + + Forwarding server %@ failed to connect to destination server %@. Please try later. + No comment provided by engineer. + + + Forwarding server address is incompatible with network settings: %@. + No comment provided by engineer. + + + Forwarding server version is incompatible with network settings: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + snd error text + Found desktop No comment provided by engineer. @@ -2764,6 +3118,14 @@ This cannot be undone! GIFとステッカー No comment provided by engineer. + + Good afternoon! + message preview + + + Good morning! + message preview + Group グループ @@ -3038,6 +3400,10 @@ This cannot be undone! Import failed No comment provided by engineer. + + Import theme + No comment provided by engineer. + Importing archive No comment provided by engineer. @@ -3154,6 +3520,10 @@ This cannot be undone! インターフェース No comment provided by engineer. + + Interface colors + No comment provided by engineer. + Invalid QR code No comment provided by engineer. @@ -3249,6 +3619,10 @@ This cannot be undone! 3. 接続に問題があった。 No comment provided by engineer. + + It protects your IP address and connections. + No comment provided by engineer. + It seems like you are already connected via this link. If it is not the case, there was an error (%@). このリンクからすでに接続されているようです。そうでない場合は、エラー(%@)が発生しました。 @@ -3267,7 +3641,7 @@ This cannot be undone! Join 参加 - No comment provided by engineer. + swipe action Join group @@ -3305,6 +3679,10 @@ This is your link for group %@! Keep No comment provided by engineer. + + Keep conversation + No comment provided by engineer. + Keep the app open to use it from desktop No comment provided by engineer. @@ -3346,7 +3724,7 @@ This is your link for group %@! Leave 脱退 - No comment provided by engineer. + swipe action Leave group @@ -3475,11 +3853,23 @@ This is your link for group %@! 最大 30 秒で即時受信します。 No comment provided by engineer. + + Media & file servers + No comment provided by engineer. + + + Medium + blur media + Member メンバー No comment provided by engineer. + + Member inactive + item status text + Member role will be changed to "%@". All group members will be notified. メンバーの役割が "%@" に変更されます。 グループメンバー全員に通知されます。 @@ -3495,6 +3885,10 @@ This is your link for group %@! メンバーをグループから除名する (※元に戻せません※)! No comment provided by engineer. + + Menus + No comment provided by engineer. + Message delivery error メッセージ送信エラー @@ -3504,11 +3898,27 @@ This is your link for group %@! Message delivery receipts! No comment provided by engineer. + + Message delivery warning + item status text + Message draft メッセージの下書き No comment provided by engineer. + + Message forwarded + item status text + + + Message may be delivered later if member becomes active. + item status description + + + Message queue info + No comment provided by engineer. + Message reactions メッセージへのリアクション @@ -3524,10 +3934,26 @@ This is your link for group %@! このグループではメッセージへのリアクションは禁止されています。 No comment provided by engineer. + + Message reception + No comment provided by engineer. + + + Message servers + No comment provided by engineer. + Message source remains private. No comment provided by engineer. + + Message status + No comment provided by engineer. + + + Message status: %@ + copied message info + Message text メッセージ内容 @@ -3551,6 +3977,14 @@ This is your link for group %@! Messages from %@ will be shown! No comment provided by engineer. + + Messages received + No comment provided by engineer. + + + Messages sent + No comment provided by engineer. + Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. No comment provided by engineer. @@ -3641,11 +4075,6 @@ This is your link for group %@! おそらく、この接続は削除されています。 item status description - - Most likely this contact has deleted the connection with you. - 恐らくこの連絡先があなたとの接続を削除しました。 - No comment provided by engineer. - Multiple chat profiles 複数チャットのプロフィール @@ -3654,7 +4083,7 @@ This is your link for group %@! Mute ミュート - No comment provided by engineer. + swipe action Muted when inactive! @@ -3664,7 +4093,7 @@ This is your link for group %@! Name 名前 - No comment provided by engineer. + swipe action Network & servers @@ -3675,6 +4104,10 @@ This is your link for group %@! Network connection No comment provided by engineer. + + Network issues - message expired after many attempts to send it. + snd error text + Network management No comment provided by engineer. @@ -3698,6 +4131,10 @@ This is your link for group %@! New chat No comment provided by engineer. + + New chat experience 🎉 + No comment provided by engineer. + New contact request 新しい繋がりのリクエスト @@ -3728,6 +4165,10 @@ This is your link for group %@! %@ の新機能 No comment provided by engineer. + + New media options + No comment provided by engineer. + New member role 新しいメンバーの役割 @@ -3773,6 +4214,10 @@ This is your link for group %@! デバイストークンがありません! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + item status description + No filtered chats フィルタされたチャットはありません @@ -3788,6 +4233,10 @@ This is your link for group %@! 履歴はありません No comment provided by engineer. + + No info, try to reload + No comment provided by engineer. + No network connection No comment provided by engineer. @@ -3806,6 +4255,10 @@ This is your link for group %@! Not compatible! No comment provided by engineer. + + Nothing selected + No comment provided by engineer. + Notifications 通知 @@ -3832,7 +4285,7 @@ This is your link for group %@! Off オフ - No comment provided by engineer. + blur media Ok @@ -3854,14 +4307,18 @@ This is your link for group %@! 使い捨ての招待リンク No comment provided by engineer. - - Onion hosts will be required for connection. Requires enabling VPN. - 接続にオニオンのホストが必要となります。VPN を有効にする必要があります。 + + Onion hosts will be **required** for connection. +Requires compatible VPN. + 接続にオニオンのホストが必要となります。 +VPN を有効にする必要があります。 No comment provided by engineer. - - Onion hosts will be used when available. Requires enabling VPN. - オニオンのホストが利用可能時に使われます。VPN を有効にする必要があります。 + + Onion hosts will be used when available. +Requires compatible VPN. + オニオンのホストが利用可能時に使われます。 +VPN を有効にする必要があります。 No comment provided by engineer. @@ -3874,6 +4331,10 @@ This is your link for group %@! **2 レイヤーのエンドツーエンド暗号化**を使用して送信されたユーザー プロファイル、連絡先、グループ、メッセージを保存できるのはクライアント デバイスのみです。 No comment provided by engineer. + + Only delete conversation + No comment provided by engineer. + Only group owners can change group preferences. グループ設定を変えられるのはグループのオーナーだけです。 @@ -3967,6 +4428,10 @@ This is your link for group %@! Open migration to another device authentication reason + + Open server settings + No comment provided by engineer. + Open user profiles ユーザープロフィールを開く @@ -4001,6 +4466,10 @@ This is your link for group %@! Other No comment provided by engineer. + + Other %@ servers + No comment provided by engineer. + PING count PING回数 @@ -4062,6 +4531,10 @@ This is your link for group %@! Paste the link you received No comment provided by engineer. + + Pending + No comment provided by engineer. + People can connect to you only via the links you share. あなたと繋がることができるのは、あなたからリンクを頂いた方のみです。 @@ -4081,11 +4554,24 @@ This is your link for group %@! Picture-in-picture calls No comment provided by engineer. + + Play from the chat list. + No comment provided by engineer. + + + Please ask your contact to enable calls. + No comment provided by engineer. + Please ask your contact to enable sending voice messages. 音声メッセージを有効にするように連絡相手に要求してください。 No comment provided by engineer. + + Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers. + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. リンクが正しいかどうかご確認ください。または、連絡相手にもう一度リンクをお求めください。 @@ -4180,6 +4666,10 @@ Error: %@ プレビュー No comment provided by engineer. + + Previously connected servers + No comment provided by engineer. + Privacy & security プライバシーとセキュリティ @@ -4195,10 +4685,26 @@ Error: %@ プライベートなファイル名 No comment provided by engineer. + + Private message routing + No comment provided by engineer. + + + Private message routing 🚀 + No comment provided by engineer. + Private notes name of notes to self + + Private routing + No comment provided by engineer. + + + Private routing error + No comment provided by engineer. + Profile and server connections プロフィールとサーバ接続 @@ -4226,6 +4732,10 @@ Error: %@ プロフィールのパスワード No comment provided by engineer. + + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. 連絡先にプロフィール更新のお知らせが届きます。 @@ -4275,11 +4785,20 @@ Error: %@ 音声メッセージを使用禁止にする。 No comment provided by engineer. + + Protect IP address + No comment provided by engineer. + Protect app screen アプリ画面を守る No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! チャットのプロフィールをパスワードで保護します! @@ -4295,6 +4814,14 @@ Error: %@ KB あたりのプロトコル タイムアウト No comment provided by engineer. + + Proxied + No comment provided by engineer. + + + Proxied servers + No comment provided by engineer. + Push notifications プッシュ通知 @@ -4313,6 +4840,10 @@ Error: %@ アプリを評価 No comment provided by engineer. + + Reachable chat toolbar + No comment provided by engineer. + React… 反応する… @@ -4321,7 +4852,7 @@ Error: %@ Read 読む - No comment provided by engineer. + swipe action Read more @@ -4356,6 +4887,10 @@ Error: %@ Receipts are disabled No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at 受信 @@ -4376,15 +4911,23 @@ Error: %@ 受信したメッセージ message info title + + Received messages + No comment provided by engineer. + + + Received reply + No comment provided by engineer. + + + Received total + No comment provided by engineer. + Receiving address will be changed to a different server. Address change will complete after sender comes online. 開発中の機能です!相手のクライアントが4.2でなければ機能しません。アドレス変更が完了すると、会話にメッセージが出ます。連絡相手 (またはグループのメンバー) からメッセージを受信できないかをご確認ください。 No comment provided by engineer. - - Receiving concurrency - No comment provided by engineer. - Receiving file will be stopped. ファイルの受信を停止します。 @@ -4408,11 +4951,31 @@ Error: %@ 受信者には、入力時に更新内容が表示されます。 No comment provided by engineer. + + Reconnect + No comment provided by engineer. + Reconnect all connected servers to force message delivery. It uses additional traffic. 接続されているすべてのサーバーを再接続して、メッセージを強制的に配信します。 追加のトラフィックを使用します。 No comment provided by engineer. + + Reconnect all servers + No comment provided by engineer. + + + Reconnect all servers? + No comment provided by engineer. + + + Reconnect server to force message delivery. It uses additional traffic. + No comment provided by engineer. + + + Reconnect server? + No comment provided by engineer. + Reconnect servers? サーバーに再接続しますか? @@ -4436,7 +4999,8 @@ Error: %@ Reject 拒否 - reject incoming call via notification + reject incoming call via notification + swipe action Reject (sender NOT notified) @@ -4463,6 +5027,10 @@ Error: %@ 削除 No comment provided by engineer. + + Remove image + No comment provided by engineer. + Remove member メンバーを除名する @@ -4528,16 +5096,36 @@ Error: %@ 戻す No comment provided by engineer. + + Reset all hints + No comment provided by engineer. + + + Reset all statistics + No comment provided by engineer. + + + Reset all statistics? + No comment provided by engineer. + Reset colors 既定の色に戻す No comment provided by engineer. + + Reset to app theme + No comment provided by engineer. + Reset to defaults 既定に戻す No comment provided by engineer. + + Reset to user theme + No comment provided by engineer. + Restart the app to create a new chat profile 新しいチャットプロファイルを作成するためにアプリを再起動する @@ -4577,11 +5165,6 @@ Error: %@ 開示する chat item action - - Revert - 元に戻す - No comment provided by engineer. - Revoke 取り消す @@ -4607,9 +5190,12 @@ Error: %@ チャット起動 No comment provided by engineer. - - SMP servers - SMPサーバ + + SMP server + No comment provided by engineer. + + + Safely receive files No comment provided by engineer. @@ -4636,6 +5222,10 @@ Error: %@ 保存して、グループのメンバーにに知らせる No comment provided by engineer. + + Save and reconnect + No comment provided by engineer. + Save and update group profile グループプロファイルの保存と更新 @@ -4713,6 +5303,14 @@ Error: %@ Saved message message info title + + Scale + No comment provided by engineer. + + + Scan / Paste link + No comment provided by engineer. + Scan QR code QRコードを読み込む @@ -4750,11 +5348,19 @@ Error: %@ Search or paste SimpleX link No comment provided by engineer. + + Secondary + No comment provided by engineer. + Secure queue 待ち行列セキュリティ確認 server test step + + Secured + No comment provided by engineer. + Security assessment セキュリティ評価 @@ -4768,6 +5374,14 @@ Error: %@ Select 選択 + chat item action + + + Selected %lld + No comment provided by engineer. + + + Selected chat preferences prohibit this message. No comment provided by engineer. @@ -4804,11 +5418,6 @@ Error: %@ Send delivery receipts to No comment provided by engineer. - - Send direct message - ダイレクトメッセージを送信 - No comment provided by engineer. - Send direct message to connect ダイレクトメッセージを送信して接続する @@ -4819,6 +5428,10 @@ Error: %@ 消えるメッセージを送信 No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews リンクのプレビューを送信 @@ -4829,6 +5442,18 @@ Error: %@ ライブメッセージを送信 No comment provided by engineer. + + Send message to enable calls. + No comment provided by engineer. + + + Send messages directly when IP address is protected and your or destination server does not support private routing. + No comment provided by engineer. + + + Send messages directly when your or destination server does not support private routing. + No comment provided by engineer. + Send notifications 通知を送信する @@ -4911,6 +5536,10 @@ Error: %@ 送信日時: %@ copied message info + + Sent directly + No comment provided by engineer. + Sent file event 送信済みファイルイベント @@ -4921,11 +5550,39 @@ Error: %@ 送信 message info title + + Sent messages + No comment provided by engineer. + Sent messages will be deleted after set time. 一定時間が経ったら送信されたメッセージが削除されます。 No comment provided by engineer. + + Sent reply + No comment provided by engineer. + + + Sent total + No comment provided by engineer. + + + Sent via proxy + No comment provided by engineer. + + + Server address + No comment provided by engineer. + + + Server address is incompatible with network settings. + srv error text. + + + Server address is incompatible with network settings: %@. + No comment provided by engineer. + Server requires authorization to create queues, check password キューを作成するにはサーバーの認証が必要です。パスワードを確認してください @@ -4941,11 +5598,31 @@ Error: %@ サーバテスト失敗! No comment provided by engineer. + + Server type + No comment provided by engineer. + + + Server version is incompatible with network settings. + srv error text + + + Server version is incompatible with your app: %@. + No comment provided by engineer. + Servers サーバ No comment provided by engineer. + + Servers info + No comment provided by engineer. + + + Servers statistics will be reset - this cannot be undone! + No comment provided by engineer. + Session code No comment provided by engineer. @@ -4960,6 +5637,10 @@ Error: %@ 連絡先の名前を設定… No comment provided by engineer. + + Set default theme + No comment provided by engineer. + Set group preferences グループの設定を行う @@ -5023,6 +5704,10 @@ Error: %@ アドレスを連絡先と共有しますか? No comment provided by engineer. + + Share from other apps. + No comment provided by engineer. + Share link リンクを送る @@ -5032,6 +5717,10 @@ Error: %@ Share this 1-time invite link No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts 連絡先と共有する @@ -5056,16 +5745,32 @@ Error: %@ 最新のメッセージを表示 No comment provided by engineer. + + Show message status + No comment provided by engineer. + + + Show percentage + No comment provided by engineer. + Show preview プレビューを表示 No comment provided by engineer. + + Show → on messages sent via private routing. + No comment provided by engineer. + Show: 表示する: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address SimpleXアドレス @@ -5139,6 +5844,10 @@ Error: %@ シークレットモードの簡素化 No comment provided by engineer. + + Size + No comment provided by engineer. + Skip スキップ @@ -5154,11 +5863,23 @@ Error: %@ 小グループ(最大20名) No comment provided by engineer. + + Soft + blur media + + + Some file(s) were not exported: + No comment provided by engineer. + Some non-fatal errors occurred during import - you may see Chat console for more details. インポート中に致命的でないエラーが発生しました - 詳細はチャットコンソールを参照してください。 No comment provided by engineer. + + Some non-fatal errors occurred during import: + No comment provided by engineer. + Somebody 誰か @@ -5182,6 +5903,14 @@ Error: %@ 移行の開始 No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop 停止 @@ -5240,11 +5969,27 @@ Error: %@ Stopping chat No comment provided by engineer. + + Strong + blur media + Submit 送信 No comment provided by engineer. + + Subscribed + No comment provided by engineer. + + + Subscription errors + No comment provided by engineer. + + + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat Simplex Chatを支援 @@ -5260,6 +6005,10 @@ Error: %@ システム認証 No comment provided by engineer. + + TCP connection + No comment provided by engineer. + TCP connection timeout TCP接続タイムアウト @@ -5317,9 +6066,8 @@ Error: %@ Tap to scan No comment provided by engineer. - - Tap to start a new chat - タップして新しいチャットを始める + + Temporary file error No comment provided by engineer. @@ -5374,6 +6122,10 @@ It can happen because of some bug or when the connection is compromised.アプリは、メッセージや連絡先のリクエストを受信したときに通知することができます - 設定を開いて有効にしてください。 No comment provided by engineer. + + The app will ask to confirm downloads from unknown file servers (except .onion). + No comment provided by engineer. + The attempt to change database passphrase was not completed. データベースのパスフレーズ変更が完了してません。 @@ -5418,6 +6170,14 @@ It can happen because of some bug or when the connection is compromised.メッセージは、すべてのメンバーに対してモデレートされたものとして表示されます。 No comment provided by engineer. + + The messages will be deleted for all members. + No comment provided by engineer. + + + The messages will be marked as moderated for all members. + No comment provided by engineer. + The next generation of private messaging 次世代のプライバシー・メッセンジャー @@ -5452,9 +6212,8 @@ It can happen because of some bug or when the connection is compromised.The text you pasted is not a SimpleX link. No comment provided by engineer. - - Theme - テーマ + + Themes No comment provided by engineer. @@ -5515,11 +6274,19 @@ It can happen because of some bug or when the connection is compromised.This is your own one-time link! No comment provided by engineer. + + This link was used with another mobile device, please create a new link on the desktop. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. この設定は現在のチャットプロフィール **%@** のメッセージに適用されます。 No comment provided by engineer. + + Title + No comment provided by engineer. + To ask any questions and to receive updates: 質問や最新情報を受け取るには: @@ -5549,6 +6316,10 @@ It can happen because of some bug or when the connection is compromised.時間帯を漏らさないために、画像と音声ファイルはUTCを使います。 No comment provided by engineer. + + To protect your IP address, private routing uses your SMP servers to deliver messages. + No comment provided by engineer. + To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5576,15 +6347,31 @@ You will be prompted to complete authentication before this feature is enabled.< エンドツーエンド暗号化を確認するには、ご自分の端末と連絡先の端末のコードを比べます (スキャンします)。 No comment provided by engineer. + + Toggle chat list: + No comment provided by engineer. + Toggle incognito when connecting. No comment provided by engineer. + + Toolbar opacity + No comment provided by engineer. + + + Total + No comment provided by engineer. + Transport isolation トランスポート隔離 No comment provided by engineer. + + Transport sessions + No comment provided by engineer. + Trying to connect to the server used to receive messages from this contact (error: %@). この連絡先からのメッセージの受信に使用されるサーバーに接続しようとしています (エラー: %@)。 @@ -5634,11 +6421,6 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock member? No comment provided by engineer. - - Unexpected error: %@ - 予期しないエラー: %@ - item status description - Unexpected migration state 予期しない移行状態 @@ -5647,7 +6429,7 @@ You will be prompted to complete authentication before this feature is enabled.< Unfav. お気に入りを取り消す。 - No comment provided by engineer. + swipe action Unhide @@ -5684,6 +6466,10 @@ You will be prompted to complete authentication before this feature is enabled.< 不明なエラー No comment provided by engineer. + + Unknown servers! + No comment provided by engineer. + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. iOS 通話インターフェイスを使用しない場合は、中断を避けるために「おやすみモード」を有効にしてください。 @@ -5717,12 +6503,12 @@ To connect, please ask your contact to create another connection link and check Unmute ミュート解除 - No comment provided by engineer. + swipe action Unread 未読 - No comment provided by engineer. + swipe action Up to 100 last messages are sent to new members. @@ -5733,11 +6519,6 @@ To connect, please ask your contact to create another connection link and check 更新 No comment provided by engineer. - - Update .onion hosts setting? - .onionのホスト設定を更新しますか? - No comment provided by engineer. - Update database passphrase データベースのパスフレーズを更新 @@ -5748,9 +6529,8 @@ To connect, please ask your contact to create another connection link and check ネットワーク設定を更新しますか? No comment provided by engineer. - - Update transport isolation mode? - トランスポート隔離モードを更新しますか? + + Update settings? No comment provided by engineer. @@ -5758,16 +6538,15 @@ To connect, please ask your contact to create another connection link and check 設定を更新すると、全サーバにクライントの再接続が行われます。 No comment provided by engineer. - - Updating this setting will re-connect the client to all servers. - 設定を更新すると、全サーバにクライントの再接続が行われます。 - No comment provided by engineer. - Upgrade and open chat アップグレードしてチャットを開く No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed No comment provided by engineer. @@ -5777,6 +6556,14 @@ To connect, please ask your contact to create another connection link and check ファイルをアップロードする server test step + + Uploaded + No comment provided by engineer. + + + Uploaded files + No comment provided by engineer. + Uploading archive No comment provided by engineer. @@ -5824,6 +6611,14 @@ To connect, please ask your contact to create another connection link and check Use only local notifications? No comment provided by engineer. + + Use private routing with unknown servers when IP address is not protected. + No comment provided by engineer. + + + Use private routing with unknown servers. + No comment provided by engineer. + Use server サーバを使う @@ -5833,14 +6628,17 @@ To connect, please ask your contact to create another connection link and check Use the app while in the call. No comment provided by engineer. + + Use the app with one hand. + No comment provided by engineer. + User profile ユーザープロフィール No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - .onionホストを使用するには、互換性のあるVPNプロバイダーが必要です。 + + User selection No comment provided by engineer. @@ -5964,6 +6762,14 @@ To connect, please ask your contact to create another connection link and check ビデオ待機中 No comment provided by engineer. + + Wallpaper accent + No comment provided by engineer. + + + Wallpaper background + No comment provided by engineer. + Warning: starting chat on multiple devices is not supported and will cause message delivery failures No comment provided by engineer. @@ -6041,19 +6847,34 @@ To connect, please ask your contact to create another connection link and check With reduced battery usage. No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + No comment provided by engineer. + Wrong database passphrase データベースのパスフレーズが違います No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + file error text + Wrong passphrase! パスフレーズが違います! No comment provided by engineer. - - XFTP servers - XFTPサーバ + + XFTP server No comment provided by engineer. @@ -6124,11 +6945,19 @@ Repeat join request? グループ招待が届きました No comment provided by engineer. + + You are not connected to these servers. Private routing is used to deliver messages to them. + No comment provided by engineer. + You can accept calls from lock screen, without device and app authentication. デバイスやアプリの認証を行わずに、ロック画面から通話を受けることができます。 No comment provided by engineer. + + You can change it in Appearance settings. + No comment provided by engineer. + You can create it later 後からでも作成できます @@ -6157,11 +6986,15 @@ Repeat join request? You can make it visible to your SimpleX contacts via Settings. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ %@ にメッセージを送信できるようになりました notification body + + You can send messages to %@ from Archived contacts. + No comment provided by engineer. + You can set lock screen notification preview via settings. 設定からロック画面の通知プレビューを設定できます。 @@ -6187,6 +7020,10 @@ Repeat join request? アプリの設定/データベースから、またはアプリを再起動することでチャットを開始できます No comment provided by engineer. + + You can still view conversation with %@ in the list of chats. + No comment provided by engineer. + You can turn on SimpleX Lock via Settings. 設定からSimpleXのロックをオンにすることができます。 @@ -6225,11 +7062,6 @@ Repeat join request? Repeat connection request? No comment provided by engineer. - - You have no chats - あなたはチャットがありません - No comment provided by engineer. - You have to enter passphrase every time the app starts - it is not stored on the device. アプリ起動時にパスフレーズを入力しなければなりません。端末に保存されてません。 @@ -6250,11 +7082,23 @@ Repeat connection request? グループに参加しました。招待をくれたメンバーに接続してます。 No comment provided by engineer. + + You may migrate the exported database. + No comment provided by engineer. + + + You may save the exported archive. + No comment provided by engineer. + You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. あなたの最新データベースを1つの端末にしか使わなければ、一部の連絡先からメッセージが届きかねます。 No comment provided by engineer. + + You need to allow your contact to call to be able to call them. + No comment provided by engineer. + You need to allow your contact to send voice messages to be able to send them. 音声メッセージを送るには、連絡相手からの音声メッセージを許可しなければなりません。 @@ -6368,13 +7212,6 @@ Repeat connection request? あなたのチャットプロフィール No comment provided by engineer. - - Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link). - 接続を完了するには、連絡相手がオンラインになる必要があります。 -この接続をキャンセルして、連絡先を削除をすることもできます (後でやり直すこともできます)。 - No comment provided by engineer. - Your contact sent a file that is larger than currently supported maximum size (%@). 連絡先が現在サポートされている最大サイズ (%@) より大きいファイルを送信しました。 @@ -6518,6 +7355,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 and %lld other events No comment provided by engineer. + + attempts + No comment provided by engineer. + audio call (not e2e encrypted) 音声通話 (エンドツーエンド暗号化なし) @@ -6554,6 +7395,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 太文字 No comment provided by engineer. + + call + No comment provided by engineer. + call error 通話エラー @@ -6702,6 +7547,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 time unit + + decryption errors + No comment provided by engineer. + default (%@) デフォルト (%@) @@ -6751,6 +7600,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 重複メッセージ integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted エンドツーエンド暗号化済み @@ -6831,6 +7684,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 イベント発生 No comment provided by engineer. + + expired + No comment provided by engineer. + forwarded No comment provided by engineer. @@ -6860,6 +7717,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 iOS キーチェーンは、アプリを再起動するかパスフレーズを変更した後にパスフレーズを安全に保存するために使用され、プッシュ通知を受信できるようになります。 No comment provided by engineer. + + inactive + No comment provided by engineer. + incognito via contact address link 連絡先リンク経由でシークレットモード @@ -6900,6 +7761,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 グループ %@ への招待 group name + + invite + No comment provided by engineer. + invited 招待済み @@ -6954,6 +7819,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 接続中 rcv group event chat item + + message + No comment provided by engineer. + message received メッセージを受信 @@ -6984,6 +7853,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 time unit + + mute + No comment provided by engineer. + never 一度も @@ -7036,6 +7909,14 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 オン group pref value + + other + No comment provided by engineer. + + + other errors + No comment provided by engineer. + owner オーナー @@ -7100,6 +7981,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 saved from %@ No comment provided by engineer. + + search + No comment provided by engineer. + sec @@ -7124,6 +8009,12 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 send direct message No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + queue info + set new contact address profile update event chat item @@ -7160,10 +8051,22 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 不明 connection info + + unknown servers + No comment provided by engineer. + unknown status No comment provided by engineer. + + unmute + No comment provided by engineer. + + + unprotected + No comment provided by engineer. + updated group profile グループプロフィールを更新しました @@ -7202,6 +8105,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 リレー経由 No comment provided by engineer. + + video + No comment provided by engineer. + video call (not e2e encrypted) ビデオ通話 (非エンドツーエンド暗号化) @@ -7227,6 +8134,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 time unit + + when IP hidden + No comment provided by engineer. + yes はい @@ -7308,7 +8219,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
- +
@@ -7344,7 +8255,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
- +
@@ -7364,4 +8275,178 @@ SimpleX サーバーはあなたのプロファイルを参照できません。
+ +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + No comment provided by engineer. + + + App is locked! + No comment provided by engineer. + + + Cancel + No comment provided by engineer. + + + Cannot access keychain to save database password + No comment provided by engineer. + + + Cannot forward message + No comment provided by engineer. + + + Comment + No comment provided by engineer. + + + Currently maximum supported file size is %@. + No comment provided by engineer. + + + Database downgrade required + No comment provided by engineer. + + + Database encrypted! + No comment provided by engineer. + + + Database error + No comment provided by engineer. + + + Database passphrase is different from saved in the keychain. + No comment provided by engineer. + + + Database passphrase is required to open chat. + No comment provided by engineer. + + + Database upgrade required + No comment provided by engineer. + + + Error preparing file + No comment provided by engineer. + + + Error preparing message + No comment provided by engineer. + + + Error: %@ + No comment provided by engineer. + + + File error + No comment provided by engineer. + + + Incompatible database version + No comment provided by engineer. + + + Invalid migration confirmation + No comment provided by engineer. + + + Keychain error + No comment provided by engineer. + + + Large file! + No comment provided by engineer. + + + No active profile + No comment provided by engineer. + + + Ok + No comment provided by engineer. + + + Open the app to downgrade the database. + No comment provided by engineer. + + + Open the app to upgrade the database. + No comment provided by engineer. + + + Passphrase + No comment provided by engineer. + + + Please create a profile in the SimpleX app + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending a message takes longer than expected. + No comment provided by engineer. + + + Sending message… + No comment provided by engineer. + + + Share + No comment provided by engineer. + + + Slow network? + No comment provided by engineer. + + + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + No comment provided by engineer. + + + Wait + No comment provided by engineer. + + + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/ja.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/ja.xcloc/contents.json b/apps/ios/SimpleX Localizations/ja.xcloc/contents.json index 7d3c224e68..604a21be97 100644 --- a/apps/ios/SimpleX Localizations/ja.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/ja.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "ja", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "15F31d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "15.4" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff b/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff index cc7b5522e6..511536427d 100644 --- a/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff +++ b/apps/ios/SimpleX Localizations/ko.xcloc/Localized Contents/ko.xliff @@ -5,9 +5,11 @@ - + + + No comment provided by engineer. @@ -300,8 +302,8 @@ Add servers by scanning QR codes. No comment provided by engineer. - - Add server… + + Add server No comment provided by engineer. @@ -484,53 +486,62 @@ Can't delete user profile! No comment provided by engineer. - + Can't invite contact! + 주소를 초대할 수 없습니다. No comment provided by engineer. Can't invite contacts! No comment provided by engineer. - + Cancel + 취소 No comment provided by engineer. - + Cannot access keychain to save database password + 데이터베이스 암호를 저장하는 키체인에 접근 할 수 없습니다 No comment provided by engineer. - + Cannot receive file + 파일을 받을 수 없습니다 No comment provided by engineer. - + Change + 변경 No comment provided by engineer. Change database passphrase? No comment provided by engineer. - + Change member role? + 멤버 역할을 변경하시겠습니까? No comment provided by engineer. - + Change receiving address + 수신 주소 변경 No comment provided by engineer. Change receiving address? - 修改接收地址? + 수신 주소를 변경하시겠습니까? No comment provided by engineer. - + Change role + 역할 변경 No comment provided by engineer. - + Chat archive + 채팅 기록 보관함 No comment provided by engineer. @@ -545,8 +556,9 @@ Chat database deleted No comment provided by engineer. - + Chat database imported + 채팅 데이터베이스를 가져옴 No comment provided by engineer. @@ -2397,24 +2409,29 @@ We will be adding server redundancy to prevent lost messages. Send live message No comment provided by engineer. - + Send notifications + 알림 전송 No comment provided by engineer. - + Send notifications: + 알림 전송: No comment provided by engineer. - + Send questions and ideas + 질문이나 아이디어 보내기 No comment provided by engineer. - + Send them from gallery or custom keyboards. + 갤러리 또는 사용자 정의 키보드에서 그들을 보내십시오. No comment provided by engineer. - + Sender cancelled file transfer. + 상대방이 파일 전송을 취소했습니다. No comment provided by engineer. @@ -3755,6 +3772,26 @@ SimpleX servers cannot see your profile. \~strike~ No comment provided by engineer. + + Change passcode + 패스코드 변경 + authentication reason + + + Cellular + 셀룰러 + No comment provided by engineer. + + + Send messages directly when your or destination server does not support private routing. + 이 서버 또는 도착 서버가 비밀 라우팅을 지원하지 않을 때 직통 메시지 보내기. + No comment provided by engineer. + + + Send up to 100 last messages to new members. + 새로운 멤버에게 최대 100개의 마지막 메시지 보내기. + No comment provided by engineer. + @@ -3778,8 +3815,9 @@ SimpleX servers cannot see your profile. SimpleX needs microphone access for audio and video calls, and to record voice messages. Privacy - Microphone Usage Description - + SimpleX needs access to Photo Library for saving captured and received media + SimpleX는 캡처 및 수신 된 미디어를 저장하기 위해 사진 라이브러리에 접근이 필요합니다 Privacy - Photo Library Additions Usage Description @@ -3793,8 +3831,9 @@ SimpleX servers cannot see your profile. SimpleX NSE Bundle display name - + SimpleX NSE + SimpleX NSE Bundle name diff --git a/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff b/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff index feb1e177f1..6df24149e9 100644 --- a/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff +++ b/apps/ios/SimpleX Localizations/lt.xcloc/Localized Contents/lt.xliff @@ -329,9 +329,9 @@ Pridėti serverius skenuojant QR kodus. No comment provided by engineer. - - Add server… - Pridėti serverį… + + Add server + Pridėti serverį No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/ml.xcloc/Localized Contents/ml.xliff b/apps/ios/SimpleX Localizations/ml.xcloc/Localized Contents/ml.xliff deleted file mode 100644 index f4a1a815ea..0000000000 --- a/apps/ios/SimpleX Localizations/ml.xcloc/Localized Contents/ml.xliff +++ /dev/null @@ -1,4624 +0,0 @@ - - - -
- -
- - - - - No comment provided by engineer. - - - - No comment provided by engineer. - - - - No comment provided by engineer. - - - - No comment provided by engineer. - - - ( - No comment provided by engineer. - - - (can be copied) - No comment provided by engineer. - - - !1 colored! - No comment provided by engineer. - - - #secret# - No comment provided by engineer. - - - %@ - No comment provided by engineer. - - - %@ %@ - No comment provided by engineer. - - - %@ (current) - No comment provided by engineer. - - - %@ (current): - copied message info - - - %@ / %@ - No comment provided by engineer. - - - %@ is connected! - notification title - - - %@ is not verified - No comment provided by engineer. - - - %@ is verified - No comment provided by engineer. - - - %@ servers - No comment provided by engineer. - - - %@ wants to connect! - notification title - - - %@: - copied message info - - - %d days - time interval - - - %d hours - time interval - - - %d min - time interval - - - %d months - time interval - - - %d sec - time interval - - - %d skipped message(s) - integrity error chat item - - - %d weeks - time interval - - - %lld - No comment provided by engineer. - - - %lld %@ - No comment provided by engineer. - - - %lld contact(s) selected - No comment provided by engineer. - - - %lld file(s) with total size of %@ - No comment provided by engineer. - - - %lld members - No comment provided by engineer. - - - %lld minutes - No comment provided by engineer. - - - %lld second(s) - No comment provided by engineer. - - - %lld seconds - No comment provided by engineer. - - - %lldd - No comment provided by engineer. - - - %lldh - No comment provided by engineer. - - - %lldk - No comment provided by engineer. - - - %lldm - No comment provided by engineer. - - - %lldmth - No comment provided by engineer. - - - %llds - No comment provided by engineer. - - - %lldw - No comment provided by engineer. - - - %u messages failed to decrypt. - No comment provided by engineer. - - - %u messages skipped. - No comment provided by engineer. - - - ( - No comment provided by engineer. - - - ) - No comment provided by engineer. - - - **Add new contact**: to create your one-time QR Code or link for your contact. - No comment provided by engineer. - - - **Create link / QR code** for your contact to use. - No comment provided by engineer. - - - **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. - No comment provided by engineer. - - - **Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app). - No comment provided by engineer. - - - **Paste received link** or open it in the browser and tap **Open in mobile app**. - No comment provided by engineer. - - - **Please note**: you will NOT be able to recover or change passphrase if you lose it. - No comment provided by engineer. - - - **Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from. - No comment provided by engineer. - - - **Scan QR code**: to connect to your contact in person or via video call. - No comment provided by engineer. - - - **Warning**: Instant push notifications require passphrase saved in Keychain. - No comment provided by engineer. - - - **e2e encrypted** audio call - No comment provided by engineer. - - - **e2e encrypted** video call - No comment provided by engineer. - - - \*bold* - No comment provided by engineer. - - - , - No comment provided by engineer. - - - - voice messages up to 5 minutes. -- custom time to disappear. -- editing history. - No comment provided by engineer. - - - . - No comment provided by engineer. - - - 0s - No comment provided by engineer. - - - 1 day - time interval - - - 1 hour - time interval - - - 1 minute - No comment provided by engineer. - - - 1 month - time interval - - - 1 week - time interval - - - 1-time link - No comment provided by engineer. - - - 5 minutes - No comment provided by engineer. - - - 6 - No comment provided by engineer. - - - 30 seconds - No comment provided by engineer. - - - : - No comment provided by engineer. - - - <p>Hi!</p> -<p><a href="%@">Connect to me via SimpleX Chat</a></p> - email text - - - A new contact - notification title - - - A random profile will be sent to the contact that you received this link from - No comment provided by engineer. - - - A random profile will be sent to your contact - No comment provided by engineer. - - - A separate TCP connection will be used **for each chat profile you have in the app**. - No comment provided by engineer. - - - A separate TCP connection will be used **for each contact and group member**. -**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail. - No comment provided by engineer. - - - About SimpleX - No comment provided by engineer. - - - About SimpleX Chat - No comment provided by engineer. - - - About SimpleX address - No comment provided by engineer. - - - Accent color - No comment provided by engineer. - - - Accept - accept contact request via notification - accept incoming call via notification - - - Accept contact - No comment provided by engineer. - - - Accept contact request from %@? - notification body - - - Accept incognito - No comment provided by engineer. - - - Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. - No comment provided by engineer. - - - Add preset servers - No comment provided by engineer. - - - Add profile - No comment provided by engineer. - - - Add servers by scanning QR codes. - No comment provided by engineer. - - - Add server… - No comment provided by engineer. - - - Add to another device - No comment provided by engineer. - - - Add welcome message - No comment provided by engineer. - - - Address - No comment provided by engineer. - - - Admins can create the links to join groups. - No comment provided by engineer. - - - Advanced network settings - No comment provided by engineer. - - - All app data is deleted. - No comment provided by engineer. - - - All chats and messages will be deleted - this cannot be undone! - No comment provided by engineer. - - - All data is erased when it is entered. - No comment provided by engineer. - - - All group members will remain connected. - No comment provided by engineer. - - - All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you. - No comment provided by engineer. - - - All your contacts will remain connected. - No comment provided by engineer. - - - All your contacts will remain connected. Profile update will be sent to your contacts. - No comment provided by engineer. - - - Allow - No comment provided by engineer. - - - Allow calls only if your contact allows them. - No comment provided by engineer. - - - Allow disappearing messages only if your contact allows it to you. - No comment provided by engineer. - - - Allow irreversible message deletion only if your contact allows it to you. - No comment provided by engineer. - - - Allow message reactions only if your contact allows them. - No comment provided by engineer. - - - Allow message reactions. - No comment provided by engineer. - - - Allow sending direct messages to members. - No comment provided by engineer. - - - Allow sending disappearing messages. - No comment provided by engineer. - - - Allow to irreversibly delete sent messages. - No comment provided by engineer. - - - Allow to send voice messages. - No comment provided by engineer. - - - Allow voice messages only if your contact allows them. - No comment provided by engineer. - - - Allow voice messages? - No comment provided by engineer. - - - Allow your contacts adding message reactions. - No comment provided by engineer. - - - Allow your contacts to call you. - No comment provided by engineer. - - - Allow your contacts to irreversibly delete sent messages. - No comment provided by engineer. - - - Allow your contacts to send disappearing messages. - No comment provided by engineer. - - - Allow your contacts to send voice messages. - No comment provided by engineer. - - - Already connected? - No comment provided by engineer. - - - Always use relay - No comment provided by engineer. - - - An empty chat profile with the provided name is created, and the app opens as usual. - No comment provided by engineer. - - - Answer call - No comment provided by engineer. - - - App build: %@ - No comment provided by engineer. - - - App icon - No comment provided by engineer. - - - App passcode - No comment provided by engineer. - - - App passcode is replaced with self-destruct passcode. - No comment provided by engineer. - - - App version - No comment provided by engineer. - - - App version: v%@ - No comment provided by engineer. - - - Appearance - No comment provided by engineer. - - - Attach - No comment provided by engineer. - - - Audio & video calls - No comment provided by engineer. - - - Audio and video calls - No comment provided by engineer. - - - Audio/video calls - chat feature - - - Audio/video calls are prohibited. - No comment provided by engineer. - - - Authentication cancelled - PIN entry - - - Authentication failed - No comment provided by engineer. - - - Authentication is required before the call is connected, but you may miss calls. - No comment provided by engineer. - - - Authentication unavailable - No comment provided by engineer. - - - Auto-accept - No comment provided by engineer. - - - Auto-accept contact requests - No comment provided by engineer. - - - Auto-accept images - No comment provided by engineer. - - - Back - No comment provided by engineer. - - - Bad message ID - No comment provided by engineer. - - - Bad message hash - No comment provided by engineer. - - - Better messages - No comment provided by engineer. - - - Both you and your contact can add message reactions. - No comment provided by engineer. - - - Both you and your contact can irreversibly delete sent messages. - No comment provided by engineer. - - - Both you and your contact can make calls. - No comment provided by engineer. - - - Both you and your contact can send disappearing messages. - No comment provided by engineer. - - - Both you and your contact can send voice messages. - No comment provided by engineer. - - - By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA). - No comment provided by engineer. - - - Call already ended! - No comment provided by engineer. - - - Calls - No comment provided by engineer. - - - Can't delete user profile! - No comment provided by engineer. - - - Can't invite contact! - No comment provided by engineer. - - - Can't invite contacts! - No comment provided by engineer. - - - Cancel - No comment provided by engineer. - - - Cannot access keychain to save database password - No comment provided by engineer. - - - Cannot receive file - No comment provided by engineer. - - - Change - No comment provided by engineer. - - - Change database passphrase? - No comment provided by engineer. - - - Change lock mode - authentication reason - - - Change member role? - No comment provided by engineer. - - - Change passcode - authentication reason - - - Change receiving address - No comment provided by engineer. - - - Change receiving address? - No comment provided by engineer. - - - Change role - No comment provided by engineer. - - - Change self-destruct mode - authentication reason - - - Change self-destruct passcode - authentication reason - set passcode view - - - Chat archive - No comment provided by engineer. - - - Chat console - No comment provided by engineer. - - - Chat database - No comment provided by engineer. - - - Chat database deleted - No comment provided by engineer. - - - Chat database imported - No comment provided by engineer. - - - Chat is running - No comment provided by engineer. - - - Chat is stopped - No comment provided by engineer. - - - Chat preferences - No comment provided by engineer. - - - Chats - No comment provided by engineer. - - - Check server address and try again. - No comment provided by engineer. - - - Chinese and Spanish interface - No comment provided by engineer. - - - Choose file - No comment provided by engineer. - - - Choose from library - No comment provided by engineer. - - - Clear - No comment provided by engineer. - - - Clear conversation - No comment provided by engineer. - - - Clear conversation? - No comment provided by engineer. - - - Clear verification - No comment provided by engineer. - - - Colors - No comment provided by engineer. - - - Compare file - server test step - - - Compare security codes with your contacts. - No comment provided by engineer. - - - Configure ICE servers - No comment provided by engineer. - - - Confirm - No comment provided by engineer. - - - Confirm Passcode - No comment provided by engineer. - - - Confirm database upgrades - No comment provided by engineer. - - - Confirm new passphrase… - No comment provided by engineer. - - - Confirm password - No comment provided by engineer. - - - Connect - server test step - - - Connect via contact link? - No comment provided by engineer. - - - Connect via group link? - No comment provided by engineer. - - - Connect via link - No comment provided by engineer. - - - Connect via link / QR code - No comment provided by engineer. - - - Connect via one-time link? - No comment provided by engineer. - - - Connecting to server… - No comment provided by engineer. - - - Connecting to server… (error: %@) - No comment provided by engineer. - - - Connection - No comment provided by engineer. - - - Connection error - No comment provided by engineer. - - - Connection error (AUTH) - No comment provided by engineer. - - - Connection request - No comment provided by engineer. - - - Connection request sent! - No comment provided by engineer. - - - Connection timeout - No comment provided by engineer. - - - Contact allows - No comment provided by engineer. - - - Contact already exists - No comment provided by engineer. - - - Contact and all messages will be deleted - this cannot be undone! - No comment provided by engineer. - - - Contact hidden: - notification - - - Contact is connected - notification - - - Contact is not connected yet! - No comment provided by engineer. - - - Contact name - No comment provided by engineer. - - - Contact preferences - No comment provided by engineer. - - - Contacts can mark messages for deletion; you will be able to view them. - No comment provided by engineer. - - - Continue - No comment provided by engineer. - - - Copy - chat item action - - - Core version: v%@ - No comment provided by engineer. - - - Create - No comment provided by engineer. - - - Create SimpleX address - No comment provided by engineer. - - - Create an address to let people connect with you. - No comment provided by engineer. - - - Create file - server test step - - - Create group link - No comment provided by engineer. - - - Create link - No comment provided by engineer. - - - Create one-time invitation link - No comment provided by engineer. - - - Create queue - server test step - - - Create secret group - No comment provided by engineer. - - - Create your profile - No comment provided by engineer. - - - Created on %@ - No comment provided by engineer. - - - Current Passcode - No comment provided by engineer. - - - Current passphrase… - No comment provided by engineer. - - - Currently maximum supported file size is %@. - No comment provided by engineer. - - - Custom time - No comment provided by engineer. - - - Dark - No comment provided by engineer. - - - Database ID - No comment provided by engineer. - - - Database ID: %d - copied message info - - - Database IDs and Transport isolation option. - No comment provided by engineer. - - - Database downgrade - No comment provided by engineer. - - - Database encrypted! - No comment provided by engineer. - - - Database encryption passphrase will be updated and stored in the keychain. - - No comment provided by engineer. - - - Database encryption passphrase will be updated. - - No comment provided by engineer. - - - Database error - No comment provided by engineer. - - - Database is encrypted using a random passphrase, you can change it. - No comment provided by engineer. - - - Database is encrypted using a random passphrase. Please change it before exporting. - No comment provided by engineer. - - - Database passphrase - No comment provided by engineer. - - - Database passphrase & export - No comment provided by engineer. - - - Database passphrase is different from saved in the keychain. - No comment provided by engineer. - - - Database passphrase is required to open chat. - No comment provided by engineer. - - - Database upgrade - No comment provided by engineer. - - - Database will be encrypted and the passphrase stored in the keychain. - - No comment provided by engineer. - - - Database will be encrypted. - - No comment provided by engineer. - - - Database will be migrated when the app restarts - No comment provided by engineer. - - - Decentralized - No comment provided by engineer. - - - Decryption error - No comment provided by engineer. - - - Delete - chat item action - - - Delete Contact - No comment provided by engineer. - - - Delete address - No comment provided by engineer. - - - Delete address? - No comment provided by engineer. - - - Delete after - No comment provided by engineer. - - - Delete all files - No comment provided by engineer. - - - Delete archive - No comment provided by engineer. - - - Delete chat archive? - No comment provided by engineer. - - - Delete chat profile - No comment provided by engineer. - - - Delete chat profile? - No comment provided by engineer. - - - Delete connection - No comment provided by engineer. - - - Delete contact - No comment provided by engineer. - - - Delete contact? - No comment provided by engineer. - - - Delete database - No comment provided by engineer. - - - Delete file - server test step - - - Delete files and media? - No comment provided by engineer. - - - Delete files for all chat profiles - No comment provided by engineer. - - - Delete for everyone - chat feature - - - Delete for me - No comment provided by engineer. - - - Delete group - No comment provided by engineer. - - - Delete group? - No comment provided by engineer. - - - Delete invitation - No comment provided by engineer. - - - Delete link - No comment provided by engineer. - - - Delete link? - No comment provided by engineer. - - - Delete member message? - No comment provided by engineer. - - - Delete message? - No comment provided by engineer. - - - Delete messages - No comment provided by engineer. - - - Delete messages after - No comment provided by engineer. - - - Delete old database - No comment provided by engineer. - - - Delete old database? - No comment provided by engineer. - - - Delete pending connection - No comment provided by engineer. - - - Delete pending connection? - No comment provided by engineer. - - - Delete profile - No comment provided by engineer. - - - Delete queue - server test step - - - Delete user profile? - No comment provided by engineer. - - - Deleted at - No comment provided by engineer. - - - Deleted at: %@ - copied message info - - - Description - No comment provided by engineer. - - - Develop - No comment provided by engineer. - - - Developer tools - No comment provided by engineer. - - - Device - No comment provided by engineer. - - - Device authentication is disabled. Turning off SimpleX Lock. - No comment provided by engineer. - - - Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication. - No comment provided by engineer. - - - Different names, avatars and transport isolation. - No comment provided by engineer. - - - Direct messages - chat feature - - - Direct messages between members are prohibited in this group. - No comment provided by engineer. - - - Disable SimpleX Lock - authentication reason - - - Disappearing message - No comment provided by engineer. - - - Disappearing messages - chat feature - - - Disappearing messages are prohibited in this chat. - No comment provided by engineer. - - - Disappearing messages are prohibited in this group. - No comment provided by engineer. - - - Disappears at - No comment provided by engineer. - - - Disappears at: %@ - copied message info - - - Disconnect - server test step - - - Display name - No comment provided by engineer. - - - Display name: - No comment provided by engineer. - - - Do NOT use SimpleX for emergency calls. - No comment provided by engineer. - - - Do it later - No comment provided by engineer. - - - Don't create address - No comment provided by engineer. - - - Don't show again - No comment provided by engineer. - - - Downgrade and open chat - No comment provided by engineer. - - - Download file - server test step - - - Duplicate display name! - No comment provided by engineer. - - - Duration - No comment provided by engineer. - - - Edit - chat item action - - - Edit group profile - No comment provided by engineer. - - - Enable - No comment provided by engineer. - - - Enable SimpleX Lock - authentication reason - - - Enable TCP keep-alive - No comment provided by engineer. - - - Enable automatic message deletion? - No comment provided by engineer. - - - Enable instant notifications? - No comment provided by engineer. - - - Enable lock - No comment provided by engineer. - - - Enable notifications - No comment provided by engineer. - - - Enable periodic notifications? - No comment provided by engineer. - - - Enable self-destruct - No comment provided by engineer. - - - Enable self-destruct passcode - set passcode view - - - Encrypt - No comment provided by engineer. - - - Encrypt database? - No comment provided by engineer. - - - Encrypted database - No comment provided by engineer. - - - Encrypted message or another event - notification - - - Encrypted message: database error - notification - - - Encrypted message: database migration error - notification - - - Encrypted message: keychain error - notification - - - Encrypted message: no passphrase - notification - - - Encrypted message: unexpected error - notification - - - Enter Passcode - No comment provided by engineer. - - - Enter correct passphrase. - No comment provided by engineer. - - - Enter passphrase… - No comment provided by engineer. - - - Enter password above to show! - No comment provided by engineer. - - - Enter server manually - No comment provided by engineer. - - - Enter welcome message… - placeholder - - - Enter welcome message… (optional) - placeholder - - - Error - No comment provided by engineer. - - - Error accepting contact request - No comment provided by engineer. - - - Error accessing database file - No comment provided by engineer. - - - Error adding member(s) - No comment provided by engineer. - - - Error changing address - No comment provided by engineer. - - - Error changing role - No comment provided by engineer. - - - Error changing setting - No comment provided by engineer. - - - Error creating address - No comment provided by engineer. - - - Error creating group - No comment provided by engineer. - - - Error creating group link - No comment provided by engineer. - - - Error creating profile! - No comment provided by engineer. - - - Error deleting chat database - No comment provided by engineer. - - - Error deleting chat! - No comment provided by engineer. - - - Error deleting connection - No comment provided by engineer. - - - Error deleting contact - No comment provided by engineer. - - - Error deleting database - No comment provided by engineer. - - - Error deleting old database - No comment provided by engineer. - - - Error deleting token - No comment provided by engineer. - - - Error deleting user profile - No comment provided by engineer. - - - Error enabling notifications - No comment provided by engineer. - - - Error encrypting database - No comment provided by engineer. - - - Error exporting chat database - No comment provided by engineer. - - - Error importing chat database - No comment provided by engineer. - - - Error joining group - No comment provided by engineer. - - - Error loading %@ servers - No comment provided by engineer. - - - Error receiving file - No comment provided by engineer. - - - Error removing member - No comment provided by engineer. - - - Error saving %@ servers - No comment provided by engineer. - - - Error saving ICE servers - No comment provided by engineer. - - - Error saving group profile - No comment provided by engineer. - - - Error saving passcode - No comment provided by engineer. - - - Error saving passphrase to keychain - No comment provided by engineer. - - - Error saving user password - No comment provided by engineer. - - - Error sending email - No comment provided by engineer. - - - Error sending message - No comment provided by engineer. - - - Error starting chat - No comment provided by engineer. - - - Error stopping chat - No comment provided by engineer. - - - Error switching profile! - No comment provided by engineer. - - - Error updating group link - No comment provided by engineer. - - - Error updating message - No comment provided by engineer. - - - Error updating settings - No comment provided by engineer. - - - Error updating user privacy - No comment provided by engineer. - - - Error: - No comment provided by engineer. - - - Error: %@ - No comment provided by engineer. - - - Error: URL is invalid - No comment provided by engineer. - - - Error: no database file - No comment provided by engineer. - - - Exit without saving - No comment provided by engineer. - - - Export database - No comment provided by engineer. - - - Export error: - No comment provided by engineer. - - - Exported database archive. - No comment provided by engineer. - - - Exporting database archive... - No comment provided by engineer. - - - Failed to remove passphrase - No comment provided by engineer. - - - Fast and no wait until the sender is online! - No comment provided by engineer. - - - File will be deleted from servers. - No comment provided by engineer. - - - File will be received when your contact completes uploading it. - No comment provided by engineer. - - - File will be received when your contact is online, please wait or check later! - No comment provided by engineer. - - - File: %@ - No comment provided by engineer. - - - Files & media - No comment provided by engineer. - - - Finally, we have them! 🚀 - No comment provided by engineer. - - - For console - No comment provided by engineer. - - - French interface - No comment provided by engineer. - - - Full link - No comment provided by engineer. - - - Full name (optional) - No comment provided by engineer. - - - Full name: - No comment provided by engineer. - - - Fully re-implemented - work in background! - No comment provided by engineer. - - - Further reduced battery usage - No comment provided by engineer. - - - GIFs and stickers - No comment provided by engineer. - - - Group - No comment provided by engineer. - - - Group display name - No comment provided by engineer. - - - Group full name (optional) - No comment provided by engineer. - - - Group image - No comment provided by engineer. - - - Group invitation - No comment provided by engineer. - - - Group invitation expired - No comment provided by engineer. - - - Group invitation is no longer valid, it was removed by sender. - No comment provided by engineer. - - - Group link - No comment provided by engineer. - - - Group links - No comment provided by engineer. - - - Group members can add message reactions. - No comment provided by engineer. - - - Group members can irreversibly delete sent messages. - No comment provided by engineer. - - - Group members can send direct messages. - No comment provided by engineer. - - - Group members can send disappearing messages. - No comment provided by engineer. - - - Group members can send voice messages. - No comment provided by engineer. - - - Group message: - notification - - - Group moderation - No comment provided by engineer. - - - Group preferences - No comment provided by engineer. - - - Group profile - No comment provided by engineer. - - - Group profile is stored on members' devices, not on the servers. - No comment provided by engineer. - - - Group welcome message - No comment provided by engineer. - - - Group will be deleted for all members - this cannot be undone! - No comment provided by engineer. - - - Group will be deleted for you - this cannot be undone! - No comment provided by engineer. - - - Help - No comment provided by engineer. - - - Hidden - No comment provided by engineer. - - - Hidden chat profiles - No comment provided by engineer. - - - Hidden profile password - No comment provided by engineer. - - - Hide - chat item action - - - Hide app screen in the recent apps. - No comment provided by engineer. - - - Hide profile - No comment provided by engineer. - - - Hide: - No comment provided by engineer. - - - History - copied message info - - - How SimpleX works - No comment provided by engineer. - - - How it works - No comment provided by engineer. - - - How to - No comment provided by engineer. - - - How to use it - No comment provided by engineer. - - - How to use your servers - No comment provided by engineer. - - - ICE servers (one per line) - No comment provided by engineer. - - - If you can't meet in person, show QR code in a video call, or share the link. - No comment provided by engineer. - - - If you cannot meet in person, you can **scan QR code in the video call**, or your contact can share an invitation link. - No comment provided by engineer. - - - If you enter this passcode when opening the app, all app data will be irreversibly removed! - No comment provided by engineer. - - - If you enter your self-destruct passcode while opening the app: - No comment provided by engineer. - - - If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app). - No comment provided by engineer. - - - Ignore - No comment provided by engineer. - - - Image will be received when your contact completes uploading it. - No comment provided by engineer. - - - Image will be received when your contact is online, please wait or check later! - No comment provided by engineer. - - - Immediately - No comment provided by engineer. - - - Immune to spam and abuse - No comment provided by engineer. - - - Import - No comment provided by engineer. - - - Import chat database? - No comment provided by engineer. - - - Import database - No comment provided by engineer. - - - Improved privacy and security - No comment provided by engineer. - - - Improved server configuration - No comment provided by engineer. - - - Incognito - No comment provided by engineer. - - - Incognito mode - No comment provided by engineer. - - - Incognito mode is not supported here - your main profile will be sent to group members - No comment provided by engineer. - - - Incognito mode protects the privacy of your main profile name and image — for each new contact a new random profile is created. - No comment provided by engineer. - - - Incoming audio call - notification - - - Incoming call - notification - - - Incoming video call - notification - - - Incompatible database version - No comment provided by engineer. - - - Incorrect passcode - PIN entry - - - Incorrect security code! - No comment provided by engineer. - - - Info - chat item action - - - Initial role - No comment provided by engineer. - - - Install [SimpleX Chat for terminal](https://github.com/simplex-chat/simplex-chat) - No comment provided by engineer. - - - Instant push notifications will be hidden! - - No comment provided by engineer. - - - Instantly - No comment provided by engineer. - - - Interface - No comment provided by engineer. - - - Invalid connection link - No comment provided by engineer. - - - Invalid server address! - No comment provided by engineer. - - - Invitation expired! - No comment provided by engineer. - - - Invite friends - No comment provided by engineer. - - - Invite members - No comment provided by engineer. - - - Invite to group - No comment provided by engineer. - - - Irreversible message deletion - No comment provided by engineer. - - - Irreversible message deletion is prohibited in this chat. - No comment provided by engineer. - - - Irreversible message deletion is prohibited in this group. - No comment provided by engineer. - - - It allows having many anonymous connections without any shared data between them in a single chat profile. - No comment provided by engineer. - - - It can happen when you or your connection used the old database backup. - No comment provided by engineer. - - - It can happen when: -1. The messages expired in the sending client after 2 days or on the server after 30 days. -2. Message decryption failed, because you or your contact used old database backup. -3. The connection was compromised. - No comment provided by engineer. - - - It seems like you are already connected via this link. If it is not the case, there was an error (%@). - No comment provided by engineer. - - - Italian interface - No comment provided by engineer. - - - Japanese interface - No comment provided by engineer. - - - Join - No comment provided by engineer. - - - Join group - No comment provided by engineer. - - - Join incognito - No comment provided by engineer. - - - Joining group - No comment provided by engineer. - - - KeyChain error - No comment provided by engineer. - - - Keychain error - No comment provided by engineer. - - - LIVE - No comment provided by engineer. - - - Large file! - No comment provided by engineer. - - - Learn more - No comment provided by engineer. - - - Leave - No comment provided by engineer. - - - Leave group - No comment provided by engineer. - - - Leave group? - No comment provided by engineer. - - - Let's talk in SimpleX Chat - email subject - - - Light - No comment provided by engineer. - - - Limitations - No comment provided by engineer. - - - Live message! - No comment provided by engineer. - - - Live messages - No comment provided by engineer. - - - Local name - No comment provided by engineer. - - - Local profile data only - No comment provided by engineer. - - - Lock after - No comment provided by engineer. - - - Lock mode - No comment provided by engineer. - - - Make a private connection - No comment provided by engineer. - - - Make profile private! - No comment provided by engineer. - - - Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@). - No comment provided by engineer. - - - Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated. - No comment provided by engineer. - - - Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?* - No comment provided by engineer. - - - Mark deleted for everyone - No comment provided by engineer. - - - Mark read - No comment provided by engineer. - - - Mark verified - No comment provided by engineer. - - - Markdown in messages - No comment provided by engineer. - - - Max 30 seconds, received instantly. - No comment provided by engineer. - - - Member - No comment provided by engineer. - - - Member role will be changed to "%@". All group members will be notified. - No comment provided by engineer. - - - Member role will be changed to "%@". The member will receive a new invitation. - No comment provided by engineer. - - - Member will be removed from group - this cannot be undone! - No comment provided by engineer. - - - Message delivery error - No comment provided by engineer. - - - Message draft - No comment provided by engineer. - - - Message reactions - chat feature - - - Message reactions are prohibited in this chat. - No comment provided by engineer. - - - Message reactions are prohibited in this group. - No comment provided by engineer. - - - Message text - No comment provided by engineer. - - - Messages - No comment provided by engineer. - - - Messages & files - No comment provided by engineer. - - - Migrating database archive... - No comment provided by engineer. - - - Migration error: - No comment provided by engineer. - - - Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat). - No comment provided by engineer. - - - Migration is completed - No comment provided by engineer. - - - Migrations: %@ - No comment provided by engineer. - - - Moderate - chat item action - - - Moderated at - No comment provided by engineer. - - - Moderated at: %@ - copied message info - - - More improvements are coming soon! - No comment provided by engineer. - - - Most likely this contact has deleted the connection with you. - No comment provided by engineer. - - - Multiple chat profiles - No comment provided by engineer. - - - Mute - No comment provided by engineer. - - - Muted when inactive! - No comment provided by engineer. - - - Name - No comment provided by engineer. - - - Network & servers - No comment provided by engineer. - - - Network settings - No comment provided by engineer. - - - Network status - No comment provided by engineer. - - - New Passcode - No comment provided by engineer. - - - New contact request - notification - - - New contact: - notification - - - New database archive - No comment provided by engineer. - - - New display name - No comment provided by engineer. - - - New in %@ - No comment provided by engineer. - - - New member role - No comment provided by engineer. - - - New message - notification - - - New passphrase… - No comment provided by engineer. - - - No - No comment provided by engineer. - - - No app password - Authentication unavailable - - - No contacts selected - No comment provided by engineer. - - - No contacts to add - No comment provided by engineer. - - - No device token! - No comment provided by engineer. - - - Group not found! - No comment provided by engineer. - - - No permission to record voice message - No comment provided by engineer. - - - No received or sent files - No comment provided by engineer. - - - Notifications - No comment provided by engineer. - - - Notifications are disabled! - No comment provided by engineer. - - - Now admins can: -- delete members' messages. -- disable members ("observer" role) - No comment provided by engineer. - - - Off - No comment provided by engineer. - - - Off (Local) - No comment provided by engineer. - - - Ok - No comment provided by engineer. - - - Old database - No comment provided by engineer. - - - Old database archive - No comment provided by engineer. - - - One-time invitation link - No comment provided by engineer. - - - Onion hosts will be required for connection. Requires enabling VPN. - No comment provided by engineer. - - - Onion hosts will be used when available. Requires enabling VPN. - No comment provided by engineer. - - - Onion hosts will not be used. - No comment provided by engineer. - - - Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**. - No comment provided by engineer. - - - Only group owners can change group preferences. - No comment provided by engineer. - - - Only group owners can enable voice messages. - No comment provided by engineer. - - - Only you can add message reactions. - No comment provided by engineer. - - - Only you can irreversibly delete messages (your contact can mark them for deletion). - No comment provided by engineer. - - - Only you can make calls. - No comment provided by engineer. - - - Only you can send disappearing messages. - No comment provided by engineer. - - - Only you can send voice messages. - No comment provided by engineer. - - - Only your contact can add message reactions. - No comment provided by engineer. - - - Only your contact can irreversibly delete messages (you can mark them for deletion). - No comment provided by engineer. - - - Only your contact can make calls. - No comment provided by engineer. - - - Only your contact can send disappearing messages. - No comment provided by engineer. - - - Only your contact can send voice messages. - No comment provided by engineer. - - - Open Settings - No comment provided by engineer. - - - Open chat - No comment provided by engineer. - - - Open chat console - authentication reason - - - Open user profiles - authentication reason - - - Open-source protocol and code – anybody can run the servers. - No comment provided by engineer. - - - Opening database… - No comment provided by engineer. - - - Opening the link in the browser may reduce connection privacy and security. Untrusted SimpleX links will be red. - No comment provided by engineer. - - - PING count - No comment provided by engineer. - - - PING interval - No comment provided by engineer. - - - Passcode - No comment provided by engineer. - - - Passcode changed! - No comment provided by engineer. - - - Passcode entry - No comment provided by engineer. - - - Passcode not changed! - No comment provided by engineer. - - - Passcode set! - No comment provided by engineer. - - - Password to show - No comment provided by engineer. - - - Paste - No comment provided by engineer. - - - Paste image - No comment provided by engineer. - - - Paste received link - No comment provided by engineer. - - - Paste the link you received into the box below to connect with your contact. - No comment provided by engineer. - - - People can connect to you only via the links you share. - No comment provided by engineer. - - - Periodically - No comment provided by engineer. - - - Permanent decryption error - message decrypt error item - - - Please ask your contact to enable sending voice messages. - No comment provided by engineer. - - - Please check that you used the correct link or ask your contact to send you another one. - No comment provided by engineer. - - - Please check your network connection with %@ and try again. - No comment provided by engineer. - - - Please check yours and your contact preferences. - No comment provided by engineer. - - - Please contact group admin. - No comment provided by engineer. - - - Please enter correct current passphrase. - No comment provided by engineer. - - - Please enter the previous password after restoring database backup. This action can not be undone. - No comment provided by engineer. - - - Please remember or store it securely - there is no way to recover a lost passcode! - No comment provided by engineer. - - - Please report it to the developers. - No comment provided by engineer. - - - Please restart the app and migrate the database to enable push notifications. - No comment provided by engineer. - - - Please store passphrase securely, you will NOT be able to access chat if you lose it. - No comment provided by engineer. - - - Please store passphrase securely, you will NOT be able to change it if you lose it. - No comment provided by engineer. - - - Polish interface - No comment provided by engineer. - - - Possibly, certificate fingerprint in server address is incorrect - server test error - - - Preserve the last message draft, with attachments. - No comment provided by engineer. - - - Preset server - No comment provided by engineer. - - - Preset server address - No comment provided by engineer. - - - Preview - No comment provided by engineer. - - - Privacy & security - No comment provided by engineer. - - - Privacy redefined - No comment provided by engineer. - - - Private filenames - No comment provided by engineer. - - - Profile and server connections - No comment provided by engineer. - - - Profile image - No comment provided by engineer. - - - Profile password - No comment provided by engineer. - - - Profile update will be sent to your contacts. - No comment provided by engineer. - - - Prohibit audio/video calls. - No comment provided by engineer. - - - Prohibit irreversible message deletion. - No comment provided by engineer. - - - Prohibit message reactions. - No comment provided by engineer. - - - Prohibit messages reactions. - No comment provided by engineer. - - - Prohibit sending direct messages to members. - No comment provided by engineer. - - - Prohibit sending disappearing messages. - No comment provided by engineer. - - - Prohibit sending voice messages. - No comment provided by engineer. - - - Protect app screen - No comment provided by engineer. - - - Protect your chat profiles with a password! - No comment provided by engineer. - - - Protocol timeout - No comment provided by engineer. - - - Push notifications - No comment provided by engineer. - - - Rate the app - No comment provided by engineer. - - - React... - chat item menu - - - Read - No comment provided by engineer. - - - Read more - No comment provided by engineer. - - - Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address). - No comment provided by engineer. - - - Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends). - No comment provided by engineer. - - - Read more in our GitHub repository. - No comment provided by engineer. - - - Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme). - No comment provided by engineer. - - - Received at - No comment provided by engineer. - - - Received at: %@ - copied message info - - - Received file event - notification - - - Received message - message info title - - - Receiving file will be stopped. - No comment provided by engineer. - - - Receiving via - No comment provided by engineer. - - - Recipients see updates as you type them. - No comment provided by engineer. - - - Record updated at - No comment provided by engineer. - - - Record updated at: %@ - copied message info - - - Reduced battery usage - No comment provided by engineer. - - - Reject - reject incoming call via notification - - - Reject contact (sender NOT notified) - No comment provided by engineer. - - - Reject contact request - No comment provided by engineer. - - - Relay server is only used if necessary. Another party can observe your IP address. - No comment provided by engineer. - - - Relay server protects your IP address, but it can observe the duration of the call. - No comment provided by engineer. - - - Remove - No comment provided by engineer. - - - Remove member - No comment provided by engineer. - - - Remove member? - No comment provided by engineer. - - - Remove passphrase from keychain? - No comment provided by engineer. - - - Reply - chat item action - - - Required - No comment provided by engineer. - - - Reset - No comment provided by engineer. - - - Reset colors - No comment provided by engineer. - - - Reset to defaults - No comment provided by engineer. - - - Restart the app to create a new chat profile - No comment provided by engineer. - - - Restart the app to use imported chat database - No comment provided by engineer. - - - Restore - No comment provided by engineer. - - - Restore database backup - No comment provided by engineer. - - - Restore database backup? - No comment provided by engineer. - - - Restore database error - No comment provided by engineer. - - - Reveal - chat item action - - - Revert - No comment provided by engineer. - - - Revoke - No comment provided by engineer. - - - Revoke file - cancel file action - - - Revoke file? - No comment provided by engineer. - - - Role - No comment provided by engineer. - - - Run chat - No comment provided by engineer. - - - SMP servers - No comment provided by engineer. - - - Save - chat item action - - - Save (and notify contacts) - No comment provided by engineer. - - - Save and notify contact - No comment provided by engineer. - - - Save and notify group members - No comment provided by engineer. - - - Save and update group profile - No comment provided by engineer. - - - Save archive - No comment provided by engineer. - - - Save auto-accept settings - No comment provided by engineer. - - - Save group profile - No comment provided by engineer. - - - Save passphrase and open chat - No comment provided by engineer. - - - Save passphrase in Keychain - No comment provided by engineer. - - - Save preferences? - No comment provided by engineer. - - - Save profile password - No comment provided by engineer. - - - Save servers - No comment provided by engineer. - - - Save servers? - No comment provided by engineer. - - - Save settings? - No comment provided by engineer. - - - Save welcome message? - No comment provided by engineer. - - - Saved WebRTC ICE servers will be removed - No comment provided by engineer. - - - Scan QR code - No comment provided by engineer. - - - Scan code - No comment provided by engineer. - - - Scan security code from your contact's app. - No comment provided by engineer. - - - Scan server QR code - No comment provided by engineer. - - - Search - No comment provided by engineer. - - - Secure queue - server test step - - - Security assessment - No comment provided by engineer. - - - Security code - No comment provided by engineer. - - - Select - No comment provided by engineer. - - - Self-destruct - No comment provided by engineer. - - - Self-destruct passcode - No comment provided by engineer. - - - Self-destruct passcode changed! - No comment provided by engineer. - - - Self-destruct passcode enabled! - No comment provided by engineer. - - - Send - No comment provided by engineer. - - - Send a live message - it will update for the recipient(s) as you type it - No comment provided by engineer. - - - Send direct message - No comment provided by engineer. - - - Send disappearing message - No comment provided by engineer. - - - Send link previews - No comment provided by engineer. - - - Send live message - No comment provided by engineer. - - - Send notifications - No comment provided by engineer. - - - Send notifications: - No comment provided by engineer. - - - Send questions and ideas - No comment provided by engineer. - - - Send them from gallery or custom keyboards. - No comment provided by engineer. - - - Sender cancelled file transfer. - No comment provided by engineer. - - - Sender may have deleted the connection request. - No comment provided by engineer. - - - Sending file will be stopped. - No comment provided by engineer. - - - Sending via - No comment provided by engineer. - - - Sent at - No comment provided by engineer. - - - Sent at: %@ - copied message info - - - Sent file event - notification - - - Sent message - message info title - - - Sent messages will be deleted after set time. - No comment provided by engineer. - - - Server requires authorization to create queues, check password - server test error - - - Server requires authorization to upload, check password - server test error - - - Server test failed! - No comment provided by engineer. - - - Servers - No comment provided by engineer. - - - Set 1 day - No comment provided by engineer. - - - Set contact name… - No comment provided by engineer. - - - Set group preferences - No comment provided by engineer. - - - Set it instead of system authentication. - No comment provided by engineer. - - - Set passcode - No comment provided by engineer. - - - Set passphrase to export - No comment provided by engineer. - - - Set the message shown to new members! - No comment provided by engineer. - - - Set timeouts for proxy/VPN - No comment provided by engineer. - - - Settings - No comment provided by engineer. - - - Share - chat item action - - - Share 1-time link - No comment provided by engineer. - - - Share address - No comment provided by engineer. - - - Share address with contacts? - No comment provided by engineer. - - - Share link - No comment provided by engineer. - - - Share one-time invitation link - No comment provided by engineer. - - - Share with contacts - No comment provided by engineer. - - - Show calls in phone history - No comment provided by engineer. - - - Show developer options - No comment provided by engineer. - - - Show preview - No comment provided by engineer. - - - Show: - No comment provided by engineer. - - - SimpleX Address - No comment provided by engineer. - - - SimpleX Chat security was audited by Trail of Bits. - No comment provided by engineer. - - - SimpleX Lock - No comment provided by engineer. - - - SimpleX Lock mode - No comment provided by engineer. - - - SimpleX Lock not enabled! - No comment provided by engineer. - - - SimpleX Lock turned on - No comment provided by engineer. - - - SimpleX address - No comment provided by engineer. - - - SimpleX contact address - simplex link type - - - SimpleX encrypted message or connection event - notification - - - SimpleX group link - simplex link type - - - SimpleX links - No comment provided by engineer. - - - SimpleX one-time invitation - simplex link type - - - Skip - No comment provided by engineer. - - - Skipped messages - No comment provided by engineer. - - - Some non-fatal errors occurred during import - you may see Chat console for more details. - No comment provided by engineer. - - - Somebody - notification title - - - Start a new chat - No comment provided by engineer. - - - Start chat - No comment provided by engineer. - - - Start migration - No comment provided by engineer. - - - Stop - No comment provided by engineer. - - - Stop SimpleX - authentication reason - - - Stop chat to enable database actions - No comment provided by engineer. - - - Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped. - No comment provided by engineer. - - - Stop chat? - No comment provided by engineer. - - - Stop file - cancel file action - - - Stop receiving file? - No comment provided by engineer. - - - Stop sending file? - No comment provided by engineer. - - - Stop sharing - No comment provided by engineer. - - - Stop sharing address? - No comment provided by engineer. - - - Submit - No comment provided by engineer. - - - Support SimpleX Chat - No comment provided by engineer. - - - System - No comment provided by engineer. - - - System authentication - No comment provided by engineer. - - - TCP connection timeout - No comment provided by engineer. - - - TCP_KEEPCNT - No comment provided by engineer. - - - TCP_KEEPIDLE - No comment provided by engineer. - - - TCP_KEEPINTVL - No comment provided by engineer. - - - Take picture - No comment provided by engineer. - - - Tap button - No comment provided by engineer. - - - Tap to activate profile. - No comment provided by engineer. - - - Tap to join - No comment provided by engineer. - - - Tap to join incognito - No comment provided by engineer. - - - Tap to start a new chat - No comment provided by engineer. - - - Test failed at step %@. - server test failure - - - Test server - No comment provided by engineer. - - - Test servers - No comment provided by engineer. - - - Tests failed! - No comment provided by engineer. - - - Thank you for installing SimpleX Chat! - No comment provided by engineer. - - - Thanks to the users – [contribute via Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! - No comment provided by engineer. - - - Thanks to the users – contribute via Weblate! - No comment provided by engineer. - - - The 1st platform without any user identifiers – private by design. - No comment provided by engineer. - - - The ID of the next message is incorrect (less or equal to the previous). -It can happen because of some bug or when the connection is compromised. - No comment provided by engineer. - - - The app can notify you when you receive messages or contact requests - please open settings to enable. - No comment provided by engineer. - - - The attempt to change database passphrase was not completed. - No comment provided by engineer. - - - The connection you accepted will be cancelled! - No comment provided by engineer. - - - The contact you shared this link with will NOT be able to connect! - No comment provided by engineer. - - - The created archive is available via app Settings / Database / Old database archive. - No comment provided by engineer. - - - The group is fully decentralized – it is visible only to the members. - No comment provided by engineer. - - - The hash of the previous message is different. - No comment provided by engineer. - - - The message will be deleted for all members. - No comment provided by engineer. - - - The message will be marked as moderated for all members. - No comment provided by engineer. - - - The next generation of private messaging - No comment provided by engineer. - - - The old database was not removed during the migration, it can be deleted. - No comment provided by engineer. - - - The profile is only shared with your contacts. - No comment provided by engineer. - - - The sender will NOT be notified - No comment provided by engineer. - - - The servers for new connections of your current chat profile **%@**. - No comment provided by engineer. - - - Theme - No comment provided by engineer. - - - There should be at least one user profile. - No comment provided by engineer. - - - There should be at least one visible user profile. - No comment provided by engineer. - - - This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain. - No comment provided by engineer. - - - This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes. - No comment provided by engineer. - - - This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. - No comment provided by engineer. - - - This error is permanent for this connection, please re-connect. - No comment provided by engineer. - - - This feature is experimental! It will only work if the other client has version 4.2 installed. You should see the message in the conversation once the address change is completed – please check that you can still receive messages from this contact (or group member). - No comment provided by engineer. - - - This group no longer exists. - No comment provided by engineer. - - - This setting applies to messages in your current chat profile **%@**. - No comment provided by engineer. - - - To ask any questions and to receive updates: - No comment provided by engineer. - - - To connect, your contact can scan QR code or use the link in the app. - No comment provided by engineer. - - - To find the profile used for an incognito connection, tap the contact or group name on top of the chat. - No comment provided by engineer. - - - To make a new connection - No comment provided by engineer. - - - To protect privacy, instead of user IDs used by all other platforms, SimpleX has identifiers for message queues, separate for each of your contacts. - No comment provided by engineer. - - - To protect timezone, image/voice files use UTC. - No comment provided by engineer. - - - To protect your information, turn on SimpleX Lock. -You will be prompted to complete authentication before this feature is enabled. - No comment provided by engineer. - - - To record voice message please grant permission to use Microphone. - No comment provided by engineer. - - - To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page. - No comment provided by engineer. - - - To support instant push notifications the chat database has to be migrated. - No comment provided by engineer. - - - To verify end-to-end encryption with your contact compare (or scan) the code on your devices. - No comment provided by engineer. - - - Transport isolation - No comment provided by engineer. - - - Trying to connect to the server used to receive messages from this contact (error: %@). - No comment provided by engineer. - - - Trying to connect to the server used to receive messages from this contact. - No comment provided by engineer. - - - Turn off - No comment provided by engineer. - - - Turn off notifications? - No comment provided by engineer. - - - Turn on - No comment provided by engineer. - - - Unable to record voice message - No comment provided by engineer. - - - Unexpected error: %@ - No comment provided by engineer. - - - Unexpected migration state - No comment provided by engineer. - - - Unhide - No comment provided by engineer. - - - Unhide chat profile - No comment provided by engineer. - - - Unhide profile - No comment provided by engineer. - - - Unit - No comment provided by engineer. - - - Unknown caller - callkit banner - - - Unknown database error: %@ - No comment provided by engineer. - - - Unknown error - No comment provided by engineer. - - - Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. - No comment provided by engineer. - - - Unless your contact deleted the connection or this link was already used, it might be a bug - please report it. -To connect, please ask your contact to create another connection link and check that you have a stable network connection. - No comment provided by engineer. - - - Unlock - No comment provided by engineer. - - - Unlock app - authentication reason - - - Unmute - No comment provided by engineer. - - - Unread - No comment provided by engineer. - - - Update - No comment provided by engineer. - - - Update .onion hosts setting? - No comment provided by engineer. - - - Update database passphrase - No comment provided by engineer. - - - Update network settings? - No comment provided by engineer. - - - Update transport isolation mode? - No comment provided by engineer. - - - Updating settings will re-connect the client to all servers. - No comment provided by engineer. - - - Updating this setting will re-connect the client to all servers. - No comment provided by engineer. - - - Upgrade and open chat - No comment provided by engineer. - - - Upload file - server test step - - - Use .onion hosts - No comment provided by engineer. - - - Use SimpleX Chat servers? - No comment provided by engineer. - - - Use chat - No comment provided by engineer. - - - Use for new connections - No comment provided by engineer. - - - Use iOS call interface - No comment provided by engineer. - - - Use server - No comment provided by engineer. - - - User profile - No comment provided by engineer. - - - Using .onion hosts requires compatible VPN provider. - No comment provided by engineer. - - - Using SimpleX Chat servers. - No comment provided by engineer. - - - Verify connection security - No comment provided by engineer. - - - Verify security code - No comment provided by engineer. - - - Via browser - No comment provided by engineer. - - - Video call - No comment provided by engineer. - - - Video will be received when your contact completes uploading it. - No comment provided by engineer. - - - Video will be received when your contact is online, please wait or check later! - No comment provided by engineer. - - - Videos and files up to 1gb - No comment provided by engineer. - - - View security code - No comment provided by engineer. - - - Voice messages - chat feature - - - Voice messages are prohibited in this chat. - No comment provided by engineer. - - - Voice messages are prohibited in this group. - No comment provided by engineer. - - - Voice messages prohibited! - No comment provided by engineer. - - - Voice message… - No comment provided by engineer. - - - Waiting for file - No comment provided by engineer. - - - Waiting for image - No comment provided by engineer. - - - Waiting for video - No comment provided by engineer. - - - Warning: you may lose some data! - No comment provided by engineer. - - - WebRTC ICE servers - No comment provided by engineer. - - - Welcome %@! - No comment provided by engineer. - - - Welcome message - No comment provided by engineer. - - - What's new - No comment provided by engineer. - - - When available - No comment provided by engineer. - - - When people request to connect, you can accept or reject it. - No comment provided by engineer. - - - When you share an incognito profile with somebody, this profile will be used for the groups they invite you to. - No comment provided by engineer. - - - With optional welcome message. - No comment provided by engineer. - - - Wrong database passphrase - No comment provided by engineer. - - - Wrong passphrase! - No comment provided by engineer. - - - XFTP servers - No comment provided by engineer. - - - You - No comment provided by engineer. - - - You accepted connection - No comment provided by engineer. - - - You allow - No comment provided by engineer. - - - You already have a chat profile with the same display name. Please choose another name. - No comment provided by engineer. - - - You are already connected to %@. - No comment provided by engineer. - - - You are connected to the server used to receive messages from this contact. - No comment provided by engineer. - - - You are invited to group - No comment provided by engineer. - - - You can accept calls from lock screen, without device and app authentication. - No comment provided by engineer. - - - You can also connect by clicking the link. If it opens in the browser, click **Open in mobile app** button. - No comment provided by engineer. - - - You can create it later - No comment provided by engineer. - - - You can hide or mute a user profile - swipe it to the right. - No comment provided by engineer. - - - You can now send messages to %@ - notification body - - - You can set lock screen notification preview via settings. - No comment provided by engineer. - - - You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it. - No comment provided by engineer. - - - You can share this address with your contacts to let them connect with **%@**. - No comment provided by engineer. - - - You can share your address as a link or QR code - anybody can connect to you. - No comment provided by engineer. - - - You can start chat via app Settings / Database or by restarting the app - No comment provided by engineer. - - - You can turn on SimpleX Lock via Settings. - No comment provided by engineer. - - - You can use markdown to format messages: - No comment provided by engineer. - - - You can't send messages! - No comment provided by engineer. - - - You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them. - No comment provided by engineer. - - - You could not be verified; please try again. - No comment provided by engineer. - - - You have no chats - No comment provided by engineer. - - - You have to enter passphrase every time the app starts - it is not stored on the device. - No comment provided by engineer. - - - You invited your contact - No comment provided by engineer. - - - You joined this group - No comment provided by engineer. - - - You joined this group. Connecting to inviting group member. - No comment provided by engineer. - - - You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. - No comment provided by engineer. - - - You need to allow your contact to send voice messages to be able to send them. - No comment provided by engineer. - - - You rejected group invitation - No comment provided by engineer. - - - You sent group invitation - No comment provided by engineer. - - - You will be connected to group when the group host's device is online, please wait or check later! - No comment provided by engineer. - - - You will be connected when your connection request is accepted, please wait or check later! - No comment provided by engineer. - - - You will be connected when your contact's device is online, please wait or check later! - No comment provided by engineer. - - - You will be required to authenticate when you start or resume the app after 30 seconds in background. - No comment provided by engineer. - - - You will join a group this link refers to and connect to its group members. - No comment provided by engineer. - - - You will still receive calls and notifications from muted profiles when they are active. - No comment provided by engineer. - - - You will stop receiving messages from this group. Chat history will be preserved. - No comment provided by engineer. - - - You won't lose your contacts if you later delete your address. - No comment provided by engineer. - - - You're trying to invite contact with whom you've shared an incognito profile to the group in which you're using your main profile - No comment provided by engineer. - - - You're using an incognito profile for this group - to prevent sharing your main profile inviting contacts is not allowed - No comment provided by engineer. - - - Your %@ servers - No comment provided by engineer. - - - Your ICE servers - No comment provided by engineer. - - - Your SMP servers - No comment provided by engineer. - - - Your SimpleX address - No comment provided by engineer. - - - Your XFTP servers - No comment provided by engineer. - - - Your calls - No comment provided by engineer. - - - Your chat database - No comment provided by engineer. - - - Your chat database is not encrypted - set passphrase to encrypt it. - No comment provided by engineer. - - - Your chat profile will be sent to group members - No comment provided by engineer. - - - Your chat profile will be sent to your contact - No comment provided by engineer. - - - Your chat profiles - No comment provided by engineer. - - - Your chats - No comment provided by engineer. - - - Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link). - No comment provided by engineer. - - - Your contact sent a file that is larger than currently supported maximum size (%@). - No comment provided by engineer. - - - Your contacts can allow full message deletion. - No comment provided by engineer. - - - Your contacts in SimpleX will see it. -You can change it in Settings. - No comment provided by engineer. - - - Your contacts will remain connected. - No comment provided by engineer. - - - Your current chat database will be DELETED and REPLACED with the imported one. - No comment provided by engineer. - - - Your current profile - No comment provided by engineer. - - - Your preferences - No comment provided by engineer. - - - Your privacy - No comment provided by engineer. - - - Your profile is stored on your device and shared only with your contacts. -SimpleX servers cannot see your profile. - No comment provided by engineer. - - - Your profile will be sent to the contact that you received this link from - No comment provided by engineer. - - - Your profile, contacts and delivered messages are stored on your device. - No comment provided by engineer. - - - Your random profile - No comment provided by engineer. - - - Your server - No comment provided by engineer. - - - Your server address - No comment provided by engineer. - - - Your settings - No comment provided by engineer. - - - [Contribute](https://github.com/simplex-chat/simplex-chat#contribute) - No comment provided by engineer. - - - [Send us email](mailto:chat@simplex.chat) - No comment provided by engineer. - - - [Star on GitHub](https://github.com/simplex-chat/simplex-chat) - No comment provided by engineer. - - - \_italic_ - No comment provided by engineer. - - - \`a + b` - No comment provided by engineer. - - - above, then choose: - No comment provided by engineer. - - - accepted call - call status - - - admin - member role - - - always - pref value - - - audio call (not e2e encrypted) - No comment provided by engineer. - - - bad message ID - integrity error chat item - - - bad message hash - integrity error chat item - - - bold - No comment provided by engineer. - - - call error - call status - - - call in progress - call status - - - calling… - call status - - - cancelled %@ - feature offered item - - - changed address for you - chat item text - - - changed role of %1$@ to %2$@ - rcv group event chat item - - - changed your role to %@ - rcv group event chat item - - - changing address for %@... - chat item text - - - changing address... - chat item text - - - colored - No comment provided by engineer. - - - complete - No comment provided by engineer. - - - connect to SimpleX Chat developers. - No comment provided by engineer. - - - connected - No comment provided by engineer. - - - connecting - No comment provided by engineer. - - - connecting (accepted) - No comment provided by engineer. - - - connecting (announced) - No comment provided by engineer. - - - connecting (introduced) - No comment provided by engineer. - - - connecting (introduction invitation) - No comment provided by engineer. - - - connecting call… - call status - - - connecting… - chat list item title - - - connection established - chat list item title (it should not be shown - - - connection:%@ - connection information - - - contact has e2e encryption - No comment provided by engineer. - - - contact has no e2e encryption - No comment provided by engineer. - - - creator - No comment provided by engineer. - - - custom - dropdown time picker choice - - - database version is newer than the app, but no down migration for: %@ - No comment provided by engineer. - - - days - time unit - - - default (%@) - pref value - - - deleted - deleted chat item - - - deleted group - rcv group event chat item - - - different migration in the app/database: %@ / %@ - No comment provided by engineer. - - - direct - connection level description - - - duplicate message - integrity error chat item - - - e2e encrypted - No comment provided by engineer. - - - enabled - enabled status - - - enabled for contact - enabled status - - - enabled for you - enabled status - - - ended - No comment provided by engineer. - - - ended call %@ - call status - - - error - No comment provided by engineer. - - - group deleted - No comment provided by engineer. - - - group profile updated - snd group event chat item - - - hours - time unit - - - iOS Keychain is used to securely store passphrase - it allows receiving push notifications. - No comment provided by engineer. - - - iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications. - No comment provided by engineer. - - - incognito via contact address link - chat list item description - - - incognito via group link - chat list item description - - - incognito via one-time link - chat list item description - - - indirect (%d) - connection level description - - - invalid chat - invalid chat data - - - invalid chat data - No comment provided by engineer. - - - invalid data - invalid chat item - - - invitation to group %@ - group name - - - invited - No comment provided by engineer. - - - invited %@ - rcv group event chat item - - - invited to connect - chat list item title - - - invited via your group link - rcv group event chat item - - - italic - No comment provided by engineer. - - - join as %@ - No comment provided by engineer. - - - left - rcv group event chat item - - - marked deleted - marked deleted chat item preview text - - - member - member role - - - connected - rcv group event chat item - - - message received - notification - - - minutes - time unit - - - missed call - call status - - - moderated - moderated chat item - - - moderated by %@ - No comment provided by engineer. - - - months - time unit - - - never - No comment provided by engineer. - - - new message - notification - - - no - pref value - - - no e2e encryption - No comment provided by engineer. - - - no text - copied message info in history - - - observer - member role - - - off - enabled status - group pref value - - - offered %@ - feature offered item - - - offered %1$@: %2$@ - feature offered item - - - on - group pref value - - - or chat with the developers - No comment provided by engineer. - - - owner - member role - - - peer-to-peer - No comment provided by engineer. - - - received answer… - No comment provided by engineer. - - - received confirmation… - No comment provided by engineer. - - - rejected call - call status - - - removed - No comment provided by engineer. - - - removed %@ - rcv group event chat item - - - removed you - rcv group event chat item - - - sec - network option - - - seconds - time unit - - - secret - No comment provided by engineer. - - - starting… - No comment provided by engineer. - - - strike - No comment provided by engineer. - - - this contact - notification title - - - unknown - connection info - - - updated group profile - rcv group event chat item - - - v%@ (%@) - No comment provided by engineer. - - - via contact address link - chat list item description - - - via group link - chat list item description - - - via one-time link - chat list item description - - - via relay - No comment provided by engineer. - - - video call (not e2e encrypted) - No comment provided by engineer. - - - waiting for answer… - No comment provided by engineer. - - - waiting for confirmation… - No comment provided by engineer. - - - wants to connect to you! - No comment provided by engineer. - - - weeks - time unit - - - yes - pref value - - - you are invited to group - No comment provided by engineer. - - - you are observer - No comment provided by engineer. - - - you changed address - chat item text - - - you changed address for %@ - chat item text - - - you changed role for yourself to %@ - snd group event chat item - - - you changed role of %1$@ to %2$@ - snd group event chat item - - - you left - snd group event chat item - - - you removed %@ - snd group event chat item - - - you shared one-time link - chat list item description - - - you shared one-time link incognito - chat list item description - - - you: - No comment provided by engineer. - - - \~strike~ - No comment provided by engineer. - - -
- -
- -
- - - SimpleX - Bundle name - - - SimpleX needs camera access to scan QR codes to connect to other users and for video calls. - Privacy - Camera Usage Description - - - SimpleX uses Face ID for local authentication - Privacy - Face ID Usage Description - - - SimpleX needs microphone access for audio and video calls, and to record voice messages. - Privacy - Microphone Usage Description - - - SimpleX needs access to Photo Library for saving captured and received media - Privacy - Photo Library Additions Usage Description - - -
- -
- -
- - - SimpleX NSE - Bundle display name - - - SimpleX NSE - Bundle name - - - Copyright © 2022 SimpleX Chat. All rights reserved. - Copyright (human-readable) - - -
-
diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index c5a9ba4ea8..15a8c01a64 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -2,7 +2,7 @@
- +
@@ -127,11 +127,6 @@ %@ is geverifieerd No comment provided by engineer. - - %@ servers - %@ servers - No comment provided by engineer. - %@ uploaded %@ geüpload @@ -557,16 +552,17 @@ Over SimpleX adres No comment provided by engineer. - - Accent color - Accent kleur + + Accent + Accent No comment provided by engineer. Accept Accepteer accept contact request via notification - accept incoming call via notification + accept incoming call via notification + swipe action Accept connection request? @@ -581,7 +577,23 @@ Accept incognito Accepteer incognito - accept contact request via notification + accept contact request via notification + swipe action + + + Acknowledged + Erkend + No comment provided by engineer. + + + Acknowledgement errors + Bevestigingsfouten + No comment provided by engineer. + + + Active connections + Actieve verbindingen + No comment provided by engineer. Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. @@ -603,16 +615,16 @@ Profiel toevoegen No comment provided by engineer. + + Add server + Server toevoegen + No comment provided by engineer. + Add servers by scanning QR codes. Servers toevoegen door QR-codes te scannen. No comment provided by engineer. - - Add server… - Server toevoegen… - No comment provided by engineer. - Add to another device Toevoegen aan een ander apparaat @@ -620,7 +632,22 @@ Add welcome message - Welkomst bericht toevoegen + Welkom bericht toevoegen + No comment provided by engineer. + + + Additional accent + Extra accent + No comment provided by engineer. + + + Additional accent 2 + Extra accent 2 + No comment provided by engineer. + + + Additional secondary + Extra secundair No comment provided by engineer. @@ -648,6 +675,11 @@ Geavanceerde netwerk instellingen No comment provided by engineer. + + Advanced settings + Geavanceerde instellingen + No comment provided by engineer. + All app data is deleted. Alle app-gegevens worden verwijderd. @@ -663,6 +695,11 @@ Alle gegevens worden bij het invoeren gewist. No comment provided by engineer. + + All data is private to your device. + Alle gegevens zijn privé op uw apparaat. + No comment provided by engineer. + All group members will remain connected. Alle groepsleden blijven verbonden. @@ -683,6 +720,11 @@ Alle nieuwe berichten van %@ worden verborgen! No comment provided by engineer. + + All profiles + Alle profielen + No comment provided by engineer. + All your contacts will remain connected. Al uw contacten blijven verbonden. @@ -708,11 +750,21 @@ Sta oproepen alleen toe als uw contact dit toestaat. No comment provided by engineer. + + Allow calls? + Oproepen toestaan? + No comment provided by engineer. + Allow disappearing messages only if your contact allows it to you. Sta verdwijnende berichten alleen toe als uw contact dit toestaat. No comment provided by engineer. + + Allow downgrade + Downgraden toestaan + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Sta het onomkeerbaar verwijderen van berichten alleen toe als uw contact dit toestaat. (24 uur) @@ -720,12 +772,12 @@ Allow message reactions only if your contact allows them. - Sta berichtreacties alleen toe als uw contact dit toestaat. + Sta bericht reacties alleen toe als uw contact dit toestaat. No comment provided by engineer. Allow message reactions. - Sta berichtreacties toe. + Sta bericht reacties toe. No comment provided by engineer. @@ -738,6 +790,11 @@ Toestaan dat verdwijnende berichten worden verzonden. No comment provided by engineer. + + Allow sharing + Delen toestaan + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Sta toe om verzonden berichten onomkeerbaar te verwijderen. (24 uur) @@ -770,7 +827,7 @@ Allow your contacts adding message reactions. - Sta uw contactpersonen toe om berichtreacties toe te voegen. + Sta uw contactpersonen toe om bericht reacties toe te voegen. No comment provided by engineer. @@ -808,6 +865,11 @@ Al lid van de groep! No comment provided by engineer. + + Always use private routing. + Gebruik altijd privéroutering. + No comment provided by engineer. + Always use relay Altijd relay gebruiken @@ -873,11 +935,26 @@ Toepassen No comment provided by engineer. + + Apply to + Toepassen op + No comment provided by engineer. + Archive and upload Archiveren en uploaden No comment provided by engineer. + + Archive contacts to chat later. + Archiveer contacten om later te chatten. + No comment provided by engineer. + + + Archived contacts + Gearchiveerde contacten + No comment provided by engineer. + Archiving database Database archiveren @@ -948,6 +1025,11 @@ Terug No comment provided by engineer. + + Background + Achtergrond + No comment provided by engineer. + Bad desktop address Onjuist desktopadres @@ -973,6 +1055,16 @@ Betere berichten No comment provided by engineer. + + Better networking + Beter netwerk + No comment provided by engineer. + + + Black + Zwart + No comment provided by engineer. + Block Blokkeren @@ -1008,9 +1100,19 @@ Geblokkeerd door beheerder No comment provided by engineer. + + Blur for better privacy. + Vervagen voor betere privacy. + No comment provided by engineer. + + + Blur media + Vervaag media + No comment provided by engineer. + Both you and your contact can add message reactions. - Zowel u als uw contact kunnen berichtreacties toevoegen. + Zowel u als uw contact kunnen bericht reacties toevoegen. No comment provided by engineer. @@ -1053,11 +1155,26 @@ Oproepen No comment provided by engineer. + + Calls prohibited! + Bellen verboden! + No comment provided by engineer. + Camera not available Camera niet beschikbaar No comment provided by engineer. + + Can't call contact + Kan contact niet bellen + No comment provided by engineer. + + + Can't call member + Kan lid niet bellen + No comment provided by engineer. + Can't invite contact! Kan contact niet uitnodigen! @@ -1068,6 +1185,11 @@ Kan geen contacten uitnodigen! No comment provided by engineer. + + Can't message member + Kan geen bericht sturen naar lid + No comment provided by engineer. + Cancel Annuleren @@ -1083,11 +1205,21 @@ Geen toegang tot de keychain om database wachtwoord op te slaan No comment provided by engineer. + + Cannot forward message + Kan bericht niet doorsturen + No comment provided by engineer. + Cannot receive file Kan bestand niet ontvangen No comment provided by engineer. + + Capacity exceeded - recipient did not receive previously sent messages. + Capaciteit overschreden - ontvanger heeft eerder verzonden berichten niet ontvangen. + snd error text + Cellular Mobiel @@ -1149,6 +1281,11 @@ Gesprek archief No comment provided by engineer. + + Chat colors + Chat kleuren + No comment provided by engineer. + Chat console Chat console @@ -1164,6 +1301,11 @@ Chat database verwijderd No comment provided by engineer. + + Chat database exported + Chat database geëxporteerd + No comment provided by engineer. + Chat database imported Chat database geïmporteerd @@ -1184,6 +1326,11 @@ Chat is gestopt. Als je deze database al op een ander apparaat hebt gebruikt, moet je deze terugzetten voordat je met chatten begint. No comment provided by engineer. + + Chat list + Chatlijst + No comment provided by engineer. + Chat migrated! Chat gemigreerd! @@ -1194,6 +1341,11 @@ Gesprek voorkeuren No comment provided by engineer. + + Chat theme + Chat thema + No comment provided by engineer. + Chats Gesprekken @@ -1224,10 +1376,25 @@ Kies uit bibliotheek No comment provided by engineer. + + Chunks deleted + Stukken verwijderd + No comment provided by engineer. + + + Chunks downloaded + Stukken gedownload + No comment provided by engineer. + + + Chunks uploaded + Stukken geüpload + No comment provided by engineer. + Clear Wissen - No comment provided by engineer. + swipe action Clear conversation @@ -1249,9 +1416,14 @@ Verwijderd verificatie No comment provided by engineer. - - Colors - Kleuren + + Color chats with the new themes. + Kleurchats met de nieuwe thema's. + No comment provided by engineer. + + + Color mode + Kleur mode No comment provided by engineer. @@ -1264,11 +1436,21 @@ Vergelijk beveiligingscodes met je contacten. No comment provided by engineer. + + Completed + voltooid + No comment provided by engineer. + Configure ICE servers ICE servers configureren No comment provided by engineer. + + Configured %@ servers + %@ servers geconfigureerd + No comment provided by engineer. + Confirm Bevestigen @@ -1279,11 +1461,21 @@ Bevestig toegangscode No comment provided by engineer. + + Confirm contact deletion? + Contact verwijderen bevestigen? + No comment provided by engineer. + Confirm database upgrades Bevestig database upgrades No comment provided by engineer. + + Confirm files from unknown servers. + Bevestig bestanden van onbekende servers. + No comment provided by engineer. + Confirm network settings Bevestig netwerk instellingen @@ -1329,6 +1521,11 @@ Verbinden met desktop No comment provided by engineer. + + Connect to your friends faster. + Maak sneller verbinding met je vrienden. + No comment provided by engineer. + Connect to yourself? Verbinding maken met jezelf? @@ -1368,16 +1565,31 @@ Dit is uw eigen eenmalige link! Verbonden met %@ No comment provided by engineer. + + Connected + Verbonden + No comment provided by engineer. + Connected desktop Verbonden desktop No comment provided by engineer. + + Connected servers + Verbonden servers + No comment provided by engineer. + Connected to desktop Verbonden met desktop No comment provided by engineer. + + Connecting + Verbinden + No comment provided by engineer. + Connecting to server… Verbinden met de server… @@ -1388,6 +1600,11 @@ Dit is uw eigen eenmalige link! Verbinden met server... (fout: %@) No comment provided by engineer. + + Connecting to contact, please wait or check later! + Er wordt verbinding gemaakt met het contact. Even geduld of controleer het later! + No comment provided by engineer. + Connecting to desktop Verbinding maken met desktop @@ -1398,6 +1615,11 @@ Dit is uw eigen eenmalige link! Verbinding No comment provided by engineer. + + Connection and servers status. + Verbindings- en serverstatus. + No comment provided by engineer. + Connection error Verbindingsfout @@ -1408,6 +1630,11 @@ Dit is uw eigen eenmalige link! Verbindingsfout (AUTH) No comment provided by engineer. + + Connection notifications + Verbindingsmeldingen + No comment provided by engineer. + Connection request sent! Verbindingsverzoek verzonden! @@ -1423,6 +1650,16 @@ Dit is uw eigen eenmalige link! Timeout verbinding No comment provided by engineer. + + Connection with desktop stopped + Verbinding met desktop is gestopt + No comment provided by engineer. + + + Connections + Verbindingen + No comment provided by engineer. + Contact allows Contact maakt het mogelijk @@ -1433,6 +1670,11 @@ Dit is uw eigen eenmalige link! Contact bestaat al No comment provided by engineer. + + Contact deleted! + Contact verwijderd! + No comment provided by engineer. + Contact hidden: Contact verborgen: @@ -1443,9 +1685,9 @@ Dit is uw eigen eenmalige link! Contact is verbonden notification - - Contact is not connected yet! - Contact is nog niet verbonden! + + Contact is deleted. + Contact is verwijderd. No comment provided by engineer. @@ -1458,6 +1700,11 @@ Dit is uw eigen eenmalige link! Contact voorkeuren No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + Het contact wordt verwijderd. Dit kan niet ongedaan worden gemaakt! + No comment provided by engineer. + Contacts Contacten @@ -1473,10 +1720,20 @@ Dit is uw eigen eenmalige link! Doorgaan No comment provided by engineer. + + Conversation deleted! + Gesprek verwijderd! + No comment provided by engineer. + Copy Kopiëren - chat item action + No comment provided by engineer. + + + Copy error + Kopieerfout + No comment provided by engineer. Core version: v%@ @@ -1553,6 +1810,11 @@ Dit is uw eigen eenmalige link! Maak je profiel aan No comment provided by engineer. + + Created + Gemaakt + No comment provided by engineer. + Created at Gemaakt op @@ -1588,6 +1850,11 @@ Dit is uw eigen eenmalige link! Huidige wachtwoord… No comment provided by engineer. + + Current profile + Huidig profiel + No comment provided by engineer. + Currently maximum supported file size is %@. De momenteel maximaal ondersteunde bestandsgrootte is %@. @@ -1598,11 +1865,21 @@ Dit is uw eigen eenmalige link! Aangepaste tijd No comment provided by engineer. + + Customize theme + Thema aanpassen + No comment provided by engineer. + Dark Donker No comment provided by engineer. + + Dark mode colors + Kleuren in donkere modus + No comment provided by engineer. + Database ID Database-ID @@ -1701,6 +1978,11 @@ Dit is uw eigen eenmalige link! De database wordt gemigreerd wanneer de app opnieuw wordt opgestart No comment provided by engineer. + + Debug delivery + Foutopsporing bezorging + No comment provided by engineer. + Decentralized Gedecentraliseerd @@ -1714,18 +1996,19 @@ Dit is uw eigen eenmalige link! Delete Verwijderen - chat item action + chat item action + swipe action + + + Delete %lld messages of members? + %lld berichten van leden verwijderen? + No comment provided by engineer. Delete %lld messages? %lld berichten verwijderen? No comment provided by engineer. - - Delete Contact - Verwijder contact - No comment provided by engineer. - Delete address Adres verwijderen @@ -1781,11 +2064,9 @@ Dit is uw eigen eenmalige link! Verwijder contact No comment provided by engineer. - - Delete contact? -This cannot be undone! - Verwijder contact? -Dit kan niet ongedaan gemaakt worden! + + Delete contact? + Verwijder contact? No comment provided by engineer. @@ -1878,11 +2159,6 @@ Dit kan niet ongedaan gemaakt worden! Oude database verwijderen? No comment provided by engineer. - - Delete pending connection - Wachtende verbinding verwijderen - No comment provided by engineer. - Delete pending connection? Wachtende verbinding verwijderen? @@ -1898,11 +2174,26 @@ Dit kan niet ongedaan gemaakt worden! Wachtrij verwijderen server test step + + Delete up to 20 messages at once. + Verwijder maximaal 20 berichten tegelijk. + No comment provided by engineer. + Delete user profile? Gebruikers profiel verwijderen? No comment provided by engineer. + + Delete without notification + Verwijderen zonder melding + No comment provided by engineer. + + + Deleted + Verwijderd + No comment provided by engineer. + Deleted at Verwijderd om @@ -1913,6 +2204,11 @@ Dit kan niet ongedaan gemaakt worden! Verwijderd om: %@ copied message info + + Deletion errors + Verwijderingsfouten + No comment provided by engineer. + Delivery Bezorging @@ -1948,11 +2244,41 @@ Dit kan niet ongedaan gemaakt worden! Desktop apparaten No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + Het bestemmingsserveradres van %@ is niet compatibel met de doorstuurserverinstellingen %@. + No comment provided by engineer. + + + Destination server error: %@ + Bestemmingsserverfout: %@ + snd error text + + + Destination server version of %@ is incompatible with forwarding server %@. + De versie van de bestemmingsserver %@ is niet compatibel met de doorstuurserver %@. + No comment provided by engineer. + + + Detailed statistics + Gedetailleerde statistieken + No comment provided by engineer. + + + Details + Details + No comment provided by engineer. + Develop Ontwikkelen No comment provided by engineer. + + Developer options + Ontwikkelaars opties + No comment provided by engineer. + Developer tools Ontwikkel gereedschap @@ -2003,6 +2329,11 @@ Dit kan niet ongedaan gemaakt worden! Uitschakelen voor iedereen No comment provided by engineer. + + Disabled + Uitgeschakeld + No comment provided by engineer. + Disappearing message Verdwijnend bericht @@ -2053,11 +2384,21 @@ Dit kan niet ongedaan gemaakt worden! Ontdek via het lokale netwerk No comment provided by engineer. + + Do NOT send messages directly, even if your or destination server does not support private routing. + Stuur GEEN berichten rechtstreeks, zelfs als uw of de bestemmingsserver geen privéroutering ondersteunt. + No comment provided by engineer. + Do NOT use SimpleX for emergency calls. Gebruik SimpleX NIET voor noodoproepen. No comment provided by engineer. + + Do NOT use private routing. + Gebruik GEEN privéroutering. + No comment provided by engineer. + Do it later Doe het later @@ -2093,6 +2434,11 @@ Dit kan niet ongedaan gemaakt worden! Downloaden chat item action + + Download errors + Downloadfouten + No comment provided by engineer. + Download failed Download mislukt @@ -2103,6 +2449,16 @@ Dit kan niet ongedaan gemaakt worden! Download bestand server test step + + Downloaded + Gedownload + No comment provided by engineer. + + + Downloaded files + Gedownloade bestanden + No comment provided by engineer. + Downloading archive Archief downloaden @@ -2203,6 +2559,11 @@ Dit kan niet ongedaan gemaakt worden! Zelfvernietigings wachtwoord inschakelen set passcode view + + Enabled + Ingeschakeld + No comment provided by engineer. + Enabled for Ingeschakeld voor @@ -2320,12 +2681,12 @@ Dit kan niet ongedaan gemaakt worden! Enter welcome message… - Welkomst bericht invoeren… + Welkom bericht invoeren… placeholder Enter welcome message… (optional) - Voer welkomst bericht in... (optioneel) + Voer welkom bericht in... (optioneel) placeholder @@ -2373,6 +2734,11 @@ Dit kan niet ongedaan gemaakt worden! Fout bij wijzigen van instelling No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + Fout bij het verbinden met doorstuurserver %@. Probeer het later opnieuw. + No comment provided by engineer. + Error creating address Fout bij aanmaken van adres @@ -2423,11 +2789,6 @@ Dit kan niet ongedaan gemaakt worden! Fout bij verwijderen van verbinding No comment provided by engineer. - - Error deleting contact - Fout bij het verwijderen van contact - No comment provided by engineer. - Error deleting database Fout bij het verwijderen van de database @@ -2473,6 +2834,11 @@ Dit kan niet ongedaan gemaakt worden! Fout bij het exporteren van de chat database No comment provided by engineer. + + Error exporting theme: %@ + Fout bij exporteren van thema: %@ + No comment provided by engineer. + Error importing chat database Fout bij het importeren van de chat database @@ -2498,11 +2864,26 @@ Dit kan niet ongedaan gemaakt worden! Fout bij ontvangen van bestand No comment provided by engineer. + + Error reconnecting server + Fout bij opnieuw verbinding maken met de server + No comment provided by engineer. + + + Error reconnecting servers + Fout bij opnieuw verbinden van servers + No comment provided by engineer. + Error removing member Fout bij verwijderen van lid No comment provided by engineer. + + Error resetting statistics + Fout bij het resetten van statistieken + No comment provided by engineer. + Error saving %@ servers Fout bij opslaan van %@ servers @@ -2621,7 +3002,8 @@ Dit kan niet ongedaan gemaakt worden! Error: %@ Fout: %@ - No comment provided by engineer. + file error text + snd error text Error: URL is invalid @@ -2633,6 +3015,11 @@ Dit kan niet ongedaan gemaakt worden! Fout: geen database bestand No comment provided by engineer. + + Errors + Fouten + No comment provided by engineer. + Even when disabled in the conversation. Zelfs wanneer uitgeschakeld in het gesprek. @@ -2658,6 +3045,11 @@ Dit kan niet ongedaan gemaakt worden! Exportfout: No comment provided by engineer. + + Export theme + Exporteer thema + No comment provided by engineer. + Exported database archive. Geëxporteerd database archief. @@ -2691,8 +3083,33 @@ Dit kan niet ongedaan gemaakt worden! Favorite Favoriet + swipe action + + + File error + Bestandsfout No comment provided by engineer. + + File not found - most likely file was deleted or cancelled. + Bestand niet gevonden - hoogstwaarschijnlijk is het bestand verwijderd of geannuleerd. + file error text + + + File server error: %@ + Bestandsserverfout: %@ + file error text + + + File status + Bestandsstatus + No comment provided by engineer. + + + File status: %@ + Bestandsstatus: %@ + copied message info + File will be deleted from servers. Het bestand wordt van de servers verwijderd. @@ -2713,6 +3130,11 @@ Dit kan niet ongedaan gemaakt worden! Bestand: %@ No comment provided by engineer. + + Files + Bestanden + No comment provided by engineer. + Files & media Bestanden en media @@ -2818,6 +3240,35 @@ Dit kan niet ongedaan gemaakt worden! Doorgestuurd vanuit No comment provided by engineer. + + Forwarding server %@ failed to connect to destination server %@. Please try later. + De doorstuurserver %@ kon geen verbinding maken met de bestemmingsserver %@. Probeer het later opnieuw. + No comment provided by engineer. + + + Forwarding server address is incompatible with network settings: %@. + Het adres van de doorstuurserver is niet compatibel met de netwerkinstellingen: %@. + No comment provided by engineer. + + + Forwarding server version is incompatible with network settings: %@. + De doorstuurserverversie is niet compatibel met de netwerkinstellingen: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + Doorstuurserver: %1$@ +Bestemmingsserverfout: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + Doorstuurserver: %1$@ +Fout: %2$@ + snd error text + Found desktop Desktop gevonden @@ -2863,6 +3314,16 @@ Dit kan niet ongedaan gemaakt worden! GIF's en stickers No comment provided by engineer. + + Good afternoon! + Goedemiddag! + message preview + + + Good morning! + Goedemorgen! + message preview + Group Groep @@ -2920,7 +3381,7 @@ Dit kan niet ongedaan gemaakt worden! Group members can add message reactions. - Groepsleden kunnen berichtreacties toevoegen. + Groepsleden kunnen bericht reacties toevoegen. No comment provided by engineer. @@ -2980,7 +3441,7 @@ Dit kan niet ongedaan gemaakt worden! Group welcome message - Groep welkomst bericht + Groep welkom bericht No comment provided by engineer. @@ -3143,6 +3604,11 @@ Dit kan niet ongedaan gemaakt worden! Importeren is mislukt No comment provided by engineer. + + Import theme + Thema importeren + No comment provided by engineer. + Importing archive Archief importeren @@ -3265,6 +3731,11 @@ Dit kan niet ongedaan gemaakt worden! Interface No comment provided by engineer. + + Interface colors + Interface kleuren + No comment provided by engineer. + Invalid QR code Ongeldige QR-code @@ -3366,6 +3837,11 @@ Dit kan niet ongedaan gemaakt worden! 3. De verbinding is verbroken. No comment provided by engineer. + + It protects your IP address and connections. + Het beschermt uw IP-adres en verbindingen. + No comment provided by engineer. + It seems like you are already connected via this link. If it is not the case, there was an error (%@). Het lijkt erop dat u al bent verbonden via deze link. Als dit niet het geval is, is er een fout opgetreden (%@). @@ -3384,7 +3860,7 @@ Dit kan niet ongedaan gemaakt worden! Join Word lid van - No comment provided by engineer. + swipe action Join group @@ -3428,6 +3904,11 @@ Dit is jouw link voor groep %@! Bewaar No comment provided by engineer. + + Keep conversation + Behoud het gesprek + No comment provided by engineer. + Keep the app open to use it from desktop Houd de app geopend om deze vanaf de desktop te gebruiken @@ -3471,7 +3952,7 @@ Dit is jouw link voor groep %@! Leave Verlaten - No comment provided by engineer. + swipe action Leave group @@ -3603,11 +4084,26 @@ Dit is jouw link voor groep %@! Max 30 seconden, direct ontvangen. No comment provided by engineer. + + Media & file servers + Media- en bestandsservers + No comment provided by engineer. + + + Medium + Medium + blur media + Member Lid No comment provided by engineer. + + Member inactive + Lid inactief + item status text + Member role will be changed to "%@". All group members will be notified. De rol van lid wordt gewijzigd in "%@". Alle groepsleden worden op de hoogte gebracht. @@ -3623,6 +4119,11 @@ Dit is jouw link voor groep %@! Lid wordt uit de groep verwijderd, dit kan niet ongedaan worden gemaakt! No comment provided by engineer. + + Menus + Menu's + No comment provided by engineer. + Message delivery error Fout bij bezorging van bericht @@ -3633,11 +4134,31 @@ Dit is jouw link voor groep %@! Ontvangst bevestiging voor berichten! No comment provided by engineer. + + Message delivery warning + Waarschuwing voor berichtbezorging + item status text + Message draft Concept bericht No comment provided by engineer. + + Message forwarded + Bericht doorgestuurd + item status text + + + Message may be delivered later if member becomes active. + Het bericht kan later worden bezorgd als het lid actief wordt. + item status description + + + Message queue info + Informatie over berichtenwachtrij + No comment provided by engineer. + Message reactions Reacties op berichten @@ -3653,11 +4174,31 @@ Dit is jouw link voor groep %@! Reacties op berichten zijn verboden in deze groep. No comment provided by engineer. + + Message reception + Bericht ontvangst + No comment provided by engineer. + + + Message servers + Berichtservers + No comment provided by engineer. + Message source remains private. Berichtbron blijft privé. No comment provided by engineer. + + Message status + Berichtstatus + No comment provided by engineer. + + + Message status: %@ + Berichtstatus: %@ + copied message info + Message text Bericht tekst @@ -3683,6 +4224,16 @@ Dit is jouw link voor groep %@! Berichten van %@ worden getoond! No comment provided by engineer. + + Messages received + Berichten ontvangen + No comment provided by engineer. + + + Messages sent + Berichten verzonden + No comment provided by engineer. + Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. Berichten, bestanden en oproepen worden beschermd door **end-to-end codering** met perfecte voorwaartse geheimhouding, afwijzing en inbraakherstel. @@ -3740,7 +4291,7 @@ Dit is jouw link voor groep %@! Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat). - Migratie mislukt. Tik hieronder op **Overslaan** om de huidige database te blijven gebruiken. Meld het probleem aan de app-ontwikkelaars via chat of e-mail [chat@simplex.chat](mailto:chat@simplex.chat). + Migratie mislukt. Tik hier onder op **Overslaan** om de huidige database te blijven gebruiken. Meld het probleem aan de app-ontwikkelaars via chat of e-mail [chat@simplex.chat](mailto:chat@simplex.chat). No comment provided by engineer. @@ -3783,11 +4334,6 @@ Dit is jouw link voor groep %@! Hoogstwaarschijnlijk is deze verbinding verwijderd. item status description - - Most likely this contact has deleted the connection with you. - Hoogstwaarschijnlijk heeft dit contact de verbinding met jou verwijderd. - No comment provided by engineer. - Multiple chat profiles Meerdere chat profielen @@ -3796,7 +4342,7 @@ Dit is jouw link voor groep %@! Mute Dempen - No comment provided by engineer. + swipe action Muted when inactive! @@ -3806,7 +4352,7 @@ Dit is jouw link voor groep %@! Name Naam - No comment provided by engineer. + swipe action Network & servers @@ -3818,6 +4364,11 @@ Dit is jouw link voor groep %@! Netwerkverbinding No comment provided by engineer. + + Network issues - message expired after many attempts to send it. + Netwerkproblemen - bericht is verlopen na vele pogingen om het te verzenden. + snd error text + Network management Netwerkbeheer @@ -3843,6 +4394,11 @@ Dit is jouw link voor groep %@! Nieuw gesprek No comment provided by engineer. + + New chat experience 🎉 + Nieuwe chatervaring 🎉 + No comment provided by engineer. + New contact request Nieuw contactverzoek @@ -3873,6 +4429,11 @@ Dit is jouw link voor groep %@! Nieuw in %@ No comment provided by engineer. + + New media options + Nieuwe media-opties + No comment provided by engineer. + New member role Nieuwe leden rol @@ -3918,6 +4479,11 @@ Dit is jouw link voor groep %@! Geen apparaattoken! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + Nog geen directe verbinding, bericht wordt doorgestuurd door beheerder. + item status description + No filtered chats Geen gefilterde gesprekken @@ -3933,6 +4499,11 @@ Dit is jouw link voor groep %@! Geen geschiedenis No comment provided by engineer. + + No info, try to reload + Geen info, probeer opnieuw te laden + No comment provided by engineer. + No network connection Geen netwerkverbinding @@ -3953,6 +4524,11 @@ Dit is jouw link voor groep %@! Niet compatibel! No comment provided by engineer. + + Nothing selected + Niets geselecteerd + No comment provided by engineer. + Notifications Meldingen @@ -3980,7 +4556,7 @@ Dit is jouw link voor groep %@! Off Uit - No comment provided by engineer. + blur media Ok @@ -4002,14 +4578,18 @@ Dit is jouw link voor groep %@! Eenmalige uitnodiging link No comment provided by engineer. - - Onion hosts will be required for connection. Requires enabling VPN. - Onion hosts zullen nodig zijn voor verbinding. Vereist het inschakelen van VPN. + + Onion hosts will be **required** for connection. +Requires compatible VPN. + Onion hosts zullen nodig zijn voor verbinding. +Vereist het inschakelen van VPN. No comment provided by engineer. - - Onion hosts will be used when available. Requires enabling VPN. - Onion hosts worden gebruikt indien beschikbaar. Vereist het inschakelen van VPN. + + Onion hosts will be used when available. +Requires compatible VPN. + Onion hosts worden gebruikt indien beschikbaar. +Vereist het inschakelen van VPN. No comment provided by engineer. @@ -4022,6 +4602,11 @@ Dit is jouw link voor groep %@! Alleen client apparaten slaan gebruikers profielen, contacten, groepen en berichten op die zijn verzonden met **2-laags end-to-end-codering**. No comment provided by engineer. + + Only delete conversation + Alleen conversatie verwijderen + No comment provided by engineer. + Only group owners can change group preferences. Alleen groep eigenaren kunnen groep voorkeuren wijzigen. @@ -4039,7 +4624,7 @@ Dit is jouw link voor groep %@! Only you can add message reactions. - Alleen jij kunt berichtreacties toevoegen. + Alleen jij kunt bericht reacties toevoegen. No comment provided by engineer. @@ -4064,7 +4649,7 @@ Dit is jouw link voor groep %@! Only your contact can add message reactions. - Alleen uw contact kan berichtreacties toevoegen. + Alleen uw contact kan bericht reacties toevoegen. No comment provided by engineer. @@ -4117,6 +4702,11 @@ Dit is jouw link voor groep %@! Open de migratie naar een ander apparaat authentication reason + + Open server settings + Server instellingen openen + No comment provided by engineer. + Open user profiles Gebruikers profielen openen @@ -4157,6 +4747,11 @@ Dit is jouw link voor groep %@! Ander No comment provided by engineer. + + Other %@ servers + Andere %@ servers + No comment provided by engineer. + PING count PING count @@ -4222,6 +4817,11 @@ Dit is jouw link voor groep %@! Plak de link die je hebt ontvangen No comment provided by engineer. + + Pending + in behandeling + No comment provided by engineer. + People can connect to you only via the links you share. Mensen kunnen alleen verbinding met u maken via de links die u deelt. @@ -4242,11 +4842,28 @@ Dit is jouw link voor groep %@! Beeld-in-beeld oproepen No comment provided by engineer. + + Play from the chat list. + Afspelen via de gesprekken lijst. + No comment provided by engineer. + + + Please ask your contact to enable calls. + Vraag uw contactpersoon om oproepen in te schakelen. + No comment provided by engineer. + Please ask your contact to enable sending voice messages. Vraag uw contact om het verzenden van spraak berichten in te schakelen. No comment provided by engineer. + + Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers. + Controleer of mobiel en desktop met hetzelfde lokale netwerk zijn verbonden en of de desktopfirewall de verbinding toestaat. +Deel eventuele andere problemen met de ontwikkelaars. + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. Controleer of u de juiste link heeft gebruikt of vraag uw contact om u een andere te sturen. @@ -4344,6 +4961,11 @@ Fout: %@ Voorbeeld No comment provided by engineer. + + Previously connected servers + Eerder verbonden servers + No comment provided by engineer. + Privacy & security Privacy en beveiliging @@ -4359,11 +4981,31 @@ Fout: %@ Privé bestandsnamen No comment provided by engineer. + + Private message routing + Routering van privéberichten + No comment provided by engineer. + + + Private message routing 🚀 + Routing van privéberichten🚀 + No comment provided by engineer. + Private notes Privé notities name of notes to self + + Private routing + Privéroutering + No comment provided by engineer. + + + Private routing error + Fout in privéroutering + No comment provided by engineer. + Profile and server connections Profiel- en serververbindingen @@ -4394,6 +5036,11 @@ Fout: %@ Profiel wachtwoord No comment provided by engineer. + + Profile theme + Profiel thema + No comment provided by engineer. + Profile update will be sent to your contacts. Profiel update wordt naar uw contacten verzonden. @@ -4411,7 +5058,7 @@ Fout: %@ Prohibit message reactions. - Berichtreacties verbieden. + Bericht reacties verbieden. No comment provided by engineer. @@ -4444,11 +5091,23 @@ Fout: %@ Verbieden het verzenden van spraak berichten. No comment provided by engineer. + + Protect IP address + Bescherm het IP-adres + No comment provided by engineer. + Protect app screen App scherm verbergen No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + Bescherm uw IP-adres tegen de berichtenrelais die door uw contacten zijn gekozen. +Schakel dit in in *Netwerk en servers*-instellingen. + No comment provided by engineer. + Protect your chat profiles with a password! Bescherm je chat profielen met een wachtwoord! @@ -4464,6 +5123,16 @@ Fout: %@ Protocol timeout per KB No comment provided by engineer. + + Proxied + Proxied + No comment provided by engineer. + + + Proxied servers + Proxied servers + No comment provided by engineer. + Push notifications Push meldingen @@ -4484,6 +5153,11 @@ Fout: %@ Beoordeel de app No comment provided by engineer. + + Reachable chat toolbar + Toegankelijke chatwerkbalk + No comment provided by engineer. + React… Reageer… @@ -4492,7 +5166,7 @@ Fout: %@ Read Lees - No comment provided by engineer. + swipe action Read more @@ -4529,6 +5203,11 @@ Fout: %@ Bevestigingen zijn uitgeschakeld No comment provided by engineer. + + Receive errors + Fouten ontvangen + No comment provided by engineer. + Received at Ontvangen op @@ -4549,16 +5228,26 @@ Fout: %@ Ontvangen bericht message info title + + Received messages + Ontvangen berichten + No comment provided by engineer. + + + Received reply + Antwoord ontvangen + No comment provided by engineer. + + + Received total + Totaal ontvangen + No comment provided by engineer. + Receiving address will be changed to a different server. Address change will complete after sender comes online. Het ontvangstadres wordt gewijzigd naar een andere server. Adres wijziging wordt voltooid nadat de afzender online is. No comment provided by engineer. - - Receiving concurrency - Gelijktijdig ontvangen - No comment provided by engineer. - Receiving file will be stopped. Het ontvangen van het bestand wordt gestopt. @@ -4584,11 +5273,36 @@ Fout: %@ Ontvangers zien updates terwijl u ze typt. No comment provided by engineer. + + Reconnect + opnieuw verbinden + No comment provided by engineer. + Reconnect all connected servers to force message delivery. It uses additional traffic. Verbind alle verbonden servers opnieuw om de bezorging van berichten af te dwingen. Het maakt gebruik van extra data. No comment provided by engineer. + + Reconnect all servers + Maak opnieuw verbinding met alle servers + No comment provided by engineer. + + + Reconnect all servers? + Alle servers opnieuw verbinden? + No comment provided by engineer. + + + Reconnect server to force message delivery. It uses additional traffic. + Maak opnieuw verbinding met de server om de bezorging van berichten te forceren. Er wordt gebruik gemaakt van extra verkeer.Maak opnieuw verbinding met de server om de bezorging van berichten te forceren. Er wordt gebruik gemaakt van extra data. + No comment provided by engineer. + + + Reconnect server? + Server opnieuw verbinden? + No comment provided by engineer. + Reconnect servers? Servers opnieuw verbinden? @@ -4612,7 +5326,8 @@ Fout: %@ Reject Afwijzen - reject incoming call via notification + reject incoming call via notification + swipe action Reject (sender NOT notified) @@ -4639,6 +5354,11 @@ Fout: %@ Verwijderen No comment provided by engineer. + + Remove image + Verwijder afbeelding + No comment provided by engineer. + Remove member Lid verwijderen @@ -4709,16 +5429,41 @@ Fout: %@ Resetten No comment provided by engineer. + + Reset all hints + Alle hints resetten + No comment provided by engineer. + + + Reset all statistics + Reset alle statistieken + No comment provided by engineer. + + + Reset all statistics? + Alle statistieken resetten? + No comment provided by engineer. + Reset colors Kleuren resetten No comment provided by engineer. + + Reset to app theme + Terugzetten naar app thema + No comment provided by engineer. + Reset to defaults Resetten naar standaardwaarden No comment provided by engineer. + + Reset to user theme + Terugzetten naar gebruikersthema + No comment provided by engineer. + Restart the app to create a new chat profile Start de app opnieuw om een nieuw chat profiel aan te maken @@ -4759,11 +5504,6 @@ Fout: %@ Onthullen chat item action - - Revert - Terugdraaien - No comment provided by engineer. - Revoke Intrekken @@ -4789,9 +5529,14 @@ Fout: %@ Chat uitvoeren No comment provided by engineer. - - SMP servers - SMP servers + + SMP server + SMP server + No comment provided by engineer. + + + Safely receive files + Veilig bestanden ontvangen No comment provided by engineer. @@ -4819,6 +5564,11 @@ Fout: %@ Opslaan en groep leden melden No comment provided by engineer. + + Save and reconnect + Opslaan en opnieuw verbinden + No comment provided by engineer. + Save and update group profile Groep profiel opslaan en bijwerken @@ -4876,7 +5626,7 @@ Fout: %@ Save welcome message? - Welkomst bericht opslaan? + Welkom bericht opslaan? No comment provided by engineer. @@ -4899,6 +5649,16 @@ Fout: %@ Opgeslagen bericht message info title + + Scale + Schaal + No comment provided by engineer. + + + Scan / Paste link + Link scannen/plakken + No comment provided by engineer. + Scan QR code Scan QR-code @@ -4939,11 +5699,21 @@ Fout: %@ Zoeken of plak een SimpleX link No comment provided by engineer. + + Secondary + Secundair + No comment provided by engineer. + Secure queue Veilige wachtrij server test step + + Secured + Beveiligd + No comment provided by engineer. + Security assessment Beveiligingsbeoordeling @@ -4957,6 +5727,16 @@ Fout: %@ Select Selecteer + chat item action + + + Selected %lld + %lld geselecteerd + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + Geselecteerde chat voorkeuren verbieden dit bericht. No comment provided by engineer. @@ -4994,11 +5774,6 @@ Fout: %@ Stuur ontvangstbewijzen naar No comment provided by engineer. - - Send direct message - Direct bericht sturen - No comment provided by engineer. - Send direct message to connect Stuur een direct bericht om verbinding te maken @@ -5009,6 +5784,11 @@ Fout: %@ Stuur een verdwijnend bericht No comment provided by engineer. + + Send errors + Verzend fouten + No comment provided by engineer. + Send link previews Link voorbeelden verzenden @@ -5019,6 +5799,21 @@ Fout: %@ Stuur een livebericht No comment provided by engineer. + + Send message to enable calls. + Stuur een bericht om oproepen mogelijk te maken. + No comment provided by engineer. + + + Send messages directly when IP address is protected and your or destination server does not support private routing. + Stuur berichten rechtstreeks als het IP-adres beschermd is en uw of bestemmingsserver geen privéroutering ondersteunt. + No comment provided by engineer. + + + Send messages directly when your or destination server does not support private routing. + Stuur berichten rechtstreeks wanneer uw of de doelserver geen privéroutering ondersteunt. + No comment provided by engineer. + Send notifications Meldingen verzenden @@ -5109,6 +5904,11 @@ Fout: %@ Verzonden op: %@ copied message info + + Sent directly + Direct verzonden + No comment provided by engineer. + Sent file event Verzonden bestandsgebeurtenis @@ -5119,11 +5919,46 @@ Fout: %@ Verzonden bericht message info title + + Sent messages + Verzonden berichten + No comment provided by engineer. + Sent messages will be deleted after set time. Verzonden berichten worden na ingestelde tijd verwijderd. No comment provided by engineer. + + Sent reply + Antwoord verzonden + No comment provided by engineer. + + + Sent total + Totaal verzonden + No comment provided by engineer. + + + Sent via proxy + Verzonden via proxy + No comment provided by engineer. + + + Server address + Server adres + No comment provided by engineer. + + + Server address is incompatible with network settings. + Serveradres is niet compatibel met netwerkinstellingen. + srv error text. + + + Server address is incompatible with network settings: %@. + Serveradres is incompatibel met netwerkinstellingen: %@. + No comment provided by engineer. + Server requires authorization to create queues, check password Server vereist autorisatie om wachtrijen te maken, controleer wachtwoord @@ -5139,11 +5974,36 @@ Fout: %@ Servertest mislukt! No comment provided by engineer. + + Server type + Server type + No comment provided by engineer. + + + Server version is incompatible with network settings. + Serverversie is incompatibel met netwerkinstellingen. + srv error text + + + Server version is incompatible with your app: %@. + Serverversie is incompatibel met uw app: %@. + No comment provided by engineer. + Servers Servers No comment provided by engineer. + + Servers info + Server informatie + No comment provided by engineer. + + + Servers statistics will be reset - this cannot be undone! + Serverstatistieken worden gereset - dit kan niet ongedaan worden gemaakt! + No comment provided by engineer. + Session code Sessie code @@ -5159,6 +6019,11 @@ Fout: %@ Contactnaam instellen… No comment provided by engineer. + + Set default theme + Stel het standaard thema in + No comment provided by engineer. + Set group preferences Groep voorkeuren instellen @@ -5224,6 +6089,11 @@ Fout: %@ Adres delen met contacten? No comment provided by engineer. + + Share from other apps. + Delen vanuit andere apps. + No comment provided by engineer. + Share link Deel link @@ -5234,6 +6104,11 @@ Fout: %@ Deel deze eenmalige uitnodigingslink No comment provided by engineer. + + Share to SimpleX + Delen op SimpleX + No comment provided by engineer. + Share with contacts Delen met contacten @@ -5259,16 +6134,36 @@ Fout: %@ Laat laatste berichten zien No comment provided by engineer. + + Show message status + Toon berichtstatus + No comment provided by engineer. + + + Show percentage + Percentage weergeven + No comment provided by engineer. + Show preview Toon voorbeeld No comment provided by engineer. + + Show → on messages sent via private routing. + Toon → bij berichten verzonden via privéroutering. + No comment provided by engineer. + Show: Toon: No comment provided by engineer. + + SimpleX + SimpleX + No comment provided by engineer. + SimpleX Address SimpleX adres @@ -5344,6 +6239,11 @@ Fout: %@ Vereenvoudigde incognitomodus No comment provided by engineer. + + Size + Maat + No comment provided by engineer. + Skip Overslaan @@ -5359,11 +6259,26 @@ Fout: %@ Kleine groepen (max 20) No comment provided by engineer. + + Soft + Soft + blur media + + + Some file(s) were not exported: + Sommige bestanden zijn niet geëxporteerd: + No comment provided by engineer. + Some non-fatal errors occurred during import - you may see Chat console for more details. Er zijn enkele niet-fatale fouten opgetreden tijdens het importeren - u kunt de Chat console raadplegen voor meer details. No comment provided by engineer. + + Some non-fatal errors occurred during import: + Er zijn enkele niet-fatale fouten opgetreden tijdens het importeren: + No comment provided by engineer. + Somebody Iemand @@ -5389,6 +6304,16 @@ Fout: %@ Start migratie No comment provided by engineer. + + Starting from %@. + Beginnend vanaf %@. + No comment provided by engineer. + + + Statistics + Statistieken + No comment provided by engineer. + Stop Stop @@ -5449,11 +6374,31 @@ Fout: %@ Chat stoppen No comment provided by engineer. + + Strong + Krachtig + blur media + Submit Indienen No comment provided by engineer. + + Subscribed + Ingeschreven + No comment provided by engineer. + + + Subscription errors + Inschrijving fouten + No comment provided by engineer. + + + Subscriptions ignored + Inschrijvingen genegeerd + No comment provided by engineer. + Support SimpleX Chat Ondersteuning van SimpleX Chat @@ -5469,6 +6414,11 @@ Fout: %@ Systeem authenticatie No comment provided by engineer. + + TCP connection + TCP verbinding + No comment provided by engineer. + TCP connection timeout Timeout van TCP-verbinding @@ -5501,37 +6451,37 @@ Fout: %@ Tap to Connect - Tik om verbinding te maken + Tik hier om verbinding te maken No comment provided by engineer. Tap to activate profile. - Tik om profiel te activeren. + Tik hier om profiel te activeren. No comment provided by engineer. Tap to join - Tik om lid te worden + Tik hier om lid te worden No comment provided by engineer. Tap to join incognito - Tik om incognito lid te worden + Tik hier om incognito lid te worden No comment provided by engineer. Tap to paste link - Tik om de link te plakken + Tik hier om de link te plakken No comment provided by engineer. Tap to scan - Tik om te scannen + Tik hier om te scannen No comment provided by engineer. - - Tap to start a new chat - Tik om een nieuw gesprek te starten + + Temporary file error + Tijdelijke bestandsfout No comment provided by engineer. @@ -5586,6 +6536,11 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. De app kan u op de hoogte stellen wanneer u berichten of contact verzoeken ontvangt - open de instellingen om dit in te schakelen. No comment provided by engineer. + + The app will ask to confirm downloads from unknown file servers (except .onion). + De app vraagt om downloads van onbekende bestandsservers (behalve .onion) te bevestigen. + No comment provided by engineer. + The attempt to change database passphrase was not completed. De poging om het wachtwoord van de database te wijzigen is niet voltooid. @@ -5631,6 +6586,16 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. Het bericht wordt gemarkeerd als gemodereerd voor alle leden. No comment provided by engineer. + + The messages will be deleted for all members. + De berichten worden voor alle leden verwijderd. + No comment provided by engineer. + + + The messages will be marked as moderated for all members. + De berichten worden voor alle leden als gemodereerd gemarkeerd. + No comment provided by engineer. + The next generation of private messaging De volgende generatie privéberichten @@ -5666,9 +6631,9 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. De tekst die u hebt geplakt is geen SimpleX link. No comment provided by engineer. - - Theme - Thema + + Themes + Thema's No comment provided by engineer. @@ -5736,11 +6701,21 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. Dit is uw eigen eenmalige link! No comment provided by engineer. + + This link was used with another mobile device, please create a new link on the desktop. + Deze link is gebruikt met een ander mobiel apparaat. Maak een nieuwe link op de desktop. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. Deze instelling is van toepassing op berichten in je huidige chat profiel **%@**. No comment provided by engineer. + + Title + Titel + No comment provided by engineer. + To ask any questions and to receive updates: Om vragen te stellen en updates te ontvangen: @@ -5771,6 +6746,11 @@ Het kan gebeuren vanwege een bug of wanneer de verbinding is aangetast. Om de tijdzone te beschermen, gebruiken afbeeldings-/spraakbestanden UTC. No comment provided by engineer. + + To protect your IP address, private routing uses your SMP servers to deliver messages. + Om uw IP-adres te beschermen, gebruikt privéroutering uw SMP-servers om berichten te bezorgen. + No comment provided by engineer. + To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5798,16 +6778,36 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Vergelijk (of scan) de code op uw apparaten om end-to-end-codering met uw contact te verifiëren. No comment provided by engineer. + + Toggle chat list: + Chatlijst wisselen: + No comment provided by engineer. + Toggle incognito when connecting. Schakel incognito in tijdens het verbinden. No comment provided by engineer. + + Toolbar opacity + De transparantie van de werkbalk + No comment provided by engineer. + + + Total + Totaal + No comment provided by engineer. + Transport isolation Transport isolation No comment provided by engineer. + + Transport sessions + Transportsessies + No comment provided by engineer. + Trying to connect to the server used to receive messages from this contact (error: %@). Proberen verbinding te maken met de server die wordt gebruikt om berichten van dit contact te ontvangen (fout: %@). @@ -5863,11 +6863,6 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Lid deblokkeren? No comment provided by engineer. - - Unexpected error: %@ - Onverwachte fout: %@ - item status description - Unexpected migration state Onverwachte migratiestatus @@ -5876,7 +6871,7 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Unfav. Niet fav. - No comment provided by engineer. + swipe action Unhide @@ -5913,6 +6908,11 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Onbekende fout No comment provided by engineer. + + Unknown servers! + Onbekende servers! + No comment provided by engineer. + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. Schakel de modus Niet storen in om onderbrekingen te voorkomen, tenzij u de iOS-oproepinterface gebruikt. @@ -5948,12 +6948,12 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Unmute Dempen opheffen - No comment provided by engineer. + swipe action Unread Ongelezen - No comment provided by engineer. + swipe action Up to 100 last messages are sent to new members. @@ -5965,11 +6965,6 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Update No comment provided by engineer. - - Update .onion hosts setting? - .onion hosts-instelling updaten? - No comment provided by engineer. - Update database passphrase Database wachtwoord bijwerken @@ -5980,9 +6975,9 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Netwerk instellingen bijwerken? No comment provided by engineer. - - Update transport isolation mode? - Transportisolatiemodus updaten? + + Update settings? + Instellingen actualiseren? No comment provided by engineer. @@ -5990,16 +6985,16 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Door de instellingen bij te werken, wordt de client opnieuw verbonden met alle servers. No comment provided by engineer. - - Updating this setting will re-connect the client to all servers. - Als u deze instelling bijwerkt, wordt de client opnieuw verbonden met alle servers. - No comment provided by engineer. - Upgrade and open chat Upgrade en open chat No comment provided by engineer. + + Upload errors + Upload fouten + No comment provided by engineer. + Upload failed Upload mislukt @@ -6010,6 +7005,16 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Upload bestand server test step + + Uploaded + Geüpload + No comment provided by engineer. + + + Uploaded files + Geüploade bestanden + No comment provided by engineer. + Uploading archive Archief uploaden @@ -6060,6 +7065,16 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Alleen lokale meldingen gebruiken? No comment provided by engineer. + + Use private routing with unknown servers when IP address is not protected. + Gebruik privéroutering met onbekende servers wanneer het IP-adres niet beveiligd is. + No comment provided by engineer. + + + Use private routing with unknown servers. + Gebruik privéroutering met onbekende servers. + No comment provided by engineer. + Use server Gebruik server @@ -6070,14 +7085,19 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Gebruik de app tijdens het gesprek. No comment provided by engineer. + + Use the app with one hand. + Gebruik de app met één hand. + No comment provided by engineer. + User profile Gebruikers profiel No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - Het gebruik van .onion-hosts vereist een compatibele VPN-provider. + + User selection + Gebruikersselectie No comment provided by engineer. @@ -6210,6 +7230,16 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Wachten op video No comment provided by engineer. + + Wallpaper accent + Achtergrond accent + No comment provided by engineer. + + + Wallpaper background + Wallpaper achtergrond + No comment provided by engineer. + Warning: starting chat on multiple devices is not supported and will cause message delivery failures Waarschuwing: het starten van de chat op meerdere apparaten wordt niet ondersteund en zal leiden tot mislukte bezorging van berichten @@ -6232,12 +7262,12 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Welcome message - Welkomst bericht + Welkom bericht No comment provided by engineer. Welcome message is too long - Welkomstbericht is te lang + Welkom bericht is te lang No comment provided by engineer. @@ -6287,7 +7317,7 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak With optional welcome message. - Met optioneel welkomst bericht. + Met optioneel welkom bericht. No comment provided by engineer. @@ -6295,19 +7325,39 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Met verminderd batterijgebruik. No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + Zonder Tor of VPN is uw IP-adres zichtbaar voor bestandsservers. + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + Zonder Tor of VPN zal uw IP-adres zichtbaar zijn voor deze XFTP-relays: %@. + No comment provided by engineer. + Wrong database passphrase Verkeerd wachtwoord voor de database No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + Verkeerde sleutel of onbekende verbinding - hoogstwaarschijnlijk is deze verbinding verwijderd. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + Verkeerde sleutel of onbekend bestanddeeladres - hoogstwaarschijnlijk is het bestand verwijderd. + file error text + Wrong passphrase! Verkeerd wachtwoord! No comment provided by engineer. - - XFTP servers - XFTP servers + + XFTP server + XFTP server No comment provided by engineer. @@ -6387,11 +7437,21 @@ Deelnameverzoek herhalen? Je bent uitgenodigd voor de groep No comment provided by engineer. + + You are not connected to these servers. Private routing is used to deliver messages to them. + U bent niet verbonden met deze servers. Privéroutering wordt gebruikt om berichten bij hen af te leveren. + No comment provided by engineer. + You can accept calls from lock screen, without device and app authentication. U kunt oproepen van het vergrendelingsscherm accepteren, zonder apparaat- en app-verificatie. No comment provided by engineer. + + You can change it in Appearance settings. + U kunt dit wijzigen in de instellingen onder uiterlijk. + No comment provided by engineer. + You can create it later U kan het later maken @@ -6422,11 +7482,16 @@ Deelnameverzoek herhalen? Je kunt het via Instellingen zichtbaar maken voor je SimpleX contacten. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Je kunt nu berichten sturen naar %@ notification body + + You can send messages to %@ from Archived contacts. + U kunt berichten naar %@ sturen vanuit gearchiveerde contacten. + No comment provided by engineer. + You can set lock screen notification preview via settings. U kunt een voorbeeld van een melding op het vergrendeld scherm instellen via instellingen. @@ -6452,6 +7517,11 @@ Deelnameverzoek herhalen? U kunt de chat starten via app Instellingen / Database of door de app opnieuw op te starten No comment provided by engineer. + + You can still view conversation with %@ in the list of chats. + Je kunt het gesprek met %@ nog steeds bekijken in de lijst met chats. + No comment provided by engineer. + You can turn on SimpleX Lock via Settings. Je kunt SimpleX Vergrendeling aanzetten via Instellingen. @@ -6494,11 +7564,6 @@ Repeat connection request? Verbindingsverzoek herhalen? No comment provided by engineer. - - You have no chats - Je hebt geen gesprekken - No comment provided by engineer. - You have to enter passphrase every time the app starts - it is not stored on the device. U moet elke keer dat de app start het wachtwoord invoeren, deze wordt niet op het apparaat opgeslagen. @@ -6519,11 +7584,26 @@ Verbindingsverzoek herhalen? Je bent lid geworden van deze groep. Verbinding maken met uitnodigend groepslid. No comment provided by engineer. + + You may migrate the exported database. + U kunt de geëxporteerde database migreren. + No comment provided by engineer. + + + You may save the exported archive. + U kunt het geëxporteerde archief opslaan. + No comment provided by engineer. + You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. U mag ALLEEN de meest recente versie van uw chat database op één apparaat gebruiken, anders ontvangt u mogelijk geen berichten meer van sommige contacten. No comment provided by engineer. + + You need to allow your contact to call to be able to call them. + U moet uw contactpersoon toestemming geven om te bellen, zodat hij/zij je kan bellen. + No comment provided by engineer. + You need to allow your contact to send voice messages to be able to send them. U moet uw contact toestemming geven om spraak berichten te verzenden om ze te kunnen verzenden. @@ -6639,13 +7719,6 @@ Verbindingsverzoek herhalen? Uw chat profielen No comment provided by engineer. - - Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link). - Uw contact moet online zijn om de verbinding te voltooien. -U kunt deze verbinding verbreken en het contact verwijderen en later proberen met een nieuwe link. - No comment provided by engineer. - Your contact sent a file that is larger than currently supported maximum size (%@). Uw contact heeft een bestand verzonden dat groter is dan de momenteel ondersteunde maximale grootte (%@). @@ -6793,6 +7866,11 @@ SimpleX servers kunnen uw profiel niet zien. en %lld andere gebeurtenissen No comment provided by engineer. + + attempts + pogingen + No comment provided by engineer. + audio call (not e2e encrypted) audio oproep (niet e2e versleuteld) @@ -6833,6 +7911,11 @@ SimpleX servers kunnen uw profiel niet zien. vetgedrukt No comment provided by engineer. + + call + bellen + No comment provided by engineer. + call error oproepfout @@ -6983,6 +8066,11 @@ SimpleX servers kunnen uw profiel niet zien. dagen time unit + + decryption errors + decoderingsfouten + No comment provided by engineer. + default (%@) standaard (%@) @@ -7033,6 +8121,11 @@ SimpleX servers kunnen uw profiel niet zien. dubbel bericht integrity error chat item + + duplicates + duplicaten + No comment provided by engineer. + e2e encrypted e2e versleuteld @@ -7113,6 +8206,11 @@ SimpleX servers kunnen uw profiel niet zien. gebeurtenis gebeurd No comment provided by engineer. + + expired + verlopen + No comment provided by engineer. + forwarded doorgestuurd @@ -7143,6 +8241,11 @@ SimpleX servers kunnen uw profiel niet zien. iOS-keychain wordt gebruikt om het wachtwoord veilig op te slaan nadat u de app opnieuw hebt opgestart of het wachtwoord hebt gewijzigd, hiermee kunt u push meldingen ontvangen. No comment provided by engineer. + + inactive + inactief + No comment provided by engineer. + incognito via contact address link incognito via contact adres link @@ -7183,6 +8286,11 @@ SimpleX servers kunnen uw profiel niet zien. uitnodiging voor groep %@ group name + + invite + uitnodiging + No comment provided by engineer. + invited uitgenodigd @@ -7238,6 +8346,11 @@ SimpleX servers kunnen uw profiel niet zien. is toegetreden rcv group event chat item + + message + bericht + No comment provided by engineer. + message received bericht ontvangen @@ -7268,6 +8381,11 @@ SimpleX servers kunnen uw profiel niet zien. maanden time unit + + mute + dempen + No comment provided by engineer. + never nooit @@ -7320,6 +8438,16 @@ SimpleX servers kunnen uw profiel niet zien. aan group pref value + + other + overig + No comment provided by engineer. + + + other errors + overige fouten + No comment provided by engineer. + owner Eigenaar @@ -7390,6 +8518,11 @@ SimpleX servers kunnen uw profiel niet zien. opgeslagen van %@ No comment provided by engineer. + + search + zoekopdracht + No comment provided by engineer. + sec sec @@ -7415,6 +8548,15 @@ SimpleX servers kunnen uw profiel niet zien. stuur een direct bericht No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + informatie over serverwachtrij: %1$@ + +laatst ontvangen bericht: %2$@ + queue info + set new contact address nieuw contactadres instellen @@ -7455,11 +8597,26 @@ SimpleX servers kunnen uw profiel niet zien. onbekend connection info + + unknown servers + onbekende relays + No comment provided by engineer. + unknown status onbekende status No comment provided by engineer. + + unmute + dempen opheffen + No comment provided by engineer. + + + unprotected + onbeschermd + No comment provided by engineer. + updated group profile bijgewerkt groep profiel @@ -7500,6 +8657,11 @@ SimpleX servers kunnen uw profiel niet zien. via relay No comment provided by engineer. + + video + video + No comment provided by engineer. + video call (not e2e encrypted) video gesprek (niet e2e versleuteld) @@ -7525,6 +8687,11 @@ SimpleX servers kunnen uw profiel niet zien. weken time unit + + when IP hidden + wanneer IP verborgen is + No comment provided by engineer. + yes Ja @@ -7609,7 +8776,7 @@ SimpleX servers kunnen uw profiel niet zien.
- +
@@ -7646,7 +8813,7 @@ SimpleX servers kunnen uw profiel niet zien.
- +
@@ -7666,4 +8833,218 @@ SimpleX servers kunnen uw profiel niet zien.
+ +
+ +
+ + + SimpleX SE + SimpleX SE + Bundle display name + + + SimpleX SE + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + %@ + No comment provided by engineer. + + + App is locked! + App is vergrendeld! + No comment provided by engineer. + + + Cancel + Annuleren + No comment provided by engineer. + + + Cannot access keychain to save database password + Kan geen toegang krijgen tot de keychain om het database wachtwoord op te slaan + No comment provided by engineer. + + + Cannot forward message + Kan bericht niet doorsturen + No comment provided by engineer. + + + Comment + Opmerking + No comment provided by engineer. + + + Currently maximum supported file size is %@. + De momenteel maximaal ondersteunde bestandsgrootte is %@. + No comment provided by engineer. + + + Database downgrade required + Database downgrade vereist + No comment provided by engineer. + + + Database encrypted! + Database versleuteld! + No comment provided by engineer. + + + Database error + Database fout + No comment provided by engineer. + + + Database passphrase is different from saved in the keychain. + Het wachtwoord van de database verschilt van het wachtwoord die in de keychain is opgeslagen. + No comment provided by engineer. + + + Database passphrase is required to open chat. + Database wachtwoord is vereist om je gesprekken te openen. + No comment provided by engineer. + + + Database upgrade required + Database upgrade vereist + No comment provided by engineer. + + + Error preparing file + Fout bij voorbereiden bestand + No comment provided by engineer. + + + Error preparing message + Fout bij het voorbereiden van bericht + No comment provided by engineer. + + + Error: %@ + Fout: %@ + No comment provided by engineer. + + + File error + Bestandsfout + No comment provided by engineer. + + + Incompatible database version + Incompatibele database versie + No comment provided by engineer. + + + Invalid migration confirmation + Ongeldige migratie bevestiging + No comment provided by engineer. + + + Keychain error + Keychain fout + No comment provided by engineer. + + + Large file! + Groot bestand! + No comment provided by engineer. + + + No active profile + Geen actief profiel + No comment provided by engineer. + + + Ok + Ok + No comment provided by engineer. + + + Open the app to downgrade the database. + Open de app om de database te downgraden. + No comment provided by engineer. + + + Open the app to upgrade the database. + Open de app om de database te upgraden. + No comment provided by engineer. + + + Passphrase + Wachtwoord + No comment provided by engineer. + + + Please create a profile in the SimpleX app + Maak een profiel aan in de SimpleX app + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + Geselecteerde chat voorkeuren verbieden dit bericht. + No comment provided by engineer. + + + Sending a message takes longer than expected. + Het verzenden van een bericht duurt langer dan verwacht. + No comment provided by engineer. + + + Sending message… + Bericht versturen… + No comment provided by engineer. + + + Share + Deel + No comment provided by engineer. + + + Slow network? + Traag netwerk? + No comment provided by engineer. + + + Unknown database error: %@ + Onbekende database fout: %@ + No comment provided by engineer. + + + Unsupported format + Niet ondersteund formaat + No comment provided by engineer. + + + Wait + wachten + No comment provided by engineer. + + + Wrong database passphrase + Verkeerde database wachtwoord + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + U kunt delen toestaan in de instellingen voor Privacy en beveiliging / SimpleX Lock. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/contents.json b/apps/ios/SimpleX Localizations/nl.xcloc/contents.json index 20246f53d4..4c631c367e 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/nl.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "nl", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "15F31d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "15.4" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index d71bd5843a..525d30daa6 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -2,7 +2,7 @@
- +
@@ -127,11 +127,6 @@ %@ jest zweryfikowany No comment provided by engineer. - - %@ servers - %@ serwery - No comment provided by engineer. - %@ uploaded %@ wgrane @@ -557,16 +552,17 @@ O adresie SimpleX No comment provided by engineer. - - Accent color - Kolor akcentu + + Accent + Akcent No comment provided by engineer. Accept Akceptuj accept contact request via notification - accept incoming call via notification + accept incoming call via notification + swipe action Accept connection request? @@ -581,7 +577,23 @@ Accept incognito Akceptuj incognito - accept contact request via notification + accept contact request via notification + swipe action + + + Acknowledged + Potwierdzono + No comment provided by engineer. + + + Acknowledgement errors + Błędy potwierdzenia + No comment provided by engineer. + + + Active connections + Aktywne połączenia + No comment provided by engineer. Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. @@ -603,16 +615,16 @@ Dodaj profil No comment provided by engineer. + + Add server + Dodaj serwer + No comment provided by engineer. + Add servers by scanning QR codes. Dodaj serwery, skanując kody QR. No comment provided by engineer. - - Add server… - Dodaj serwer… - No comment provided by engineer. - Add to another device Dodaj do innego urządzenia @@ -623,6 +635,21 @@ Dodaj wiadomość powitalną No comment provided by engineer. + + Additional accent + Dodatkowy akcent + No comment provided by engineer. + + + Additional accent 2 + Dodatkowy akcent 2 + No comment provided by engineer. + + + Additional secondary + Dodatkowy drugorzędny + No comment provided by engineer. + Address Adres @@ -648,6 +675,11 @@ Zaawansowane ustawienia sieci No comment provided by engineer. + + Advanced settings + Zaawansowane ustawienia + No comment provided by engineer. + All app data is deleted. Wszystkie dane aplikacji są usunięte. @@ -663,6 +695,11 @@ Wszystkie dane są usuwane po jego wprowadzeniu. No comment provided by engineer. + + All data is private to your device. + Wszystkie dane są prywatne na Twoim urządzeniu. + No comment provided by engineer. + All group members will remain connected. Wszyscy członkowie grupy pozostaną połączeni. @@ -683,6 +720,11 @@ Wszystkie nowe wiadomości z %@ zostaną ukryte! No comment provided by engineer. + + All profiles + Wszystkie profile + No comment provided by engineer. + All your contacts will remain connected. Wszystkie Twoje kontakty pozostaną połączone. @@ -708,11 +750,21 @@ Zezwalaj na połączenia tylko wtedy, gdy Twój kontakt na to pozwala. No comment provided by engineer. + + Allow calls? + Zezwolić na połączenia? + No comment provided by engineer. + Allow disappearing messages only if your contact allows it to you. Zezwól na znikające wiadomości tylko wtedy, gdy Twój kontakt Ci na to pozwoli. No comment provided by engineer. + + Allow downgrade + Zezwól na obniżenie wersji + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Zezwalaj na nieodwracalne usuwanie wiadomości tylko wtedy, gdy Twój kontakt Ci na to pozwoli. (24 godziny) @@ -738,6 +790,11 @@ Zezwól na wysyłanie znikających wiadomości. No comment provided by engineer. + + Allow sharing + Zezwól na udostępnianie + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Zezwól na nieodwracalne usunięcie wysłanych wiadomości. (24 godziny) @@ -808,6 +865,11 @@ Już dołączono do grupy! No comment provided by engineer. + + Always use private routing. + Zawsze używaj prywatnego trasowania. + No comment provided by engineer. + Always use relay Zawsze używaj przekaźnika @@ -873,11 +935,26 @@ Zastosuj No comment provided by engineer. + + Apply to + Zastosuj dla + No comment provided by engineer. + Archive and upload Archiwizuj i prześlij No comment provided by engineer. + + Archive contacts to chat later. + Archiwizuj kontakty aby porozmawiać później. + No comment provided by engineer. + + + Archived contacts + Zarchiwizowane kontakty + No comment provided by engineer. + Archiving database Archiwizowanie bazy danych @@ -948,6 +1025,11 @@ Wstecz No comment provided by engineer. + + Background + Tło + No comment provided by engineer. + Bad desktop address Zły adres komputera @@ -973,6 +1055,16 @@ Lepsze wiadomości No comment provided by engineer. + + Better networking + Lepsze sieciowanie + No comment provided by engineer. + + + Black + Czarny + No comment provided by engineer. + Block Zablokuj @@ -1008,6 +1100,16 @@ Zablokowany przez admina No comment provided by engineer. + + Blur for better privacy. + Rozmycie dla lepszej prywatności. + No comment provided by engineer. + + + Blur media + Rozmycie mediów + No comment provided by engineer. + Both you and your contact can add message reactions. Zarówno Ty, jak i Twój kontakt możecie dodawać reakcje wiadomości. @@ -1053,11 +1155,26 @@ Połączenia No comment provided by engineer. + + Calls prohibited! + Połączenia zakazane! + No comment provided by engineer. + Camera not available Kamera nie dostępna No comment provided by engineer. + + Can't call contact + Nie można zadzwonić do kontaktu + No comment provided by engineer. + + + Can't call member + Nie można zadzwonić do członka + No comment provided by engineer. + Can't invite contact! Nie można zaprosić kontaktu! @@ -1068,6 +1185,11 @@ Nie można zaprosić kontaktów! No comment provided by engineer. + + Can't message member + Nie można wysłać wiadomości do członka + No comment provided by engineer. + Cancel Anuluj @@ -1083,11 +1205,21 @@ Nie można uzyskać dostępu do pęku kluczy, aby zapisać hasło do bazy danych No comment provided by engineer. + + Cannot forward message + Nie można przekazać wiadomości + No comment provided by engineer. + Cannot receive file Nie można odebrać pliku No comment provided by engineer. + + Capacity exceeded - recipient did not receive previously sent messages. + Przekroczono pojemność - odbiorca nie otrzymał wcześniej wysłanych wiadomości. + snd error text + Cellular Sieć komórkowa @@ -1149,6 +1281,11 @@ Archiwum czatu No comment provided by engineer. + + Chat colors + Kolory czatu + No comment provided by engineer. + Chat console Konsola czatu @@ -1164,6 +1301,11 @@ Baza danych czatu usunięta No comment provided by engineer. + + Chat database exported + Wyeksportowano bazę danych czatu + No comment provided by engineer. + Chat database imported Zaimportowano bazę danych czatu @@ -1184,6 +1326,11 @@ Czat został zatrzymany. Jeśli korzystałeś już z tej bazy danych na innym urządzeniu, powinieneś przenieść ją z powrotem przed rozpoczęciem czatu. No comment provided by engineer. + + Chat list + Lista czatów + No comment provided by engineer. + Chat migrated! Czat zmigrowany! @@ -1194,6 +1341,11 @@ Preferencje czatu No comment provided by engineer. + + Chat theme + Motyw czatu + No comment provided by engineer. + Chats Czaty @@ -1224,10 +1376,25 @@ Wybierz z biblioteki No comment provided by engineer. + + Chunks deleted + Fragmenty usunięte + No comment provided by engineer. + + + Chunks downloaded + Fragmenty pobrane + No comment provided by engineer. + + + Chunks uploaded + Fragmenty przesłane + No comment provided by engineer. + Clear Wyczyść - No comment provided by engineer. + swipe action Clear conversation @@ -1249,9 +1416,14 @@ Wyczyść weryfikację No comment provided by engineer. - - Colors - Kolory + + Color chats with the new themes. + Koloruj czaty z nowymi motywami. + No comment provided by engineer. + + + Color mode + Tryb koloru No comment provided by engineer. @@ -1264,11 +1436,21 @@ Porównaj kody bezpieczeństwa ze swoimi kontaktami. No comment provided by engineer. + + Completed + Zakończono + No comment provided by engineer. + Configure ICE servers Skonfiguruj serwery ICE No comment provided by engineer. + + Configured %@ servers + Skonfigurowano %@ serwerów + No comment provided by engineer. + Confirm Potwierdź @@ -1279,11 +1461,21 @@ Potwierdź Pin No comment provided by engineer. + + Confirm contact deletion? + Potwierdzić usunięcie kontaktu? + No comment provided by engineer. + Confirm database upgrades Potwierdź aktualizacje bazy danych No comment provided by engineer. + + Confirm files from unknown servers. + Potwierdzaj pliki z nieznanych serwerów. + No comment provided by engineer. + Confirm network settings Potwierdź ustawienia sieciowe @@ -1329,6 +1521,11 @@ Połącz do komputera No comment provided by engineer. + + Connect to your friends faster. + Szybciej łącz się ze znajomymi. + No comment provided by engineer. + Connect to yourself? Połączyć się ze sobą? @@ -1368,16 +1565,31 @@ To jest twój jednorazowy link! Połącz z %@ No comment provided by engineer. + + Connected + Połączony + No comment provided by engineer. + Connected desktop Połączony komputer No comment provided by engineer. + + Connected servers + Połączone serwery + No comment provided by engineer. + Connected to desktop Połączony do komputera No comment provided by engineer. + + Connecting + Łączenie + No comment provided by engineer. + Connecting to server… Łączenie z serwerem… @@ -1388,6 +1600,11 @@ To jest twój jednorazowy link! Łączenie z serwerem... (błąd: %@) No comment provided by engineer. + + Connecting to contact, please wait or check later! + Łączenie z kontaktem, poczekaj lub sprawdź później! + No comment provided by engineer. + Connecting to desktop Łączenie z komputerem @@ -1398,6 +1615,11 @@ To jest twój jednorazowy link! Połączenie No comment provided by engineer. + + Connection and servers status. + Stan połączenia i serwerów. + No comment provided by engineer. + Connection error Błąd połączenia @@ -1408,6 +1630,11 @@ To jest twój jednorazowy link! Błąd połączenia (UWIERZYTELNIANIE) No comment provided by engineer. + + Connection notifications + Powiadomienia o połączeniu + No comment provided by engineer. + Connection request sent! Prośba o połączenie wysłana! @@ -1423,6 +1650,16 @@ To jest twój jednorazowy link! Czas połączenia minął No comment provided by engineer. + + Connection with desktop stopped + Połączenie z komputerem zakończone + No comment provided by engineer. + + + Connections + Połączenia + No comment provided by engineer. + Contact allows Kontakt pozwala @@ -1433,6 +1670,11 @@ To jest twój jednorazowy link! Kontakt już istnieje No comment provided by engineer. + + Contact deleted! + Kontakt usunięty! + No comment provided by engineer. + Contact hidden: Kontakt ukryty: @@ -1443,9 +1685,9 @@ To jest twój jednorazowy link! Kontakt jest połączony notification - - Contact is not connected yet! - Kontakt nie jest jeszcze połączony! + + Contact is deleted. + Kontakt jest usunięty. No comment provided by engineer. @@ -1458,6 +1700,11 @@ To jest twój jednorazowy link! Preferencje kontaktu No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + Kontakt zostanie usunięty – nie można tego cofnąć! + No comment provided by engineer. + Contacts Kontakty @@ -1473,10 +1720,20 @@ To jest twój jednorazowy link! Kontynuuj No comment provided by engineer. + + Conversation deleted! + Rozmowa usunięta! + No comment provided by engineer. + Copy Kopiuj - chat item action + No comment provided by engineer. + + + Copy error + Kopiuj błąd + No comment provided by engineer. Core version: v%@ @@ -1553,6 +1810,11 @@ To jest twój jednorazowy link! Utwórz swój profil No comment provided by engineer. + + Created + Utworzono + No comment provided by engineer. + Created at Utworzony o @@ -1588,6 +1850,11 @@ To jest twój jednorazowy link! Obecne hasło… No comment provided by engineer. + + Current profile + Bieżący profil + No comment provided by engineer. + Currently maximum supported file size is %@. Obecnie maksymalna obsługiwana wielkość pliku wynosi %@. @@ -1598,11 +1865,21 @@ To jest twój jednorazowy link! Niestandardowy czas No comment provided by engineer. + + Customize theme + Dostosuj motyw + No comment provided by engineer. + Dark Ciemny No comment provided by engineer. + + Dark mode colors + Kolory ciemnego trybu + No comment provided by engineer. + Database ID ID bazy danych @@ -1701,6 +1978,11 @@ To jest twój jednorazowy link! Baza danych zostanie zmigrowana po ponownym uruchomieniu aplikacji No comment provided by engineer. + + Debug delivery + Dostarczenie debugowania + No comment provided by engineer. + Decentralized Zdecentralizowane @@ -1714,18 +1996,19 @@ To jest twój jednorazowy link! Delete Usuń - chat item action + chat item action + swipe action + + + Delete %lld messages of members? + Usunąć %lld wiadomości członków? + No comment provided by engineer. Delete %lld messages? Usunąć %lld wiadomości? No comment provided by engineer. - - Delete Contact - Usuń Kontakt - No comment provided by engineer. - Delete address Usuń adres @@ -1781,11 +2064,9 @@ To jest twój jednorazowy link! Usuń kontakt No comment provided by engineer. - - Delete contact? -This cannot be undone! - Usunąć kontakt? -To nie może być cofnięte! + + Delete contact? + Usunąć kontakt? No comment provided by engineer. @@ -1878,11 +2159,6 @@ To nie może być cofnięte! Usunąć starą bazę danych? No comment provided by engineer. - - Delete pending connection - Usuń oczekujące połączenie - No comment provided by engineer. - Delete pending connection? Usunąć oczekujące połączenie? @@ -1898,11 +2174,26 @@ To nie może być cofnięte! Usuń kolejkę server test step + + Delete up to 20 messages at once. + Usuń do 20 wiadomości na raz. + No comment provided by engineer. + Delete user profile? Usunąć profil użytkownika? No comment provided by engineer. + + Delete without notification + Usuń bez powiadomienia + No comment provided by engineer. + + + Deleted + Usunięto + No comment provided by engineer. + Deleted at Usunięto o @@ -1913,6 +2204,11 @@ To nie może być cofnięte! Usunięto o: %@ copied message info + + Deletion errors + Błędy usuwania + No comment provided by engineer. + Delivery Dostarczenie @@ -1948,11 +2244,41 @@ To nie może być cofnięte! Urządzenia komputerowe No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + Adres serwera docelowego %@ jest niekompatybilny z ustawieniami serwera przekazującego %@. + No comment provided by engineer. + + + Destination server error: %@ + Błąd docelowego serwera: %@ + snd error text + + + Destination server version of %@ is incompatible with forwarding server %@. + Wersja serwera docelowego %@ jest niekompatybilna z serwerem przekierowującym %@. + No comment provided by engineer. + + + Detailed statistics + Szczegółowe statystyki + No comment provided by engineer. + + + Details + Szczegóły + No comment provided by engineer. + Develop Deweloperskie No comment provided by engineer. + + Developer options + Opcje deweloperskie + No comment provided by engineer. + Developer tools Narzędzia deweloperskie @@ -2003,6 +2329,11 @@ To nie może być cofnięte! Wyłącz dla wszystkich No comment provided by engineer. + + Disabled + Wyłączony + No comment provided by engineer. + Disappearing message Znikająca wiadomość @@ -2053,11 +2384,21 @@ To nie może być cofnięte! Odkryj przez sieć lokalną No comment provided by engineer. + + Do NOT send messages directly, even if your or destination server does not support private routing. + NIE wysyłaj wiadomości bezpośrednio, nawet jeśli serwer docelowy nie obsługuje prywatnego trasowania. + No comment provided by engineer. + Do NOT use SimpleX for emergency calls. NIE używaj SimpleX do połączeń alarmowych. No comment provided by engineer. + + Do NOT use private routing. + NIE używaj prywatnego trasowania. + No comment provided by engineer. + Do it later Zrób to później @@ -2093,6 +2434,11 @@ To nie może być cofnięte! Pobierz chat item action + + Download errors + Błędy pobierania + No comment provided by engineer. + Download failed Pobieranie nie udane @@ -2103,6 +2449,16 @@ To nie może być cofnięte! Pobierz plik server test step + + Downloaded + Pobrane + No comment provided by engineer. + + + Downloaded files + Pobrane pliki + No comment provided by engineer. + Downloading archive Pobieranie archiwum @@ -2203,6 +2559,11 @@ To nie może być cofnięte! Włącz pin samodestrukcji set passcode view + + Enabled + Włączony + No comment provided by engineer. + Enabled for Włączony dla @@ -2373,6 +2734,11 @@ To nie może być cofnięte! Błąd zmiany ustawienia No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + Błąd połączenia z serwerem przekierowania %@. Spróbuj ponownie później. + No comment provided by engineer. + Error creating address Błąd tworzenia adresu @@ -2423,11 +2789,6 @@ To nie może być cofnięte! Błąd usuwania połączenia No comment provided by engineer. - - Error deleting contact - Błąd usuwania kontaktu - No comment provided by engineer. - Error deleting database Błąd usuwania bazy danych @@ -2473,6 +2834,11 @@ To nie może być cofnięte! Błąd eksportu bazy danych czatu No comment provided by engineer. + + Error exporting theme: %@ + Błąd eksportowania motywu: %@ + No comment provided by engineer. + Error importing chat database Błąd importu bazy danych czatu @@ -2498,11 +2864,26 @@ To nie może być cofnięte! Błąd odbioru pliku No comment provided by engineer. + + Error reconnecting server + Błąd ponownego łączenia z serwerem + No comment provided by engineer. + + + Error reconnecting servers + Błąd ponownego łączenia serwerów + No comment provided by engineer. + Error removing member Błąd usuwania członka No comment provided by engineer. + + Error resetting statistics + Błąd resetowania statystyk + No comment provided by engineer. + Error saving %@ servers Błąd zapisu %@ serwerów @@ -2621,7 +3002,8 @@ To nie może być cofnięte! Error: %@ Błąd: %@ - No comment provided by engineer. + file error text + snd error text Error: URL is invalid @@ -2633,6 +3015,11 @@ To nie może być cofnięte! Błąd: brak pliku bazy danych No comment provided by engineer. + + Errors + Błędy + No comment provided by engineer. + Even when disabled in the conversation. Nawet po wyłączeniu w rozmowie. @@ -2658,6 +3045,11 @@ To nie może być cofnięte! Błąd eksportu: No comment provided by engineer. + + Export theme + Eksportuj motyw + No comment provided by engineer. + Exported database archive. Wyeksportowane archiwum bazy danych. @@ -2691,8 +3083,33 @@ To nie może być cofnięte! Favorite Ulubione + swipe action + + + File error + Błąd pliku No comment provided by engineer. + + File not found - most likely file was deleted or cancelled. + Nie odnaleziono pliku - najprawdopodobniej plik został usunięty lub anulowany. + file error text + + + File server error: %@ + Błąd serwera plików: %@ + file error text + + + File status + Status pliku + No comment provided by engineer. + + + File status: %@ + Status pliku: %@ + copied message info + File will be deleted from servers. Plik zostanie usunięty z serwerów. @@ -2713,6 +3130,11 @@ To nie może być cofnięte! Plik: %@ No comment provided by engineer. + + Files + Pliki + No comment provided by engineer. + Files & media Pliki i media @@ -2818,6 +3240,35 @@ To nie może być cofnięte! Przekazane dalej od No comment provided by engineer. + + Forwarding server %@ failed to connect to destination server %@. Please try later. + Serwer przekazujący %@ nie mógł połączyć się z serwerem docelowym %@. Spróbuj ponownie później. + No comment provided by engineer. + + + Forwarding server address is incompatible with network settings: %@. + Adres serwera przekierowującego jest niekompatybilny z ustawieniami sieciowymi: %@. + No comment provided by engineer. + + + Forwarding server version is incompatible with network settings: %@. + Wersja serwera przekierowującego jest niekompatybilna z ustawieniami sieciowymi: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + Serwer przekazujący: %1$@ +Błąd serwera docelowego: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + Serwer przekazujący: %1$@ +Błąd: %2$@ + snd error text + Found desktop Znaleziono komputer @@ -2863,6 +3314,16 @@ To nie może być cofnięte! GIF-y i naklejki No comment provided by engineer. + + Good afternoon! + Dzień dobry! + message preview + + + Good morning! + Dzień dobry! + message preview + Group Grupa @@ -3143,6 +3604,11 @@ To nie może być cofnięte! Import nie udał się No comment provided by engineer. + + Import theme + Importuj motyw + No comment provided by engineer. + Importing archive Importowanie archiwum @@ -3265,6 +3731,11 @@ To nie może być cofnięte! Interfejs No comment provided by engineer. + + Interface colors + Kolory interfejsu + No comment provided by engineer. + Invalid QR code Nieprawidłowy kod QR @@ -3366,6 +3837,11 @@ To nie może być cofnięte! 3. Połączenie zostało skompromitowane. No comment provided by engineer. + + It protects your IP address and connections. + Chroni Twój adres IP i połączenia. + No comment provided by engineer. + It seems like you are already connected via this link. If it is not the case, there was an error (%@). Wygląda na to, że jesteś już połączony przez ten link. Jeśli tak nie jest, wystąpił błąd (%@). @@ -3384,7 +3860,7 @@ To nie może być cofnięte! Join Dołącz - No comment provided by engineer. + swipe action Join group @@ -3428,6 +3904,11 @@ To jest twój link do grupy %@! Zachowaj No comment provided by engineer. + + Keep conversation + Zachowaj rozmowę + No comment provided by engineer. + Keep the app open to use it from desktop Zostaw aplikację otwartą i używaj ją z komputera @@ -3471,7 +3952,7 @@ To jest twój link do grupy %@! Leave Opuść - No comment provided by engineer. + swipe action Leave group @@ -3603,11 +4084,26 @@ To jest twój link do grupy %@! Maksymalnie 30 sekund, odbierane natychmiast. No comment provided by engineer. + + Media & file servers + Serwery mediów i plików + No comment provided by engineer. + + + Medium + Średni + blur media + Member Członek No comment provided by engineer. + + Member inactive + Członek nieaktywny + item status text + Member role will be changed to "%@". All group members will be notified. Rola członka grupy zostanie zmieniona na "%@". Wszyscy członkowie grupy zostaną powiadomieni. @@ -3623,6 +4119,11 @@ To jest twój link do grupy %@! Członek zostanie usunięty z grupy - nie można tego cofnąć! No comment provided by engineer. + + Menus + Menu + No comment provided by engineer. + Message delivery error Błąd dostarczenia wiadomości @@ -3633,11 +4134,31 @@ To jest twój link do grupy %@! Potwierdzenia dostarczenia wiadomości! No comment provided by engineer. + + Message delivery warning + Ostrzeżenie dostarczenia wiadomości + item status text + Message draft Wersja robocza wiadomości No comment provided by engineer. + + Message forwarded + Wiadomość przekazana + item status text + + + Message may be delivered later if member becomes active. + Wiadomość może zostać dostarczona później jeśli członek stanie się aktywny. + item status description + + + Message queue info + Informacje kolejki wiadomości + No comment provided by engineer. + Message reactions Reakcje wiadomości @@ -3653,11 +4174,31 @@ To jest twój link do grupy %@! Reakcje wiadomości są zabronione w tej grupie. No comment provided by engineer. + + Message reception + Odebranie wiadomości + No comment provided by engineer. + + + Message servers + Serwery wiadomości + No comment provided by engineer. + Message source remains private. Źródło wiadomości pozostaje prywatne. No comment provided by engineer. + + Message status + Status wiadomości + No comment provided by engineer. + + + Message status: %@ + Status wiadomości: %@ + copied message info + Message text Tekst wiadomości @@ -3683,6 +4224,16 @@ To jest twój link do grupy %@! Wiadomości od %@ zostaną pokazane! No comment provided by engineer. + + Messages received + Otrzymane wiadomości + No comment provided by engineer. + + + Messages sent + Wysłane wiadomości + No comment provided by engineer. + Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. Wiadomości, pliki i połączenia są chronione przez **szyfrowanie end-to-end** z doskonałym utajnianiem z wyprzedzeniem i odzyskiem po złamaniu. @@ -3783,11 +4334,6 @@ To jest twój link do grupy %@! Najprawdopodobniej to połączenie jest usunięte. item status description - - Most likely this contact has deleted the connection with you. - Najprawdopodobniej ten kontakt usunął połączenie z Tobą. - No comment provided by engineer. - Multiple chat profiles Wiele profili czatu @@ -3796,7 +4342,7 @@ To jest twój link do grupy %@! Mute Wycisz - No comment provided by engineer. + swipe action Muted when inactive! @@ -3806,7 +4352,7 @@ To jest twój link do grupy %@! Name Nazwa - No comment provided by engineer. + swipe action Network & servers @@ -3818,6 +4364,11 @@ To jest twój link do grupy %@! Połączenie z siecią No comment provided by engineer. + + Network issues - message expired after many attempts to send it. + Błąd sieciowy - wiadomość wygasła po wielu próbach wysłania jej. + snd error text + Network management Zarządzenie sieciowe @@ -3843,6 +4394,11 @@ To jest twój link do grupy %@! Nowy czat No comment provided by engineer. + + New chat experience 🎉 + Nowe możliwości czatu 🎉 + No comment provided by engineer. + New contact request Nowa prośba o kontakt @@ -3873,6 +4429,11 @@ To jest twój link do grupy %@! Nowość w %@ No comment provided by engineer. + + New media options + Nowe opcje mediów + No comment provided by engineer. + New member role Nowa rola członka @@ -3918,6 +4479,11 @@ To jest twój link do grupy %@! Brak tokenu urządzenia! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + Brak bezpośredniego połączenia, wiadomość została przekazana przez administratora. + item status description + No filtered chats Brak filtrowanych czatów @@ -3933,6 +4499,11 @@ To jest twój link do grupy %@! Brak historii No comment provided by engineer. + + No info, try to reload + Brak informacji, spróbuj przeładować + No comment provided by engineer. + No network connection Brak połączenia z siecią @@ -3953,6 +4524,11 @@ To jest twój link do grupy %@! Nie kompatybilny! No comment provided by engineer. + + Nothing selected + Nic nie jest zaznaczone + No comment provided by engineer. + Notifications Powiadomienia @@ -3980,7 +4556,7 @@ To jest twój link do grupy %@! Off Wyłączony - No comment provided by engineer. + blur media Ok @@ -4002,14 +4578,18 @@ To jest twój link do grupy %@! Jednorazowy link zaproszenia No comment provided by engineer. - - Onion hosts will be required for connection. Requires enabling VPN. - Hosty onion będą wymagane do połączenia. Wymaga włączenia VPN. + + Onion hosts will be **required** for connection. +Requires compatible VPN. + Hosty onion będą wymagane do połączenia. +Wymaga włączenia VPN. No comment provided by engineer. - - Onion hosts will be used when available. Requires enabling VPN. - Hosty onion będą używane, gdy będą dostępne. Wymaga włączenia VPN. + + Onion hosts will be used when available. +Requires compatible VPN. + Hosty onion będą używane, gdy będą dostępne. +Wymaga włączenia VPN. No comment provided by engineer. @@ -4022,6 +4602,11 @@ To jest twój link do grupy %@! Tylko urządzenia klienckie przechowują profile użytkowników, kontakty, grupy i wiadomości wysyłane za pomocą **2-warstwowego szyfrowania end-to-end**. No comment provided by engineer. + + Only delete conversation + Usuń tylko rozmowę + No comment provided by engineer. + Only group owners can change group preferences. Tylko właściciele grup mogą zmieniać preferencje grupy. @@ -4117,6 +4702,11 @@ To jest twój link do grupy %@! Otwórz migrację na innym urządzeniu authentication reason + + Open server settings + Otwórz ustawienia serwera + No comment provided by engineer. + Open user profiles Otwórz profile użytkownika @@ -4157,6 +4747,11 @@ To jest twój link do grupy %@! Inne No comment provided by engineer. + + Other %@ servers + Inne %@ serwery + No comment provided by engineer. + PING count Liczba PINGÓW @@ -4222,6 +4817,11 @@ To jest twój link do grupy %@! Wklej link, który otrzymałeś No comment provided by engineer. + + Pending + Oczekujące + No comment provided by engineer. + People can connect to you only via the links you share. Ludzie mogą się z Tobą połączyć tylko poprzez linki, które udostępniasz. @@ -4242,11 +4842,28 @@ To jest twój link do grupy %@! Połączenia obraz-w-obrazie No comment provided by engineer. + + Play from the chat list. + Odtwórz z listy czatów. + No comment provided by engineer. + + + Please ask your contact to enable calls. + Poproś kontakt o włącznie połączeń. + No comment provided by engineer. + Please ask your contact to enable sending voice messages. Poproś Twój kontakt o włączenie wysyłania wiadomości głosowych. No comment provided by engineer. + + Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers. + Sprawdź, czy telefon i komputer są podłączone do tej samej sieci lokalnej i czy zapora sieciowa komputera umożliwia połączenie. +Proszę podzielić się innymi problemami z deweloperami. + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. Sprawdź, czy użyłeś prawidłowego linku lub poproś Twój kontakt o przesłanie innego. @@ -4344,6 +4961,11 @@ Błąd: %@ Podgląd No comment provided by engineer. + + Previously connected servers + Wcześniej połączone serwery + No comment provided by engineer. + Privacy & security Prywatność i bezpieczeństwo @@ -4359,11 +4981,31 @@ Błąd: %@ Prywatne nazwy plików No comment provided by engineer. + + Private message routing + Trasowanie prywatnych wiadomości + No comment provided by engineer. + + + Private message routing 🚀 + Trasowanie prywatnych wiadomości🚀 + No comment provided by engineer. + Private notes Prywatne notatki name of notes to self + + Private routing + Prywatne trasowanie + No comment provided by engineer. + + + Private routing error + Błąd prywatnego trasowania + No comment provided by engineer. + Profile and server connections Profil i połączenia z serwerem @@ -4394,6 +5036,11 @@ Błąd: %@ Hasło profilu No comment provided by engineer. + + Profile theme + Motyw profilu + No comment provided by engineer. + Profile update will be sent to your contacts. Aktualizacja profilu zostanie wysłana do Twoich kontaktów. @@ -4444,11 +5091,23 @@ Błąd: %@ Zabroń wysyłania wiadomości głosowych. No comment provided by engineer. + + Protect IP address + Chroń adres IP + No comment provided by engineer. + Protect app screen Chroń ekran aplikacji No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + Chroni Twój adres IP przed przekaźnikami wiadomości wybranych przez Twoje kontakty. +Włącz w ustawianiach *Sieć i serwery* . + No comment provided by engineer. + Protect your chat profiles with a password! Chroń swoje profile czatu hasłem! @@ -4464,6 +5123,16 @@ Błąd: %@ Limit czasu protokołu na KB No comment provided by engineer. + + Proxied + Trasowane przez proxy + No comment provided by engineer. + + + Proxied servers + Serwery trasowane przez proxy + No comment provided by engineer. + Push notifications Powiadomienia push @@ -4484,6 +5153,11 @@ Błąd: %@ Oceń aplikację No comment provided by engineer. + + Reachable chat toolbar + Osiągalny pasek narzędzi czatu + No comment provided by engineer. + React… Reaguj… @@ -4492,7 +5166,7 @@ Błąd: %@ Read Czytaj - No comment provided by engineer. + swipe action Read more @@ -4529,6 +5203,11 @@ Błąd: %@ Potwierdzenia są wyłączone No comment provided by engineer. + + Receive errors + Błędy otrzymania + No comment provided by engineer. + Received at Otrzymane o @@ -4549,16 +5228,26 @@ Błąd: %@ Otrzymano wiadomość message info title + + Received messages + Otrzymane wiadomości + No comment provided by engineer. + + + Received reply + Otrzymano odpowiedź + No comment provided by engineer. + + + Received total + Otrzymano łącznie + No comment provided by engineer. + Receiving address will be changed to a different server. Address change will complete after sender comes online. Adres odbiorczy zostanie zmieniony na inny serwer. Zmiana adresu zostanie zakończona gdy nadawca będzie online. No comment provided by engineer. - - Receiving concurrency - Konkurencyjne odbieranie - No comment provided by engineer. - Receiving file will be stopped. Odbieranie pliku zostanie przerwane. @@ -4584,11 +5273,36 @@ Błąd: %@ Odbiorcy widzą aktualizacje podczas ich wpisywania. No comment provided by engineer. + + Reconnect + Połącz ponownie + No comment provided by engineer. + Reconnect all connected servers to force message delivery. It uses additional traffic. Połącz ponownie wszystkie połączone serwery, aby wymusić dostarczanie wiadomości. Wykorzystuje dodatkowy ruch. No comment provided by engineer. + + Reconnect all servers + Połącz ponownie wszystkie serwery + No comment provided by engineer. + + + Reconnect all servers? + Połączyć ponownie wszystkie serwery? + No comment provided by engineer. + + + Reconnect server to force message delivery. It uses additional traffic. + Ponownie połącz z serwerem w celu wymuszenia dostarczenia wiadomości. Wykorzystuje to dodatkowy ruch. + No comment provided by engineer. + + + Reconnect server? + Połączyć ponownie serwer? + No comment provided by engineer. + Reconnect servers? Ponownie połączyć serwery? @@ -4612,7 +5326,8 @@ Błąd: %@ Reject Odrzuć - reject incoming call via notification + reject incoming call via notification + swipe action Reject (sender NOT notified) @@ -4639,6 +5354,11 @@ Błąd: %@ Usuń No comment provided by engineer. + + Remove image + Usuń obraz + No comment provided by engineer. + Remove member Usuń członka @@ -4709,16 +5429,41 @@ Błąd: %@ Resetuj No comment provided by engineer. + + Reset all hints + Zresetuj wszystkie wskazówki + No comment provided by engineer. + + + Reset all statistics + Resetuj wszystkie statystyki + No comment provided by engineer. + + + Reset all statistics? + Zresetować wszystkie statystyki? + No comment provided by engineer. + Reset colors Resetuj kolory No comment provided by engineer. + + Reset to app theme + Zresetuj do motywu aplikacji + No comment provided by engineer. + Reset to defaults Przywróć wartości domyślne No comment provided by engineer. + + Reset to user theme + Zresetuj do motywu użytkownika + No comment provided by engineer. + Restart the app to create a new chat profile Uruchom ponownie aplikację, aby utworzyć nowy profil czatu @@ -4759,11 +5504,6 @@ Błąd: %@ Ujawnij chat item action - - Revert - Przywrócić - No comment provided by engineer. - Revoke Odwołaj @@ -4789,9 +5529,14 @@ Błąd: %@ Uruchom czat No comment provided by engineer. - - SMP servers - Serwery SMP + + SMP server + Serwer SMP + No comment provided by engineer. + + + Safely receive files + Bezpiecznie otrzymuj pliki No comment provided by engineer. @@ -4819,6 +5564,11 @@ Błąd: %@ Zapisz i powiadom członków grupy No comment provided by engineer. + + Save and reconnect + Zapisz i połącz ponownie + No comment provided by engineer. + Save and update group profile Zapisz i zaktualizuj profil grupowy @@ -4899,6 +5649,16 @@ Błąd: %@ Zachowano wiadomość message info title + + Scale + Skaluj + No comment provided by engineer. + + + Scan / Paste link + Skanuj / Wklej link + No comment provided by engineer. + Scan QR code Zeskanuj kod QR @@ -4939,11 +5699,21 @@ Błąd: %@ Wyszukaj lub wklej link SimpleX No comment provided by engineer. + + Secondary + Drugorzędny + No comment provided by engineer. + Secure queue Bezpieczna kolejka server test step + + Secured + Zabezpieczone + No comment provided by engineer. + Security assessment Ocena bezpieczeństwa @@ -4957,6 +5727,16 @@ Błąd: %@ Select Wybierz + chat item action + + + Selected %lld + Zaznaczono %lld + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + Wybrane preferencje czatu zabraniają tej wiadomości. No comment provided by engineer. @@ -4994,11 +5774,6 @@ Błąd: %@ Wyślij potwierdzenia dostawy do No comment provided by engineer. - - Send direct message - Wyślij wiadomość bezpośrednią - No comment provided by engineer. - Send direct message to connect Wyślij wiadomość bezpośrednią aby połączyć @@ -5009,6 +5784,11 @@ Błąd: %@ Wyślij znikającą wiadomość No comment provided by engineer. + + Send errors + Wyślij błędy + No comment provided by engineer. + Send link previews Wyślij podgląd linku @@ -5019,6 +5799,21 @@ Błąd: %@ Wyślij wiadomość na żywo No comment provided by engineer. + + Send message to enable calls. + Wyślij wiadomość aby włączyć połączenia. + No comment provided by engineer. + + + Send messages directly when IP address is protected and your or destination server does not support private routing. + Wysyłaj wiadomości bezpośrednio, gdy adres IP jest chroniony i Twój lub docelowy serwer nie obsługuje prywatnego trasowania. + No comment provided by engineer. + + + Send messages directly when your or destination server does not support private routing. + Wysyłaj wiadomości bezpośrednio, gdy Twój lub docelowy serwer nie obsługuje prywatnego trasowania. + No comment provided by engineer. + Send notifications Wyślij powiadomienia @@ -5109,6 +5904,11 @@ Błąd: %@ Wysłano o: %@ copied message info + + Sent directly + Wysłano bezpośrednio + No comment provided by engineer. + Sent file event Wyślij zdarzenie pliku @@ -5119,11 +5919,46 @@ Błąd: %@ Wyślij wiadomość message info title + + Sent messages + Wysłane wiadomości + No comment provided by engineer. + Sent messages will be deleted after set time. Wysłane wiadomości zostaną usunięte po ustawionym czasie. No comment provided by engineer. + + Sent reply + Wyślij odpowiedź + No comment provided by engineer. + + + Sent total + Wysłano łącznie + No comment provided by engineer. + + + Sent via proxy + Wysłano przez proxy + No comment provided by engineer. + + + Server address + Adres serwera + No comment provided by engineer. + + + Server address is incompatible with network settings. + Adres serwera jest niekompatybilny z ustawieniami sieciowymi. + srv error text. + + + Server address is incompatible with network settings: %@. + Adres serwera jest niekompatybilny z ustawieniami sieci: %@. + No comment provided by engineer. + Server requires authorization to create queues, check password Serwer wymaga autoryzacji do tworzenia kolejek, sprawdź hasło @@ -5139,11 +5974,36 @@ Błąd: %@ Test serwera nie powiódł się! No comment provided by engineer. + + Server type + Typ serwera + No comment provided by engineer. + + + Server version is incompatible with network settings. + Wersja serwera jest niekompatybilna z ustawieniami sieciowymi. + srv error text + + + Server version is incompatible with your app: %@. + Wersja serwera jest niekompatybilna z aplikacją: %@. + No comment provided by engineer. + Servers Serwery No comment provided by engineer. + + Servers info + Informacje o serwerach + No comment provided by engineer. + + + Servers statistics will be reset - this cannot be undone! + Statystyki serwerów zostaną zresetowane - nie można tego cofnąć! + No comment provided by engineer. + Session code Kod sesji @@ -5159,6 +6019,11 @@ Błąd: %@ Ustaw nazwę kontaktu… No comment provided by engineer. + + Set default theme + Ustaw domyślny motyw + No comment provided by engineer. + Set group preferences Ustaw preferencje grupy @@ -5224,6 +6089,11 @@ Błąd: %@ Udostępnić adres kontaktom? No comment provided by engineer. + + Share from other apps. + Udostępnij z innych aplikacji. + No comment provided by engineer. + Share link Udostępnij link @@ -5234,6 +6104,11 @@ Błąd: %@ Udostępnij ten jednorazowy link No comment provided by engineer. + + Share to SimpleX + Udostępnij do SimpleX + No comment provided by engineer. + Share with contacts Udostępnij kontaktom @@ -5259,16 +6134,36 @@ Błąd: %@ Pokaż ostatnie wiadomości No comment provided by engineer. + + Show message status + Pokaż status wiadomości + No comment provided by engineer. + + + Show percentage + Pokaż procent + No comment provided by engineer. + Show preview Pokaż podgląd No comment provided by engineer. + + Show → on messages sent via private routing. + Pokaż → na wiadomościach wysłanych przez prywatne trasowanie. + No comment provided by engineer. + Show: Pokaż: No comment provided by engineer. + + SimpleX + SimpleX + No comment provided by engineer. + SimpleX Address Adres SimpleX @@ -5344,6 +6239,11 @@ Błąd: %@ Uproszczony tryb incognito No comment provided by engineer. + + Size + Rozmiar + No comment provided by engineer. + Skip Pomiń @@ -5359,11 +6259,26 @@ Błąd: %@ Małe grupy (maks. 20) No comment provided by engineer. + + Soft + Łagodny + blur media + + + Some file(s) were not exported: + Niektóre plik(i) nie zostały wyeksportowane: + No comment provided by engineer. + Some non-fatal errors occurred during import - you may see Chat console for more details. Podczas importu wystąpiły niekrytyczne błędy - więcej szczegółów można znaleźć w konsoli czatu. No comment provided by engineer. + + Some non-fatal errors occurred during import: + Podczas importu wystąpiły niekrytyczne błędy: + No comment provided by engineer. + Somebody Ktoś @@ -5389,6 +6304,16 @@ Błąd: %@ Rozpocznij migrację No comment provided by engineer. + + Starting from %@. + Zaczynanie od %@. + No comment provided by engineer. + + + Statistics + Statystyki + No comment provided by engineer. + Stop Zatrzymaj @@ -5449,11 +6374,31 @@ Błąd: %@ Zatrzymywanie czatu No comment provided by engineer. + + Strong + Silne + blur media + Submit Zatwierdź No comment provided by engineer. + + Subscribed + Zasubskrybowano + No comment provided by engineer. + + + Subscription errors + Błędy subskrypcji + No comment provided by engineer. + + + Subscriptions ignored + Subskrypcje zignorowane + No comment provided by engineer. + Support SimpleX Chat Wspieraj SimpleX Chat @@ -5469,6 +6414,11 @@ Błąd: %@ Uwierzytelnianie systemu No comment provided by engineer. + + TCP connection + Połączenie TCP + No comment provided by engineer. + TCP connection timeout Limit czasu połączenia TCP @@ -5529,9 +6479,9 @@ Błąd: %@ Dotknij, aby zeskanować No comment provided by engineer. - - Tap to start a new chat - Dotknij, aby rozpocząć nowy czat + + Temporary file error + Tymczasowy błąd pliku No comment provided by engineer. @@ -5586,6 +6536,11 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Aplikacja może powiadamiać Cię, gdy otrzymujesz wiadomości lub prośby o kontakt — otwórz ustawienia, aby włączyć. No comment provided by engineer. + + The app will ask to confirm downloads from unknown file servers (except .onion). + Aplikacja zapyta o potwierdzenie pobierania od nieznanych serwerów plików (poza .onion). + No comment provided by engineer. + The attempt to change database passphrase was not completed. Próba zmiany hasła bazy danych nie została zakończona. @@ -5631,6 +6586,16 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Wiadomość zostanie oznaczona jako moderowana dla wszystkich członków. No comment provided by engineer. + + The messages will be deleted for all members. + Wiadomości zostaną usunięte dla wszystkich członków. + No comment provided by engineer. + + + The messages will be marked as moderated for all members. + Wiadomości zostaną oznaczone jako moderowane dla wszystkich członków. + No comment provided by engineer. + The next generation of private messaging Następna generacja prywatnych wiadomości @@ -5666,9 +6631,9 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Tekst, który wkleiłeś nie jest linkiem SimpleX. No comment provided by engineer. - - Theme - Motyw + + Themes + Motywy No comment provided by engineer. @@ -5736,11 +6701,21 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom To jest twój jednorazowy link! No comment provided by engineer. + + This link was used with another mobile device, please create a new link on the desktop. + Ten link dostał użyty z innym urządzeniem mobilnym, proszę stworzyć nowy link na komputerze. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. To ustawienie dotyczy wiadomości Twojego bieżącego profilu czatu **%@**. No comment provided by engineer. + + Title + Tytuł + No comment provided by engineer. + To ask any questions and to receive updates: Aby zadać wszelkie pytania i otrzymywać aktualizacje: @@ -5771,6 +6746,11 @@ Może się to zdarzyć z powodu jakiegoś błędu lub gdy połączenie jest skom Aby chronić strefę czasową, pliki obrazów/głosów używają UTC. No comment provided by engineer. + + To protect your IP address, private routing uses your SMP servers to deliver messages. + Aby chronić Twój adres IP, prywatne trasowanie używa Twoich serwerów SMP, aby dostarczyć wiadomości. + No comment provided by engineer. + To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5798,16 +6778,36 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.Aby zweryfikować szyfrowanie end-to-end z Twoim kontaktem porównaj (lub zeskanuj) kod na waszych urządzeniach. No comment provided by engineer. + + Toggle chat list: + Przełącz listę czatów: + No comment provided by engineer. + Toggle incognito when connecting. Przełącz incognito przy połączeniu. No comment provided by engineer. + + Toolbar opacity + Nieprzezroczystość paska narzędzi + No comment provided by engineer. + + + Total + Łącznie + No comment provided by engineer. + Transport isolation Izolacja transportu No comment provided by engineer. + + Transport sessions + Sesje transportowe + No comment provided by engineer. + Trying to connect to the server used to receive messages from this contact (error: %@). Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu (błąd: %@). @@ -5863,11 +6863,6 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.Odblokować członka? No comment provided by engineer. - - Unexpected error: %@ - Nieoczekiwany błąd: %@ - item status description - Unexpected migration state Nieoczekiwany stan migracji @@ -5876,7 +6871,7 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania. Unfav. Nie ulub. - No comment provided by engineer. + swipe action Unhide @@ -5913,6 +6908,11 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.Nieznany błąd No comment provided by engineer. + + Unknown servers! + Nieznane serwery! + No comment provided by engineer. + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. O ile nie korzystasz z interfejsu połączeń systemu iOS, włącz tryb Nie przeszkadzać, aby uniknąć przerywania. @@ -5948,12 +6948,12 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Unmute Wyłącz wyciszenie - No comment provided by engineer. + swipe action Unread Nieprzeczytane - No comment provided by engineer. + swipe action Up to 100 last messages are sent to new members. @@ -5965,11 +6965,6 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Aktualizuj No comment provided by engineer. - - Update .onion hosts setting? - Zaktualizować ustawienie hostów .onion? - No comment provided by engineer. - Update database passphrase Aktualizuj hasło do bazy danych @@ -5980,9 +6975,9 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Zaktualizować ustawienia sieci? No comment provided by engineer. - - Update transport isolation mode? - Zaktualizować tryb izolacji transportu? + + Update settings? + Zaktualizować ustawienia? No comment provided by engineer. @@ -5990,16 +6985,16 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Aktualizacja ustawień spowoduje ponowne połączenie klienta ze wszystkimi serwerami. No comment provided by engineer. - - Updating this setting will re-connect the client to all servers. - Aktualizacja tych ustawień spowoduje ponowne połączenie klienta ze wszystkimi serwerami. - No comment provided by engineer. - Upgrade and open chat Zaktualizuj i otwórz czat No comment provided by engineer. + + Upload errors + Błędy przesłania + No comment provided by engineer. + Upload failed Wgrywanie nie udane @@ -6010,6 +7005,16 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Prześlij plik server test step + + Uploaded + Przesłane + No comment provided by engineer. + + + Uploaded files + Przesłane pliki + No comment provided by engineer. + Uploading archive Wgrywanie archiwum @@ -6060,6 +7065,16 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Używać tylko lokalnych powiadomień? No comment provided by engineer. + + Use private routing with unknown servers when IP address is not protected. + Używaj prywatnego trasowania z nieznanymi serwerami, gdy adres IP nie jest chroniony. + No comment provided by engineer. + + + Use private routing with unknown servers. + Używaj prywatnego trasowania z nieznanymi serwerami. + No comment provided by engineer. + Use server Użyj serwera @@ -6070,14 +7085,19 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Używaj aplikacji podczas połączenia. No comment provided by engineer. + + Use the app with one hand. + Korzystaj z aplikacji jedną ręką. + No comment provided by engineer. + User profile Profil użytkownika No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - Używanie hostów .onion wymaga kompatybilnego dostawcy VPN. + + User selection + Wybór użytkownika No comment provided by engineer. @@ -6210,6 +7230,16 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Oczekiwanie na film No comment provided by engineer. + + Wallpaper accent + Akcent tapety + No comment provided by engineer. + + + Wallpaper background + Tło tapety + No comment provided by engineer. + Warning: starting chat on multiple devices is not supported and will cause message delivery failures Ostrzeżenie: rozpoczęcie czatu na wielu urządzeniach nie jest wspierane i spowoduje niepowodzenia dostarczania wiadomości @@ -6295,19 +7325,39 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Ze zmniejszonym zużyciem baterii. No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + Bez Tor lub VPN, Twój adres IP będzie widoczny do serwerów plików. + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + Bez Tor lub VPN, Twój adres IP będzie widoczny dla tych przekaźników XFTP: %@. + No comment provided by engineer. + Wrong database passphrase Nieprawidłowe hasło bazy danych No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + Zły klucz lub nieznane połączenie - najprawdopodobniej to połączenie jest usunięte. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + Zły klucz lub nieznany adres fragmentu pliku - najprawdopodobniej plik został usunięty. + file error text + Wrong passphrase! Nieprawidłowe hasło! No comment provided by engineer. - - XFTP servers - Serwery XFTP + + XFTP server + Serwer XFTP No comment provided by engineer. @@ -6387,11 +7437,21 @@ Powtórzyć prośbę dołączenia? Jesteś zaproszony do grupy No comment provided by engineer. + + You are not connected to these servers. Private routing is used to deliver messages to them. + Nie jesteś połączony z tymi serwerami. Prywatne trasowanie jest używane do dostarczania do nich wiadomości. + No comment provided by engineer. + You can accept calls from lock screen, without device and app authentication. Możesz przyjmować połączenia z ekranu blokady, bez uwierzytelniania urządzenia i aplikacji. No comment provided by engineer. + + You can change it in Appearance settings. + Możesz to zmienić w ustawieniach wyglądu. + No comment provided by engineer. + You can create it later Możesz go utworzyć później @@ -6422,11 +7482,16 @@ Powtórzyć prośbę dołączenia? Możesz ustawić go jako widoczny dla swoich kontaktów SimpleX w Ustawieniach. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Możesz teraz wysyłać wiadomości do %@ notification body + + You can send messages to %@ from Archived contacts. + Możesz wysyłać wiadomości do %@ ze zarchiwizowanych kontaktów. + No comment provided by engineer. + You can set lock screen notification preview via settings. Podgląd powiadomień na ekranie blokady można ustawić w ustawieniach. @@ -6452,6 +7517,11 @@ Powtórzyć prośbę dołączenia? Możesz rozpocząć czat poprzez Ustawienia aplikacji / Baza danych lub poprzez ponowne uruchomienie aplikacji No comment provided by engineer. + + You can still view conversation with %@ in the list of chats. + Nadal możesz przeglądać rozmowę z %@ na liście czatów. + No comment provided by engineer. + You can turn on SimpleX Lock via Settings. Możesz włączyć blokadę SimpleX poprzez Ustawienia. @@ -6494,11 +7564,6 @@ Repeat connection request? Powtórzyć prośbę połączenia? No comment provided by engineer. - - You have no chats - Nie masz czatów - No comment provided by engineer. - You have to enter passphrase every time the app starts - it is not stored on the device. Musisz wprowadzić hasło przy każdym uruchomieniu aplikacji - nie jest one przechowywane na urządzeniu. @@ -6519,11 +7584,26 @@ Powtórzyć prośbę połączenia? Dołączyłeś do tej grupy. Łączenie z zapraszającym członkiem grupy. No comment provided by engineer. + + You may migrate the exported database. + Możesz zmigrować wyeksportowaną bazy danych. + No comment provided by engineer. + + + You may save the exported archive. + Możesz zapisać wyeksportowane archiwum. + No comment provided by engineer. + You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. Musisz używać najnowszej wersji bazy danych czatu TYLKO na jednym urządzeniu, w przeciwnym razie możesz przestać otrzymywać wiadomości od niektórych kontaktów. No comment provided by engineer. + + You need to allow your contact to call to be able to call them. + Aby móc dzwonić, musisz zezwolić kontaktowi na połączenia. + No comment provided by engineer. + You need to allow your contact to send voice messages to be able to send them. Musisz zezwolić Twojemu kontaktowi na wysyłanie wiadomości głosowych, aby móc je wysyłać. @@ -6639,13 +7719,6 @@ Powtórzyć prośbę połączenia? Twoje profile czatu No comment provided by engineer. - - Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link). - Twój kontakt musi być online, aby połączenie zostało zakończone. -Możesz anulować to połączenie i usunąć kontakt (i spróbować później z nowym linkiem). - No comment provided by engineer. - Your contact sent a file that is larger than currently supported maximum size (%@). Twój kontakt wysłał plik, który jest większy niż obecnie obsługiwany maksymalny rozmiar (%@). @@ -6793,6 +7866,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. i %lld innych wydarzeń No comment provided by engineer. + + attempts + próby + No comment provided by engineer. + audio call (not e2e encrypted) połączenie audio (nie szyfrowane e2e) @@ -6833,6 +7911,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. pogrubiona No comment provided by engineer. + + call + zadzwoń + No comment provided by engineer. + call error błąd połączenia @@ -6983,6 +8066,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. dni time unit + + decryption errors + błąd odszyfrowywania + No comment provided by engineer. + default (%@) domyślne (%@) @@ -7033,6 +8121,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. zduplikowana wiadomość integrity error chat item + + duplicates + duplikaty + No comment provided by engineer. + e2e encrypted zaszyfrowany e2e @@ -7113,6 +8206,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. nowe wydarzenie No comment provided by engineer. + + expired + wygasły + No comment provided by engineer. + forwarded przekazane dalej @@ -7143,6 +8241,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. iOS Keychain będzie używany do bezpiecznego przechowywania hasła po ponownym uruchomieniu aplikacji lub zmianie hasła - pozwoli to na otrzymywanie powiadomień push. No comment provided by engineer. + + inactive + nieaktywny + No comment provided by engineer. + incognito via contact address link incognito poprzez link adresu kontaktowego @@ -7183,6 +8286,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. zaproszenie do grupy %@ group name + + invite + zaproś + No comment provided by engineer. + invited zaproszony @@ -7238,6 +8346,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. połączony rcv group event chat item + + message + wiadomość + No comment provided by engineer. + message received wiadomość otrzymana @@ -7268,6 +8381,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. miesiące time unit + + mute + wycisz + No comment provided by engineer. + never nigdy @@ -7320,6 +8438,16 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. włączone group pref value + + other + inne + No comment provided by engineer. + + + other errors + inne błędy + No comment provided by engineer. + owner właściciel @@ -7390,6 +8518,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. zapisane od %@ No comment provided by engineer. + + search + szukaj + No comment provided by engineer. + sec sek @@ -7415,6 +8548,15 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. wyślij wiadomość bezpośrednią No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + Informacje kolejki serwera: %1$@ + +ostatnia otrzymana wiadomość: %2$@ + queue info + set new contact address ustaw nowy adres kontaktu @@ -7455,11 +8597,26 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. nieznany connection info + + unknown servers + nieznane przekaźniki + No comment provided by engineer. + unknown status nieznany status No comment provided by engineer. + + unmute + wyłącz wyciszenie + No comment provided by engineer. + + + unprotected + niezabezpieczony + No comment provided by engineer. + updated group profile zaktualizowano profil grupy @@ -7500,6 +8657,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. przez przekaźnik No comment provided by engineer. + + video + wideo + No comment provided by engineer. + video call (not e2e encrypted) połączenie wideo (bez szyfrowania e2e) @@ -7525,6 +8687,11 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. tygodnie time unit + + when IP hidden + gdy IP ukryty + No comment provided by engineer. + yes tak @@ -7609,7 +8776,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.
- +
@@ -7646,7 +8813,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.
- +
@@ -7666,4 +8833,218 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu.
+ +
+ +
+ + + SimpleX SE + SimpleX SE + Bundle display name + + + SimpleX SE + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright © 2024 SimpleX Chat. Wszelkie prawa zastrzeżone. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + %@ + No comment provided by engineer. + + + App is locked! + Aplikacja zablokowana! + No comment provided by engineer. + + + Cancel + Anuluj + No comment provided by engineer. + + + Cannot access keychain to save database password + Nie można uzyskać dostępu do pęku kluczy aby zapisać hasło do bazy danych + No comment provided by engineer. + + + Cannot forward message + Nie można przekazać wiadomości + No comment provided by engineer. + + + Comment + Komentarz + No comment provided by engineer. + + + Currently maximum supported file size is %@. + Obecnie maksymalny obsługiwany rozmiar pliku to %@. + No comment provided by engineer. + + + Database downgrade required + Wymagane obniżenie wersji bazy danych + No comment provided by engineer. + + + Database encrypted! + Baza danych zaszyfrowana! + No comment provided by engineer. + + + Database error + Błąd bazy danych + No comment provided by engineer. + + + Database passphrase is different from saved in the keychain. + Hasło bazy danych jest inne niż zapisane w pęku kluczy. + No comment provided by engineer. + + + Database passphrase is required to open chat. + Hasło do bazy danych jest wymagane do otwarcia czatu. + No comment provided by engineer. + + + Database upgrade required + Wymagana aktualizacja bazy danych + No comment provided by engineer. + + + Error preparing file + Błąd przygotowania pliku + No comment provided by engineer. + + + Error preparing message + Błąd przygotowania wiadomości + No comment provided by engineer. + + + Error: %@ + Błąd: %@ + No comment provided by engineer. + + + File error + Błąd pliku + No comment provided by engineer. + + + Incompatible database version + Niekompatybilna wersja bazy danych + No comment provided by engineer. + + + Invalid migration confirmation + Nieprawidłowe potwierdzenie migracji + No comment provided by engineer. + + + Keychain error + Błąd pęku kluczy + No comment provided by engineer. + + + Large file! + Duży plik! + No comment provided by engineer. + + + No active profile + Brak aktywnego profilu + No comment provided by engineer. + + + Ok + Ok + No comment provided by engineer. + + + Open the app to downgrade the database. + Otwórz aplikację aby obniżyć wersję bazy danych. + No comment provided by engineer. + + + Open the app to upgrade the database. + Otwórz aplikację aby zaktualizować bazę danych. + No comment provided by engineer. + + + Passphrase + Hasło + No comment provided by engineer. + + + Please create a profile in the SimpleX app + Proszę utworzyć profil w aplikacji SimpleX + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + Wybrane preferencje czatu zabraniają tej wiadomości. + No comment provided by engineer. + + + Sending a message takes longer than expected. + Wysłanie wiadomości trwa dłużej niż oczekiwano. + No comment provided by engineer. + + + Sending message… + Wysyłanie wiadomości… + No comment provided by engineer. + + + Share + Udostępnij + No comment provided by engineer. + + + Slow network? + Wolna sieć? + No comment provided by engineer. + + + Unknown database error: %@ + Nieznany błąd bazy danych: %@ + No comment provided by engineer. + + + Unsupported format + Niewspierany format + No comment provided by engineer. + + + Wait + Czekaj + No comment provided by engineer. + + + Wrong database passphrase + Nieprawidłowe hasło bazy danych + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + Możesz zezwolić na udostępnianie w ustawieniach Prywatność i bezpieczeństwo / Blokada SimpleX. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/contents.json b/apps/ios/SimpleX Localizations/pl.xcloc/contents.json index 22043b831d..0074d85662 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/pl.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "pl", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "15F31d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "15.4" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff b/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff index b4fa69449f..5f6cbc3b8f 100644 --- a/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff +++ b/apps/ios/SimpleX Localizations/pt-BR.xcloc/Localized Contents/pt-BR.xliff @@ -374,9 +374,9 @@ Adicione servidores escaneando o QR code. No comment provided by engineer.
- - Add server… - Adicionar servidor… + + Add server + Adicionar servidor No comment provided by engineer. @@ -5234,6 +5234,274 @@ Isso pode acontecer por causa de algum bug ou quando a conexão está comprometi %1$@ em %2$@: copied message info, <sender> at <time> + + Allow your contacts to irreversibly delete sent messages. (24 hours) + Permitir que seus contatos deletem mensagens enviadas de maneira irreversível. (24 horas) + + + %@ downloaded + baixado + + + %@ uploaded + transferido + + + A new random profile will be shared. + Um novo perfil aleatório será compartilhado. + + + Camera not available + Câmera indisponível + + + Admins can block a member for all. + Administradores podem bloquear um membro para todos. + + + Allow to irreversibly delete sent messages. (24 hours) + Permitir que mensagens enviadas sejam deletadas de maneira irreversível. (24 horas) + + + Apply + Aplicar + + + Accent + Esquema + + + Accept connection request? + Aceitar solicitação de conexão? + + + Active connections + Conexões ativas + + + Add contact + Adicionar contato + + + Additional accent + Esquema adicional + + + All new messages from %@ will be hidden! + Todas as novas mensagens de %@ serão ocultas! + + + All profiles + Todos perfis + + + Allow calls? + Permitir chamadas? + + + Archive contacts to chat later. + Arquivar contatos para conversar depois. + + + Blur media + Censurar mídia + + + Calls prohibited! + Chamadas proibidas! + + + Can't call contact + Não foi possível ligar para o contato + + + %lld messages marked deleted + mensagens deletadas + + + 0 sec + 0 seg + + + %lld messages blocked + mensagens bloqueadas + + + %lld messages blocked by admin + mensagens bloqueadas pelo administrador + + + **Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection. + **Nota**: usar o mesmo banco de dados em dois dispositivos irá quebrar a desencriptação das mensagens de suas conexões como uma medida de segurança. + + + - more stable message delivery. +- a bit better groups. +- and more! + - entrega de mensagens mais estável. +- grupos melhorados. +- e muito mais! + + + All messages will be deleted - this cannot be undone! + Todas as mensagens serão deletadas - isto não pode ser desfeito! + + + Allow to send files and media. + Permitir o envio de arquivos e mídia. + + + Allow to send SimpleX links. + Permitir envio de links SimpleX. + + + Block for all + Bloquear para todos + + + Block member + Bloquear membro + + + Blocked by admin + Bloqueado por um administrador + + + Block group members + Bloquear membros de grupo + + + Block member for all? + Bloquear membro para todos? + + + Block member? + Bloquear membro? + + + Both you and your contact can irreversibly delete sent messages. (24 hours) + Você e seu contato podem apagar mensagens enviadas de maneira irreversível. (24 horas) + + + Can't call member + Não foi possível ligar para este membro + + + Can't message member + Não foi possível enviar mensagem para este membro + + + Cancel migration + Cancelar migração + + + Abort + Abortar + + + Abort changing address + Abortar troca de endereço + + + Abort changing address? + Abortar troca de endereço? + + + - optionally notify deleted contacts. +- profile names with spaces. +- and more! + - notificar contatos apagados de maneira opcional. +- nome de perfil com espaços. +- e muito mais! + + + Allow sharing + Permitir compartilhamento + + + Block + Bloquear + + + Additional accent 2 + Esquema adicional 2 + + + Address change will be aborted. Old receiving address will be used. + Alteração de endereço será abortada. O endereço antigo será utilizado. + + + Advanced settings + Configurações avançadas + + + All data is private to your device. + Toda informação é privada em seu dispositivo. + + + All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays. + Todos os seus contatos, conversas e arquivos serão encriptados e enviados em pedaços para nós XFTP. + + + Allow irreversible message deletion only if your contact allows it to you. (24 hours) + Permitir deletar mensagens de maneira irreversível apenas se seu contato permitir para você. (24 horas) + + + Already connecting! + Já está conectando! + + + Already joining the group! + Já está entrando no grupo! + + + Always use private routing. + Sempre use rotas privadas. + + + Apply to + Aplicar em + + + Archiving database + Arquivando banco de dados + + + Black + Preto + + + Cannot forward message + Não é possível encaminhar mensagem + + + (new) + (novo) + + + (this device v%@) + este dispositivo + + + **Add contact**: to create a new invitation link, or connect via a link you received. + **Adicionar contato**: criar um novo link de convite ou conectar via um link que você recebeu. + + + **Create group**: to create a new group. + **Criar grupo**: criar um novo grupo. + + + **Warning**: the archive will be removed. + **Aviso**: o arquivo será removido. + + + A few more things + E mais algumas coisas + + + Archived contacts + Contatos arquivados + diff --git a/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff b/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff index c9c9707c39..a9bf86e778 100644 --- a/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff +++ b/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff @@ -5,9 +5,11 @@ - + + + No comment provided by engineer. @@ -50,16 +52,19 @@ Available in v5.1 #secreto# No comment provided by engineer. - + %@ + %@ No comment provided by engineer. - + %@ %@ + %@ %@ No comment provided by engineer. - + %@ / %@ + %@ / %@ No comment provided by engineer. @@ -117,12 +122,14 @@ Available in v5.1 %d mensagem(s) ignorada(s) integrity error chat item - + %lld + %lld No comment provided by engineer. - + %lld %@ + %lld %@ No comment provided by engineer. @@ -155,24 +162,29 @@ Available in v5.1 %lld segundos No comment provided by engineer. - + %lldd + %lldd No comment provided by engineer. - + %lldh + %lldh No comment provided by engineer. - + %lldk + %lldk No comment provided by engineer. - + %lldm + %lldm No comment provided by engineer. - + %lldmth + %lldmth No comment provided by engineer. @@ -193,8 +205,9 @@ Available in v5.1 %u mensagens ignoradas. No comment provided by engineer. - + ( + ( No comment provided by engineer. @@ -359,8 +372,8 @@ Available in v5.1 Add servers by scanning QR codes. No comment provided by engineer. - - Add server… + + Add server No comment provided by engineer. @@ -4540,6 +4553,31 @@ SimpleX servers cannot see your profile. Confirmar envio No comment provided by engineer. + + %@ downloaded + %@ baixado + No comment provided by engineer. + + + # %@ + # %@ + copied message info title, # <title> + + + %@: + %@: + copied message info + + + %@ (current) + %@(atual) + No comment provided by engineer. + + + %@ (current): + %@ (atual): + copied message info + diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index ba1ac7a929..969a7d68e0 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -2,7 +2,7 @@
- +
@@ -127,11 +127,6 @@ %@ подтверждён No comment provided by engineer. - - %@ servers - %@ серверы - No comment provided by engineer. - %@ uploaded %@ загружено @@ -557,16 +552,17 @@ Об адресе SimpleX No comment provided by engineer. - - Accent color - Основной цвет + + Accent + Акцент No comment provided by engineer. Accept Принять accept contact request via notification - accept incoming call via notification + accept incoming call via notification + swipe action Accept connection request? @@ -581,7 +577,23 @@ Accept incognito Принять инкогнито - accept contact request via notification + accept contact request via notification + swipe action + + + Acknowledged + Подтверждено + No comment provided by engineer. + + + Acknowledgement errors + Ошибки подтверждения + No comment provided by engineer. + + + Active connections + Активные соединения + No comment provided by engineer. Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. @@ -603,16 +615,16 @@ Добавить профиль No comment provided by engineer. + + Add server + Добавить сервер + No comment provided by engineer. + Add servers by scanning QR codes. Добавить серверы через QR код. No comment provided by engineer. - - Add server… - Добавить сервер… - No comment provided by engineer. - Add to another device Добавить на другое устройство @@ -623,6 +635,21 @@ Добавить приветственное сообщение No comment provided by engineer. + + Additional accent + Дополнительный акцент + No comment provided by engineer. + + + Additional accent 2 + Дополнительный акцент 2 + No comment provided by engineer. + + + Additional secondary + Вторичный 2 + No comment provided by engineer. + Address Адрес @@ -648,6 +675,11 @@ Настройки сети No comment provided by engineer. + + Advanced settings + Настройки сети + No comment provided by engineer. + All app data is deleted. Все данные приложения будут удалены. @@ -663,6 +695,11 @@ Все данные удаляются при его вводе. No comment provided by engineer. + + All data is private to your device. + Все данные хранятся только на вашем устройстве. + No comment provided by engineer. + All group members will remain connected. Все члены группы, которые соединились через эту ссылку, останутся в группе. @@ -683,6 +720,11 @@ Все новые сообщения от %@ будут скрыты! No comment provided by engineer. + + All profiles + Все профили + No comment provided by engineer. + All your contacts will remain connected. Все контакты, которые соединились через этот адрес, сохранятся. @@ -708,11 +750,21 @@ Разрешить звонки, только если их разрешает Ваш контакт. No comment provided by engineer. + + Allow calls? + Разрешить звонки? + No comment provided by engineer. + Allow disappearing messages only if your contact allows it to you. Разрешить исчезающие сообщения, только если Ваш контакт разрешает их Вам. No comment provided by engineer. + + Allow downgrade + Разрешить прямую доставку + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Разрешить необратимое удаление сообщений, только если Ваш контакт разрешает это Вам. (24 часа) @@ -738,6 +790,11 @@ Разрешить посылать исчезающие сообщения. No comment provided by engineer. + + Allow sharing + Разрешить поделиться + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Разрешить необратимо удалять отправленные сообщения. (24 часа) @@ -808,6 +865,11 @@ Вступление в группу уже начато! No comment provided by engineer. + + Always use private routing. + Всегда использовать конфиденциальную доставку. + No comment provided by engineer. + Always use relay Всегда соединяться через relay @@ -873,11 +935,26 @@ Применить No comment provided by engineer. + + Apply to + Применить к + No comment provided by engineer. + Archive and upload Архивировать и загрузить No comment provided by engineer. + + Archive contacts to chat later. + Архивируйте контакты чтобы продолжить переписку. + No comment provided by engineer. + + + Archived contacts + Архивированные контакты + No comment provided by engineer. + Archiving database Подготовка архива @@ -948,6 +1025,11 @@ Назад No comment provided by engineer. + + Background + Фон + No comment provided by engineer. + Bad desktop address Неверный адрес компьютера @@ -973,6 +1055,16 @@ Улучшенные сообщения No comment provided by engineer. + + Better networking + Улучшенные сетевые функции + No comment provided by engineer. + + + Black + Черная + No comment provided by engineer. + Block Заблокировать @@ -1008,6 +1100,16 @@ Заблокирован администратором No comment provided by engineer. + + Blur for better privacy. + Размыть для конфиденциальности. + No comment provided by engineer. + + + Blur media + Размытие изображений + No comment provided by engineer. + Both you and your contact can add message reactions. И Вы, и Ваш контакт можете добавлять реакции на сообщения. @@ -1053,11 +1155,26 @@ Звонки No comment provided by engineer. + + Calls prohibited! + Звонки запрещены! + No comment provided by engineer. + Camera not available Камера недоступна No comment provided by engineer. + + Can't call contact + Не удается позвонить контакту + No comment provided by engineer. + + + Can't call member + Не удается позвонить члену группы + No comment provided by engineer. + Can't invite contact! Нельзя пригласить контакт! @@ -1068,6 +1185,11 @@ Нельзя пригласить контакты! No comment provided by engineer. + + Can't message member + Не удается написать члену группы + No comment provided by engineer. + Cancel Отменить @@ -1083,11 +1205,21 @@ Ошибка доступа к Keychain при сохранении пароля No comment provided by engineer. + + Cannot forward message + Невозможно переслать сообщение + No comment provided by engineer. + Cannot receive file Невозможно получить файл No comment provided by engineer. + + Capacity exceeded - recipient did not receive previously sent messages. + Превышено количество сообщений - предыдущие сообщения не доставлены. + snd error text + Cellular Мобильная сеть @@ -1149,6 +1281,11 @@ Архив чата No comment provided by engineer. + + Chat colors + Цвета чата + No comment provided by engineer. + Chat console Консоль @@ -1164,6 +1301,11 @@ Данные чата удалены No comment provided by engineer. + + Chat database exported + Данные чата экспортированы + No comment provided by engineer. + Chat database imported Архив чата импортирован @@ -1184,6 +1326,11 @@ Чат остановлен. Если вы уже использовали эту базу данных на другом устройстве, перенесите ее обратно до запуска чата. No comment provided by engineer. + + Chat list + Список чатов + No comment provided by engineer. + Chat migrated! Чат мигрирован! @@ -1194,6 +1341,11 @@ Предпочтения No comment provided by engineer. + + Chat theme + Тема чата + No comment provided by engineer. + Chats Чаты @@ -1224,10 +1376,25 @@ Выбрать из библиотеки No comment provided by engineer. + + Chunks deleted + Блоков удалено + No comment provided by engineer. + + + Chunks downloaded + Блоков принято + No comment provided by engineer. + + + Chunks uploaded + Блоков загружено + No comment provided by engineer. + Clear Очистить - No comment provided by engineer. + swipe action Clear conversation @@ -1249,9 +1416,14 @@ Сбросить подтверждение No comment provided by engineer. - - Colors - Цвета + + Color chats with the new themes. + Добавьте цвета к чатам в настройках. + No comment provided by engineer. + + + Color mode + Режим цветов No comment provided by engineer. @@ -1264,11 +1436,21 @@ Сравните код безопасности с Вашими контактами. No comment provided by engineer. + + Completed + Готово + No comment provided by engineer. + Configure ICE servers Настройка ICE серверов No comment provided by engineer. + + Configured %@ servers + Настроенные %@ серверы + No comment provided by engineer. + Confirm Подтвердить @@ -1279,11 +1461,21 @@ Подтвердить Код No comment provided by engineer. + + Confirm contact deletion? + Потвердить удаление контакта? + No comment provided by engineer. + Confirm database upgrades Подтвердить обновление базы данных No comment provided by engineer. + + Confirm files from unknown servers. + Подтверждать файлы с неизвестных серверов. + No comment provided by engineer. + Confirm network settings Подтвердите настройки сети @@ -1329,6 +1521,11 @@ Подключиться к компьютеру No comment provided by engineer. + + Connect to your friends faster. + Соединяйтесь с друзьями быстрее. + No comment provided by engineer. + Connect to yourself? Соединиться с самим собой? @@ -1368,16 +1565,31 @@ This is your own one-time link! Соединиться с %@ No comment provided by engineer. + + Connected + Соединено + No comment provided by engineer. + Connected desktop Подключенный компьютер No comment provided by engineer. + + Connected servers + Подключенные серверы + No comment provided by engineer. + Connected to desktop Компьютер подключен No comment provided by engineer. + + Connecting + Соединяется + No comment provided by engineer. + Connecting to server… Устанавливается соединение с сервером… @@ -1388,6 +1600,11 @@ This is your own one-time link! Устанавливается соединение с сервером… (ошибка: %@) No comment provided by engineer. + + Connecting to contact, please wait or check later! + Контакт соединяется, подождите или проверьте позже! + No comment provided by engineer. + Connecting to desktop Подключение к компьютеру @@ -1398,6 +1615,11 @@ This is your own one-time link! Соединение No comment provided by engineer. + + Connection and servers status. + Состояние соединения и серверов. + No comment provided by engineer. + Connection error Ошибка соединения @@ -1408,6 +1630,11 @@ This is your own one-time link! Ошибка соединения (AUTH) No comment provided by engineer. + + Connection notifications + Уведомления по соединениям + No comment provided by engineer. + Connection request sent! Запрос на соединение отправлен! @@ -1423,6 +1650,16 @@ This is your own one-time link! Превышено время соединения No comment provided by engineer. + + Connection with desktop stopped + Соединение с компьютером остановлено + No comment provided by engineer. + + + Connections + Соединения + No comment provided by engineer. + Contact allows Контакт разрешает @@ -1433,6 +1670,11 @@ This is your own one-time link! Существующий контакт No comment provided by engineer. + + Contact deleted! + Контакт удален! + No comment provided by engineer. + Contact hidden: Контакт скрыт: @@ -1443,9 +1685,9 @@ This is your own one-time link! Соединение с контактом установлено notification - - Contact is not connected yet! - Соединение еще не установлено! + + Contact is deleted. + Контакт удален. No comment provided by engineer. @@ -1458,6 +1700,11 @@ This is your own one-time link! Предпочтения контакта No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + Контакт будет удален — это нельзя отменить! + No comment provided by engineer. + Contacts Контакты @@ -1473,10 +1720,20 @@ This is your own one-time link! Продолжить No comment provided by engineer. + + Conversation deleted! + Разговор удален! + No comment provided by engineer. + Copy Скопировать - chat item action + No comment provided by engineer. + + + Copy error + Ошибка копирования + No comment provided by engineer. Core version: v%@ @@ -1553,6 +1810,11 @@ This is your own one-time link! Создать профиль No comment provided by engineer. + + Created + Создано + No comment provided by engineer. + Created at Создано @@ -1588,6 +1850,11 @@ This is your own one-time link! Текущий пароль… No comment provided by engineer. + + Current profile + Текущий профиль + No comment provided by engineer. + Currently maximum supported file size is %@. Максимальный размер файла - %@. @@ -1598,11 +1865,21 @@ This is your own one-time link! Пользовательское время No comment provided by engineer. + + Customize theme + Настроить тему + No comment provided by engineer. + Dark Тёмная No comment provided by engineer. + + Dark mode colors + Цвета тёмного режима + No comment provided by engineer. + Database ID ID базы данных @@ -1701,6 +1978,11 @@ This is your own one-time link! Данные чата будут мигрированы при перезапуске No comment provided by engineer. + + Debug delivery + Отладка доставки + No comment provided by engineer. + Decentralized Децентрализованный @@ -1714,18 +1996,19 @@ This is your own one-time link! Delete Удалить - chat item action + chat item action + swipe action + + + Delete %lld messages of members? + Удалить %lld сообщений членов группы? + No comment provided by engineer. Delete %lld messages? Удалить %lld сообщений? No comment provided by engineer. - - Delete Contact - Удалить контакт - No comment provided by engineer. - Delete address Удалить адрес @@ -1781,11 +2064,9 @@ This is your own one-time link! Удалить контакт No comment provided by engineer. - - Delete contact? -This cannot be undone! - Удалить контакт? -Это не может быть отменено! + + Delete contact? + Удалить контакт? No comment provided by engineer. @@ -1878,11 +2159,6 @@ This cannot be undone! Удалить предыдущую версию данных? No comment provided by engineer. - - Delete pending connection - Удалить соединение - No comment provided by engineer. - Delete pending connection? Удалить ожидаемое соединение? @@ -1898,11 +2174,26 @@ This cannot be undone! Удаление очереди server test step + + Delete up to 20 messages at once. + Удаляйте до 20 сообщений за раз. + No comment provided by engineer. + Delete user profile? Удалить профиль пользователя? No comment provided by engineer. + + Delete without notification + Удалить без уведомления + No comment provided by engineer. + + + Deleted + Удалено + No comment provided by engineer. + Deleted at Удалено @@ -1913,6 +2204,11 @@ This cannot be undone! Удалено: %@ copied message info + + Deletion errors + Ошибки удаления + No comment provided by engineer. + Delivery Доставка @@ -1948,11 +2244,41 @@ This cannot be undone! Компьютеры No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + Адрес сервера назначения %@ несовместим с настройками пересылающего сервера %@. + No comment provided by engineer. + + + Destination server error: %@ + Ошибка сервера получателя: %@ + snd error text + + + Destination server version of %@ is incompatible with forwarding server %@. + Версия сервера назначения %@ несовместима с пересылающим сервером %@. + No comment provided by engineer. + + + Detailed statistics + Подробная статистика + No comment provided by engineer. + + + Details + Подробности + No comment provided by engineer. + Develop Для разработчиков No comment provided by engineer. + + Developer options + Опции разработчика + No comment provided by engineer. + Developer tools Инструменты разработчика @@ -2003,6 +2329,11 @@ This cannot be undone! Выключить для всех No comment provided by engineer. + + Disabled + Выключено + No comment provided by engineer. + Disappearing message Исчезающее сообщение @@ -2053,11 +2384,21 @@ This cannot be undone! Обнаружение по локальной сети No comment provided by engineer. + + Do NOT send messages directly, even if your or destination server does not support private routing. + Не отправлять сообщения напрямую, даже если сервер получателя не поддерживает конфиденциальную доставку. + No comment provided by engineer. + Do NOT use SimpleX for emergency calls. Не используйте SimpleX для экстренных звонков. No comment provided by engineer. + + Do NOT use private routing. + Не использовать конфиденциальную доставку. + No comment provided by engineer. + Do it later Отложить @@ -2093,6 +2434,11 @@ This cannot be undone! Загрузить chat item action + + Download errors + Ошибки приема + No comment provided by engineer. + Download failed Ошибка загрузки @@ -2103,6 +2449,16 @@ This cannot be undone! Загрузка файла server test step + + Downloaded + Принято + No comment provided by engineer. + + + Downloaded files + Принятые файлы + No comment provided by engineer. + Downloading archive Загрузка архива @@ -2203,6 +2559,11 @@ This cannot be undone! Включить код самоуничтожения set passcode view + + Enabled + Включено + No comment provided by engineer. + Enabled for Включено для @@ -2373,6 +2734,11 @@ This cannot be undone! Ошибка при изменении настройки No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + Ошибка подключения к пересылающему серверу %@. Попробуйте позже. + No comment provided by engineer. + Error creating address Ошибка при создании адреса @@ -2423,11 +2789,6 @@ This cannot be undone! Ошибка при удалении соединения No comment provided by engineer. - - Error deleting contact - Ошибка при удалении контакта - No comment provided by engineer. - Error deleting database Ошибка при удалении данных чата @@ -2473,6 +2834,11 @@ This cannot be undone! Ошибка при экспорте архива чата No comment provided by engineer. + + Error exporting theme: %@ + Ошибка экспорта темы: %@ + No comment provided by engineer. + Error importing chat database Ошибка при импорте архива чата @@ -2498,11 +2864,26 @@ This cannot be undone! Ошибка при получении файла No comment provided by engineer. + + Error reconnecting server + Ошибка переподключения к серверу + No comment provided by engineer. + + + Error reconnecting servers + Ошибка переподключения к серверам + No comment provided by engineer. + Error removing member Ошибка при удалении члена группы No comment provided by engineer. + + Error resetting statistics + Ошибка сброса статистики + No comment provided by engineer. + Error saving %@ servers Ошибка при сохранении %@ серверов @@ -2621,7 +3002,8 @@ This cannot be undone! Error: %@ Ошибка: %@ - No comment provided by engineer. + file error text + snd error text Error: URL is invalid @@ -2633,6 +3015,11 @@ This cannot be undone! Ошибка: данные чата не найдены No comment provided by engineer. + + Errors + Ошибки + No comment provided by engineer. + Even when disabled in the conversation. Даже когда они выключены в разговоре. @@ -2658,6 +3045,11 @@ This cannot be undone! Ошибка при экспорте: No comment provided by engineer. + + Export theme + Экспорт темы + No comment provided by engineer. + Exported database archive. Архив чата экспортирован. @@ -2691,8 +3083,33 @@ This cannot be undone! Favorite Избранный + swipe action + + + File error + Ошибка файла No comment provided by engineer. + + File not found - most likely file was deleted or cancelled. + Файл не найден - скорее всего, файл был удален или отменен. + file error text + + + File server error: %@ + Ошибка сервера файлов: %@ + file error text + + + File status + Статус файла + No comment provided by engineer. + + + File status: %@ + Статус файла: %@ + copied message info + File will be deleted from servers. Файл будет удалён с серверов. @@ -2713,6 +3130,11 @@ This cannot be undone! Файл: %@ No comment provided by engineer. + + Files + Файлы + No comment provided by engineer. + Files & media Файлы и медиа @@ -2818,6 +3240,35 @@ This cannot be undone! Переслано из No comment provided by engineer. + + Forwarding server %@ failed to connect to destination server %@. Please try later. + Пересылающий сервер %@ не смог подключиться к серверу назначения %@. Попробуйте позже. + No comment provided by engineer. + + + Forwarding server address is incompatible with network settings: %@. + Адрес пересылающего сервера несовместим с настройками сети: %@. + No comment provided by engineer. + + + Forwarding server version is incompatible with network settings: %@. + Версия пересылающего сервера несовместима с настройками сети: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + Пересылающий сервер: %1$@ +Ошибка сервера получателя: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + Пересылающий сервер: %1$@ +Ошибка: %2$@ + snd error text + Found desktop Компьютер найден @@ -2863,6 +3314,16 @@ This cannot be undone! ГИФ файлы и стикеры No comment provided by engineer. + + Good afternoon! + Добрый день! + message preview + + + Good morning! + Доброе утро! + message preview + Group Группа @@ -3143,6 +3604,11 @@ This cannot be undone! Ошибка импорта No comment provided by engineer. + + Import theme + Импорт темы + No comment provided by engineer. + Importing archive Импорт архива @@ -3265,6 +3731,11 @@ This cannot be undone! Интерфейс No comment provided by engineer. + + Interface colors + Цвета интерфейса + No comment provided by engineer. + Invalid QR code Неверный QR код @@ -3366,6 +3837,11 @@ This cannot be undone! 3. Соединение компроментировано. No comment provided by engineer. + + It protects your IP address and connections. + Защищает ваш IP адрес и соединения. + No comment provided by engineer. + It seems like you are already connected via this link. If it is not the case, there was an error (%@). Возможно, Вы уже соединились через эту ссылку. Если это не так, то это ошибка (%@). @@ -3384,7 +3860,7 @@ This cannot be undone! Join Вступить - No comment provided by engineer. + swipe action Join group @@ -3428,6 +3904,11 @@ This is your link for group %@! Оставить No comment provided by engineer. + + Keep conversation + Оставить разговор + No comment provided by engineer. + Keep the app open to use it from desktop Оставьте приложение открытым, чтобы использовать его с компьютера @@ -3471,7 +3952,7 @@ This is your link for group %@! Leave Выйти - No comment provided by engineer. + swipe action Leave group @@ -3603,11 +4084,26 @@ This is your link for group %@! Макс. 30 секунд, доставляются мгновенно. No comment provided by engineer. + + Media & file servers + Серверы файлов и медиа + No comment provided by engineer. + + + Medium + Среднее + blur media + Member Член группы No comment provided by engineer. + + Member inactive + Член неактивен + item status text + Member role will be changed to "%@". All group members will be notified. Роль члена группы будет изменена на "%@". Все члены группы получат сообщение. @@ -3623,6 +4119,11 @@ This is your link for group %@! Член группы будет удален - это действие нельзя отменить! No comment provided by engineer. + + Menus + Меню + No comment provided by engineer. + Message delivery error Ошибка доставки сообщения @@ -3633,11 +4134,31 @@ This is your link for group %@! Отчеты о доставке сообщений! No comment provided by engineer. + + Message delivery warning + Предупреждение доставки сообщения + item status text + Message draft Черновик сообщения No comment provided by engineer. + + Message forwarded + Сообщение переслано + item status text + + + Message may be delivered later if member becomes active. + Сообщение может быть доставлено позже, если член группы станет активным. + item status description + + + Message queue info + Информация об очереди сообщений + No comment provided by engineer. + Message reactions Реакции на сообщения @@ -3653,11 +4174,31 @@ This is your link for group %@! Реакции на сообщения запрещены в этой группе. No comment provided by engineer. + + Message reception + Прием сообщений + No comment provided by engineer. + + + Message servers + Серверы сообщений + No comment provided by engineer. + Message source remains private. Источник сообщения остаётся конфиденциальным. No comment provided by engineer. + + Message status + Статус сообщения + No comment provided by engineer. + + + Message status: %@ + Статус сообщения: %@ + copied message info + Message text Текст сообщения @@ -3683,6 +4224,16 @@ This is your link for group %@! Сообщения от %@ будут показаны! No comment provided by engineer. + + Messages received + Получено сообщений + No comment provided by engineer. + + + Messages sent + Сообщений отправлено + No comment provided by engineer. + Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. Сообщения, файлы и звонки защищены **end-to-end шифрованием** с прямой секретностью (PFS), правдоподобным отрицанием и восстановлением от взлома. @@ -3783,11 +4334,6 @@ This is your link for group %@! Скорее всего, соединение удалено. item status description - - Most likely this contact has deleted the connection with you. - Скорее всего, этот контакт удалил соединение с Вами. - No comment provided by engineer. - Multiple chat profiles Много профилей чата @@ -3796,7 +4342,7 @@ This is your link for group %@! Mute Без звука - No comment provided by engineer. + swipe action Muted when inactive! @@ -3806,7 +4352,7 @@ This is your link for group %@! Name Имя - No comment provided by engineer. + swipe action Network & servers @@ -3818,6 +4364,11 @@ This is your link for group %@! Интернет-соединение No comment provided by engineer. + + Network issues - message expired after many attempts to send it. + Ошибка сети - сообщение не было отправлено после многократных попыток. + snd error text + Network management Статус сети @@ -3843,6 +4394,11 @@ This is your link for group %@! Новый чат No comment provided by engineer. + + New chat experience 🎉 + Новый интерфейс 🎉 + No comment provided by engineer. + New contact request Новый запрос на соединение @@ -3873,6 +4429,11 @@ This is your link for group %@! Новое в %@ No comment provided by engineer. + + New media options + Новые медиа-опции + No comment provided by engineer. + New member role Роль члена группы @@ -3918,6 +4479,11 @@ This is your link for group %@! Отсутствует токен устройства! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + Прямого соединения пока нет, сообщение переслано или будет переслано админом. + item status description + No filtered chats Нет отфильтрованных разговоров @@ -3933,6 +4499,11 @@ This is your link for group %@! Нет истории No comment provided by engineer. + + No info, try to reload + Нет информации, попробуйте перезагрузить + No comment provided by engineer. + No network connection Нет интернет-соединения @@ -3953,6 +4524,11 @@ This is your link for group %@! Несовместимая версия! No comment provided by engineer. + + Nothing selected + Ничего не выбрано + No comment provided by engineer. + Notifications Уведомления @@ -3980,7 +4556,7 @@ This is your link for group %@! Off Выключено - No comment provided by engineer. + blur media Ok @@ -4002,14 +4578,18 @@ This is your link for group %@! Одноразовая ссылка No comment provided by engineer. - - Onion hosts will be required for connection. Requires enabling VPN. - Подключаться только к onion хостам. Требуется включенный VPN. + + Onion hosts will be **required** for connection. +Requires compatible VPN. + Подключаться только к **onion** хостам. +Требуется совместимый VPN. No comment provided by engineer. - - Onion hosts will be used when available. Requires enabling VPN. - Onion хосты используются, если возможно. Требуется включенный VPN. + + Onion hosts will be used when available. +Requires compatible VPN. + Onion хосты используются, если возможно. +Требуется совместимый VPN. No comment provided by engineer. @@ -4022,6 +4602,11 @@ This is your link for group %@! Только пользовательские устройства хранят контакты, группы и сообщения, которые отправляются **с двухуровневым end-to-end шифрованием**. No comment provided by engineer. + + Only delete conversation + Удалить только разговор + No comment provided by engineer. + Only group owners can change group preferences. Только владельцы группы могут изменять предпочтения группы. @@ -4117,6 +4702,11 @@ This is your link for group %@! Открытие миграции на другое устройство authentication reason + + Open server settings + Открыть настройки серверов + No comment provided by engineer. + Open user profiles Открыть профили пользователя @@ -4157,6 +4747,11 @@ This is your link for group %@! Другaя сеть No comment provided by engineer. + + Other %@ servers + Другие %@ серверы + No comment provided by engineer. + PING count Количество PING @@ -4222,6 +4817,11 @@ This is your link for group %@! Вставьте полученную ссылку No comment provided by engineer. + + Pending + В ожидании + No comment provided by engineer. + People can connect to you only via the links you share. С Вами можно соединиться только через созданные Вами ссылки. @@ -4242,11 +4842,28 @@ This is your link for group %@! Звонки с картинкой-в-картинке No comment provided by engineer. + + Play from the chat list. + Открыть из списка чатов. + No comment provided by engineer. + + + Please ask your contact to enable calls. + Попросите Вашего контакта разрешить звонки. + No comment provided by engineer. + Please ask your contact to enable sending voice messages. Попросите у Вашего контакта разрешить отправку голосовых сообщений. No comment provided by engineer. + + Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers. + Пожалуйста, проверьте, что мобильный и компьютер находятся в одной и той же локальной сети, и что брандмауэр компьютера разрешает подключение. +Пожалуйста, поделитесь любыми другими ошибками с разработчиками. + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. Пожалуйста, проверьте, что Вы использовали правильную ссылку или попросите, чтобы Ваш контакт отправил Вам другую ссылку. @@ -4344,6 +4961,11 @@ Error: %@ Просмотр No comment provided by engineer. + + Previously connected servers + Ранее подключенные серверы + No comment provided by engineer. + Privacy & security Конфиденциальность @@ -4359,11 +4981,31 @@ Error: %@ Защищенные имена файлов No comment provided by engineer. + + Private message routing + Конфиденциальная доставка сообщений + No comment provided by engineer. + + + Private message routing 🚀 + Конфиденциальная доставка 🚀 + No comment provided by engineer. + Private notes Личные заметки name of notes to self + + Private routing + Конфиденциальная доставка + No comment provided by engineer. + + + Private routing error + Ошибка конфиденциальной доставки + No comment provided by engineer. + Profile and server connections Профиль и соединения на сервере @@ -4394,6 +5036,11 @@ Error: %@ Пароль профиля No comment provided by engineer. + + Profile theme + Тема профиля + No comment provided by engineer. + Profile update will be sent to your contacts. Обновлённый профиль будет отправлен Вашим контактам. @@ -4444,11 +5091,23 @@ Error: %@ Запретить отправлять голосовые сообщений. No comment provided by engineer. + + Protect IP address + Защитить IP адрес + No comment provided by engineer. + Protect app screen Защитить экран приложения No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + Защитите ваш IP адрес от серверов сообщений, выбранных Вашими контактами. +Включите в настройках *Сеть и серверы*. + No comment provided by engineer. + Protect your chat profiles with a password! Защитите Ваши профили чата паролем! @@ -4464,6 +5123,16 @@ Error: %@ Таймаут протокола на KB No comment provided by engineer. + + Proxied + Проксировано + No comment provided by engineer. + + + Proxied servers + Проксированные серверы + No comment provided by engineer. + Push notifications Доставка уведомлений @@ -4484,6 +5153,11 @@ Error: %@ Оценить приложение No comment provided by engineer. + + Reachable chat toolbar + Доступная панель чата + No comment provided by engineer. + React… Реакция… @@ -4492,7 +5166,7 @@ Error: %@ Read Прочитано - No comment provided by engineer. + swipe action Read more @@ -4529,6 +5203,11 @@ Error: %@ Отчёты о доставке выключены No comment provided by engineer. + + Receive errors + Ошибки приема + No comment provided by engineer. + Received at Получено @@ -4549,16 +5228,26 @@ Error: %@ Полученное сообщение message info title + + Received messages + Полученные сообщения + No comment provided by engineer. + + + Received reply + Полученный ответ + No comment provided by engineer. + + + Received total + Всего получено + No comment provided by engineer. + Receiving address will be changed to a different server. Address change will complete after sender comes online. Адрес получения сообщений будет перемещён на другой сервер. Изменение адреса завершится после того как отправитель будет онлайн. No comment provided by engineer. - - Receiving concurrency - Одновременный приём - No comment provided by engineer. - Receiving file will be stopped. Приём файла будет прекращён. @@ -4584,11 +5273,36 @@ Error: %@ Получатели видят их в то время как Вы их набираете. No comment provided by engineer. + + Reconnect + Переподключить + No comment provided by engineer. + Reconnect all connected servers to force message delivery. It uses additional traffic. Повторно подключите все серверы, чтобы принудительно доставить сообщения. Используется дополнительный трафик. No comment provided by engineer. + + Reconnect all servers + Переподключить все серверы + No comment provided by engineer. + + + Reconnect all servers? + Переподключить все серверы? + No comment provided by engineer. + + + Reconnect server to force message delivery. It uses additional traffic. + Переподключить сервер для устранения неполадок доставки сообщений. Это использует дополнительный трафик. + No comment provided by engineer. + + + Reconnect server? + Переподключить сервер? + No comment provided by engineer. + Reconnect servers? Переподключить серверы? @@ -4612,7 +5326,8 @@ Error: %@ Reject Отклонить - reject incoming call via notification + reject incoming call via notification + swipe action Reject (sender NOT notified) @@ -4639,6 +5354,11 @@ Error: %@ Удалить No comment provided by engineer. + + Remove image + Удалить изображение + No comment provided by engineer. + Remove member Удалить члена группы @@ -4709,16 +5429,41 @@ Error: %@ Сбросить No comment provided by engineer. + + Reset all hints + Сбросить все подсказки + No comment provided by engineer. + + + Reset all statistics + Сбросить всю статистику + No comment provided by engineer. + + + Reset all statistics? + Сбросить всю статистику? + No comment provided by engineer. + Reset colors Сбросить цвета No comment provided by engineer. + + Reset to app theme + Сбросить на тему приложения + No comment provided by engineer. + Reset to defaults Сбросить настройки No comment provided by engineer. + + Reset to user theme + Сбросить на тему пользователя + No comment provided by engineer. + Restart the app to create a new chat profile Перезапустите приложение, чтобы создать новый профиль. @@ -4759,11 +5504,6 @@ Error: %@ Показать chat item action - - Revert - Отменить изменения - No comment provided by engineer. - Revoke Отозвать @@ -4789,9 +5529,14 @@ Error: %@ Запустить chat No comment provided by engineer. - - SMP servers - SMP серверы + + SMP server + SMP сервер + No comment provided by engineer. + + + Safely receive files + Получайте файлы безопасно No comment provided by engineer. @@ -4819,6 +5564,11 @@ Error: %@ Сохранить и уведомить членов группы No comment provided by engineer. + + Save and reconnect + Сохранить и переподключиться + No comment provided by engineer. + Save and update group profile Сохранить сообщение и обновить группу @@ -4899,6 +5649,16 @@ Error: %@ Сохраненное сообщение message info title + + Scale + Масштаб + No comment provided by engineer. + + + Scan / Paste link + Сканировать / Вставить ссылку + No comment provided by engineer. + Scan QR code Сканировать QR код @@ -4939,11 +5699,21 @@ Error: %@ Искать или вставьте ссылку SimpleX No comment provided by engineer. + + Secondary + Вторичный + No comment provided by engineer. + Secure queue Защита очереди server test step + + Secured + Защищено + No comment provided by engineer. + Security assessment Аудит безопасности @@ -4957,6 +5727,16 @@ Error: %@ Select Выбрать + chat item action + + + Selected %lld + Выбрано %lld + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + Выбранные настройки чата запрещают это сообщение. No comment provided by engineer. @@ -4994,11 +5774,6 @@ Error: %@ Отправка отчётов о доставке No comment provided by engineer. - - Send direct message - Отправить сообщение - No comment provided by engineer. - Send direct message to connect Отправьте сообщение чтобы соединиться @@ -5009,6 +5784,11 @@ Error: %@ Отправить исчезающее сообщение No comment provided by engineer. + + Send errors + Ошибки отправки + No comment provided by engineer. + Send link previews Отправлять картинки ссылок @@ -5019,6 +5799,21 @@ Error: %@ Отправить живое сообщение No comment provided by engineer. + + Send message to enable calls. + Отправьте сообщение, чтобы включить звонки. + No comment provided by engineer. + + + Send messages directly when IP address is protected and your or destination server does not support private routing. + Отправлять сообщения напрямую, когда IP адрес защищен, и Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку. + No comment provided by engineer. + + + Send messages directly when your or destination server does not support private routing. + Отправлять сообщения напрямую, когда Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку. + No comment provided by engineer. + Send notifications Отправлять уведомления @@ -5109,6 +5904,11 @@ Error: %@ Отправлено: %@ copied message info + + Sent directly + Отправлено напрямую + No comment provided by engineer. + Sent file event Отправка файла @@ -5119,11 +5919,46 @@ Error: %@ Отправленное сообщение message info title + + Sent messages + Отправленные сообщения + No comment provided by engineer. + Sent messages will be deleted after set time. Отправленные сообщения будут удалены через заданное время. No comment provided by engineer. + + Sent reply + Отправленный ответ + No comment provided by engineer. + + + Sent total + Всего отправлено + No comment provided by engineer. + + + Sent via proxy + Отправлено через прокси + No comment provided by engineer. + + + Server address + Адрес сервера + No comment provided by engineer. + + + Server address is incompatible with network settings. + Адрес сервера несовместим с настройками сети. + srv error text. + + + Server address is incompatible with network settings: %@. + Адрес сервера несовместим с сетевыми настройками: %@. + No comment provided by engineer. + Server requires authorization to create queues, check password Сервер требует авторизации для создания очередей, проверьте пароль @@ -5139,11 +5974,36 @@ Error: %@ Ошибка теста сервера! No comment provided by engineer. + + Server type + Тип сервера + No comment provided by engineer. + + + Server version is incompatible with network settings. + Версия сервера несовместима с настройками сети. + srv error text + + + Server version is incompatible with your app: %@. + Версия сервера несовместима с вашим приложением: %@. + No comment provided by engineer. + Servers Серверы No comment provided by engineer. + + Servers info + Информация о серверах + No comment provided by engineer. + + + Servers statistics will be reset - this cannot be undone! + Статистика серверов будет сброшена - это нельзя отменить! + No comment provided by engineer. + Session code Код сессии @@ -5159,6 +6019,11 @@ Error: %@ Имя контакта… No comment provided by engineer. + + Set default theme + Установить тему по умолчанию + No comment provided by engineer. + Set group preferences Предпочтения группы @@ -5224,6 +6089,11 @@ Error: %@ Поделиться адресом с контактами? No comment provided by engineer. + + Share from other apps. + Поделитесь из других приложений. + No comment provided by engineer. + Share link Поделиться ссылкой @@ -5234,6 +6104,11 @@ Error: %@ Поделиться одноразовой ссылкой-приглашением No comment provided by engineer. + + Share to SimpleX + Поделиться в SimpleX + No comment provided by engineer. + Share with contacts Поделиться с контактами @@ -5259,16 +6134,36 @@ Error: %@ Показывать последние сообщения No comment provided by engineer. + + Show message status + Показать статус сообщения + No comment provided by engineer. + + + Show percentage + Показать процент + No comment provided by engineer. + Show preview Показывать уведомления No comment provided by engineer. + + Show → on messages sent via private routing. + Показать → на сообщениях доставленных конфиденциально. + No comment provided by engineer. + Show: Показать: No comment provided by engineer. + + SimpleX + SimpleX + No comment provided by engineer. + SimpleX Address Адрес SimpleX @@ -5344,6 +6239,11 @@ Error: %@ Упрощенный режим Инкогнито No comment provided by engineer. + + Size + Размер + No comment provided by engineer. + Skip Пропустить @@ -5359,11 +6259,26 @@ Error: %@ Маленькие группы (до 20) No comment provided by engineer. + + Soft + Слабое + blur media + + + Some file(s) were not exported: + Некоторые файл(ы) не были экспортированы: + No comment provided by engineer. + Some non-fatal errors occurred during import - you may see Chat console for more details. Во время импорта произошли некоторые ошибки - для получения более подробной информации вы можете обратиться к консоли. No comment provided by engineer. + + Some non-fatal errors occurred during import: + Во время импорта произошли некоторые ошибки: + No comment provided by engineer. + Somebody Контакт @@ -5389,6 +6304,16 @@ Error: %@ Запустить перемещение данных No comment provided by engineer. + + Starting from %@. + Начиная с %@. + No comment provided by engineer. + + + Statistics + Статистика + No comment provided by engineer. + Stop Остановить @@ -5449,11 +6374,31 @@ Error: %@ Остановка чата No comment provided by engineer. + + Strong + Сильное + blur media + Submit Продолжить No comment provided by engineer. + + Subscribed + Подписано + No comment provided by engineer. + + + Subscription errors + Ошибки подписки + No comment provided by engineer. + + + Subscriptions ignored + Подписок игнорировано + No comment provided by engineer. + Support SimpleX Chat Поддержать SimpleX Chat @@ -5469,6 +6414,11 @@ Error: %@ Системная аутентификация No comment provided by engineer. + + TCP connection + TCP-соединение + No comment provided by engineer. + TCP connection timeout Таймаут TCP соединения @@ -5529,9 +6479,9 @@ Error: %@ Нажмите, чтобы сканировать No comment provided by engineer. - - Tap to start a new chat - Нажмите, чтобы начать чат + + Temporary file error + Временная ошибка файла No comment provided by engineer. @@ -5586,6 +6536,11 @@ It can happen because of some bug or when the connection is compromised.Приложение может посылать Вам уведомления о сообщениях и запросах на соединение - уведомления можно включить в Настройках. No comment provided by engineer. + + The app will ask to confirm downloads from unknown file servers (except .onion). + Приложение будет запрашивать подтверждение загрузки с неизвестных серверов (за исключением .onion адресов). + No comment provided by engineer. + The attempt to change database passphrase was not completed. Попытка поменять пароль базы данных не была завершена. @@ -5631,6 +6586,16 @@ It can happen because of some bug or when the connection is compromised.Сообщение будет помечено как удаленное для всех членов группы. No comment provided by engineer. + + The messages will be deleted for all members. + Сообщения будут удалены для всех членов группы. + No comment provided by engineer. + + + The messages will be marked as moderated for all members. + Сообщения будут помечены как удаленные для всех членов группы. + No comment provided by engineer. + The next generation of private messaging Новое поколение приватных сообщений @@ -5666,9 +6631,9 @@ It can happen because of some bug or when the connection is compromised.Вставленный текст не является SimpleX-ссылкой. No comment provided by engineer. - - Theme - Тема + + Themes + Темы No comment provided by engineer. @@ -5736,11 +6701,21 @@ It can happen because of some bug or when the connection is compromised.Это ваша собственная одноразовая ссылка! No comment provided by engineer. + + This link was used with another mobile device, please create a new link on the desktop. + Эта ссылка была использована на другом мобильном, пожалуйста, создайте новую ссылку на компьютере. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. Эта настройка применяется к сообщениям в Вашем текущем профиле чата **%@**. No comment provided by engineer. + + Title + Заголовок + No comment provided by engineer. + To ask any questions and to receive updates: Чтобы задать вопросы и получать уведомления о новых версиях, @@ -5771,6 +6746,11 @@ It can happen because of some bug or when the connection is compromised.Чтобы защитить Ваш часовой пояс, файлы картинок и голосовых сообщений используют UTC. No comment provided by engineer. + + To protect your IP address, private routing uses your SMP servers to deliver messages. + Чтобы защитить ваш IP адрес, приложение использует Ваши SMP серверы для конфиденциальной доставки сообщений. + No comment provided by engineer. + To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5798,16 +6778,36 @@ You will be prompted to complete authentication before this feature is enabled.< Чтобы подтвердить end-to-end шифрование с Вашим контактом сравните (или сканируйте) код безопасности на Ваших устройствах. No comment provided by engineer. + + Toggle chat list: + Переключите список чатов: + No comment provided by engineer. + Toggle incognito when connecting. Установите режим Инкогнито при соединении. No comment provided by engineer. + + Toolbar opacity + Прозрачность тулбара + No comment provided by engineer. + + + Total + Всего + No comment provided by engineer. + Transport isolation Отдельные сессии для No comment provided by engineer. + + Transport sessions + Транспортные сессии + No comment provided by engineer. + Trying to connect to the server used to receive messages from this contact (error: %@). Устанавливается соединение с сервером, через который Вы получаете сообщения от этого контакта (ошибка: %@). @@ -5863,11 +6863,6 @@ You will be prompted to complete authentication before this feature is enabled.< Разблокировать члена группы? No comment provided by engineer. - - Unexpected error: %@ - Неожиданная ошибка: %@ - item status description - Unexpected migration state Неожиданная ошибка при перемещении данных чата @@ -5876,7 +6871,7 @@ You will be prompted to complete authentication before this feature is enabled.< Unfav. Не избр. - No comment provided by engineer. + swipe action Unhide @@ -5913,6 +6908,11 @@ You will be prompted to complete authentication before this feature is enabled.< Неизвестная ошибка No comment provided by engineer. + + Unknown servers! + Неизвестные серверы! + No comment provided by engineer. + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. Если Вы не используете интерфейс iOS, включите режим Не отвлекать, чтобы звонок не прерывался. @@ -5948,12 +6948,12 @@ To connect, please ask your contact to create another connection link and check Unmute Уведомлять - No comment provided by engineer. + swipe action Unread Не прочитано - No comment provided by engineer. + swipe action Up to 100 last messages are sent to new members. @@ -5965,11 +6965,6 @@ To connect, please ask your contact to create another connection link and check Обновить No comment provided by engineer. - - Update .onion hosts setting? - Обновить настройки .onion хостов? - No comment provided by engineer. - Update database passphrase Поменять пароль @@ -5980,9 +6975,9 @@ To connect, please ask your contact to create another connection link and check Обновить настройки сети? No comment provided by engineer. - - Update transport isolation mode? - Обновить режим отдельных сессий? + + Update settings? + Обновить настройки? No comment provided by engineer. @@ -5990,16 +6985,16 @@ To connect, please ask your contact to create another connection link and check Обновление настроек приведет к сбросу и установке нового соединения со всеми серверами. No comment provided by engineer. - - Updating this setting will re-connect the client to all servers. - Обновление этих настроек приведет к сбросу и установке нового соединения со всеми серверами. - No comment provided by engineer. - Upgrade and open chat Обновить и открыть чат No comment provided by engineer. + + Upload errors + Ошибки загрузки + No comment provided by engineer. + Upload failed Ошибка загрузки @@ -6010,6 +7005,16 @@ To connect, please ask your contact to create another connection link and check Загрузка файла server test step + + Uploaded + Загружено + No comment provided by engineer. + + + Uploaded files + Отправленные файлы + No comment provided by engineer. + Uploading archive Загрузка архива @@ -6060,6 +7065,16 @@ To connect, please ask your contact to create another connection link and check Использовать только локальные нотификации? No comment provided by engineer. + + Use private routing with unknown servers when IP address is not protected. + Использовать конфиденциальную доставку с неизвестными серверами, когда IP адрес не защищен. + No comment provided by engineer. + + + Use private routing with unknown servers. + Использовать конфиденциальную доставку с неизвестными серверами. + No comment provided by engineer. + Use server Использовать сервер @@ -6070,14 +7085,19 @@ To connect, please ask your contact to create another connection link and check Используйте приложение во время звонка. No comment provided by engineer. + + Use the app with one hand. + Используйте приложение одной рукой. + No comment provided by engineer. + User profile Профиль чата No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - Для использования .onion хостов требуется совместимый VPN провайдер. + + User selection + Выбор пользователя No comment provided by engineer. @@ -6210,6 +7230,16 @@ To connect, please ask your contact to create another connection link and check Ожидание видео No comment provided by engineer. + + Wallpaper accent + Рисунок обоев + No comment provided by engineer. + + + Wallpaper background + Фон обоев + No comment provided by engineer. + Warning: starting chat on multiple devices is not supported and will cause message delivery failures Внимание: запуск чата на нескольких устройствах не поддерживается и приведет к сбоям доставки сообщений @@ -6295,19 +7325,39 @@ To connect, please ask your contact to create another connection link and check С уменьшенным потреблением батареи. No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + Без Тора или ВПН, Ваш IP адрес будет доступен серверам файлов. + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + Без Тора или ВПН, Ваш IP адрес будет доступен этим серверам файлов: %@. + No comment provided by engineer. + Wrong database passphrase Неправильный пароль базы данных No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + Неверный ключ или неизвестное соединение - скорее всего, это соединение удалено. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + Неверный ключ или неизвестный адрес блока файла - скорее всего, файл удален. + file error text + Wrong passphrase! Неправильный пароль! No comment provided by engineer. - - XFTP servers - XFTP серверы + + XFTP server + XFTP сервер No comment provided by engineer. @@ -6387,11 +7437,21 @@ Repeat join request? Вы приглашены в группу No comment provided by engineer. + + You are not connected to these servers. Private routing is used to deliver messages to them. + Вы не подключены к этим серверам. Для доставки сообщений на них используется конфиденциальная доставка. + No comment provided by engineer. + You can accept calls from lock screen, without device and app authentication. Вы можете принимать звонки на экране блокировки, без аутентификации. No comment provided by engineer. + + You can change it in Appearance settings. + Вы можете изменить это в настройках Интерфейса. + No comment provided by engineer. + You can create it later Вы можете создать его позже @@ -6422,11 +7482,16 @@ Repeat join request? Вы можете сделать его видимым для ваших контактов в SimpleX через Настройки. No comment provided by engineer. - - You can now send messages to %@ - Вы теперь можете отправлять сообщения %@ + + You can now chat with %@ + Вы теперь можете общаться с %@ notification body + + You can send messages to %@ from Archived contacts. + Вы можете отправлять сообщения %@ из Архивированных контактов. + No comment provided by engineer. + You can set lock screen notification preview via settings. Вы можете установить просмотр уведомлений на экране блокировки в настройках. @@ -6452,6 +7517,11 @@ Repeat join request? Вы можете запустить чат через Настройки приложения или перезапустив приложение. No comment provided by engineer. + + You can still view conversation with %@ in the list of chats. + Вы по-прежнему можете просмотреть разговор с %@ в списке чатов. + No comment provided by engineer. + You can turn on SimpleX Lock via Settings. Вы можете включить Блокировку SimpleX через Настройки. @@ -6494,11 +7564,6 @@ Repeat connection request? Повторить запрос? No comment provided by engineer. - - You have no chats - У Вас нет чатов - No comment provided by engineer. - You have to enter passphrase every time the app starts - it is not stored on the device. Пароль не сохранен на устройстве — Вы будете должны ввести его при каждом запуске чата. @@ -6519,11 +7584,26 @@ Repeat connection request? Вы вступили в эту группу. Устанавливается соединение с пригласившим членом группы. No comment provided by engineer. + + You may migrate the exported database. + Вы можете мигрировать экспортированную базу данных. + No comment provided by engineer. + + + You may save the exported archive. + Вы можете сохранить экспортированный архив. + No comment provided by engineer. + You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. Вы должны всегда использовать самую новую версию данных чата, ТОЛЬКО на одном устройстве, иначе Вы можете перестать получать сообщения от каких то контактов. No comment provided by engineer. + + You need to allow your contact to call to be able to call them. + Чтобы включить звонки, разрешите их Вашему контакту. + No comment provided by engineer. + You need to allow your contact to send voice messages to be able to send them. Чтобы включить отправку голосовых сообщений, разрешите их Вашему контакту. @@ -6639,13 +7719,6 @@ Repeat connection request? Ваши профили чата No comment provided by engineer. - - Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link). - Ваш контакт должен быть в сети чтобы установить соединение. -Вы можете отменить соединение и удалить контакт (и попробовать позже с другой ссылкой). - No comment provided by engineer. - Your contact sent a file that is larger than currently supported maximum size (%@). Ваш контакт отправил файл, размер которого превышает максимальный размер (%@). @@ -6793,6 +7866,11 @@ SimpleX серверы не могут получить доступ к Ваше и %lld других событий No comment provided by engineer. + + attempts + попытки + No comment provided by engineer. + audio call (not e2e encrypted) аудиозвонок (не e2e зашифрованный) @@ -6833,6 +7911,11 @@ SimpleX серверы не могут получить доступ к Ваше жирный No comment provided by engineer. + + call + звонок + No comment provided by engineer. + call error ошибка звонка @@ -6983,6 +8066,11 @@ SimpleX серверы не могут получить доступ к Ваше дней time unit + + decryption errors + ошибки расшифровки + No comment provided by engineer. + default (%@) по умолчанию (%@) @@ -7033,6 +8121,11 @@ SimpleX серверы не могут получить доступ к Ваше повторное сообщение integrity error chat item + + duplicates + дубликаты + No comment provided by engineer. + e2e encrypted e2e зашифровано @@ -7113,6 +8206,11 @@ SimpleX серверы не могут получить доступ к Ваше событие произошло No comment provided by engineer. + + expired + истекло + No comment provided by engineer. + forwarded переслано @@ -7143,6 +8241,11 @@ SimpleX серверы не могут получить доступ к Ваше Пароль базы данных будет безопасно сохранен в iOS Keychain после запуска чата или изменения пароля - это позволит получать мгновенные уведомления. No comment provided by engineer. + + inactive + неактивен + No comment provided by engineer. + incognito via contact address link инкогнито через ссылку-контакт @@ -7183,6 +8286,11 @@ SimpleX серверы не могут получить доступ к Ваше приглашение в группу %@ group name + + invite + пригласить + No comment provided by engineer. + invited приглашен(а) @@ -7238,6 +8346,11 @@ SimpleX серверы не могут получить доступ к Ваше соединен(а) rcv group event chat item + + message + написать + No comment provided by engineer. + message received получено сообщение @@ -7268,6 +8381,11 @@ SimpleX серверы не могут получить доступ к Ваше месяцев time unit + + mute + без звука + No comment provided by engineer. + never никогда @@ -7320,6 +8438,16 @@ SimpleX серверы не могут получить доступ к Ваше да group pref value + + other + другое + No comment provided by engineer. + + + other errors + другие ошибки + No comment provided by engineer. + owner владелец @@ -7390,6 +8518,11 @@ SimpleX серверы не могут получить доступ к Ваше сохранено из %@ No comment provided by engineer. + + search + поиск + No comment provided by engineer. + sec сек @@ -7415,6 +8548,15 @@ SimpleX серверы не могут получить доступ к Ваше отправьте сообщение No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + информация сервера об очереди: %1$@ + +последнее полученное сообщение: %2$@ + queue info + set new contact address установлен новый адрес контакта @@ -7455,11 +8597,26 @@ SimpleX серверы не могут получить доступ к Ваше неизвестно connection info + + unknown servers + неизвестные серверы + No comment provided by engineer. + unknown status неизвестный статус No comment provided by engineer. + + unmute + уведомлять + No comment provided by engineer. + + + unprotected + незащищённый + No comment provided by engineer. + updated group profile обновил(а) профиль группы @@ -7500,6 +8657,11 @@ SimpleX серверы не могут получить доступ к Ваше через relay сервер No comment provided by engineer. + + video + видеозвонок + No comment provided by engineer. + video call (not e2e encrypted) видеозвонок (не e2e зашифрованный) @@ -7525,6 +8687,11 @@ SimpleX серверы не могут получить доступ к Ваше недель time unit + + when IP hidden + когда IP защищен + No comment provided by engineer. + yes да @@ -7609,7 +8776,7 @@ SimpleX серверы не могут получить доступ к Ваше
- +
@@ -7646,7 +8813,7 @@ SimpleX серверы не могут получить доступ к Ваше
- +
@@ -7666,4 +8833,218 @@ SimpleX серверы не могут получить доступ к Ваше
+ +
+ +
+ + + SimpleX SE + SimpleX SE + Bundle display name + + + SimpleX SE + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright © 2024 SimpleX Chat. Все права защищены. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + %@ + No comment provided by engineer. + + + App is locked! + Приложение заблокировано! + No comment provided by engineer. + + + Cancel + Отменить + No comment provided by engineer. + + + Cannot access keychain to save database password + Невозможно сохранить пароль в keychain + No comment provided by engineer. + + + Cannot forward message + Невозможно переслать сообщение + No comment provided by engineer. + + + Comment + Комментарий + No comment provided by engineer. + + + Currently maximum supported file size is %@. + В настоящее время максимальный поддерживаемый размер файла составляет %@. + No comment provided by engineer. + + + Database downgrade required + Требуется откат базы данных + No comment provided by engineer. + + + Database encrypted! + База данных зашифрована! + No comment provided by engineer. + + + Database error + Ошибка базы данных + No comment provided by engineer. + + + Database passphrase is different from saved in the keychain. + Пароль базы данных отличается от сохраненного в keychain. + No comment provided by engineer. + + + Database passphrase is required to open chat. + Введите пароль базы данных, чтобы открыть чат. + No comment provided by engineer. + + + Database upgrade required + Требуется обновление базы данных + No comment provided by engineer. + + + Error preparing file + Ошибка подготовки файла + No comment provided by engineer. + + + Error preparing message + Ошибка подготовки сообщения + No comment provided by engineer. + + + Error: %@ + Ошибка: %@ + No comment provided by engineer. + + + File error + Ошибка файла + No comment provided by engineer. + + + Incompatible database version + Несовместимая версия базы данных + No comment provided by engineer. + + + Invalid migration confirmation + Ошибка подтверждения миграции + No comment provided by engineer. + + + Keychain error + Ошибка keychain + No comment provided by engineer. + + + Large file! + Большой файл! + No comment provided by engineer. + + + No active profile + Нет активного профиля + No comment provided by engineer. + + + Ok + Ок + No comment provided by engineer. + + + Open the app to downgrade the database. + Откройте приложение, чтобы откатить базу данных. + No comment provided by engineer. + + + Open the app to upgrade the database. + Откройте приложение, чтобы обновить базу данных. + No comment provided by engineer. + + + Passphrase + Пароль + No comment provided by engineer. + + + Please create a profile in the SimpleX app + Пожалуйста, создайте профиль в приложении SimpleX. + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + Выбранные настройки чата запрещают это сообщение. + No comment provided by engineer. + + + Sending a message takes longer than expected. + Отправка сообщения занимает дольше ожиданного. + No comment provided by engineer. + + + Sending message… + Отправка сообщения… + No comment provided by engineer. + + + Share + Поделиться + No comment provided by engineer. + + + Slow network? + Медленная сеть? + No comment provided by engineer. + + + Unknown database error: %@ + Неизвестная ошибка базы данных: %@ + No comment provided by engineer. + + + Unsupported format + Неподдерживаемый формат + No comment provided by engineer. + + + Wait + Подождать + No comment provided by engineer. + + + Wrong database passphrase + Неправильный пароль базы данных + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + Вы можете разрешить функцию Поделиться в настройках Конфиденциальности / Блокировка SimpleX. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/contents.json b/apps/ios/SimpleX Localizations/ru.xcloc/contents.json index 2d5d76dd8f..a28b0ed489 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/ru.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "ru", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "15F31d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "15.4" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff index 1f62fad60f..646a94a337 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff +++ b/apps/ios/SimpleX Localizations/th.xcloc/Localized Contents/th.xliff @@ -2,7 +2,7 @@
- +
@@ -120,11 +120,6 @@ %@ ได้รับการตรวจสอบแล้ว No comment provided by engineer. - - %@ servers - %@ เซิร์ฟเวอร์ - No comment provided by engineer. - %@ uploaded No comment provided by engineer. @@ -527,16 +522,16 @@ เกี่ยวกับที่อยู่ SimpleX No comment provided by engineer. - - Accent color - สีเน้น + + Accent No comment provided by engineer. Accept รับ accept contact request via notification - accept incoming call via notification + accept incoming call via notification + swipe action Accept connection request? @@ -550,7 +545,20 @@ Accept incognito ยอมรับโหมดไม่ระบุตัวตน - accept contact request via notification + accept contact request via notification + swipe action + + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + No comment provided by engineer. + + + Active connections + No comment provided by engineer. Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. @@ -571,16 +579,16 @@ เพิ่มโปรไฟล์ No comment provided by engineer. + + Add server + เพิ่มเซิร์ฟเวอร์ + No comment provided by engineer. + Add servers by scanning QR codes. เพิ่มเซิร์ฟเวอร์โดยการสแกนรหัสคิวอาร์โค้ด No comment provided by engineer. - - Add server… - เพิ่มเซิร์ฟเวอร์… - No comment provided by engineer. - Add to another device เพิ่มเข้าไปในอุปกรณ์อื่น @@ -591,6 +599,18 @@ เพิ่มข้อความต้อนรับ No comment provided by engineer. + + Additional accent + No comment provided by engineer. + + + Additional accent 2 + No comment provided by engineer. + + + Additional secondary + No comment provided by engineer. + Address ที่อยู่ @@ -615,6 +635,10 @@ การตั้งค่าระบบเครือข่ายขั้นสูง No comment provided by engineer. + + Advanced settings + No comment provided by engineer. + All app data is deleted. ข้อมูลแอปทั้งหมดถูกลบแล้ว. @@ -630,6 +654,10 @@ ข้อมูลทั้งหมดจะถูกลบเมื่อถูกป้อน No comment provided by engineer. + + All data is private to your device. + No comment provided by engineer. + All group members will remain connected. สมาชิกในกลุ่มทุกคนจะยังคงเชื่อมต่ออยู่. @@ -648,6 +676,10 @@ All new messages from %@ will be hidden! No comment provided by engineer. + + All profiles + No comment provided by engineer. + All your contacts will remain connected. ผู้ติดต่อทั้งหมดของคุณจะยังคงเชื่อมต่ออยู่. @@ -672,11 +704,19 @@ อนุญาตการโทรเฉพาะเมื่อผู้ติดต่อของคุณอนุญาตเท่านั้น. No comment provided by engineer. + + Allow calls? + No comment provided by engineer. + Allow disappearing messages only if your contact allows it to you. อนุญาตให้ข้อความที่หายไปเฉพาะในกรณีที่ผู้ติดต่อของคุณอนุญาตเท่านั้น. No comment provided by engineer. + + Allow downgrade + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) อนุญาตให้ลบข้อความแบบถาวรเฉพาะในกรณีที่ผู้ติดต่อของคุณอนุญาตให้คุณเท่านั้น @@ -702,6 +742,10 @@ อนุญาตให้ส่งข้อความที่จะหายไปหลังปิดแชท (disappearing message) No comment provided by engineer. + + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) อนุญาตให้ลบข้อความที่ส่งไปแล้วอย่างถาวร @@ -769,6 +813,10 @@ Already joining the group! No comment provided by engineer. + + Always use private routing. + No comment provided by engineer. + Always use relay ใช้รีเลย์เสมอ @@ -831,10 +879,22 @@ Apply No comment provided by engineer. + + Apply to + No comment provided by engineer. + Archive and upload No comment provided by engineer. + + Archive contacts to chat later. + No comment provided by engineer. + + + Archived contacts + No comment provided by engineer. + Archiving database No comment provided by engineer. @@ -904,6 +964,10 @@ กลับ No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address No comment provided by engineer. @@ -927,6 +991,14 @@ ข้อความที่ดีขึ้น No comment provided by engineer. + + Better networking + No comment provided by engineer. + + + Black + No comment provided by engineer. + Block No comment provided by engineer. @@ -955,6 +1027,14 @@ Blocked by admin No comment provided by engineer. + + Blur for better privacy. + No comment provided by engineer. + + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. ทั้งคุณและผู้ติดต่อของคุณสามารถเพิ่มปฏิกิริยาของข้อความได้ @@ -999,10 +1079,22 @@ โทร No comment provided by engineer. + + Calls prohibited! + No comment provided by engineer. + Camera not available No comment provided by engineer. + + Can't call contact + No comment provided by engineer. + + + Can't call member + No comment provided by engineer. + Can't invite contact! ไม่สามารถเชิญผู้ติดต่อได้! @@ -1013,6 +1105,10 @@ ไม่สามารถเชิญผู้ติดต่อได้! No comment provided by engineer. + + Can't message member + No comment provided by engineer. + Cancel ยกเลิก @@ -1027,11 +1123,19 @@ ไม่สามารถเข้าถึง keychain เพื่อบันทึกรหัสผ่านฐานข้อมูล No comment provided by engineer. + + Cannot forward message + No comment provided by engineer. + Cannot receive file ไม่สามารถรับไฟล์ได้ No comment provided by engineer. + + Capacity exceeded - recipient did not receive previously sent messages. + snd error text + Cellular No comment provided by engineer. @@ -1092,6 +1196,10 @@ ที่เก็บแชทถาวร No comment provided by engineer. + + Chat colors + No comment provided by engineer. + Chat console คอนโซลแชท @@ -1107,6 +1215,10 @@ ลบฐานข้อมูลแชทแล้ว No comment provided by engineer. + + Chat database exported + No comment provided by engineer. + Chat database imported นำฐานข้อมูลแชทเข้าแล้ว @@ -1126,6 +1238,10 @@ Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat. No comment provided by engineer. + + Chat list + No comment provided by engineer. + Chat migrated! No comment provided by engineer. @@ -1135,6 +1251,10 @@ ค่ากําหนดในการแชท No comment provided by engineer. + + Chat theme + No comment provided by engineer. + Chats แชท @@ -1164,10 +1284,22 @@ เลือกจากอัลบั้ม No comment provided by engineer. + + Chunks deleted + No comment provided by engineer. + + + Chunks downloaded + No comment provided by engineer. + + + Chunks uploaded + No comment provided by engineer. + Clear ลบ - No comment provided by engineer. + swipe action Clear conversation @@ -1188,9 +1320,12 @@ ล้างการยืนยัน No comment provided by engineer. - - Colors - สี + + Color chats with the new themes. + No comment provided by engineer. + + + Color mode No comment provided by engineer. @@ -1203,11 +1338,19 @@ เปรียบเทียบรหัสความปลอดภัยกับผู้ติดต่อของคุณ No comment provided by engineer. + + Completed + No comment provided by engineer. + Configure ICE servers กำหนดค่าเซิร์ฟเวอร์ ICE No comment provided by engineer. + + Configured %@ servers + No comment provided by engineer. + Confirm ยืนยัน @@ -1218,11 +1361,19 @@ ยืนยันรหัสผ่าน No comment provided by engineer. + + Confirm contact deletion? + No comment provided by engineer. + Confirm database upgrades ยืนยันการอัพเกรดฐานข้อมูล No comment provided by engineer. + + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings No comment provided by engineer. @@ -1262,6 +1413,10 @@ Connect to desktop No comment provided by engineer. + + Connect to your friends faster. + No comment provided by engineer. + Connect to yourself? No comment provided by engineer. @@ -1293,14 +1448,26 @@ This is your own one-time link! Connect with %@ No comment provided by engineer. + + Connected + No comment provided by engineer. + Connected desktop No comment provided by engineer. + + Connected servers + No comment provided by engineer. + Connected to desktop No comment provided by engineer. + + Connecting + No comment provided by engineer. + Connecting to server… กำลังเชื่อมต่อกับเซิร์ฟเวอร์… @@ -1311,6 +1478,10 @@ This is your own one-time link! กำลังเชื่อมต่อกับเซิร์ฟเวอร์... (ข้อผิดพลาด: %@) No comment provided by engineer. + + Connecting to contact, please wait or check later! + No comment provided by engineer. + Connecting to desktop No comment provided by engineer. @@ -1320,6 +1491,10 @@ This is your own one-time link! การเชื่อมต่อ No comment provided by engineer. + + Connection and servers status. + No comment provided by engineer. + Connection error การเชื่อมต่อผิดพลาด @@ -1330,6 +1505,10 @@ This is your own one-time link! การเชื่อมต่อผิดพลาด (AUTH) No comment provided by engineer. + + Connection notifications + No comment provided by engineer. + Connection request sent! ส่งคําขอเชื่อมต่อแล้ว! @@ -1344,6 +1523,14 @@ This is your own one-time link! หมดเวลาการเชื่อมต่อ No comment provided by engineer. + + Connection with desktop stopped + No comment provided by engineer. + + + Connections + No comment provided by engineer. + Contact allows ผู้ติดต่ออนุญาต @@ -1354,6 +1541,10 @@ This is your own one-time link! ผู้ติดต่อรายนี้มีอยู่แล้ว No comment provided by engineer. + + Contact deleted! + No comment provided by engineer. + Contact hidden: ผู้ติดต่อถูกซ่อน: @@ -1364,9 +1555,8 @@ This is your own one-time link! เชื่อมต่อกับผู้ติดต่อแล้ว notification - - Contact is not connected yet! - ผู้ติดต่อยังไม่ได้เชื่อมต่อ! + + Contact is deleted. No comment provided by engineer. @@ -1379,6 +1569,10 @@ This is your own one-time link! การกําหนดลักษณะการติดต่อ No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + No comment provided by engineer. + Contacts ติดต่อ @@ -1394,10 +1588,18 @@ This is your own one-time link! ดำเนินการต่อ No comment provided by engineer. + + Conversation deleted! + No comment provided by engineer. + Copy คัดลอก - chat item action + No comment provided by engineer. + + + Copy error + No comment provided by engineer. Core version: v%@ @@ -1469,6 +1671,10 @@ This is your own one-time link! สร้างโปรไฟล์ของคุณ No comment provided by engineer. + + Created + No comment provided by engineer. + Created at No comment provided by engineer. @@ -1500,6 +1706,10 @@ This is your own one-time link! รหัสผ่านปัจจุบัน… No comment provided by engineer. + + Current profile + No comment provided by engineer. + Currently maximum supported file size is %@. ขนาดไฟล์ที่รองรับสูงสุดในปัจจุบันคือ %@ @@ -1510,11 +1720,19 @@ This is your own one-time link! เวลาที่กําหนดเอง No comment provided by engineer. + + Customize theme + No comment provided by engineer. + Dark มืด No comment provided by engineer. + + Dark mode colors + No comment provided by engineer. + Database ID ID ฐานข้อมูล @@ -1613,6 +1831,10 @@ This is your own one-time link! ระบบจะย้ายฐานข้อมูลเมื่อแอปรีสตาร์ท No comment provided by engineer. + + Debug delivery + No comment provided by engineer. + Decentralized กระจายอำนาจแล้ว @@ -1626,17 +1848,17 @@ This is your own one-time link! Delete ลบ - chat item action + chat item action + swipe action + + + Delete %lld messages of members? + No comment provided by engineer. Delete %lld messages? No comment provided by engineer. - - Delete Contact - ลบผู้ติดต่อ - No comment provided by engineer. - Delete address ลบที่อยู่ @@ -1691,9 +1913,8 @@ This is your own one-time link! ลบผู้ติดต่อ No comment provided by engineer. - - Delete contact? -This cannot be undone! + + Delete contact? No comment provided by engineer. @@ -1785,11 +2006,6 @@ This cannot be undone! ลบฐานข้อมูลเก่า? No comment provided by engineer. - - Delete pending connection - ลบการเชื่อมต่อที่รอดำเนินการ - No comment provided by engineer. - Delete pending connection? ลบการเชื่อมต่อที่รอดำเนินการหรือไม่? @@ -1805,11 +2021,23 @@ This cannot be undone! ลบคิว server test step + + Delete up to 20 messages at once. + No comment provided by engineer. + Delete user profile? ลบโปรไฟล์ผู้ใช้? No comment provided by engineer. + + Delete without notification + No comment provided by engineer. + + + Deleted + No comment provided by engineer. + Deleted at ลบที่ @@ -1820,6 +2048,10 @@ This cannot be undone! ลบที่: %@ copied message info + + Deletion errors + No comment provided by engineer. + Delivery No comment provided by engineer. @@ -1851,11 +2083,35 @@ This cannot be undone! Desktop devices No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + No comment provided by engineer. + + + Destination server error: %@ + snd error text + + + Destination server version of %@ is incompatible with forwarding server %@. + No comment provided by engineer. + + + Detailed statistics + No comment provided by engineer. + + + Details + No comment provided by engineer. + Develop พัฒนา No comment provided by engineer. + + Developer options + No comment provided by engineer. + Developer tools เครื่องมือสำหรับนักพัฒนา @@ -1906,6 +2162,10 @@ This cannot be undone! ปิดการใช้งานสำหรับทุกคน No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message ข้อความที่จะหายไปหลังเวลาที่กําหนด (disappearing message) @@ -1953,11 +2213,19 @@ This cannot be undone! Discover via local network No comment provided by engineer. + + Do NOT send messages directly, even if your or destination server does not support private routing. + No comment provided by engineer. + Do NOT use SimpleX for emergency calls. อย่าใช้ SimpleX สําหรับการโทรฉุกเฉิน No comment provided by engineer. + + Do NOT use private routing. + No comment provided by engineer. + Do it later ทำในภายหลัง @@ -1991,6 +2259,10 @@ This cannot be undone! Download chat item action + + Download errors + No comment provided by engineer. + Download failed No comment provided by engineer. @@ -2000,6 +2272,14 @@ This cannot be undone! ดาวน์โหลดไฟล์ server test step + + Downloaded + No comment provided by engineer. + + + Downloaded files + No comment provided by engineer. + Downloading archive No comment provided by engineer. @@ -2096,6 +2376,10 @@ This cannot be undone! เปิดใช้งานรหัสผ่านแบบทําลายตัวเอง set passcode view + + Enabled + No comment provided by engineer. + Enabled for No comment provided by engineer. @@ -2256,6 +2540,10 @@ This cannot be undone! เกิดข้อผิดพลาดในการเปลี่ยนการตั้งค่า No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address เกิดข้อผิดพลาดในการสร้างที่อยู่ @@ -2303,11 +2591,6 @@ This cannot be undone! เกิดข้อผิดพลาดในการลบการเชื่อมต่อ No comment provided by engineer. - - Error deleting contact - เกิดข้อผิดพลาดในการลบผู้ติดต่อ - No comment provided by engineer. - Error deleting database เกิดข้อผิดพลาดในการลบฐานข้อมูล @@ -2352,6 +2635,10 @@ This cannot be undone! เกิดข้อผิดพลาดในการส่งออกฐานข้อมูลแชท No comment provided by engineer. + + Error exporting theme: %@ + No comment provided by engineer. + Error importing chat database เกิดข้อผิดพลาดในการนำเข้าฐานข้อมูลแชท @@ -2376,11 +2663,23 @@ This cannot be undone! เกิดข้อผิดพลาดในการรับไฟล์ No comment provided by engineer. + + Error reconnecting server + No comment provided by engineer. + + + Error reconnecting servers + No comment provided by engineer. + Error removing member เกิดข้อผิดพลาดในการลบสมาชิก No comment provided by engineer. + + Error resetting statistics + No comment provided by engineer. + Error saving %@ servers เกิดข้อผิดพลาดในการบันทึกเซิร์ฟเวอร์ %@ @@ -2494,7 +2793,8 @@ This cannot be undone! Error: %@ ข้อผิดพลาด: % @ - No comment provided by engineer. + file error text + snd error text Error: URL is invalid @@ -2506,6 +2806,10 @@ This cannot be undone! เกิดข้อผิดพลาด: ไม่มีแฟ้มฐานข้อมูล No comment provided by engineer. + + Errors + No comment provided by engineer. + Even when disabled in the conversation. แม้ในขณะที่ปิดใช้งานในการสนทนา @@ -2530,6 +2834,10 @@ This cannot be undone! ข้อผิดพลาดในการส่งออก: No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. ที่เก็บถาวรฐานข้อมูลที่ส่งออก @@ -2561,8 +2869,28 @@ This cannot be undone! Favorite ที่ชอบ + swipe action + + + File error No comment provided by engineer. + + File not found - most likely file was deleted or cancelled. + file error text + + + File server error: %@ + file error text + + + File status + No comment provided by engineer. + + + File status: %@ + copied message info + File will be deleted from servers. ไฟล์จะถูกลบออกจากเซิร์ฟเวอร์ @@ -2583,6 +2911,10 @@ This cannot be undone! ไฟล์: % @ No comment provided by engineer. + + Files + No comment provided by engineer. + Files & media ไฟล์และสื่อ @@ -2681,6 +3013,28 @@ This cannot be undone! Forwarded from No comment provided by engineer. + + Forwarding server %@ failed to connect to destination server %@. Please try later. + No comment provided by engineer. + + + Forwarding server address is incompatible with network settings: %@. + No comment provided by engineer. + + + Forwarding server version is incompatible with network settings: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + snd error text + Found desktop No comment provided by engineer. @@ -2724,6 +3078,14 @@ This cannot be undone! GIFs และสติกเกอร์ No comment provided by engineer. + + Good afternoon! + message preview + + + Good morning! + message preview + Group กลุ่ม @@ -2998,6 +3360,10 @@ This cannot be undone! Import failed No comment provided by engineer. + + Import theme + No comment provided by engineer. + Importing archive No comment provided by engineer. @@ -3113,6 +3479,10 @@ This cannot be undone! อินเตอร์เฟซ No comment provided by engineer. + + Interface colors + No comment provided by engineer. + Invalid QR code No comment provided by engineer. @@ -3207,6 +3577,10 @@ This cannot be undone! 3. การเชื่อมต่อถูกบุกรุก No comment provided by engineer. + + It protects your IP address and connections. + No comment provided by engineer. + It seems like you are already connected via this link. If it is not the case, there was an error (%@). ดูเหมือนว่าคุณได้เชื่อมต่อผ่านลิงก์นี้แล้ว หากไม่เป็นเช่นนั้น แสดงว่ามีข้อผิดพลาด (%@). @@ -3225,7 +3599,7 @@ This cannot be undone! Join เข้าร่วม - No comment provided by engineer. + swipe action Join group @@ -3263,6 +3637,10 @@ This is your link for group %@! Keep No comment provided by engineer. + + Keep conversation + No comment provided by engineer. + Keep the app open to use it from desktop No comment provided by engineer. @@ -3304,7 +3682,7 @@ This is your link for group %@! Leave ออกจาก - No comment provided by engineer. + swipe action Leave group @@ -3433,11 +3811,23 @@ This is your link for group %@! สูงสุด 30 วินาที รับทันที No comment provided by engineer. + + Media & file servers + No comment provided by engineer. + + + Medium + blur media + Member สมาชิก No comment provided by engineer. + + Member inactive + item status text + Member role will be changed to "%@". All group members will be notified. บทบาทของสมาชิกจะถูกเปลี่ยนเป็น "%@" สมาชิกกลุ่มทั้งหมดจะได้รับแจ้ง @@ -3453,6 +3843,10 @@ This is your link for group %@! สมาชิกจะถูกลบออกจากกลุ่ม - ไม่สามารถยกเลิกได้! No comment provided by engineer. + + Menus + No comment provided by engineer. + Message delivery error ข้อผิดพลาดในการส่งข้อความ @@ -3463,11 +3857,27 @@ This is your link for group %@! ใบเสร็จการส่งข้อความ! No comment provided by engineer. + + Message delivery warning + item status text + Message draft ร่างข้อความ No comment provided by engineer. + + Message forwarded + item status text + + + Message may be delivered later if member becomes active. + item status description + + + Message queue info + No comment provided by engineer. + Message reactions ปฏิกิริยาของข้อความ @@ -3483,10 +3893,26 @@ This is your link for group %@! ปฏิกิริยาบนข้อความเป็นสิ่งต้องห้ามในกลุ่มนี้ No comment provided by engineer. + + Message reception + No comment provided by engineer. + + + Message servers + No comment provided by engineer. + Message source remains private. No comment provided by engineer. + + Message status + No comment provided by engineer. + + + Message status: %@ + copied message info + Message text ข้อความ @@ -3510,6 +3936,14 @@ This is your link for group %@! Messages from %@ will be shown! No comment provided by engineer. + + Messages received + No comment provided by engineer. + + + Messages sent + No comment provided by engineer. + Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. No comment provided by engineer. @@ -3599,11 +4033,6 @@ This is your link for group %@! Most likely this connection is deleted. item status description - - Most likely this contact has deleted the connection with you. - เป็นไปได้มากว่าผู้ติดต่อนี้ได้ลบการเชื่อมต่อกับคุณ - No comment provided by engineer. - Multiple chat profiles โปรไฟล์การแชทหลายรายการ @@ -3612,7 +4041,7 @@ This is your link for group %@! Mute ปิดเสียง - No comment provided by engineer. + swipe action Muted when inactive! @@ -3622,7 +4051,7 @@ This is your link for group %@! Name ชื่อ - No comment provided by engineer. + swipe action Network & servers @@ -3633,6 +4062,10 @@ This is your link for group %@! Network connection No comment provided by engineer. + + Network issues - message expired after many attempts to send it. + snd error text + Network management No comment provided by engineer. @@ -3656,6 +4089,10 @@ This is your link for group %@! New chat No comment provided by engineer. + + New chat experience 🎉 + No comment provided by engineer. + New contact request คำขอติดต่อใหม่ @@ -3685,6 +4122,10 @@ This is your link for group %@! ใหม่ใน %@ No comment provided by engineer. + + New media options + No comment provided by engineer. + New member role บทบาทของสมาชิกใหม่ @@ -3729,6 +4170,10 @@ This is your link for group %@! ไม่มีโทเค็นอุปกรณ์! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + item status description + No filtered chats ไม่มีการกรองการแชท @@ -3744,6 +4189,10 @@ This is your link for group %@! ไม่มีประวัติ No comment provided by engineer. + + No info, try to reload + No comment provided by engineer. + No network connection No comment provided by engineer. @@ -3762,6 +4211,10 @@ This is your link for group %@! Not compatible! No comment provided by engineer. + + Nothing selected + No comment provided by engineer. + Notifications การแจ้งเตือน @@ -3788,7 +4241,7 @@ This is your link for group %@! Off ปิด - No comment provided by engineer. + blur media Ok @@ -3810,13 +4263,15 @@ This is your link for group %@! ลิงก์คำเชิญแบบใช้ครั้งเดียว No comment provided by engineer. - - Onion hosts will be required for connection. Requires enabling VPN. + + Onion hosts will be **required** for connection. +Requires compatible VPN. จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ ต้องเปิดใช้งาน VPN สำหรับการเชื่อมต่อ No comment provided by engineer. - - Onion hosts will be used when available. Requires enabling VPN. + + Onion hosts will be used when available. +Requires compatible VPN. จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ ต้องเปิดใช้งาน VPN สำหรับการเชื่อมต่อ No comment provided by engineer. @@ -3830,6 +4285,10 @@ This is your link for group %@! เฉพาะอุปกรณ์ไคลเอนต์เท่านั้นที่จัดเก็บโปรไฟล์ผู้ใช้ ผู้ติดต่อ กลุ่ม และข้อความที่ส่งด้วย **การเข้ารหัส encrypt แบบ 2 ชั้น** No comment provided by engineer. + + Only delete conversation + No comment provided by engineer. + Only group owners can change group preferences. เฉพาะเจ้าของกลุ่มเท่านั้นที่สามารถเปลี่ยนค่ากําหนดลักษณะกลุ่มได้ @@ -3922,6 +4381,10 @@ This is your link for group %@! Open migration to another device authentication reason + + Open server settings + No comment provided by engineer. + Open user profiles เปิดโปรไฟล์ผู้ใช้ @@ -3956,6 +4419,10 @@ This is your link for group %@! Other No comment provided by engineer. + + Other %@ servers + No comment provided by engineer. + PING count จํานวน PING @@ -4017,6 +4484,10 @@ This is your link for group %@! Paste the link you received No comment provided by engineer. + + Pending + No comment provided by engineer. + People can connect to you only via the links you share. ผู้คนสามารถเชื่อมต่อกับคุณผ่านลิงก์ที่คุณแบ่งปันเท่านั้น @@ -4036,11 +4507,24 @@ This is your link for group %@! Picture-in-picture calls No comment provided by engineer. + + Play from the chat list. + No comment provided by engineer. + + + Please ask your contact to enable calls. + No comment provided by engineer. + Please ask your contact to enable sending voice messages. โปรดขอให้ผู้ติดต่อของคุณเปิดใช้งานการส่งข้อความเสียง No comment provided by engineer. + + Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers. + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. โปรดตรวจสอบว่าคุณใช้ลิงก์ที่ถูกต้องหรือขอให้ผู้ติดต่อของคุณส่งลิงก์ใหม่ให้คุณ @@ -4135,6 +4619,10 @@ Error: %@ ดูตัวอย่าง No comment provided by engineer. + + Previously connected servers + No comment provided by engineer. + Privacy & security ความเป็นส่วนตัวและความปลอดภัย @@ -4150,10 +4638,26 @@ Error: %@ ชื่อไฟล์ส่วนตัว No comment provided by engineer. + + Private message routing + No comment provided by engineer. + + + Private message routing 🚀 + No comment provided by engineer. + Private notes name of notes to self + + Private routing + No comment provided by engineer. + + + Private routing error + No comment provided by engineer. + Profile and server connections การเชื่อมต่อโปรไฟล์และเซิร์ฟเวอร์ @@ -4181,6 +4685,10 @@ Error: %@ รหัสผ่านโปรไฟล์ No comment provided by engineer. + + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ @@ -4230,11 +4738,20 @@ Error: %@ ห้ามส่งข้อความเสียง No comment provided by engineer. + + Protect IP address + No comment provided by engineer. + Protect app screen ปกป้องหน้าจอแอป No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! ปกป้องโปรไฟล์การแชทของคุณด้วยรหัสผ่าน! @@ -4250,6 +4767,14 @@ Error: %@ การหมดเวลาของโปรโตคอลต่อ KB No comment provided by engineer. + + Proxied + No comment provided by engineer. + + + Proxied servers + No comment provided by engineer. + Push notifications การแจ้งเตือนแบบทันที @@ -4268,6 +4793,10 @@ Error: %@ ให้คะแนนแอป No comment provided by engineer. + + Reachable chat toolbar + No comment provided by engineer. + React… ตอบสนอง… @@ -4276,7 +4805,7 @@ Error: %@ Read อ่าน - No comment provided by engineer. + swipe action Read more @@ -4311,6 +4840,10 @@ Error: %@ Receipts are disabled No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at ได้รับเมื่อ @@ -4331,15 +4864,23 @@ Error: %@ ได้รับข้อความ message info title + + Received messages + No comment provided by engineer. + + + Received reply + No comment provided by engineer. + + + Received total + No comment provided by engineer. + Receiving address will be changed to a different server. Address change will complete after sender comes online. ที่อยู่ผู้รับจะถูกเปลี่ยนเป็นเซิร์ฟเวอร์อื่น การเปลี่ยนแปลงที่อยู่จะเสร็จสมบูรณ์หลังจากที่ผู้ส่งออนไลน์ No comment provided by engineer. - - Receiving concurrency - No comment provided by engineer. - Receiving file will be stopped. การรับไฟล์จะหยุดลง @@ -4363,11 +4904,31 @@ Error: %@ ผู้รับจะเห็นการอัปเดตเมื่อคุณพิมพ์ No comment provided by engineer. + + Reconnect + No comment provided by engineer. + Reconnect all connected servers to force message delivery. It uses additional traffic. เชื่อมต่อเซิร์ฟเวอร์ที่เชื่อมต่อทั้งหมดอีกครั้งเพื่อบังคับให้ส่งข้อความ มันใช้การจราจรเพิ่มเติม No comment provided by engineer. + + Reconnect all servers + No comment provided by engineer. + + + Reconnect all servers? + No comment provided by engineer. + + + Reconnect server to force message delivery. It uses additional traffic. + No comment provided by engineer. + + + Reconnect server? + No comment provided by engineer. + Reconnect servers? เชื่อมต่อเซิร์ฟเวอร์อีกครั้งหรือไม่? @@ -4391,7 +4952,8 @@ Error: %@ Reject ปฏิเสธ - reject incoming call via notification + reject incoming call via notification + swipe action Reject (sender NOT notified) @@ -4417,6 +4979,10 @@ Error: %@ ลบ No comment provided by engineer. + + Remove image + No comment provided by engineer. + Remove member ลบสมาชิกออก @@ -4482,16 +5048,36 @@ Error: %@ รีเซ็ต No comment provided by engineer. + + Reset all hints + No comment provided by engineer. + + + Reset all statistics + No comment provided by engineer. + + + Reset all statistics? + No comment provided by engineer. + Reset colors รีเซ็ตสี No comment provided by engineer. + + Reset to app theme + No comment provided by engineer. + Reset to defaults รีเซ็ตเป็นค่าเริ่มต้น No comment provided by engineer. + + Reset to user theme + No comment provided by engineer. + Restart the app to create a new chat profile รีสตาร์ทแอปเพื่อสร้างโปรไฟล์แชทใหม่ @@ -4531,11 +5117,6 @@ Error: %@ เปิดเผย chat item action - - Revert - เปลี่ยนกลับ - No comment provided by engineer. - Revoke ถอน @@ -4561,9 +5142,12 @@ Error: %@ เรียกใช้แชท No comment provided by engineer. - - SMP servers - เซิร์ฟเวอร์ SMP + + SMP server + No comment provided by engineer. + + + Safely receive files No comment provided by engineer. @@ -4590,6 +5174,10 @@ Error: %@ บันทึกและแจ้งให้สมาชิกในกลุ่มทราบ No comment provided by engineer. + + Save and reconnect + No comment provided by engineer. + Save and update group profile บันทึกและอัปเดตโปรไฟล์กลุ่ม @@ -4667,6 +5255,14 @@ Error: %@ Saved message message info title + + Scale + No comment provided by engineer. + + + Scan / Paste link + No comment provided by engineer. + Scan QR code สแกนคิวอาร์โค้ด @@ -4704,11 +5300,19 @@ Error: %@ Search or paste SimpleX link No comment provided by engineer. + + Secondary + No comment provided by engineer. + Secure queue คิวที่ปลอดภัย server test step + + Secured + No comment provided by engineer. + Security assessment การประเมินความปลอดภัย @@ -4722,6 +5326,14 @@ Error: %@ Select เลือก + chat item action + + + Selected %lld + No comment provided by engineer. + + + Selected chat preferences prohibit this message. No comment provided by engineer. @@ -4759,11 +5371,6 @@ Error: %@ ส่งใบเสร็จรับการจัดส่งข้อความไปที่ No comment provided by engineer. - - Send direct message - ส่งข้อความโดยตรง - No comment provided by engineer. - Send direct message to connect No comment provided by engineer. @@ -4773,6 +5380,10 @@ Error: %@ ส่งข้อความแบบที่หายไป No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews ส่งตัวอย่างลิงก์ @@ -4783,6 +5394,18 @@ Error: %@ ส่งข้อความสด No comment provided by engineer. + + Send message to enable calls. + No comment provided by engineer. + + + Send messages directly when IP address is protected and your or destination server does not support private routing. + No comment provided by engineer. + + + Send messages directly when your or destination server does not support private routing. + No comment provided by engineer. + Send notifications ส่งการแจ้งเตือน @@ -4870,6 +5493,10 @@ Error: %@ ส่งเมื่อ: %@ copied message info + + Sent directly + No comment provided by engineer. + Sent file event เหตุการณ์ไฟล์ที่ส่ง @@ -4880,11 +5507,39 @@ Error: %@ ข้อความที่ส่งแล้ว message info title + + Sent messages + No comment provided by engineer. + Sent messages will be deleted after set time. ข้อความที่ส่งจะถูกลบหลังเกินเวลาที่กําหนด No comment provided by engineer. + + Sent reply + No comment provided by engineer. + + + Sent total + No comment provided by engineer. + + + Sent via proxy + No comment provided by engineer. + + + Server address + No comment provided by engineer. + + + Server address is incompatible with network settings. + srv error text. + + + Server address is incompatible with network settings: %@. + No comment provided by engineer. + Server requires authorization to create queues, check password เซิร์ฟเวอร์ต้องการการอนุญาตในการสร้างคิว โปรดตรวจสอบรหัสผ่าน @@ -4900,11 +5555,31 @@ Error: %@ การทดสอบเซิร์ฟเวอร์ล้มเหลว! No comment provided by engineer. + + Server type + No comment provided by engineer. + + + Server version is incompatible with network settings. + srv error text + + + Server version is incompatible with your app: %@. + No comment provided by engineer. + Servers เซิร์ฟเวอร์ No comment provided by engineer. + + Servers info + No comment provided by engineer. + + + Servers statistics will be reset - this cannot be undone! + No comment provided by engineer. + Session code No comment provided by engineer. @@ -4919,6 +5594,10 @@ Error: %@ ตั้งชื่อผู้ติดต่อ… No comment provided by engineer. + + Set default theme + No comment provided by engineer. + Set group preferences ตั้งค่าการกําหนดลักษณะกลุ่ม @@ -4982,6 +5661,10 @@ Error: %@ แชร์ที่อยู่กับผู้ติดต่อ? No comment provided by engineer. + + Share from other apps. + No comment provided by engineer. + Share link แชร์ลิงก์ @@ -4991,6 +5674,10 @@ Error: %@ Share this 1-time invite link No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts แชร์กับผู้ติดต่อ @@ -5014,16 +5701,32 @@ Error: %@ Show last messages No comment provided by engineer. + + Show message status + No comment provided by engineer. + + + Show percentage + No comment provided by engineer. + Show preview แสดงตัวอย่าง No comment provided by engineer. + + Show → on messages sent via private routing. + No comment provided by engineer. + Show: แสดง: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address ที่อยู่ SimpleX @@ -5096,6 +5799,10 @@ Error: %@ Simplified incognito mode No comment provided by engineer. + + Size + No comment provided by engineer. + Skip ข้าม @@ -5110,11 +5817,23 @@ Error: %@ Small groups (max 20) No comment provided by engineer. + + Soft + blur media + + + Some file(s) were not exported: + No comment provided by engineer. + Some non-fatal errors occurred during import - you may see Chat console for more details. ข้อผิดพลาดที่ไม่ร้ายแรงบางอย่างเกิดขึ้นระหว่างการนำเข้า - คุณอาจดูรายละเอียดเพิ่มเติมได้ที่คอนโซล Chat No comment provided by engineer. + + Some non-fatal errors occurred during import: + No comment provided by engineer. + Somebody ใครบางคน @@ -5138,6 +5857,14 @@ Error: %@ เริ่มการย้ายข้อมูล No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop หยุด @@ -5196,11 +5923,27 @@ Error: %@ Stopping chat No comment provided by engineer. + + Strong + blur media + Submit ส่ง No comment provided by engineer. + + Subscribed + No comment provided by engineer. + + + Subscription errors + No comment provided by engineer. + + + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat สนับสนุน SimpleX แชท @@ -5216,6 +5959,10 @@ Error: %@ การรับรองความถูกต้องของระบบ No comment provided by engineer. + + TCP connection + No comment provided by engineer. + TCP connection timeout หมดเวลาการเชื่อมต่อ TCP @@ -5273,9 +6020,8 @@ Error: %@ Tap to scan No comment provided by engineer. - - Tap to start a new chat - แตะเพื่อเริ่มแชทใหม่ + + Temporary file error No comment provided by engineer. @@ -5331,6 +6077,10 @@ It can happen because of some bug or when the connection is compromised.แอปสามารถแจ้งให้คุณทราบเมื่อคุณได้รับข้อความหรือคำขอติดต่อ - โปรดเปิดการตั้งค่าเพื่อเปิดใช้งาน No comment provided by engineer. + + The app will ask to confirm downloads from unknown file servers (except .onion). + No comment provided by engineer. + The attempt to change database passphrase was not completed. ความพยายามในการเปลี่ยนรหัสผ่านของฐานข้อมูลไม่เสร็จสมบูรณ์ @@ -5375,6 +6125,14 @@ It can happen because of some bug or when the connection is compromised.ข้อความจะถูกทำเครื่องหมายว่ากลั่นกรองสำหรับสมาชิกทุกคน No comment provided by engineer. + + The messages will be deleted for all members. + No comment provided by engineer. + + + The messages will be marked as moderated for all members. + No comment provided by engineer. + The next generation of private messaging การส่งข้อความส่วนตัวรุ่นต่อไป @@ -5409,9 +6167,8 @@ It can happen because of some bug or when the connection is compromised.The text you pasted is not a SimpleX link. No comment provided by engineer. - - Theme - ธีม + + Themes No comment provided by engineer. @@ -5471,11 +6228,19 @@ It can happen because of some bug or when the connection is compromised.This is your own one-time link! No comment provided by engineer. + + This link was used with another mobile device, please create a new link on the desktop. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. การตั้งค่านี้ใช้กับข้อความในโปรไฟล์แชทปัจจุบันของคุณ **%@** No comment provided by engineer. + + Title + No comment provided by engineer. + To ask any questions and to receive updates: หากต้องการถามคำถามและรับการอัปเดต: @@ -5505,6 +6270,10 @@ It can happen because of some bug or when the connection is compromised.ไฟล์ภาพ/เสียงใช้ UTC เพื่อป้องกันเขตเวลา No comment provided by engineer. + + To protect your IP address, private routing uses your SMP servers to deliver messages. + No comment provided by engineer. + To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5532,15 +6301,31 @@ You will be prompted to complete authentication before this feature is enabled.< ในการตรวจสอบการเข้ารหัสแบบ encrypt จากต้นจนจบ กับผู้ติดต่อของคุณ ให้เปรียบเทียบ (หรือสแกน) รหัสบนอุปกรณ์ของคุณ No comment provided by engineer. + + Toggle chat list: + No comment provided by engineer. + Toggle incognito when connecting. No comment provided by engineer. + + Toolbar opacity + No comment provided by engineer. + + + Total + No comment provided by engineer. + Transport isolation การแยกการขนส่ง No comment provided by engineer. + + Transport sessions + No comment provided by engineer. + Trying to connect to the server used to receive messages from this contact (error: %@). กำลังพยายามเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้ (ข้อผิดพลาด: %@) @@ -5590,11 +6375,6 @@ You will be prompted to complete authentication before this feature is enabled.< Unblock member? No comment provided by engineer. - - Unexpected error: %@ - ข้อผิดพลาดที่ไม่คาดคิด: %@ - item status description - Unexpected migration state สถานะการย้ายข้อมูลที่ไม่คาดคิด @@ -5603,7 +6383,7 @@ You will be prompted to complete authentication before this feature is enabled.< Unfav. เลิกชอบ - No comment provided by engineer. + swipe action Unhide @@ -5640,6 +6420,10 @@ You will be prompted to complete authentication before this feature is enabled.< ข้อผิดพลาดที่ไม่รู้จัก No comment provided by engineer. + + Unknown servers! + No comment provided by engineer. + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. ยกเว้นกรณีที่คุณใช้อินเทอร์เฟซการโทรของ iOS ให้เปิดใช้งานโหมดห้ามรบกวนเพื่อหลีกเลี่ยงการรบกวน @@ -5673,12 +6457,12 @@ To connect, please ask your contact to create another connection link and check Unmute เปิดเสียง - No comment provided by engineer. + swipe action Unread เปลี่ยนเป็นยังไม่ได้อ่าน - No comment provided by engineer. + swipe action Up to 100 last messages are sent to new members. @@ -5689,11 +6473,6 @@ To connect, please ask your contact to create another connection link and check อัปเดต No comment provided by engineer. - - Update .onion hosts setting? - อัปเดตการตั้งค่าโฮสต์ .onion ไหม? - No comment provided by engineer. - Update database passphrase อัปเดตรหัสผ่านของฐานข้อมูล @@ -5704,9 +6483,8 @@ To connect, please ask your contact to create another connection link and check อัปเดตการตั้งค่าเครือข่ายไหม? No comment provided by engineer. - - Update transport isolation mode? - อัปเดตโหมดการแยกการขนส่งไหม? + + Update settings? No comment provided by engineer. @@ -5714,16 +6492,15 @@ To connect, please ask your contact to create another connection link and check การอัปเดตการตั้งค่าจะเชื่อมต่อไคลเอนต์กับเซิร์ฟเวอร์ทั้งหมดอีกครั้ง No comment provided by engineer. - - Updating this setting will re-connect the client to all servers. - การอัปเดตการตั้งค่านี้จะเชื่อมต่อไคลเอนต์กับเซิร์ฟเวอร์ทั้งหมดอีกครั้ง - No comment provided by engineer. - Upgrade and open chat อัปเกรดและเปิดการแชท No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed No comment provided by engineer. @@ -5733,6 +6510,14 @@ To connect, please ask your contact to create another connection link and check อัปโหลดไฟล์ server test step + + Uploaded + No comment provided by engineer. + + + Uploaded files + No comment provided by engineer. + Uploading archive No comment provided by engineer. @@ -5778,6 +6563,14 @@ To connect, please ask your contact to create another connection link and check Use only local notifications? No comment provided by engineer. + + Use private routing with unknown servers when IP address is not protected. + No comment provided by engineer. + + + Use private routing with unknown servers. + No comment provided by engineer. + Use server ใช้เซิร์ฟเวอร์ @@ -5787,14 +6580,17 @@ To connect, please ask your contact to create another connection link and check Use the app while in the call. No comment provided by engineer. + + Use the app with one hand. + No comment provided by engineer. + User profile โปรไฟล์ผู้ใช้ No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - การใช้โฮสต์ .onion ต้องการผู้ให้บริการ VPN ที่เข้ากันได้ + + User selection No comment provided by engineer. @@ -5918,6 +6714,14 @@ To connect, please ask your contact to create another connection link and check กําลังรอวิดีโอ No comment provided by engineer. + + Wallpaper accent + No comment provided by engineer. + + + Wallpaper background + No comment provided by engineer. + Warning: starting chat on multiple devices is not supported and will cause message delivery failures No comment provided by engineer. @@ -5995,19 +6799,34 @@ To connect, please ask your contact to create another connection link and check With reduced battery usage. No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + No comment provided by engineer. + Wrong database passphrase รหัสผ่านฐานข้อมูลไม่ถูกต้อง No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + file error text + Wrong passphrase! รหัสผ่านผิด! No comment provided by engineer. - - XFTP servers - เซิร์ฟเวอร์ XFTP + + XFTP server No comment provided by engineer. @@ -6078,11 +6897,19 @@ Repeat join request? คุณได้รับเชิญให้เข้าร่วมกลุ่ม No comment provided by engineer. + + You are not connected to these servers. Private routing is used to deliver messages to them. + No comment provided by engineer. + You can accept calls from lock screen, without device and app authentication. คุณสามารถรับสายจากหน้าจอล็อกโดยไม่ต้องมีการตรวจสอบสิทธิ์อุปกรณ์และแอป No comment provided by engineer. + + You can change it in Appearance settings. + No comment provided by engineer. + You can create it later คุณสามารถสร้างได้ในภายหลัง @@ -6111,11 +6938,15 @@ Repeat join request? You can make it visible to your SimpleX contacts via Settings. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ ตอนนี้คุณสามารถส่งข้อความถึง %@ notification body + + You can send messages to %@ from Archived contacts. + No comment provided by engineer. + You can set lock screen notification preview via settings. คุณสามารถตั้งค่าแสดงตัวอย่างการแจ้งเตือนบนหน้าจอล็อคผ่านการตั้งค่า @@ -6141,6 +6972,10 @@ Repeat join request? คุณสามารถเริ่มแชทผ่านการตั้งค่าแอป / ฐานข้อมูล หรือโดยการรีสตาร์ทแอป No comment provided by engineer. + + You can still view conversation with %@ in the list of chats. + No comment provided by engineer. + You can turn on SimpleX Lock via Settings. คุณสามารถเปิด SimpleX Lock ผ่านการตั้งค่า @@ -6179,11 +7014,6 @@ Repeat join request? Repeat connection request? No comment provided by engineer. - - You have no chats - คุณไม่มีการแชท - No comment provided by engineer. - You have to enter passphrase every time the app starts - it is not stored on the device. คุณต้องใส่รหัสผ่านทุกครั้งที่เริ่มแอป - รหัสผ่านไม่ได้จัดเก็บไว้ในอุปกรณ์ @@ -6203,11 +7033,23 @@ Repeat connection request? คุณเข้าร่วมกลุ่มนี้แล้ว กำลังเชื่อมต่อเพื่อเชิญสมาชิกกลุ่ม No comment provided by engineer. + + You may migrate the exported database. + No comment provided by engineer. + + + You may save the exported archive. + No comment provided by engineer. + You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. คุณต้องใช้ฐานข้อมูลแชทเวอร์ชันล่าสุดบนอุปกรณ์เครื่องเดียวเท่านั้น มิฉะนั้น คุณอาจหยุดได้รับข้อความจากผู้ติดต่อบางคน No comment provided by engineer. + + You need to allow your contact to call to be able to call them. + No comment provided by engineer. + You need to allow your contact to send voice messages to be able to send them. คุณต้องอนุญาตให้ผู้ติดต่อของคุณส่งข้อความเสียงจึงจะสามารถส่งได้ @@ -6321,13 +7163,6 @@ Repeat connection request? โปรไฟล์แชทของคุณ No comment provided by engineer. - - Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link). - ผู้ติดต่อของคุณจะต้องออนไลน์เพื่อให้การเชื่อมต่อเสร็จสมบูรณ์ -คุณสามารถยกเลิกการเชื่อมต่อนี้และลบผู้ติดต่อออก (และลองใหม่ในภายหลังด้วยลิงก์ใหม่) - No comment provided by engineer. - Your contact sent a file that is larger than currently supported maximum size (%@). ผู้ติดต่อของคุณส่งไฟล์ที่ใหญ่กว่าขนาดสูงสุดที่รองรับในปัจจุบัน (%@) @@ -6470,6 +7305,10 @@ SimpleX servers cannot see your profile. and %lld other events No comment provided by engineer. + + attempts + No comment provided by engineer. + audio call (not e2e encrypted) การโทรด้วยเสียง (ไม่ได้ encrypt จากต้นจนจบ) @@ -6506,6 +7345,10 @@ SimpleX servers cannot see your profile. ตัวหนา No comment provided by engineer. + + call + No comment provided by engineer. + call error การโทรผิดพลาด @@ -6654,6 +7497,10 @@ SimpleX servers cannot see your profile. วัน time unit + + decryption errors + No comment provided by engineer. + default (%@) ค่าเริ่มต้น (%@) @@ -6702,6 +7549,10 @@ SimpleX servers cannot see your profile. ข้อความที่ซ้ำกัน integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted encrypted จากต้นจนจบ @@ -6781,6 +7632,10 @@ SimpleX servers cannot see your profile. event happened No comment provided by engineer. + + expired + No comment provided by engineer. + forwarded No comment provided by engineer. @@ -6810,6 +7665,10 @@ SimpleX servers cannot see your profile. iOS Keychain จะใช้เพื่อจัดเก็บรหัสผ่านอย่างปลอดภัยหลังจากที่คุณรีสตาร์ทแอปหรือเปลี่ยนรหัสผ่าน ซึ่งจะช่วยให้รับการแจ้งเตือนแบบทันทีได้ No comment provided by engineer. + + inactive + No comment provided by engineer. + incognito via contact address link ไม่ระบุตัวตนผ่านลิงค์ที่อยู่ติดต่อ @@ -6850,6 +7709,10 @@ SimpleX servers cannot see your profile. คำเชิญเข้าร่วมกลุ่ม %@ group name + + invite + No comment provided by engineer. + invited เชิญ @@ -6904,6 +7767,10 @@ SimpleX servers cannot see your profile. เชื่อมต่อสำเร็จ rcv group event chat item + + message + No comment provided by engineer. + message received ข้อความที่ได้รับ @@ -6934,6 +7801,10 @@ SimpleX servers cannot see your profile. เดือน time unit + + mute + No comment provided by engineer. + never ไม่เคย @@ -6986,6 +7857,14 @@ SimpleX servers cannot see your profile. เปิด group pref value + + other + No comment provided by engineer. + + + other errors + No comment provided by engineer. + owner เจ้าของ @@ -7050,6 +7929,10 @@ SimpleX servers cannot see your profile. saved from %@ No comment provided by engineer. + + search + No comment provided by engineer. + sec วินาที @@ -7074,6 +7957,12 @@ SimpleX servers cannot see your profile. send direct message No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + queue info + set new contact address profile update event chat item @@ -7110,10 +7999,22 @@ SimpleX servers cannot see your profile. ไม่ทราบ connection info + + unknown servers + No comment provided by engineer. + unknown status No comment provided by engineer. + + unmute + No comment provided by engineer. + + + unprotected + No comment provided by engineer. + updated group profile อัปเดตโปรไฟล์กลุ่มแล้ว @@ -7152,6 +8053,10 @@ SimpleX servers cannot see your profile. ผ่านรีเลย์ No comment provided by engineer. + + video + No comment provided by engineer. + video call (not e2e encrypted) การสนทนาทางวิดีโอ (ไม่ได้ encrypt จากต้นจนจบ) @@ -7177,6 +8082,10 @@ SimpleX servers cannot see your profile. สัปดาห์ time unit + + when IP hidden + No comment provided by engineer. + yes ใช่ @@ -7258,7 +8167,7 @@ SimpleX servers cannot see your profile.
- +
@@ -7294,7 +8203,7 @@ SimpleX servers cannot see your profile.
- +
@@ -7314,4 +8223,178 @@ SimpleX servers cannot see your profile.
+ +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + No comment provided by engineer. + + + App is locked! + No comment provided by engineer. + + + Cancel + No comment provided by engineer. + + + Cannot access keychain to save database password + No comment provided by engineer. + + + Cannot forward message + No comment provided by engineer. + + + Comment + No comment provided by engineer. + + + Currently maximum supported file size is %@. + No comment provided by engineer. + + + Database downgrade required + No comment provided by engineer. + + + Database encrypted! + No comment provided by engineer. + + + Database error + No comment provided by engineer. + + + Database passphrase is different from saved in the keychain. + No comment provided by engineer. + + + Database passphrase is required to open chat. + No comment provided by engineer. + + + Database upgrade required + No comment provided by engineer. + + + Error preparing file + No comment provided by engineer. + + + Error preparing message + No comment provided by engineer. + + + Error: %@ + No comment provided by engineer. + + + File error + No comment provided by engineer. + + + Incompatible database version + No comment provided by engineer. + + + Invalid migration confirmation + No comment provided by engineer. + + + Keychain error + No comment provided by engineer. + + + Large file! + No comment provided by engineer. + + + No active profile + No comment provided by engineer. + + + Ok + No comment provided by engineer. + + + Open the app to downgrade the database. + No comment provided by engineer. + + + Open the app to upgrade the database. + No comment provided by engineer. + + + Passphrase + No comment provided by engineer. + + + Please create a profile in the SimpleX app + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending a message takes longer than expected. + No comment provided by engineer. + + + Sending message… + No comment provided by engineer. + + + Share + No comment provided by engineer. + + + Slow network? + No comment provided by engineer. + + + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + No comment provided by engineer. + + + Wait + No comment provided by engineer. + + + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/th.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/th.xcloc/contents.json b/apps/ios/SimpleX Localizations/th.xcloc/contents.json index b60f9edb3e..4562ab8385 100644 --- a/apps/ios/SimpleX Localizations/th.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/th.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "th", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "15F31d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "15.4" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff index df5076fb07..054f65110f 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Localized Contents/tr.xliff @@ -2,7 +2,7 @@
- +
@@ -127,11 +127,6 @@ %@ onaylandı No comment provided by engineer. - - %@ servers - %@ sunucuları - No comment provided by engineer. - %@ uploaded %@ yüklendi @@ -445,7 +440,7 @@ 0s - 0 saniye + 0sn No comment provided by engineer. @@ -557,16 +552,16 @@ SimpleX Chat adresi hakkında No comment provided by engineer. - - Accent color - Vurgu rengi + + Accent No comment provided by engineer. Accept Kabul et accept contact request via notification - accept incoming call via notification + accept incoming call via notification + swipe action Accept connection request? @@ -581,7 +576,20 @@ Accept incognito Takma adla kabul et - accept contact request via notification + accept contact request via notification + swipe action + + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + No comment provided by engineer. + + + Active connections + No comment provided by engineer. Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. @@ -603,16 +611,16 @@ Profil ekle No comment provided by engineer. + + Add server + Sunucu ekle + No comment provided by engineer. + Add servers by scanning QR codes. Karekod taratarak sunucuları ekleyin. No comment provided by engineer. - - Add server… - Sunucu ekle… - No comment provided by engineer. - Add to another device Başka bir cihaza ekle @@ -623,6 +631,18 @@ Karşılama mesajı ekleyin No comment provided by engineer. + + Additional accent + No comment provided by engineer. + + + Additional accent 2 + No comment provided by engineer. + + + Additional secondary + No comment provided by engineer. + Address Adres @@ -648,6 +668,10 @@ Gelişmiş ağ ayarları No comment provided by engineer. + + Advanced settings + No comment provided by engineer. + All app data is deleted. Tüm uygulama verileri silinir. @@ -663,6 +687,10 @@ Kullanıldığında bütün veriler silinir. No comment provided by engineer. + + All data is private to your device. + No comment provided by engineer. + All group members will remain connected. Tüm grup üyeleri bağlı kalacaktır. @@ -683,6 +711,10 @@ %@ 'den gelen bütün yeni mesajlar saklı olacak! No comment provided by engineer. + + All profiles + No comment provided by engineer. + All your contacts will remain connected. Konuştuğun kişilerin tümü bağlı kalacaktır. @@ -695,7 +727,7 @@ All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays. - Tüm kişileriniz, sohbetleriniz ve dosyalarınız güvenli bir şekilde şifrelenecek ve parçalar halinde yapılandırılmış XFTP rölelerine yüklenecektir. + Tüm kişileriniz, konuşmalarınız ve dosyalarınız güvenli bir şekilde şifrelenir ve yapılandırılmış XFTP yönlendiricilerine parçalar halinde yüklenir. No comment provided by engineer. @@ -708,11 +740,20 @@ Yalnızca irtibat kişiniz izin veriyorsa aramalara izin verin. No comment provided by engineer. + + Allow calls? + No comment provided by engineer. + Allow disappearing messages only if your contact allows it to you. Eğer kişide izin verirse kaybolan mesajlara izin ver. No comment provided by engineer. + + Allow downgrade + Sürüm düşürmeye izin ver + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Konuştuğun kişi, kalıcı olarak silinebilen mesajlara izin veriyorsa sen de ver. (24 saat içinde) @@ -730,7 +771,7 @@ Allow sending direct messages to members. - Üyelere direkt mesaj göndermeye izin ver. + Üyelere doğrudan mesaj göndermeye izin ver. No comment provided by engineer. @@ -738,6 +779,10 @@ Kendiliğinden yok olan mesajlar göndermeye izin ver. No comment provided by engineer. + + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Gönderilen mesajların kalıcı olarak silinmesine izin ver. (24 saat içinde) @@ -808,6 +853,11 @@ Zaten gruba bağlanılıyor! No comment provided by engineer. + + Always use private routing. + Her zaman gizli yönlendirme kullan. + No comment provided by engineer. + Always use relay Her zaman yönlendirici kullan @@ -873,11 +923,23 @@ Uygula No comment provided by engineer. + + Apply to + No comment provided by engineer. + Archive and upload Arşivle ve yükle No comment provided by engineer. + + Archive contacts to chat later. + No comment provided by engineer. + + + Archived contacts + No comment provided by engineer. + Archiving database Veritabanı arşivleniyor @@ -948,6 +1010,10 @@ Geri No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address Kötü bilgisayar adresi @@ -973,6 +1039,14 @@ Daha iyi mesajlar No comment provided by engineer. + + Better networking + No comment provided by engineer. + + + Black + No comment provided by engineer. + Block Engelle @@ -1008,6 +1082,14 @@ Yönetici tarafından engellendi No comment provided by engineer. + + Blur for better privacy. + No comment provided by engineer. + + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. Sen ve konuştuğun kişi mesaj tepkileri ekleyebilir. @@ -1053,11 +1135,23 @@ Aramalar No comment provided by engineer. + + Calls prohibited! + No comment provided by engineer. + Camera not available Kamera mevcut değil No comment provided by engineer. + + Can't call contact + No comment provided by engineer. + + + Can't call member + No comment provided by engineer. + Can't invite contact! Kişi davet edilemiyor! @@ -1068,6 +1162,10 @@ Kişiler davet edilemiyor! No comment provided by engineer. + + Can't message member + No comment provided by engineer. + Cancel İptal et @@ -1083,11 +1181,20 @@ Veritabanı şifresini kaydetmek için Anahtar Zinciri'ne erişilemiyor No comment provided by engineer. + + Cannot forward message + No comment provided by engineer. + Cannot receive file Dosya alınamıyor No comment provided by engineer. + + Capacity exceeded - recipient did not receive previously sent messages. + Kapasite aşıldı - alıcı önceden gönderilen mesajları almadı. + snd error text + Cellular Hücresel Veri @@ -1149,6 +1256,10 @@ Sohbet arşivi No comment provided by engineer. + + Chat colors + No comment provided by engineer. + Chat console Sohbet konsolu @@ -1164,6 +1275,10 @@ Sohbet veritabanı silindi No comment provided by engineer. + + Chat database exported + No comment provided by engineer. + Chat database imported Sohbet veritabanı içe aktarıldı @@ -1184,6 +1299,10 @@ Sohbet durduruldu. Bu veritabanını zaten başka bir cihazda kullandıysanız, sohbete başlamadan önce onu geri aktarmalısınız. No comment provided by engineer. + + Chat list + No comment provided by engineer. + Chat migrated! Sohbet taşındı! @@ -1194,6 +1313,10 @@ Sohbet tercihleri No comment provided by engineer. + + Chat theme + No comment provided by engineer. + Chats Sohbetler @@ -1224,10 +1347,22 @@ Kütüphaneden seç No comment provided by engineer. + + Chunks deleted + No comment provided by engineer. + + + Chunks downloaded + No comment provided by engineer. + + + Chunks uploaded + No comment provided by engineer. + Clear Temizle - No comment provided by engineer. + swipe action Clear conversation @@ -1249,9 +1384,12 @@ Doğrulamayı temizle No comment provided by engineer. - - Colors - Renkler + + Color chats with the new themes. + No comment provided by engineer. + + + Color mode No comment provided by engineer. @@ -1264,11 +1402,19 @@ Güvenlik kodlarını kişilerinle karşılaştır. No comment provided by engineer. + + Completed + No comment provided by engineer. + Configure ICE servers ICE sunucularını ayarla No comment provided by engineer. + + Configured %@ servers + No comment provided by engineer. + Confirm Onayla @@ -1279,11 +1425,20 @@ Parolayı onayla No comment provided by engineer. + + Confirm contact deletion? + No comment provided by engineer. + Confirm database upgrades Veritabanı geliştirmelerini onayla No comment provided by engineer. + + Confirm files from unknown servers. + Bilinmeyen sunuculardan gelen dosyaları onayla. + No comment provided by engineer. + Confirm network settings Ağ ayarlarını onaylayın @@ -1329,6 +1484,10 @@ Bilgisayara bağlan No comment provided by engineer. + + Connect to your friends faster. + No comment provided by engineer. + Connect to yourself? Kendine mi bağlanacaksın? @@ -1368,16 +1527,28 @@ Bu senin kendi tek kullanımlık bağlantın! %@ ile bağlan No comment provided by engineer. + + Connected + No comment provided by engineer. + Connected desktop Bilgisayara bağlandı No comment provided by engineer. + + Connected servers + No comment provided by engineer. + Connected to desktop Bilgisayara bağlanıldı No comment provided by engineer. + + Connecting + No comment provided by engineer. + Connecting to server… Sunucuya bağlanıyor… @@ -1388,6 +1559,10 @@ Bu senin kendi tek kullanımlık bağlantın! Sunucuya bağlanıyor…(hata:%@) No comment provided by engineer. + + Connecting to contact, please wait or check later! + No comment provided by engineer. + Connecting to desktop Bilgisayara bağlanıyor @@ -1398,6 +1573,10 @@ Bu senin kendi tek kullanımlık bağlantın! Bağlantı No comment provided by engineer. + + Connection and servers status. + No comment provided by engineer. + Connection error Bağlantı hatası @@ -1408,6 +1587,10 @@ Bu senin kendi tek kullanımlık bağlantın! Bağlantı hatası (DOĞRULAMA) No comment provided by engineer. + + Connection notifications + No comment provided by engineer. + Connection request sent! Bağlantı daveti gönderildi! @@ -1423,6 +1606,14 @@ Bu senin kendi tek kullanımlık bağlantın! Bağlantı süresi geçmiş No comment provided by engineer. + + Connection with desktop stopped + No comment provided by engineer. + + + Connections + No comment provided by engineer. + Contact allows Kişi izin veriyor @@ -1433,6 +1624,10 @@ Bu senin kendi tek kullanımlık bağlantın! Kişi zaten mevcut No comment provided by engineer. + + Contact deleted! + No comment provided by engineer. + Contact hidden: Kişi gizli: @@ -1443,9 +1638,8 @@ Bu senin kendi tek kullanımlık bağlantın! Kişi bağlandı notification - - Contact is not connected yet! - Kişi şuan bağlanmadı! + + Contact is deleted. No comment provided by engineer. @@ -1458,6 +1652,10 @@ Bu senin kendi tek kullanımlık bağlantın! Kişi tercihleri No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + No comment provided by engineer. + Contacts Kişiler @@ -1473,10 +1671,18 @@ Bu senin kendi tek kullanımlık bağlantın! Devam et No comment provided by engineer. + + Conversation deleted! + No comment provided by engineer. + Copy Kopyala - chat item action + No comment provided by engineer. + + + Copy error + No comment provided by engineer. Core version: v%@ @@ -1553,6 +1759,10 @@ Bu senin kendi tek kullanımlık bağlantın! Profilini oluştur No comment provided by engineer. + + Created + No comment provided by engineer. + Created at Şurada oluşturuldu @@ -1588,6 +1798,10 @@ Bu senin kendi tek kullanımlık bağlantın! Şu anki parola… No comment provided by engineer. + + Current profile + No comment provided by engineer. + Currently maximum supported file size is %@. Şu anki maksimum desteklenen dosya boyutu %@ kadardır. @@ -1598,11 +1812,19 @@ Bu senin kendi tek kullanımlık bağlantın! Özel saat No comment provided by engineer. + + Customize theme + No comment provided by engineer. + Dark Karanlık No comment provided by engineer. + + Dark mode colors + No comment provided by engineer. + Database ID Veritabanı kimliği @@ -1701,6 +1923,11 @@ Bu senin kendi tek kullanımlık bağlantın! Uygulama yeniden başlatıldığında veritabanı taşınacaktır No comment provided by engineer. + + Debug delivery + Hata ayıklama teslimatı + No comment provided by engineer. + Decentralized Merkezi Olmayan @@ -1714,18 +1941,18 @@ Bu senin kendi tek kullanımlık bağlantın! Delete Sil - chat item action + chat item action + swipe action + + + Delete %lld messages of members? + No comment provided by engineer. Delete %lld messages? %lld mesaj silinsin mi? No comment provided by engineer. - - Delete Contact - Kişiyi sil - No comment provided by engineer. - Delete address Adresi sil @@ -1781,11 +2008,8 @@ Bu senin kendi tek kullanımlık bağlantın! Kişiyi sil No comment provided by engineer. - - Delete contact? -This cannot be undone! - Kişi silinsin mi? -Bu geri alınamaz! + + Delete contact? No comment provided by engineer. @@ -1878,11 +2102,6 @@ Bu geri alınamaz! Eski veritabanı silinsin mi? No comment provided by engineer. - - Delete pending connection - Bekleyen bağlantıyı sil - No comment provided by engineer. - Delete pending connection? Bekleyen bağlantı silinsin mi? @@ -1898,11 +2117,23 @@ Bu geri alınamaz! Sırayı sil server test step + + Delete up to 20 messages at once. + No comment provided by engineer. + Delete user profile? Kullanıcı profili silinsin mi? No comment provided by engineer. + + Delete without notification + No comment provided by engineer. + + + Deleted + No comment provided by engineer. + Deleted at de silindi @@ -1913,6 +2144,10 @@ Bu geri alınamaz! %@ de silindi copied message info + + Deletion errors + No comment provided by engineer. + Delivery Teslimat @@ -1948,11 +2183,36 @@ Bu geri alınamaz! Bilgisayar cihazları No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + No comment provided by engineer. + + + Destination server error: %@ + Hedef sunucu hatası: %@ + snd error text + + + Destination server version of %@ is incompatible with forwarding server %@. + No comment provided by engineer. + + + Detailed statistics + No comment provided by engineer. + + + Details + No comment provided by engineer. + Develop Geliştir No comment provided by engineer. + + Developer options + No comment provided by engineer. + Developer tools Geliştirici araçları @@ -2003,6 +2263,10 @@ Bu geri alınamaz! Herkes için devre dışı bırak No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message Kaybolan mesaj @@ -2053,11 +2317,21 @@ Bu geri alınamaz! Yerel ağ aracılığıyla keşfet No comment provided by engineer. + + Do NOT send messages directly, even if your or destination server does not support private routing. + Sizin veya hedef sunucunun özel yönlendirmeyi desteklememesi durumunda bile mesajları doğrudan GÖNDERMEYİN. + No comment provided by engineer. + Do NOT use SimpleX for emergency calls. Acil aramalar için SimpleX'i KULLANMAYIN. No comment provided by engineer. + + Do NOT use private routing. + Gizli yönlendirmeyi KULLANMA. + No comment provided by engineer. + Do it later Sonra yap @@ -2093,6 +2367,10 @@ Bu geri alınamaz! İndir chat item action + + Download errors + No comment provided by engineer. + Download failed Yükleme başarısız oldu @@ -2103,6 +2381,14 @@ Bu geri alınamaz! Dosya indir server test step + + Downloaded + No comment provided by engineer. + + + Downloaded files + No comment provided by engineer. + Downloading archive Arşiv indiriliyor @@ -2203,6 +2489,10 @@ Bu geri alınamaz! Kendini imha şifresini etkinleştir set passcode view + + Enabled + No comment provided by engineer. + Enabled for Şunlar için etkinleştirildi @@ -2373,6 +2663,10 @@ Bu geri alınamaz! Ayar değiştirilirken hata oluştu No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address Adres oluşturulurken hata oluştu @@ -2423,11 +2717,6 @@ Bu geri alınamaz! Bağlantı silinirken hata oluştu No comment provided by engineer. - - Error deleting contact - Kişi silinirken hata oluştu - No comment provided by engineer. - Error deleting database Veritabanı silinirken hata oluştu @@ -2473,6 +2762,10 @@ Bu geri alınamaz! Sohbet veritabanı dışa aktarılırken hata oluştu No comment provided by engineer. + + Error exporting theme: %@ + No comment provided by engineer. + Error importing chat database Sohbet veritabanı içe aktarılırken hata oluştu @@ -2498,11 +2791,23 @@ Bu geri alınamaz! Dosya alınırken sorun oluştu No comment provided by engineer. + + Error reconnecting server + No comment provided by engineer. + + + Error reconnecting servers + No comment provided by engineer. + Error removing member Kişiyi silerken sorun oluştu No comment provided by engineer. + + Error resetting statistics + No comment provided by engineer. + Error saving %@ servers %@ sunucuları kaydedilirken sorun oluştu @@ -2621,7 +2926,8 @@ Bu geri alınamaz! Error: %@ Hata: %@ - No comment provided by engineer. + file error text + snd error text Error: URL is invalid @@ -2633,6 +2939,10 @@ Bu geri alınamaz! Hata: veritabanı dosyası yok No comment provided by engineer. + + Errors + No comment provided by engineer. + Even when disabled in the conversation. Konuşma sırasında devre dışı bırakılsa bile. @@ -2658,6 +2968,10 @@ Bu geri alınamaz! Dışarı çıkarma hatası: No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. Dışarı çıkarılmış veritabanı arşivi. @@ -2691,8 +3005,28 @@ Bu geri alınamaz! Favorite Favori + swipe action + + + File error No comment provided by engineer. + + File not found - most likely file was deleted or cancelled. + file error text + + + File server error: %@ + file error text + + + File status + No comment provided by engineer. + + + File status: %@ + copied message info + File will be deleted from servers. Dosya sunuculardan silinecek. @@ -2713,6 +3047,11 @@ Bu geri alınamaz! Dosya: %@ No comment provided by engineer. + + Files + Dosyalar + No comment provided by engineer. + Files & media Dosyalar & medya @@ -2818,6 +3157,32 @@ Bu geri alınamaz! Şuradan iletildi No comment provided by engineer. + + Forwarding server %@ failed to connect to destination server %@. Please try later. + No comment provided by engineer. + + + Forwarding server address is incompatible with network settings: %@. + No comment provided by engineer. + + + Forwarding server version is incompatible with network settings: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + Yönlendirme sunucusu: %1$@ +Hedef sunucu hatası: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + Yönlendirme sunucusu: %1$@ +Hata: %2$@ + snd error text + Found desktop Bilgisayar bulundu @@ -2863,6 +3228,14 @@ Bu geri alınamaz! GİFler ve çıkartmalar No comment provided by engineer. + + Good afternoon! + message preview + + + Good morning! + message preview + Group Grup @@ -3143,6 +3516,10 @@ Bu geri alınamaz! İçe aktarma başarısız oldu No comment provided by engineer. + + Import theme + No comment provided by engineer. + Importing archive Arşiv içe aktarılıyor @@ -3265,6 +3642,10 @@ Bu geri alınamaz! Arayüz No comment provided by engineer. + + Interface colors + No comment provided by engineer. + Invalid QR code Geçersiz QR kodu @@ -3366,6 +3747,10 @@ Bu geri alınamaz! 3. Bağlantı tehlikeye girmiştir. No comment provided by engineer. + + It protects your IP address and connections. + No comment provided by engineer. + It seems like you are already connected via this link. If it is not the case, there was an error (%@). Bu bağlantı üzerinden zaten bağlanmışsınız gibi görünüyor. Eğer durum böyle değilse, bir hata oluştu (%@). @@ -3384,7 +3769,7 @@ Bu geri alınamaz! Join Katıl - No comment provided by engineer. + swipe action Join group @@ -3428,6 +3813,10 @@ Bu senin grup için bağlantın %@! Tut No comment provided by engineer. + + Keep conversation + No comment provided by engineer. + Keep the app open to use it from desktop Bilgisayardan kullanmak için uygulamayı açık tut @@ -3471,7 +3860,7 @@ Bu senin grup için bağlantın %@! Leave Ayrıl - No comment provided by engineer. + swipe action Leave group @@ -3603,11 +3992,23 @@ Bu senin grup için bağlantın %@! Maksimum 30 saniye, anında alındı. No comment provided by engineer. + + Media & file servers + No comment provided by engineer. + + + Medium + blur media + Member Kişi No comment provided by engineer. + + Member inactive + item status text + Member role will be changed to "%@". All group members will be notified. Üye rolü "%@" olarak değiştirilecektir. Ve tüm grup üyeleri bilgilendirilecektir. @@ -3623,6 +4024,10 @@ Bu senin grup için bağlantın %@! Üye gruptan çıkarılacaktır - bu geri alınamaz! No comment provided by engineer. + + Menus + No comment provided by engineer. + Message delivery error Mesaj gönderim hatası @@ -3633,11 +4038,29 @@ Bu senin grup için bağlantın %@! Mesaj alındı bilgisi! No comment provided by engineer. + + Message delivery warning + Mesaj iletimi uyarısı + item status text + Message draft Mesaj taslağı No comment provided by engineer. + + Message forwarded + item status text + + + Message may be delivered later if member becomes active. + item status description + + + Message queue info + Mesaj kuyruğu bilgisi + No comment provided by engineer. + Message reactions Mesaj tepkileri @@ -3653,11 +4076,27 @@ Bu senin grup için bağlantın %@! Mesaj tepkileri bu grupta yasaklandı. No comment provided by engineer. + + Message reception + No comment provided by engineer. + + + Message servers + No comment provided by engineer. + Message source remains private. Mesaj kaynağı gizli kalır. No comment provided by engineer. + + Message status + No comment provided by engineer. + + + Message status: %@ + copied message info + Message text Mesaj yazısı @@ -3683,6 +4122,14 @@ Bu senin grup için bağlantın %@! %@ den gelen mesajlar gösterilecektir! No comment provided by engineer. + + Messages received + No comment provided by engineer. + + + Messages sent + No comment provided by engineer. + Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. Mesajlar, dosyalar ve aramalar **uçtan uca şifreleme** ile mükemmel ileri gizlilik, inkar ve izinsiz giriş kurtarma ile korunur. @@ -3783,11 +4230,6 @@ Bu senin grup için bağlantın %@! Büyük ihtimalle bu bağlantı silinmiş. item status description - - Most likely this contact has deleted the connection with you. - Büyük ihtimalle bu kişi seninle bağlantını sildi. - No comment provided by engineer. - Multiple chat profiles Çoklu sohbet profili @@ -3796,7 +4238,7 @@ Bu senin grup için bağlantın %@! Mute Sustur - No comment provided by engineer. + swipe action Muted when inactive! @@ -3806,7 +4248,7 @@ Bu senin grup için bağlantın %@! Name İsim - No comment provided by engineer. + swipe action Network & servers @@ -3818,6 +4260,11 @@ Bu senin grup için bağlantın %@! Ağ bağlantısı No comment provided by engineer. + + Network issues - message expired after many attempts to send it. + Ağ sorunları - birçok gönderme denemesinden sonra mesajın süresi doldu. + snd error text + Network management Ağ yönetimi @@ -3843,6 +4290,10 @@ Bu senin grup için bağlantın %@! Yeni sohbet No comment provided by engineer. + + New chat experience 🎉 + No comment provided by engineer. + New contact request Yeni bağlantı isteği @@ -3873,6 +4324,10 @@ Bu senin grup için bağlantın %@! %@ da yeni No comment provided by engineer. + + New media options + No comment provided by engineer. + New member role Yeni üye rolü @@ -3918,6 +4373,10 @@ Bu senin grup için bağlantın %@! Cihaz tokeni yok! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + item status description + No filtered chats Filtrelenmiş sohbetler yok @@ -3933,6 +4392,10 @@ Bu senin grup için bağlantın %@! Geçmiş yok No comment provided by engineer. + + No info, try to reload + No comment provided by engineer. + No network connection Ağ bağlantısı yok @@ -3953,6 +4416,10 @@ Bu senin grup için bağlantın %@! Uyumlu değil! No comment provided by engineer. + + Nothing selected + No comment provided by engineer. + Notifications Bildirimler @@ -3980,7 +4447,7 @@ Bu senin grup için bağlantın %@! Off Kapalı - No comment provided by engineer. + blur media Ok @@ -4002,14 +4469,18 @@ Bu senin grup için bağlantın %@! Tek zamanlı bağlantı daveti No comment provided by engineer. - - Onion hosts will be required for connection. Requires enabling VPN. - Bağlantı için Onion ana bilgisayarları gerekecektir. VPN'nin etkinleştirilmesi gerekir. + + Onion hosts will be **required** for connection. +Requires compatible VPN. + Bağlantı için Onion ana bilgisayarları gerekecektir. +VPN'nin etkinleştirilmesi gerekir. No comment provided by engineer. - - Onion hosts will be used when available. Requires enabling VPN. - Onion ana bilgisayarları mevcutsa kullanılacaktır. VPN'nin etkinleştirilmesi gerekir. + + Onion hosts will be used when available. +Requires compatible VPN. + Onion ana bilgisayarları mevcutsa kullanılacaktır. +VPN'nin etkinleştirilmesi gerekir. No comment provided by engineer. @@ -4022,6 +4493,10 @@ Bu senin grup için bağlantın %@! Yalnızca istemci cihazlar kullanıcı profillerini, kişileri, grupları ve **2 katmanlı uçtan uca şifreleme** ile gönderilen mesajları depolar. No comment provided by engineer. + + Only delete conversation + No comment provided by engineer. + Only group owners can change group preferences. Grup tercihlerini yalnızca grup sahipleri değiştirebilir. @@ -4117,6 +4592,10 @@ Bu senin grup için bağlantın %@! Başka bir cihaza açık geçiş authentication reason + + Open server settings + No comment provided by engineer. + Open user profiles Kullanıcı profillerini aç @@ -4157,6 +4636,10 @@ Bu senin grup için bağlantın %@! Diğer No comment provided by engineer. + + Other %@ servers + No comment provided by engineer. + PING count PING sayısı @@ -4222,6 +4705,10 @@ Bu senin grup için bağlantın %@! Aldığın bağlantıyı yapıştır No comment provided by engineer. + + Pending + No comment provided by engineer. + People can connect to you only via the links you share. İnsanlar size yalnızca paylaştığınız bağlantılar üzerinden ulaşabilir. @@ -4242,11 +4729,24 @@ Bu senin grup için bağlantın %@! Resim içinde resim aramaları No comment provided by engineer. + + Play from the chat list. + No comment provided by engineer. + + + Please ask your contact to enable calls. + No comment provided by engineer. + Please ask your contact to enable sending voice messages. Lütfen konuştuğunuz kişiden sesli mesaj göndermeyi etkinleştirmesini isteyin. No comment provided by engineer. + + Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers. + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. Lütfen doğru bağlantıyı kullandığınızı kontrol edin veya kişiden size başka bir bağlantı göndermesini isteyin. @@ -4344,6 +4844,10 @@ Hata: %@ Ön izleme No comment provided by engineer. + + Previously connected servers + No comment provided by engineer. + Privacy & security Gizlilik & güvenlik @@ -4359,11 +4863,30 @@ Hata: %@ Gizli dosya adları No comment provided by engineer. + + Private message routing + Gizli mesaj yönlendirme + No comment provided by engineer. + + + Private message routing 🚀 + Gizli mesaj yönlendirme 🚀 + No comment provided by engineer. + Private notes Gizli notlar name of notes to self + + Private routing + Gizli yönlendirme + No comment provided by engineer. + + + Private routing error + No comment provided by engineer. + Profile and server connections Profil ve sunucu bağlantıları @@ -4394,6 +4917,10 @@ Hata: %@ Profil parolası No comment provided by engineer. + + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. Profil güncellemesi kişilerinize gönderilecektir. @@ -4426,7 +4953,7 @@ Hata: %@ Prohibit sending direct messages to members. - Geri dönülmez mesaj silme işlemini yasakla. + Üyelere doğrudan mesaj göndermeyi yasakla. No comment provided by engineer. @@ -4444,11 +4971,23 @@ Hata: %@ Sesli mesajların gönderimini yasakla. No comment provided by engineer. + + Protect IP address + IP adresini koru + No comment provided by engineer. + Protect app screen Uygulama ekranını koru No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + IP adresinizi kişileriniz tarafından seçilen mesajlaşma yönlendiricilerinden koruyun. +*Ağ ve sunucular* ayarlarında etkinleştirin. + No comment provided by engineer. + Protect your chat profiles with a password! Bir parolayla birlikte sohbet profillerini koru! @@ -4464,6 +5003,14 @@ Hata: %@ KB başına protokol zaman aşımı No comment provided by engineer. + + Proxied + No comment provided by engineer. + + + Proxied servers + No comment provided by engineer. + Push notifications Anında bildirimler @@ -4484,6 +5031,10 @@ Hata: %@ Uygulamayı değerlendir No comment provided by engineer. + + Reachable chat toolbar + No comment provided by engineer. + React… Tepki ver… @@ -4492,7 +5043,7 @@ Hata: %@ Read Oku - No comment provided by engineer. + swipe action Read more @@ -4529,6 +5080,10 @@ Hata: %@ Gönderildi bilgisi devre dışı bırakıldı No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at Şuradan alındı @@ -4549,16 +5104,23 @@ Hata: %@ Mesaj alındı message info title + + Received messages + No comment provided by engineer. + + + Received reply + No comment provided by engineer. + + + Received total + No comment provided by engineer. + Receiving address will be changed to a different server. Address change will complete after sender comes online. Alıcı adresi farklı bir sunucuya değiştirilecektir. Gönderici çevrimiçi olduktan sonra adres değişikliği tamamlanacaktır. No comment provided by engineer. - - Receiving concurrency - Eşzamanlılık alınıyor - No comment provided by engineer. - Receiving file will be stopped. Dosya alımı durdurulacaktır. @@ -4584,11 +5146,31 @@ Hata: %@ Alıcılar yazdığına göre güncellemeleri görecektir. No comment provided by engineer. + + Reconnect + No comment provided by engineer. + Reconnect all connected servers to force message delivery. It uses additional traffic. Mesaj teslimini zorlamak için bağlı tüm sunucuları yeniden bağlayın. Ek trafik kullanır. No comment provided by engineer. + + Reconnect all servers + No comment provided by engineer. + + + Reconnect all servers? + No comment provided by engineer. + + + Reconnect server to force message delivery. It uses additional traffic. + No comment provided by engineer. + + + Reconnect server? + No comment provided by engineer. + Reconnect servers? Sunuculara yeniden bağlanılsın mı? @@ -4612,7 +5194,8 @@ Hata: %@ Reject Reddet - reject incoming call via notification + reject incoming call via notification + swipe action Reject (sender NOT notified) @@ -4626,12 +5209,12 @@ Hata: %@ Relay server is only used if necessary. Another party can observe your IP address. - Aktarma sunucusu yalnızca gerekli olduğunda kullanılır. Başka bir taraf IP adresinizi gözlemleyebilir. + Yönlendirici sunucusu yalnızca gerekli olduğunda kullanılır. Başka bir taraf IP adresinizi gözlemleyebilir. No comment provided by engineer. Relay server protects your IP address, but it can observe the duration of the call. - Aktarıcı sunucu IP adresinizi korur, ancak aramanın süresini gözlemleyebilir. + Yönlendirici sunucu IP adresinizi korur, ancak aramanın süresini gözlemleyebilir. No comment provided by engineer. @@ -4639,6 +5222,10 @@ Hata: %@ Sil No comment provided by engineer. + + Remove image + No comment provided by engineer. + Remove member Kişiyi sil @@ -4709,16 +5296,36 @@ Hata: %@ Sıfırla No comment provided by engineer. + + Reset all hints + No comment provided by engineer. + + + Reset all statistics + No comment provided by engineer. + + + Reset all statistics? + No comment provided by engineer. + Reset colors Renkleri sıfırla No comment provided by engineer. + + Reset to app theme + No comment provided by engineer. + Reset to defaults Varsayılanlara sıfırla No comment provided by engineer. + + Reset to user theme + No comment provided by engineer. + Restart the app to create a new chat profile Yeni bir sohbet profili oluşturmak için uygulamayı yeniden başlatın @@ -4759,11 +5366,6 @@ Hata: %@ Göster chat item action - - Revert - Geri al - No comment provided by engineer. - Revoke İptal et @@ -4789,9 +5391,13 @@ Hata: %@ Sohbeti çalıştır No comment provided by engineer. - - SMP servers - SMP sunucuları + + SMP server + No comment provided by engineer. + + + Safely receive files + Dosyaları güvenle alın No comment provided by engineer. @@ -4819,6 +5425,10 @@ Hata: %@ Kaydet ve grup üyelerine bildir No comment provided by engineer. + + Save and reconnect + No comment provided by engineer. + Save and update group profile Kaydet ve grup profilini güncelle @@ -4899,6 +5509,14 @@ Hata: %@ Kaydedilmiş mesaj message info title + + Scale + No comment provided by engineer. + + + Scan / Paste link + No comment provided by engineer. + Scan QR code QR kodu okut @@ -4939,11 +5557,19 @@ Hata: %@ Ara veya SimpleX bağlantısını yapıştır No comment provided by engineer. + + Secondary + No comment provided by engineer. + Secure queue Sırayı koru server test step + + Secured + No comment provided by engineer. + Security assessment Güvenlik değerlendirmesi @@ -4957,6 +5583,14 @@ Hata: %@ Select Seç + chat item action + + + Selected %lld + No comment provided by engineer. + + + Selected chat preferences prohibit this message. No comment provided by engineer. @@ -4994,11 +5628,6 @@ Hata: %@ Görüldü bilgilerini şuraya gönder No comment provided by engineer. - - Send direct message - Doğrudan mesaj gönder - No comment provided by engineer. - Send direct message to connect Bağlanmak için doğrudan mesaj gönder @@ -5009,6 +5638,10 @@ Hata: %@ Kaybolan bir mesaj gönder No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews Bağlantı ön gösterimleri gönder @@ -5019,6 +5652,20 @@ Hata: %@ Canlı mesaj gönder No comment provided by engineer. + + Send message to enable calls. + No comment provided by engineer. + + + Send messages directly when IP address is protected and your or destination server does not support private routing. + IP adresi korumalı olduğunda ve sizin veya hedef sunucunun özel yönlendirmeyi desteklemediği durumlarda mesajları doğrudan gönderin. + No comment provided by engineer. + + + Send messages directly when your or destination server does not support private routing. + Sizin veya hedef sunucunun özel yönlendirmeyi desteklemediği durumlarda mesajları doğrudan gönderin. + No comment provided by engineer. + Send notifications Bildirimler gönder @@ -5109,6 +5756,10 @@ Hata: %@ Şuradan gönderildi: %@ copied message info + + Sent directly + No comment provided by engineer. + Sent file event Dosya etkinliği gönderildi @@ -5119,11 +5770,40 @@ Hata: %@ Mesaj gönderildi message info title + + Sent messages + No comment provided by engineer. + Sent messages will be deleted after set time. Gönderilen mesajlar ayarlanan süreden sonra silinecektir. No comment provided by engineer. + + Sent reply + No comment provided by engineer. + + + Sent total + No comment provided by engineer. + + + Sent via proxy + No comment provided by engineer. + + + Server address + No comment provided by engineer. + + + Server address is incompatible with network settings. + Sunucu adresi ağ ayarlarıyla uyumlu değil. + srv error text. + + + Server address is incompatible with network settings: %@. + No comment provided by engineer. + Server requires authorization to create queues, check password Sunucunun sıra oluşturması için yetki gereklidir, şifreyi kontrol edin @@ -5139,11 +5819,32 @@ Hata: %@ Sunucu testinde hata oluştu! No comment provided by engineer. + + Server type + No comment provided by engineer. + + + Server version is incompatible with network settings. + Sunucu sürümü ağ ayarlarıyla uyumlu değil. + srv error text + + + Server version is incompatible with your app: %@. + No comment provided by engineer. + Servers Sunucular No comment provided by engineer. + + Servers info + No comment provided by engineer. + + + Servers statistics will be reset - this cannot be undone! + No comment provided by engineer. + Session code Oturum kodu @@ -5159,6 +5860,10 @@ Hata: %@ Kişi adı gir… No comment provided by engineer. + + Set default theme + No comment provided by engineer. + Set group preferences Grup tercihlerini ayarla @@ -5224,6 +5929,10 @@ Hata: %@ Kişilerle adres paylaşılsın mı? No comment provided by engineer. + + Share from other apps. + No comment provided by engineer. + Share link Bağlantıyı paylaş @@ -5234,6 +5943,10 @@ Hata: %@ Bu tek kullanımlık bağlantı davetini paylaş No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts Kişilerle paylaş @@ -5259,16 +5972,34 @@ Hata: %@ Son mesajları göster No comment provided by engineer. + + Show message status + Mesaj durumunu göster + No comment provided by engineer. + + + Show percentage + No comment provided by engineer. + Show preview Ön gösterimi göser No comment provided by engineer. + + Show → on messages sent via private routing. + Gizli yönlendirme yoluyla gönderilen mesajlarda → işaretini göster. + No comment provided by engineer. + Show: Göster: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address SimpleX Adresi @@ -5344,6 +6075,10 @@ Hata: %@ Basitleştirilmiş gizli mod No comment provided by engineer. + + Size + No comment provided by engineer. + Skip Atla @@ -5359,11 +6094,23 @@ Hata: %@ Küçük gruplar (en fazla 20 kişi) No comment provided by engineer. + + Soft + blur media + + + Some file(s) were not exported: + No comment provided by engineer. + Some non-fatal errors occurred during import - you may see Chat console for more details. İçe aktarma sırasında bazı ölümcül olmayan hatalar oluştu - daha fazla ayrıntı için Sohbet konsoluna bakabilirsiniz. No comment provided by engineer. + + Some non-fatal errors occurred during import: + No comment provided by engineer. + Somebody Biri @@ -5389,6 +6136,14 @@ Hata: %@ Geçişi başlat No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop Dur @@ -5449,11 +6204,27 @@ Hata: %@ Sohbeti durdurma No comment provided by engineer. + + Strong + blur media + Submit Gönder No comment provided by engineer. + + Subscribed + No comment provided by engineer. + + + Subscription errors + No comment provided by engineer. + + + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat SimpleX Chat'e destek ol @@ -5469,6 +6240,10 @@ Hata: %@ Sistem yetkilendirilmesi No comment provided by engineer. + + TCP connection + No comment provided by engineer. + TCP connection timeout TCP bağlantı zaman aşımı @@ -5529,9 +6304,8 @@ Hata: %@ Taramak için tıkla No comment provided by engineer. - - Tap to start a new chat - Yeni bir sohbet başlatmak için tıkla + + Temporary file error No comment provided by engineer. @@ -5586,6 +6360,11 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Uygulama, mesaj veya iletişim isteği aldığınızda sizi bilgilendirebilir - etkinleştirmek için lütfen ayarları açın. No comment provided by engineer. + + The app will ask to confirm downloads from unknown file servers (except .onion). + Uygulama bilinmeyen dosya sunucularından indirmeleri onaylamanızı isteyecektir (.onion hariç). + No comment provided by engineer. + The attempt to change database passphrase was not completed. Veritabanı parolasını değiştirme girişimi tamamlanmadı. @@ -5631,6 +6410,14 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Mesaj tüm üyeler için yönetilmiş olarak işaretlenecektir. No comment provided by engineer. + + The messages will be deleted for all members. + No comment provided by engineer. + + + The messages will be marked as moderated for all members. + No comment provided by engineer. + The next generation of private messaging Gizli mesajlaşmanın yeni nesli @@ -5666,9 +6453,8 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Yapıştırdığın metin bir SimpleX bağlantısı değildir. No comment provided by engineer. - - Theme - Tema + + Themes No comment provided by engineer. @@ -5736,11 +6522,19 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Bu senin kendi tek kullanımlık bağlantın! No comment provided by engineer. + + This link was used with another mobile device, please create a new link on the desktop. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. Bu ayar, geçerli sohbet profiliniz **%@** deki mesajlara uygulanır. No comment provided by engineer. + + Title + No comment provided by engineer. + To ask any questions and to receive updates: Soru sormak ve güncellemeleri almak için: @@ -5771,6 +6565,11 @@ Bazı hatalar nedeniyle veya bağlantı tehlikeye girdiğinde meydana gelebilir. Zaman bölgesini korumak için,fotoğraf/ses dosyaları UTC kullanır. No comment provided by engineer. + + To protect your IP address, private routing uses your SMP servers to deliver messages. + IP adresinizi korumak için,gizli yönlendirme mesajları iletmek için SMP sunucularınızı kullanır. + No comment provided by engineer. + To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5798,16 +6597,32 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Kişinizle uçtan uca şifrelemeyi doğrulamak için cihazlarınızdaki kodu karşılaştırın (veya tarayın). No comment provided by engineer. + + Toggle chat list: + No comment provided by engineer. + Toggle incognito when connecting. Bağlanırken gizli moda geçiş yap. No comment provided by engineer. + + Toolbar opacity + No comment provided by engineer. + + + Total + No comment provided by engineer. + Transport isolation Taşıma izolasyonu No comment provided by engineer. + + Transport sessions + No comment provided by engineer. + Trying to connect to the server used to receive messages from this contact (error: %@). Bu kişiden mesaj almak için kullanılan sunucuya bağlanılmaya çalışılıyor (hata: %@). @@ -5863,11 +6678,6 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Üyenin engeli kaldırılsın mı? No comment provided by engineer. - - Unexpected error: %@ - Beklenmeyen hata: %@ - item status description - Unexpected migration state Beklenmeyen geçiş durumu @@ -5876,7 +6686,7 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Unfav. Favorilerden çık. - No comment provided by engineer. + swipe action Unhide @@ -5913,6 +6723,11 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Bilinmeyen hata No comment provided by engineer. + + Unknown servers! + Bilinmeyen sunucular! + No comment provided by engineer. + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. iOS arama arayüzünü kullanmadığınız sürece, kesintileri önlemek için Rahatsız Etmeyin modunu etkinleştirin. @@ -5948,12 +6763,12 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Unmute Susturmayı kaldır - No comment provided by engineer. + swipe action Unread Okunmamış - No comment provided by engineer. + swipe action Up to 100 last messages are sent to new members. @@ -5965,11 +6780,6 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Güncelle No comment provided by engineer. - - Update .onion hosts setting? - .onion ana bilgisayarların ayarı güncellensin mi? - No comment provided by engineer. - Update database passphrase Veritabanı parolasını güncelle @@ -5980,9 +6790,8 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Bağlantı ayarları güncellensin mi? No comment provided by engineer. - - Update transport isolation mode? - Taşıma izolasyon modu güncellensin mi? + + Update settings? No comment provided by engineer. @@ -5990,16 +6799,15 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Ayarların güncellenmesi, istemciyi tüm sunuculara yeniden bağlayacaktır. No comment provided by engineer. - - Updating this setting will re-connect the client to all servers. - Bu ayarın güncellenmesi, istemciyi tüm sunuculara yeniden bağlayacaktır. - No comment provided by engineer. - Upgrade and open chat Yükselt ve sohbeti aç No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed Yükleme başarısız @@ -6010,6 +6818,14 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Dosya yükle server test step + + Uploaded + No comment provided by engineer. + + + Uploaded files + No comment provided by engineer. + Uploading archive Arşiv yükleme @@ -6060,6 +6876,16 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Sadece yerel bildirimler kullanılsın mı? No comment provided by engineer. + + Use private routing with unknown servers when IP address is not protected. + IP adresi korunmadığında bilinmeyen sunucularla gizli yönlendirme kullan. + No comment provided by engineer. + + + Use private routing with unknown servers. + Bilinmeyen sunucularla gizli yönlendirme kullan. + No comment provided by engineer. + Use server Sunucu kullan @@ -6070,14 +6896,17 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Görüşme sırasında uygulamayı kullanın. No comment provided by engineer. + + Use the app with one hand. + No comment provided by engineer. + User profile Kullanıcı profili No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - .onion ana bilgisayarlarını kullanmak için uyumlu VPN sağlayıcısı gerekir. + + User selection No comment provided by engineer. @@ -6210,6 +7039,14 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Video bekleniyor No comment provided by engineer. + + Wallpaper accent + No comment provided by engineer. + + + Wallpaper background + No comment provided by engineer. + Warning: starting chat on multiple devices is not supported and will cause message delivery failures Uyarı: birden fazla cihazda sohbet başlatmak desteklenmez ve mesaj teslim hatalarına neden olur @@ -6295,19 +7132,37 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Azaltılmış pil kullanımı ile birlikte. No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + Tor veya VPN olmadan, IP adresiniz dosya sunucularına görülebilir. + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + Tor veya VPN olmadan, IP adresiniz bu XFTP aktarıcıları tarafından görülebilir: %@. + No comment provided by engineer. + Wrong database passphrase Yanlış veritabanı parolası No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + Yanlış anahtar veya bilinmeyen bağlantı - büyük olasılıkla bu bağlantı silinmiştir. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + file error text + Wrong passphrase! Yanlış parola! No comment provided by engineer. - - XFTP servers - XFTP sunucuları + + XFTP server No comment provided by engineer. @@ -6387,11 +7242,19 @@ Katılma isteği tekrarlansın mı? Gruba davet edildiniz No comment provided by engineer. + + You are not connected to these servers. Private routing is used to deliver messages to them. + No comment provided by engineer. + You can accept calls from lock screen, without device and app authentication. Cihaz ve uygulama kimlik doğrulaması olmadan kilit ekranından çağrı kabul edebilirsiniz. No comment provided by engineer. + + You can change it in Appearance settings. + No comment provided by engineer. + You can create it later Daha sonra oluşturabilirsiniz @@ -6422,11 +7285,15 @@ Katılma isteği tekrarlansın mı? Ayarlardan SimpleX kişilerinize görünür yapabilirsiniz. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Artık %@ adresine mesaj gönderebilirsin notification body + + You can send messages to %@ from Archived contacts. + No comment provided by engineer. + You can set lock screen notification preview via settings. Kilit ekranı bildirim önizlemesini ayarlar üzerinden ayarlayabilirsiniz. @@ -6452,6 +7319,10 @@ Katılma isteği tekrarlansın mı? Sohbeti uygulamada Ayarlar / Veritabanı üzerinden veya uygulamayı yeniden başlatarak başlatabilirsiniz No comment provided by engineer. + + You can still view conversation with %@ in the list of chats. + No comment provided by engineer. + You can turn on SimpleX Lock via Settings. SimpleX Kilidini Ayarlar üzerinden açabilirsiniz. @@ -6494,11 +7365,6 @@ Repeat connection request? Bağlantı isteği tekrarlansın mı? No comment provided by engineer. - - You have no chats - Hiç sohbetiniz yok - No comment provided by engineer. - You have to enter passphrase every time the app starts - it is not stored on the device. Uygulama her başladığında parola girmeniz gerekir - parola cihazınızda saklanmaz. @@ -6519,11 +7385,23 @@ Bağlantı isteği tekrarlansın mı? Bu gruba katıldınız. Davet eden grup üyesine bağlanılıyor. No comment provided by engineer. + + You may migrate the exported database. + No comment provided by engineer. + + + You may save the exported archive. + No comment provided by engineer. + You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. Sohbet veritabanınızın en son sürümünü SADECE bir cihazda kullanmalısınız, aksi takdirde bazı kişilerden daha fazla mesaj alamayabilirsiniz. No comment provided by engineer. + + You need to allow your contact to call to be able to call them. + No comment provided by engineer. + You need to allow your contact to send voice messages to be able to send them. Sesli mesaj gönderebilmeniz için kişinizin de sesli mesaj göndermesine izin vermeniz gerekir. @@ -6639,13 +7517,6 @@ Bağlantı isteği tekrarlansın mı? Sohbet profillerin No comment provided by engineer. - - Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link). - Bağlantının tamamlanması için kişinizin çevrimiçi olması gerekir. -Bu bağlantıyı iptal edebilir ve kişiyi kaldırabilirsiniz (ve daha sonra yeni bir bağlantıyla deneyebilirsiniz). - No comment provided by engineer. - Your contact sent a file that is larger than currently supported maximum size (%@). Kişiniz şu anda desteklenen maksimum boyuttan (%@) daha büyük bir dosya gönderdi. @@ -6793,6 +7664,10 @@ SimpleX sunucuları profilinizi göremez. ve %lld diğer etkinlikler No comment provided by engineer. + + attempts + No comment provided by engineer. + audio call (not e2e encrypted) sesli arama (uçtan uca şifreli değil) @@ -6833,6 +7708,10 @@ SimpleX sunucuları profilinizi göremez. kalın No comment provided by engineer. + + call + No comment provided by engineer. + call error arama hatası @@ -6983,6 +7862,10 @@ SimpleX sunucuları profilinizi göremez. gün time unit + + decryption errors + No comment provided by engineer. + default (%@) varsayılan (%@) @@ -7033,6 +7916,10 @@ SimpleX sunucuları profilinizi göremez. yinelenen mesaj integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted uçtan uca şifrelenmiş @@ -7113,6 +8000,10 @@ SimpleX sunucuları profilinizi göremez. etkinlik yaşandı No comment provided by engineer. + + expired + No comment provided by engineer. + forwarded iletildi @@ -7143,6 +8034,10 @@ SimpleX sunucuları profilinizi göremez. iOS Anahtar Zinciri, uygulamayı yeniden başlattıktan veya parolayı değiştirdikten sonra parolayı güvenli bir şekilde saklamak için kullanılacaktır - anlık bildirimlerin alınmasına izin verecektir. No comment provided by engineer. + + inactive + No comment provided by engineer. + incognito via contact address link kişi bağlantı linki aracılığıyla gizli @@ -7183,6 +8078,10 @@ SimpleX sunucuları profilinizi göremez. %@ grubuna davet group name + + invite + No comment provided by engineer. + invited davet edildi @@ -7238,6 +8137,10 @@ SimpleX sunucuları profilinizi göremez. bağlanıldı rcv group event chat item + + message + No comment provided by engineer. + message received mesaj alındı @@ -7268,6 +8171,10 @@ SimpleX sunucuları profilinizi göremez. aylar time unit + + mute + No comment provided by engineer. + never asla @@ -7320,6 +8227,14 @@ SimpleX sunucuları profilinizi göremez. açık group pref value + + other + No comment provided by engineer. + + + other errors + No comment provided by engineer. + owner sahip @@ -7390,6 +8305,10 @@ SimpleX sunucuları profilinizi göremez. %@ tarafından kaydedildi No comment provided by engineer. + + search + No comment provided by engineer. + sec sn @@ -7415,6 +8334,15 @@ SimpleX sunucuları profilinizi göremez. doğrudan mesaj gönder No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + sunucu kuyruk bilgisi: %1$@ + +son alınan msj: %2$@ + queue info + set new contact address yeni kişi adresi ayarla @@ -7455,11 +8383,25 @@ SimpleX sunucuları profilinizi göremez. bilinmeyen connection info + + unknown servers + bilinmeyen yönlendiriciler + No comment provided by engineer. + unknown status bilinmeyen durum No comment provided by engineer. + + unmute + No comment provided by engineer. + + + unprotected + korumasız + No comment provided by engineer. + updated group profile grup profili güncellendi @@ -7500,6 +8442,10 @@ SimpleX sunucuları profilinizi göremez. yönlendirici aracılığıyla No comment provided by engineer. + + video + No comment provided by engineer. + video call (not e2e encrypted) Görüntülü arama (şifrelenmiş değil) @@ -7525,6 +8471,11 @@ SimpleX sunucuları profilinizi göremez. haftalar time unit + + when IP hidden + IP gizliyken + No comment provided by engineer. + yes evet @@ -7609,7 +8560,7 @@ SimpleX sunucuları profilinizi göremez.
- +
@@ -7646,7 +8597,7 @@ SimpleX sunucuları profilinizi göremez.
- +
@@ -7666,4 +8617,178 @@ SimpleX sunucuları profilinizi göremez.
+ +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + No comment provided by engineer. + + + App is locked! + No comment provided by engineer. + + + Cancel + No comment provided by engineer. + + + Cannot access keychain to save database password + No comment provided by engineer. + + + Cannot forward message + No comment provided by engineer. + + + Comment + No comment provided by engineer. + + + Currently maximum supported file size is %@. + No comment provided by engineer. + + + Database downgrade required + No comment provided by engineer. + + + Database encrypted! + No comment provided by engineer. + + + Database error + No comment provided by engineer. + + + Database passphrase is different from saved in the keychain. + No comment provided by engineer. + + + Database passphrase is required to open chat. + No comment provided by engineer. + + + Database upgrade required + No comment provided by engineer. + + + Error preparing file + No comment provided by engineer. + + + Error preparing message + No comment provided by engineer. + + + Error: %@ + No comment provided by engineer. + + + File error + No comment provided by engineer. + + + Incompatible database version + No comment provided by engineer. + + + Invalid migration confirmation + No comment provided by engineer. + + + Keychain error + No comment provided by engineer. + + + Large file! + No comment provided by engineer. + + + No active profile + No comment provided by engineer. + + + Ok + No comment provided by engineer. + + + Open the app to downgrade the database. + No comment provided by engineer. + + + Open the app to upgrade the database. + No comment provided by engineer. + + + Passphrase + No comment provided by engineer. + + + Please create a profile in the SimpleX app + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending a message takes longer than expected. + No comment provided by engineer. + + + Sending message… + No comment provided by engineer. + + + Share + No comment provided by engineer. + + + Slow network? + No comment provided by engineer. + + + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + No comment provided by engineer. + + + Wait + No comment provided by engineer. + + + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/tr.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/tr.xcloc/contents.json b/apps/ios/SimpleX Localizations/tr.xcloc/contents.json index 0aee97a599..6f74640a6b 100644 --- a/apps/ios/SimpleX Localizations/tr.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/tr.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "tr", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "15F31d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "15.4" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff index 1ed1f5ffd6..7bcb30c1db 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Localized Contents/uk.xliff @@ -2,7 +2,7 @@
- +
@@ -127,11 +127,6 @@ %@ перевірено No comment provided by engineer. - - %@ servers - %@ сервери - No comment provided by engineer. - %@ uploaded %@ завантажено @@ -557,16 +552,17 @@ Про адресу SimpleX No comment provided by engineer. - - Accent color - Акцентний колір + + Accent + Акцент No comment provided by engineer. Accept Прийняти accept contact request via notification - accept incoming call via notification + accept incoming call via notification + swipe action Accept connection request? @@ -581,7 +577,23 @@ Accept incognito Прийняти інкогніто - accept contact request via notification + accept contact request via notification + swipe action + + + Acknowledged + Визнано + No comment provided by engineer. + + + Acknowledgement errors + Помилки підтвердження + No comment provided by engineer. + + + Active connections + Активні з'єднання + No comment provided by engineer. Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. @@ -603,16 +615,16 @@ Додати профіль No comment provided by engineer. + + Add server + Додати сервер + No comment provided by engineer. + Add servers by scanning QR codes. Додайте сервери, відсканувавши QR-код. No comment provided by engineer. - - Add server… - Додати сервер… - No comment provided by engineer. - Add to another device Додати до іншого пристрою @@ -623,6 +635,21 @@ Додати вітальне повідомлення No comment provided by engineer. + + Additional accent + Додатковий акцент + No comment provided by engineer. + + + Additional accent 2 + Додатковий акцент 2 + No comment provided by engineer. + + + Additional secondary + Додаткова вторинна + No comment provided by engineer. + Address Адреса @@ -648,6 +675,11 @@ Розширені налаштування мережі No comment provided by engineer. + + Advanced settings + Додаткові налаштування + No comment provided by engineer. + All app data is deleted. Всі дані програми видаляються. @@ -663,6 +695,11 @@ Всі дані стираються при введенні. No comment provided by engineer. + + All data is private to your device. + Всі дані є приватними для вашого пристрою. + No comment provided by engineer. + All group members will remain connected. Всі учасники групи залишаться на зв'язку. @@ -683,6 +720,11 @@ Всі нові повідомлення від %@ будуть приховані! No comment provided by engineer. + + All profiles + Всі профілі + No comment provided by engineer. + All your contacts will remain connected. Всі ваші контакти залишаться на зв'язку. @@ -708,11 +750,21 @@ Дозволяйте дзвінки, тільки якщо ваш контакт дозволяє їх. No comment provided by engineer. + + Allow calls? + Дозволити дзвінки? + No comment provided by engineer. + Allow disappearing messages only if your contact allows it to you. Дозволяйте зникати повідомленням, тільки якщо контакт дозволяє вам це робити. No comment provided by engineer. + + Allow downgrade + Дозволити пониження версії + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) Дозволяйте безповоротне видалення повідомлень, тільки якщо контакт дозволяє вам це зробити. (24 години) @@ -738,6 +790,11 @@ Дозволити надсилання зникаючих повідомлень. No comment provided by engineer. + + Allow sharing + Дозволити спільний доступ + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) Дозволяє безповоротно видаляти надіслані повідомлення. (24 години) @@ -745,6 +802,7 @@ Allow to send SimpleX links. + Дозволити надсилати посилання SimpleX. No comment provided by engineer. @@ -807,6 +865,11 @@ Вже приєднуємося до групи! No comment provided by engineer. + + Always use private routing. + Завжди використовуйте приватну маршрутизацію. + No comment provided by engineer. + Always use relay Завжди використовуйте реле @@ -872,11 +935,26 @@ Подати заявку No comment provided by engineer. + + Apply to + Звертатися до + No comment provided by engineer. + Archive and upload Архівування та завантаження No comment provided by engineer. + + Archive contacts to chat later. + Архівуйте контакти, щоб поспілкуватися пізніше. + No comment provided by engineer. + + + Archived contacts + Архівні контакти + No comment provided by engineer. + Archiving database Архівування бази даних @@ -947,6 +1025,11 @@ Назад No comment provided by engineer. + + Background + Фон + No comment provided by engineer. + Bad desktop address Неправильна адреса робочого столу @@ -972,6 +1055,16 @@ Кращі повідомлення No comment provided by engineer. + + Better networking + Краща мережа + No comment provided by engineer. + + + Black + Чорний + No comment provided by engineer. + Block Блокувати @@ -1007,6 +1100,16 @@ Заблокований адміністратором No comment provided by engineer. + + Blur for better privacy. + Розмиття для кращої приватності. + No comment provided by engineer. + + + Blur media + Розмиття медіа + No comment provided by engineer. + Both you and your contact can add message reactions. Реакції на повідомлення можете додавати як ви, так і ваш контакт. @@ -1052,11 +1155,26 @@ Дзвінки No comment provided by engineer. + + Calls prohibited! + Дзвінки заборонені! + No comment provided by engineer. + Camera not available Камера недоступна No comment provided by engineer. + + Can't call contact + Не вдається додзвонитися до контакту + No comment provided by engineer. + + + Can't call member + Не вдається зателефонувати користувачеві + No comment provided by engineer. + Can't invite contact! Не вдається запросити контакт! @@ -1067,6 +1185,11 @@ Неможливо запросити контакти! No comment provided by engineer. + + Can't message member + Не можу надіслати повідомлення користувачеві + No comment provided by engineer. + Cancel Скасувати @@ -1082,13 +1205,24 @@ Не вдається отримати доступ до зв'язки ключів для збереження пароля до бази даних No comment provided by engineer. + + Cannot forward message + Неможливо переслати повідомлення + No comment provided by engineer. + Cannot receive file Не вдається отримати файл No comment provided by engineer. + + Capacity exceeded - recipient did not receive previously sent messages. + Перевищено ліміт - одержувач не отримав раніше надіслані повідомлення. + snd error text + Cellular + Стільниковий No comment provided by engineer. @@ -1147,6 +1281,11 @@ Архів чату No comment provided by engineer. + + Chat colors + Кольори чату + No comment provided by engineer. + Chat console Консоль чату @@ -1162,6 +1301,11 @@ Видалено базу даних чату No comment provided by engineer. + + Chat database exported + Експортовано базу даних чату + No comment provided by engineer. + Chat database imported Імпорт бази даних чату @@ -1182,6 +1326,11 @@ Чат зупинено. Якщо ви вже використовували цю базу даних на іншому пристрої, перенесіть її назад перед запуском чату. No comment provided by engineer. + + Chat list + Список чатів + No comment provided by engineer. + Chat migrated! Чат перемістився! @@ -1192,6 +1341,11 @@ Налаштування чату No comment provided by engineer. + + Chat theme + Тема чату + No comment provided by engineer. + Chats Чати @@ -1222,10 +1376,25 @@ Виберіть з бібліотеки No comment provided by engineer. + + Chunks deleted + Фрагменти видалено + No comment provided by engineer. + + + Chunks downloaded + Завантажено фрагменти + No comment provided by engineer. + + + Chunks uploaded + Завантажено фрагменти + No comment provided by engineer. + Clear Чисто - No comment provided by engineer. + swipe action Clear conversation @@ -1247,9 +1416,14 @@ Очистити перевірку No comment provided by engineer. - - Colors - Кольори + + Color chats with the new themes. + Кольорові чати з новими темами. + No comment provided by engineer. + + + Color mode + Колірний режим No comment provided by engineer. @@ -1262,11 +1436,21 @@ Порівняйте коди безпеки зі своїми контактами. No comment provided by engineer. + + Completed + Завершено + No comment provided by engineer. + Configure ICE servers Налаштування серверів ICE No comment provided by engineer. + + Configured %@ servers + Налаштовані сервери %@ + No comment provided by engineer. + Confirm Підтвердити @@ -1277,11 +1461,21 @@ Підтвердити пароль No comment provided by engineer. + + Confirm contact deletion? + Підтвердити видалення контакту? + No comment provided by engineer. + Confirm database upgrades Підтвердити оновлення бази даних No comment provided by engineer. + + Confirm files from unknown servers. + Підтвердити файли з невідомих серверів. + No comment provided by engineer. + Confirm network settings Підтвердьте налаштування мережі @@ -1327,6 +1521,11 @@ Підключення до комп'ютера No comment provided by engineer. + + Connect to your friends faster. + Швидше спілкуйтеся з друзями. + No comment provided by engineer. + Connect to yourself? З'єднатися з самим собою? @@ -1366,16 +1565,31 @@ This is your own one-time link! Підключитися до %@ No comment provided by engineer. + + Connected + Підключено + No comment provided by engineer. + Connected desktop Підключений робочий стіл No comment provided by engineer. + + Connected servers + Підключені сервери + No comment provided by engineer. + Connected to desktop Підключено до настільного комп'ютера No comment provided by engineer. + + Connecting + Підключення + No comment provided by engineer. + Connecting to server… Підключення до сервера… @@ -1386,6 +1600,11 @@ This is your own one-time link! Підключення до сервера... (помилка: %@) No comment provided by engineer. + + Connecting to contact, please wait or check later! + З'єднання з контактом, будь ласка, зачекайте або перевірте пізніше! + No comment provided by engineer. + Connecting to desktop Підключення до ПК @@ -1396,6 +1615,11 @@ This is your own one-time link! Підключення No comment provided by engineer. + + Connection and servers status. + Стан з'єднання та серверів. + No comment provided by engineer. + Connection error Помилка підключення @@ -1406,6 +1630,11 @@ This is your own one-time link! Помилка підключення (AUTH) No comment provided by engineer. + + Connection notifications + Сповіщення про підключення + No comment provided by engineer. + Connection request sent! Запит на підключення відправлено! @@ -1421,6 +1650,16 @@ This is your own one-time link! Тайм-аут з'єднання No comment provided by engineer. + + Connection with desktop stopped + Припинено зв'язок з робочим столом + No comment provided by engineer. + + + Connections + З'єднання + No comment provided by engineer. + Contact allows Контакт дозволяє @@ -1431,6 +1670,11 @@ This is your own one-time link! Контакт вже існує No comment provided by engineer. + + Contact deleted! + Контакт видалено! + No comment provided by engineer. + Contact hidden: Контакт приховано: @@ -1441,9 +1685,9 @@ This is your own one-time link! Контакт підключений notification - - Contact is not connected yet! - Контакт ще не підключено! + + Contact is deleted. + Контакт видалено. No comment provided by engineer. @@ -1456,6 +1700,11 @@ This is your own one-time link! Налаштування контактів No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + Контакт буде видалено - це неможливо скасувати! + No comment provided by engineer. + Contacts Контакти @@ -1471,10 +1720,20 @@ This is your own one-time link! Продовжуйте No comment provided by engineer. + + Conversation deleted! + Розмова видалена! + No comment provided by engineer. + Copy Копіювати - chat item action + No comment provided by engineer. + + + Copy error + Помилка копіювання + No comment provided by engineer. Core version: v%@ @@ -1551,6 +1810,11 @@ This is your own one-time link! Створіть свій профіль No comment provided by engineer. + + Created + Створено + No comment provided by engineer. + Created at Створено за адресою @@ -1586,6 +1850,11 @@ This is your own one-time link! Поточна парольна фраза… No comment provided by engineer. + + Current profile + Поточний профіль + No comment provided by engineer. + Currently maximum supported file size is %@. Наразі максимальний підтримуваний розмір файлу - %@. @@ -1596,11 +1865,21 @@ This is your own one-time link! Індивідуальний час No comment provided by engineer. + + Customize theme + Налаштувати тему + No comment provided by engineer. + Dark Темний No comment provided by engineer. + + Dark mode colors + Кольори темного режиму + No comment provided by engineer. + Database ID Ідентифікатор бази даних @@ -1699,6 +1978,11 @@ This is your own one-time link! База даних буде перенесена під час перезапуску програми No comment provided by engineer. + + Debug delivery + Доставка налагодження + No comment provided by engineer. + Decentralized Децентралізований @@ -1712,18 +1996,19 @@ This is your own one-time link! Delete Видалити - chat item action + chat item action + swipe action + + + Delete %lld messages of members? + Видалити %lld повідомлень користувачів? + No comment provided by engineer. Delete %lld messages? Видалити %lld повідомлень? No comment provided by engineer. - - Delete Contact - Видалити контакт - No comment provided by engineer. - Delete address Видалити адресу @@ -1779,11 +2064,9 @@ This is your own one-time link! Видалити контакт No comment provided by engineer. - - Delete contact? -This cannot be undone! - Видалити контакт? -Це не можна скасувати! + + Delete contact? + Видалити контакт? No comment provided by engineer. @@ -1876,11 +2159,6 @@ This cannot be undone! Видалити стару базу даних? No comment provided by engineer. - - Delete pending connection - Видалити очікуване з'єднання - No comment provided by engineer. - Delete pending connection? Видалити очікуване з'єднання? @@ -1896,11 +2174,26 @@ This cannot be undone! Видалити чергу server test step + + Delete up to 20 messages at once. + Видаляйте до 20 повідомлень одночасно. + No comment provided by engineer. + Delete user profile? Видалити профіль користувача? No comment provided by engineer. + + Delete without notification + Видалення без попередження + No comment provided by engineer. + + + Deleted + Видалено + No comment provided by engineer. + Deleted at Видалено за @@ -1911,6 +2204,11 @@ This cannot be undone! Видалено за: %@ copied message info + + Deletion errors + Помилки видалення + No comment provided by engineer. + Delivery Доставка @@ -1946,11 +2244,41 @@ This cannot be undone! Настільні пристрої No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + Адреса сервера призначення %@ несумісна з налаштуваннями сервера пересилання %@. + No comment provided by engineer. + + + Destination server error: %@ + Помилка сервера призначення: %@ + snd error text + + + Destination server version of %@ is incompatible with forwarding server %@. + Версія сервера призначення %@ несумісна з версією сервера переадресації %@. + No comment provided by engineer. + + + Detailed statistics + Детальна статистика + No comment provided by engineer. + + + Details + Деталі + No comment provided by engineer. + Develop Розробник No comment provided by engineer. + + Developer options + Можливості для розробників + No comment provided by engineer. + Developer tools Інструменти для розробників @@ -2001,6 +2329,11 @@ This cannot be undone! Вимкнути для всіх No comment provided by engineer. + + Disabled + Вимкнено + No comment provided by engineer. + Disappearing message Зникаюче повідомлення @@ -2051,11 +2384,21 @@ This cannot be undone! Відкриття через локальну мережу No comment provided by engineer. + + Do NOT send messages directly, even if your or destination server does not support private routing. + НЕ надсилайте повідомлення напряму, навіть якщо ваш сервер або сервер призначення не підтримує приватну маршрутизацію. + No comment provided by engineer. + Do NOT use SimpleX for emergency calls. НЕ використовуйте SimpleX для екстрених викликів. No comment provided by engineer. + + Do NOT use private routing. + НЕ використовуйте приватну маршрутизацію. + No comment provided by engineer. + Do it later Зробіть це пізніше @@ -2088,8 +2431,14 @@ This cannot be undone! Download + Завантажити chat item action + + Download errors + Помилки завантаження + No comment provided by engineer. + Download failed Не вдалося завантажити @@ -2100,6 +2449,16 @@ This cannot be undone! Завантажити файл server test step + + Downloaded + Завантажено + No comment provided by engineer. + + + Downloaded files + Завантажені файли + No comment provided by engineer. + Downloading archive Завантажити архів @@ -2200,8 +2559,14 @@ This cannot be undone! Увімкнути пароль самознищення set passcode view + + Enabled + Увімкнено + No comment provided by engineer. + Enabled for + Увімкнено для No comment provided by engineer. @@ -2369,6 +2734,11 @@ This cannot be undone! Помилка зміни налаштування No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + Помилка підключення до сервера переадресації %@. Спробуйте пізніше. + No comment provided by engineer. + Error creating address Помилка створення адреси @@ -2419,11 +2789,6 @@ This cannot be undone! Помилка видалення з'єднання No comment provided by engineer. - - Error deleting contact - Помилка видалення контакту - No comment provided by engineer. - Error deleting database Помилка видалення бази даних @@ -2469,6 +2834,11 @@ This cannot be undone! Помилка експорту бази даних чату No comment provided by engineer. + + Error exporting theme: %@ + Помилка експорту теми: %@ + No comment provided by engineer. + Error importing chat database Помилка імпорту бази даних чату @@ -2494,11 +2864,26 @@ This cannot be undone! Помилка отримання файлу No comment provided by engineer. + + Error reconnecting server + Помилка перепідключення сервера + No comment provided by engineer. + + + Error reconnecting servers + Помилка перепідключення серверів + No comment provided by engineer. + Error removing member Помилка видалення учасника No comment provided by engineer. + + Error resetting statistics + Статистика скидання помилок + No comment provided by engineer. + Error saving %@ servers Помилка збереження %@ серверів @@ -2617,7 +3002,8 @@ This cannot be undone! Error: %@ Помилка: %@ - No comment provided by engineer. + file error text + snd error text Error: URL is invalid @@ -2629,6 +3015,11 @@ This cannot be undone! Помилка: немає файлу бази даних No comment provided by engineer. + + Errors + Помилки + No comment provided by engineer. + Even when disabled in the conversation. Навіть коли вимкнений у розмові. @@ -2654,6 +3045,11 @@ This cannot be undone! Помилка експорту: No comment provided by engineer. + + Export theme + Тема експорту + No comment provided by engineer. + Exported database archive. Експортований архів бази даних. @@ -2687,8 +3083,33 @@ This cannot be undone! Favorite Улюблений + swipe action + + + File error + Помилка файлу No comment provided by engineer. + + File not found - most likely file was deleted or cancelled. + Файл не знайдено - найімовірніше, файл було видалено або скасовано. + file error text + + + File server error: %@ + Помилка файлового сервера: %@ + file error text + + + File status + Статус файлу + No comment provided by engineer. + + + File status: %@ + Статус файлу: %@ + copied message info + File will be deleted from servers. Файл буде видалено з серверів. @@ -2709,6 +3130,11 @@ This cannot be undone! Файл: %@ No comment provided by engineer. + + Files + Файли + No comment provided by engineer. + Files & media Файли та медіа @@ -2726,6 +3152,7 @@ This cannot be undone! Files and media not allowed + Файли та медіафайли заборонені No comment provided by engineer. @@ -2795,20 +3222,53 @@ This cannot be undone! Forward + Пересилання chat item action Forward and save messages + Пересилання та збереження повідомлень No comment provided by engineer. Forwarded + Переслано No comment provided by engineer. Forwarded from + Переслано з No comment provided by engineer. + + Forwarding server %@ failed to connect to destination server %@. Please try later. + Серверу переадресації %@ не вдалося з'єднатися з сервером призначення %@. Спробуйте пізніше. + No comment provided by engineer. + + + Forwarding server address is incompatible with network settings: %@. + Адреса сервера переадресації несумісна з налаштуваннями мережі: %@. + No comment provided by engineer. + + + Forwarding server version is incompatible with network settings: %@. + Версія сервера переадресації несумісна з мережевими налаштуваннями: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + Сервер переадресації: %1$@ +Помилка сервера призначення: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + Сервер переадресації: %1$@ +Помилка: %2$@ + snd error text + Found desktop Знайдено робочий стіл @@ -2854,6 +3314,16 @@ This cannot be undone! GIF-файли та наклейки No comment provided by engineer. + + Good afternoon! + Доброго дня! + message preview + + + Good morning! + Доброго ранку! + message preview + Group Група @@ -2921,6 +3391,7 @@ This cannot be undone! Group members can send SimpleX links. + Учасники групи можуть надсилати посилання SimpleX. No comment provided by engineer. @@ -3133,6 +3604,11 @@ This cannot be undone! Не вдалося імпортувати No comment provided by engineer. + + Import theme + Імпорт теми + No comment provided by engineer. + Importing archive Імпорт архіву @@ -3165,6 +3641,7 @@ This cannot be undone! In-call sounds + Звуки вхідного дзвінка No comment provided by engineer. @@ -3254,6 +3731,11 @@ This cannot be undone! Інтерфейс No comment provided by engineer. + + Interface colors + Кольори інтерфейсу + No comment provided by engineer. + Invalid QR code Неправильний QR-код @@ -3355,6 +3837,11 @@ This cannot be undone! 3. З'єднання було скомпрометовано. No comment provided by engineer. + + It protects your IP address and connections. + Він захищає вашу IP-адресу та з'єднання. + No comment provided by engineer. + It seems like you are already connected via this link. If it is not the case, there was an error (%@). Схоже, що ви вже підключені за цим посиланням. Якщо це не так, сталася помилка (%@). @@ -3373,7 +3860,7 @@ This cannot be undone! Join Приєднуйтесь - No comment provided by engineer. + swipe action Join group @@ -3417,6 +3904,11 @@ This is your link for group %@! Тримай No comment provided by engineer. + + Keep conversation + Підтримуйте розмову + No comment provided by engineer. + Keep the app open to use it from desktop Тримайте додаток відкритим, щоб використовувати його з робочого столу @@ -3460,7 +3952,7 @@ This is your link for group %@! Leave Залишити - No comment provided by engineer. + swipe action Leave group @@ -3592,11 +4084,26 @@ This is your link for group %@! Максимум 30 секунд, отримується миттєво. No comment provided by engineer. + + Media & file servers + Медіа та файлові сервери + No comment provided by engineer. + + + Medium + Середній + blur media + Member Учасник No comment provided by engineer. + + Member inactive + Користувач неактивний + item status text + Member role will be changed to "%@". All group members will be notified. Роль учасника буде змінено на "%@". Всі учасники групи будуть повідомлені про це. @@ -3612,6 +4119,11 @@ This is your link for group %@! Учасник буде видалений з групи - це неможливо скасувати! No comment provided by engineer. + + Menus + Меню + No comment provided by engineer. + Message delivery error Помилка доставки повідомлення @@ -3622,11 +4134,31 @@ This is your link for group %@! Підтвердження доставки повідомлення! No comment provided by engineer. + + Message delivery warning + Попередження про доставку повідомлення + item status text + Message draft Чернетка повідомлення No comment provided by engineer. + + Message forwarded + Повідомлення переслано + item status text + + + Message may be delivered later if member becomes active. + Повідомлення може бути доставлене пізніше, якщо користувач стане активним. + item status description + + + Message queue info + Інформація про чергу повідомлень + No comment provided by engineer. + Message reactions Реакції на повідомлення @@ -3642,10 +4174,31 @@ This is your link for group %@! Реакції на повідомлення в цій групі заборонені. No comment provided by engineer. + + Message reception + Прийом повідомлень + No comment provided by engineer. + + + Message servers + Сервери повідомлень + No comment provided by engineer. + Message source remains private. + Джерело повідомлення залишається приватним. No comment provided by engineer. + + Message status + Статус повідомлення + No comment provided by engineer. + + + Message status: %@ + Статус повідомлення: %@ + copied message info + Message text Текст повідомлення @@ -3671,6 +4224,16 @@ This is your link for group %@! Повідомлення від %@ будуть показані! No comment provided by engineer. + + Messages received + Отримані повідомлення + No comment provided by engineer. + + + Messages sent + Надіслані повідомлення + No comment provided by engineer. + Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. Повідомлення, файли та дзвінки захищені **наскрізним шифруванням** з ідеальною секретністю переадресації, відмовою та відновленням після злому. @@ -3763,6 +4326,7 @@ This is your link for group %@! More reliable network connection. + Більш надійне з'єднання з мережею. No comment provided by engineer. @@ -3770,11 +4334,6 @@ This is your link for group %@! Швидше за все, це з'єднання видалено. item status description - - Most likely this contact has deleted the connection with you. - Швидше за все, цей контакт видалив зв'язок з вами. - No comment provided by engineer. - Multiple chat profiles Кілька профілів чату @@ -3783,7 +4342,7 @@ This is your link for group %@! Mute Вимкнути звук - No comment provided by engineer. + swipe action Muted when inactive! @@ -3793,7 +4352,7 @@ This is your link for group %@! Name Ім'я - No comment provided by engineer. + swipe action Network & servers @@ -3802,10 +4361,17 @@ This is your link for group %@! Network connection + Підключення до мережі No comment provided by engineer. + + Network issues - message expired after many attempts to send it. + Проблеми з мережею - термін дії повідомлення закінчився після багатьох спроб надіслати його. + snd error text + Network management + Керування мережею No comment provided by engineer. @@ -3828,6 +4394,11 @@ This is your link for group %@! Новий чат No comment provided by engineer. + + New chat experience 🎉 + Новий досвід спілкування в чаті 🎉 + No comment provided by engineer. + New contact request Новий запит на контакт @@ -3858,6 +4429,11 @@ This is your link for group %@! Нове в %@ No comment provided by engineer. + + New media options + Нові медіа-опції + No comment provided by engineer. + New member role Нова роль учасника @@ -3903,6 +4479,11 @@ This is your link for group %@! Токен пристрою відсутній! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + Прямого зв'язку ще немає, повідомлення пересилається адміністратором. + item status description + No filtered chats Немає фільтрованих чатів @@ -3918,8 +4499,14 @@ This is your link for group %@! Немає історії No comment provided by engineer. + + No info, try to reload + Немає інформації, спробуйте перезавантажити + No comment provided by engineer. + No network connection + Немає підключення до мережі No comment provided by engineer. @@ -3937,6 +4524,11 @@ This is your link for group %@! Не сумісні! No comment provided by engineer. + + Nothing selected + Нічого не вибрано + No comment provided by engineer. + Notifications Сповіщення @@ -3964,7 +4556,7 @@ This is your link for group %@! Off Вимкнено - No comment provided by engineer. + blur media Ok @@ -3986,14 +4578,18 @@ This is your link for group %@! Посилання на одноразове запрошення No comment provided by engineer. - - Onion hosts will be required for connection. Requires enabling VPN. - Для підключення будуть потрібні хости onion. Потрібно увімкнути VPN. + + Onion hosts will be **required** for connection. +Requires compatible VPN. + Для підключення будуть потрібні хости onion. +Потрібно увімкнути VPN. No comment provided by engineer. - - Onion hosts will be used when available. Requires enabling VPN. - Onion хости будуть використовуватися, коли вони будуть доступні. Потрібно увімкнути VPN. + + Onion hosts will be used when available. +Requires compatible VPN. + Onion хости будуть використовуватися, коли вони будуть доступні. +Потрібно увімкнути VPN. No comment provided by engineer. @@ -4006,6 +4602,11 @@ This is your link for group %@! Тільки клієнтські пристрої зберігають профілі користувачів, контакти, групи та повідомлення, надіслані за допомогою **2-шарового наскрізного шифрування**. No comment provided by engineer. + + Only delete conversation + Видаляйте тільки розмови + No comment provided by engineer. + Only group owners can change group preferences. Тільки власники груп можуть змінювати налаштування групи. @@ -4101,6 +4702,11 @@ This is your link for group %@! Відкрита міграція на інший пристрій authentication reason + + Open server settings + Відкрити налаштування сервера + No comment provided by engineer. + Open user profiles Відкрити профілі користувачів @@ -4138,6 +4744,12 @@ This is your link for group %@! Other + Інше + No comment provided by engineer. + + + Other %@ servers + Інші сервери %@ No comment provided by engineer. @@ -4205,6 +4817,11 @@ This is your link for group %@! Вставте отримане посилання No comment provided by engineer. + + Pending + В очікуванні + No comment provided by engineer. + People can connect to you only via the links you share. Люди можуть зв'язатися з вами лише за посиланнями, якими ви ділитеся. @@ -4225,11 +4842,28 @@ This is your link for group %@! Дзвінки "картинка в картинці No comment provided by engineer. + + Play from the chat list. + Грати зі списку чату. + No comment provided by engineer. + + + Please ask your contact to enable calls. + Будь ласка, попросіть свого контакту ввімкнути дзвінки. + No comment provided by engineer. + Please ask your contact to enable sending voice messages. Будь ласка, попросіть вашого контакту увімкнути відправку голосових повідомлень. No comment provided by engineer. + + Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers. + Переконайтеся, що мобільний і настільний комп'ютери підключені до однієї локальної мережі, і що брандмауер настільного комп'ютера дозволяє з'єднання. +Будь ласка, повідомте про будь-які інші проблеми розробникам. + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. Будь ласка, перевірте, чи ви скористалися правильним посиланням, або попросіть контактну особу надіслати вам інше. @@ -4327,6 +4961,11 @@ Error: %@ Попередній перегляд No comment provided by engineer. + + Previously connected servers + Раніше підключені сервери + No comment provided by engineer. + Privacy & security Конфіденційність і безпека @@ -4342,11 +4981,31 @@ Error: %@ Приватні імена файлів No comment provided by engineer. + + Private message routing + Маршрутизація приватних повідомлень + No comment provided by engineer. + + + Private message routing 🚀 + Маршрутизація приватних повідомлень 🚀 + No comment provided by engineer. + Private notes Приватні нотатки name of notes to self + + Private routing + Приватна маршрутизація + No comment provided by engineer. + + + Private routing error + Помилка приватної маршрутизації + No comment provided by engineer. + Profile and server connections З'єднання профілю та сервера @@ -4359,6 +5018,7 @@ Error: %@ Profile images + Зображення профілю No comment provided by engineer. @@ -4376,6 +5036,11 @@ Error: %@ Пароль до профілю No comment provided by engineer. + + Profile theme + Тема профілю + No comment provided by engineer. + Profile update will be sent to your contacts. Оновлення профілю буде надіслано вашим контактам. @@ -4403,6 +5068,7 @@ Error: %@ Prohibit sending SimpleX links. + Заборонити надсилання посилань SimpleX. No comment provided by engineer. @@ -4425,11 +5091,23 @@ Error: %@ Заборонити надсилання голосових повідомлень. No comment provided by engineer. + + Protect IP address + Захист IP-адреси + No comment provided by engineer. + Protect app screen Захистіть екран програми No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + Захистіть свою IP-адресу від ретрансляторів повідомлень, обраних вашими контактами. +Увімкніть у налаштуваннях *Мережа та сервери*. + No comment provided by engineer. + Protect your chat profiles with a password! Захистіть свої профілі чату паролем! @@ -4445,6 +5123,16 @@ Error: %@ Тайм-аут протоколу на КБ No comment provided by engineer. + + Proxied + Проксі-сервер + No comment provided by engineer. + + + Proxied servers + Проксі-сервери + No comment provided by engineer. + Push notifications Push-повідомлення @@ -4465,6 +5153,11 @@ Error: %@ Оцініть додаток No comment provided by engineer. + + Reachable chat toolbar + Доступна панель інструментів чату + No comment provided by engineer. + React… Реагуй… @@ -4473,7 +5166,7 @@ Error: %@ Read Читати - No comment provided by engineer. + swipe action Read more @@ -4510,6 +5203,11 @@ Error: %@ Підтвердження виключені No comment provided by engineer. + + Receive errors + Отримання помилок + No comment provided by engineer. + Received at Отримано за @@ -4530,15 +5228,26 @@ Error: %@ Отримано повідомлення message info title + + Received messages + Отримані повідомлення + No comment provided by engineer. + + + Received reply + Отримано відповідь + No comment provided by engineer. + + + Received total + Отримано всього + No comment provided by engineer. + Receiving address will be changed to a different server. Address change will complete after sender comes online. Адреса отримувача буде змінена на інший сервер. Зміна адреси завершиться після того, як відправник з'явиться в мережі. No comment provided by engineer. - - Receiving concurrency - No comment provided by engineer. - Receiving file will be stopped. Отримання файлу буде зупинено. @@ -4556,6 +5265,7 @@ Error: %@ Recipient(s) can't see who this message is from. + Одержувач(и) не бачить, від кого це повідомлення. No comment provided by engineer. @@ -4563,11 +5273,36 @@ Error: %@ Одержувачі бачать оновлення, коли ви їх вводите. No comment provided by engineer. + + Reconnect + Повторне підключення + No comment provided by engineer. + Reconnect all connected servers to force message delivery. It uses additional traffic. Перепідключіть всі підключені сервери, щоб примусово доставити повідомлення. Це використовує додатковий трафік. No comment provided by engineer. + + Reconnect all servers + Перепідключіть усі сервери + No comment provided by engineer. + + + Reconnect all servers? + Перепідключити всі сервери? + No comment provided by engineer. + + + Reconnect server to force message delivery. It uses additional traffic. + Перепідключити сервер для примусової доставки повідомлень. Використовує додатковий трафік. + No comment provided by engineer. + + + Reconnect server? + Перепідключити сервер? + No comment provided by engineer. + Reconnect servers? Перепідключити сервери? @@ -4591,7 +5326,8 @@ Error: %@ Reject Відхилити - reject incoming call via notification + reject incoming call via notification + swipe action Reject (sender NOT notified) @@ -4618,6 +5354,11 @@ Error: %@ Видалити No comment provided by engineer. + + Remove image + Видалити зображення + No comment provided by engineer. + Remove member Видалити учасника @@ -4688,16 +5429,41 @@ Error: %@ Перезавантаження No comment provided by engineer. + + Reset all hints + Скинути всі підказки + No comment provided by engineer. + + + Reset all statistics + Скинути всю статистику + No comment provided by engineer. + + + Reset all statistics? + Скинути всю статистику? + No comment provided by engineer. + Reset colors Скинути кольори No comment provided by engineer. + + Reset to app theme + Повернутися до теми програми + No comment provided by engineer. + Reset to defaults Відновити налаштування за замовчуванням No comment provided by engineer. + + Reset to user theme + Повернутися до теми користувача + No comment provided by engineer. + Restart the app to create a new chat profile Перезапустіть програму, щоб створити новий профіль чату @@ -4738,11 +5504,6 @@ Error: %@ Показувати chat item action - - Revert - Повернутися - No comment provided by engineer. - Revoke Відкликати @@ -4768,9 +5529,14 @@ Error: %@ Запустити чат No comment provided by engineer. - - SMP servers - Сервери SMP + + SMP server + Сервер SMP + No comment provided by engineer. + + + Safely receive files + Безпечне отримання файлів No comment provided by engineer. @@ -4798,6 +5564,11 @@ Error: %@ Зберегти та повідомити учасників групи No comment provided by engineer. + + Save and reconnect + Збережіть і підключіться знову + No comment provided by engineer. + Save and update group profile Збереження та оновлення профілю групи @@ -4860,6 +5631,7 @@ Error: %@ Saved + Збережено No comment provided by engineer. @@ -4869,6 +5641,7 @@ Error: %@ Saved from + Збережено з No comment provided by engineer. @@ -4876,6 +5649,16 @@ Error: %@ Збережене повідомлення message info title + + Scale + Масштаб + No comment provided by engineer. + + + Scan / Paste link + Відсканувати / Вставити посилання + No comment provided by engineer. + Scan QR code Відскануйте QR-код @@ -4916,11 +5699,21 @@ Error: %@ Знайдіть або вставте посилання SimpleX No comment provided by engineer. + + Secondary + Вторинний + No comment provided by engineer. + Secure queue Безпечна черга server test step + + Secured + Забезпечено + No comment provided by engineer. + Security assessment Оцінка безпеки @@ -4934,6 +5727,16 @@ Error: %@ Select Виберіть + chat item action + + + Selected %lld + Вибрано %lld + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + Вибрані налаштування чату забороняють це повідомлення. No comment provided by engineer. @@ -4971,11 +5774,6 @@ Error: %@ Надсилання звітів про доставку No comment provided by engineer. - - Send direct message - Надішліть пряме повідомлення - No comment provided by engineer. - Send direct message to connect Надішліть пряме повідомлення, щоб підключитися @@ -4986,6 +5784,11 @@ Error: %@ Надіслати зникаюче повідомлення No comment provided by engineer. + + Send errors + Помилки надсилання + No comment provided by engineer. + Send link previews Надіслати попередній перегляд за посиланням @@ -4996,6 +5799,21 @@ Error: %@ Надіслати живе повідомлення No comment provided by engineer. + + Send message to enable calls. + Надішліть повідомлення, щоб увімкнути дзвінки. + No comment provided by engineer. + + + Send messages directly when IP address is protected and your or destination server does not support private routing. + Надсилайте повідомлення напряму, якщо IP-адреса захищена, а ваш сервер або сервер призначення не підтримує приватну маршрутизацію. + No comment provided by engineer. + + + Send messages directly when your or destination server does not support private routing. + Надсилайте повідомлення напряму, якщо ваш сервер або сервер призначення не підтримує приватну маршрутизацію. + No comment provided by engineer. + Send notifications Надсилати сповіщення @@ -5086,6 +5904,11 @@ Error: %@ Надіслано за: %@ copied message info + + Sent directly + Відправлено напряму + No comment provided by engineer. + Sent file event Подія надісланого файлу @@ -5096,11 +5919,46 @@ Error: %@ Надіслано повідомлення message info title + + Sent messages + Надіслані повідомлення + No comment provided by engineer. + Sent messages will be deleted after set time. Надіслані повідомлення будуть видалені через встановлений час. No comment provided by engineer. + + Sent reply + Надіслано відповідь + No comment provided by engineer. + + + Sent total + Відправлено всього + No comment provided by engineer. + + + Sent via proxy + Відправлено через проксі + No comment provided by engineer. + + + Server address + Адреса сервера + No comment provided by engineer. + + + Server address is incompatible with network settings. + Адреса сервера несумісна з налаштуваннями мережі. + srv error text. + + + Server address is incompatible with network settings: %@. + Адреса сервера несумісна з налаштуваннями мережі: %@. + No comment provided by engineer. + Server requires authorization to create queues, check password Сервер вимагає авторизації для створення черг, перевірте пароль @@ -5116,11 +5974,36 @@ Error: %@ Тест сервера завершився невдало! No comment provided by engineer. + + Server type + Тип сервера + No comment provided by engineer. + + + Server version is incompatible with network settings. + Серверна версія несумісна з мережевими налаштуваннями. + srv error text + + + Server version is incompatible with your app: %@. + Версія сервера несумісна з вашим додатком: %@. + No comment provided by engineer. + Servers Сервери No comment provided by engineer. + + Servers info + Інформація про сервери + No comment provided by engineer. + + + Servers statistics will be reset - this cannot be undone! + Статистика серверів буде скинута - це неможливо скасувати! + No comment provided by engineer. + Session code Код сесії @@ -5136,6 +6019,11 @@ Error: %@ Встановити ім'я контакту… No comment provided by engineer. + + Set default theme + Встановлення теми за замовчуванням + No comment provided by engineer. + Set group preferences Встановіть налаштування групи @@ -5178,6 +6066,7 @@ Error: %@ Shape profile images + Сформуйте зображення профілю No comment provided by engineer. @@ -5200,6 +6089,11 @@ Error: %@ Поділіться адресою з контактами? No comment provided by engineer. + + Share from other apps. + Діліться з інших програм. + No comment provided by engineer. + Share link Поділіться посиланням @@ -5210,6 +6104,11 @@ Error: %@ Поділіться цим одноразовим посиланням-запрошенням No comment provided by engineer. + + Share to SimpleX + Поділіться з SimpleX + No comment provided by engineer. + Share with contacts Поділіться з контактами @@ -5235,16 +6134,36 @@ Error: %@ Показати останні повідомлення No comment provided by engineer. + + Show message status + Показати статус повідомлення + No comment provided by engineer. + + + Show percentage + Показати відсоток + No comment provided by engineer. + Show preview Показати попередній перегляд No comment provided by engineer. + + Show → on messages sent via private routing. + Показувати → у повідомленнях, надісланих через приватну маршрутизацію. + No comment provided by engineer. + Show: Показати: No comment provided by engineer. + + SimpleX + SimpleX + No comment provided by engineer. + SimpleX Address Адреса SimpleX @@ -5302,10 +6221,12 @@ Error: %@ SimpleX links are prohibited in this group. + У цій групі заборонені посилання на SimpleX. No comment provided by engineer. SimpleX links not allowed + Посилання SimpleX заборонені No comment provided by engineer. @@ -5318,6 +6239,11 @@ Error: %@ Спрощений режим інкогніто No comment provided by engineer. + + Size + Розмір + No comment provided by engineer. + Skip Пропустити @@ -5333,11 +6259,26 @@ Error: %@ Невеликі групи (максимум 20 осіб) No comment provided by engineer. + + Soft + М'який + blur media + + + Some file(s) were not exported: + Деякі файли не було експортовано: + No comment provided by engineer. + Some non-fatal errors occurred during import - you may see Chat console for more details. Під час імпорту виникли деякі нефатальні помилки – ви можете переглянути консоль чату, щоб дізнатися більше. No comment provided by engineer. + + Some non-fatal errors occurred during import: + Під час імпорту виникли деякі несмертельні помилки: + No comment provided by engineer. + Somebody Хтось @@ -5345,6 +6286,7 @@ Error: %@ Square, circle, or anything in between. + Квадрат, коло або щось середнє між ними. No comment provided by engineer. @@ -5362,6 +6304,16 @@ Error: %@ Почати міграцію No comment provided by engineer. + + Starting from %@. + Починаючи з %@. + No comment provided by engineer. + + + Statistics + Статистика + No comment provided by engineer. + Stop Зупинити @@ -5422,11 +6374,31 @@ Error: %@ Зупинка чату No comment provided by engineer. + + Strong + Сильний + blur media + Submit Надіслати No comment provided by engineer. + + Subscribed + Підписано + No comment provided by engineer. + + + Subscription errors + Помилки підписки + No comment provided by engineer. + + + Subscriptions ignored + Підписки ігноруються + No comment provided by engineer. + Support SimpleX Chat Підтримка чату SimpleX @@ -5442,6 +6414,11 @@ Error: %@ Автентифікація системи No comment provided by engineer. + + TCP connection + TCP-з'єднання + No comment provided by engineer. + TCP connection timeout Тайм-аут TCP-з'єднання @@ -5502,9 +6479,9 @@ Error: %@ Натисніть, щоб сканувати No comment provided by engineer. - - Tap to start a new chat - Натисніть, щоб почати новий чат + + Temporary file error + Тимчасова помилка файлу No comment provided by engineer. @@ -5559,6 +6536,11 @@ It can happen because of some bug or when the connection is compromised.Додаток може сповіщати вас, коли ви отримуєте повідомлення або запити на контакт - будь ласка, відкрийте налаштування, щоб увімкнути цю функцію. No comment provided by engineer. + + The app will ask to confirm downloads from unknown file servers (except .onion). + Програма попросить підтвердити завантаження з невідомих файлових серверів (крім .onion). + No comment provided by engineer. + The attempt to change database passphrase was not completed. Спроба змінити пароль до бази даних не була завершена. @@ -5604,6 +6586,16 @@ It can happen because of some bug or when the connection is compromised.Повідомлення буде позначено як модероване для всіх учасників. No comment provided by engineer. + + The messages will be deleted for all members. + Повідомлення будуть видалені для всіх учасників. + No comment provided by engineer. + + + The messages will be marked as moderated for all members. + Повідомлення будуть позначені як модеровані для всіх учасників. + No comment provided by engineer. + The next generation of private messaging Наступне покоління приватних повідомлень @@ -5639,9 +6631,9 @@ It can happen because of some bug or when the connection is compromised.Текст, який ви вставили, не є посиланням SimpleX. No comment provided by engineer. - - Theme - Тема + + Themes + Теми No comment provided by engineer. @@ -5709,11 +6701,21 @@ It can happen because of some bug or when the connection is compromised.Це ваше власне одноразове посилання! No comment provided by engineer. + + This link was used with another mobile device, please create a new link on the desktop. + Це посилання було використано з іншого мобільного пристрою, будь ласка, створіть нове посилання на робочому столі. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. Це налаштування застосовується до повідомлень у вашому поточному профілі чату **%@**. No comment provided by engineer. + + Title + Заголовок + No comment provided by engineer. + To ask any questions and to receive updates: Задати будь-які питання та отримувати новини: @@ -5744,6 +6746,11 @@ It can happen because of some bug or when the connection is compromised.Для захисту часового поясу у файлах зображень/голосу використовується UTC. No comment provided by engineer. + + To protect your IP address, private routing uses your SMP servers to deliver messages. + Щоб захистити вашу IP-адресу, приватна маршрутизація використовує ваші SMP-сервери для доставки повідомлень. + No comment provided by engineer. + To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5771,16 +6778,36 @@ You will be prompted to complete authentication before this feature is enabled.< Щоб перевірити наскрізне шифрування з вашим контактом, порівняйте (або відскануйте) код на ваших пристроях. No comment provided by engineer. + + Toggle chat list: + Перемикання списку чату: + No comment provided by engineer. + Toggle incognito when connecting. Увімкніть інкогніто при підключенні. No comment provided by engineer. + + Toolbar opacity + Непрозорість панелі інструментів + No comment provided by engineer. + + + Total + Всього + No comment provided by engineer. + Transport isolation Транспортна ізоляція No comment provided by engineer. + + Transport sessions + Транспортні сесії + No comment provided by engineer. + Trying to connect to the server used to receive messages from this contact (error: %@). Спроба з'єднатися з сервером, який використовується для отримання повідомлень від цього контакту (помилка: %@). @@ -5836,11 +6863,6 @@ You will be prompted to complete authentication before this feature is enabled.< Розблокувати учасника? No comment provided by engineer. - - Unexpected error: %@ - Неочікувана помилка: %@ - item status description - Unexpected migration state Неочікуваний стан міграції @@ -5849,7 +6871,7 @@ You will be prompted to complete authentication before this feature is enabled.< Unfav. Нелюб. - No comment provided by engineer. + swipe action Unhide @@ -5886,6 +6908,11 @@ You will be prompted to complete authentication before this feature is enabled.< Невідома помилка No comment provided by engineer. + + Unknown servers! + Невідомі сервери! + No comment provided by engineer. + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. Якщо ви не користуєтеся інтерфейсом виклику iOS, увімкніть режим "Не турбувати", щоб уникнути переривань. @@ -5921,12 +6948,12 @@ To connect, please ask your contact to create another connection link and check Unmute Увімкнути звук - No comment provided by engineer. + swipe action Unread Непрочитане - No comment provided by engineer. + swipe action Up to 100 last messages are sent to new members. @@ -5938,11 +6965,6 @@ To connect, please ask your contact to create another connection link and check Оновлення No comment provided by engineer. - - Update .onion hosts setting? - Оновити налаштування хостів .onion? - No comment provided by engineer. - Update database passphrase Оновити парольну фразу бази даних @@ -5953,9 +6975,9 @@ To connect, please ask your contact to create another connection link and check Оновити налаштування мережі? No comment provided by engineer. - - Update transport isolation mode? - Оновити режим транспортної ізоляції? + + Update settings? + Оновити налаштування? No comment provided by engineer. @@ -5963,16 +6985,16 @@ To connect, please ask your contact to create another connection link and check Оновлення налаштувань призведе до перепідключення клієнта до всіх серверів. No comment provided by engineer. - - Updating this setting will re-connect the client to all servers. - Оновлення цього параметра призведе до перепідключення клієнта до всіх серверів. - No comment provided by engineer. - Upgrade and open chat Оновлення та відкритий чат No comment provided by engineer. + + Upload errors + Помилки завантаження + No comment provided by engineer. + Upload failed Не вдалося завантфжити @@ -5983,6 +7005,16 @@ To connect, please ask your contact to create another connection link and check Завантажити файл server test step + + Uploaded + Завантажено + No comment provided by engineer. + + + Uploaded files + Завантажені файли + No comment provided by engineer. + Uploading archive Завантаження архіву @@ -6033,6 +7065,16 @@ To connect, please ask your contact to create another connection link and check Використовувати лише локальні сповіщення? No comment provided by engineer. + + Use private routing with unknown servers when IP address is not protected. + Використовуйте приватну маршрутизацію з невідомими серверами, якщо IP-адреса не захищена. + No comment provided by engineer. + + + Use private routing with unknown servers. + Використовуйте приватну маршрутизацію з невідомими серверами. + No comment provided by engineer. + Use server Використовувати сервер @@ -6043,14 +7085,19 @@ To connect, please ask your contact to create another connection link and check Використовуйте додаток під час розмови. No comment provided by engineer. + + Use the app with one hand. + Використовуйте додаток однією рукою. + No comment provided by engineer. + User profile Профіль користувача No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - Для використання хостів .onion потрібен сумісний VPN-провайдер. + + User selection + Вибір користувача No comment provided by engineer. @@ -6150,6 +7197,7 @@ To connect, please ask your contact to create another connection link and check Voice messages not allowed + Голосові повідомлення заборонені No comment provided by engineer. @@ -6182,6 +7230,16 @@ To connect, please ask your contact to create another connection link and check Чекаємо на відео No comment provided by engineer. + + Wallpaper accent + Акцент на шпалерах + No comment provided by engineer. + + + Wallpaper background + Фон шпалер + No comment provided by engineer. + Warning: starting chat on multiple devices is not supported and will cause message delivery failures Попередження: запуск чату на декількох пристроях не підтримується і може призвести до збоїв у доставці повідомлень @@ -6224,6 +7282,7 @@ To connect, please ask your contact to create another connection link and check When connecting audio and video calls. + При підключенні аудіо та відеодзвінків. No comment provided by engineer. @@ -6238,14 +7297,17 @@ To connect, please ask your contact to create another connection link and check WiFi + WiFi No comment provided by engineer. Will be enabled in direct chats! + Буде ввімкнено в прямих чатах! No comment provided by engineer. Wired ethernet + Дротова мережа Ethernet No comment provided by engineer. @@ -6263,19 +7325,39 @@ To connect, please ask your contact to create another connection link and check З меншим споживанням заряду акумулятора. No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + Без Tor або VPN ваша IP-адреса буде видимою для файлових серверів. + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + Без Tor або VPN ваша IP-адреса буде видимою для цих XFTP-ретрансляторів: %@. + No comment provided by engineer. + Wrong database passphrase Неправильний пароль до бази даних No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + Неправильний ключ або невідоме з'єднання - швидше за все, це з'єднання видалено. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + Неправильний ключ або невідома адреса фрагмента файлу - найімовірніше, файл видалено. + file error text + Wrong passphrase! Неправильний пароль! No comment provided by engineer. - - XFTP servers - Сервери XFTP + + XFTP server + XFTP-сервер No comment provided by engineer. @@ -6355,11 +7437,21 @@ Repeat join request? Запрошуємо вас до групи No comment provided by engineer. + + You are not connected to these servers. Private routing is used to deliver messages to them. + Не підключені до цих серверів. Для доставлення повідомлень до них використовується приватна маршрутизація. + No comment provided by engineer. + You can accept calls from lock screen, without device and app authentication. Ви можете приймати дзвінки з екрана блокування без автентифікації пристрою та програми. No comment provided by engineer. + + You can change it in Appearance settings. + Ви можете змінити його в налаштуваннях зовнішнього вигляду. + No comment provided by engineer. + You can create it later Ви можете створити його пізніше @@ -6390,11 +7482,16 @@ Repeat join request? Ви можете зробити його видимим для ваших контактів у SimpleX за допомогою налаштувань. No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ Тепер ви можете надсилати повідомлення на адресу %@ notification body + + You can send messages to %@ from Archived contacts. + Ви можете надсилати повідомлення на %@ з архівних контактів. + No comment provided by engineer. + You can set lock screen notification preview via settings. Ви можете налаштувати попередній перегляд сповіщень на екрані блокування за допомогою налаштувань. @@ -6420,6 +7517,11 @@ Repeat join request? Запустити чат можна через Налаштування програми / База даних або перезапустивши програму No comment provided by engineer. + + You can still view conversation with %@ in the list of chats. + Ви все ще можете переглянути розмову з %@ у списку чатів. + No comment provided by engineer. + You can turn on SimpleX Lock via Settings. Увімкнути SimpleX Lock можна в Налаштуваннях. @@ -6462,11 +7564,6 @@ Repeat connection request? Повторити запит на підключення? No comment provided by engineer. - - You have no chats - У вас немає чатів - No comment provided by engineer. - You have to enter passphrase every time the app starts - it is not stored on the device. Вам доведеться вводити парольну фразу щоразу під час запуску програми - вона не зберігається на пристрої. @@ -6487,11 +7584,26 @@ Repeat connection request? Ви приєдналися до цієї групи. Підключення до запрошеного учасника групи. No comment provided by engineer. + + You may migrate the exported database. + Ви можете мігрувати експортовану базу даних. + No comment provided by engineer. + + + You may save the exported archive. + Ви можете зберегти експортований архів. + No comment provided by engineer. + You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. Ви повинні використовувати найновішу версію бази даних чату ТІЛЬКИ на одному пристрої, інакше ви можете перестати отримувати повідомлення від деяких контактів. No comment provided by engineer. + + You need to allow your contact to call to be able to call them. + Щоб мати змогу зателефонувати контакту, вам потрібно дозволити йому зателефонувати. + No comment provided by engineer. + You need to allow your contact to send voice messages to be able to send them. Щоб мати змогу надсилати голосові повідомлення, вам потрібно дозволити контакту надсилати їх. @@ -6607,13 +7719,6 @@ Repeat connection request? Ваші профілі чату No comment provided by engineer. - - Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link). - Для завершення з'єднання ваш контакт має бути онлайн. -Ви можете скасувати це з'єднання і видалити контакт (і спробувати пізніше з новим посиланням). - No comment provided by engineer. - Your contact sent a file that is larger than currently supported maximum size (%@). Ваш контакт надіслав файл, розмір якого перевищує підтримуваний на цей момент максимальний розмір (%@). @@ -6733,6 +7838,7 @@ SimpleX servers cannot see your profile. admins + адміністратори feature role @@ -6747,6 +7853,7 @@ SimpleX servers cannot see your profile. all members + всі учасники feature role @@ -6759,6 +7866,11 @@ SimpleX servers cannot see your profile. та %lld інших подій No comment provided by engineer. + + attempts + спроби + No comment provided by engineer. + audio call (not e2e encrypted) аудіовиклик (без шифрування e2e) @@ -6799,6 +7911,11 @@ SimpleX servers cannot see your profile. жирний No comment provided by engineer. + + call + дзвонити + No comment provided by engineer. + call error помилка дзвінка @@ -6949,6 +8066,11 @@ SimpleX servers cannot see your profile. днів time unit + + decryption errors + помилки розшифровки + No comment provided by engineer. + default (%@) за замовчуванням (%@) @@ -6999,6 +8121,11 @@ SimpleX servers cannot see your profile. дублююче повідомлення integrity error chat item + + duplicates + дублікати + No comment provided by engineer. + e2e encrypted e2e зашифрований @@ -7079,8 +8206,14 @@ SimpleX servers cannot see your profile. відбулася подія No comment provided by engineer. + + expired + закінчився + No comment provided by engineer. + forwarded + переслано No comment provided by engineer. @@ -7108,6 +8241,11 @@ SimpleX servers cannot see your profile. Пароль бази даних буде безпечно збережено в iOS Keychain після запуску чату або зміни пароля - це дасть змогу отримувати миттєві повідомлення. No comment provided by engineer. + + inactive + неактивний + No comment provided by engineer. + incognito via contact address link інкогніто за посиланням на контактну адресу @@ -7148,6 +8286,11 @@ SimpleX servers cannot see your profile. запрошення до групи %@ group name + + invite + запросити + No comment provided by engineer. + invited запрошені @@ -7203,6 +8346,11 @@ SimpleX servers cannot see your profile. з'єднаний rcv group event chat item + + message + повідомлення + No comment provided by engineer. + message received повідомлення отримано @@ -7233,6 +8381,11 @@ SimpleX servers cannot see your profile. місяців time unit + + mute + приглушити + No comment provided by engineer. + never ніколи @@ -7285,6 +8438,16 @@ SimpleX servers cannot see your profile. увімкненo group pref value + + other + інший + No comment provided by engineer. + + + other errors + інші помилки + No comment provided by engineer. + owner власник @@ -7292,6 +8455,7 @@ SimpleX servers cannot see your profile. owners + власники feature role @@ -7346,10 +8510,17 @@ SimpleX servers cannot see your profile. saved + збережено No comment provided by engineer. saved from %@ + збережено з %@ + No comment provided by engineer. + + + search + пошук No comment provided by engineer. @@ -7377,6 +8548,15 @@ SimpleX servers cannot see your profile. надіслати пряме повідомлення No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + інформація про чергу на сервері: %1$@ + +останнє отримане повідомлення: %2$@ + queue info + set new contact address встановити нову контактну адресу @@ -7417,11 +8597,26 @@ SimpleX servers cannot see your profile. невідомий connection info + + unknown servers + невідомі реле + No comment provided by engineer. + unknown status невідомий статус No comment provided by engineer. + + unmute + увімкнути звук + No comment provided by engineer. + + + unprotected + незахищені + No comment provided by engineer. + updated group profile оновлений профіль групи @@ -7462,6 +8657,11 @@ SimpleX servers cannot see your profile. за допомогою ретранслятора No comment provided by engineer. + + video + відео + No comment provided by engineer. + video call (not e2e encrypted) відеодзвінок (без шифрування e2e) @@ -7487,6 +8687,11 @@ SimpleX servers cannot see your profile. тижнів time unit + + when IP hidden + коли IP приховано + No comment provided by engineer. + yes так @@ -7494,6 +8699,7 @@ SimpleX servers cannot see your profile. you + ти No comment provided by engineer. @@ -7570,7 +8776,7 @@ SimpleX servers cannot see your profile.
- +
@@ -7607,7 +8813,7 @@ SimpleX servers cannot see your profile.
- +
@@ -7627,4 +8833,218 @@ SimpleX servers cannot see your profile.
+ +
+ +
+ + + SimpleX SE + SimpleX SE + Bundle display name + + + SimpleX SE + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright © 2024 SimpleX Chat. Всі права захищені. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + %@ + No comment provided by engineer. + + + App is locked! + Додаток заблоковано! + No comment provided by engineer. + + + Cancel + Скасувати + No comment provided by engineer. + + + Cannot access keychain to save database password + Не вдається отримати доступ до зв'язки ключів для збереження пароля до бази даних + No comment provided by engineer. + + + Cannot forward message + Неможливо переслати повідомлення + No comment provided by engineer. + + + Comment + Коментар + No comment provided by engineer. + + + Currently maximum supported file size is %@. + Наразі максимальний підтримуваний розмір файлу - %@. + No comment provided by engineer. + + + Database downgrade required + Потрібне оновлення бази даних + No comment provided by engineer. + + + Database encrypted! + База даних зашифрована! + No comment provided by engineer. + + + Database error + Помилка в базі даних + No comment provided by engineer. + + + Database passphrase is different from saved in the keychain. + Парольна фраза бази даних відрізняється від збереженої у в’язці ключів. + No comment provided by engineer. + + + Database passphrase is required to open chat. + Для відкриття чату потрібно ввести пароль до бази даних. + No comment provided by engineer. + + + Database upgrade required + Потрібне оновлення бази даних + No comment provided by engineer. + + + Error preparing file + Помилка підготовки файлу + No comment provided by engineer. + + + Error preparing message + Повідомлення про підготовку до помилки + No comment provided by engineer. + + + Error: %@ + Помилка: %@ + No comment provided by engineer. + + + File error + Помилка файлу + No comment provided by engineer. + + + Incompatible database version + Несумісна версія бази даних + No comment provided by engineer. + + + Invalid migration confirmation + Недійсне підтвердження міграції + No comment provided by engineer. + + + Keychain error + Помилка зв'язки ключів + No comment provided by engineer. + + + Large file! + Великий файл! + No comment provided by engineer. + + + No active profile + Немає активного профілю + No comment provided by engineer. + + + Ok + Гаразд + No comment provided by engineer. + + + Open the app to downgrade the database. + Відкрийте програму, щоб знизити версію бази даних. + No comment provided by engineer. + + + Open the app to upgrade the database. + Відкрийте програму, щоб оновити базу даних. + No comment provided by engineer. + + + Passphrase + Парольна фраза + No comment provided by engineer. + + + Please create a profile in the SimpleX app + Будь ласка, створіть профіль у додатку SimpleX + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + Вибрані налаштування чату забороняють це повідомлення. + No comment provided by engineer. + + + Sending a message takes longer than expected. + Надсилання повідомлення займає більше часу, ніж очікувалося. + No comment provided by engineer. + + + Sending message… + Надсилаю повідомлення… + No comment provided by engineer. + + + Share + Поділіться + No comment provided by engineer. + + + Slow network? + Повільна мережа? + No comment provided by engineer. + + + Unknown database error: %@ + Невідома помилка бази даних: %@ + No comment provided by engineer. + + + Unsupported format + Непідтримуваний формат + No comment provided by engineer. + + + Wait + Зачекай + No comment provided by engineer. + + + Wrong database passphrase + Неправильна ключова фраза до бази даних + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + Ви можете дозволити спільний доступ у налаштуваннях Конфіденційність і безпека / SimpleX Lock. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/uk.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/uk.xcloc/contents.json b/apps/ios/SimpleX Localizations/uk.xcloc/contents.json index 6c122f11ab..38238e7802 100644 --- a/apps/ios/SimpleX Localizations/uk.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/uk.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "uk", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "15F31d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "15.4" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index 4bf9e05665..8c3641549d 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -2,7 +2,7 @@
- +
@@ -126,11 +126,6 @@ %@ 已认证 No comment provided by engineer. - - %@ servers - %@ 服务器 - No comment provided by engineer. - %@ uploaded No comment provided by engineer. @@ -545,16 +540,16 @@ 关于 SimpleX 地址 No comment provided by engineer. - - Accent color - 色调 + + Accent No comment provided by engineer. Accept 接受 accept contact request via notification - accept incoming call via notification + accept incoming call via notification + swipe action Accept connection request? @@ -569,7 +564,20 @@ Accept incognito 接受隐身聊天 - accept contact request via notification + accept contact request via notification + swipe action + + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + No comment provided by engineer. + + + Active connections + No comment provided by engineer. Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. @@ -591,16 +599,16 @@ 添加个人资料 No comment provided by engineer. + + Add server + 添加服务器 + No comment provided by engineer. + Add servers by scanning QR codes. 扫描二维码来添加服务器。 No comment provided by engineer. - - Add server… - 添加服务器… - No comment provided by engineer. - Add to another device 添加另一设备 @@ -611,6 +619,18 @@ 添加欢迎信息 No comment provided by engineer. + + Additional accent + No comment provided by engineer. + + + Additional accent 2 + No comment provided by engineer. + + + Additional secondary + No comment provided by engineer. + Address 地址 @@ -623,6 +643,7 @@ Admins can block a member for all. + 管理员可以为所有人封禁一名成员。 No comment provided by engineer. @@ -635,6 +656,10 @@ 高级网络设置 No comment provided by engineer. + + Advanced settings + No comment provided by engineer. + All app data is deleted. 已删除所有应用程序数据。 @@ -650,6 +675,10 @@ 所有数据在输入后将被删除。 No comment provided by engineer. + + All data is private to your device. + No comment provided by engineer. + All group members will remain connected. 所有群组成员将保持连接。 @@ -669,6 +698,10 @@ All new messages from %@ will be hidden! No comment provided by engineer. + + All profiles + No comment provided by engineer. + All your contacts will remain connected. 所有联系人会保持连接。 @@ -681,6 +714,7 @@ All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays. + 你的所有联系人、对话和文件将被安全加密并分块上传到配置的 XFTP 中继。 No comment provided by engineer. @@ -693,11 +727,19 @@ 仅当您的联系人允许时才允许呼叫。 No comment provided by engineer. + + Allow calls? + No comment provided by engineer. + Allow disappearing messages only if your contact allows it to you. 仅当您的联系人允许时才允许限时消息。 No comment provided by engineer. + + Allow downgrade + No comment provided by engineer. + Allow irreversible message deletion only if your contact allows it to you. (24 hours) 仅有您的联系人许可后才允许不可撤回消息移除。 @@ -723,6 +765,10 @@ 允许发送限时消息。 No comment provided by engineer. + + Allow sharing + No comment provided by engineer. + Allow to irreversibly delete sent messages. (24 hours) 允许不可撤回地删除已发送消息。 @@ -730,6 +776,7 @@ Allow to send SimpleX links. + 允许发送 SimpleX 链接。 No comment provided by engineer. @@ -792,6 +839,10 @@ 已经加入了该群组! No comment provided by engineer. + + Always use private routing. + No comment provided by engineer. + Always use relay 一直使用中继 @@ -814,6 +865,7 @@ App data migration + 应用数据迁移 No comment provided by engineer. @@ -853,14 +905,29 @@ Apply + 应用 + No comment provided by engineer. + + + Apply to No comment provided by engineer. Archive and upload + 存档和上传 + No comment provided by engineer. + + + Archive contacts to chat later. + No comment provided by engineer. + + + Archived contacts No comment provided by engineer. Archiving database + 正在存档数据库 No comment provided by engineer. @@ -928,6 +995,10 @@ 返回 No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address 糟糕的桌面地址 @@ -953,6 +1024,14 @@ 更好的消息 No comment provided by engineer. + + Better networking + No comment provided by engineer. + + + Black + No comment provided by engineer. + Block 封禁 @@ -988,6 +1067,14 @@ 由管理员封禁 No comment provided by engineer. + + Blur for better privacy. + No comment provided by engineer. + + + Blur media + No comment provided by engineer. + Both you and your contact can add message reactions. 您和您的联系人都可以添加消息回应。 @@ -1033,11 +1120,23 @@ 通话 No comment provided by engineer. + + Calls prohibited! + No comment provided by engineer. + Camera not available 相机不可用 No comment provided by engineer. + + Can't call contact + No comment provided by engineer. + + + Can't call member + No comment provided by engineer. + Can't invite contact! 无法邀请联系人! @@ -1048,6 +1147,10 @@ 无法邀请联系人! No comment provided by engineer. + + Can't message member + No comment provided by engineer. + Cancel 取消 @@ -1055,6 +1158,7 @@ Cancel migration + 取消迁移 No comment provided by engineer. @@ -1062,13 +1166,22 @@ 无法访问钥匙串以保存数据库密码 No comment provided by engineer. + + Cannot forward message + No comment provided by engineer. + Cannot receive file 无法接收文件 No comment provided by engineer. + + Capacity exceeded - recipient did not receive previously sent messages. + snd error text + Cellular + 移动网络 No comment provided by engineer. @@ -1127,6 +1240,10 @@ 聊天档案 No comment provided by engineer. + + Chat colors + No comment provided by engineer. + Chat console 聊天控制台 @@ -1142,6 +1259,10 @@ 聊天数据库已删除 No comment provided by engineer. + + Chat database exported + No comment provided by engineer. + Chat database imported 聊天数据库已导入 @@ -1162,8 +1283,13 @@ 聊天已停止。如果你已经在另一台设备商使用过此数据库,你应该在启动聊天前将数据库传输回来。 No comment provided by engineer. + + Chat list + No comment provided by engineer. + Chat migrated! + 已迁移聊天! No comment provided by engineer. @@ -1171,6 +1297,10 @@ 聊天偏好设置 No comment provided by engineer. + + Chat theme + No comment provided by engineer. + Chats 聊天 @@ -1200,10 +1330,22 @@ 从库中选择 No comment provided by engineer. + + Chunks deleted + No comment provided by engineer. + + + Chunks downloaded + No comment provided by engineer. + + + Chunks uploaded + No comment provided by engineer. + Clear 清除 - No comment provided by engineer. + swipe action Clear conversation @@ -1225,9 +1367,12 @@ 清除验证 No comment provided by engineer. - - Colors - 颜色 + + Color chats with the new themes. + No comment provided by engineer. + + + Color mode No comment provided by engineer. @@ -1240,11 +1385,19 @@ 与您的联系人比较安全码。 No comment provided by engineer. + + Completed + No comment provided by engineer. + Configure ICE servers 配置 ICE 服务器 No comment provided by engineer. + + Configured %@ servers + No comment provided by engineer. + Confirm 确认 @@ -1255,13 +1408,22 @@ 确认密码 No comment provided by engineer. + + Confirm contact deletion? + No comment provided by engineer. + Confirm database upgrades 确认数据库升级 No comment provided by engineer. + + Confirm files from unknown servers. + No comment provided by engineer. + Confirm network settings + 确认网络设置 No comment provided by engineer. @@ -1276,10 +1438,12 @@ Confirm that you remember database passphrase to migrate it. + 请在迁移前确认你记得数据库的密码短语。 No comment provided by engineer. Confirm upload + 确认上传 No comment provided by engineer. @@ -1302,6 +1466,10 @@ 连接到桌面 No comment provided by engineer. + + Connect to your friends faster. + No comment provided by engineer. + Connect to yourself? 连接到你自己? @@ -1335,16 +1503,28 @@ This is your own one-time link! Connect with %@ No comment provided by engineer. + + Connected + No comment provided by engineer. + Connected desktop 已连接的桌面 No comment provided by engineer. + + Connected servers + No comment provided by engineer. + Connected to desktop 已连接到桌面 No comment provided by engineer. + + Connecting + No comment provided by engineer. + Connecting to server… 连接服务器中…… @@ -1355,6 +1535,10 @@ This is your own one-time link! 连接服务器中……(错误:%@) No comment provided by engineer. + + Connecting to contact, please wait or check later! + No comment provided by engineer. + Connecting to desktop 正连接到桌面 @@ -1365,6 +1549,10 @@ This is your own one-time link! 连接 No comment provided by engineer. + + Connection and servers status. + No comment provided by engineer. + Connection error 连接错误 @@ -1375,6 +1563,10 @@ This is your own one-time link! 连接错误(AUTH) No comment provided by engineer. + + Connection notifications + No comment provided by engineer. + Connection request sent! 已发送连接请求! @@ -1390,6 +1582,14 @@ This is your own one-time link! 连接超时 No comment provided by engineer. + + Connection with desktop stopped + No comment provided by engineer. + + + Connections + No comment provided by engineer. + Contact allows 联系人允许 @@ -1400,6 +1600,10 @@ This is your own one-time link! 联系人已存在 No comment provided by engineer. + + Contact deleted! + No comment provided by engineer. + Contact hidden: 联系人已隐藏: @@ -1410,9 +1614,8 @@ This is your own one-time link! 联系已连接 notification - - Contact is not connected yet! - 联系人尚未连接! + + Contact is deleted. No comment provided by engineer. @@ -1425,6 +1628,10 @@ This is your own one-time link! 联系人偏好设置 No comment provided by engineer. + + Contact will be deleted - this cannot be undone! + No comment provided by engineer. + Contacts 联系人 @@ -1440,10 +1647,18 @@ This is your own one-time link! 继续 No comment provided by engineer. + + Conversation deleted! + No comment provided by engineer. + Copy 复制 - chat item action + No comment provided by engineer. + + + Copy error + No comment provided by engineer. Core version: v%@ @@ -1519,6 +1734,10 @@ This is your own one-time link! 创建您的资料 No comment provided by engineer. + + Created + No comment provided by engineer. + Created at 创建于 @@ -1535,6 +1754,7 @@ This is your own one-time link! Creating archive link + 正在创建存档链接 No comment provided by engineer. @@ -1552,6 +1772,10 @@ This is your own one-time link! 现有密码…… No comment provided by engineer. + + Current profile + No comment provided by engineer. + Currently maximum supported file size is %@. 目前支持的最大文件大小为 %@。 @@ -1562,11 +1786,19 @@ This is your own one-time link! 自定义时间 No comment provided by engineer. + + Customize theme + No comment provided by engineer. + Dark 深色 No comment provided by engineer. + + Dark mode colors + No comment provided by engineer. + Database ID 数据库 ID @@ -1665,6 +1897,10 @@ This is your own one-time link! 应用程序重新启动时将迁移数据库 No comment provided by engineer. + + Debug delivery + No comment provided by engineer. + Decentralized 分散式 @@ -1678,17 +1914,17 @@ This is your own one-time link! Delete 删除 - chat item action + chat item action + swipe action + + + Delete %lld messages of members? + No comment provided by engineer. Delete %lld messages? No comment provided by engineer. - - Delete Contact - 删除联系人 - No comment provided by engineer. - Delete address 删除地址 @@ -1744,9 +1980,8 @@ This is your own one-time link! 删除联系人 No comment provided by engineer. - - Delete contact? -This cannot be undone! + + Delete contact? No comment provided by engineer. @@ -1756,6 +1991,7 @@ This cannot be undone! Delete database from this device + 从这部设备上删除数据库 No comment provided by engineer. @@ -1838,11 +2074,6 @@ This cannot be undone! 删除旧数据库吗? No comment provided by engineer. - - Delete pending connection - 删除挂起连接 - No comment provided by engineer. - Delete pending connection? 删除待定连接? @@ -1858,11 +2089,23 @@ This cannot be undone! 删除队列 server test step + + Delete up to 20 messages at once. + No comment provided by engineer. + Delete user profile? 删除用户资料? No comment provided by engineer. + + Delete without notification + No comment provided by engineer. + + + Deleted + No comment provided by engineer. + Deleted at 已删除于 @@ -1873,6 +2116,10 @@ This cannot be undone! 已删除于:%@ copied message info + + Deletion errors + No comment provided by engineer. + Delivery 传送 @@ -1907,11 +2154,35 @@ This cannot be undone! 桌面设备 No comment provided by engineer. + + Destination server address of %@ is incompatible with forwarding server %@ settings. + No comment provided by engineer. + + + Destination server error: %@ + snd error text + + + Destination server version of %@ is incompatible with forwarding server %@. + No comment provided by engineer. + + + Detailed statistics + No comment provided by engineer. + + + Details + No comment provided by engineer. + Develop 开发 No comment provided by engineer. + + Developer options + No comment provided by engineer. + Developer tools 开发者工具 @@ -1962,6 +2233,10 @@ This cannot be undone! 全部禁用 No comment provided by engineer. + + Disabled + No comment provided by engineer. + Disappearing message 限时消息 @@ -2012,11 +2287,19 @@ This cannot be undone! 通过本地网络发现 No comment provided by engineer. + + Do NOT send messages directly, even if your or destination server does not support private routing. + No comment provided by engineer. + Do NOT use SimpleX for emergency calls. 请勿使用 SimpleX 进行紧急通话。 No comment provided by engineer. + + Do NOT use private routing. + No comment provided by engineer. + Do it later 稍后再做 @@ -2049,10 +2332,16 @@ This cannot be undone! Download + 下载 chat item action + + Download errors + No comment provided by engineer. + Download failed + 下载失败了 No comment provided by engineer. @@ -2060,12 +2349,22 @@ This cannot be undone! 下载文件 server test step + + Downloaded + No comment provided by engineer. + + + Downloaded files + No comment provided by engineer. + Downloading archive + 正在下载存档 No comment provided by engineer. Downloading link details + 正在下载链接详情 No comment provided by engineer. @@ -2125,6 +2424,7 @@ This cannot be undone! Enable in direct chats (BETA)! + 在私聊中开启(公测)! No comment provided by engineer. @@ -2157,8 +2457,13 @@ This cannot be undone! 启用自毁密码 set passcode view + + Enabled + No comment provided by engineer. + Enabled for + 启用对象 No comment provided by engineer. @@ -2246,6 +2551,7 @@ This cannot be undone! Enter passphrase + 输入密码短语 No comment provided by engineer. @@ -2322,6 +2628,10 @@ This cannot be undone! 更改设置错误 No comment provided by engineer. + + Error connecting to forwarding server %@. Please try later. + No comment provided by engineer. + Error creating address 创建地址错误 @@ -2372,11 +2682,6 @@ This cannot be undone! 删除连接错误 No comment provided by engineer. - - Error deleting contact - 删除联系人错误 - No comment provided by engineer. - Error deleting database 删除数据库错误 @@ -2399,6 +2704,7 @@ This cannot be undone! Error downloading the archive + 下载存档出错 No comment provided by engineer. @@ -2421,6 +2727,10 @@ This cannot be undone! 导出聊天数据库错误 No comment provided by engineer. + + Error exporting theme: %@ + No comment provided by engineer. + Error importing chat database 导入聊天数据库错误 @@ -2445,11 +2755,23 @@ This cannot be undone! 接收文件错误 No comment provided by engineer. + + Error reconnecting server + No comment provided by engineer. + + + Error reconnecting servers + No comment provided by engineer. + Error removing member 删除成员错误 No comment provided by engineer. + + Error resetting statistics + No comment provided by engineer. + Error saving %@ servers 保存 %@ 服务器错误 @@ -2477,6 +2799,7 @@ This cannot be undone! Error saving settings + 保存设置出错 when migrating @@ -2550,10 +2873,12 @@ This cannot be undone! Error uploading the archive + 上传存档出错 No comment provided by engineer. Error verifying passphrase: + 验证密码短语出错: No comment provided by engineer. @@ -2564,7 +2889,8 @@ This cannot be undone! Error: %@ 错误: %@ - No comment provided by engineer. + file error text + snd error text Error: URL is invalid @@ -2576,6 +2902,10 @@ This cannot be undone! 错误:没有数据库文件 No comment provided by engineer. + + Errors + No comment provided by engineer. + Even when disabled in the conversation. 即使在对话中被禁用。 @@ -2601,6 +2931,10 @@ This cannot be undone! 导出错误: No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. 导出数据库归档。 @@ -2608,6 +2942,7 @@ This cannot be undone! Exported file doesn't exist + 导出的文件不存在 No comment provided by engineer. @@ -2633,8 +2968,28 @@ This cannot be undone! Favorite 最喜欢 + swipe action + + + File error No comment provided by engineer. + + File not found - most likely file was deleted or cancelled. + file error text + + + File server error: %@ + file error text + + + File status + No comment provided by engineer. + + + File status: %@ + copied message info + File will be deleted from servers. 文件将从服务器中删除。 @@ -2655,6 +3010,10 @@ This cannot be undone! 文件:%@ No comment provided by engineer. + + Files + No comment provided by engineer. + Files & media 文件和媒体 @@ -2672,6 +3031,7 @@ This cannot be undone! Files and media not allowed + 不允许文件和媒体 No comment provided by engineer. @@ -2686,10 +3046,12 @@ This cannot be undone! Finalize migration + 完成迁移 No comment provided by engineer. Finalize migration on another device. + 在另一部设备上完成迁移 No comment provided by engineer. @@ -2739,20 +3101,46 @@ This cannot be undone! Forward + 转发 chat item action Forward and save messages + 转发并保存消息 No comment provided by engineer. Forwarded + 已转发 No comment provided by engineer. Forwarded from + 转发自 No comment provided by engineer. + + Forwarding server %@ failed to connect to destination server %@. Please try later. + No comment provided by engineer. + + + Forwarding server address is incompatible with network settings: %@. + No comment provided by engineer. + + + Forwarding server version is incompatible with network settings: %@. + No comment provided by engineer. + + + Forwarding server: %1$@ +Destination server error: %2$@ + snd error text + + + Forwarding server: %1$@ +Error: %2$@ + snd error text + Found desktop 找到了桌面 @@ -2798,6 +3186,14 @@ This cannot be undone! GIF 和贴纸 No comment provided by engineer. + + Good afternoon! + message preview + + + Good morning! + message preview + Group 群组 @@ -2864,6 +3260,7 @@ This cannot be undone! Group members can send SimpleX links. + 群成员可发送 SimpleX 链接。 No comment provided by engineer. @@ -3072,10 +3469,16 @@ This cannot be undone! Import failed + 导入失败了 + No comment provided by engineer. + + + Import theme No comment provided by engineer. Importing archive + 正在导入存档 No comment provided by engineer. @@ -3095,6 +3498,7 @@ This cannot be undone! In order to continue, chat should be stopped. + 必须停止聊天才能继续。 No comment provided by engineer. @@ -3104,6 +3508,7 @@ This cannot be undone! In-call sounds + 通话声音 No comment provided by engineer. @@ -3193,6 +3598,10 @@ This cannot be undone! 界面 No comment provided by engineer. + + Interface colors + No comment provided by engineer. + Invalid QR code 无效的二维码 @@ -3210,10 +3619,12 @@ This cannot be undone! Invalid link + 无效链接 No comment provided by engineer. Invalid migration confirmation + 迁移确认无效 No comment provided by engineer. @@ -3291,6 +3702,10 @@ This cannot be undone! 3.连接被破坏。 No comment provided by engineer. + + It protects your IP address and connections. + No comment provided by engineer. + It seems like you are already connected via this link. If it is not the case, there was an error (%@). 您似乎已经通过此链接连接。如果不是这样,则有一个错误 (%@)。 @@ -3309,7 +3724,7 @@ This cannot be undone! Join 加入 - No comment provided by engineer. + swipe action Join group @@ -3350,6 +3765,10 @@ This is your link for group %@! 保留 No comment provided by engineer. + + Keep conversation + No comment provided by engineer. + Keep the app open to use it from desktop No comment provided by engineer. @@ -3392,7 +3811,7 @@ This is your link for group %@! Leave 离开 - No comment provided by engineer. + swipe action Leave group @@ -3524,11 +3943,23 @@ This is your link for group %@! 最长30秒,立即接收。 No comment provided by engineer. + + Media & file servers + No comment provided by engineer. + + + Medium + blur media + Member 成员 No comment provided by engineer. + + Member inactive + item status text + Member role will be changed to "%@". All group members will be notified. 成员角色将更改为 "%@"。所有群成员将收到通知。 @@ -3544,6 +3975,10 @@ This is your link for group %@! 成员将被移出群组——此操作无法撤消! No comment provided by engineer. + + Menus + No comment provided by engineer. + Message delivery error 消息传递错误 @@ -3554,11 +3989,27 @@ This is your link for group %@! 消息送达回执! No comment provided by engineer. + + Message delivery warning + item status text + Message draft 消息草稿 No comment provided by engineer. + + Message forwarded + item status text + + + Message may be delivered later if member becomes active. + item status description + + + Message queue info + No comment provided by engineer. + Message reactions 消息回应 @@ -3574,10 +4025,27 @@ This is your link for group %@! 该群组禁用了消息回应。 No comment provided by engineer. + + Message reception + No comment provided by engineer. + + + Message servers + No comment provided by engineer. + Message source remains private. + 消息来源保持私密。 No comment provided by engineer. + + Message status + No comment provided by engineer. + + + Message status: %@ + copied message info + Message text 消息正文 @@ -3585,6 +4053,7 @@ This is your link for group %@! Message too large + 消息太大了 No comment provided by engineer. @@ -3601,6 +4070,14 @@ This is your link for group %@! Messages from %@ will be shown! No comment provided by engineer. + + Messages received + No comment provided by engineer. + + + Messages sent + No comment provided by engineer. + Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery. No comment provided by engineer. @@ -3611,26 +4088,32 @@ This is your link for group %@! Migrate device + 迁移设备 No comment provided by engineer. Migrate from another device + 从另一台设备迁移 No comment provided by engineer. Migrate here + 迁移到此处 No comment provided by engineer. Migrate to another device + 迁移到另一部设备 No comment provided by engineer. Migrate to another device via QR code. + 通过二维码迁移到另一部设备。 No comment provided by engineer. Migrating + 迁移中 No comment provided by engineer. @@ -3640,6 +4123,7 @@ This is your link for group %@! Migration complete + 迁移完毕 No comment provided by engineer. @@ -3684,6 +4168,7 @@ This is your link for group %@! More reliable network connection. + 更可靠的网络连接。 No comment provided by engineer. @@ -3691,11 +4176,6 @@ This is your link for group %@! 此连接很可能已被删除。 item status description - - Most likely this contact has deleted the connection with you. - 很可能此联系人已经删除了与您的联系。 - No comment provided by engineer. - Multiple chat profiles 多个聊天资料 @@ -3704,7 +4184,7 @@ This is your link for group %@! Mute 静音 - No comment provided by engineer. + swipe action Muted when inactive! @@ -3714,7 +4194,7 @@ This is your link for group %@! Name 名称 - No comment provided by engineer. + swipe action Network & servers @@ -3723,10 +4203,16 @@ This is your link for group %@! Network connection + 网络连接 No comment provided by engineer. + + Network issues - message expired after many attempts to send it. + snd error text + Network management + 网络管理 No comment provided by engineer. @@ -3749,6 +4235,10 @@ This is your link for group %@! 新聊天 No comment provided by engineer. + + New chat experience 🎉 + No comment provided by engineer. + New contact request 新联系人请求 @@ -3779,6 +4269,10 @@ This is your link for group %@! %@ 的新内容 No comment provided by engineer. + + New media options + No comment provided by engineer. + New member role 新成员角色 @@ -3824,6 +4318,10 @@ This is your link for group %@! 无设备令牌! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + item status description + No filtered chats 无过滤聊天 @@ -3839,8 +4337,13 @@ This is your link for group %@! 无历史记录 No comment provided by engineer. + + No info, try to reload + No comment provided by engineer. + No network connection + 无网络连接 No comment provided by engineer. @@ -3858,6 +4361,10 @@ This is your link for group %@! 不兼容! No comment provided by engineer. + + Nothing selected + No comment provided by engineer. + Notifications 通知 @@ -3885,7 +4392,7 @@ This is your link for group %@! Off 关闭 - No comment provided by engineer. + blur media Ok @@ -3907,13 +4414,15 @@ This is your link for group %@! 一次性邀请链接 No comment provided by engineer. - - Onion hosts will be required for connection. Requires enabling VPN. + + Onion hosts will be **required** for connection. +Requires compatible VPN. Onion 主机将用于连接。需要启用 VPN。 No comment provided by engineer. - - Onion hosts will be used when available. Requires enabling VPN. + + Onion hosts will be used when available. +Requires compatible VPN. 当可用时,将使用 Onion 主机。需要启用 VPN。 No comment provided by engineer. @@ -3927,6 +4436,10 @@ This is your link for group %@! 只有客户端设备存储用户资料、联系人、群组和**双层端到端加密**发送的消息。 No comment provided by engineer. + + Only delete conversation + No comment provided by engineer. + Only group owners can change group preferences. 只有群主可以改变群组偏好设置。 @@ -4021,6 +4534,10 @@ This is your link for group %@! Open migration to another device authentication reason + + Open server settings + No comment provided by engineer. + Open user profiles 打开用户个人资料 @@ -4037,6 +4554,7 @@ This is your link for group %@! Or paste archive link + 或粘贴存档链接 No comment provided by engineer. @@ -4046,6 +4564,7 @@ This is your link for group %@! Or securely share this file link + 或安全地分享此文件链接 No comment provided by engineer. @@ -4055,6 +4574,11 @@ This is your link for group %@! Other + 其他 + No comment provided by engineer. + + + Other %@ servers No comment provided by engineer. @@ -4121,6 +4645,10 @@ This is your link for group %@! 粘贴您收到的链接 No comment provided by engineer. + + Pending + No comment provided by engineer. + People can connect to you only via the links you share. 人们只能通过您共享的链接与您建立联系。 @@ -4138,6 +4666,15 @@ This is your link for group %@! Picture-in-picture calls + 画中画通话 + No comment provided by engineer. + + + Play from the chat list. + No comment provided by engineer. + + + Please ask your contact to enable calls. No comment provided by engineer. @@ -4145,6 +4682,11 @@ This is your link for group %@! 请让您的联系人启用发送语音消息。 No comment provided by engineer. + + Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection. +Please share any other issues with the developers. + No comment provided by engineer. + Please check that you used the correct link or ask your contact to send you another one. 请检查您使用的链接是否正确,或者让您的联系人给您发送另一个链接。 @@ -4162,6 +4704,7 @@ This is your link for group %@! Please confirm that network settings are correct for this device. + 请确认网络设置对此这台设备正确无误。 No comment provided by engineer. @@ -4239,6 +4782,10 @@ Error: %@ 预览 No comment provided by engineer. + + Previously connected servers + No comment provided by engineer. + Privacy & security 隐私和安全 @@ -4254,11 +4801,27 @@ Error: %@ 私密文件名 No comment provided by engineer. + + Private message routing + No comment provided by engineer. + + + Private message routing 🚀 + No comment provided by engineer. + Private notes 私密笔记 name of notes to self + + Private routing + No comment provided by engineer. + + + Private routing error + No comment provided by engineer. + Profile and server connections 资料和服务器连接 @@ -4271,6 +4834,7 @@ Error: %@ Profile images + 个人资料图 No comment provided by engineer. @@ -4287,6 +4851,10 @@ Error: %@ 个人资料密码 No comment provided by engineer. + + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. 个人资料更新将被发送给您的联系人。 @@ -4336,11 +4904,20 @@ Error: %@ 禁止发送语音消息。 No comment provided by engineer. + + Protect IP address + No comment provided by engineer. + Protect app screen 保护应用程序屏幕 No comment provided by engineer. + + Protect your IP address from the messaging relays chosen by your contacts. +Enable in *Network & servers* settings. + No comment provided by engineer. + Protect your chat profiles with a password! 使用密码保护您的聊天资料! @@ -4356,6 +4933,14 @@ Error: %@ 每 KB 协议超时 No comment provided by engineer. + + Proxied + No comment provided by engineer. + + + Proxied servers + No comment provided by engineer. + Push notifications 推送通知 @@ -4367,6 +4952,7 @@ Error: %@ Quantum resistant encryption + 抗量子加密 No comment provided by engineer. @@ -4374,6 +4960,10 @@ Error: %@ 评价此应用程序 No comment provided by engineer. + + Reachable chat toolbar + No comment provided by engineer. + React… 回应… @@ -4382,7 +4972,7 @@ Error: %@ Read 已读 - No comment provided by engineer. + swipe action Read more @@ -4419,6 +5009,10 @@ Error: %@ 回执已禁用 No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at 已收到于 @@ -4439,15 +5033,23 @@ Error: %@ 收到的信息 message info title + + Received messages + No comment provided by engineer. + + + Received reply + No comment provided by engineer. + + + Received total + No comment provided by engineer. + Receiving address will be changed to a different server. Address change will complete after sender comes online. 接收地址将变更到不同的服务器。地址更改将在发件人上线后完成。 No comment provided by engineer. - - Receiving concurrency - No comment provided by engineer. - Receiving file will be stopped. 即将停止接收文件。 @@ -4464,6 +5066,7 @@ Error: %@ Recipient(s) can't see who this message is from. + 收件人看不到这条消息来自何人。 No comment provided by engineer. @@ -4471,11 +5074,31 @@ Error: %@ 对方会在您键入时看到更新。 No comment provided by engineer. + + Reconnect + No comment provided by engineer. + Reconnect all connected servers to force message delivery. It uses additional traffic. 重新连接所有已连接的服务器以强制发送信息。这会耗费更多流量。 No comment provided by engineer. + + Reconnect all servers + No comment provided by engineer. + + + Reconnect all servers? + No comment provided by engineer. + + + Reconnect server to force message delivery. It uses additional traffic. + No comment provided by engineer. + + + Reconnect server? + No comment provided by engineer. + Reconnect servers? 是否重新连接服务器? @@ -4499,7 +5122,8 @@ Error: %@ Reject 拒绝 - reject incoming call via notification + reject incoming call via notification + swipe action Reject (sender NOT notified) @@ -4526,6 +5150,10 @@ Error: %@ 移除 No comment provided by engineer. + + Remove image + No comment provided by engineer. + Remove member 删除成员 @@ -4563,10 +5191,12 @@ Error: %@ Repeat download + 重复下载 No comment provided by engineer. Repeat import + 重复导入 No comment provided by engineer. @@ -4576,6 +5206,7 @@ Error: %@ Repeat upload + 重复上传 No comment provided by engineer. @@ -4593,16 +5224,36 @@ Error: %@ 重置 No comment provided by engineer. + + Reset all hints + No comment provided by engineer. + + + Reset all statistics + No comment provided by engineer. + + + Reset all statistics? + No comment provided by engineer. + Reset colors 重置颜色 No comment provided by engineer. + + Reset to app theme + No comment provided by engineer. + Reset to defaults 重置为默认 No comment provided by engineer. + + Reset to user theme + No comment provided by engineer. + Restart the app to create a new chat profile 重新启动应用程序以创建新的聊天资料 @@ -4643,11 +5294,6 @@ Error: %@ 揭示 chat item action - - Revert - 恢复 - No comment provided by engineer. - Revoke 撤销 @@ -4673,13 +5319,17 @@ Error: %@ 运行聊天程序 No comment provided by engineer. - - SMP servers - SMP 服务器 + + SMP server + No comment provided by engineer. + + + Safely receive files No comment provided by engineer. Safer groups + 更安全的群组 No comment provided by engineer. @@ -4702,6 +5352,10 @@ Error: %@ 保存并通知群组成员 No comment provided by engineer. + + Save and reconnect + No comment provided by engineer. + Save and update group profile 保存和更新组配置文件 @@ -4764,6 +5418,7 @@ Error: %@ Saved + 已保存 No comment provided by engineer. @@ -4773,6 +5428,7 @@ Error: %@ Saved from + 保存自 No comment provided by engineer. @@ -4780,6 +5436,14 @@ Error: %@ 已保存的消息 message info title + + Scale + No comment provided by engineer. + + + Scan / Paste link + No comment provided by engineer. + Scan QR code 扫描二维码 @@ -4820,11 +5484,19 @@ Error: %@ 搜索或粘贴 SimpleX 链接 No comment provided by engineer. + + Secondary + No comment provided by engineer. + Secure queue 保护队列 server test step + + Secured + No comment provided by engineer. + Security assessment 安全评估 @@ -4838,6 +5510,14 @@ Error: %@ Select 选择 + chat item action + + + Selected %lld + No comment provided by engineer. + + + Selected chat preferences prohibit this message. No comment provided by engineer. @@ -4875,11 +5555,6 @@ Error: %@ 将送达回执发送给 No comment provided by engineer. - - Send direct message - 发送私信 - No comment provided by engineer. - Send direct message to connect 发送私信来连接 @@ -4890,6 +5565,10 @@ Error: %@ 发送限时消息中 No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews 发送链接预览 @@ -4900,6 +5579,18 @@ Error: %@ 发送实时消息 No comment provided by engineer. + + Send message to enable calls. + No comment provided by engineer. + + + Send messages directly when IP address is protected and your or destination server does not support private routing. + No comment provided by engineer. + + + Send messages directly when your or destination server does not support private routing. + No comment provided by engineer. + Send notifications 发送通知 @@ -4990,6 +5681,10 @@ Error: %@ 已发送于:%@ copied message info + + Sent directly + No comment provided by engineer. + Sent file event 已发送文件项目 @@ -5000,11 +5695,39 @@ Error: %@ 已发信息 message info title + + Sent messages + No comment provided by engineer. + Sent messages will be deleted after set time. 已发送的消息将在设定的时间后被删除。 No comment provided by engineer. + + Sent reply + No comment provided by engineer. + + + Sent total + No comment provided by engineer. + + + Sent via proxy + No comment provided by engineer. + + + Server address + No comment provided by engineer. + + + Server address is incompatible with network settings. + srv error text. + + + Server address is incompatible with network settings: %@. + No comment provided by engineer. + Server requires authorization to create queues, check password 服务器需要授权才能创建队列,检查密码 @@ -5020,11 +5743,31 @@ Error: %@ 服务器测试失败! No comment provided by engineer. + + Server type + No comment provided by engineer. + + + Server version is incompatible with network settings. + srv error text + + + Server version is incompatible with your app: %@. + No comment provided by engineer. + Servers 服务器 No comment provided by engineer. + + Servers info + No comment provided by engineer. + + + Servers statistics will be reset - this cannot be undone! + No comment provided by engineer. + Session code 会话码 @@ -5040,6 +5783,10 @@ Error: %@ 设置联系人姓名…… No comment provided by engineer. + + Set default theme + No comment provided by engineer. + Set group preferences 设置群组偏好设置 @@ -5057,6 +5804,7 @@ Error: %@ Set passphrase + 设置密码短语 No comment provided by engineer. @@ -5081,6 +5829,7 @@ Error: %@ Shape profile images + 改变个人资料图形状 No comment provided by engineer. @@ -5103,6 +5852,10 @@ Error: %@ 与联系人分享地址? No comment provided by engineer. + + Share from other apps. + No comment provided by engineer. + Share link 分享链接 @@ -5113,6 +5866,10 @@ Error: %@ 分享此一次性邀请链接 No comment provided by engineer. + + Share to SimpleX + No comment provided by engineer. + Share with contacts 与联系人分享 @@ -5120,6 +5877,7 @@ Error: %@ Show QR code + 显示二维码 No comment provided by engineer. @@ -5137,16 +5895,32 @@ Error: %@ 显示最近的消息 No comment provided by engineer. + + Show message status + No comment provided by engineer. + + + Show percentage + No comment provided by engineer. + Show preview 显示预览 No comment provided by engineer. + + Show → on messages sent via private routing. + No comment provided by engineer. + Show: 显示: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address SimpleX 地址 @@ -5204,10 +5978,12 @@ Error: %@ SimpleX links are prohibited in this group. + 此群禁止 SimpleX 链接。 No comment provided by engineer. SimpleX links not allowed + 不允许SimpleX 链接 No comment provided by engineer. @@ -5220,6 +5996,10 @@ Error: %@ 简化的隐身模式 No comment provided by engineer. + + Size + No comment provided by engineer. + Skip 跳过 @@ -5235,11 +6015,23 @@ Error: %@ 小群组(最多 20 人) No comment provided by engineer. + + Soft + blur media + + + Some file(s) were not exported: + No comment provided by engineer. + Some non-fatal errors occurred during import - you may see Chat console for more details. 导入过程中发生了一些非致命错误——您可以查看聊天控制台了解更多详细信息。 No comment provided by engineer. + + Some non-fatal errors occurred during import: + No comment provided by engineer. + Somebody 某人 @@ -5247,6 +6039,7 @@ Error: %@ Square, circle, or anything in between. + 方形、圆形、或两者之间的任意形状 No comment provided by engineer. @@ -5264,6 +6057,14 @@ Error: %@ 开始迁移 No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop 停止 @@ -5276,6 +6077,7 @@ Error: %@ Stop chat + 停止聊天程序 No comment provided by engineer. @@ -5320,13 +6122,30 @@ Error: %@ Stopping chat + 正在停止聊天 No comment provided by engineer. + + Strong + blur media + Submit 提交 No comment provided by engineer. + + Subscribed + No comment provided by engineer. + + + Subscription errors + No comment provided by engineer. + + + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat 支持 SimpleX Chat @@ -5342,6 +6161,10 @@ Error: %@ 系统验证 No comment provided by engineer. + + TCP connection + No comment provided by engineer. + TCP connection timeout TCP 连接超时 @@ -5402,9 +6225,8 @@ Error: %@ 轻按扫描 No comment provided by engineer. - - Tap to start a new chat - 点击开始一个新聊天 + + Temporary file error No comment provided by engineer. @@ -5459,6 +6281,10 @@ It can happen because of some bug or when the connection is compromised.该应用可以在您收到消息或联系人请求时通知您——请打开设置以启用通知。 No comment provided by engineer. + + The app will ask to confirm downloads from unknown file servers (except .onion). + No comment provided by engineer. + The attempt to change database passphrase was not completed. 更改数据库密码的尝试未完成。 @@ -5504,6 +6330,14 @@ It can happen because of some bug or when the connection is compromised.该消息将对所有成员标记为已被管理员移除。 No comment provided by engineer. + + The messages will be deleted for all members. + No comment provided by engineer. + + + The messages will be marked as moderated for all members. + No comment provided by engineer. + The next generation of private messaging 下一代私密通讯软件 @@ -5539,9 +6373,8 @@ It can happen because of some bug or when the connection is compromised.您粘贴的文本不是 SimpleX 链接。 No comment provided by engineer. - - Theme - 主题 + + Themes No comment provided by engineer. @@ -5571,10 +6404,12 @@ It can happen because of some bug or when the connection is compromised. This chat is protected by end-to-end encryption. + 此聊天受端到端加密保护。 E2EE info chat item This chat is protected by quantum resistant end-to-end encryption. + 此聊天受抗量子的端到端加密保护。 E2EE info chat item @@ -5607,11 +6442,19 @@ It can happen because of some bug or when the connection is compromised.这是你自己的一次性链接! No comment provided by engineer. + + This link was used with another mobile device, please create a new link on the desktop. + No comment provided by engineer. + This setting applies to messages in your current chat profile **%@**. 此设置适用于您当前聊天资料 **%@** 中的消息。 No comment provided by engineer. + + Title + No comment provided by engineer. + To ask any questions and to receive updates: 要提出任何问题并接收更新,请: @@ -5642,6 +6485,10 @@ It can happen because of some bug or when the connection is compromised.为了保护时区,图像/语音文件使用 UTC。 No comment provided by engineer. + + To protect your IP address, private routing uses your SMP servers to deliver messages. + No comment provided by engineer. + To protect your information, turn on SimpleX Lock. You will be prompted to complete authentication before this feature is enabled. @@ -5669,16 +6516,32 @@ You will be prompted to complete authentication before this feature is enabled.< 要与您的联系人验证端到端加密,请比较(或扫描)您设备上的代码。 No comment provided by engineer. + + Toggle chat list: + No comment provided by engineer. + Toggle incognito when connecting. 在连接时切换隐身模式。 No comment provided by engineer. + + Toolbar opacity + No comment provided by engineer. + + + Total + No comment provided by engineer. + Transport isolation 传输隔离 No comment provided by engineer. + + Transport sessions + No comment provided by engineer. + Trying to connect to the server used to receive messages from this contact (error: %@). 正在尝试连接到用于从该联系人接收消息的服务器(错误:%@)。 @@ -5733,11 +6596,6 @@ You will be prompted to complete authentication before this feature is enabled.< 解封成员吗? No comment provided by engineer. - - Unexpected error: %@ - 意外错误: %@ - item status description - Unexpected migration state 未预料的迁移状态 @@ -5746,7 +6604,7 @@ You will be prompted to complete authentication before this feature is enabled.< Unfav. 取消最喜欢 - No comment provided by engineer. + swipe action Unhide @@ -5783,6 +6641,10 @@ You will be prompted to complete authentication before this feature is enabled.< 未知错误 No comment provided by engineer. + + Unknown servers! + No comment provided by engineer. + Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions. 除非您使用 iOS 通话界面,否则请启用请勿打扰模式以避免打扰。 @@ -5818,12 +6680,12 @@ To connect, please ask your contact to create another connection link and check Unmute 取消静音 - No comment provided by engineer. + swipe action Unread 未读 - No comment provided by engineer. + swipe action Up to 100 last messages are sent to new members. @@ -5835,11 +6697,6 @@ To connect, please ask your contact to create another connection link and check 更新 No comment provided by engineer. - - Update .onion hosts setting? - 更新 .onion 主机设置? - No comment provided by engineer. - Update database passphrase 更新数据库密码 @@ -5850,9 +6707,8 @@ To connect, please ask your contact to create another connection link and check 更新网络设置? No comment provided by engineer. - - Update transport isolation mode? - 更新传输隔离模式? + + Update settings? No comment provided by engineer. @@ -5860,18 +6716,18 @@ To connect, please ask your contact to create another connection link and check 更新设置会将客户端重新连接到所有服务器。 No comment provided by engineer. - - Updating this setting will re-connect the client to all servers. - 更新此设置将重新连接客户端到所有服务器。 - No comment provided by engineer. - Upgrade and open chat 升级并打开聊天 No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed + 上传失败了 No comment provided by engineer. @@ -5879,8 +6735,17 @@ To connect, please ask your contact to create another connection link and check 上传文件 server test step + + Uploaded + No comment provided by engineer. + + + Uploaded files + No comment provided by engineer. + Uploading archive + 正在上传存档 No comment provided by engineer. @@ -5927,6 +6792,14 @@ To connect, please ask your contact to create another connection link and check Use only local notifications? No comment provided by engineer. + + Use private routing with unknown servers when IP address is not protected. + No comment provided by engineer. + + + Use private routing with unknown servers. + No comment provided by engineer. + Use server 使用服务器 @@ -5934,6 +6807,11 @@ To connect, please ask your contact to create another connection link and check Use the app while in the call. + 通话时使用本应用 + No comment provided by engineer. + + + Use the app with one hand. No comment provided by engineer. @@ -5941,9 +6819,8 @@ To connect, please ask your contact to create another connection link and check 用户资料 No comment provided by engineer. - - Using .onion hosts requires compatible VPN provider. - 使用 .onion 主机需要兼容的 VPN 提供商。 + + User selection No comment provided by engineer. @@ -5973,10 +6850,12 @@ To connect, please ask your contact to create another connection link and check Verify database passphrase + 验证数据库密码短语 No comment provided by engineer. Verify passphrase + 验证密码短语 No comment provided by engineer. @@ -6041,6 +6920,7 @@ To connect, please ask your contact to create another connection link and check Voice messages not allowed + 不允许语音消息 No comment provided by engineer. @@ -6072,8 +6952,17 @@ To connect, please ask your contact to create another connection link and check 等待视频中 No comment provided by engineer. + + Wallpaper accent + No comment provided by engineer. + + + Wallpaper background + No comment provided by engineer. + Warning: starting chat on multiple devices is not supported and will cause message delivery failures + 警告:不支持在多部设备上启动聊天,这么做会导致消息传送失败。 No comment provided by engineer. @@ -6098,6 +6987,7 @@ To connect, please ask your contact to create another connection link and check Welcome message is too long + 欢迎消息太大了 No comment provided by engineer. @@ -6112,6 +7002,7 @@ To connect, please ask your contact to create another connection link and check When connecting audio and video calls. + 连接音频和视频通话时。 No comment provided by engineer. @@ -6126,14 +7017,17 @@ To connect, please ask your contact to create another connection link and check WiFi + WiFi No comment provided by engineer. Will be enabled in direct chats! + 将在私聊中启用! No comment provided by engineer. Wired ethernet + 有线以太网 No comment provided by engineer. @@ -6151,19 +7045,34 @@ To connect, please ask your contact to create another connection link and check 降低了电量使用。 No comment provided by engineer. + + Without Tor or VPN, your IP address will be visible to file servers. + No comment provided by engineer. + + + Without Tor or VPN, your IP address will be visible to these XFTP relays: %@. + No comment provided by engineer. + Wrong database passphrase 数据库密码错误 No comment provided by engineer. + + Wrong key or unknown connection - most likely this connection is deleted. + snd error text + + + Wrong key or unknown file chunk address - most likely file is deleted. + file error text + Wrong passphrase! 密码错误! No comment provided by engineer. - - XFTP servers - XFTP 服务器 + + XFTP server No comment provided by engineer. @@ -6236,11 +7145,19 @@ Repeat join request? 您被邀请加入群组 No comment provided by engineer. + + You are not connected to these servers. Private routing is used to deliver messages to them. + No comment provided by engineer. + You can accept calls from lock screen, without device and app authentication. 您可以从锁屏上接听电话,无需设备和应用程序的认证。 No comment provided by engineer. + + You can change it in Appearance settings. + No comment provided by engineer. + You can create it later 您可以以后创建它 @@ -6258,6 +7175,7 @@ Repeat join request? You can give another try. + 你可以再试一次。 No comment provided by engineer. @@ -6270,11 +7188,15 @@ Repeat join request? 你可以通过设置让它对你的 SimpleX 联系人可见。 No comment provided by engineer. - - You can now send messages to %@ + + You can now chat with %@ 您现在可以给 %@ 发送消息 notification body + + You can send messages to %@ from Archived contacts. + No comment provided by engineer. + You can set lock screen notification preview via settings. 您可以通过设置来设置锁屏通知预览。 @@ -6300,6 +7222,10 @@ Repeat join request? 您可以通过应用程序设置/数据库或重新启动应用程序开始聊天 No comment provided by engineer. + + You can still view conversation with %@ in the list of chats. + No comment provided by engineer. + You can turn on SimpleX Lock via Settings. 您可以通过设置开启 SimpleX 锁定。 @@ -6340,11 +7266,6 @@ Repeat join request? Repeat connection request? No comment provided by engineer. - - You have no chats - 您没有聊天记录 - No comment provided by engineer. - You have to enter passphrase every time the app starts - it is not stored on the device. 您必须在每次应用程序启动时输入密码——它不存储在设备上。 @@ -6365,11 +7286,23 @@ Repeat connection request? 你加入了这个群组。连接到邀请组成员。 No comment provided by engineer. + + You may migrate the exported database. + No comment provided by engineer. + + + You may save the exported archive. + No comment provided by engineer. + You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts. 您只能在一台设备上使用最新版本的聊天数据库,否则您可能会停止接收来自某些联系人的消息。 No comment provided by engineer. + + You need to allow your contact to call to be able to call them. + No comment provided by engineer. + You need to allow your contact to send voice messages to be able to send them. 您需要允许您的联系人发送语音消息,以便您能够发送语音消息。 @@ -6484,13 +7417,6 @@ Repeat connection request? 您的聊天资料 No comment provided by engineer. - - Your contact needs to be online for the connection to complete. -You can cancel this connection and remove the contact (and try later with a new link). - 您的联系人需要在线才能完成连接。 -您可以取消此连接并删除联系人(然后尝试使用新链接)。 - No comment provided by engineer. - Your contact sent a file that is larger than currently supported maximum size (%@). 您的联系人发送的文件大于当前支持的最大大小 (%@)。 @@ -6609,6 +7535,7 @@ SimpleX 服务器无法看到您的资料。 admins + 管理员 feature role @@ -6623,6 +7550,7 @@ SimpleX 服务器无法看到您的资料。 all members + 所有成员 feature role @@ -6634,6 +7562,10 @@ SimpleX 服务器无法看到您的资料。 and %lld other events No comment provided by engineer. + + attempts + No comment provided by engineer. + audio call (not e2e encrypted) 语音通话(非端到端加密) @@ -6674,6 +7606,10 @@ SimpleX 服务器无法看到您的资料。 加粗 No comment provided by engineer. + + call + No comment provided by engineer. + call error 通话错误 @@ -6823,6 +7759,10 @@ SimpleX 服务器无法看到您的资料。 time unit + + decryption errors + No comment provided by engineer. + default (%@) 默认 (%@) @@ -6873,6 +7813,10 @@ SimpleX 服务器无法看到您的资料。 重复的消息 integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted 端到端加密 @@ -6953,8 +7897,13 @@ SimpleX 服务器无法看到您的资料。 发生的事 No comment provided by engineer. + + expired + No comment provided by engineer. + forwarded + 已转发 No comment provided by engineer. @@ -6982,6 +7931,10 @@ SimpleX 服务器无法看到您的资料。 在您重启应用或改变密码后,iOS钥匙串将被用来安全地存储密码——它将允许接收推送通知。 No comment provided by engineer. + + inactive + No comment provided by engineer. + incognito via contact address link 通过联系人地址链接隐身聊天 @@ -7022,6 +7975,10 @@ SimpleX 服务器无法看到您的资料。 邀请您加入群组 %@ group name + + invite + No comment provided by engineer. + invited 已邀请 @@ -7076,6 +8033,10 @@ SimpleX 服务器无法看到您的资料。 已连接 rcv group event chat item + + message + No comment provided by engineer. + message received 消息已收到 @@ -7106,6 +8067,10 @@ SimpleX 服务器无法看到您的资料。 time unit + + mute + No comment provided by engineer. + never 从不 @@ -7158,6 +8123,14 @@ SimpleX 服务器无法看到您的资料。 开启 group pref value + + other + No comment provided by engineer. + + + other errors + No comment provided by engineer. + owner 群主 @@ -7165,6 +8138,7 @@ SimpleX 服务器无法看到您的资料。 owners + 所有者 feature role @@ -7174,6 +8148,7 @@ SimpleX 服务器无法看到您的资料。 quantum resistant e2e encryption + 抗量子端到端加密 chat item text @@ -7218,12 +8193,17 @@ SimpleX 服务器无法看到您的资料。 saved + 已保存 No comment provided by engineer. saved from %@ No comment provided by engineer. + + search + No comment provided by engineer. + sec @@ -7249,6 +8229,12 @@ SimpleX 服务器无法看到您的资料。 发送私信 No comment provided by engineer. + + server queue info: %1$@ + +last received msg: %2$@ + queue info + set new contact address 设置新的联系地址 @@ -7261,6 +8247,7 @@ SimpleX 服务器无法看到您的资料。 standard end-to-end encryption + 标准端到端加密 chat item text @@ -7287,11 +8274,23 @@ SimpleX 服务器无法看到您的资料。 未知 connection info + + unknown servers + No comment provided by engineer. + unknown status 未知状态 No comment provided by engineer. + + unmute + No comment provided by engineer. + + + unprotected + No comment provided by engineer. + updated group profile 已更新的群组资料 @@ -7331,6 +8330,10 @@ SimpleX 服务器无法看到您的资料。 通过中继 No comment provided by engineer. + + video + No comment provided by engineer. + video call (not e2e encrypted) 视频通话(非端到端加密) @@ -7356,6 +8359,10 @@ SimpleX 服务器无法看到您的资料。 time unit + + when IP hidden + No comment provided by engineer. + yes @@ -7363,6 +8370,7 @@ SimpleX 服务器无法看到您的资料。 you + No comment provided by engineer. @@ -7437,7 +8445,7 @@ SimpleX 服务器无法看到您的资料。
- +
@@ -7473,7 +8481,7 @@ SimpleX 服务器无法看到您的资料。
- +
@@ -7493,4 +8501,178 @@ SimpleX 服务器无法看到您的资料。
+ +
+ +
+ + + SimpleX SE + Bundle display name + + + SimpleX SE + Bundle name + + + Copyright © 2024 SimpleX Chat. All rights reserved. + Copyright (human-readable) + + +
+ +
+ +
+ + + %@ + No comment provided by engineer. + + + App is locked! + No comment provided by engineer. + + + Cancel + No comment provided by engineer. + + + Cannot access keychain to save database password + No comment provided by engineer. + + + Cannot forward message + No comment provided by engineer. + + + Comment + No comment provided by engineer. + + + Currently maximum supported file size is %@. + No comment provided by engineer. + + + Database downgrade required + No comment provided by engineer. + + + Database encrypted! + No comment provided by engineer. + + + Database error + No comment provided by engineer. + + + Database passphrase is different from saved in the keychain. + No comment provided by engineer. + + + Database passphrase is required to open chat. + No comment provided by engineer. + + + Database upgrade required + No comment provided by engineer. + + + Error preparing file + No comment provided by engineer. + + + Error preparing message + No comment provided by engineer. + + + Error: %@ + No comment provided by engineer. + + + File error + No comment provided by engineer. + + + Incompatible database version + No comment provided by engineer. + + + Invalid migration confirmation + No comment provided by engineer. + + + Keychain error + No comment provided by engineer. + + + Large file! + No comment provided by engineer. + + + No active profile + No comment provided by engineer. + + + Ok + No comment provided by engineer. + + + Open the app to downgrade the database. + No comment provided by engineer. + + + Open the app to upgrade the database. + No comment provided by engineer. + + + Passphrase + No comment provided by engineer. + + + Please create a profile in the SimpleX app + No comment provided by engineer. + + + Selected chat preferences prohibit this message. + No comment provided by engineer. + + + Sending a message takes longer than expected. + No comment provided by engineer. + + + Sending message… + No comment provided by engineer. + + + Share + No comment provided by engineer. + + + Slow network? + No comment provided by engineer. + + + Unknown database error: %@ + No comment provided by engineer. + + + Unsupported format + No comment provided by engineer. + + + Wait + No comment provided by engineer. + + + Wrong database passphrase + No comment provided by engineer. + + + You can allow sharing in Privacy & Security / SimpleX Lock settings. + No comment provided by engineer. + + +
diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings index 124ddbcc33..9c675514f4 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX NSE/en.lproj/InfoPlist.strings @@ -1,6 +1,9 @@ /* Bundle display name */ "CFBundleDisplayName" = "SimpleX NSE"; + /* Bundle name */ "CFBundleName" = "SimpleX NSE"; + /* Copyright (human-readable) */ "NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Source Contents/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/contents.json b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/contents.json index 807a15f96c..6416a2d8fa 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/contents.json +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/contents.json @@ -3,10 +3,10 @@ "project" : "SimpleX.xcodeproj", "targetLocale" : "zh-Hans", "toolInfo" : { - "toolBuildNumber" : "15A240d", + "toolBuildNumber" : "15F31d", "toolID" : "com.apple.dt.xcode", "toolName" : "Xcode", - "toolVersion" : "15.0" + "toolVersion" : "15.4" }, "version" : "1.0" } \ No newline at end of file diff --git a/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff b/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff index 03a108d112..2b8649935c 100644 --- a/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff @@ -314,7 +314,7 @@
About SimpleX Chat - 關於 SimpleX 對話 + 關於 SimpleX Chat No comment provided by engineer. @@ -358,9 +358,9 @@ 使用二維碼掃描以新增伺服器。 No comment provided by engineer. - - Add server… - 新增伺服器… + + Add server + 新增伺服器 No comment provided by engineer. @@ -445,7 +445,7 @@ Allow your contacts to send disappearing messages. - 允許你的聯絡人傳送自動銷毀的訊息。 + 允許您的聯絡人傳送限時訊息。 No comment provided by engineer. @@ -5898,6 +5898,230 @@ It can happen because of some bug or when the connection is compromised.%@ 和 %@ 已連接 No comment provided by engineer. + + %@ downloaded + %@ 下載 + + + %@ uploaded + %@ 上傳 + + + Abort + 中止 + + + **Create group**: to create a new group. + **創建群組**: 創建一個新的群組。 + + + Abort changing address + 中止更改地址 + + + Accept connection request? + 接受連線請求? + + + Camera not available + 相機不可用 + + + All messages will be deleted - this cannot be undone! + 所有訊息都將被刪除 - 這不能還原! + + + Allow irreversible message deletion only if your contact allows it to you. (24 hours) + 只有你的聯絡人允許的情況下,才允許不可逆地將訊息刪除。(24小時) + + + Allow to irreversibly delete sent messages. (24 hours) + 允許將不可撤銷的訊息刪除。(24小時) + + + Allow your contacts to irreversibly delete sent messages. (24 hours) + 允許您的聯絡人不可復原地刪除已傳送的訊息。(24小時) + + + Bad desktop address + 無效的桌面地址 + + + Error decrypting file + 解密檔案時出錯 + + + Add contact + 新增聯絡人 + + + Advanced settings + 進階設定 + + + Allow calls? + 允許通話? + + + Allow to send files and media. + 允許傳送檔案和媒體。 + + + Already joining the group! + 已加入群組! + + + App data migration + 應用資料轉移 + + + Apply + 應用 + + + Apply to + 應用到 + + + Archive and upload + 儲存並上傳 + + + Block + 封鎖 + + + Block group members + 封鎖群組成員 + + + Block member + 封鎖成員 + + + Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! + 保加利亞語、芬蘭語、泰語和烏克蘭語——感謝使用者們和[Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)! + + + Can't call member + 無法與成員通話 + + + Can't message member + 無法傳送訊息給成員 + + + Cancel migration + 取消遷移 + + + Chat database exported + 導出聊天數據庫 + + + 0 sec + 0 秒 + + + All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays. + 你的所有聯絡人、對話和文件將被安全加密並切塊上傳到設置的 XFTP 中繼。 + + + Address change will be aborted. Old receiving address will be used. + 將取消地址更改。將使用舊聯絡地址。 + + + Archiving database + 正在儲存資料庫 + + + Cellular + 行動網路 + + + %@, %@ and %lld members + %@, %@ 和 %lld 成員 + + + %lld messages marked deleted + %lld 條訊息已刪除 + + + Already connecting! + 已連接! + + + Block member? + 封鎖成員? + + + (new) + (新) + + + %@, %@ and %lld other members connected + %@, %@ 和 %lld 個成員已連接 + + + A few more things + 其他 + + + Show last messages + 顯示最新的訊息 + + + App encrypts new local files (except videos). + 應用程式將為新的本機文件(影片除外)加密。 + + + Better groups + 更加的群組 + + + %lld new interface languages + %lld 種新的介面語言 + + + Blocked by admin + 由管理員封鎖 + + + Both you and your contact can irreversibly delete sent messages. (24 hours) + 您與您的聯絡人都可以不可復原地删除已傳送的訊息。(24小時) + + + Encrypt local files + 加密本機檔案 + + + - more stable message delivery. +- a bit better groups. +- and more! + - 更穩定的傳送! +- 更好的社群! +- 以及更多! + + + - optionally notify deleted contacts. +- profile names with spaces. +- and more! + - 可選擇通知已刪除的聯絡人 +- 帶空格的共人資料名稱。 +-以及更多! + + + Abort changing address? + 中止更改地址? + + + Allow to send SimpleX links. + 允許傳送 SimpleX 連結。 + + + Background + 後台 + diff --git a/apps/ios/SimpleX NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index faa7f4f44c..1a2a27ba9b 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -119,7 +119,7 @@ class NotificationService: UNNotificationServiceExtension { var threadId: UUID? = NSEThreads.shared.newThread() var notificationInfo: NtfMessages? var receiveEntityId: String? - var expectedMessages: Set = [] + var expectedMessage: String? // return true if the message is taken - it prevents sending it to another NotificationService instance for processing var shouldProcessNtf = false var appSubscriber: AppSubscriber? @@ -191,7 +191,7 @@ class NotificationService: UNNotificationServiceExtension { let dbStatus = startChat() if case .ok = dbStatus, let ntfInfo = apiGetNtfMessage(nonce: nonce, encNtfInfo: encNtfInfo) { - logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo.ntfMessages.count))") + logger.debug("NotificationService: receiveNtfMessages: apiGetNtfMessage \(String(describing: ntfInfo.ntfMessage_ == nil ? 0 : 1))") if let connEntity = ntfInfo.connEntity_ { setBestAttemptNtf( ntfInfo.ntfsEnabled @@ -201,7 +201,7 @@ class NotificationService: UNNotificationServiceExtension { if let id = connEntity.id, ntfInfo.msgTs != nil { notificationInfo = ntfInfo receiveEntityId = id - expectedMessages = Set(ntfInfo.ntfMessages.map { $0.msgId }) + expectedMessage = ntfInfo.ntfMessage_.flatMap { $0.msgId } shouldProcessNtf = true return } @@ -224,12 +224,10 @@ class NotificationService: UNNotificationServiceExtension { self.setBestAttemptNtf(.empty) } if case let .msgInfo(info) = ntf { - let found = expectedMessages.remove(info.msgId) - if found != nil { - logger.debug("NotificationService processNtf: msgInfo, last: \(self.expectedMessages.isEmpty)") - if expectedMessages.isEmpty { - self.deliverBestAttemptNtf() - } + if info.msgId == expectedMessage { + expectedMessage = nil + logger.debug("NotificationService processNtf: msgInfo") + self.deliverBestAttemptNtf() return true } else if info.msgTs > msgTs { logger.debug("NotificationService processNtf: unexpected msgInfo, let other instance to process it, stopping this one") @@ -392,6 +390,16 @@ func appStateSubscriber(onState: @escaping (AppState) -> Void) -> AppSubscriber } } +let seSubscriber = seMessageSubscriber { + switch $0 { + case let .state(state): + if state == .sendingMessage && NSEChatState.shared.value.canSuspend { + logger.debug("NotificationService: seSubscriber app state \(state.rawValue), suspending") + suspendChat(fastNSESuspendSchedule.timeout) + } + } +} + var receiverStarted = false let startLock = DispatchSemaphore(value: 1) let suspendLock = DispatchSemaphore(value: 1) @@ -439,8 +447,7 @@ func doStartChat() -> DBMigrationResult? { logger.debug("NotificationService active user \(String(describing: user))") do { try setNetworkConfig(networkConfig) - try apiSetTempFolder(tempFolder: getTempFilesDirectory().path) - try apiSetFilesFolder(filesFolder: getAppFilesDirectory().path) + try apiSetAppFilePaths(filesFolder: getAppFilesDirectory().path, tempFolder: getTempFilesDirectory().path, assetsFolder: getWallpaperDirectory().deletingLastPathComponent().path) try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get()) // prevent suspension while starting chat suspendLock.wait() @@ -637,7 +644,7 @@ func apiGetActiveUser() -> User? { } func apiStartChat() throws -> Bool { - let r = sendSimpleXCmd(.startChat(mainApp: false)) + let r = sendSimpleXCmd(.startChat(mainApp: false, enableSndFiles: false)) switch r { case .chatStarted: return true case .chatRunning: return false @@ -660,14 +667,8 @@ func apiSuspendChat(timeoutMicroseconds: Int) -> Bool { return false } -func apiSetTempFolder(tempFolder: String) throws { - let r = sendSimpleXCmd(.setTempFolder(tempFolder: tempFolder)) - if case .cmdOk = r { return } - throw r -} - -func apiSetFilesFolder(filesFolder: String) throws { - let r = sendSimpleXCmd(.setFilesFolder(filesFolder: filesFolder)) +func apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String) throws { + let r = sendSimpleXCmd(.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder)) if case .cmdOk = r { return } throw r } @@ -684,9 +685,9 @@ 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 { - logger.debug("apiGetNtfMessage response ntfMessages: \(ntfMessages.count)") - return NtfMessages(user: user, connEntity_: connEntity_, msgTs: msgTs, ntfMessages: ntfMessages) + if case let .ntfMessages(user, connEntity_, msgTs, ntfMessage_) = r, let user = user { + logger.debug("apiGetNtfMessage response ntfMessages: \(ntfMessage_ == nil ? 0 : 1)") + return NtfMessages(user: user, connEntity_: connEntity_, msgTs: msgTs, ntfMessage_: ntfMessage_) } else if case let .chatCmdError(_, error) = r { logger.debug("apiGetNtfMessage error response: \(String.init(describing: error))") } else { @@ -696,14 +697,16 @@ func apiGetNtfMessage(nonce: String, encNtfInfo: String) -> NtfMessages? { } func apiReceiveFile(fileId: Int64, encrypted: Bool, inline: Bool? = nil) -> AChatItem? { - let r = sendSimpleXCmd(.receiveFile(fileId: fileId, encrypted: encrypted, inline: inline)) + let userApprovedRelays = !privacyAskToApproveRelaysGroupDefault.get() + let r = sendSimpleXCmd(.receiveFile(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted, inline: inline)) if case let .rcvFileAccepted(_, chatItem) = r { return chatItem } logger.error("receiveFile error: \(responseError(r))") return nil } func apiSetFileToReceive(fileId: Int64, encrypted: Bool) { - let r = sendSimpleXCmd(.setFileToReceive(fileId: fileId, encrypted: encrypted)) + let userApprovedRelays = !privacyAskToApproveRelaysGroupDefault.get() + let r = sendSimpleXCmd(.setFileToReceive(fileId: fileId, userApprovedRelays: userApprovedRelays, encrypted: encrypted)) if case .cmdOk = r { return } logger.error("setFileToReceive error: \(responseError(r))") } @@ -731,7 +734,7 @@ struct NtfMessages { var user: User var connEntity_: ConnectionEntity? var msgTs: Date? - var ntfMessages: [NtfMsgInfo] + var ntfMessage_: NtfMsgInfo? var ntfsEnabled: Bool { user.showNotifications && (connEntity_?.ntfsEnabled ?? false) diff --git a/apps/ios/SimpleX NSE/bg.lproj/Localizable.strings b/apps/ios/SimpleX NSE/bg.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/bg.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/cs.lproj/Localizable.strings b/apps/ios/SimpleX NSE/cs.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/cs.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/de.lproj/InfoPlist.strings b/apps/ios/SimpleX NSE/de.lproj/InfoPlist.strings index 9c675514f4..6cc768efe1 100644 --- a/apps/ios/SimpleX NSE/de.lproj/InfoPlist.strings +++ b/apps/ios/SimpleX NSE/de.lproj/InfoPlist.strings @@ -5,5 +5,5 @@ "CFBundleName" = "SimpleX NSE"; /* Copyright (human-readable) */ -"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; +"NSHumanReadableCopyright" = "Copyright © 2024 SimpleX Chat. All rights reserved."; diff --git a/apps/ios/SimpleX NSE/de.lproj/Localizable.strings b/apps/ios/SimpleX NSE/de.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/de.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX NSE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..9c675514f4 --- /dev/null +++ b/apps/ios/SimpleX NSE/en.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX NSE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX NSE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2022 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX NSE/en.lproj/Localizable.strings b/apps/ios/SimpleX NSE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/es.lproj/Localizable.strings b/apps/ios/SimpleX NSE/es.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/es.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/fi.lproj/Localizable.strings b/apps/ios/SimpleX NSE/fi.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/fi.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/fr.lproj/Localizable.strings b/apps/ios/SimpleX NSE/fr.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/fr.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/hu.lproj/Localizable.strings b/apps/ios/SimpleX NSE/hu.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/hu.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/it.lproj/Localizable.strings b/apps/ios/SimpleX NSE/it.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/it.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/ja.lproj/Localizable.strings b/apps/ios/SimpleX NSE/ja.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/ja.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings b/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/nl.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/pl.lproj/Localizable.strings b/apps/ios/SimpleX NSE/pl.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/pl.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings b/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/ru.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/th.lproj/Localizable.strings b/apps/ios/SimpleX NSE/th.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/th.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/tr.lproj/Localizable.strings b/apps/ios/SimpleX NSE/tr.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/tr.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/uk.lproj/Localizable.strings b/apps/ios/SimpleX NSE/uk.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/uk.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX NSE/zh-Hans.lproj/Localizable.strings b/apps/ios/SimpleX NSE/zh-Hans.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX NSE/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/Info.plist b/apps/ios/SimpleX SE/Info.plist new file mode 100644 index 0000000000..2ce1f45040 --- /dev/null +++ b/apps/ios/SimpleX SE/Info.plist @@ -0,0 +1,35 @@ + + + + + NSExtension + + NSExtensionAttributes + + NSExtensionActivationRule + + NSExtensionActivationSupportsText + + NSExtensionActivationSupportsAttachmentsWithMinCount + 0 + NSExtensionActivationSupportsAttachmentsWithMaxCount + 1 + NSExtensionActivationSupportsWebPageWithMaxCount + 1 + NSExtensionActivationSupportsWebURLWithMaxCount + 1 + NSExtensionActivationSupportsFileWithMaxCount + 1 + NSExtensionActivationSupportsImageWithMaxCount + 1 + NSExtensionActivationSupportsMovieWithMaxCount + 1 + + + NSExtensionPointIdentifier + com.apple.share-services + NSExtensionPrincipalClass + ShareViewController + + + diff --git a/apps/ios/SimpleX SE/SEChatState.swift b/apps/ios/SimpleX SE/SEChatState.swift new file mode 100644 index 0000000000..581bff894a --- /dev/null +++ b/apps/ios/SimpleX SE/SEChatState.swift @@ -0,0 +1,39 @@ +// +// SEChatState.swift +// SimpleX SE +// +// Created by User on 18/07/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +// SEStateGroupDefault must not be used in the share extension directly, only via this singleton +class SEChatState { + static let shared = SEChatState() + private var value_ = seStateGroupDefault.get() + + var value: SEState { + value_ + } + + func set(_ state: SEState) { + seStateGroupDefault.set(state) + sendSEState(state) + value_ = state + } +} + +/// Waits for other processes to set their state to suspended +/// Will wait for maximum of two seconds, since they might not be running +func waitForOtherProcessesToSuspend() async { + let startTime = CFAbsoluteTimeGetCurrent() + while CFAbsoluteTimeGetCurrent() - startTime < 2 { + try? await Task.sleep(nanoseconds: 100 * NSEC_PER_MSEC) + if appStateGroupDefault.get() == .suspended && + nseStateGroupDefault.get() == .suspended { + break + } + } +} diff --git a/apps/ios/SimpleX SE/ShareAPI.swift b/apps/ios/SimpleX SE/ShareAPI.swift new file mode 100644 index 0000000000..47e072ae78 --- /dev/null +++ b/apps/ios/SimpleX SE/ShareAPI.swift @@ -0,0 +1,115 @@ +// +// ShareAPI.swift +// SimpleX SE +// +// Created by User on 15/07/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import OSLog +import Foundation +import SimpleXChat + +let logger = Logger() + +func apiGetActiveUser() throws -> User? { + let r = sendSimpleXCmd(.showActiveUser) + switch r { + case let .activeUser(user): return user + case .chatCmdError(_, .error(.noActiveUser)): return nil + default: throw r + } +} + +func apiStartChat() throws -> Bool { + let r = sendSimpleXCmd(.startChat(mainApp: false, enableSndFiles: true)) + switch r { + case .chatStarted: return true + case .chatRunning: return false + default: throw r + } +} + +func apiSetNetworkConfig(_ cfg: NetCfg) throws { + let r = sendSimpleXCmd(.apiSetNetworkConfig(networkConfig: cfg)) + if case .cmdOk = r { return } + throw r +} + +func apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String) throws { + let r = sendSimpleXCmd(.apiSetAppFilePaths(filesFolder: filesFolder, tempFolder: tempFolder, assetsFolder: assetsFolder)) + if case .cmdOk = r { return } + throw r +} + +func apiSetEncryptLocalFiles(_ enable: Bool) throws { + let r = sendSimpleXCmd(.apiSetEncryptLocalFiles(enable: enable)) + if case .cmdOk = r { return } + throw r +} + +func apiGetChats(userId: User.ID) throws -> Array { + let r = sendSimpleXCmd(.apiGetChats(userId: userId)) + if case let .apiChats(user: _, chats: chats) = r { return chats } + throw r +} + +func apiSendMessage( + chatInfo: ChatInfo, + cryptoFile: CryptoFile?, + msgContent: MsgContent +) throws -> AChatItem { + let r = sendSimpleXCmd( + chatInfo.chatType == .local + ? .apiCreateChatItem( + noteFolderId: chatInfo.apiId, + file: cryptoFile, + msg: msgContent + ) + : .apiSendMessage( + type: chatInfo.chatType, + id: chatInfo.apiId, + file: cryptoFile, + quotedItemId: nil, + msg: msgContent, + live: false, + ttl: nil + ) + ) + if case let .newChatItem(_, chatItem) = r { + return chatItem + } else { + if let filePath = cryptoFile?.filePath { removeFile(filePath) } + throw r + } +} + +func apiActivateChat() throws { + chatReopenStore() + let r = sendSimpleXCmd(.apiActivateChat(restoreChat: false)) + if case .cmdOk = r { return } + throw r +} + +func apiSuspendChat(expired: Bool) { + let r = sendSimpleXCmd(.apiSuspendChat(timeoutMicroseconds: expired ? 0 : 3_000000)) + // Block until `chatSuspended` received or 3 seconds has passed + var suspended = false + if case .cmdOk = r, !expired { + let startTime = CFAbsoluteTimeGetCurrent() + while CFAbsoluteTimeGetCurrent() - startTime < 3 { + switch recvSimpleXMsg(messageTimeout: 3_500000) { + case .chatSuspended: + suspended = false + break + default: continue + } + } + } + if !suspended { + _ = sendSimpleXCmd(.apiSuspendChat(timeoutMicroseconds: 0)) + } + logger.debug("close store") + chatCloseStore() + SEChatState.shared.set(.inactive) +} diff --git a/apps/ios/SimpleX SE/ShareModel.swift b/apps/ios/SimpleX SE/ShareModel.swift new file mode 100644 index 0000000000..5bda361126 --- /dev/null +++ b/apps/ios/SimpleX SE/ShareModel.swift @@ -0,0 +1,540 @@ +// +// ShareModel.swift +// SimpleX SE +// +// Created by Levitating Pineapple on 09/07/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import UniformTypeIdentifiers +import AVFoundation +import SwiftUI +import SimpleXChat + +/// Maximum size of hex encoded media previews +private let MAX_DATA_SIZE: Int64 = 14000 + +/// Maximum dimension (width or height) of an image, before passed for processing +private let MAX_DOWNSAMPLE_SIZE: Int64 = 2000 + +class ShareModel: ObservableObject { + @Published var sharedContent: SharedContent? + @Published var chats: [ChatData] = [] + @Published var profileImages: [ChatInfo.ID: UIImage] = [:] + @Published var search = "" + @Published var comment = "" + @Published var selected: ChatData? + @Published var isLoaded = false + @Published var bottomBar: BottomBar = .loadingSpinner + @Published var errorAlert: ErrorAlert? + @Published var hasSimplexLink = false + @Published var alertRequiresPassword = false + var networkTimeout = CFAbsoluteTimeGetCurrent() + + enum BottomBar { + case sendButton + case loadingSpinner + case loadingBar(progress: Double) + + var isLoading: Bool { + switch self { + case .sendButton: false + case .loadingSpinner: true + case .loadingBar: true + } + } + } + + var completion: () -> Void = { + fatalError("completion has not been set") + } + + private var itemProvider: NSItemProvider? + + var isSendDisbled: Bool { sharedContent == nil || selected == nil || isProhibited(selected) } + + var isLinkPreview: Bool { + switch sharedContent { + case .url: true + default: false + } + } + + func isProhibited(_ chat: ChatData?) -> Bool { + if let chat, let sharedContent { + sharedContent.prohibited(in: chat, hasSimplexLink: hasSimplexLink) + } else { false } + } + + var filteredChats: [ChatData] { + search.isEmpty + ? filterChatsToForwardTo(chats: chats) + : filterChatsToForwardTo(chats: chats) + .filter { foundChat($0, search.localizedLowercase) } + } + + func setup(context: NSExtensionContext) { + if appLocalAuthEnabledGroupDefault.get() && !allowShareExtensionGroupDefault.get() { + errorAlert = ErrorAlert(title: "App is locked!", message: "You can allow sharing in Privacy & Security / SimpleX Lock settings.") + return + } + if let item = context.inputItems.first as? NSExtensionItem, + let itemProvider = item.attachments?.first { + self.itemProvider = itemProvider + self.completion = { + ShareModel.CompletionHandler.isEventLoopEnabled = false + context.completeRequest(returningItems: [item]) { + apiSuspendChat(expired: $0) + } + } + setup() + } + } + + func setup(with dbKey: String? = nil) { + // Init Chat + Task { + if let e = initChat(with: dbKey) { + await MainActor.run { errorAlert = e } + } else { + // Load Chats + Task { + switch fetchChats() { + case let .success(chats): + // Decode base64 images on background thread + let profileImages = chats.reduce(into: Dictionary()) { dict, chatData in + if let profileImage = chatData.chatInfo.image, + let uiImage = UIImage(base64Encoded: profileImage) { + dict[chatData.id] = uiImage + } + } + await MainActor.run { + self.chats = chats + self.profileImages = profileImages + withAnimation { isLoaded = true } + } + case let .failure(error): + await MainActor.run { errorAlert = error } + } + } + // Process Attachment + Task { + switch await getSharedContent(self.itemProvider!) { + case let .success(chatItemContent): + await MainActor.run { + self.sharedContent = chatItemContent + self.bottomBar = .sendButton + if case let .text(string) = chatItemContent { comment = string } + } + case let .failure(errorAlert): + await MainActor.run { self.errorAlert = errorAlert } + } + } + } + } + } + + func send() { + if let sharedContent, let selected { + Task { + await MainActor.run { self.bottomBar = .loadingSpinner } + do { + SEChatState.shared.set(.sendingMessage) + await waitForOtherProcessesToSuspend() + let ci = try apiSendMessage( + chatInfo: selected.chatInfo, + cryptoFile: sharedContent.cryptoFile, + msgContent: sharedContent.msgContent(comment: self.comment) + ) + if selected.chatInfo.chatType == .local { + completion() + } else { + await MainActor.run { self.bottomBar = .loadingBar(progress: 0) } + if let e = await handleEvents( + isGroupChat: ci.chatInfo.chatType == .group, + isWithoutFile: sharedContent.cryptoFile == nil, + chatItemId: ci.chatItem.id + ) { + await MainActor.run { errorAlert = e } + } else { + completion() + } + } + } catch { + if let e = error as? ErrorAlert { + await MainActor.run { errorAlert = e } + } + } + } + } + } + + private func initChat(with dbKey: String? = nil) -> ErrorAlert? { + do { + if hasChatCtrl() && dbKey == nil { + try apiActivateChat() + } else { + resetChatCtrl() // Clears retained migration result + registerGroupDefaults() + haskell_init_se() + let (_, result) = chatMigrateInit(dbKey, confirmMigrations: defaultMigrationConfirmation()) + if let e = migrationError(result) { return e } + try apiSetAppFilePaths( + filesFolder: getAppFilesDirectory().path, + tempFolder: getTempFilesDirectory().path, + assetsFolder: getWallpaperDirectory().deletingLastPathComponent().path + ) + let isRunning = try apiStartChat() + logger.log(level: .debug, "chat started, running: \(isRunning)") + } + try apiSetNetworkConfig(getNetCfg()) + try apiSetEncryptLocalFiles(privacyEncryptLocalFilesGroupDefault.get()) + } catch { return ErrorAlert(error) } + return nil + } + + private func migrationError(_ r: DBMigrationResult) -> ErrorAlert? { + let useKeychain = storeDBPassphraseGroupDefault.get() + let storedDBKey = kcDatabasePassword.get() + if case .errorNotADatabase = r { + Task { await MainActor.run { self.alertRequiresPassword = true } } + } + return switch r { + case .errorNotADatabase: + if useKeychain && storedDBKey != nil && storedDBKey != "" { + ErrorAlert( + title: "Wrong database passphrase", + message: "Database passphrase is different from saved in the keychain." + ) + } else { + ErrorAlert( + title: "Database encrypted!", + message: "Database passphrase is required to open chat." + ) + } + case let .errorMigration(_, migrationError): + switch migrationError { + case .upgrade: + ErrorAlert( + title: "Database upgrade required", + message: "Open the app to upgrade the database." + ) + case .downgrade: + ErrorAlert( + title: "Database downgrade required", + message: "Open the app to downgrade the database." + ) + case let .migrationError(mtrError): + ErrorAlert( + title: "Incompatible database version", + message: mtrErrorDescription(mtrError) + ) + } + case let .errorSQL(_, migrationSQLError): + ErrorAlert( + title: "Database error", + message: "Error: \(migrationSQLError)" + ) + case .errorKeychain: + ErrorAlert( + title: "Keychain error", + message: "Cannot access keychain to save database password" + ) + case .invalidConfirmation: + ErrorAlert("Invalid migration confirmation") + case let .unknown(json): + ErrorAlert( + title: "Database error", + message: "Unknown database error: \(json)" + ) + case .ok: nil + } + } + + private func fetchChats() -> Result, ErrorAlert> { + do { + guard let user = try apiGetActiveUser() else { + return .failure( + ErrorAlert( + title: "No active profile", + message: "Please create a profile in the SimpleX app" + ) + ) + } + return .success(try apiGetChats(userId: user.id)) + } catch { + return .failure(ErrorAlert(error)) + } + } + + actor CompletionHandler { + static var isEventLoopEnabled = false + private var fileCompleted = false + private var messageCompleted = false + + func completeFile() { fileCompleted = true } + + func completeMessage() { messageCompleted = true } + + var isRunning: Bool { + Self.isEventLoopEnabled && !(fileCompleted && messageCompleted) + } + } + + /// Polls and processes chat events + /// Returns when message sending has completed optionally returning and error. + private func handleEvents(isGroupChat: Bool, isWithoutFile: Bool, chatItemId: ChatItem.ID) async -> ErrorAlert? { + func isMessage(for item: AChatItem?) -> Bool { + item.map { $0.chatItem.id == chatItemId } ?? false + } + + CompletionHandler.isEventLoopEnabled = true + let ch = CompletionHandler() + if isWithoutFile { await ch.completeFile() } + networkTimeout = CFAbsoluteTimeGetCurrent() + while await ch.isRunning { + if CFAbsoluteTimeGetCurrent() - networkTimeout > 30 { + await MainActor.run { + self.errorAlert = ErrorAlert(title: "Slow network?", message: "Sending a message takes longer than expected.") { + Button("Wait", role: .cancel) { self.networkTimeout = CFAbsoluteTimeGetCurrent() } + Button("Cancel", role: .destructive) { self.completion() } + } + } + } + switch recvSimpleXMsg(messageTimeout: 1_000_000) { + case let .sndFileProgressXFTP(_, ci, _, sentSize, totalSize): + guard isMessage(for: ci) else { continue } + networkTimeout = CFAbsoluteTimeGetCurrent() + await MainActor.run { + withAnimation { + let progress = Double(sentSize) / Double(totalSize) + bottomBar = .loadingBar(progress: progress) + } + } + case let .sndFileCompleteXFTP(_, ci, _): + guard isMessage(for: ci) else { continue } + if isGroupChat { + await MainActor.run { bottomBar = .loadingSpinner } + } + await ch.completeFile() + if await !ch.isRunning { break } + case let .chatItemStatusUpdated(_, ci): + guard isMessage(for: ci) else { continue } + if let (title, message) = ci.chatItem.meta.itemStatus.statusInfo { + // `title` and `message` already localized and interpolated + return ErrorAlert( + title: "\(title)", + message: "\(message)" + ) + } else if case let .sndSent(sndProgress) = ci.chatItem.meta.itemStatus { + switch sndProgress { + case .complete: + await ch.completeMessage() + case .partial: + if isGroupChat { + Task { + try? await Task.sleep(nanoseconds: 5 * NSEC_PER_SEC) + await ch.completeMessage() + } + } + } + } + case let .sndFileError(_, ci, _, errorMessage): + guard isMessage(for: ci) else { continue } + if let ci { cleanupFile(ci) } + return ErrorAlert(title: "File error", message: "\(fileErrorInfo(ci) ?? errorMessage)") + case let .sndFileWarning(_, ci, _, errorMessage): + guard isMessage(for: ci) else { continue } + if let ci { cleanupFile(ci) } + return ErrorAlert(title: "File error", message: "\(fileErrorInfo(ci) ?? errorMessage)") + case let .chatError(_, chatError): + return ErrorAlert(chatError) + case let .chatCmdError(_, chatError): + return ErrorAlert(chatError) + default: continue + } + } + return nil + } + + private func fileErrorInfo(_ ci: AChatItem?) -> String? { + switch ci?.chatItem.file?.fileStatus { + case let .sndError(e): e.errorInfo + case let .sndWarning(e): e.errorInfo + default: nil + } + } +} + +/// Chat Item Content extracted from `NSItemProvider` without the comment +enum SharedContent { + case image(preview: String, cryptoFile: CryptoFile) + case movie(preview: String, duration: Int, cryptoFile: CryptoFile) + case url(preview: LinkPreview) + case text(string: String) + case data(cryptoFile: CryptoFile) + + var cryptoFile: CryptoFile? { + switch self { + case let .image(_, cryptoFile): cryptoFile + case let .movie(_, _, cryptoFile): cryptoFile + case .url: nil + case .text: nil + case let .data(cryptoFile): cryptoFile + } + } + + func msgContent(comment: String) -> MsgContent { + switch self { + case let .image(preview, _): .image(text: comment, image: preview) + case let .movie(preview, duration, _): .video(text: comment, image: preview, duration: duration) + case let .url(preview): .link(text: preview.uri.absoluteString + (comment == "" ? "" : "\n" + comment), preview: preview) + case .text: .text(comment) + case .data: .file(comment) + } + } + + func prohibited(in chatData: ChatData, hasSimplexLink: Bool) -> Bool { + chatData.prohibitedByPref( + hasSimplexLink: hasSimplexLink, + isMediaOrFileAttachment: cryptoFile != nil, + isVoice: false + ) + } +} + +fileprivate func getSharedContent(_ ip: NSItemProvider) async -> Result { + if let type = firstMatching(of: [.image, .movie, .fileURL, .url, .text]) { + switch type { + // Prepare Image message + case .image: + // Animated + return if ip.hasItemConformingToTypeIdentifier(UTType.gif.identifier) { + if let url = try? await inPlaceUrl(type: type), + let data = try? Data(contentsOf: url), + let image = UIImage(data: data), + let cryptoFile = saveFile(data, generateNewFileName("IMG", "gif"), encrypted: privacyEncryptLocalFilesGroupDefault.get()), + let preview = resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE) { + .success(.image(preview: preview, cryptoFile: cryptoFile)) + } else { .failure(ErrorAlert("Error preparing message")) } + + // Static + } else { + if let image = await staticImage(), + let cryptoFile = saveImage(image), + let preview = resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE) { + .success(.image(preview: preview, cryptoFile: cryptoFile)) + } else { .failure(ErrorAlert("Error preparing message")) } + } + + // Prepare Movie message + case .movie: + if let url = try? await inPlaceUrl(type: type), + let trancodedUrl = await transcodeVideo(from: url), + let (image, duration) = AVAsset(url: trancodedUrl).generatePreview(), + let preview = resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE), + let cryptoFile = moveTempFileFromURL(trancodedUrl) { + try? FileManager.default.removeItem(at: trancodedUrl) + return .success(.movie(preview: preview, duration: duration, cryptoFile: cryptoFile)) + } else { return .failure(ErrorAlert("Error preparing message")) } + + // Prepare Data message + case .fileURL: + if let url = try? await inPlaceUrl(type: .data) { + if isFileTooLarge(for: url) { + let sizeString = ByteCountFormatter.string( + fromByteCount: Int64(getMaxFileSize(.xftp)), + countStyle: .binary + ) + return .failure( + ErrorAlert( + title: "Large file!", + message: "Currently maximum supported file size is \(sizeString)." + ) + ) + } + if let file = saveFileFromURL(url) { + return .success(.data(cryptoFile: file)) + } + } + return .failure(ErrorAlert("Error preparing file")) + + // Prepare Link message + case .url: + if let url = try? await ip.loadItem(forTypeIdentifier: type.identifier) as? URL { + let content: SharedContent = + if privacyLinkPreviewsGroupDefault.get(), let linkPreview = await getLinkPreview(for: url) { + .url(preview: linkPreview) + } else { + .text(string: url.absoluteString) + } + return .success(content) + } else { return .failure(ErrorAlert("Error preparing message")) } + + // Prepare Text message + case .text: + return if let text = try? await ip.loadItem(forTypeIdentifier: type.identifier) as? String { + .success(.text(string: text)) + } else { .failure(ErrorAlert("Error preparing message")) } + default: return .failure(ErrorAlert("Unsupported format")) + } + } else { + return .failure(ErrorAlert("Unsupported format")) + } + + + func inPlaceUrl(type: UTType) async throws -> URL { + try await withCheckedThrowingContinuation { cont in + let _ = ip.loadInPlaceFileRepresentation(forTypeIdentifier: type.identifier) { url, bool, error in + if let url = url { + cont.resume(returning: url) + } else if let error = error { + cont.resume(throwing: error) + } else { + fatalError("Either `url` or `error` must be present") + } + } + } + } + + func firstMatching(of types: Array) -> UTType? { + for type in types { + if ip.hasItemConformingToTypeIdentifier(type.identifier) { return type } + } + return nil + } + + func staticImage() async -> UIImage? { + if let url = try? await inPlaceUrl(type: .image), + let downsampledImage = downsampleImage(at: url, to: MAX_DOWNSAMPLE_SIZE) { + downsampledImage + } else { + /// Fallback to loading image directly from `ItemProvider` + /// in case loading from disk is not possible. Required for sharing screenshots. + try? await ip.loadItem(forTypeIdentifier: UTType.image.identifier) as? UIImage + } + } +} + + +fileprivate func transcodeVideo(from input: URL) async -> URL? { + let outputUrl = URL( + fileURLWithPath: generateNewFileName( + getTempFilesDirectory().path + "/" + "video", "mp4", + fullPath: true + ) + ) + if await makeVideoQualityLower(input, outputUrl: outputUrl) { + return outputUrl + } else { + try? FileManager.default.removeItem(at: outputUrl) + return nil + } +} + +fileprivate func isFileTooLarge(for url: URL) -> Bool { + fileSize(url) + .map { $0 > getMaxFileSize(.xftp) } + ?? false +} + diff --git a/apps/ios/SimpleX SE/ShareView.swift b/apps/ios/SimpleX SE/ShareView.swift new file mode 100644 index 0000000000..1f502ffcff --- /dev/null +++ b/apps/ios/SimpleX SE/ShareView.swift @@ -0,0 +1,230 @@ +// +// ShareView.swift +// SimpleX SE +// +// Created by Levitating Pineapple on 09/07/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI +import SimpleXChat + +struct ShareView: View { + @ObservedObject var model: ShareModel + @Environment(\.colorScheme) var colorScheme + @State private var password = String() + @AppStorage(GROUP_DEFAULT_PROFILE_IMAGE_CORNER_RADIUS, store: groupDefaults) private var radius = defaultProfileImageCorner + + var body: some View { + NavigationView { + ZStack(alignment: .bottom) { + if model.isLoaded { + List(model.filteredChats) { chat in + let isProhibited = model.isProhibited(chat) + let isSelected = model.selected == chat + HStack { + profileImage( + chatInfoId: chat.chatInfo.id, + iconName: chatIconName(chat.chatInfo), + size: 30 + ) + Text(chat.chatInfo.displayName).foregroundStyle( + isProhibited ? .secondary : .primary + ) + Spacer() + radioButton(selected: isSelected && !isProhibited) + } + .contentShape(Rectangle()) + .onTapGesture { + if isProhibited { + model.errorAlert = ErrorAlert( + title: "Cannot forward message", + message: "Selected chat preferences prohibit this message." + ) { Button("Ok", role: .cancel) { } } + } else { + model.selected = isSelected ? nil : chat + } + } + .tag(chat) + } + } else { + ProgressView().frame(maxHeight: .infinity) + } + } + .navigationTitle("Share") + .safeAreaInset(edge: .bottom) { + switch model.bottomBar { + case .sendButton: + compose(isLoading: false) + case .loadingSpinner: + compose(isLoading: true) + case .loadingBar(let progress): + loadingBar(progress: progress) + } + } + } + .searchable( + text: $model.search, + placement: .navigationBarDrawer(displayMode: .always) + ) + .alert($model.errorAlert) { alert in + if model.alertRequiresPassword { + SecureField("Passphrase", text: $password) + Button("Ok") { + model.setup(with: password) + password = String() + } + Button("Cancel", role: .cancel) { model.completion() } + } else { + Button("Ok") { model.completion() } + } + } + .onChange(of: model.comment) { + model.hasSimplexLink = hasSimplexLink($0) + } + } + + private func compose(isLoading: Bool) -> some View { + VStack(spacing: 0) { + Divider() + if let content = model.sharedContent { + itemPreview(content) + } + HStack { + Group { + if #available(iOSApplicationExtension 16.0, *) { + TextField("Comment", text: $model.comment, axis: .vertical).lineLimit(6) + } else { + TextField("Comment", text: $model.comment) + } + } + .contentShape(Rectangle()) + .disabled(isLoading) + .padding(.horizontal, 12) + .padding(.vertical, 4) + Group { + if isLoading { + ProgressView() + } else { + Button(action: model.send) { + Image(systemName: "arrow.up.circle.fill") + .resizable() + } + .disabled(model.isSendDisbled) + } + } + .frame(width: 28, height: 28) + .padding(6) + + } + .background(Color(.systemBackground)) + .clipShape(RoundedRectangle(cornerRadius: 20)) + .overlay( + RoundedRectangle(cornerRadius: 20) + .strokeBorder(.secondary, lineWidth: 0.5).opacity(0.7) + ) + .padding(8) + } + .background(.thinMaterial) + } + + @ViewBuilder private func itemPreview(_ content: SharedContent) -> some View { + switch content { + case let .image(preview, _): imagePreview(preview) + case let .movie(preview, _, _): imagePreview(preview) + case let .url(preview): linkPreview(preview) + case let .data(cryptoFile): + previewArea { + Image(systemName: "doc.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 30, height: 30) + .foregroundColor(Color(uiColor: .tertiaryLabel)) + .padding(.leading, 4) + Text(cryptoFile.filePath) + } + case .text: EmptyView() + } + } + + @ViewBuilder private func imagePreview(_ img: String) -> some View { + if let img = UIImage(base64Encoded: img) { + previewArea { + Image(uiImage: img) + .resizable() + .scaledToFit() + .frame(minHeight: 40, maxHeight: 60) + } + } else { + EmptyView() + } + } + + @ViewBuilder private func linkPreview(_ linkPreview: LinkPreview) -> some View { + previewArea { + HStack(alignment: .center, spacing: 8) { + if let uiImage = UIImage(base64Encoded: linkPreview.image) { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 80, maxHeight: 60) + } + VStack(alignment: .center, spacing: 4) { + Text(linkPreview.title) + .lineLimit(1) + Text(linkPreview.uri.absoluteString) + .font(.caption) + .lineLimit(1) + .foregroundColor(.secondary) + } + .padding(.vertical, 5) + .frame(maxWidth: .infinity, minHeight: 60) + } + } + } + + @ViewBuilder private func previewArea(@ViewBuilder content: @escaping () -> V) -> some View { + HStack(alignment: .center, spacing: 8) { + content() + Spacer() + } + .padding(.vertical, 1) + .frame(minHeight: 54) + .background { + switch colorScheme { + case .light: LightColorPaletteApp.sentMessage + case .dark: DarkColorPaletteApp.sentMessage + @unknown default: Color(.tertiarySystemBackground) + } + } + Divider() + } + + private func loadingBar(progress: Double) -> some View { + VStack { + Text("Sending message…") + ProgressView(value: progress) + } + .padding() + .background(Material.ultraThin) + } + + @ViewBuilder private func profileImage(chatInfoId: ChatInfo.ID, iconName: String, size: Double) -> some View { + if let uiImage = model.profileImages[chatInfoId] { + clipProfileImage(Image(uiImage: uiImage), size: size, radius: radius) + } else { + Image(systemName: iconName) + .resizable() + .foregroundColor(Color(uiColor: .tertiaryLabel)) + .frame(width: size, height: size) +// add background when adding themes to SE +// .background(Circle().fill(backgroundColor != nil ? backgroundColor! : .clear)) + } + } + + private func radioButton(selected: Bool) -> some View { + Image(systemName: selected ? "checkmark.circle.fill" : "circle") + .imageScale(.large) + .foregroundStyle(selected ? Color.accentColor : Color(.tertiaryLabel)) + } +} diff --git a/apps/ios/SimpleX SE/ShareViewController.swift b/apps/ios/SimpleX SE/ShareViewController.swift new file mode 100644 index 0000000000..bf22f44a3b --- /dev/null +++ b/apps/ios/SimpleX SE/ShareViewController.swift @@ -0,0 +1,46 @@ +// +// ShareViewController.swift +// SimpleX SE +// +// Created by Levitating Pineapple on 08/07/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import UIKit +import SwiftUI +import SimpleXChat + +/// Extension Entry point +/// System will create this controller each time share sheet is invoked +/// using `NSExtensionPrincipalClass` in the info.plist +@objc(ShareViewController) +class ShareViewController: UIHostingController { + private let model = ShareModel() + // Assuming iOS continues to only allow single share sheet to be presented at once + static var isVisible: Bool = false + + @objc init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(rootView: ShareView(model: model)) + } + + @available(*, unavailable) + required init?(coder aDecoder: NSCoder) { fatalError() } + + override func viewDidLoad() { + ShareModel.CompletionHandler.isEventLoopEnabled = false + model.setup(context: extensionContext!) + } + + override func viewWillAppear(_ animated: Bool) { + logger.debug("ShareSheet will appear") + super.viewWillAppear(animated) + Self.isVisible = true + } + + override func viewWillDisappear(_ animated: Bool) { + logger.debug("ShareSheet will dissappear") + super.viewWillDisappear(animated) + ShareModel.CompletionHandler.isEventLoopEnabled = false + Self.isVisible = false + } +} diff --git a/apps/ios/SimpleX SE/SimpleX SE.entitlements b/apps/ios/SimpleX SE/SimpleX SE.entitlements new file mode 100644 index 0000000000..51dea2c806 --- /dev/null +++ b/apps/ios/SimpleX SE/SimpleX SE.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.application-groups + + group.chat.simplex.app + + keychain-access-groups + + $(AppIdentifierPrefix)chat.simplex.app + + + diff --git a/apps/ios/SimpleX SE/bg.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/bg.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/bg.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/bg.lproj/Localizable.strings b/apps/ios/SimpleX SE/bg.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/bg.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/cs.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/cs.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/cs.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/cs.lproj/Localizable.strings b/apps/ios/SimpleX SE/cs.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/cs.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/de.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/de.lproj/InfoPlist.strings new file mode 100644 index 0000000000..48f774742e --- /dev/null +++ b/apps/ios/SimpleX SE/de.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX SE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX SE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2024 SimpleX Chat. Alle Rechte vorbehalten."; + diff --git a/apps/ios/SimpleX SE/de.lproj/Localizable.strings b/apps/ios/SimpleX SE/de.lproj/Localizable.strings new file mode 100644 index 0000000000..081d7f8c66 --- /dev/null +++ b/apps/ios/SimpleX SE/de.lproj/Localizable.strings @@ -0,0 +1,111 @@ +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"App is locked!" = "Die App ist gesperrt!"; + +/* No comment provided by engineer. */ +"Cancel" = "Abbrechen"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Es ist nicht möglich, auf den Schlüsselbund zuzugreifen, um das Datenbankpasswort zu speichern"; + +/* No comment provided by engineer. */ +"Cannot forward message" = "Nachricht kann nicht weitergeleitet werden"; + +/* No comment provided by engineer. */ +"Comment" = "Kommentieren"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "Die maximale erlaubte Dateigröße beträgt aktuell %@."; + +/* No comment provided by engineer. */ +"Database downgrade required" = "Datenbank-Herabstufung erforderlich"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "Datenbank verschlüsselt!"; + +/* No comment provided by engineer. */ +"Database error" = "Datenbankfehler"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "Das Datenbank-Passwort unterscheidet sich vom im Schlüsselbund gespeicherten."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "Ein Datenbank-Passwort ist erforderlich, um den Chat zu öffnen."; + +/* No comment provided by engineer. */ +"Database upgrade required" = "Datenbank-Aktualisierung erforderlich"; + +/* No comment provided by engineer. */ +"Error preparing file" = "Fehler beim Vorbereiten der Datei"; + +/* No comment provided by engineer. */ +"Error preparing message" = "Fehler beim Vorbereiten der Nachricht"; + +/* No comment provided by engineer. */ +"Error: %@" = "Fehler: %@"; + +/* No comment provided by engineer. */ +"File error" = "Dateifehler"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Datenbank-Version nicht kompatibel"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Migrations-Bestätigung ungültig"; + +/* No comment provided by engineer. */ +"Keychain error" = "Schlüsselbund-Fehler"; + +/* No comment provided by engineer. */ +"Large file!" = "Große Datei!"; + +/* No comment provided by engineer. */ +"No active profile" = "Kein aktives Profil"; + +/* No comment provided by engineer. */ +"Ok" = "OK"; + +/* No comment provided by engineer. */ +"Open the app to downgrade the database." = "Öffne die App, um die Datenbank herabzustufen."; + +/* No comment provided by engineer. */ +"Open the app to upgrade the database." = "Öffne die App, um die Datenbank zu aktualisieren."; + +/* No comment provided by engineer. */ +"Passphrase" = "Passwort"; + +/* No comment provided by engineer. */ +"Please create a profile in the SimpleX app" = "Bitte erstelle ein Profil in der SimpleX-App"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Diese Nachricht ist wegen der gewählten Chat-Einstellungen nicht erlaubt."; + +/* No comment provided by engineer. */ +"Sending a message takes longer than expected." = "Das Senden einer Nachricht dauert länger als erwartet."; + +/* No comment provided by engineer. */ +"Sending message…" = "Nachricht wird gesendet…"; + +/* No comment provided by engineer. */ +"Share" = "Teilen"; + +/* No comment provided by engineer. */ +"Slow network?" = "Langsames Netzwerk?"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Unbekannter Datenbankfehler: %@"; + +/* No comment provided by engineer. */ +"Unsupported format" = "Nicht unterstütztes Format"; + +/* No comment provided by engineer. */ +"Wait" = "Warten"; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Falsches Datenbank-Passwort"; + +/* No comment provided by engineer. */ +"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Du kannst das Teilen in den Einstellungen zu Datenschutz & Sicherheit - SimpleX-Sperre erlauben."; + diff --git a/apps/ios/SimpleX SE/en.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/en.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/en.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/en.lproj/Localizable.strings b/apps/ios/SimpleX SE/en.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/en.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/es.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/es.lproj/InfoPlist.strings new file mode 100644 index 0000000000..74bda58efb --- /dev/null +++ b/apps/ios/SimpleX SE/es.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX SE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX SE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2024 SimpleX Chat. Todos los derechos reservados."; + diff --git a/apps/ios/SimpleX SE/es.lproj/Localizable.strings b/apps/ios/SimpleX SE/es.lproj/Localizable.strings new file mode 100644 index 0000000000..5ecdd410df --- /dev/null +++ b/apps/ios/SimpleX SE/es.lproj/Localizable.strings @@ -0,0 +1,111 @@ +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"App is locked!" = "¡Aplicación bloqueada!"; + +/* No comment provided by engineer. */ +"Cancel" = "Cancelar"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Keychain inaccesible para guardar la contraseña de la base de datos"; + +/* No comment provided by engineer. */ +"Cannot forward message" = "No se puede reenviar el mensaje"; + +/* No comment provided by engineer. */ +"Comment" = "Comentario"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "El tamaño máximo de archivo admitido es %@."; + +/* No comment provided by engineer. */ +"Database downgrade required" = "Se requiere volver a versión anterior de la base de datos"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "¡Base de datos cifrada!"; + +/* No comment provided by engineer. */ +"Database error" = "Error en base de datos"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "La contraseña de la base de datos es distinta a la almacenada en keychain."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "Se requiere la contraseña de la base de datos para abrir la aplicación."; + +/* No comment provided by engineer. */ +"Database upgrade required" = "Se requiere actualizar la base de datos"; + +/* No comment provided by engineer. */ +"Error preparing file" = "Error al preparar el archivo"; + +/* No comment provided by engineer. */ +"Error preparing message" = "Error al preparar el mensaje"; + +/* No comment provided by engineer. */ +"Error: %@" = "Error: %@"; + +/* No comment provided by engineer. */ +"File error" = "Error de archivo"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Versión de base de datos incompatible"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Confirmación de migración no válida"; + +/* No comment provided by engineer. */ +"Keychain error" = "Error en keychain"; + +/* No comment provided by engineer. */ +"Large file!" = "¡Archivo grande!"; + +/* No comment provided by engineer. */ +"No active profile" = "Ningún perfil activo"; + +/* No comment provided by engineer. */ +"Ok" = "Ok"; + +/* No comment provided by engineer. */ +"Open the app to downgrade the database." = "Abre la aplicación para volver a versión anterior de la base de datos."; + +/* No comment provided by engineer. */ +"Open the app to upgrade the database." = "Abre la aplicación para actualizar la base de datos."; + +/* No comment provided by engineer. */ +"Passphrase" = "Frase de contraseña"; + +/* No comment provided by engineer. */ +"Please create a profile in the SimpleX app" = "Por favor, crea un perfil en SimpleX"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Las preferencias seleccionadas no permiten este mensaje."; + +/* No comment provided by engineer. */ +"Sending a message takes longer than expected." = "Enviar el mensaje lleva más tiempo del esperado."; + +/* No comment provided by engineer. */ +"Sending message…" = "Enviando mensaje…"; + +/* No comment provided by engineer. */ +"Share" = "Compartir"; + +/* No comment provided by engineer. */ +"Slow network?" = "¿Red lenta?"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Error desconocido en la base de datos: %@"; + +/* No comment provided by engineer. */ +"Unsupported format" = "Formato sin soporte"; + +/* No comment provided by engineer. */ +"Wait" = "Espera"; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Contraseña incorrecta de la base de datos"; + +/* No comment provided by engineer. */ +"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Puedes dar permiso para compartir en Privacidad y Seguridad / Bloque SimpleX."; + diff --git a/apps/ios/SimpleX SE/fi.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/fi.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/fi.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/fi.lproj/Localizable.strings b/apps/ios/SimpleX SE/fi.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/fi.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/fr.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/fr.lproj/InfoPlist.strings new file mode 100644 index 0000000000..4f89e54128 --- /dev/null +++ b/apps/ios/SimpleX SE/fr.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX SE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX SE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2024 SimpleX Chat. Tous droits réservés."; + diff --git a/apps/ios/SimpleX SE/fr.lproj/Localizable.strings b/apps/ios/SimpleX SE/fr.lproj/Localizable.strings new file mode 100644 index 0000000000..46a458b471 --- /dev/null +++ b/apps/ios/SimpleX SE/fr.lproj/Localizable.strings @@ -0,0 +1,111 @@ +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"App is locked!" = "L'app est verrouillée !"; + +/* No comment provided by engineer. */ +"Cancel" = "Annuler"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Impossible d'accéder à la keychain pour enregistrer le mot de passe de la base de données"; + +/* No comment provided by engineer. */ +"Cannot forward message" = "Impossible de transférer le message"; + +/* No comment provided by engineer. */ +"Comment" = "Commenter"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "Actuellement, la taille maximale des fichiers supportés est de %@."; + +/* No comment provided by engineer. */ +"Database downgrade required" = "Mise à jour de la base de données nécessaire"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "Base de données chiffrée !"; + +/* No comment provided by engineer. */ +"Database error" = "Erreur de base de données"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "La phrase secrète de la base de données est différente de celle enregistrée dans la keychain."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "La phrase secrète de la base de données est nécessaire pour ouvrir le chat."; + +/* No comment provided by engineer. */ +"Database upgrade required" = "Mise à niveau de la base de données nécessaire"; + +/* No comment provided by engineer. */ +"Error preparing file" = "Erreur lors de la préparation du fichier"; + +/* No comment provided by engineer. */ +"Error preparing message" = "Erreur lors de la préparation du message"; + +/* No comment provided by engineer. */ +"Error: %@" = "Erreur : %@"; + +/* No comment provided by engineer. */ +"File error" = "Erreur de fichier"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Version de la base de données incompatible"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Confirmation de migration invalide"; + +/* No comment provided by engineer. */ +"Keychain error" = "Erreur de la keychain"; + +/* No comment provided by engineer. */ +"Large file!" = "Fichier trop lourd !"; + +/* No comment provided by engineer. */ +"No active profile" = "Pas de profil actif"; + +/* No comment provided by engineer. */ +"Ok" = "Ok"; + +/* No comment provided by engineer. */ +"Open the app to downgrade the database." = "Ouvrez l'app pour rétrograder la base de données."; + +/* No comment provided by engineer. */ +"Open the app to upgrade the database." = "Ouvrez l'app pour mettre à jour la base de données."; + +/* No comment provided by engineer. */ +"Passphrase" = "Phrase secrète"; + +/* No comment provided by engineer. */ +"Please create a profile in the SimpleX app" = "Veuillez créer un profil dans l'app SimpleX"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Les paramètres de chat sélectionnés ne permettent pas l'envoi de ce message."; + +/* No comment provided by engineer. */ +"Sending a message takes longer than expected." = "L'envoi d'un message prend plus de temps que prévu."; + +/* No comment provided by engineer. */ +"Sending message…" = "Envoi du message…"; + +/* No comment provided by engineer. */ +"Share" = "Partager"; + +/* No comment provided by engineer. */ +"Slow network?" = "Réseau lent ?"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Erreur inconnue de la base de données : %@"; + +/* No comment provided by engineer. */ +"Unsupported format" = "Format non pris en charge"; + +/* No comment provided by engineer. */ +"Wait" = "Attendez"; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Mauvaise phrase secrète pour la base de données"; + +/* No comment provided by engineer. */ +"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Vous pouvez autoriser le partage dans les paramètres Confidentialité et sécurité / SimpleX Lock."; + diff --git a/apps/ios/SimpleX SE/hu.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/hu.lproj/InfoPlist.strings new file mode 100644 index 0000000000..e1979850d1 --- /dev/null +++ b/apps/ios/SimpleX SE/hu.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX SE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX SE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2024 SimpleX Chat. Minden jog fenntartva."; + diff --git a/apps/ios/SimpleX SE/hu.lproj/Localizable.strings b/apps/ios/SimpleX SE/hu.lproj/Localizable.strings new file mode 100644 index 0000000000..8ce4317a9b --- /dev/null +++ b/apps/ios/SimpleX SE/hu.lproj/Localizable.strings @@ -0,0 +1,111 @@ +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"App is locked!" = "Az alkalmazás zárolva!"; + +/* No comment provided by engineer. */ +"Cancel" = "Mégse"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Nem lehet hozzáférni a kulcstartóhoz az adatbázis jelszavának mentéséhez"; + +/* No comment provided by engineer. */ +"Cannot forward message" = "Nem lehet továbbítani az üzenetet"; + +/* No comment provided by engineer. */ +"Comment" = "Hozzászólás"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "Jelenleg a maximális támogatott fájlméret %@."; + +/* No comment provided by engineer. */ +"Database downgrade required" = "Adatbázis visszafejlesztése szükséges"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "Adatbázis titkosítva!"; + +/* No comment provided by engineer. */ +"Database error" = "Adatbázis hiba"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "Az adatbázis jelmondata eltér a kulcstartóban lévőtől."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "Adatbázis jelmondat szükséges a csevegés megnyitásához."; + +/* No comment provided by engineer. */ +"Database upgrade required" = "Adatbázis fejlesztése szükséges"; + +/* No comment provided by engineer. */ +"Error preparing file" = "Hiba a fájl előkészítésekor"; + +/* No comment provided by engineer. */ +"Error preparing message" = "Hiba az üzenet előkészítésekor"; + +/* No comment provided by engineer. */ +"Error: %@" = "Hiba: %@"; + +/* No comment provided by engineer. */ +"File error" = "Fájlhiba"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Nem kompatibilis adatbázis verzió"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Érvénytelen átköltöztetési visszaigazolás"; + +/* No comment provided by engineer. */ +"Keychain error" = "Kulcstartó hiba"; + +/* No comment provided by engineer. */ +"Large file!" = "Nagy fájl!"; + +/* No comment provided by engineer. */ +"No active profile" = "Nincs aktív profil"; + +/* No comment provided by engineer. */ +"Ok" = "Rendben"; + +/* No comment provided by engineer. */ +"Open the app to downgrade the database." = "Nyissa meg az alkalmazást az adatbázis visszafejlesztéséhez."; + +/* No comment provided by engineer. */ +"Open the app to upgrade the database." = "Nyissa meg az alkalmazást az adatbázis fejlesztéséhez."; + +/* No comment provided by engineer. */ +"Passphrase" = "Jelmondat"; + +/* No comment provided by engineer. */ +"Please create a profile in the SimpleX app" = "Hozzon létre egy profilt a SimpleX alkalmazásban"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "A kiválasztott csevegési beállítások tiltják ezt az üzenetet."; + +/* No comment provided by engineer. */ +"Sending a message takes longer than expected." = "Az üzenet elküldése a vártnál tovább tart."; + +/* No comment provided by engineer. */ +"Sending message…" = "Üzenet küldése…"; + +/* No comment provided by engineer. */ +"Share" = "Megosztás"; + +/* No comment provided by engineer. */ +"Slow network?" = "Lassú internetkapcsolat?"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Ismeretlen adatbázis hiba: %@"; + +/* No comment provided by engineer. */ +"Unsupported format" = "Nem támogatott formátum"; + +/* No comment provided by engineer. */ +"Wait" = "Várjon"; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Hibás adatbázis jelmondat"; + +/* No comment provided by engineer. */ +"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "A megosztást az Adatvédelem és biztonság / SimpleX zár menüben engedélyezheti."; + diff --git a/apps/ios/SimpleX SE/it.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/it.lproj/InfoPlist.strings new file mode 100644 index 0000000000..78145285c2 --- /dev/null +++ b/apps/ios/SimpleX SE/it.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX SE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX SE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2024 SimpleX Chat. Tutti i diritti riservati."; + diff --git a/apps/ios/SimpleX SE/it.lproj/Localizable.strings b/apps/ios/SimpleX SE/it.lproj/Localizable.strings new file mode 100644 index 0000000000..e3d34650a3 --- /dev/null +++ b/apps/ios/SimpleX SE/it.lproj/Localizable.strings @@ -0,0 +1,111 @@ +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"App is locked!" = "L'app è bloccata!"; + +/* No comment provided by engineer. */ +"Cancel" = "Annulla"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Impossibile accedere al portachiavi per salvare la password del database"; + +/* No comment provided by engineer. */ +"Cannot forward message" = "Impossibile inoltrare il messaggio"; + +/* No comment provided by engineer. */ +"Comment" = "Commento"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "Attualmente la dimensione massima supportata è di %@."; + +/* No comment provided by engineer. */ +"Database downgrade required" = "Downgrade del database necessario"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "Database crittografato!"; + +/* No comment provided by engineer. */ +"Database error" = "Errore del database"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "La password del database è diversa da quella salvata nel portachiavi."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "La password del database è necessaria per aprire la chat."; + +/* No comment provided by engineer. */ +"Database upgrade required" = "Aggiornamento del database necessario"; + +/* No comment provided by engineer. */ +"Error preparing file" = "Errore nella preparazione del file"; + +/* No comment provided by engineer. */ +"Error preparing message" = "Errore nella preparazione del messaggio"; + +/* No comment provided by engineer. */ +"Error: %@" = "Errore: %@"; + +/* No comment provided by engineer. */ +"File error" = "Errore del file"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Versione del database incompatibile"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Conferma di migrazione non valida"; + +/* No comment provided by engineer. */ +"Keychain error" = "Errore del portachiavi"; + +/* No comment provided by engineer. */ +"Large file!" = "File grande!"; + +/* No comment provided by engineer. */ +"No active profile" = "Nessun profilo attivo"; + +/* No comment provided by engineer. */ +"Ok" = "Ok"; + +/* No comment provided by engineer. */ +"Open the app to downgrade the database." = "Apri l'app per eseguire il downgrade del database."; + +/* No comment provided by engineer. */ +"Open the app to upgrade the database." = "Apri l'app per aggiornare il database."; + +/* No comment provided by engineer. */ +"Passphrase" = "Password"; + +/* No comment provided by engineer. */ +"Please create a profile in the SimpleX app" = "Crea un profilo nell'app SimpleX"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Le preferenze della chat selezionata vietano questo messaggio."; + +/* No comment provided by engineer. */ +"Sending a message takes longer than expected." = "L'invio di un messaggio richiede più tempo del previsto."; + +/* No comment provided by engineer. */ +"Sending message…" = "Invio messaggio…"; + +/* No comment provided by engineer. */ +"Share" = "Condividi"; + +/* No comment provided by engineer. */ +"Slow network?" = "Rete lenta?"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Errore del database sconosciuto: %@"; + +/* No comment provided by engineer. */ +"Unsupported format" = "Formato non supportato"; + +/* No comment provided by engineer. */ +"Wait" = "Attendi"; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Password del database sbagliata"; + +/* No comment provided by engineer. */ +"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Puoi consentire la condivisione in Privacy e sicurezza / impostazioni di SimpleX Lock."; + diff --git a/apps/ios/SimpleX SE/ja.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/ja.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/ja.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/ja.lproj/Localizable.strings b/apps/ios/SimpleX SE/ja.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/ja.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/nl.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/nl.lproj/InfoPlist.strings new file mode 100644 index 0000000000..c61e43a87f --- /dev/null +++ b/apps/ios/SimpleX SE/nl.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX SE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX SE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2024 SimpleX Chat. All rights reserved."; + diff --git a/apps/ios/SimpleX SE/nl.lproj/Localizable.strings b/apps/ios/SimpleX SE/nl.lproj/Localizable.strings new file mode 100644 index 0000000000..8412c88ea6 --- /dev/null +++ b/apps/ios/SimpleX SE/nl.lproj/Localizable.strings @@ -0,0 +1,111 @@ +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"App is locked!" = "App is vergrendeld!"; + +/* No comment provided by engineer. */ +"Cancel" = "Annuleren"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Kan geen toegang krijgen tot de keychain om het database wachtwoord op te slaan"; + +/* No comment provided by engineer. */ +"Cannot forward message" = "Kan bericht niet doorsturen"; + +/* No comment provided by engineer. */ +"Comment" = "Opmerking"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "De momenteel maximaal ondersteunde bestandsgrootte is %@."; + +/* No comment provided by engineer. */ +"Database downgrade required" = "Database downgrade vereist"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "Database versleuteld!"; + +/* No comment provided by engineer. */ +"Database error" = "Database fout"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "Het wachtwoord van de database verschilt van het wachtwoord die in de keychain is opgeslagen."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "Database wachtwoord is vereist om je gesprekken te openen."; + +/* No comment provided by engineer. */ +"Database upgrade required" = "Database upgrade vereist"; + +/* No comment provided by engineer. */ +"Error preparing file" = "Fout bij voorbereiden bestand"; + +/* No comment provided by engineer. */ +"Error preparing message" = "Fout bij het voorbereiden van bericht"; + +/* No comment provided by engineer. */ +"Error: %@" = "Fout: %@"; + +/* No comment provided by engineer. */ +"File error" = "Bestandsfout"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Incompatibele database versie"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Ongeldige migratie bevestiging"; + +/* No comment provided by engineer. */ +"Keychain error" = "Keychain fout"; + +/* No comment provided by engineer. */ +"Large file!" = "Groot bestand!"; + +/* No comment provided by engineer. */ +"No active profile" = "Geen actief profiel"; + +/* No comment provided by engineer. */ +"Ok" = "Ok"; + +/* No comment provided by engineer. */ +"Open the app to downgrade the database." = "Open de app om de database te downgraden."; + +/* No comment provided by engineer. */ +"Open the app to upgrade the database." = "Open de app om de database te upgraden."; + +/* No comment provided by engineer. */ +"Passphrase" = "Wachtwoord"; + +/* No comment provided by engineer. */ +"Please create a profile in the SimpleX app" = "Maak een profiel aan in de SimpleX app"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Geselecteerde chat voorkeuren verbieden dit bericht."; + +/* No comment provided by engineer. */ +"Sending a message takes longer than expected." = "Het verzenden van een bericht duurt langer dan verwacht."; + +/* No comment provided by engineer. */ +"Sending message…" = "Bericht versturen…"; + +/* No comment provided by engineer. */ +"Share" = "Deel"; + +/* No comment provided by engineer. */ +"Slow network?" = "Traag netwerk?"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Onbekende database fout: %@"; + +/* No comment provided by engineer. */ +"Unsupported format" = "Niet ondersteund formaat"; + +/* No comment provided by engineer. */ +"Wait" = "wachten"; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Verkeerde database wachtwoord"; + +/* No comment provided by engineer. */ +"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "U kunt delen toestaan in de instellingen voor Privacy en beveiliging / SimpleX Lock."; + diff --git a/apps/ios/SimpleX SE/pl.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/pl.lproj/InfoPlist.strings new file mode 100644 index 0000000000..81283a3f02 --- /dev/null +++ b/apps/ios/SimpleX SE/pl.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX SE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX SE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2024 SimpleX Chat. Wszelkie prawa zastrzeżone."; + diff --git a/apps/ios/SimpleX SE/pl.lproj/Localizable.strings b/apps/ios/SimpleX SE/pl.lproj/Localizable.strings new file mode 100644 index 0000000000..c563431c28 --- /dev/null +++ b/apps/ios/SimpleX SE/pl.lproj/Localizable.strings @@ -0,0 +1,111 @@ +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"App is locked!" = "Aplikacja zablokowana!"; + +/* No comment provided by engineer. */ +"Cancel" = "Anuluj"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Nie można uzyskać dostępu do pęku kluczy aby zapisać hasło do bazy danych"; + +/* No comment provided by engineer. */ +"Cannot forward message" = "Nie można przekazać wiadomości"; + +/* No comment provided by engineer. */ +"Comment" = "Komentarz"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "Obecnie maksymalny obsługiwany rozmiar pliku to %@."; + +/* No comment provided by engineer. */ +"Database downgrade required" = "Wymagane obniżenie wersji bazy danych"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "Baza danych zaszyfrowana!"; + +/* No comment provided by engineer. */ +"Database error" = "Błąd bazy danych"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "Hasło bazy danych jest inne niż zapisane w pęku kluczy."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "Hasło do bazy danych jest wymagane do otwarcia czatu."; + +/* No comment provided by engineer. */ +"Database upgrade required" = "Wymagana aktualizacja bazy danych"; + +/* No comment provided by engineer. */ +"Error preparing file" = "Błąd przygotowania pliku"; + +/* No comment provided by engineer. */ +"Error preparing message" = "Błąd przygotowania wiadomości"; + +/* No comment provided by engineer. */ +"Error: %@" = "Błąd: %@"; + +/* No comment provided by engineer. */ +"File error" = "Błąd pliku"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Niekompatybilna wersja bazy danych"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Nieprawidłowe potwierdzenie migracji"; + +/* No comment provided by engineer. */ +"Keychain error" = "Błąd pęku kluczy"; + +/* No comment provided by engineer. */ +"Large file!" = "Duży plik!"; + +/* No comment provided by engineer. */ +"No active profile" = "Brak aktywnego profilu"; + +/* No comment provided by engineer. */ +"Ok" = "Ok"; + +/* No comment provided by engineer. */ +"Open the app to downgrade the database." = "Otwórz aplikację aby obniżyć wersję bazy danych."; + +/* No comment provided by engineer. */ +"Open the app to upgrade the database." = "Otwórz aplikację aby zaktualizować bazę danych."; + +/* No comment provided by engineer. */ +"Passphrase" = "Hasło"; + +/* No comment provided by engineer. */ +"Please create a profile in the SimpleX app" = "Proszę utworzyć profil w aplikacji SimpleX"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Wybrane preferencje czatu zabraniają tej wiadomości."; + +/* No comment provided by engineer. */ +"Sending a message takes longer than expected." = "Wysłanie wiadomości trwa dłużej niż oczekiwano."; + +/* No comment provided by engineer. */ +"Sending message…" = "Wysyłanie wiadomości…"; + +/* No comment provided by engineer. */ +"Share" = "Udostępnij"; + +/* No comment provided by engineer. */ +"Slow network?" = "Wolna sieć?"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Nieznany błąd bazy danych: %@"; + +/* No comment provided by engineer. */ +"Unsupported format" = "Niewspierany format"; + +/* No comment provided by engineer. */ +"Wait" = "Czekaj"; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Nieprawidłowe hasło bazy danych"; + +/* No comment provided by engineer. */ +"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Możesz zezwolić na udostępnianie w ustawieniach Prywatność i bezpieczeństwo / Blokada SimpleX."; + diff --git a/apps/ios/SimpleX SE/ru.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/ru.lproj/InfoPlist.strings new file mode 100644 index 0000000000..d45b3d735d --- /dev/null +++ b/apps/ios/SimpleX SE/ru.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX SE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX SE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2024 SimpleX Chat. Все права защищены."; + diff --git a/apps/ios/SimpleX SE/ru.lproj/Localizable.strings b/apps/ios/SimpleX SE/ru.lproj/Localizable.strings new file mode 100644 index 0000000000..0841e8e47f --- /dev/null +++ b/apps/ios/SimpleX SE/ru.lproj/Localizable.strings @@ -0,0 +1,111 @@ +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"App is locked!" = "Приложение заблокировано!"; + +/* No comment provided by engineer. */ +"Cancel" = "Отменить"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Невозможно сохранить пароль в keychain"; + +/* No comment provided by engineer. */ +"Cannot forward message" = "Невозможно переслать сообщение"; + +/* No comment provided by engineer. */ +"Comment" = "Комментарий"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "В настоящее время максимальный поддерживаемый размер файла составляет %@."; + +/* No comment provided by engineer. */ +"Database downgrade required" = "Требуется откат базы данных"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "База данных зашифрована!"; + +/* No comment provided by engineer. */ +"Database error" = "Ошибка базы данных"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "Пароль базы данных отличается от сохраненного в keychain."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "Введите пароль базы данных, чтобы открыть чат."; + +/* No comment provided by engineer. */ +"Database upgrade required" = "Требуется обновление базы данных"; + +/* No comment provided by engineer. */ +"Error preparing file" = "Ошибка подготовки файла"; + +/* No comment provided by engineer. */ +"Error preparing message" = "Ошибка подготовки сообщения"; + +/* No comment provided by engineer. */ +"Error: %@" = "Ошибка: %@"; + +/* No comment provided by engineer. */ +"File error" = "Ошибка файла"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Несовместимая версия базы данных"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Ошибка подтверждения миграции"; + +/* No comment provided by engineer. */ +"Keychain error" = "Ошибка keychain"; + +/* No comment provided by engineer. */ +"Large file!" = "Большой файл!"; + +/* No comment provided by engineer. */ +"No active profile" = "Нет активного профиля"; + +/* No comment provided by engineer. */ +"Ok" = "Ок"; + +/* No comment provided by engineer. */ +"Open the app to downgrade the database." = "Откройте приложение, чтобы откатить базу данных."; + +/* No comment provided by engineer. */ +"Open the app to upgrade the database." = "Откройте приложение, чтобы обновить базу данных."; + +/* No comment provided by engineer. */ +"Passphrase" = "Пароль"; + +/* No comment provided by engineer. */ +"Please create a profile in the SimpleX app" = "Пожалуйста, создайте профиль в приложении SimpleX."; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Выбранные настройки чата запрещают это сообщение."; + +/* No comment provided by engineer. */ +"Sending a message takes longer than expected." = "Отправка сообщения занимает дольше ожиданного."; + +/* No comment provided by engineer. */ +"Sending message…" = "Отправка сообщения…"; + +/* No comment provided by engineer. */ +"Share" = "Поделиться"; + +/* No comment provided by engineer. */ +"Slow network?" = "Медленная сеть?"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Неизвестная ошибка базы данных: %@"; + +/* No comment provided by engineer. */ +"Unsupported format" = "Неподдерживаемый формат"; + +/* No comment provided by engineer. */ +"Wait" = "Подождать"; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Неправильный пароль базы данных"; + +/* No comment provided by engineer. */ +"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Вы можете разрешить функцию Поделиться в настройках Конфиденциальности / Блокировка SimpleX."; + diff --git a/apps/ios/SimpleX SE/th.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/th.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/th.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/th.lproj/Localizable.strings b/apps/ios/SimpleX SE/th.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/th.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/tr.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/tr.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/tr.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/tr.lproj/Localizable.strings b/apps/ios/SimpleX SE/tr.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/tr.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/uk.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/uk.lproj/InfoPlist.strings new file mode 100644 index 0000000000..18c4d5e8a5 --- /dev/null +++ b/apps/ios/SimpleX SE/uk.lproj/InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle display name */ +"CFBundleDisplayName" = "SimpleX SE"; + +/* Bundle name */ +"CFBundleName" = "SimpleX SE"; + +/* Copyright (human-readable) */ +"NSHumanReadableCopyright" = "Copyright © 2024 SimpleX Chat. Всі права захищені."; + diff --git a/apps/ios/SimpleX SE/uk.lproj/Localizable.strings b/apps/ios/SimpleX SE/uk.lproj/Localizable.strings new file mode 100644 index 0000000000..a6da81185e --- /dev/null +++ b/apps/ios/SimpleX SE/uk.lproj/Localizable.strings @@ -0,0 +1,111 @@ +/* No comment provided by engineer. */ +"%@" = "%@"; + +/* No comment provided by engineer. */ +"App is locked!" = "Додаток заблоковано!"; + +/* No comment provided by engineer. */ +"Cancel" = "Скасувати"; + +/* No comment provided by engineer. */ +"Cannot access keychain to save database password" = "Не вдається отримати доступ до зв'язки ключів для збереження пароля до бази даних"; + +/* No comment provided by engineer. */ +"Cannot forward message" = "Неможливо переслати повідомлення"; + +/* No comment provided by engineer. */ +"Comment" = "Коментар"; + +/* No comment provided by engineer. */ +"Currently maximum supported file size is %@." = "Наразі максимальний підтримуваний розмір файлу - %@."; + +/* No comment provided by engineer. */ +"Database downgrade required" = "Потрібне оновлення бази даних"; + +/* No comment provided by engineer. */ +"Database encrypted!" = "База даних зашифрована!"; + +/* No comment provided by engineer. */ +"Database error" = "Помилка в базі даних"; + +/* No comment provided by engineer. */ +"Database passphrase is different from saved in the keychain." = "Парольна фраза бази даних відрізняється від збереженої у в’язці ключів."; + +/* No comment provided by engineer. */ +"Database passphrase is required to open chat." = "Для відкриття чату потрібно ввести пароль до бази даних."; + +/* No comment provided by engineer. */ +"Database upgrade required" = "Потрібне оновлення бази даних"; + +/* No comment provided by engineer. */ +"Error preparing file" = "Помилка підготовки файлу"; + +/* No comment provided by engineer. */ +"Error preparing message" = "Повідомлення про підготовку до помилки"; + +/* No comment provided by engineer. */ +"Error: %@" = "Помилка: %@"; + +/* No comment provided by engineer. */ +"File error" = "Помилка файлу"; + +/* No comment provided by engineer. */ +"Incompatible database version" = "Несумісна версія бази даних"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "Недійсне підтвердження міграції"; + +/* No comment provided by engineer. */ +"Keychain error" = "Помилка зв'язки ключів"; + +/* No comment provided by engineer. */ +"Large file!" = "Великий файл!"; + +/* No comment provided by engineer. */ +"No active profile" = "Немає активного профілю"; + +/* No comment provided by engineer. */ +"Ok" = "Гаразд"; + +/* No comment provided by engineer. */ +"Open the app to downgrade the database." = "Відкрийте програму, щоб знизити версію бази даних."; + +/* No comment provided by engineer. */ +"Open the app to upgrade the database." = "Відкрийте програму, щоб оновити базу даних."; + +/* No comment provided by engineer. */ +"Passphrase" = "Парольна фраза"; + +/* No comment provided by engineer. */ +"Please create a profile in the SimpleX app" = "Будь ласка, створіть профіль у додатку SimpleX"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Вибрані налаштування чату забороняють це повідомлення."; + +/* No comment provided by engineer. */ +"Sending a message takes longer than expected." = "Надсилання повідомлення займає більше часу, ніж очікувалося."; + +/* No comment provided by engineer. */ +"Sending message…" = "Надсилаю повідомлення…"; + +/* No comment provided by engineer. */ +"Share" = "Поділіться"; + +/* No comment provided by engineer. */ +"Slow network?" = "Повільна мережа?"; + +/* No comment provided by engineer. */ +"Unknown database error: %@" = "Невідома помилка бази даних: %@"; + +/* No comment provided by engineer. */ +"Unsupported format" = "Непідтримуваний формат"; + +/* No comment provided by engineer. */ +"Wait" = "Зачекай"; + +/* No comment provided by engineer. */ +"Wrong database passphrase" = "Неправильна ключова фраза до бази даних"; + +/* No comment provided by engineer. */ +"You can allow sharing in Privacy & Security / SimpleX Lock settings." = "Ви можете дозволити спільний доступ у налаштуваннях Конфіденційність і безпека / SimpleX Lock."; + diff --git a/apps/ios/SimpleX SE/zh-Hans.lproj/InfoPlist.strings b/apps/ios/SimpleX SE/zh-Hans.lproj/InfoPlist.strings new file mode 100644 index 0000000000..388ac01f7f --- /dev/null +++ b/apps/ios/SimpleX SE/zh-Hans.lproj/InfoPlist.strings @@ -0,0 +1,7 @@ +/* + InfoPlist.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX SE/zh-Hans.lproj/Localizable.strings b/apps/ios/SimpleX SE/zh-Hans.lproj/Localizable.strings new file mode 100644 index 0000000000..5ef592ec70 --- /dev/null +++ b/apps/ios/SimpleX SE/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,7 @@ +/* + Localizable.strings + SimpleX + + Created by EP on 30/07/2024. + Copyright © 2024 SimpleX Chat. All rights reserved. +*/ diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 9a15ef2f8a..b59b1aeab3 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -18,17 +18,11 @@ 18415FEFE153C5920BFB7828 /* GroupWelcomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1841516F0CE5992B0EDFB377 /* GroupWelcomeView.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 */; }; 5C00168128C4FE760094D739 /* KeyChain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C00168028C4FE760094D739 /* KeyChain.swift */; }; 5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA72837DBB3004A9677 /* CICallItemView.swift */; }; 5C029EAA283942EA004A9677 /* CallController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C029EA9283942EA004A9677 /* CallController.swift */; }; 5C05DF532840AA1D00C683F9 /* CallSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C05DF522840AA1D00C683F9 /* CallSettings.swift */; }; 5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */; }; - 5C0EA1312C0B05C000AD2E5E /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0EA12C2C0B05C000AD2E5E /* libgmp.a */; }; - 5C0EA1322C0B05C000AD2E5E /* libHSsimplex-chat-5.7.5.0-IiqaTGbIKN89ePlb2e8Hnq-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0EA12D2C0B05C000AD2E5E /* libHSsimplex-chat-5.7.5.0-IiqaTGbIKN89ePlb2e8Hnq-ghc9.6.3.a */; }; - 5C0EA1332C0B05C000AD2E5E /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0EA12E2C0B05C000AD2E5E /* libffi.a */; }; - 5C0EA1342C0B05C000AD2E5E /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0EA12F2C0B05C000AD2E5E /* libgmpxx.a */; }; - 5C0EA1352C0B05C000AD2E5E /* libHSsimplex-chat-5.7.5.0-IiqaTGbIKN89ePlb2e8Hnq.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C0EA1302C0B05C000AD2E5E /* libHSsimplex-chat-5.7.5.0-IiqaTGbIKN89ePlb2e8Hnq.a */; }; 5C10D88828EED12E00E58BF0 /* ContactConnectionInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */; }; 5C10D88A28F187F300E58BF0 /* FullScreenMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C10D88928F187F300E58BF0 /* FullScreenMediaView.swift */; }; 5C116CDC27AABE0400E66D01 /* ContactRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */; }; @@ -106,7 +100,6 @@ 5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924D627A8563F00ACCCDD /* SettingsView.swift */; }; 5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB924E027A867BA00ACCCDD /* UserProfile.swift */; }; 5CB9250D27A9432000ACCCDD /* ChatListNavLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CB9250C27A9432000ACCCDD /* ChatListNavLink.swift */; }; - 5CBD285A295711D700EC2CF4 /* ImageUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CBD2859295711D700EC2CF4 /* ImageUtils.swift */; }; 5CBD285C29575B8E00EC2CF4 /* WhatsNewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CBD285B29575B8E00EC2CF4 /* WhatsNewView.swift */; }; 5CBE6C12294487F7002D9531 /* VerifyCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CBE6C11294487F7002D9531 /* VerifyCodeView.swift */; }; 5CBE6C142944CC12002D9531 /* ScanCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CBE6C132944CC12002D9531 /* ScanCodeView.swift */; }; @@ -142,7 +135,6 @@ 5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CE4407827ADB701007B033A /* EmojiItemView.swift */; }; 5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CEACCE227DE9246000BD591 /* ComposeView.swift */; }; 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 */; }; 5CF937202B24DE8C00E1D781 /* SharedFileSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */; }; 5CF937232B2503D000E1D781 /* NSESubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF937212B25034A00E1D781 /* NSESubscriber.swift */; }; @@ -183,20 +175,54 @@ 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */; }; 64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */; }; 64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; }; + 64EEB0F72C353F1C00972D62 /* ServersSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */; }; 64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; }; - 8C05382E2B39887E006436DC /* VideoUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C05382D2B39887E006436DC /* VideoUtils.swift */; }; 8C69FE7D2B8C7D2700267E38 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */; }; + 8C74C3E52C1B900600039E77 /* ThemeTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7E3CE32C0DEAC400BFF63A /* ThemeTypes.swift */; }; + 8C74C3E72C1B901900039E77 /* Color.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C852B072C1086D100BA61E8 /* Color.swift */; }; + 8C74C3E82C1B905B00039E77 /* ChatWallpaperTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C804B1D2C11F966007A63C8 /* ChatWallpaperTypes.swift */; }; + 8C74C3EA2C1B90AF00039E77 /* ThemeManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C86EBE42C0DAE4F00E12243 /* ThemeManager.swift */; }; + 8C74C3EC2C1B92A900039E77 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C74C3EB2C1B92A900039E77 /* Theme.swift */; }; + 8C74C3EE2C1B942300039E77 /* ChatWallpaper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C74C3ED2C1B942300039E77 /* ChatWallpaper.swift */; }; 8C7D949A2B88952700B7B9E1 /* MigrateToDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7D94992B88952700B7B9E1 /* MigrateToDevice.swift */; }; 8C7DF3202B7CDB0A00C886D0 /* MigrateFromDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7DF31F2B7CDB0A00C886D0 /* MigrateFromDevice.swift */; }; + 8C7F8F0E2C19C0C100D16888 /* ViewModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7F8F0D2C19C0C100D16888 /* ViewModifiers.swift */; }; + 8C8118722C220B5B00E6FC94 /* Yams in Frameworks */ = {isa = PBXBuildFile; productRef = 8C8118712C220B5B00E6FC94 /* Yams */; }; 8C81482C2BD91CD4002CBEC3 /* AudioDevicePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C81482B2BD91CD4002CBEC3 /* AudioDevicePicker.swift */; }; + 8C9BC2652C240D5200875A27 /* ThemeModeEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */; }; 8CC4ED902BD7B8530078AEE8 /* CallAudioDeviceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */; }; 8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; }; + 8CE848A32C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */; }; + B76E6C312C5C41D900EC11AA /* ContactListNavLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */; }; + CE1EB0E42C459A660099D896 /* ShareAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1EB0E32C459A660099D896 /* ShareAPI.swift */; }; + CE2AD9CE2C452A4D00E844E3 /* ChatUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */; }; + CE3097FB2C4C0C9F00180898 /* ErrorAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */; }; + CE38A29A2C3FCA54005ED185 /* ImageUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CBD2859295711D700EC2CF4 /* ImageUtils.swift */; }; + CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = CE38A29B2C3FCD72005ED185 /* SwiftyGif */; }; + CE75480A2C622630009579B7 /* SwipeLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7548092C622630009579B7 /* SwipeLabel.swift */; }; + CE984D4B2C36C5D500E3AEFF /* ChatItemClipShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */; }; + CEDE70222C48FD9500233B1F /* SEChatState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEDE70212C48FD9500233B1F /* SEChatState.swift */; }; + CEE723AA2C3BD3D70009AE93 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE723A92C3BD3D70009AE93 /* ShareViewController.swift */; }; + CEE723B12C3BD3D70009AE93 /* SimpleX SE.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + CEE723F02C3D25C70009AE93 /* ShareView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE723EF2C3D25C70009AE93 /* ShareView.swift */; }; + CEE723F22C3D25ED0009AE93 /* ShareModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE723F12C3D25ED0009AE93 /* ShareModel.swift */; }; + CEEA861D2C2ABCB50084E1EA /* ReverseList.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEEA861C2C2ABCB50084E1EA /* ReverseList.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 */; }; D741547A29AF90B00022400A /* PushKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547929AF90B00022400A /* PushKit.framework */; }; D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; }; D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; }; + E51CC1E62C62085600DB91FE /* OneHandUICard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51CC1E52C62085600DB91FE /* OneHandUICard.swift */; }; + E51ED5762C7691A2009F2C7C /* libHSsimplex-chat-6.0.2.0-CUdJmkNQ3mRF4AChEwYJvy.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED5712C7691A2009F2C7C /* libHSsimplex-chat-6.0.2.0-CUdJmkNQ3mRF4AChEwYJvy.a */; }; + E51ED5772C7691A2009F2C7C /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED5722C7691A2009F2C7C /* libgmp.a */; }; + E51ED5782C7691A2009F2C7C /* libHSsimplex-chat-6.0.2.0-CUdJmkNQ3mRF4AChEwYJvy-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED5732C7691A2009F2C7C /* libHSsimplex-chat-6.0.2.0-CUdJmkNQ3mRF4AChEwYJvy-ghc9.6.3.a */; }; + E51ED5792C7691A2009F2C7C /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED5742C7691A2009F2C7C /* libgmpxx.a */; }; + E51ED57A2C7691A2009F2C7C /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E51ED5752C7691A2009F2C7C /* libffi.a */; }; + E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; }; + E5DCF9712C590272007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF96F2C590272007928CC /* Localizable.strings */; }; + E5DCF9842C5902CE007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF9822C5902CE007928CC /* Localizable.strings */; }; + E5DCF9982C5906FF007928CC /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF9962C5906FF007928CC /* InfoPlist.strings */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -228,6 +254,20 @@ remoteGlobalIDString = 5CE2BA672845308900EC33A6; remoteInfo = SimpleXChat; }; + CEE723AF2C3BD3D70009AE93 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5CA059BE279559F40002BEB4 /* Project object */; + proxyType = 1; + remoteGlobalIDString = CEE723A62C3BD3D70009AE93; + remoteInfo = "SimpleX SE"; + }; + CEE723D12C3C21C90009AE93 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 5CA059BE279559F40002BEB4 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 5CE2BA672845308900EC33A6; + remoteInfo = SimpleXChat; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -248,6 +288,7 @@ dstPath = ""; dstSubfolderSpec = 13; files = ( + CEE723B12C3BD3D70009AE93 /* SimpleX SE.appex in Embed App Extensions */, 5CE2BA9D284555F500EC33A6 /* SimpleX NSE.appex in Embed App Extensions */, ); name = "Embed App Extensions"; @@ -267,17 +308,11 @@ 18415FD2E36F13F596A45BB4 /* CIVideoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CIVideoView.swift; sourceTree = ""; }; 3CDBCF4127FAE51000354CDD /* ComposeLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeLinkView.swift; sourceTree = ""; }; 3CDBCF4727FF621E00354CDD /* CILinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CILinkView.swift; sourceTree = ""; }; - 5C00164328A26FBC0094D739 /* ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = ""; }; 5C00168028C4FE760094D739 /* KeyChain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyChain.swift; sourceTree = ""; }; 5C029EA72837DBB3004A9677 /* CICallItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CICallItemView.swift; sourceTree = ""; }; 5C029EA9283942EA004A9677 /* CallController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallController.swift; sourceTree = ""; }; 5C05DF522840AA1D00C683F9 /* CallSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallSettings.swift; sourceTree = ""; }; 5C063D2627A4564100AEC577 /* ChatPreviewView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatPreviewView.swift; sourceTree = ""; }; - 5C0EA12C2C0B05C000AD2E5E /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - 5C0EA12D2C0B05C000AD2E5E /* libHSsimplex-chat-5.7.5.0-IiqaTGbIKN89ePlb2e8Hnq-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.5.0-IiqaTGbIKN89ePlb2e8Hnq-ghc9.6.3.a"; sourceTree = ""; }; - 5C0EA12E2C0B05C000AD2E5E /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - 5C0EA12F2C0B05C000AD2E5E /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; - 5C0EA1302C0B05C000AD2E5E /* libHSsimplex-chat-5.7.5.0-IiqaTGbIKN89ePlb2e8Hnq.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.7.5.0-IiqaTGbIKN89ePlb2e8Hnq.a"; sourceTree = ""; }; 5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactConnectionInfo.swift; sourceTree = ""; }; 5C10D88928F187F300E58BF0 /* FullScreenMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenMediaView.swift; sourceTree = ""; }; 5C116CDB27AABE0400E66D01 /* ContactRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRequestView.swift; sourceTree = ""; }; @@ -438,7 +473,6 @@ 5CE6C7B42AAB1527007F345C /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; 5CEACCE227DE9246000BD591 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = ""; }; 5CEACCEC27DEA495000BD591 /* MsgContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MsgContentView.swift; sourceTree = ""; }; - 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardPadding.swift; sourceTree = ""; }; 5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetDeliveryReceiptsView.swift; sourceTree = ""; }; 5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedFileSubscriber.swift; sourceTree = ""; }; 5CF937212B25034A00E1D781 /* NSESubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSESubscriber.swift; sourceTree = ""; }; @@ -480,18 +514,99 @@ 64D0C2C529FAC1EC00B38D5F /* AddContactLearnMore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddContactLearnMore.swift; sourceTree = ""; }; 64DAE1502809D9F5000DA960 /* FileUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUtils.swift; sourceTree = ""; }; 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = ""; }; + 64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersSummaryView.swift; sourceTree = ""; }; 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = ""; }; - 8C05382D2B39887E006436DC /* VideoUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoUtils.swift; sourceTree = ""; }; 8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = ""; }; + 8C74C3EB2C1B92A900039E77 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; + 8C74C3ED2C1B942300039E77 /* ChatWallpaper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatWallpaper.swift; sourceTree = ""; }; 8C7D94992B88952700B7B9E1 /* MigrateToDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToDevice.swift; sourceTree = ""; }; 8C7DF31F2B7CDB0A00C886D0 /* MigrateFromDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateFromDevice.swift; sourceTree = ""; }; + 8C7E3CE32C0DEAC400BFF63A /* ThemeTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeTypes.swift; sourceTree = ""; }; + 8C7F8F0D2C19C0C100D16888 /* ViewModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModifiers.swift; sourceTree = ""; }; + 8C804B1D2C11F966007A63C8 /* ChatWallpaperTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatWallpaperTypes.swift; sourceTree = ""; }; 8C81482B2BD91CD4002CBEC3 /* AudioDevicePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioDevicePicker.swift; sourceTree = ""; }; + 8C852B072C1086D100BA61E8 /* Color.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Color.swift; sourceTree = ""; }; + 8C86EBE42C0DAE4F00E12243 /* ThemeManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeManager.swift; sourceTree = ""; }; + 8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeModeEditor.swift; sourceTree = ""; }; 8CC4ED8F2BD7B8530078AEE8 /* CallAudioDeviceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CallAudioDeviceManager.swift; sourceTree = ""; }; 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = ""; }; + 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableChatItemToolbars.swift; sourceTree = ""; }; + B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListNavLink.swift; sourceTree = ""; }; + CE1EB0E32C459A660099D896 /* ShareAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAPI.swift; sourceTree = ""; }; + CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatUtils.swift; sourceTree = ""; }; + CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlert.swift; sourceTree = ""; }; + CE7548092C622630009579B7 /* SwipeLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwipeLabel.swift; sourceTree = ""; }; + CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemClipShape.swift; sourceTree = ""; }; + CEDE70212C48FD9500233B1F /* SEChatState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SEChatState.swift; sourceTree = ""; }; + CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "SimpleX SE.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; + CEE723A92C3BD3D70009AE93 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; + CEE723AE2C3BD3D70009AE93 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CEE723D42C3C21F50009AE93 /* SimpleX SE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "SimpleX SE.entitlements"; sourceTree = ""; }; + CEE723EF2C3D25C70009AE93 /* ShareView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareView.swift; sourceTree = ""; }; + CEE723F12C3D25ED0009AE93 /* ShareModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareModel.swift; sourceTree = ""; }; + CEEA861C2C2ABCB50084E1EA /* ReverseList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReverseList.swift; sourceTree = ""; }; D72A9087294BD7A70047C86D /* NativeTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTextEditor.swift; sourceTree = ""; }; D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; }; D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; }; D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + E51CC1E52C62085600DB91FE /* OneHandUICard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneHandUICard.swift; sourceTree = ""; }; + E51ED5712C7691A2009F2C7C /* libHSsimplex-chat-6.0.2.0-CUdJmkNQ3mRF4AChEwYJvy.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.2.0-CUdJmkNQ3mRF4AChEwYJvy.a"; sourceTree = ""; }; + E51ED5722C7691A2009F2C7C /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + E51ED5732C7691A2009F2C7C /* libHSsimplex-chat-6.0.2.0-CUdJmkNQ3mRF4AChEwYJvy-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.2.0-CUdJmkNQ3mRF4AChEwYJvy-ghc9.6.3.a"; sourceTree = ""; }; + E51ED5742C7691A2009F2C7C /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + E51ED5752C7691A2009F2C7C /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + E5DCF9702C590272007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9722C590274007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9732C590275007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; + E5DCF9742C590276007928CC /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9752C590277007928CC /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9762C590278007928CC /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9772C590279007928CC /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9782C590279007928CC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9792C59027A007928CC /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF97A2C59027A007928CC /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF97B2C59027B007928CC /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF97C2C59027B007928CC /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF97D2C59027C007928CC /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF97E2C59027C007928CC /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF97F2C59027D007928CC /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9802C59027D007928CC /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9812C59027D007928CC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9832C5902CE007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9852C5902D4007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9862C5902D5007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; + E5DCF9872C5902D8007928CC /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9882C5902DC007928CC /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9892C5902DC007928CC /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF98A2C5902DD007928CC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF98B2C5902DD007928CC /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF98C2C5902DE007928CC /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF98D2C5902DE007928CC /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF98E2C5902E0007928CC /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF98F2C5902E0007928CC /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9902C5902E1007928CC /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9912C5902E1007928CC /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9922C5902E2007928CC /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9932C5902E2007928CC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9942C5902E3007928CC /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = ""; }; + E5DCF9952C59067B007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9972C5906FF007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9992C59072A007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF99A2C59072B007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = ""; }; + E5DCF99B2C59072B007928CC /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF99C2C59072C007928CC /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF99D2C59072D007928CC /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF99E2C59072E007928CC /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF99F2C59072E007928CC /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = de; path = de.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A02C59072F007928CC /* hu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hu; path = hu.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A12C59072F007928CC /* it */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = it; path = it.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A22C590730007928CC /* ja */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ja; path = ja.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A32C590730007928CC /* pl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pl; path = pl.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A42C590731007928CC /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A52C590731007928CC /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A62C590731007928CC /* th */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = th; path = th.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A72C590732007928CC /* tr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = tr; path = tr.lproj/InfoPlist.strings; sourceTree = ""; }; + E5DCF9A82C590732007928CC /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/InfoPlist.strings; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -500,6 +615,7 @@ buildActionMask = 2147483647; files = ( 5CE2BA702845308900EC33A6 /* SimpleXChat.framework in Frameworks */, + 8C8118722C220B5B00E6FC94 /* Yams in Frameworks */, D741547829AF89AF0022400A /* StoreKit.framework in Frameworks */, D7197A1829AE89660055C05A /* WebRTC in Frameworks */, D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */, @@ -529,13 +645,22 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5C0EA1342C0B05C000AD2E5E /* libgmpxx.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, - 5C0EA1352C0B05C000AD2E5E /* libHSsimplex-chat-5.7.5.0-IiqaTGbIKN89ePlb2e8Hnq.a in Frameworks */, - 5C0EA1312C0B05C000AD2E5E /* libgmp.a in Frameworks */, - 5C0EA1322C0B05C000AD2E5E /* libHSsimplex-chat-5.7.5.0-IiqaTGbIKN89ePlb2e8Hnq-ghc9.6.3.a in Frameworks */, - 5C0EA1332C0B05C000AD2E5E /* libffi.a in Frameworks */, + E51ED5772C7691A2009F2C7C /* libgmp.a in Frameworks */, + E51ED5762C7691A2009F2C7C /* libHSsimplex-chat-6.0.2.0-CUdJmkNQ3mRF4AChEwYJvy.a in Frameworks */, + E51ED57A2C7691A2009F2C7C /* libffi.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, + CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */, + E51ED5782C7691A2009F2C7C /* libHSsimplex-chat-6.0.2.0-CUdJmkNQ3mRF4AChEwYJvy-ghc9.6.3.a in Frameworks */, + E51ED5792C7691A2009F2C7C /* libgmpxx.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + E5DCF8DA2C56FABA007928CC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -562,6 +687,7 @@ 5C2E260D27A30E2400F70299 /* Views */ = { isa = PBXGroup; children = ( + B76E6C2F2C5C41C300EC11AA /* Contacts */, 5CB0BA8C282711BC00B3292C /* Onboarding */, 3C714775281C080100CB4D4B /* Call */, 5C971E1F27AEBF7000C8A3CE /* Helpers */, @@ -591,9 +717,11 @@ 5CE4407127ADB1D0007B033A /* Emoji.swift */, 5CADE79B292131E900072E13 /* ContactPreferencesView.swift */, 5CBE6C11294487F7002D9531 /* VerifyCodeView.swift */, + CEEA861C2C2ABCB50084E1EA /* ReverseList.swift */, 5CBE6C132944CC12002D9531 /* ScanCodeView.swift */, 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */, 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */, + 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */, ); path = Chat; sourceTree = ""; @@ -601,11 +729,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - 5C0EA12E2C0B05C000AD2E5E /* libffi.a */, - 5C0EA12C2C0B05C000AD2E5E /* libgmp.a */, - 5C0EA12F2C0B05C000AD2E5E /* libgmpxx.a */, - 5C0EA12D2C0B05C000AD2E5E /* libHSsimplex-chat-5.7.5.0-IiqaTGbIKN89ePlb2e8Hnq-ghc9.6.3.a */, - 5C0EA1302C0B05C000AD2E5E /* libHSsimplex-chat-5.7.5.0-IiqaTGbIKN89ePlb2e8Hnq.a */, + E51ED5752C7691A2009F2C7C /* libffi.a */, + E51ED5722C7691A2009F2C7C /* libgmp.a */, + E51ED5742C7691A2009F2C7C /* libgmpxx.a */, + E51ED5732C7691A2009F2C7C /* libHSsimplex-chat-6.0.2.0-CUdJmkNQ3mRF4AChEwYJvy-ghc9.6.3.a */, + E51ED5712C7691A2009F2C7C /* libHSsimplex-chat-6.0.2.0-CUdJmkNQ3mRF4AChEwYJvy.a */, ); path = Libraries; sourceTree = ""; @@ -633,7 +761,6 @@ 5CF937212B25034A00E1D781 /* NSESubscriber.swift */, 5CB346E82869E8BA001FD2EF /* PushEnvironment.swift */, 5C93293E2928E0FD0090FFF9 /* AudioRecPlay.swift */, - 5CBD2859295711D700EC2CF4 /* ImageUtils.swift */, 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */, ); path = Model; @@ -651,15 +778,17 @@ 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */, 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */, 5C6BA666289BD954009B8ECC /* DismissSheets.swift */, - 5C00164328A26FBC0094D739 /* ContextMenu.swift */, 5CA7DFC229302AF000F7FDDE /* AppSheet.swift */, 18415A7F0F189D87DEFEABCA /* PressedButtonStyle.swift */, 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */, 18415DAAAD1ADBEDB0EDA852 /* VideoPlayerView.swift */, 64466DCB29FFE3E800E3D48D /* MailView.swift */, 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */, - 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */, - 8C05382D2B39887E006436DC /* VideoUtils.swift */, + 8C7F8F0D2C19C0C100D16888 /* ViewModifiers.swift */, + 8C74C3ED2C1B942300039E77 /* ChatWallpaper.swift */, + 8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */, + CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */, + CE7548092C622630009579B7 /* SwipeLabel.swift */, ); path = Helpers; sourceTree = ""; @@ -674,6 +803,7 @@ 5C764E5C279C70B7000C6508 /* Libraries */, 5CA059C2279559F40002BEB4 /* Shared */, 5CDCAD462818589900503DA2 /* SimpleX NSE */, + CEE723A82C3BD3D70009AE93 /* SimpleX SE */, 5CA059DA279559F40002BEB4 /* Tests iOS */, 5CE2BA692845308900EC33A6 /* SimpleXChat */, 5CA059CB279559F40002BEB4 /* Products */, @@ -684,6 +814,7 @@ 5CA059C2279559F40002BEB4 /* Shared */ = { isa = PBXGroup; children = ( + 8C74C3E92C1B909200039E77 /* Theme */, 5CA059C3279559F40002BEB4 /* SimpleXApp.swift */, 5C36027227F47AD5009F19D9 /* AppDelegate.swift */, 5CA059C4279559F40002BEB4 /* ContentView.swift */, @@ -703,6 +834,7 @@ 5CA059D7279559F40002BEB4 /* Tests iOS.xctest */, 5CDCAD452818589900503DA2 /* SimpleX NSE.appex */, 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */, + CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */, ); name = Products; sourceTree = ""; @@ -802,6 +934,8 @@ 5C13730A28156D2700F43030 /* ContactConnectionView.swift */, 5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */, 18415835CBD939A9ABDC108A /* UserPicker.swift */, + 64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */, + E51CC1E52C62085600DB91FE /* OneHandUICard.swift */, ); path = ChatList; sourceTree = ""; @@ -813,6 +947,7 @@ 5CDCAD472818589900503DA2 /* NotificationService.swift */, 5CDCAD492818589900503DA2 /* Info.plist */, 5CB0BA862826CB3A00B3292C /* InfoPlist.strings */, + E5DCF9822C5902CE007928CC /* Localizable.strings */, ); path = "SimpleX NSE"; sourceTree = ""; @@ -820,13 +955,17 @@ 5CE2BA692845308900EC33A6 /* SimpleXChat */ = { isa = PBXGroup; children = ( + 8C86EBE32C0DAE3700E12243 /* Theme */, 5CDCAD5228186F9500503DA2 /* AppGroup.swift */, 5CDCAD7228188CFF00503DA2 /* ChatTypes.swift */, 5CDCAD7428188D2900503DA2 /* APITypes.swift */, 5C5E5D3C282447AB00B0488A /* CallTypes.swift */, + CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */, 5C9FD96A27A56D4D0075386C /* JSON.swift */, 5CDCAD7D2818941F00503DA2 /* API.swift */, 5CDCAD80281A7E2700503DA2 /* Notifications.swift */, + 5CBD2859295711D700EC2CF4 /* ImageUtils.swift */, + CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */, 64DAE1502809D9F5000DA960 /* FileUtils.swift */, 5C9D81182AA7A4F1001D49FD /* CryptoFile.swift */, 5C00168028C4FE760094D739 /* KeyChain.swift */, @@ -913,6 +1052,15 @@ path = Group; sourceTree = ""; }; + 8C74C3E92C1B909200039E77 /* Theme */ = { + isa = PBXGroup; + children = ( + 8C86EBE42C0DAE4F00E12243 /* ThemeManager.swift */, + 8C74C3EB2C1B92A900039E77 /* Theme.swift */, + ); + path = Theme; + sourceTree = ""; + }; 8C7D94982B8894D300B7B9E1 /* Migration */ = { isa = PBXGroup; children = ( @@ -922,6 +1070,40 @@ path = Migration; sourceTree = ""; }; + 8C86EBE32C0DAE3700E12243 /* Theme */ = { + isa = PBXGroup; + children = ( + 8C7E3CE32C0DEAC400BFF63A /* ThemeTypes.swift */, + 8C852B072C1086D100BA61E8 /* Color.swift */, + 8C804B1D2C11F966007A63C8 /* ChatWallpaperTypes.swift */, + ); + path = Theme; + sourceTree = ""; + }; + B76E6C2F2C5C41C300EC11AA /* Contacts */ = { + isa = PBXGroup; + children = ( + B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */, + ); + path = Contacts; + sourceTree = ""; + }; + CEE723A82C3BD3D70009AE93 /* SimpleX SE */ = { + isa = PBXGroup; + children = ( + CEE723D42C3C21F50009AE93 /* SimpleX SE.entitlements */, + CEE723AE2C3BD3D70009AE93 /* Info.plist */, + CEDE70212C48FD9500233B1F /* SEChatState.swift */, + CE1EB0E32C459A660099D896 /* ShareAPI.swift */, + CEE723F12C3D25ED0009AE93 /* ShareModel.swift */, + CEE723EF2C3D25C70009AE93 /* ShareView.swift */, + CEE723A92C3BD3D70009AE93 /* ShareViewController.swift */, + E5DCF96F2C590272007928CC /* Localizable.strings */, + E5DCF9962C5906FF007928CC /* InfoPlist.strings */, + ); + path = "SimpleX SE"; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -953,6 +1135,7 @@ dependencies = ( 5CE2BA6F2845308900EC33A6 /* PBXTargetDependency */, 5CE2BA9F284555F500EC33A6 /* PBXTargetDependency */, + CEE723B02C3BD3D70009AE93 /* PBXTargetDependency */, ); name = "SimpleX (iOS)"; packageProductDependencies = ( @@ -960,6 +1143,7 @@ D77B92DB2952372200A5A1CC /* SwiftyGif */, D7F0E33829964E7E0068AF69 /* LZString */, D7197A1729AE89660055C05A /* WebRTC */, + 8C8118712C220B5B00E6FC94 /* Yams */, ); productName = "SimpleX (iOS)"; productReference = 5CA059CA279559F40002BEB4 /* SimpleX.app */; @@ -1016,11 +1200,30 @@ ); name = SimpleXChat; packageProductDependencies = ( + CE38A29B2C3FCD72005ED185 /* SwiftyGif */, ); productName = SimpleXChat; productReference = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; productType = "com.apple.product-type.framework"; }; + CEE723A62C3BD3D70009AE93 /* SimpleX SE */ = { + isa = PBXNativeTarget; + buildConfigurationList = CEE723B42C3BD3D70009AE93 /* Build configuration list for PBXNativeTarget "SimpleX SE" */; + buildPhases = ( + CEE723A32C3BD3D70009AE93 /* Sources */, + CEE723A52C3BD3D70009AE93 /* Resources */, + E5DCF8DA2C56FABA007928CC /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + CEE723D22C3C21C90009AE93 /* PBXTargetDependency */, + ); + name = "SimpleX SE"; + productName = "SimpleX SE"; + productReference = CEE723A72C3BD3D70009AE93 /* SimpleX SE.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -1028,7 +1231,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; - LastSwiftUpdateCheck = 1330; + LastSwiftUpdateCheck = 1540; LastUpgradeCheck = 1340; ORGANIZATIONNAME = "SimpleX Chat"; TargetAttributes = { @@ -1048,6 +1251,9 @@ CreatedOnToolsVersion = 13.3; LastSwiftMigration = 1330; }; + CEE723A62C3BD3D70009AE93 = { + CreatedOnToolsVersion = 15.4; + }; }; }; buildConfigurationList = 5CA059C1279559F40002BEB4 /* Build configuration list for PBXProject "SimpleX" */; @@ -1080,6 +1286,7 @@ D77B92DA2952372200A5A1CC /* XCRemoteSwiftPackageReference "SwiftyGif" */, D7F0E33729964E7D0068AF69 /* XCRemoteSwiftPackageReference "lzstring-swift" */, D7197A1629AE89660055C05A /* XCRemoteSwiftPackageReference "WebRTC" */, + 8C73C1162C21E17B00892670 /* XCRemoteSwiftPackageReference "Yams" */, ); productRefGroup = 5CA059CB279559F40002BEB4 /* Products */; projectDirPath = ""; @@ -1088,6 +1295,7 @@ 5CA059C9279559F40002BEB4 /* SimpleX (iOS) */, 5CA059D6279559F40002BEB4 /* Tests iOS */, 5CDCAD442818589900503DA2 /* SimpleX NSE */, + CEE723A62C3BD3D70009AE93 /* SimpleX SE */, 5CE2BA672845308900EC33A6 /* SimpleXChat */, ); }; @@ -1117,6 +1325,7 @@ buildActionMask = 2147483647; files = ( 5CB0BA882826CB3A00B3292C /* InfoPlist.strings in Resources */, + E5DCF9842C5902CE007928CC /* Localizable.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1127,6 +1336,15 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + CEE723A52C3BD3D70009AE93 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E5DCF9712C590272007928CC /* Localizable.strings in Resources */, + E5DCF9982C5906FF007928CC /* InfoPlist.strings in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -1134,6 +1352,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + CEEA861D2C2ABCB50084E1EA /* ReverseList.swift in Sources */, 64C06EB52A0A4A7C00792D4D /* ChatItemInfoView.swift in Sources */, 640417CE2B29B8C200CCB412 /* NewChatView.swift in Sources */, 6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */, @@ -1142,6 +1361,7 @@ 5C93292F29239A170090FFF9 /* ProtocolServersView.swift in Sources */, 5CB924D727A8563F00ACCCDD /* SettingsView.swift in Sources */, 5CEACCE327DE9246000BD591 /* ComposeView.swift in Sources */, + E51CC1E62C62085600DB91FE /* OneHandUICard.swift in Sources */, 5C65DAF929D0CC20003CEE45 /* DeveloperView.swift in Sources */, 5C36027327F47AD5009F19D9 /* AppDelegate.swift in Sources */, 5CB924E127A867BA00ACCCDD /* UserProfile.swift in Sources */, @@ -1151,7 +1371,7 @@ 6432857C2925443C00FBE5C8 /* GroupPreferencesView.swift in Sources */, 5C93293129239BED0090FFF9 /* ProtocolServerView.swift in Sources */, 5C9CC7AD28C55D7800BEF955 /* DatabaseEncryptionView.swift in Sources */, - 5CBD285A295711D700EC2CF4 /* ImageUtils.swift in Sources */, + 8C74C3EC2C1B92A900039E77 /* Theme.swift in Sources */, 6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */, 5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */, 8C7D949A2B88952700B7B9E1 /* MigrateToDevice.swift in Sources */, @@ -1174,9 +1394,10 @@ 64466DC829FC2B3B00E3D48D /* CreateSimpleXAddress.swift in Sources */, 3CDBCF4827FF621E00354CDD /* CILinkView.swift in Sources */, 5C7505A827B6D34800BE3227 /* ChatInfoToolbar.swift in Sources */, + B76E6C312C5C41D900EC11AA /* ContactListNavLink.swift in Sources */, 5C10D88A28F187F300E58BF0 /* FullScreenMediaView.swift in Sources */, D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */, - 5C00164428A26FBC0094D739 /* ContextMenu.swift in Sources */, + CE984D4B2C36C5D500E3AEFF /* ChatItemClipShape.swift in Sources */, 64D0C2C629FAC1EC00B38D5F /* AddContactLearnMore.swift in Sources */, 5C3A88D127DF57800060F1C2 /* FramedItemView.swift in Sources */, 5C65F343297D45E100B67AF3 /* VersionView.swift in Sources */, @@ -1184,6 +1405,7 @@ 5CB0BA90282713D900B3292C /* SimpleXInfo.swift in Sources */, 5C063D2727A4564100AEC577 /* ChatPreviewView.swift in Sources */, 5CC868F329EB540C0017BBFD /* CIRcvDecryptionError.swift in Sources */, + 8C7F8F0E2C19C0C100D16888 /* ViewModifiers.swift in Sources */, 5C35CFCB27B2E91D00FB6C6D /* NtfManager.swift in Sources */, 5C9D13A3282187BB00AB8B43 /* WebRTC.swift in Sources */, 5C9A5BDB2871E05400A5B906 /* SetNotificationsMode.swift in Sources */, @@ -1208,8 +1430,8 @@ 5CB346E72868D76D001FD2EF /* NotificationsView.swift in Sources */, 647F090E288EA27B00644C40 /* GroupMemberInfoView.swift in Sources */, 646BB38E283FDB6D001CE359 /* LocalAuthenticationUtils.swift in Sources */, + 8C74C3EA2C1B90AF00039E77 /* ThemeManager.swift in Sources */, 5C7505A227B65FDB00BE3227 /* CIMetaView.swift in Sources */, - 5CEBD7462A5C0A8F00665FE2 /* KeyboardPadding.swift in Sources */, 5C35CFC827B2782E00FB6C6D /* BGManager.swift in Sources */, 5CB634B129E5EFEA0066AD6B /* PasscodeView.swift in Sources */, 8C69FE7D2B8C7D2700267E38 /* AppSettings.swift in Sources */, @@ -1219,12 +1441,15 @@ 5CF937232B2503D000E1D781 /* NSESubscriber.swift in Sources */, 6419EC582AB97507004A607A /* CIMemberCreatedContactView.swift in Sources */, 64D0C2C229FA57AB00B38D5F /* UserAddressLearnMore.swift in Sources */, + 8CE848A32C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift in Sources */, 64466DCC29FFE3E800E3D48D /* MailView.swift in Sources */, + CE75480A2C622630009579B7 /* SwipeLabel.swift in Sources */, 5C971E2127AEBF8300C8A3CE /* ChatInfoImage.swift in Sources */, 5C55A921283CCCB700C4E99E /* IncomingCallView.swift in Sources */, 6454036F2822A9750090DDFF /* ComposeFileView.swift in Sources */, 5C5DB70E289ABDD200730FFF /* AppearanceSettings.swift in Sources */, 5C5F2B6D27EBC3FE006A9D5F /* ImagePicker.swift in Sources */, + 8C74C3EE2C1B942300039E77 /* ChatWallpaper.swift in Sources */, 5C3CCFCC2AE6BD3100C3F0C3 /* ConnectDesktopView.swift in Sources */, 5C9C2DA92899DA6F00CC63B1 /* NetworkAndServers.swift in Sources */, 5C6BA667289BD954009B8ECC /* DismissSheets.swift in Sources */, @@ -1234,11 +1459,11 @@ 5CA059EB279559F40002BEB4 /* SimpleXApp.swift in Sources */, 64D0C2C029F9688300B38D5F /* UserAddressView.swift in Sources */, 6448BBB628FA9D56000D2AB9 /* GroupLinkView.swift in Sources */, + 8C9BC2652C240D5200875A27 /* ThemeModeEditor.swift in Sources */, 5CB346E92869E8BA001FD2EF /* PushEnvironment.swift in Sources */, 5C55A91F283AD0E400C4E99E /* CallManager.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 */, @@ -1256,6 +1481,7 @@ 5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */, 5C9329412929248A0090FFF9 /* ScanProtocolServer.swift in Sources */, 8C7DF3202B7CDB0A00C886D0 /* MigrateFromDevice.swift in Sources */, + 64EEB0F72C353F1C00972D62 /* ServersSummaryView.swift in Sources */, 64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */, 5C93293F2928E0FD0090FFF9 /* AudioRecPlay.swift in Sources */, 5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */, @@ -1306,22 +1532,40 @@ buildActionMask = 2147483647; files = ( 5CF937202B24DE8C00E1D781 /* SharedFileSubscriber.swift in Sources */, + CE3097FB2C4C0C9F00180898 /* ErrorAlert.swift in Sources */, 5C00168128C4FE760094D739 /* KeyChain.swift in Sources */, 5CE2BA97284537A800EC33A6 /* dummy.m in Sources */, 5CE2BA922845340900EC33A6 /* FileUtils.swift in Sources */, + 8C74C3E72C1B901900039E77 /* Color.swift in Sources */, 5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */, 5CE2BA91284533A300EC33A6 /* Notifications.swift in Sources */, 5CE2BA79284530CC00EC33A6 /* SimpleXChat.docc in Sources */, 5CE2BA90284533A300EC33A6 /* JSON.swift in Sources */, 5CE2BA8B284533A300EC33A6 /* ChatTypes.swift in Sources */, 5CE2BA8F284533A300EC33A6 /* APITypes.swift in Sources */, + CE38A29A2C3FCA54005ED185 /* ImageUtils.swift in Sources */, 5C9D811A2AA8727A001D49FD /* CryptoFile.swift in Sources */, 5CE2BA8C284533A300EC33A6 /* AppGroup.swift in Sources */, + 8C74C3E52C1B900600039E77 /* ThemeTypes.swift in Sources */, 5CE2BA8D284533A300EC33A6 /* CallTypes.swift in Sources */, + 8C74C3E82C1B905B00039E77 /* ChatWallpaperTypes.swift in Sources */, + CE2AD9CE2C452A4D00E844E3 /* ChatUtils.swift in Sources */, 5CE2BA8E284533A300EC33A6 /* API.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; + CEE723A32C3BD3D70009AE93 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CEDE70222C48FD9500233B1F /* SEChatState.swift in Sources */, + CEE723F02C3D25C70009AE93 /* ShareView.swift in Sources */, + CE1EB0E42C459A660099D896 /* ShareAPI.swift in Sources */, + CEE723F22C3D25ED0009AE93 /* ShareModel.swift in Sources */, + CEE723AA2C3BD3D70009AE93 /* ShareViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -1346,6 +1590,16 @@ target = 5CE2BA672845308900EC33A6 /* SimpleXChat */; targetProxy = 5CE2BAA82845617C00EC33A6 /* PBXContainerItemProxy */; }; + CEE723B02C3BD3D70009AE93 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = CEE723A62C3BD3D70009AE93 /* SimpleX SE */; + targetProxy = CEE723AF2C3BD3D70009AE93 /* PBXContainerItemProxy */; + }; + CEE723D22C3C21C90009AE93 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 5CE2BA672845308900EC33A6 /* SimpleXChat */; + targetProxy = CEE723D12C3C21C90009AE93 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -1368,6 +1622,7 @@ 5C5B67932ABAF56000DA9412 /* bg */, 5C245F3E2B501F13001CC39F /* tr */, 5C371E502BA9AB6400100AD3 /* hu */, + E5DCF9952C59067B007928CC /* en */, ); name = InfoPlist.strings; sourceTree = ""; @@ -1419,6 +1674,78 @@ name = "SimpleX--iOS--InfoPlist.strings"; sourceTree = ""; }; + E5DCF96F2C590272007928CC /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + E5DCF9702C590272007928CC /* en */, + E5DCF9722C590274007928CC /* bg */, + E5DCF9732C590275007928CC /* zh-Hans */, + E5DCF9742C590276007928CC /* nl */, + E5DCF9752C590277007928CC /* cs */, + E5DCF9762C590278007928CC /* fi */, + E5DCF9772C590279007928CC /* fr */, + E5DCF9782C590279007928CC /* de */, + E5DCF9792C59027A007928CC /* hu */, + E5DCF97A2C59027A007928CC /* it */, + E5DCF97B2C59027B007928CC /* ja */, + E5DCF97C2C59027B007928CC /* pl */, + E5DCF97D2C59027C007928CC /* ru */, + E5DCF97E2C59027C007928CC /* es */, + E5DCF97F2C59027D007928CC /* th */, + E5DCF9802C59027D007928CC /* uk */, + E5DCF9812C59027D007928CC /* tr */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + E5DCF9822C5902CE007928CC /* Localizable.strings */ = { + isa = PBXVariantGroup; + children = ( + E5DCF9832C5902CE007928CC /* en */, + E5DCF9852C5902D4007928CC /* bg */, + E5DCF9862C5902D5007928CC /* zh-Hans */, + E5DCF9872C5902D8007928CC /* cs */, + E5DCF9882C5902DC007928CC /* nl */, + E5DCF9892C5902DC007928CC /* fi */, + E5DCF98A2C5902DD007928CC /* de */, + E5DCF98B2C5902DD007928CC /* fr */, + E5DCF98C2C5902DE007928CC /* it */, + E5DCF98D2C5902DE007928CC /* hu */, + E5DCF98E2C5902E0007928CC /* ja */, + E5DCF98F2C5902E0007928CC /* pl */, + E5DCF9902C5902E1007928CC /* ru */, + E5DCF9912C5902E1007928CC /* es */, + E5DCF9922C5902E2007928CC /* th */, + E5DCF9932C5902E2007928CC /* tr */, + E5DCF9942C5902E3007928CC /* uk */, + ); + name = Localizable.strings; + sourceTree = ""; + }; + E5DCF9962C5906FF007928CC /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + E5DCF9972C5906FF007928CC /* en */, + E5DCF9992C59072A007928CC /* bg */, + E5DCF99A2C59072B007928CC /* zh-Hans */, + E5DCF99B2C59072B007928CC /* cs */, + E5DCF99C2C59072C007928CC /* nl */, + E5DCF99D2C59072D007928CC /* fi */, + E5DCF99E2C59072E007928CC /* fr */, + E5DCF99F2C59072E007928CC /* de */, + E5DCF9A02C59072F007928CC /* hu */, + E5DCF9A12C59072F007928CC /* it */, + E5DCF9A22C590730007928CC /* ja */, + E5DCF9A32C590730007928CC /* pl */, + E5DCF9A42C590731007928CC /* ru */, + E5DCF9A52C590731007928CC /* es */, + E5DCF9A62C590731007928CC /* th */, + E5DCF9A72C590732007928CC /* tr */, + E5DCF9A82C590732007928CC /* uk */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ @@ -1552,7 +1879,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 222; + CURRENT_PROJECT_VERSION = 234; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1577,7 +1904,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES_THIN; - MARKETING_VERSION = 5.7.5; + MARKETING_VERSION = 6.0.2; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1601,7 +1928,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 222; + CURRENT_PROJECT_VERSION = 234; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1626,7 +1953,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.7.5; + MARKETING_VERSION = 6.0.2; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1642,11 +1969,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 234; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 6.0.2; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1662,11 +1989,11 @@ buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 234; DEVELOPMENT_TEAM = 5NN7GUYB6T; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.0; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 6.0.2; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = iphoneos; @@ -1687,7 +2014,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 222; + CURRENT_PROJECT_VERSION = 234; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -1702,7 +2029,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.7.5; + MARKETING_VERSION = 6.0.2; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1724,7 +2051,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 222; + CURRENT_PROJECT_VERSION = 234; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -1739,7 +2066,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.7.5; + MARKETING_VERSION = 6.0.2; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1761,7 +2088,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 222; + CURRENT_PROJECT_VERSION = 234; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1787,7 +2114,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.7.5; + MARKETING_VERSION = 6.0.2; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -1812,7 +2139,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 222; + CURRENT_PROJECT_VERSION = 234; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1838,7 +2165,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.7.5; + MARKETING_VERSION = 6.0.2; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -1856,6 +2183,74 @@ }; name = Release; }; + CEE723B22C3BD3D70009AE93 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 234; + DEVELOPMENT_TEAM = 5NN7GUYB6T; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "SimpleX SE/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "SimpleX SE"; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 SimpleX Chat. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 6.0.2; + PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + CEE723B32C3BD3D70009AE93 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 234; + DEVELOPMENT_TEAM = 5NN7GUYB6T; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = "SimpleX SE/Info.plist"; + INFOPLIST_KEY_CFBundleDisplayName = "SimpleX SE"; + INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 SimpleX Chat. All rights reserved."; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 6.0.2; + PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -1904,6 +2299,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + CEE723B42C3BD3D70009AE93 /* Build configuration list for PBXNativeTarget "SimpleX SE" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CEE723B22C3BD3D70009AE93 /* Debug */, + CEE723B32C3BD3D70009AE93 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -1911,8 +2315,16 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/twostraws/CodeScanner"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 2.0.0; + kind = exactVersion; + version = 2.5.0; + }; + }; + 8C73C1162C21E17B00892670 /* XCRemoteSwiftPackageReference "Yams" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/jpsim/Yams"; + requirement = { + kind = exactVersion; + version = 5.1.2; }; }; D7197A1629AE89660055C05A /* XCRemoteSwiftPackageReference "WebRTC" */ = { @@ -1927,8 +2339,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kirualex/SwiftyGif"; requirement = { - branch = master; - kind = branch; + kind = revision; + revision = 5e8619335d394901379c9add5c4c1c2f420b3800; }; }; D7F0E33729964E7D0068AF69 /* XCRemoteSwiftPackageReference "lzstring-swift" */ = { @@ -1947,6 +2359,16 @@ package = 5C8F01CB27A6F0D8007D2C8D /* XCRemoteSwiftPackageReference "CodeScanner" */; productName = CodeScanner; }; + 8C8118712C220B5B00E6FC94 /* Yams */ = { + isa = XCSwiftPackageProductDependency; + package = 8C73C1162C21E17B00892670 /* XCRemoteSwiftPackageReference "Yams" */; + productName = Yams; + }; + CE38A29B2C3FCD72005ED185 /* SwiftyGif */ = { + isa = XCSwiftPackageProductDependency; + package = D77B92DA2952372200A5A1CC /* XCRemoteSwiftPackageReference "SwiftyGif" */; + productName = SwiftyGif; + }; D7197A1729AE89660055C05A /* WebRTC */ = { isa = XCSwiftPackageProductDependency; package = D7197A1629AE89660055C05A /* XCRemoteSwiftPackageReference "WebRTC" */; diff --git a/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index cfacb2381a..c8623a95cb 100644 --- a/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,12 +1,13 @@ { + "originHash" : "e2611d1e91fd8071abc106776ba14ee2e395d2ad08a78e073381294abc10f115", "pins" : [ { "identity" : "codescanner", "kind" : "remoteSourceControl", "location" : "https://github.com/twostraws/CodeScanner", "state" : { - "revision" : "c27a66149b7483fe42e2ec6aad61d5c3fffe522d", - "version" : "2.1.1" + "revision" : "34da57fb63b47add20de8a85da58191523ccce57", + "version" : "2.5.0" } }, { @@ -22,7 +23,6 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/kirualex/SwiftyGif", "state" : { - "branch" : "master", "revision" : "5e8619335d394901379c9add5c4c1c2f420b3800" } }, @@ -33,7 +33,16 @@ "state" : { "revision" : "34bedc50f9c58dccf4967ea59c7e6a47d620803b" } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams", + "state" : { + "revision" : "9234124cff5e22e178988c18d8b95a8ae8007f76", + "version" : "5.1.2" + } } ], - "version" : 2 + "version" : 3 } diff --git a/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX SE.xcscheme b/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX SE.xcscheme new file mode 100644 index 0000000000..a2639eb263 --- /dev/null +++ b/apps/ios/SimpleX.xcodeproj/xcshareddata/xcschemes/SimpleX SE.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/ios/SimpleXChat/API.swift b/apps/ios/SimpleXChat/API.swift index e2f4adc60f..987f7f3d41 100644 --- a/apps/ios/SimpleXChat/API.swift +++ b/apps/ios/SimpleXChat/API.swift @@ -117,10 +117,10 @@ public func sendSimpleXCmd(_ cmd: ChatCommand, _ ctrl: chat_ctrl? = nil) -> Chat } // in microseconds -let MESSAGE_TIMEOUT: Int32 = 15_000_000 +public let MESSAGE_TIMEOUT: Int32 = 15_000_000 -public func recvSimpleXMsg(_ ctrl: chat_ctrl? = nil) -> ChatResponse? { - if let cjson = chat_recv_msg_wait(ctrl ?? getChatCtrl(), MESSAGE_TIMEOUT) { +public func recvSimpleXMsg(_ ctrl: chat_ctrl? = nil, messageTimeout: Int32 = MESSAGE_TIMEOUT) -> ChatResponse? { + if let cjson = chat_recv_msg_wait(ctrl ?? getChatCtrl(), messageTimeout) { let s = fromCString(cjson) return s == "" ? nil : chatResponse(s) } @@ -205,7 +205,7 @@ public func chatResponse(_ s: String) -> ChatResponse { if let chatData = try? parseChatData(jChat) { return chatData } - return ChatData.invalidJSON(prettyJSON(jChat) ?? "") + return ChatData.invalidJSON(serializeJSON(jChat, options: .prettyPrinted) ?? "") } return .apiChats(user: user, chats: chats) } @@ -218,15 +218,15 @@ public func chatResponse(_ s: String) -> ChatResponse { } } else if type == "chatCmdError" { if let jError = jResp["chatCmdError"] as? NSDictionary { - return .chatCmdError(user_: decodeUser_(jError), chatError: .invalidJSON(json: prettyJSON(jError) ?? "")) + return .chatCmdError(user_: decodeUser_(jError), chatError: .invalidJSON(json: errorJson(jError) ?? "")) } } else if type == "chatError" { if let jError = jResp["chatError"] as? NSDictionary { - return .chatError(user_: decodeUser_(jError), chatError: .invalidJSON(json: prettyJSON(jError) ?? "")) + return .chatError(user_: decodeUser_(jError), chatError: .invalidJSON(json: errorJson(jError) ?? "")) } } } - json = prettyJSON(j) + json = serializeJSON(j, options: .prettyPrinted) } return ChatResponse.response(type: type ?? "invalid", json: json ?? s) } @@ -239,6 +239,14 @@ private func decodeUser_(_ jDict: NSDictionary) -> UserRef? { } } +private func errorJson(_ jDict: NSDictionary) -> String? { + if let chatError = jDict["chatError"] { + serializeJSON(chatError) + } else { + serializeJSON(jDict) + } +} + func parseChatData(_ jChat: Any) throws -> ChatData { let jChatDict = jChat as! NSDictionary let chatInfo: ChatInfo = try decodeObject(jChatDict["chatInfo"]!) @@ -251,7 +259,7 @@ func parseChatData(_ jChat: Any) throws -> ChatData { return ChatItem.invalidJSON( chatDir: decodeProperty(jCI, "chatDir"), meta: decodeProperty(jCI, "meta"), - json: prettyJSON(jCI) ?? "" + json: serializeJSON(jCI, options: .prettyPrinted) ?? "" ) } return ChatData(chatInfo: chatInfo, chatItems: chatItems, chatStats: chatStats) @@ -268,8 +276,8 @@ func decodeProperty(_ obj: Any, _ prop: NSString) -> T? { return nil } -func prettyJSON(_ obj: Any) -> String? { - if let d = try? JSONSerialization.data(withJSONObject: obj, options: .prettyPrinted) { +func serializeJSON(_ obj: Any, options: JSONSerialization.WritingOptions = []) -> String? { + if let d = try? JSONSerialization.data(withJSONObject: obj, options: options) { return String(decoding: d, as: UTF8.self) } return nil diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 3aa610e4af..a6409dec2f 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -10,11 +10,11 @@ import Foundation import SwiftUI public let jsonDecoder = getJSONDecoder() -let jsonEncoder = getJSONEncoder() +public let jsonEncoder = getJSONEncoder() public enum ChatCommand { case showActiveUser - case createActiveUser(profile: Profile?, sameServers: Bool, pastTimestamp: Bool) + case createActiveUser(profile: Profile?, pastTimestamp: Bool) case listUsers case apiSetActiveUser(userId: Int64, viewPwd: String?) case setAllContactReceipts(enable: Bool) @@ -25,12 +25,12 @@ public enum ChatCommand { case apiMuteUser(userId: Int64) case apiUnmuteUser(userId: Int64) case apiDeleteUser(userId: Int64, delSMPQueues: Bool, viewPwd: String?) - case startChat(mainApp: Bool) + case startChat(mainApp: Bool, enableSndFiles: Bool) + case checkChatRunning case apiStopChat case apiActivateChat(restoreChat: Bool) case apiSuspendChat(timeoutMicroseconds: Int) - case setTempFolder(tempFolder: String) - case setFilesFolder(filesFolder: String) + case apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String) case apiSetEncryptLocalFiles(enable: Bool) case apiExportArchive(config: ArchiveConfig) case apiImportArchive(config: ArchiveConfig) @@ -45,10 +45,10 @@ public enum ChatCommand { case apiSendMessage(type: ChatType, id: Int64, file: CryptoFile?, quotedItemId: Int64?, msg: MsgContent, live: Bool, ttl: Int?) case apiCreateChatItem(noteFolderId: Int64, file: CryptoFile?, msg: MsgContent) case apiUpdateChatItem(type: ChatType, id: Int64, itemId: Int64, msg: MsgContent, live: Bool) - case apiDeleteChatItem(type: ChatType, id: Int64, itemId: Int64, mode: CIDeleteMode) - case apiDeleteMemberChatItem(groupId: Int64, groupMemberId: Int64, itemId: Int64) + case apiDeleteChatItem(type: ChatType, id: Int64, itemIds: [Int64], mode: CIDeleteMode) + case apiDeleteMemberChatItem(groupId: Int64, itemIds: [Int64]) case apiChatItemReaction(type: ChatType, id: Int64, itemId: Int64, add: Bool, reaction: MsgReaction) - case apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64) + case apiForwardChatItem(toChatType: ChatType, toChatId: Int64, fromChatType: ChatType, fromChatId: Int64, itemId: Int64, ttl: Int?) case apiGetNtfToken case apiRegisterToken(token: DeviceToken, notificationMode: NotificationsMode) case apiVerifyToken(token: DeviceToken, nonce: String, code: String) @@ -78,10 +78,13 @@ public enum ChatCommand { case apiGetNetworkConfig case apiSetNetworkInfo(networkInfo: UserNetworkInfo) case reconnectAllServers + case reconnectServer(userId: Int64, smpServer: String) case apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) case apiSetMemberSettings(groupId: Int64, groupMemberId: Int64, memberSettings: GroupMemberSettings) case apiContactInfo(contactId: Int64) case apiGroupMemberInfo(groupId: Int64, groupMemberId: Int64) + case apiContactQueueInfo(contactId: Int64) + case apiGroupMemberQueueInfo(groupId: Int64, groupMemberId: Int64) case apiSwitchContact(contactId: Int64) case apiSwitchGroupMember(groupId: Int64, groupMemberId: Int64) case apiAbortSwitchContact(contactId: Int64) @@ -97,13 +100,15 @@ public enum ChatCommand { case apiConnectPlan(userId: Int64, connReq: String) case apiConnect(userId: Int64, incognito: Bool, connReq: String) case apiConnectContactViaAddress(userId: Int64, incognito: Bool, contactId: Int64) - case apiDeleteChat(type: ChatType, id: Int64, notify: Bool?) + case apiDeleteChat(type: ChatType, id: Int64, chatDeleteMode: ChatDeleteMode) case apiClearChat(type: ChatType, id: Int64) case apiListContacts(userId: Int64) case apiUpdateProfile(userId: Int64, profile: Profile) case apiSetContactPrefs(contactId: Int64, preferences: Preferences) case apiSetContactAlias(contactId: Int64, localAlias: String) case apiSetConnectionAlias(connId: Int64, localAlias: String) + case apiSetUserUIThemes(userId: Int64, themes: ThemeModeOverrides?) + case apiSetChatUIThemes(chatId: String, themes: ThemeModeOverrides?) case apiCreateMyAddress(userId: Int64) case apiDeleteMyAddress(userId: Int64) case apiShowMyAddress(userId: Int64) @@ -120,11 +125,12 @@ public enum ChatCommand { case apiEndCall(contact: Contact) case apiGetCallInvitations case apiCallStatus(contact: Contact, callStatus: WebRTCCallStatus) + // WebRTC calls / case apiGetNetworkStatuses case apiChatRead(type: ChatType, id: Int64, itemRange: (Int64, Int64)) case apiChatUnread(type: ChatType, id: Int64, unreadChat: Bool) - case receiveFile(fileId: Int64, encrypted: Bool?, inline: Bool?) - case setFileToReceive(fileId: Int64, encrypted: Bool?) + case receiveFile(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?, inline: Bool?) + case setFileToReceive(fileId: Int64, userApprovedRelays: Bool, encrypted: Bool?) case cancelFile(fileId: Int64) // remote desktop commands case setLocalDeviceName(displayName: String) @@ -140,14 +146,17 @@ public enum ChatCommand { case apiStandaloneFileInfo(url: String) // misc case showVersion + case getAgentSubsTotal(userId: Int64) + case getAgentServersSummary(userId: Int64) + case resetAgentServersStats case string(String) public var cmdString: String { get { switch self { case .showActiveUser: return "/u" - case let .createActiveUser(profile, sameServers, pastTimestamp): - let user = NewUser(profile: profile, sameServers: sameServers, pastTimestamp: pastTimestamp) + case let .createActiveUser(profile, pastTimestamp): + let user = NewUser(profile: profile, pastTimestamp: pastTimestamp) return "/_create user \(encodeJSON(user))" case .listUsers: return "/users" case let .apiSetActiveUser(userId, viewPwd): return "/_user \(userId)\(maybePwd(viewPwd))" @@ -163,12 +172,12 @@ public enum ChatCommand { case let .apiMuteUser(userId): return "/_mute user \(userId)" case let .apiUnmuteUser(userId): return "/_unmute user \(userId)" case let .apiDeleteUser(userId, delSMPQueues, viewPwd): return "/_delete user \(userId) del_smp=\(onOff(delSMPQueues))\(maybePwd(viewPwd))" - case let .startChat(mainApp): return "/_start main=\(onOff(mainApp))" + case let .startChat(mainApp, enableSndFiles): return "/_start main=\(onOff(mainApp)) snd_files=\(onOff(enableSndFiles))" + case .checkChatRunning: return "/_check running" case .apiStopChat: return "/_stop" case let .apiActivateChat(restore): return "/_app activate restore=\(onOff(restore))" case let .apiSuspendChat(timeoutMicroseconds): return "/_app suspend \(timeoutMicroseconds)" - case let .setTempFolder(tempFolder): return "/_temp_folder \(tempFolder)" - case let .setFilesFolder(filesFolder): return "/_files_folder \(filesFolder)" + case let .apiSetAppFilePaths(filesFolder, tempFolder, assetsFolder): return "/set file paths \(encodeJSON(AppFilePaths(appFilesFolder: filesFolder, appTempFolder: tempFolder, appAssetsFolder: assetsFolder)))" case let .apiSetEncryptLocalFiles(enable): return "/_files_encrypt \(onOff(enable))" case let .apiExportArchive(cfg): return "/_db export \(encodeJSON(cfg))" case let .apiImportArchive(cfg): return "/_db import \(encodeJSON(cfg))" @@ -189,10 +198,12 @@ public enum ChatCommand { let msg = encodeJSON(ComposedMessage(fileSource: file, msgContent: mc)) return "/_create *\(noteFolderId) json \(msg)" case let .apiUpdateChatItem(type, id, itemId, mc, live): return "/_update item \(ref(type, id)) \(itemId) live=\(onOff(live)) \(mc.cmdString)" - case let .apiDeleteChatItem(type, id, itemId, mode): return "/_delete item \(ref(type, id)) \(itemId) \(mode.rawValue)" - case let .apiDeleteMemberChatItem(groupId, groupMemberId, itemId): return "/_delete member item #\(groupId) \(groupMemberId) \(itemId)" + case let .apiDeleteChatItem(type, id, itemIds, mode): return "/_delete item \(ref(type, id)) \(itemIds.map({ "\($0)" }).joined(separator: ",")) \(mode.rawValue)" + case let .apiDeleteMemberChatItem(groupId, itemIds): return "/_delete member item #\(groupId) \(itemIds.map({ "\($0)" }).joined(separator: ","))" case let .apiChatItemReaction(type, id, itemId, add, reaction): return "/_reaction \(ref(type, id)) \(itemId) \(onOff(add)) \(encodeJSON(reaction))" - case let .apiForwardChatItem(toChatType, toChatId, fromChatType, fromChatId, itemId): return "/_forward \(ref(toChatType, toChatId)) \(ref(fromChatType, fromChatId)) \(itemId)" + case let .apiForwardChatItem(toChatType, toChatId, fromChatType, fromChatId, itemId, ttl): + let ttlStr = ttl != nil ? "\(ttl!)" : "default" + return "/_forward \(ref(toChatType, toChatId)) \(ref(fromChatType, fromChatId)) \(itemId) ttl=\(ttlStr)" case .apiGetNtfToken: return "/_ntf get " case let .apiRegisterToken(token, notificationMode): return "/_ntf register \(token.cmdString) \(notificationMode.rawValue)" case let .apiVerifyToken(token, nonce, code): return "/_ntf verify \(token.cmdString) \(nonce) \(code)" @@ -222,10 +233,13 @@ public enum ChatCommand { case .apiGetNetworkConfig: return "/network" case let .apiSetNetworkInfo(networkInfo): return "/_network info \(encodeJSON(networkInfo))" case .reconnectAllServers: return "/reconnect" + case let .reconnectServer(userId, smpServer): return "/reconnect \(userId) \(smpServer)" case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id)) \(encodeJSON(chatSettings))" case let .apiSetMemberSettings(groupId, groupMemberId, memberSettings): return "/_member settings #\(groupId) \(groupMemberId) \(encodeJSON(memberSettings))" case let .apiContactInfo(contactId): return "/_info @\(contactId)" case let .apiGroupMemberInfo(groupId, groupMemberId): return "/_info #\(groupId) \(groupMemberId)" + case let .apiContactQueueInfo(contactId): return "/_queue info @\(contactId)" + case let .apiGroupMemberQueueInfo(groupId, groupMemberId): return "/_queue info #\(groupId) \(groupMemberId)" case let .apiSwitchContact(contactId): return "/_switch @\(contactId)" case let .apiSwitchGroupMember(groupId, groupMemberId): return "/_switch #\(groupId) \(groupMemberId)" case let .apiAbortSwitchContact(contactId): return "/_abort switch @\(contactId)" @@ -251,17 +265,15 @@ public enum ChatCommand { case let .apiConnectPlan(userId, connReq): return "/_connect plan \(userId) \(connReq)" case let .apiConnect(userId, incognito, connReq): return "/_connect \(userId) incognito=\(onOff(incognito)) \(connReq)" case let .apiConnectContactViaAddress(userId, incognito, contactId): return "/_connect contact \(userId) incognito=\(onOff(incognito)) \(contactId)" - case let .apiDeleteChat(type, id, notify): if let notify = notify { - return "/_delete \(ref(type, id)) notify=\(onOff(notify))" - } else { - return "/_delete \(ref(type, id))" - } + case let .apiDeleteChat(type, id, chatDeleteMode): return "/_delete \(ref(type, id)) \(chatDeleteMode.cmdString)" case let .apiClearChat(type, id): return "/_clear chat \(ref(type, id))" case let .apiListContacts(userId): return "/_contacts \(userId)" case let .apiUpdateProfile(userId, profile): return "/_profile \(userId) \(encodeJSON(profile))" case let .apiSetContactPrefs(contactId, preferences): return "/_set prefs @\(contactId) \(encodeJSON(preferences))" case let .apiSetContactAlias(contactId, localAlias): return "/_set alias @\(contactId) \(localAlias.trimmingCharacters(in: .whitespaces))" case let .apiSetConnectionAlias(connId, localAlias): return "/_set alias :\(connId) \(localAlias.trimmingCharacters(in: .whitespaces))" + case let .apiSetUserUIThemes(userId, themes): return "/_set theme user \(userId) \(themes != nil ? encodeJSON(themes) : "")" + case let .apiSetChatUIThemes(chatId, themes): return "/_set theme \(chatId) \(themes != nil ? encodeJSON(themes) : "")" case let .apiCreateMyAddress(userId): return "/_address \(userId)" case let .apiDeleteMyAddress(userId): return "/_delete_address \(userId)" case let .apiShowMyAddress(userId): return "/_show_address \(userId)" @@ -280,8 +292,8 @@ public enum ChatCommand { case .apiGetNetworkStatuses: return "/_network_statuses" case let .apiChatRead(type, id, itemRange: (from, to)): return "/_read chat \(ref(type, id)) from=\(from) to=\(to)" case let .apiChatUnread(type, id, unreadChat): return "/_unread chat \(ref(type, id)) \(onOff(unreadChat))" - case let .receiveFile(fileId, encrypt, inline): return "/freceive \(fileId)\(onOffParam("encrypt", encrypt))\(onOffParam("inline", inline))" - case let .setFileToReceive(fileId, encrypt): return "/_set_file_to_receive \(fileId)\(onOffParam("encrypt", encrypt))" + case let .receiveFile(fileId, userApprovedRelays, encrypt, inline): return "/freceive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))\(onOffParam("inline", inline))" + case let .setFileToReceive(fileId, userApprovedRelays, encrypt): return "/_set_file_to_receive \(fileId)\(onOffParam("approved_relays", userApprovedRelays))\(onOffParam("encrypt", encrypt))" case let .cancelFile(fileId): return "/fcancel \(fileId)" case let .setLocalDeviceName(displayName): return "/set device name \(displayName)" case let .connectRemoteCtrl(xrcpInv): return "/connect remote ctrl \(xrcpInv)" @@ -295,6 +307,9 @@ public enum ChatCommand { case let .apiDownloadStandaloneFile(userId, link, file): return "/_download \(userId) \(link) \(file.filePath)" case let .apiStandaloneFileInfo(link): return "/_download info \(link)" case .showVersion: return "/version" + case let .getAgentSubsTotal(userId): return "/get subs total \(userId)" + case let .getAgentServersSummary(userId): return "/get servers summary \(userId)" + case .resetAgentServersStats: return "/reset servers stats" case let .string(str): return str } } @@ -316,11 +331,11 @@ public enum ChatCommand { case .apiUnmuteUser: return "apiUnmuteUser" case .apiDeleteUser: return "apiDeleteUser" case .startChat: return "startChat" + case .checkChatRunning: return "checkChatRunning" case .apiStopChat: return "apiStopChat" case .apiActivateChat: return "apiActivateChat" case .apiSuspendChat: return "apiSuspendChat" - case .setTempFolder: return "setTempFolder" - case .setFilesFolder: return "setFilesFolder" + case .apiSetAppFilePaths: return "apiSetAppFilePaths" case .apiSetEncryptLocalFiles: return "apiSetEncryptLocalFiles" case .apiExportArchive: return "apiExportArchive" case .apiImportArchive: return "apiImportArchive" @@ -369,10 +384,13 @@ public enum ChatCommand { case .apiGetNetworkConfig: return "apiGetNetworkConfig" case .apiSetNetworkInfo: return "apiSetNetworkInfo" case .reconnectAllServers: return "reconnectAllServers" + case .reconnectServer: return "reconnectServer" case .apiSetChatSettings: return "apiSetChatSettings" case .apiSetMemberSettings: return "apiSetMemberSettings" case .apiContactInfo: return "apiContactInfo" case .apiGroupMemberInfo: return "apiGroupMemberInfo" + case .apiContactQueueInfo: return "apiContactQueueInfo" + case .apiGroupMemberQueueInfo: return "apiGroupMemberQueueInfo" case .apiSwitchContact: return "apiSwitchContact" case .apiSwitchGroupMember: return "apiSwitchGroupMember" case .apiAbortSwitchContact: return "apiAbortSwitchContact" @@ -394,6 +412,8 @@ public enum ChatCommand { case .apiSetContactPrefs: return "apiSetContactPrefs" case .apiSetContactAlias: return "apiSetContactAlias" case .apiSetConnectionAlias: return "apiSetConnectionAlias" + case .apiSetUserUIThemes: return "apiSetUserUIThemes" + case .apiSetChatUIThemes: return "apiSetChatUIThemes" case .apiCreateMyAddress: return "apiCreateMyAddress" case .apiDeleteMyAddress: return "apiDeleteMyAddress" case .apiShowMyAddress: return "apiShowMyAddress" @@ -427,6 +447,9 @@ public enum ChatCommand { case .apiDownloadStandaloneFile: return "apiDownloadStandaloneFile" case .apiStandaloneFileInfo: return "apiStandaloneFileInfo" case .showVersion: return "showVersion" + case .getAgentSubsTotal: return "getAgentSubsTotal" + case .getAgentServersSummary: return "getAgentServersSummary" + case .resetAgentServersStats: return "resetAgentServersStats" case .string: return "console command" } } @@ -477,10 +500,6 @@ public enum ChatCommand { return nil } - private func onOff(_ b: Bool) -> String { - b ? "on" : "off" - } - private func onOffParam(_ param: String, _ b: Bool?) -> String { if let b = b { return " \(param)=\(onOff(b))" @@ -493,6 +512,10 @@ public enum ChatCommand { } } +private func onOff(_ b: Bool) -> String { + b ? "on" : "off" +} + public struct APIResponse: Decodable { var resp: ChatResponse } @@ -514,6 +537,7 @@ public enum ChatResponse: Decodable, Error { case networkConfig(networkConfig: NetCfg) case contactInfo(user: UserRef, contact: Contact, connectionStats_: ConnectionStats?, customUserProfile: Profile?) case groupMemberInfo(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats_: ConnectionStats?) + case queueInfo(user: UserRef, rcvMsgInfo: RcvMsgInfo?, queueInfo: QueueInfo) case contactSwitchStarted(user: UserRef, contact: Contact, connectionStats: ConnectionStats) case groupMemberSwitchStarted(user: UserRef, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) case contactSwitchAborted(user: UserRef, contact: Contact, connectionStats: ConnectionStats) @@ -552,6 +576,7 @@ public enum ChatResponse: Decodable, Error { case userContactLinkDeleted(user: User) case contactConnected(user: UserRef, contact: Contact, userCustomProfile: Profile?) case contactConnecting(user: UserRef, contact: Contact) + case contactSndReady(user: UserRef, contact: Contact) case receivedContactRequest(user: UserRef, contactRequest: UserContactRequest) case acceptingContactRequest(user: UserRef, contact: Contact) case contactRequestRejected(user: UserRef) @@ -568,7 +593,7 @@ public enum ChatResponse: Decodable, Error { case chatItemUpdated(user: UserRef, chatItem: AChatItem) case chatItemNotChanged(user: UserRef, chatItem: AChatItem) case chatItemReaction(user: UserRef, added: Bool, reaction: ACIReaction) - case chatItemDeleted(user: UserRef, deletedChatItem: AChatItem, toChatItem: AChatItem?, byUser: Bool) + case chatItemsDeleted(user: UserRef, chatItemDeletions: [ChatItemDeletion], byUser: Bool) case contactsList(user: UserRef, contacts: [Contact]) // group events case groupCreated(user: UserRef, groupInfo: GroupInfo) @@ -613,7 +638,8 @@ public enum ChatResponse: Decodable, Error { case rcvStandaloneFileComplete(user: UserRef, targetPath: String, rcvFileTransfer: RcvFileTransfer) case rcvFileCancelled(user: UserRef, chatItem_: AChatItem?, rcvFileTransfer: RcvFileTransfer) case rcvFileSndCancelled(user: UserRef, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer) - case rcvFileError(user: UserRef, chatItem_: AChatItem?, rcvFileTransfer: RcvFileTransfer) + case rcvFileError(user: UserRef, chatItem_: AChatItem?, agentError: AgentErrorType, rcvFileTransfer: RcvFileTransfer) + case rcvFileWarning(user: UserRef, chatItem_: AChatItem?, agentError: AgentErrorType, rcvFileTransfer: RcvFileTransfer) // sending file events case sndFileStart(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer) case sndFileComplete(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer) @@ -626,7 +652,8 @@ public enum ChatResponse: Decodable, Error { case sndFileCompleteXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta) case sndStandaloneFileComplete(user: UserRef, fileTransferMeta: FileTransferMeta, rcvURIs: [String]) case sndFileCancelledXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta) - case sndFileError(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta) + case sndFileError(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, errorMessage: String) + case sndFileWarning(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, errorMessage: String) // call events case callInvitation(callInvitation: RcvCallInvitation) case callOffer(user: UserRef, contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool) @@ -636,9 +663,10 @@ public enum ChatResponse: Decodable, Error { case callInvitations(callInvitations: [RcvCallInvitation]) case ntfTokenStatus(status: NtfTknStatus) case ntfToken(token: DeviceToken, status: NtfTknStatus, ntfMode: NotificationsMode, ntfServer: String) - case ntfMessages(user_: User?, connEntity_: ConnectionEntity?, msgTs: Date?, ntfMessages: [NtfMsgInfo]) + case ntfMessages(user_: User?, connEntity_: ConnectionEntity?, msgTs: Date?, ntfMessage_: NtfMsgInfo?) case ntfMessage(user: UserRef, connEntity: ConnectionEntity, ntfMessage: NtfMsgInfo) case contactConnectionDeleted(user: UserRef, connection: PendingContactConnection) + case contactDisabled(user: UserRef, contact: Contact) // remote desktop responses/events case remoteCtrlList(remoteCtrls: [RemoteCtrlInfo]) case remoteCtrlFound(remoteCtrl: RemoteCtrlInfo, ctrlAppInfo_: CtrlAppInfo?, appVersion: String, compatible: Bool) @@ -651,8 +679,12 @@ public enum ChatResponse: Decodable, Error { // misc case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration]) case cmdOk(user: UserRef?) + case agentSubsTotal(user: UserRef, subsTotal: SMPServerSubs, hasSession: Bool) + case agentServersSummary(user: UserRef, serversSummary: PresentedServersSummary) + case agentSubsSummary(user: UserRef, subsSummary: SMPServerSubs) case chatCmdError(user_: UserRef?, chatError: ChatError) case chatError(user_: UserRef?, chatError: ChatError) + case archiveExported(archiveErrors: [ArchiveError]) case archiveImported(archiveErrors: [ArchiveError]) case appSettings(appSettings: AppSettings) @@ -675,6 +707,7 @@ public enum ChatResponse: Decodable, Error { case .networkConfig: return "networkConfig" case .contactInfo: return "contactInfo" case .groupMemberInfo: return "groupMemberInfo" + case .queueInfo: return "queueInfo" case .contactSwitchStarted: return "contactSwitchStarted" case .groupMemberSwitchStarted: return "groupMemberSwitchStarted" case .contactSwitchAborted: return "contactSwitchAborted" @@ -713,6 +746,7 @@ public enum ChatResponse: Decodable, Error { case .userContactLinkDeleted: return "userContactLinkDeleted" case .contactConnected: return "contactConnected" case .contactConnecting: return "contactConnecting" + case .contactSndReady: return "contactSndReady" case .receivedContactRequest: return "receivedContactRequest" case .acceptingContactRequest: return "acceptingContactRequest" case .contactRequestRejected: return "contactRequestRejected" @@ -729,7 +763,7 @@ public enum ChatResponse: Decodable, Error { case .chatItemUpdated: return "chatItemUpdated" case .chatItemNotChanged: return "chatItemNotChanged" case .chatItemReaction: return "chatItemReaction" - case .chatItemDeleted: return "chatItemDeleted" + case .chatItemsDeleted: return "chatItemsDeleted" case .contactsList: return "contactsList" case .groupCreated: return "groupCreated" case .sentGroupInvitation: return "sentGroupInvitation" @@ -773,6 +807,7 @@ public enum ChatResponse: Decodable, Error { case .rcvFileCancelled: return "rcvFileCancelled" case .rcvFileSndCancelled: return "rcvFileSndCancelled" case .rcvFileError: return "rcvFileError" + case .rcvFileWarning: return "rcvFileWarning" case .sndFileStart: return "sndFileStart" case .sndFileComplete: return "sndFileComplete" case .sndFileCancelled: return "sndFileCancelled" @@ -785,6 +820,7 @@ public enum ChatResponse: Decodable, Error { case .sndStandaloneFileComplete: return "sndStandaloneFileComplete" case .sndFileCancelledXFTP: return "sndFileCancelledXFTP" case .sndFileError: return "sndFileError" + case .sndFileWarning: return "sndFileWarning" case .callInvitation: return "callInvitation" case .callOffer: return "callOffer" case .callAnswer: return "callAnswer" @@ -796,6 +832,7 @@ public enum ChatResponse: Decodable, Error { case .ntfMessages: return "ntfMessages" case .ntfMessage: return "ntfMessage" case .contactConnectionDeleted: return "contactConnectionDeleted" + case .contactDisabled: return "contactDisabled" case .remoteCtrlList: return "remoteCtrlList" case .remoteCtrlFound: return "remoteCtrlFound" case .remoteCtrlConnecting: return "remoteCtrlConnecting" @@ -805,8 +842,12 @@ public enum ChatResponse: Decodable, Error { case .contactPQEnabled: return "contactPQEnabled" case .versionInfo: return "versionInfo" case .cmdOk: return "cmdOk" + case .agentSubsTotal: return "agentSubsTotal" + case .agentServersSummary: return "agentServersSummary" + case .agentSubsSummary: return "agentSubsSummary" case .chatCmdError: return "chatCmdError" case .chatError: return "chatError" + case .archiveExported: return "archiveExported" case .archiveImported: return "archiveImported" case .appSettings: return "appSettings" } @@ -832,6 +873,9 @@ public enum ChatResponse: Decodable, Error { case let .networkConfig(networkConfig): return String(describing: networkConfig) case let .contactInfo(u, contact, connectionStats_, customUserProfile): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats_: \(String(describing: connectionStats_))\ncustomUserProfile: \(String(describing: customUserProfile))") case let .groupMemberInfo(u, groupInfo, member, connectionStats_): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats_: \(String(describing: connectionStats_))") + case let .queueInfo(u, rcvMsgInfo, queueInfo): + let msgInfo = if let info = rcvMsgInfo { encodeJSON(info) } else { "none" } + return withUser(u, "rcvMsgInfo: \(msgInfo)\nqueueInfo: \(encodeJSON(queueInfo))") case let .contactSwitchStarted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))") case let .groupMemberSwitchStarted(u, groupInfo, member, connectionStats): return withUser(u, "groupInfo: \(String(describing: groupInfo))\nmember: \(String(describing: member))\nconnectionStats: \(String(describing: connectionStats))") case let .contactSwitchAborted(u, contact, connectionStats): return withUser(u, "contact: \(String(describing: contact))\nconnectionStats: \(String(describing: connectionStats))") @@ -870,6 +914,7 @@ public enum ChatResponse: Decodable, Error { case .userContactLinkDeleted: return noDetails case let .contactConnected(u, contact, _): return withUser(u, String(describing: contact)) case let .contactConnecting(u, contact): return withUser(u, String(describing: contact)) + case let .contactSndReady(u, contact): return withUser(u, String(describing: contact)) case let .receivedContactRequest(u, contactRequest): return withUser(u, String(describing: contactRequest)) case let .acceptingContactRequest(u, contact): return withUser(u, String(describing: contact)) case .contactRequestRejected: return noDetails @@ -886,7 +931,10 @@ public enum ChatResponse: Decodable, Error { case let .chatItemUpdated(u, chatItem): return withUser(u, String(describing: chatItem)) case let .chatItemNotChanged(u, chatItem): return withUser(u, String(describing: chatItem)) case let .chatItemReaction(u, added, reaction): return withUser(u, "added: \(added)\n\(String(describing: reaction))") - case let .chatItemDeleted(u, deletedChatItem, toChatItem, byUser): return withUser(u, "deletedChatItem:\n\(String(describing: deletedChatItem))\ntoChatItem:\n\(String(describing: toChatItem))\nbyUser: \(byUser)") + case let .chatItemsDeleted(u, items, byUser): + let itemsString = items.map { item in + "deletedChatItem:\n\(String(describing: item.deletedChatItem))\ntoChatItem:\n\(String(describing: item.toChatItem))" }.joined(separator: "\n") + return withUser(u, itemsString + "\nbyUser: \(byUser)") case let .contactsList(u, contacts): return withUser(u, String(describing: contacts)) case let .groupCreated(u, groupInfo): return withUser(u, String(describing: groupInfo)) case let .sentGroupInvitation(u, groupInfo, contact, member): return withUser(u, "groupInfo: \(groupInfo)\ncontact: \(contact)\nmember: \(member)") @@ -929,7 +977,8 @@ public enum ChatResponse: Decodable, Error { case let .rcvFileComplete(u, chatItem): return withUser(u, String(describing: chatItem)) case let .rcvFileCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem)) case let .rcvFileSndCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem)) - case let .rcvFileError(u, chatItem, _): return withUser(u, String(describing: chatItem)) + case let .rcvFileError(u, chatItem, agentError, _): return withUser(u, "agentError: \(String(describing: agentError))\nchatItem: \(String(describing: chatItem))") + case let .rcvFileWarning(u, chatItem, agentError, _): return withUser(u, "agentError: \(String(describing: agentError))\nchatItem: \(String(describing: chatItem))") case let .sndFileStart(u, chatItem, _): return withUser(u, String(describing: chatItem)) case let .sndFileComplete(u, chatItem, _): return withUser(u, String(describing: chatItem)) case let .sndFileCancelled(u, chatItem, _, _): return withUser(u, String(describing: chatItem)) @@ -941,7 +990,8 @@ public enum ChatResponse: Decodable, Error { case let .sndFileCompleteXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem)) case let .sndStandaloneFileComplete(u, _, rcvURIs): return withUser(u, String(rcvURIs.count)) case let .sndFileCancelledXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem)) - case let .sndFileError(u, chatItem, _): return withUser(u, String(describing: chatItem)) + case let .sndFileError(u, chatItem, _, err): return withUser(u, "error: \(String(describing: err))\nchatItem: \(String(describing: chatItem))") + case let .sndFileWarning(u, chatItem, _, err): return withUser(u, "error: \(String(describing: err))\nchatItem: \(String(describing: chatItem))") case let .callInvitation(inv): return String(describing: inv) case let .callOffer(u, contact, callType, offer, sharedKey, askConfirmation): return withUser(u, "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")\naskConfirmation: \(askConfirmation)\noffer: \(String(describing: offer))") case let .callAnswer(u, contact, answer): return withUser(u, "contact: \(contact.id)\nanswer: \(String(describing: answer))") @@ -953,17 +1003,22 @@ public enum ChatResponse: Decodable, Error { case let .ntfMessages(u, connEntity, msgTs, ntfMessages): return withUser(u, "connEntity: \(String(describing: connEntity))\nmsgTs: \(String(describing: msgTs))\nntfMessages: \(String(describing: ntfMessages))") case let .ntfMessage(u, connEntity, ntfMessage): return withUser(u, "connEntity: \(String(describing: connEntity))\nntfMessage: \(String(describing: ntfMessage))") case let .contactConnectionDeleted(u, connection): return withUser(u, String(describing: connection)) + case let .contactDisabled(u, contact): return withUser(u, String(describing: contact)) case let .remoteCtrlList(remoteCtrls): return String(describing: remoteCtrls) case let .remoteCtrlFound(remoteCtrl, ctrlAppInfo_, appVersion, compatible): return "remoteCtrl:\n\(String(describing: remoteCtrl))\nctrlAppInfo_:\n\(String(describing: ctrlAppInfo_))\nappVersion: \(appVersion)\ncompatible: \(compatible)" case let .remoteCtrlConnecting(remoteCtrl_, ctrlAppInfo, appVersion): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nctrlAppInfo:\n\(String(describing: ctrlAppInfo))\nappVersion: \(appVersion)" case let .remoteCtrlSessionCode(remoteCtrl_, sessionCode): return "remoteCtrl_:\n\(String(describing: remoteCtrl_))\nsessionCode: \(sessionCode)" case let .remoteCtrlConnected(remoteCtrl): return String(describing: remoteCtrl) - case .remoteCtrlStopped: return noDetails + case let .remoteCtrlStopped(rcsState, rcStopReason): return "rcsState: \(String(describing: rcsState))\nrcStopReason: \(String(describing: rcStopReason))" case let .contactPQEnabled(u, contact, pqEnabled): return withUser(u, "contact: \(String(describing: contact))\npqEnabled: \(pqEnabled)") case let .versionInfo(versionInfo, chatMigrations, agentMigrations): return "\(String(describing: versionInfo))\n\nchat migrations: \(chatMigrations.map(\.upName))\n\nagent migrations: \(agentMigrations.map(\.upName))" case .cmdOk: return noDetails + case let .agentSubsTotal(u, subsTotal, hasSession): return withUser(u, "subsTotal: \(String(describing: subsTotal))\nhasSession: \(hasSession)") + case let .agentServersSummary(u, serversSummary): return withUser(u, String(describing: serversSummary)) + case let .agentSubsSummary(u, subsSummary): return withUser(u, String(describing: subsSummary)) case let .chatCmdError(u, chatError): return withUser(u, String(describing: chatError)) case let .chatError(u, chatError): return withUser(u, String(describing: chatError)) + case let .archiveExported(archiveErrors): return String(describing: archiveErrors) case let .archiveImported(archiveErrors): return String(describing: archiveErrors) case let .appSettings(appSettings): return String(describing: appSettings) } @@ -988,20 +1043,41 @@ public func chatError(_ chatResponse: ChatResponse) -> ChatErrorType? { } } -public enum ConnectionPlan: Decodable { +public enum ChatDeleteMode: Codable { + case full(notify: Bool) + case entity(notify: Bool) + case messages + + var cmdString: String { + switch self { + case let .full(notify): "full notify=\(onOff(notify))" + case let .entity(notify): "entity notify=\(onOff(notify))" + case .messages: "messages" + } + } + + public var isEntity: Bool { + switch self { + case .entity: return true + default: return false + } + } +} + +public enum ConnectionPlan: Decodable, Hashable { case invitationLink(invitationLinkPlan: InvitationLinkPlan) case contactAddress(contactAddressPlan: ContactAddressPlan) case groupLink(groupLinkPlan: GroupLinkPlan) } -public enum InvitationLinkPlan: Decodable { +public enum InvitationLinkPlan: Decodable, Hashable { case ok case ownLink case connecting(contact_: Contact?) case known(contact: Contact) } -public enum ContactAddressPlan: Decodable { +public enum ContactAddressPlan: Decodable, Hashable { case ok case ownLink case connectingConfirmReconnect @@ -1010,7 +1086,7 @@ public enum ContactAddressPlan: Decodable { case contactViaAddress(contact: Contact) } -public enum GroupLinkPlan: Decodable { +public enum GroupLinkPlan: Decodable, Hashable { case ok case ownLink(groupInfo: GroupInfo) case connectingConfirmReconnect @@ -1018,13 +1094,12 @@ public enum GroupLinkPlan: Decodable { case known(groupInfo: GroupInfo) } -struct NewUser: Encodable { +struct NewUser: Encodable, Hashable { var profile: Profile? - var sameServers: Bool var pastTimestamp: Bool } -public enum ChatPagination { +public enum ChatPagination: Hashable { case last(count: Int) case after(chatItemId: Int64, count: Int) case before(chatItemId: Int64, count: Int) @@ -1080,10 +1155,10 @@ public struct ProtoServersConfig: Codable { public struct UserProtoServers: Decodable { public var serverProtocol: ServerProtocol public var protoServers: [ServerCfg] - public var presetServers: [String] + public var presetServers: [ServerCfg] } -public struct ServerCfg: Identifiable, Equatable, Codable { +public struct ServerCfg: Identifiable, Equatable, Codable, Hashable { public var server: String public var preset: Bool public var tested: Bool? @@ -1106,7 +1181,7 @@ public struct ServerCfg: Identifiable, Equatable, Codable { public var id: String { "\(server) \(createdAt)" } - public static var empty = ServerCfg(server: "", preset: false, tested: nil, enabled: true) + public static var empty = ServerCfg(server: "", preset: false, tested: nil, enabled: false) public var isEmpty: Bool { server.trimmingCharacters(in: .whitespaces) == "" @@ -1176,8 +1251,8 @@ public enum ProtocolTestStep: String, Decodable, Equatable { } public struct ProtocolTestFailure: Decodable, Error, Equatable { - var testStep: ProtocolTestStep - var testError: AgentErrorType + public var testStep: ProtocolTestStep + public var testError: AgentErrorType public static func == (l: ProtocolTestFailure, r: ProtocolTestFailure) -> Bool { l.testStep == r.testStep @@ -1186,7 +1261,7 @@ public struct ProtocolTestFailure: Decodable, Error, Equatable { public var localizedDescription: String { let err = String.localizedStringWithFormat(NSLocalizedString("Test failed at step %@.", comment: "server test failure"), testStep.text) switch testError { - case .SMP(.AUTH): + case .SMP(_, .AUTH): return err + " " + NSLocalizedString("Server requires authorization to create queues, check password", comment: "server test error") case .XFTP(.AUTH): return err + " " + NSLocalizedString("Server requires authorization to upload, check password", comment: "server test error") @@ -1240,56 +1315,110 @@ public struct ServerAddress: Decodable { ) } -public struct NetCfg: Codable, Equatable { +public struct NetCfg: Codable, Equatable, Hashable { public var socksProxy: String? = nil + var socksMode: SocksMode = .always public var hostMode: HostMode = .publicHost public var requiredHostMode = true - public var sessionMode: TransportSessionMode + public var sessionMode = TransportSessionMode.user + public var smpProxyMode: SMPProxyMode = .unknown + public var smpProxyFallback: SMPProxyFallback = .allowProtected public var tcpConnectTimeout: Int // microseconds public var tcpTimeout: Int // microseconds public var tcpTimeoutPerKb: Int // microseconds public var rcvConcurrency: Int // pool size - public var tcpKeepAlive: KeepAliveOpts? + public var tcpKeepAlive: KeepAliveOpts? = KeepAliveOpts.defaults public var smpPingInterval: Int // microseconds - public var smpPingCount: Int // times - public var logTLSErrors: Bool + public var smpPingCount: Int = 3 // times + public var logTLSErrors: Bool = false public static let defaults: NetCfg = NetCfg( - socksProxy: nil, - sessionMode: TransportSessionMode.user, tcpConnectTimeout: 25_000_000, tcpTimeout: 15_000_000, tcpTimeoutPerKb: 10_000, rcvConcurrency: 12, - tcpKeepAlive: KeepAliveOpts.defaults, - smpPingInterval: 1200_000_000, - smpPingCount: 3, - logTLSErrors: false + smpPingInterval: 1200_000_000 ) - public static let proxyDefaults: NetCfg = NetCfg( - socksProxy: nil, - sessionMode: TransportSessionMode.user, + static let proxyDefaults: NetCfg = NetCfg( tcpConnectTimeout: 35_000_000, tcpTimeout: 20_000_000, tcpTimeoutPerKb: 15_000, rcvConcurrency: 8, - tcpKeepAlive: KeepAliveOpts.defaults, - smpPingInterval: 1200_000_000, - smpPingCount: 3, - logTLSErrors: false + smpPingInterval: 1200_000_000 ) + + public var withProxyTimeouts: NetCfg { + var cfg = self + cfg.tcpConnectTimeout = NetCfg.proxyDefaults.tcpConnectTimeout + cfg.tcpTimeout = NetCfg.proxyDefaults.tcpTimeout + cfg.tcpTimeoutPerKb = NetCfg.proxyDefaults.tcpTimeoutPerKb + cfg.rcvConcurrency = NetCfg.proxyDefaults.rcvConcurrency + cfg.smpPingInterval = NetCfg.proxyDefaults.smpPingInterval + return cfg + } + + public var hasProxyTimeouts: Bool { + tcpConnectTimeout == NetCfg.proxyDefaults.tcpConnectTimeout && + tcpTimeout == NetCfg.proxyDefaults.tcpTimeout && + tcpTimeoutPerKb == NetCfg.proxyDefaults.tcpTimeoutPerKb && + rcvConcurrency == NetCfg.proxyDefaults.rcvConcurrency && + smpPingInterval == NetCfg.proxyDefaults.smpPingInterval + } public var enableKeepAlive: Bool { tcpKeepAlive != nil } } -public enum HostMode: String, Codable { +public enum HostMode: String, Codable, Hashable { case onionViaSocks case onionHost = "onion" case publicHost = "public" } -public enum OnionHosts: String, Identifiable { +public enum SocksMode: String, Codable, Hashable { + case always = "always" + case onion = "onion" +} + +public enum SMPProxyMode: String, Codable, Hashable, SelectableItem { + case always = "always" + case unknown = "unknown" + case unprotected = "unprotected" + case never = "never" + + public var label: LocalizedStringKey { + switch self { + case .always: return "always" + case .unknown: return "unknown servers" + case .unprotected: return "unprotected" + case .never: return "never" + } + } + + public var id: SMPProxyMode { self } + + public static let values: [SMPProxyMode] = [.always, .unknown, .unprotected, .never] +} + +public enum SMPProxyFallback: String, Codable, Hashable, SelectableItem { + case allow = "allow" + case allowProtected = "allowProtected" + case prohibit = "prohibit" + + public var label: LocalizedStringKey { + switch self { + case .allow: return "yes" + case .allowProtected: return "when IP hidden" + case .prohibit: return "no" + } + } + + public var id: SMPProxyFallback { self } + + public static let values: [SMPProxyFallback] = [.allow, .allowProtected, .prohibit] +} + +public enum OnionHosts: String, Identifiable, Hashable { case no case prefer case require @@ -1323,7 +1452,7 @@ public enum OnionHosts: String, Identifiable { public static let values: [OnionHosts] = [.no, .prefer, .require] } -public enum TransportSessionMode: String, Codable, Identifiable { +public enum TransportSessionMode: String, Codable, Identifiable, Hashable { case user case entity @@ -1339,7 +1468,7 @@ public enum TransportSessionMode: String, Codable, Identifiable { public static let values: [TransportSessionMode] = [.user, .entity] } -public struct KeepAliveOpts: Codable, Equatable { +public struct KeepAliveOpts: Codable, Equatable, Hashable { public var keepIdle: Int // seconds public var keepIntvl: Int // seconds public var keepCnt: Int // times @@ -1347,7 +1476,7 @@ public struct KeepAliveOpts: Codable, Equatable { public static let defaults: KeepAliveOpts = KeepAliveOpts(keepIdle: 30, keepIntvl: 15, keepCnt: 4) } -public enum NetworkStatus: Decodable, Equatable { +public enum NetworkStatus: Decodable, Equatable, Hashable { case unknown case connected case disconnected @@ -1385,12 +1514,12 @@ public enum NetworkStatus: Decodable, Equatable { } } -public struct ConnNetworkStatus: Decodable { +public struct ConnNetworkStatus: Decodable, Hashable { public var agentConnId: String public var networkStatus: NetworkStatus } -public struct ChatSettings: Codable { +public struct ChatSettings: Codable, Hashable { public var enableNtfs: MsgFilter public var sendRcpts: Bool? public var favorite: Bool @@ -1404,13 +1533,13 @@ public struct ChatSettings: Codable { public static let defaults: ChatSettings = ChatSettings(enableNtfs: .all, sendRcpts: nil, favorite: false) } -public enum MsgFilter: String, Codable { +public enum MsgFilter: String, Codable, Hashable { case none case all case mentions } -public struct UserMsgReceiptSettings: Codable { +public struct UserMsgReceiptSettings: Codable, Hashable { public var enable: Bool public var clearOverrides: Bool @@ -1420,7 +1549,7 @@ public struct UserMsgReceiptSettings: Codable { } } -public struct ConnectionStats: Decodable { +public struct ConnectionStats: Decodable, Hashable { public var connAgentVersion: Int public var rcvQueuesInfo: [RcvQueueInfo] public var sndQueuesInfo: [SndQueueInfo] @@ -1436,30 +1565,30 @@ public struct ConnectionStats: Decodable { } } -public struct RcvQueueInfo: Codable { +public struct RcvQueueInfo: Codable, Hashable { public var rcvServer: String public var rcvSwitchStatus: RcvSwitchStatus? public var canAbortSwitch: Bool } -public enum RcvSwitchStatus: String, Codable { +public enum RcvSwitchStatus: String, Codable, Hashable { case switchStarted = "switch_started" case sendingQADD = "sending_qadd" case sendingQUSE = "sending_quse" case receivedMessage = "received_message" } -public struct SndQueueInfo: Codable { +public struct SndQueueInfo: Codable, Hashable { public var sndServer: String public var sndSwitchStatus: SndSwitchStatus? } -public enum SndSwitchStatus: String, Codable { +public enum SndSwitchStatus: String, Codable, Hashable { case sendingQKEY = "sending_qkey" case sendingQTEST = "sending_qtest" } -public enum QueueDirection: String, Decodable { +public enum QueueDirection: String, Decodable, Hashable { case rcv case snd } @@ -1483,7 +1612,7 @@ public enum RatchetSyncState: String, Decodable { case agreed } -public struct UserContactLink: Decodable { +public struct UserContactLink: Decodable, Hashable { public var connReqContact: String public var autoAccept: AutoAccept? @@ -1497,7 +1626,7 @@ public struct UserContactLink: Decodable { } } -public struct AutoAccept: Codable { +public struct AutoAccept: Codable, Hashable { public var acceptIncognito: Bool public var autoReply: MsgContent? @@ -1519,7 +1648,7 @@ public protocol SelectableItem: Hashable, Identifiable { static var values: [Self] { get } } -public struct DeviceToken: Decodable { +public struct DeviceToken: Decodable, Hashable { var pushProvider: PushProvider var token: String @@ -1533,12 +1662,12 @@ public struct DeviceToken: Decodable { } } -public enum PushEnvironment: String { +public enum PushEnvironment: String, Hashable { case development case production } -public enum PushProvider: String, Decodable { +public enum PushProvider: String, Decodable, Hashable { case apns_dev case apns_prod @@ -1552,7 +1681,7 @@ public enum PushProvider: String, Decodable { // This notification mode is for app core, UI uses AppNotificationsMode.off to mean completely disable, // and .local for periodic background checks -public enum NotificationsMode: String, Decodable, SelectableItem { +public enum NotificationsMode: String, Decodable, SelectableItem, Hashable { case off = "OFF" case periodic = "PERIODIC" case instant = "INSTANT" @@ -1570,7 +1699,7 @@ public enum NotificationsMode: String, Decodable, SelectableItem { public static var values: [NotificationsMode] = [.instant, .periodic, .off] } -public enum NotificationPreviewMode: String, SelectableItem, Codable { +public enum NotificationPreviewMode: String, SelectableItem, Codable, Hashable { case hidden case contact case message @@ -1588,7 +1717,7 @@ public enum NotificationPreviewMode: String, SelectableItem, Codable { public static var values: [NotificationPreviewMode] = [.message, .contact, .hidden] } -public struct RemoteCtrlInfo: Decodable { +public struct RemoteCtrlInfo: Decodable, Hashable { public var remoteCtrlId: Int64 public var ctrlDeviceName: String public var sessionState: RemoteCtrlSessionState? @@ -1598,7 +1727,7 @@ public struct RemoteCtrlInfo: Decodable { } } -public enum RemoteCtrlSessionState: Decodable { +public enum RemoteCtrlSessionState: Decodable, Hashable { case starting case searching case connecting @@ -1613,17 +1742,17 @@ public enum RemoteCtrlStopReason: Decodable { case disconnected } -public struct CtrlAppInfo: Decodable { +public struct CtrlAppInfo: Decodable, Hashable { public var appVersionRange: AppVersionRange public var deviceName: String } -public struct AppVersionRange: Decodable { +public struct AppVersionRange: Decodable, Hashable { public var minVersion: String public var maxVersion: String } -public struct CoreVersionInfo: Decodable { +public struct CoreVersionInfo: Decodable, Hashable { public var version: String public var simplexmqVersion: String public var simplexmqCommit: String @@ -1645,7 +1774,7 @@ private func encodeCJSON(_ value: T) -> [CChar] { encodeJSON(value).cString(using: .utf8)! } -public enum ChatError: Decodable { +public enum ChatError: Decodable, Hashable { case error(errorType: ChatErrorType) case errorAgent(agentError: AgentErrorType) case errorStore(storeError: StoreError) @@ -1654,7 +1783,7 @@ public enum ChatError: Decodable { case invalidJSON(json: String) } -public enum ChatErrorType: Decodable { +public enum ChatErrorType: Decodable, Hashable { case noActiveUser case noConnectionUser(agentConnId: String) case noSndFileUser(agentSndFileId: String) @@ -1692,7 +1821,6 @@ public enum ChatErrorType: Decodable { case groupMemberNotActive case groupMemberUserRemoved case groupMemberNotFound - case groupMemberIntroNotFound(contactName: ContactName) case groupCantResendInvitation(groupInfo: GroupInfo, contactName: ContactName) case groupInternal(message: String) case fileNotFound(message: String) @@ -1709,8 +1837,7 @@ public enum ChatErrorType: Decodable { case fileImageType(filePath: String) case fileImageSize(filePath: String) case fileNotReceived(fileId: Int64) - // case xFTPRcvFile - // case xFTPSndFile + case fileNotApproved(fileId: Int64, unknownServers: [String]) case fallbackToSMPProhibited(fileId: Int64) case inlineFileProhibited(fileId: Int64) case invalidQuote @@ -1735,7 +1862,7 @@ public enum ChatErrorType: Decodable { case exception(message: String) } -public enum StoreError: Decodable { +public enum StoreError: Decodable, Hashable { case duplicateName case userNotFound(userId: Int64) case userNotFoundByName(contactName: ContactName) @@ -1795,7 +1922,7 @@ public enum StoreError: Decodable { case noGroupSndStatus(itemId: Int64, groupMemberId: Int64) } -public enum DatabaseError: Decodable { +public enum DatabaseError: Decodable, Hashable { case errorEncrypted case errorPlaintext case errorNoFile(dbFile: String) @@ -1803,17 +1930,18 @@ public enum DatabaseError: Decodable { case errorOpen(sqliteError: SQLiteError) } -public enum SQLiteError: Decodable { +public enum SQLiteError: Decodable, Hashable { case errorNotADatabase - case error(String) + case error(dbError: String) } -public enum AgentErrorType: Decodable { +public enum AgentErrorType: Decodable, Hashable { case CMD(cmdErr: CommandErrorType) case CONN(connErr: ConnectionErrorType) - case SMP(smpErr: ProtocolErrorType) + case SMP(serverAddress: String, smpErr: ProtocolErrorType) case NTF(ntfErr: ProtocolErrorType) case XFTP(xftpErr: XFTPErrorType) + case PROXY(proxyServer: String, relayServer: String, proxyErr: ProxyClientError) case RCP(rcpErr: RCErrorType) case BROKER(brokerAddress: String, brokerErr: BrokerErrorType) case AGENT(agentErr: SMPAgentError) @@ -1822,7 +1950,7 @@ public enum AgentErrorType: Decodable { case INACTIVE } -public enum CommandErrorType: Decodable { +public enum CommandErrorType: Decodable, Hashable { case PROHIBITED case SYNTAX case NO_CONN @@ -1830,7 +1958,7 @@ public enum CommandErrorType: Decodable { case LARGE } -public enum ConnectionErrorType: Decodable { +public enum ConnectionErrorType: Decodable, Hashable { case NOT_FOUND case DUPLICATE case SIMPLEX @@ -1838,7 +1966,7 @@ public enum ConnectionErrorType: Decodable { case NOT_AVAILABLE } -public enum BrokerErrorType: Decodable { +public enum BrokerErrorType: Decodable, Hashable { case RESPONSE(smpErr: String) case UNEXPECTED case NETWORK @@ -1847,18 +1975,28 @@ public enum BrokerErrorType: Decodable { case TIMEOUT } -public enum ProtocolErrorType: Decodable { +public enum ProtocolErrorType: Decodable, Hashable { case BLOCK case SESSION case CMD(cmdErr: ProtocolCommandError) + indirect case PROXY(proxyErr: ProxyError) case AUTH + case CRYPTO case QUOTA case NO_MSG case LARGE_MSG + case EXPIRED case INTERNAL } -public enum XFTPErrorType: Decodable { +public enum ProxyError: Decodable, Hashable { + case PROTOCOL(protocolErr: ProtocolErrorType) + case BROKER(brokerErr: BrokerErrorType) + case BASIC_AUTH + case NO_SESSION +} + +public enum XFTPErrorType: Decodable, Hashable { case BLOCK case SESSION case CMD(cmdErr: ProtocolCommandError) @@ -1875,7 +2013,13 @@ public enum XFTPErrorType: Decodable { case INTERNAL } -public enum RCErrorType: Decodable { +public enum ProxyClientError: Decodable, Hashable { + case protocolError(protocolErr: ProtocolErrorType) + case unexpectedResponse(responseStr: String) + case responseError(responseErr: ProtocolErrorType) +} + +public enum RCErrorType: Decodable, Hashable { case `internal`(internalErr: String) case identity case noLocalAddress @@ -1893,7 +2037,7 @@ public enum RCErrorType: Decodable { case syntax(syntaxErr: String) } -public enum ProtocolCommandError: Decodable { +public enum ProtocolCommandError: Decodable, Hashable { case UNKNOWN case SYNTAX case PROHIBITED @@ -1902,22 +2046,23 @@ public enum ProtocolCommandError: Decodable { case NO_ENTITY } -public enum ProtocolTransportError: Decodable { +public enum ProtocolTransportError: Decodable, Hashable { case badBlock + case version case largeMsg case badSession case noServerAuth case handshake(handshakeErr: SMPHandshakeError) } -public enum SMPHandshakeError: Decodable { +public enum SMPHandshakeError: Decodable, Hashable { case PARSE case VERSION case IDENTITY case BAD_AUTH } -public enum SMPAgentError: Decodable { +public enum SMPAgentError: Decodable, Hashable { case A_MESSAGE case A_PROHIBITED case A_VERSION @@ -1926,12 +2071,12 @@ public enum SMPAgentError: Decodable { case A_QUEUE(queueErr: String) } -public enum ArchiveError: Decodable { - case `import`(chatError: ChatError) - case importFile(file: String, chatError: ChatError) +public enum ArchiveError: Decodable, Hashable { + case `import`(importError: String) + case fileError(file: String, fileError: String) } -public enum RemoteCtrlError: Decodable { +public enum RemoteCtrlError: Decodable, Hashable { case inactive case badState case busy @@ -1945,14 +2090,14 @@ public enum RemoteCtrlError: Decodable { case protocolError } -public struct MigrationFileLinkData: Codable { +public struct MigrationFileLinkData: Codable, Hashable { let networkConfig: NetworkConfig? public init(networkConfig: NetworkConfig) { self.networkConfig = networkConfig } - public struct NetworkConfig: Codable { + public struct NetworkConfig: Codable, Hashable { let socksProxy: String? let hostMode: HostMode? let requiredHostMode: Bool? @@ -1984,14 +2129,16 @@ public struct MigrationFileLinkData: Codable { } } -public struct AppSettings: Codable, Equatable { +public struct AppSettings: Codable, Equatable, Hashable { public var networkConfig: NetCfg? = nil public var privacyEncryptLocalFiles: Bool? = nil + public var privacyAskToApproveRelays: Bool? = nil public var privacyAcceptImages: Bool? = nil public var privacyLinkPreviews: Bool? = nil public var privacyShowChatPreviews: Bool? = nil public var privacySaveLastDraft: Bool? = nil public var privacyProtectScreen: Bool? = nil + public var privacyMediaBlurRadius: Int? = nil public var notificationMode: AppSettingsNotificationMode? = nil public var notificationPreviewMode: NotificationPreviewMode? = nil public var webrtcPolicyRelay: Bool? = nil @@ -2004,17 +2151,25 @@ public struct AppSettings: Codable, Equatable { public var androidCallOnLockScreen: AppSettingsLockScreenCalls? = nil public var iosCallKitEnabled: Bool? = nil public var iosCallKitCallsInRecents: Bool? = nil - + public var uiProfileImageCornerRadius: Double? = nil + public var uiColorScheme: String? = nil + public var uiDarkColorScheme: String? = nil + public var uiCurrentThemeIds: [String: String]? = nil + public var uiThemes: [ThemeOverrides]? = nil + public var oneHandUI: Bool? = nil + public func prepareForExport() -> AppSettings { var empty = AppSettings() let def = AppSettings.defaults if networkConfig != def.networkConfig { empty.networkConfig = networkConfig } if privacyEncryptLocalFiles != def.privacyEncryptLocalFiles { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles } + if privacyAskToApproveRelays != def.privacyAskToApproveRelays { empty.privacyAskToApproveRelays = privacyAskToApproveRelays } if privacyAcceptImages != def.privacyAcceptImages { empty.privacyAcceptImages = privacyAcceptImages } if privacyLinkPreviews != def.privacyLinkPreviews { empty.privacyLinkPreviews = privacyLinkPreviews } if privacyShowChatPreviews != def.privacyShowChatPreviews { empty.privacyShowChatPreviews = privacyShowChatPreviews } if privacySaveLastDraft != def.privacySaveLastDraft { empty.privacySaveLastDraft = privacySaveLastDraft } if privacyProtectScreen != def.privacyProtectScreen { empty.privacyProtectScreen = privacyProtectScreen } + if privacyMediaBlurRadius != def.privacyMediaBlurRadius { empty.privacyMediaBlurRadius = privacyMediaBlurRadius } if notificationMode != def.notificationMode { empty.notificationMode = notificationMode } if notificationPreviewMode != def.notificationPreviewMode { empty.notificationPreviewMode = notificationPreviewMode } if webrtcPolicyRelay != def.webrtcPolicyRelay { empty.webrtcPolicyRelay = webrtcPolicyRelay } @@ -2027,6 +2182,12 @@ public struct AppSettings: Codable, Equatable { if androidCallOnLockScreen != def.androidCallOnLockScreen { empty.androidCallOnLockScreen = androidCallOnLockScreen } if iosCallKitEnabled != def.iosCallKitEnabled { empty.iosCallKitEnabled = iosCallKitEnabled } if iosCallKitCallsInRecents != def.iosCallKitCallsInRecents { empty.iosCallKitCallsInRecents = iosCallKitCallsInRecents } + if uiProfileImageCornerRadius != def.uiProfileImageCornerRadius { empty.uiProfileImageCornerRadius = uiProfileImageCornerRadius } + if uiColorScheme != def.uiColorScheme { empty.uiColorScheme = uiColorScheme } + if uiDarkColorScheme != def.uiDarkColorScheme { empty.uiDarkColorScheme = uiDarkColorScheme } + if uiCurrentThemeIds != def.uiCurrentThemeIds { empty.uiCurrentThemeIds = uiCurrentThemeIds } + if uiThemes != def.uiThemes { empty.uiThemes = uiThemes } + if oneHandUI != def.oneHandUI { empty.oneHandUI = oneHandUI } return empty } @@ -2034,11 +2195,13 @@ public struct AppSettings: Codable, Equatable { AppSettings ( networkConfig: NetCfg.defaults, privacyEncryptLocalFiles: true, + privacyAskToApproveRelays: true, privacyAcceptImages: true, privacyLinkPreviews: true, privacyShowChatPreviews: true, privacySaveLastDraft: true, privacyProtectScreen: false, + privacyMediaBlurRadius: 0, notificationMode: AppSettingsNotificationMode.instant, notificationPreviewMode: NotificationPreviewMode.message, webrtcPolicyRelay: true, @@ -2050,12 +2213,18 @@ public struct AppSettings: Codable, Equatable { confirmDBUpgrades: false, androidCallOnLockScreen: AppSettingsLockScreenCalls.show, iosCallKitEnabled: true, - iosCallKitCallsInRecents: false + iosCallKitCallsInRecents: false, + uiProfileImageCornerRadius: 22.5, + uiColorScheme: DefaultTheme.SYSTEM_THEME_NAME, + uiDarkColorScheme: DefaultTheme.SIMPLEX.themeName, + uiCurrentThemeIds: nil as [String: String]?, + uiThemes: nil as [ThemeOverrides]?, + oneHandUI: false ) } } -public enum AppSettingsNotificationMode: String, Codable { +public enum AppSettingsNotificationMode: String, Codable, Hashable { case off case periodic case instant @@ -2083,13 +2252,13 @@ public enum AppSettingsNotificationMode: String, Codable { // case message //} -public enum AppSettingsLockScreenCalls: String, Codable { +public enum AppSettingsLockScreenCalls: String, Codable, Hashable { case disable case show case accept } -public struct UserNetworkInfo: Codable, Equatable { +public struct UserNetworkInfo: Codable, Equatable, Hashable { public let networkType: UserNetworkType public let online: Bool @@ -2099,7 +2268,7 @@ public struct UserNetworkInfo: Codable, Equatable { } } -public enum UserNetworkType: String, Codable { +public enum UserNetworkType: String, Codable, Hashable { case none case cellular case wifi @@ -2116,3 +2285,203 @@ public enum UserNetworkType: String, Codable { } } } + +public struct RcvMsgInfo: Codable, Hashable { + var msgId: Int64 + var msgDeliveryId: Int64 + var msgDeliveryStatus: String + var agentMsgId: Int64 + var agentMsgMeta: String +} + +public struct QueueInfo: Codable, Hashable { + var qiSnd: Bool + var qiNtf: Bool + var qiSub: QSub? + var qiSize: Int + var qiMsg: MsgInfo? +} + +public struct QSub: Codable, Hashable { + var qSubThread: QSubThread + var qDelivered: String? +} + +public enum QSubThread: String, Codable, Hashable { + case noSub + case subPending + case subThread + case prohibitSub +} + +public struct MsgInfo: Codable, Hashable { + var msgId: String + var msgTs: Date + var msgType: MsgType +} + +public enum MsgType: String, Codable, Hashable { + case message + case quota +} + +public struct AppFilePaths: Encodable { + public let appFilesFolder: String + public let appTempFolder: String + public let appAssetsFolder: String +} + +public struct PresentedServersSummary: Codable { + public var statsStartedAt: Date + public var allUsersSMP: SMPServersSummary + public var allUsersXFTP: XFTPServersSummary + public var currentUserSMP: SMPServersSummary + public var currentUserXFTP: XFTPServersSummary +} + +public struct SMPServersSummary: Codable { + public var smpTotals: SMPTotals + public var currentlyUsedSMPServers: [SMPServerSummary] + public var previouslyUsedSMPServers: [SMPServerSummary] + public var onlyProxiedSMPServers: [SMPServerSummary] +} + +public struct SMPTotals: Codable { + public var sessions: ServerSessions + public var subs: SMPServerSubs + public var stats: AgentSMPServerStatsData +} + +public struct SMPServerSummary: Codable, Identifiable { + public var smpServer: String + public var known: Bool? + public var sessions: ServerSessions? + public var subs: SMPServerSubs? + public var stats: AgentSMPServerStatsData? + + public var id: String { smpServer } + + public var hasSubs: Bool { subs != nil } + + public var sessionsOrNew: ServerSessions { sessions ?? ServerSessions.newServerSessions } + + public var subsOrNew: SMPServerSubs { subs ?? SMPServerSubs.newSMPServerSubs } +} + +public struct ServerSessions: Codable { + public var ssConnected: Int + public var ssErrors: Int + public var ssConnecting: Int + + static public var newServerSessions = ServerSessions( + ssConnected: 0, + ssErrors: 0, + ssConnecting: 0 + ) + + public var hasSess: Bool { ssConnected > 0 } +} + +public struct SMPServerSubs: Codable { + public var ssActive: Int + public var ssPending: Int + + public init(ssActive: Int, ssPending: Int) { + self.ssActive = ssActive + self.ssPending = ssPending + } + + static public var newSMPServerSubs = SMPServerSubs( + ssActive: 0, + ssPending: 0 + ) + + public var total: Int { ssActive + ssPending } + + public var shareOfActive: Double { + guard total != 0 else { return 0.0 } + return Double(ssActive) / Double(total) + } +} + +public struct AgentSMPServerStatsData: Codable { + public var _sentDirect: Int + public var _sentViaProxy: Int + public var _sentProxied: Int + public var _sentDirectAttempts: Int + public var _sentViaProxyAttempts: Int + public var _sentProxiedAttempts: Int + public var _sentAuthErrs: Int + public var _sentQuotaErrs: Int + public var _sentExpiredErrs: Int + public var _sentOtherErrs: Int + public var _recvMsgs: Int + public var _recvDuplicates: Int + public var _recvCryptoErrs: Int + public var _recvErrs: Int + public var _ackMsgs: Int + public var _ackAttempts: Int + public var _ackNoMsgErrs: Int + public var _ackOtherErrs: Int + public var _connCreated: Int + public var _connSecured: Int + public var _connCompleted: Int + public var _connDeleted: Int + public var _connDelAttempts: Int + public var _connDelErrs: Int + public var _connSubscribed: Int + public var _connSubAttempts: Int + public var _connSubIgnored: Int + public var _connSubErrs: Int + public var _ntfKey: Int + public var _ntfKeyAttempts: Int + public var _ntfKeyDeleted: Int + public var _ntfKeyDeleteAttempts: Int +} + +public struct XFTPServersSummary: Codable { + public var xftpTotals: XFTPTotals + public var currentlyUsedXFTPServers: [XFTPServerSummary] + public var previouslyUsedXFTPServers: [XFTPServerSummary] +} + +public struct XFTPTotals: Codable { + public var sessions: ServerSessions + public var stats: AgentXFTPServerStatsData +} + +public struct XFTPServerSummary: Codable, Identifiable { + public var xftpServer: String + public var known: Bool? + public var sessions: ServerSessions? + public var stats: AgentXFTPServerStatsData? + public var rcvInProgress: Bool + public var sndInProgress: Bool + public var delInProgress: Bool + + public var id: String { xftpServer } +} + +public struct AgentXFTPServerStatsData: Codable { + public var _uploads: Int + public var _uploadsSize: Int64 + public var _uploadAttempts: Int + public var _uploadErrs: Int + public var _downloads: Int + public var _downloadsSize: Int64 + public var _downloadAttempts: Int + public var _downloadAuthErrs: Int + public var _downloadErrs: Int + public var _deletions: Int + public var _deleteAttempts: Int + public var _deleteErrs: Int +} + +public struct AgentNtfServerStatsData: Codable { + public var _ntfCreated: Int + public var _ntfCreateAttempts: Int + public var _ntfChecked: Int + public var _ntfCheckAttempts: Int + public var _ntfDeleted: Int + public var _ntfDelAttempts: Int +} diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index 0511a8486c..bd38f3568c 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -11,21 +11,34 @@ import SwiftUI public let appSuspendTimeout: Int = 15 // seconds +public let defaultProfileImageCorner: Double = 22.5 + let GROUP_DEFAULT_APP_STATE = "appState" let GROUP_DEFAULT_NSE_STATE = "nseState" +let GROUP_DEFAULT_SE_STATE = "seState" let GROUP_DEFAULT_DB_CONTAINER = "dbContainer" public let GROUP_DEFAULT_CHAT_LAST_START = "chatLastStart" public let GROUP_DEFAULT_CHAT_LAST_BACKGROUND_RUN = "chatLastBackgroundRun" let GROUP_DEFAULT_NTF_PREVIEW_MODE = "ntfPreviewMode" public let GROUP_DEFAULT_NTF_ENABLE_LOCAL = "ntfEnableLocal" // no longer used public let GROUP_DEFAULT_NTF_ENABLE_PERIODIC = "ntfEnablePeriodic" // no longer used +// replaces DEFAULT_PERFORM_LA +let GROUP_DEFAULT_APP_LOCAL_AUTH_ENABLED = "appLocalAuthEnabled" +public let GROUP_DEFAULT_ALLOW_SHARE_EXTENSION = "allowShareExtension" +// replaces DEFAULT_PRIVACY_LINK_PREVIEWS +let GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS = "privacyLinkPreviews" // This setting is a main one, while having an unused duplicate from the past: DEFAULT_PRIVACY_ACCEPT_IMAGES let GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES = "privacyAcceptImages" public let GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE = "privacyTransferImagesInline" // no longer used public let GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES = "privacyEncryptLocalFiles" +public let GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS = "privacyAskToApproveRelays" +// replaces DEFAULT_PROFILE_IMAGE_CORNER_RADIUS +public let GROUP_DEFAULT_PROFILE_IMAGE_CORNER_RADIUS = "profileImageCornerRadius" let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount" let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts" let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode" +let GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE = "networkSMPProxyMode" +let GROUP_DEFAULT_NETWORK_SMP_PROXY_FALLBACK = "networkSMPProxyFallback" let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT = "networkTCPConnectTimeout" let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT = "networkTCPTimeout" let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB = "networkTCPTimeoutPerKb" @@ -42,6 +55,7 @@ public let GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE = "initialRandomDBPassphra public let GROUP_DEFAULT_CONFIRM_DB_UPGRADES = "confirmDBUpgrades" public let GROUP_DEFAULT_CALL_KIT_ENABLED = "callKitEnabled" public let GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED = "pqExperimentalEnabled" // no longer used +public let GROUP_DEFAULT_ONE_HAND_UI = "oneHandUI" public let APP_GROUP_NAME = "group.chat.simplex.app" @@ -53,6 +67,8 @@ public func registerGroupDefaults() { GROUP_DEFAULT_NTF_ENABLE_PERIODIC: false, GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS: OnionHosts.no.rawValue, GROUP_DEFAULT_NETWORK_SESSION_MODE: TransportSessionMode.user.rawValue, + GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE: SMPProxyMode.unknown.rawValue, + GROUP_DEFAULT_NETWORK_SMP_PROXY_FALLBACK: SMPProxyFallback.allowProtected.rawValue, GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT: NetCfg.defaults.tcpConnectTimeout, GROUP_DEFAULT_NETWORK_TCP_TIMEOUT: NetCfg.defaults.tcpTimeout, GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB: NetCfg.defaults.tcpTimeoutPerKb, @@ -66,12 +82,18 @@ public func registerGroupDefaults() { GROUP_DEFAULT_INCOGNITO: false, GROUP_DEFAULT_STORE_DB_PASSPHRASE: true, GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE: false, + GROUP_DEFAULT_APP_LOCAL_AUTH_ENABLED: true, + GROUP_DEFAULT_ALLOW_SHARE_EXTENSION: false, + GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS: false, GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES: true, GROUP_DEFAULT_PRIVACY_TRANSFER_IMAGES_INLINE: false, GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES: true, + GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS: true, + GROUP_DEFAULT_PROFILE_IMAGE_CORNER_RADIUS: defaultProfileImageCorner, GROUP_DEFAULT_CONFIRM_DB_UPGRADES: false, GROUP_DEFAULT_CALL_KIT_ENABLED: true, GROUP_DEFAULT_PQ_EXPERIMENTAL_ENABLED: false, + GROUP_DEFAULT_ONE_HAND_UI: true ]) } @@ -130,6 +152,11 @@ public enum NSEState: String, Codable { } } +public enum SEState: String, Codable { + case inactive + case sendingMessage +} + public enum DBContainer: String { case documents case group @@ -149,6 +176,12 @@ public let nseStateGroupDefault = EnumDefault( withDefault: .suspended // so that NSE that was never launched does not delay the app from resuming ) +public let seStateGroupDefault = EnumDefault( + defaults: groupDefaults, + forKey: GROUP_DEFAULT_SE_STATE, + withDefault: .inactive +) + // inactive app states do not include "stopped" state public func allowBackgroundRefresh() -> Bool { appStateGroupDefault.get().inactive && nseStateGroupDefault.get().inactive @@ -172,11 +205,21 @@ public let ntfPreviewModeGroupDefault = EnumDefault( public let incognitoGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_INCOGNITO) +public let appLocalAuthEnabledGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_APP_LOCAL_AUTH_ENABLED) + +public let allowShareExtensionGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_ALLOW_SHARE_EXTENSION) + +public let privacyLinkPreviewsGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_LINK_PREVIEWS) + // This setting is a main one, while having an unused duplicate from the past: DEFAULT_PRIVACY_ACCEPT_IMAGES public let privacyAcceptImagesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ACCEPT_IMAGES) public let privacyEncryptLocalFilesGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES) +public let privacyAskToApproveRelaysGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS) + +public let profileImageCornerRadiusGroupDefault = Default(defaults: groupDefaults, forKey: GROUP_DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) + public let ntfBadgeCountGroupDefault = IntDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_NTF_BADGE_COUNT) public let networkUseOnionHostsGroupDefault = EnumDefault( @@ -191,6 +234,18 @@ public let networkSessionModeGroupDefault = EnumDefault( withDefault: .user ) +public let networkSMPProxyModeGroupDefault = EnumDefault( + defaults: groupDefaults, + forKey: GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE, + withDefault: .unknown +) + +public let networkSMPProxyFallbackGroupDefault = EnumDefault( + defaults: groupDefaults, + forKey: GROUP_DEFAULT_NETWORK_SMP_PROXY_FALLBACK, + withDefault: .allowProtected +) + public let storeDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_STORE_DB_PASSPHRASE) public let initialRandomDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE) @@ -275,6 +330,8 @@ public func getNetCfg() -> NetCfg { let onionHosts = networkUseOnionHostsGroupDefault.get() let (hostMode, requiredHostMode) = onionHosts.hostMode let sessionMode = networkSessionModeGroupDefault.get() + let smpProxyMode = networkSMPProxyModeGroupDefault.get() + let smpProxyFallback = networkSMPProxyFallbackGroupDefault.get() let tcpConnectTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT) let tcpTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT) let tcpTimeoutPerKb = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB) @@ -295,6 +352,8 @@ public func getNetCfg() -> NetCfg { hostMode: hostMode, requiredHostMode: requiredHostMode, sessionMode: sessionMode, + smpProxyMode: smpProxyMode, + smpProxyFallback: smpProxyFallback, tcpConnectTimeout: tcpConnectTimeout, tcpTimeout: tcpTimeout, tcpTimeoutPerKb: tcpTimeoutPerKb, @@ -309,6 +368,8 @@ public func getNetCfg() -> NetCfg { public func setNetCfg(_ cfg: NetCfg) { networkUseOnionHostsGroupDefault.set(OnionHosts(netCfg: cfg)) networkSessionModeGroupDefault.set(cfg.sessionMode) + networkSMPProxyModeGroupDefault.set(cfg.smpProxyMode) + networkSMPProxyFallbackGroupDefault.set(cfg.smpProxyFallback) groupDefaults.set(cfg.tcpConnectTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT) groupDefaults.set(cfg.tcpTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT) groupDefaults.set(cfg.tcpTimeoutPerKb, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 24aca0dd18..1a9cf4a216 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -9,8 +9,9 @@ import Foundation import SwiftUI -public struct User: Identifiable, Decodable, UserLike, NamedChat { +public struct User: Identifiable, Decodable, UserLike, NamedChat, Hashable { public var userId: Int64 + public var agentUserId: String var userContactId: Int64 var localDisplayName: ContactName public var profile: LocalProfile @@ -26,6 +27,7 @@ public struct User: Identifiable, Decodable, UserLike, NamedChat { public var sendRcptsContacts: Bool public var sendRcptsSmallGroups: Bool public var viewPwdHash: UserPwdHash? + public var uiThemes: ThemeModeOverrides? public var id: Int64 { userId } @@ -41,6 +43,7 @@ public struct User: Identifiable, Decodable, UserLike, NamedChat { public static let sampleData = User( userId: 1, + agentUserId: "abc", userContactId: 1, localDisplayName: "alice", profile: LocalProfile.sampleData, @@ -52,7 +55,7 @@ public struct User: Identifiable, Decodable, UserLike, NamedChat { ) } -public struct UserRef: Identifiable, Decodable, UserLike { +public struct UserRef: Identifiable, Decodable, UserLike, Hashable { public var userId: Int64 public var localDisplayName: ContactName @@ -63,12 +66,12 @@ public protocol UserLike: Identifiable { var userId: Int64 { get } } -public struct UserPwdHash: Decodable { +public struct UserPwdHash: Decodable, Hashable { public var hash: String public var salt: String } -public struct UserInfo: Decodable, Identifiable { +public struct UserInfo: Decodable, Identifiable, Hashable { public var user: User public var unreadCount: Int @@ -89,7 +92,7 @@ public typealias ContactName = String public typealias GroupName = String -public struct Profile: Codable, NamedChat { +public struct Profile: Codable, NamedChat, Hashable { public init( displayName: String, fullName: String, @@ -121,7 +124,7 @@ public struct Profile: Codable, NamedChat { ) } -public struct LocalProfile: Codable, NamedChat { +public struct LocalProfile: Codable, NamedChat, Hashable { public init( profileId: Int64, displayName: String, @@ -171,13 +174,13 @@ public func fromLocalProfile (_ profile: LocalProfile) -> Profile { Profile(displayName: profile.displayName, fullName: profile.fullName, image: profile.image, contactLink: profile.contactLink, preferences: profile.preferences) } -public struct UserProfileUpdateSummary: Decodable { +public struct UserProfileUpdateSummary: Decodable, Hashable { public var updateSuccesses: Int public var updateFailures: Int public var changedContacts: [Contact] } -public enum ChatType: String { +public enum ChatType: String, Hashable { case direct = "@" case group = "#" case local = "*" @@ -202,7 +205,7 @@ extension NamedChat { public typealias ChatId = String -public struct FullPreferences: Decodable, Equatable { +public struct FullPreferences: Decodable, Equatable, Hashable { public var timedMessages: TimedMessagesPreference public var fullDelete: SimplePreference public var reactions: SimplePreference @@ -232,7 +235,7 @@ public struct FullPreferences: Decodable, Equatable { ) } -public struct Preferences: Codable { +public struct Preferences: Codable, Hashable { public var timedMessages: TimedMessagesPreference? public var fullDelete: SimplePreference? public var reactions: SimplePreference? @@ -308,11 +311,11 @@ public func contactUserPreferencesToPreferences(_ contactUserPreferences: Contac ) } -public protocol Preference: Codable, Equatable { +public protocol Preference: Codable, Equatable, Hashable { var allow: FeatureAllowed { get set } } -public struct SimplePreference: Preference { +public struct SimplePreference: Preference, Hashable { public var allow: FeatureAllowed public init(allow: FeatureAllowed) { @@ -320,7 +323,7 @@ public struct SimplePreference: Preference { } } -public struct TimedMessagesPreference: Preference { +public struct TimedMessagesPreference: Preference, Hashable { public var allow: FeatureAllowed public var ttl: Int? @@ -334,7 +337,7 @@ public struct TimedMessagesPreference: Preference { } } -public enum CustomTimeUnit { +public enum CustomTimeUnit: Hashable { case second case minute case hour @@ -433,7 +436,7 @@ public func shortTimeText(_ seconds: Int?) -> LocalizedStringKey { return CustomTimeUnit.toShortText(seconds: seconds) } -public struct ContactUserPreferences: Decodable { +public struct ContactUserPreferences: Decodable, Hashable { public var timedMessages: ContactUserPreference public var fullDelete: ContactUserPreference public var reactions: ContactUserPreference @@ -483,7 +486,7 @@ public struct ContactUserPreferences: Decodable { ) } -public struct ContactUserPreference: Decodable { +public struct ContactUserPreference: Decodable, Hashable { public var enabled: FeatureEnabled public var userPreference: ContactUserPref

public var contactPreference: P @@ -495,7 +498,7 @@ public struct ContactUserPreference: Decodable { } } -public struct FeatureEnabled: Decodable { +public struct FeatureEnabled: Decodable, Hashable { public var forUser: Bool public var forContact: Bool @@ -521,12 +524,12 @@ public struct FeatureEnabled: Decodable { : NSLocalizedString("off", comment: "enabled status") } - public var iconColor: Color { - forUser ? .green : forContact ? .yellow : .secondary + public func iconColor(_ secondaryColor: Color) -> Color { + forUser ? .green : forContact ? .yellow : secondaryColor } } -public enum ContactUserPref: Decodable { +public enum ContactUserPref: Decodable, Hashable { case contact(preference: P) // contact override is set case user(preference: P) // global user default is used @@ -547,7 +550,7 @@ public protocol Feature { var text: String { get } } -public enum ChatFeature: String, Decodable, Feature { +public enum ChatFeature: String, Decodable, Feature, Hashable { case timedMessages case fullDelete case reactions @@ -690,7 +693,7 @@ public enum ChatFeature: String, Decodable, Feature { } } -public enum GroupFeature: String, Decodable, Feature { +public enum GroupFeature: String, Decodable, Feature, Hashable { case timedMessages case directMessages case fullDelete @@ -890,7 +893,7 @@ public enum ContactFeatureAllowed: Identifiable, Hashable { } } -public struct ContactFeaturesAllowed: Equatable { +public struct ContactFeaturesAllowed: Equatable, Hashable { public var timedMessagesAllowed: Bool public var timedMessagesTTL: Int? public var fullDelete: ContactFeatureAllowed @@ -968,7 +971,7 @@ public func contactFeatureAllowedToPref(_ contactFeatureAllowed: ContactFeatureA } } -public enum FeatureAllowed: String, Codable, Identifiable { +public enum FeatureAllowed: String, Codable, Identifiable, Hashable { case always case yes case no @@ -986,7 +989,7 @@ public enum FeatureAllowed: String, Codable, Identifiable { } } -public struct FullGroupPreferences: Decodable, Equatable { +public struct FullGroupPreferences: Decodable, Equatable, Hashable { public var timedMessages: TimedMessagesGroupPreference public var directMessages: RoleGroupPreference public var fullDelete: GroupPreference @@ -1028,7 +1031,7 @@ public struct FullGroupPreferences: Decodable, Equatable { ) } -public struct GroupPreferences: Codable { +public struct GroupPreferences: Codable, Hashable { public var timedMessages: TimedMessagesGroupPreference? public var directMessages: RoleGroupPreference? public var fullDelete: GroupPreference? @@ -1083,7 +1086,7 @@ public func toGroupPreferences(_ fullPreferences: FullGroupPreferences) -> Group ) } -public struct GroupPreference: Codable, Equatable { +public struct GroupPreference: Codable, Equatable, Hashable { public var enable: GroupFeatureEnabled public var on: Bool { @@ -1107,7 +1110,7 @@ public struct GroupPreference: Codable, Equatable { } } -public struct RoleGroupPreference: Codable, Equatable { +public struct RoleGroupPreference: Codable, Equatable, Hashable { public var enable: GroupFeatureEnabled public var role: GroupMemberRole? @@ -1121,7 +1124,7 @@ public struct RoleGroupPreference: Codable, Equatable { } } -public struct TimedMessagesGroupPreference: Codable, Equatable { +public struct TimedMessagesGroupPreference: Codable, Equatable, Hashable { public var enable: GroupFeatureEnabled public var ttl: Int? @@ -1135,7 +1138,7 @@ public struct TimedMessagesGroupPreference: Codable, Equatable { } } -public enum GroupFeatureEnabled: String, Codable, Identifiable { +public enum GroupFeatureEnabled: String, Codable, Identifiable, Hashable { case on case off @@ -1150,15 +1153,15 @@ public enum GroupFeatureEnabled: String, Codable, Identifiable { } } - public var iconColor: Color { + public func iconColor(_ secondaryColor: Color) -> Color { switch self { case .on: return .green - case .off: return .secondary + case .off: return secondaryColor } } } -public enum ChatInfo: Identifiable, Decodable, NamedChat { +public enum ChatInfo: Identifiable, Decodable, NamedChat, Hashable { case direct(contact: Contact) case group(groupInfo: GroupInfo) case local(noteFolder: NoteFolder) @@ -1286,6 +1289,15 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { } } } + + public var chatDeleted: Bool { + get { + switch self { + case let .direct(contact): return contact.chatDeleted + default: return false + } + } + } public var sendMsgEnabled: Bool { get { @@ -1370,7 +1382,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { } } - public enum ShowEnableVoiceMessagesAlert { + public enum ShowEnableVoiceMessagesAlert: Hashable { case userEnable case askContact case groupOwnerCan @@ -1398,6 +1410,27 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { } } + public enum ShowEnableCallsAlert: Hashable { + case userEnable + case askContact + case other + } + + public var showEnableCallsAlert: ShowEnableCallsAlert { + switch self { + case let .direct(contact): + if contact.mergedPreferences.calls.userPreference.preference.allow == .no { + return .userEnable + } else if contact.mergedPreferences.calls.contactPreference.allow == .no { + return .askContact + } else { + return .other + } + default: + return .other + } + } + public var ntfsEnabled: Bool { self.chatSettings?.enableNtfs == .all } @@ -1443,7 +1476,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { } } - public struct SampleData { + public struct SampleData: Hashable { public var direct: ChatInfo public var group: ChatInfo public var local: ChatInfo @@ -1460,13 +1493,19 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { ) } -public struct ChatData: Decodable, Identifiable { +public struct ChatData: Decodable, Identifiable, Hashable, ChatLike { public var chatInfo: ChatInfo public var chatItems: [ChatItem] public var chatStats: ChatStats public var id: ChatId { get { chatInfo.id } } + public init(chatInfo: ChatInfo, chatItems: [ChatItem], chatStats: ChatStats = ChatStats()) { + self.chatInfo = chatInfo + self.chatItems = chatItems + self.chatStats = chatStats + } + public static func invalidJSON(_ json: String) -> ChatData { ChatData( chatInfo: .invalidJSON(json: json), @@ -1476,7 +1515,7 @@ public struct ChatData: Decodable, Identifiable { } } -public struct ChatStats: Decodable { +public struct ChatStats: Decodable, Hashable { public init(unreadCount: Int = 0, minUnreadItemId: Int64 = 0, unreadChat: Bool = false) { self.unreadCount = unreadCount self.minUnreadItemId = minUnreadItemId @@ -1488,7 +1527,7 @@ public struct ChatStats: Decodable { public var unreadChat: Bool = false } -public struct Contact: Identifiable, Decodable, NamedChat { +public struct Contact: Identifiable, Decodable, NamedChat, Hashable { public var contactId: Int64 var localDisplayName: ContactName public var profile: LocalProfile @@ -1504,13 +1543,21 @@ public struct Contact: Identifiable, Decodable, NamedChat { var chatTs: Date? var contactGroupMemberId: Int64? var contactGrpInvSent: Bool - + public var uiThemes: ThemeModeOverrides? + public var chatDeleted: Bool + public var id: ChatId { get { "@\(contactId)" } } public var apiId: Int64 { get { contactId } } public var ready: Bool { get { activeConn?.connStatus == .ready } } + public var sndReady: Bool { get { ready || activeConn?.connStatus == .sndReady } } public var active: Bool { get { contactStatus == .active } } public var sendMsgEnabled: Bool { get { - (ready && active && !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?? false)) + ( + sndReady + && active + && !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?? false) + && !(activeConn?.connDisabled ?? true) + ) || nextSendGrpInv } } public var nextSendGrpInv: Bool { get { contactGroupMemberId != nil && !contactGrpInvSent } } @@ -1565,16 +1612,18 @@ public struct Contact: Identifiable, Decodable, NamedChat { mergedPreferences: ContactUserPreferences.sampleData, createdAt: .now, updatedAt: .now, - contactGrpInvSent: false + contactGrpInvSent: false, + chatDeleted: false ) } -public enum ContactStatus: String, Decodable { +public enum ContactStatus: String, Decodable, Hashable { case active = "active" case deleted = "deleted" + case deletedByUser = "deletedByUser" } -public struct ContactRef: Decodable, Equatable { +public struct ContactRef: Decodable, Equatable, Hashable { var contactId: Int64 public var agentConnId: String var connId: Int64 @@ -1583,12 +1632,12 @@ public struct ContactRef: Decodable, Equatable { public var id: ChatId { get { "@\(contactId)" } } } -public struct ContactSubStatus: Decodable { +public struct ContactSubStatus: Decodable, Hashable { public var contact: Contact public var contactError: ChatError? } -public struct Connection: Decodable { +public struct Connection: Decodable, Hashable { public var connId: Int64 public var agentConnId: String public var peerChatVRange: VersionRange @@ -1601,15 +1650,25 @@ public struct Connection: Decodable { public var pqEncryption: Bool public var pqSndEnabled: Bool? public var pqRcvEnabled: Bool? + public var authErrCounter: Int + public var quotaErrCounter: Int public var connectionStats: ConnectionStats? = nil private enum CodingKeys: String, CodingKey { - case connId, agentConnId, peerChatVRange, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled + case connId, agentConnId, peerChatVRange, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled, authErrCounter, quotaErrCounter } public var id: ChatId { get { ":\(connId)" } } + public var connDisabled: Bool { + authErrCounter >= 10 // authErrDisableCount in core + } + + public var connInactive: Bool { + quotaErrCounter >= 5 // quotaErrInactiveCount in core + } + public var connPQEnabled: Bool { pqSndEnabled == true && pqRcvEnabled == true } @@ -1622,11 +1681,13 @@ public struct Connection: Decodable { connLevel: 0, viaGroupLink: false, pqSupport: false, - pqEncryption: false + pqEncryption: false, + authErrCounter: 0, + quotaErrCounter: 0 ) } -public struct VersionRange: Decodable { +public struct VersionRange: Decodable, Hashable { public init(minVersion: Int, maxVersion: Int) { self.minVersion = minVersion self.maxVersion = maxVersion @@ -1640,7 +1701,7 @@ public struct VersionRange: Decodable { } } -public struct SecurityCode: Decodable, Equatable { +public struct SecurityCode: Decodable, Equatable, Hashable { public init(securityCode: String, verifiedAt: Date) { self.securityCode = securityCode self.verifiedAt = verifiedAt @@ -1650,7 +1711,7 @@ public struct SecurityCode: Decodable, Equatable { public var verifiedAt: Date } -public struct UserContact: Decodable { +public struct UserContact: Decodable, Hashable { public var userContactLinkId: Int64 // public var connReqContact: String public var groupId: Int64? @@ -1668,7 +1729,7 @@ public struct UserContact: Decodable { } } -public struct UserContactRequest: Decodable, NamedChat { +public struct UserContactRequest: Decodable, NamedChat, Hashable { var contactRequestId: Int64 public var userContactLinkId: Int64 public var cReqChatVRange: VersionRange @@ -1697,7 +1758,7 @@ public struct UserContactRequest: Decodable, NamedChat { ) } -public struct PendingContactConnection: Decodable, NamedChat { +public struct PendingContactConnection: Decodable, NamedChat, Hashable { public var pccConnId: Int64 var pccAgentConnId: String var pccConnStatus: ConnStatus @@ -1787,7 +1848,7 @@ public struct PendingContactConnection: Decodable, NamedChat { } } -public enum ConnStatus: String, Decodable { +public enum ConnStatus: String, Decodable, Hashable { case new = "new" case joined = "joined" case requested = "requested" @@ -1803,7 +1864,7 @@ public enum ConnStatus: String, Decodable { case .joined: return false case .requested: return true case .accepted: return true - case .sndReady: return false + case .sndReady: return nil case .ready: return nil case .deleted: return nil } @@ -1811,7 +1872,7 @@ public enum ConnStatus: String, Decodable { } } -public struct Group: Decodable { +public struct Group: Decodable, Hashable { public var groupInfo: GroupInfo public var members: [GroupMember] @@ -1821,7 +1882,7 @@ public struct Group: Decodable { } } -public struct GroupInfo: Identifiable, Decodable, NamedChat { +public struct GroupInfo: Identifiable, Decodable, NamedChat, Hashable { public var groupId: Int64 var localDisplayName: GroupName public var groupProfile: GroupProfile @@ -1832,6 +1893,7 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat { var createdAt: Date var updatedAt: Date var chatTs: Date? + public var uiThemes: ThemeModeOverrides? public var id: ChatId { get { "#\(groupId)" } } public var apiId: Int64 { get { groupId } } @@ -1867,12 +1929,12 @@ public struct GroupInfo: Identifiable, Decodable, NamedChat { ) } -public struct GroupRef: Decodable { +public struct GroupRef: Decodable, Hashable { public var groupId: Int64 var localDisplayName: GroupName } -public struct GroupProfile: Codable, NamedChat { +public struct GroupProfile: Codable, NamedChat, Hashable { public init(displayName: String, fullName: String, description: String? = nil, image: String? = nil, groupPreferences: GroupPreferences? = nil) { self.displayName = displayName self.fullName = fullName @@ -1894,7 +1956,7 @@ public struct GroupProfile: Codable, NamedChat { ) } -public struct GroupMember: Identifiable, Decodable { +public struct GroupMember: Identifiable, Decodable, Hashable { public var groupMemberId: Int64 public var groupId: Int64 public var memberId: String @@ -2026,21 +2088,21 @@ public struct GroupMember: Identifiable, Decodable { ) } -public struct GroupMemberSettings: Codable { +public struct GroupMemberSettings: Codable, Hashable { public var showMessages: Bool } -public struct GroupMemberRef: Decodable { +public struct GroupMemberRef: Decodable, Hashable { var groupMemberId: Int64 var profile: Profile } -public struct GroupMemberIds: Decodable { +public struct GroupMemberIds: Decodable, Hashable { var groupMemberId: Int64 var groupId: Int64 } -public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Codable { +public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Codable, Hashable { case observer = "observer" case author = "author" case member = "member" @@ -2074,7 +2136,7 @@ public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Cod } } -public enum GroupMemberCategory: String, Decodable { +public enum GroupMemberCategory: String, Decodable, Hashable { case userMember = "user" case inviteeMember = "invitee" case hostMember = "host" @@ -2082,7 +2144,7 @@ public enum GroupMemberCategory: String, Decodable { case postMember = "post" } -public enum GroupMemberStatus: String, Decodable { +public enum GroupMemberStatus: String, Decodable, Hashable { case memRemoved = "removed" case memLeft = "left" case memGroupDeleted = "deleted" @@ -2131,7 +2193,7 @@ public enum GroupMemberStatus: String, Decodable { } } -public struct NoteFolder: Identifiable, Decodable, NamedChat { +public struct NoteFolder: Identifiable, Decodable, NamedChat, Hashable { public var noteFolderId: Int64 public var favorite: Bool public var unread: Bool @@ -2164,18 +2226,18 @@ public struct NoteFolder: Identifiable, Decodable, NamedChat { ) } -public enum InvitedBy: Decodable { +public enum InvitedBy: Decodable, Hashable { case contact(byContactId: Int64) case user case unknown } -public struct MemberSubError: Decodable { +public struct MemberSubError: Decodable, Hashable { var member: GroupMemberIds var memberError: ChatError } -public enum ConnectionEntity: Decodable { +public enum ConnectionEntity: Decodable, Hashable { case rcvDirectMsgConnection(contact: Contact?) case rcvGroupMsgConnection(groupInfo: GroupInfo, groupMember: GroupMember) case sndFileConnection(sndFileTransfer: SndFileTransfer) @@ -2206,12 +2268,17 @@ public enum ConnectionEntity: Decodable { } } -public struct NtfMsgInfo: Decodable { +public struct NtfMsgInfo: Decodable, Hashable { public var msgId: String public var msgTs: Date } -public struct AChatItem: Decodable { +public struct ChatItemDeletion: Decodable, Hashable { + public var deletedChatItem: AChatItem + public var toChatItem: AChatItem? = nil +} + +public struct AChatItem: Decodable, Hashable { public var chatInfo: ChatInfo public var chatItem: ChatItem @@ -2223,19 +2290,19 @@ public struct AChatItem: Decodable { } } -public struct ACIReaction: Decodable { +public struct ACIReaction: Decodable, Hashable { public var chatInfo: ChatInfo public var chatReaction: CIReaction } -public struct CIReaction: Decodable { +public struct CIReaction: Decodable, Hashable { public var chatDir: CIDirection public var chatItem: ChatItem public var sentAt: Date public var reaction: MsgReaction } -public struct ChatItem: Identifiable, Decodable { +public struct ChatItem: Identifiable, Decodable, Hashable { public init(chatDir: CIDirection, meta: CIMeta, content: CIContent, formattedText: [FormattedText]? = nil, quotedItem: CIQuote? = nil, reactions: [CIReactionCount] = [], file: CIFile? = nil) { self.chatDir = chatDir self.meta = meta @@ -2426,13 +2493,16 @@ public struct ChatItem: Identifiable, Decodable { } } - public func memberToModerate(_ chatInfo: ChatInfo) -> (GroupInfo, GroupMember)? { + public func memberToModerate(_ chatInfo: ChatInfo) -> (GroupInfo, GroupMember?)? { switch (chatInfo, chatDir) { case let (.group(groupInfo), .groupRcv(groupMember)): let m = groupInfo.membership return m.memberRole >= .admin && m.memberRole >= groupMember.memberRole && meta.itemDeleted == nil ? (groupInfo, groupMember) : nil + case let (.group(groupInfo), .groupSnd): + let m = groupInfo.membership + return m.memberRole >= .admin ? (groupInfo, nil) : nil default: return nil } } @@ -2447,6 +2517,10 @@ public struct ChatItem: Identifiable, Decodable { } } + public var canBeDeletedForSelf: Bool { + (content.msgContent != nil && !meta.isLive) || meta.itemDeleted != nil || isDeletedContent || mergeCategory != nil || showLocalDelete + } + public static func getSample (_ id: Int64, _ dir: CIDirection, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, quotedItem: CIQuote? = nil, file: CIFile? = nil, itemDeleted: CIDeleted? = nil, itemEdited: Bool = false, itemLive: Bool = false, deletable: Bool = true, editable: Bool = true) -> ChatItem { ChatItem( chatDir: dir, @@ -2585,7 +2659,7 @@ public struct ChatItem: Identifiable, Decodable { } } -public enum CIMergeCategory { +public enum CIMergeCategory: Hashable { case memberConnected case rcvGroupEvent case sndGroupEvent @@ -2594,7 +2668,7 @@ public enum CIMergeCategory { case chatFeature } -public enum CIDirection: Decodable { +public enum CIDirection: Decodable, Hashable { case directSnd case directRcv case groupSnd @@ -2616,11 +2690,12 @@ public enum CIDirection: Decodable { } } -public struct CIMeta: Decodable { +public struct CIMeta: Decodable, Hashable { public var itemId: Int64 public var itemTs: Date var itemText: String public var itemStatus: CIStatus + public var sentViaProxy: Bool? public var createdAt: Date public var updatedAt: Date public var itemForwarded: CIForwardedFrom? @@ -2641,8 +2716,8 @@ public struct CIMeta: Decodable { return false } - public func statusIcon(_ metaColor: Color = .secondary) -> (String, Color)? { - itemStatus.statusIcon(metaColor) + public func statusIcon(_ metaColor: Color/* = .secondary*/, _ primaryColor: Color = .accentColor) -> (String, Color)? { + itemStatus.statusIcon(metaColor, primaryColor) } public static func getSample(_ id: Int64, _ ts: Date, _ text: String, _ status: CIStatus = .sndNew, itemDeleted: CIDeleted? = nil, itemEdited: Bool = false, itemLive: Bool = false, deletable: Bool = true, editable: Bool = true) -> CIMeta { @@ -2678,7 +2753,7 @@ public struct CIMeta: Decodable { } } -public struct CITimed: Decodable { +public struct CITimed: Decodable, Hashable { public var ttl: Int public var deleteAt: Date? } @@ -2705,30 +2780,32 @@ private func recent(_ date: Date) -> Bool { return isSameDay || (now < currentDay12 && date >= previousDay18 && date < currentDay00) } -public enum CIStatus: Decodable { +public enum CIStatus: Decodable, Hashable { case sndNew case sndSent(sndProgress: SndCIStatusProgress) case sndRcvd(msgRcptStatus: MsgReceiptStatus, sndProgress: SndCIStatusProgress) case sndErrorAuth - case sndError(agentError: String) + case sndError(agentError: SndError) + case sndWarning(agentError: SndError) case rcvNew case rcvRead case invalid(text: String) - var id: String { + public var id: String { switch self { case .sndNew: return "sndNew" case .sndSent: return "sndSent" case .sndRcvd: return "sndRcvd" case .sndErrorAuth: return "sndErrorAuth" case .sndError: return "sndError" + case .sndWarning: return "sndWarning" case .rcvNew: return "rcvNew" case .rcvRead: return "rcvRead" case .invalid: return "invalid" } } - public func statusIcon(_ metaColor: Color = .secondary) -> (String, Color)? { + public func statusIcon(_ metaColor: Color/* = .secondary*/, _ primaryColor: Color = .accentColor) -> (String, Color)? { switch self { case .sndNew: return nil case .sndSent: return ("checkmark", metaColor) @@ -2738,8 +2815,9 @@ public enum CIStatus: Decodable { case .badMsgHash: return ("checkmark", .red) } case .sndErrorAuth: return ("multiply", .red) - case .sndError: return ("exclamationmark.triangle.fill", .yellow) - case .rcvNew: return ("circlebadge.fill", Color.accentColor) + case .sndError: return ("multiply", .red) + case .sndWarning: return ("exclamationmark.triangle.fill", .orange) + case .rcvNew: return ("circlebadge.fill", primaryColor) case .rcvRead: return nil case .invalid: return ("questionmark", metaColor) } @@ -2756,7 +2834,11 @@ public enum CIStatus: Decodable { ) case let .sndError(agentError): return ( NSLocalizedString("Message delivery error", comment: "item status text"), - String.localizedStringWithFormat(NSLocalizedString("Unexpected error: %@", comment: "item status description"), agentError) + agentError.errorInfo + ) + case let .sndWarning(agentError): return ( + NSLocalizedString("Message delivery warning", comment: "item status text"), + agentError.errorInfo ) case .rcvNew: return nil case .rcvRead: return nil @@ -2768,17 +2850,117 @@ public enum CIStatus: Decodable { } } -public enum MsgReceiptStatus: String, Decodable { +public enum SndError: Decodable, Hashable { + case auth + case quota + case expired + case relay(srvError: SrvError) + case proxy(proxyServer: String, srvError: SrvError) + case proxyRelay(proxyServer: String, srvError: SrvError) + case other(sndError: String) + + public var errorInfo: String { + switch self { + case .auth: NSLocalizedString("Wrong key or unknown connection - most likely this connection is deleted.", comment: "snd error text") + case .quota: NSLocalizedString("Capacity exceeded - recipient did not receive previously sent messages.", comment: "snd error text") + case .expired: NSLocalizedString("Network issues - message expired after many attempts to send it.", comment: "snd error text") + case let .relay(srvError): String.localizedStringWithFormat(NSLocalizedString("Destination server error: %@", comment: "snd error text"), srvError.errorInfo) + case let .proxy(proxyServer, srvError): String.localizedStringWithFormat(NSLocalizedString("Forwarding server: %@\nError: %@", comment: "snd error text"), proxyServer, srvError.errorInfo) + case let .proxyRelay(proxyServer, srvError): String.localizedStringWithFormat(NSLocalizedString("Forwarding server: %@\nDestination server error: %@", comment: "snd error text"), proxyServer, srvError.errorInfo) + case let .other(sndError): String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: "snd error text"), sndError) + } + } +} + +public enum SrvError: Decodable, Hashable { + case host + case version + case other(srvError: String) + + var id: String { + switch self { + case .host: return "host" + case .version: return "version" + case let .other(srvError): return "other \(srvError)" + } + } + + public var errorInfo: String { + switch self { + case .host: NSLocalizedString("Server address is incompatible with network settings.", comment: "srv error text.") + case .version: NSLocalizedString("Server version is incompatible with network settings.", comment: "srv error text") + case let .other(srvError): srvError + } + } +} + +public enum MsgReceiptStatus: String, Decodable, Hashable { case ok case badMsgHash } -public enum SndCIStatusProgress: String, Decodable { +public enum SndCIStatusProgress: String, Decodable, Hashable { case partial case complete } -public enum CIDeleted: Decodable { +public enum GroupSndStatus: Decodable, Hashable { + case new + case forwarded + case inactive + case sent + case rcvd(msgRcptStatus: MsgReceiptStatus) + case error(agentError: SndError) + case warning(agentError: SndError) + case invalid(text: String) + + public func statusIcon(_ metaColor: Color/* = .secondary*/, _ primaryColor: Color = .accentColor) -> (String, Color) { + switch self { + case .new: return ("ellipsis", metaColor) + case .forwarded: return ("chevron.forward.2", metaColor) + case .inactive: return ("person.badge.minus", metaColor) + case .sent: return ("checkmark", metaColor) + case let .rcvd(msgRcptStatus): + switch msgRcptStatus { + case .ok: return ("checkmark", metaColor) + case .badMsgHash: return ("checkmark", .red) + } + case .error: return ("multiply", .red) + case .warning: return ("exclamationmark.triangle.fill", .orange) + case .invalid: return ("questionmark", metaColor) + } + } + + public var statusInfo: (String, String)? { + switch self { + case .new: return nil + case .forwarded: return ( + NSLocalizedString("Message forwarded", comment: "item status text"), + NSLocalizedString("No direct connection yet, message is forwarded by admin.", comment: "item status description") + ) + case .inactive: return ( + NSLocalizedString("Member inactive", comment: "item status text"), + NSLocalizedString("Message may be delivered later if member becomes active.", comment: "item status description") + ) + case .sent: return nil + case .rcvd: return nil + case let .error(agentError): return ( + NSLocalizedString("Message delivery error", comment: "item status text"), + agentError.errorInfo + ) + case let .warning(agentError): return ( + NSLocalizedString("Message delivery warning", comment: "item status text"), + agentError.errorInfo + ) + case let .invalid(text): return ( + NSLocalizedString("Invalid status", comment: "item status text"), + text + ) + } + } +} + +public enum CIDeleted: Decodable, Hashable { case deleted(deletedTs: Date?) case blocked(deletedTs: Date?) case blockedByAdmin(deletedTs: Date?) @@ -2794,12 +2976,12 @@ public enum CIDeleted: Decodable { } } -public enum MsgDirection: String, Decodable { +public enum MsgDirection: String, Decodable, Hashable { case rcv = "rcv" case snd = "snd" } -public enum CIForwardedFrom: Decodable { +public enum CIForwardedFrom: Decodable, Hashable { case unknown case contact(chatName: String, msgDir: MsgDirection, contactId: Int64?, chatItemId: Int64?) case group(chatName: String, msgDir: MsgDirection, groupId: Int64?, chatItemId: Int64?) @@ -2819,7 +3001,7 @@ public enum CIForwardedFrom: Decodable { } } -public enum CIDeleteMode: String, Decodable { +public enum CIDeleteMode: String, Decodable, Hashable { case cidmBroadcast = "broadcast" case cidmInternal = "internal" } @@ -2828,7 +3010,7 @@ protocol ItemContent { var text: String { get } } -public enum CIContent: Decodable, ItemContent { +public enum CIContent: Decodable, ItemContent, Hashable { case sndMsgContent(msgContent: MsgContent) case rcvMsgContent(msgContent: MsgContent) case sndDeleted(deleteMode: CIDeleteMode) // legacy - since v4.3.0 itemDeleted field is used @@ -2964,7 +3146,7 @@ public enum CIContent: Decodable, ItemContent { } } -public enum MsgDecryptError: String, Decodable { +public enum MsgDecryptError: String, Decodable, Hashable { case ratchetHeader case tooManySkipped case ratchetEarlier @@ -2982,7 +3164,7 @@ public enum MsgDecryptError: String, Decodable { } } -public struct CIQuote: Decodable, ItemContent { +public struct CIQuote: Decodable, ItemContent, Hashable { public var chatDir: CIDirection? public var itemId: Int64? var sharedMsgId: String? = nil @@ -3020,13 +3202,13 @@ public struct CIQuote: Decodable, ItemContent { } } -public struct CIReactionCount: Decodable { +public struct CIReactionCount: Decodable, Hashable { public var reaction: MsgReaction public var userReacted: Bool public var totalReacted: Int } -public enum MsgReaction: Hashable { +public enum MsgReaction: Hashable, Identifiable { case emoji(emoji: MREmojiChar) case unknown(type: String) @@ -3047,9 +3229,16 @@ public enum MsgReaction: Hashable { case type case emoji } + + public var id: String { + switch self { + case let .emoji(emoji): emoji.rawValue + case let .unknown(unknown): unknown + } + } } -public enum MREmojiChar: String, Codable, CaseIterable { +public enum MREmojiChar: String, Codable, CaseIterable, Hashable { case thumbsup = "👍" case thumbsdown = "👎" case smile = "😀" @@ -3090,7 +3279,7 @@ extension MsgReaction: Encodable { } } -public struct CIFile: Decodable { +public struct CIFile: Decodable, Hashable { public var fileId: Int64 public var fileName: String public var fileSize: Int64 @@ -3116,12 +3305,15 @@ public struct CIFile: Decodable { case .sndComplete: return true case .sndCancelled: return true case .sndError: return true + case .sndWarning: return true case .rcvInvitation: return false case .rcvAccepted: return false case .rcvTransfer: return false + case .rcvAborted: return false case .rcvCancelled: return false case .rcvComplete: return true case .rcvError: return false + case .rcvWarning: return false case .invalid: return false } } @@ -3140,19 +3332,44 @@ public struct CIFile: Decodable { } case .sndCancelled: return nil case .sndError: return nil + case .sndWarning: return sndCancelAction case .rcvInvitation: return nil case .rcvAccepted: return rcvCancelAction case .rcvTransfer: return rcvCancelAction + case .rcvAborted: return nil case .rcvCancelled: return nil case .rcvComplete: return nil + case .rcvWarning: return rcvCancelAction case .rcvError: return nil case .invalid: return nil } } } + + public var showStatusIconInSmallView: Bool { + get { + switch fileStatus { + case .sndStored: fileProtocol != .local + case .sndTransfer: true + case .sndComplete: false + case .sndCancelled: true + case .sndError: true + case .sndWarning: true + case .rcvInvitation: false + case .rcvAccepted: true + case .rcvTransfer: true + case .rcvAborted: true + case .rcvCancelled: true + case .rcvComplete: false + case .rcvError: true + case .rcvWarning: true + case .invalid: true + } + } + } } -public struct CryptoFile: Codable { +public struct CryptoFile: Codable, Hashable { public var filePath: String // the name of the file, not a full path public var cryptoArgs: CryptoFileArgs? @@ -3199,22 +3416,28 @@ public struct CryptoFile: Codable { static var decryptedUrls = Dictionary() } -public struct CryptoFileArgs: Codable { +public struct CryptoFileArgs: Codable, Hashable { public var fileKey: String public var fileNonce: String } -public struct CancelAction { +public struct CancelAction: Hashable { public var uiAction: String public var alert: AlertInfo } -public struct AlertInfo { +public struct AlertInfo: Hashable { public var title: LocalizedStringKey public var message: LocalizedStringKey public var confirm: LocalizedStringKey } +extension LocalizedStringKey: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine("\(self)") + } +} + private var sndCancelAction = CancelAction( uiAction: NSLocalizedString("Stop file", comment: "cancel file action"), alert: AlertInfo( @@ -3242,45 +3465,76 @@ private var rcvCancelAction = CancelAction( ) ) -public enum FileProtocol: String, Decodable { +public enum FileProtocol: String, Decodable, Hashable { case smp = "smp" case xftp = "xftp" case local = "local" } -public enum CIFileStatus: Decodable, Equatable { +public enum CIFileStatus: Decodable, Equatable, Hashable { case sndStored case sndTransfer(sndProgress: Int64, sndTotal: Int64) case sndComplete case sndCancelled - case sndError + case sndError(sndFileError: FileError) + case sndWarning(sndFileError: FileError) case rcvInvitation case rcvAccepted case rcvTransfer(rcvProgress: Int64, rcvTotal: Int64) + case rcvAborted case rcvComplete case rcvCancelled - case rcvError + case rcvError(rcvFileError: FileError) + case rcvWarning(rcvFileError: FileError) case invalid(text: String) - var id: String { + public var id: String { switch self { case .sndStored: return "sndStored" case let .sndTransfer(sndProgress, sndTotal): return "sndTransfer \(sndProgress) \(sndTotal)" case .sndComplete: return "sndComplete" case .sndCancelled: return "sndCancelled" - case .sndError: return "sndError" + case let .sndError(sndFileError): return "sndError \(sndFileError)" + case let .sndWarning(sndFileError): return "sndWarning \(sndFileError)" case .rcvInvitation: return "rcvInvitation" case .rcvAccepted: return "rcvAccepted" case let .rcvTransfer(rcvProgress, rcvTotal): return "rcvTransfer \(rcvProgress) \(rcvTotal)" + case .rcvAborted: return "rcvAborted" case .rcvComplete: return "rcvComplete" case .rcvCancelled: return "rcvCancelled" - case .rcvError: return "rcvError" + case let .rcvError(rcvFileError): return "rcvError \(rcvFileError)" + case let .rcvWarning(rcvFileError): return "rcvWarning \(rcvFileError)" case .invalid: return "invalid" } } } -public enum MsgContent: Equatable { +public enum FileError: Decodable, Equatable, Hashable { + case auth + case noFile + case relay(srvError: SrvError) + case other(fileError: String) + + var id: String { + switch self { + case .auth: return "auth" + case .noFile: return "noFile" + case let .relay(srvError): return "relay \(srvError)" + case let .other(fileError): return "other \(fileError)" + } + } + + public var errorInfo: String { + switch self { + case .auth: NSLocalizedString("Wrong key or unknown file chunk address - most likely file is deleted.", comment: "file error text") + case .noFile: NSLocalizedString("File not found - most likely file was deleted or cancelled.", comment: "file error text") + case let .relay(srvError): String.localizedStringWithFormat(NSLocalizedString("File server error: %@", comment: "file error text"), srvError.errorInfo) + case let .other(fileError): String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: "file error text"), fileError) + } + } +} + +public enum MsgContent: Equatable, Hashable { case text(String) case link(text: String, preview: LinkPreview) case image(text: String, image: String) @@ -3338,6 +3592,15 @@ public enum MsgContent: Equatable { } } + public var isMediaOrFileAttachment: Bool { + switch self { + case .image: true + case .video: true + case .file: true + default: false + } + } + var cmdString: String { "json \(encodeJSON(self))" } @@ -3438,7 +3701,7 @@ extension MsgContent: Encodable { } } -public struct FormattedText: Decodable { +public struct FormattedText: Decodable, Hashable { public var text: String public var format: Format? @@ -3447,7 +3710,7 @@ public struct FormattedText: Decodable { } } -public enum Format: Decodable, Equatable { +public enum Format: Decodable, Equatable, Hashable { case bold case italic case strikeThrough @@ -3469,7 +3732,7 @@ public enum Format: Decodable, Equatable { } } -public enum SimplexLinkType: String, Decodable { +public enum SimplexLinkType: String, Decodable, Hashable { case contact case invitation case group @@ -3483,7 +3746,7 @@ public enum SimplexLinkType: String, Decodable { } } -public enum FormatColor: String, Decodable { +public enum FormatColor: String, Decodable, Hashable { case red = "red" case green = "green" case blue = "blue" @@ -3510,7 +3773,7 @@ public enum FormatColor: String, Decodable { } // Struct to use with simplex API -public struct LinkPreview: Codable, Equatable { +public struct LinkPreview: Codable, Equatable, Hashable { public init(uri: URL, title: String, description: String = "", image: String) { self.uri = uri self.title = title @@ -3525,7 +3788,7 @@ public struct LinkPreview: Codable, Equatable { public var image: String } -public enum NtfTknStatus: String, Decodable { +public enum NtfTknStatus: String, Decodable, Hashable { case new = "NEW" case registered = "REGISTERED" case invalid = "INVALID" @@ -3534,22 +3797,22 @@ public enum NtfTknStatus: String, Decodable { case expired = "EXPIRED" } -public struct SndFileTransfer: Decodable { +public struct SndFileTransfer: Decodable, Hashable { } -public struct RcvFileTransfer: Decodable { +public struct RcvFileTransfer: Decodable, Hashable { public let fileId: Int64 } -public struct FileTransferMeta: Decodable { +public struct FileTransferMeta: Decodable, Hashable { public let fileId: Int64 public let fileName: String public let filePath: String public let fileSize: Int64 } -public enum CICallStatus: String, Decodable { +public enum CICallStatus: String, Decodable, Hashable { case pending case missed case rejected @@ -3581,7 +3844,7 @@ public func durationText(_ sec: Int) -> String { : String(format: "%02d:%02d:%02d", m / 60, m % 60, s) } -public enum MsgErrorType: Decodable { +public enum MsgErrorType: Decodable, Hashable { case msgSkipped(fromMsgId: Int64, toMsgId: Int64) case msgBadId(msgId: Int64) case msgBadHash @@ -3598,7 +3861,7 @@ public enum MsgErrorType: Decodable { } } -public struct CIGroupInvitation: Decodable { +public struct CIGroupInvitation: Decodable, Hashable { public var groupId: Int64 public var groupMemberId: Int64 public var localDisplayName: GroupName @@ -3614,18 +3877,18 @@ public struct CIGroupInvitation: Decodable { } } -public enum CIGroupInvitationStatus: String, Decodable { +public enum CIGroupInvitationStatus: String, Decodable, Hashable { case pending case accepted case rejected case expired } -public struct E2EEInfo: Decodable { +public struct E2EEInfo: Decodable, Hashable { public var pqEnabled: Bool } -public enum RcvDirectEvent: Decodable { +public enum RcvDirectEvent: Decodable, Hashable { case contactDeleted case profileUpdated(fromProfile: Profile, toProfile: Profile) @@ -3654,7 +3917,7 @@ public enum RcvDirectEvent: Decodable { } } -public enum RcvGroupEvent: Decodable { +public enum RcvGroupEvent: Decodable, Hashable { case memberAdded(groupMemberId: Int64, profile: Profile) case memberConnected case memberLeft @@ -3710,7 +3973,7 @@ public enum RcvGroupEvent: Decodable { } } -public enum SndGroupEvent: Decodable { +public enum SndGroupEvent: Decodable, Hashable { case memberRole(groupMemberId: Int64, profile: Profile, role: GroupMemberRole) case userRole(role: GroupMemberRole) case memberBlocked(groupMemberId: Int64, profile: Profile, blocked: Bool) @@ -3738,7 +4001,7 @@ public enum SndGroupEvent: Decodable { } } -public enum RcvConnEvent: Decodable { +public enum RcvConnEvent: Decodable, Hashable { case switchQueue(phase: SwitchPhase) case ratchetSync(syncStatus: RatchetSyncState) case verificationCodeReset @@ -3775,7 +4038,7 @@ func ratchetSyncStatusToText(_ ratchetSyncStatus: RatchetSyncState) -> String { } } -public enum SndConnEvent: Decodable { +public enum SndConnEvent: Decodable, Hashable { case switchQueue(phase: SwitchPhase, member: GroupMemberRef?) case ratchetSync(syncStatus: RatchetSyncState, member: GroupMemberRef?) case pqEnabled(enabled: Bool) @@ -3812,14 +4075,14 @@ public enum SndConnEvent: Decodable { } } -public enum SwitchPhase: String, Decodable { +public enum SwitchPhase: String, Decodable, Hashable { case started case confirmed case secured case completed } -public enum ChatItemTTL: Hashable, Identifiable, Comparable { +public enum ChatItemTTL: Identifiable, Comparable, Hashable { case day case week case month @@ -3869,13 +4132,13 @@ public enum ChatItemTTL: Hashable, Identifiable, Comparable { } } -public struct ChatItemInfo: Decodable { +public struct ChatItemInfo: Decodable, Hashable { public var itemVersions: [ChatItemVersion] public var memberDeliveryStatuses: [MemberDeliveryStatus]? public var forwardedFromChatItem: AChatItem? } -public struct ChatItemVersion: Decodable { +public struct ChatItemVersion: Decodable, Hashable { public var chatItemVersionId: Int64 public var msgContent: MsgContent public var formattedText: [FormattedText]? @@ -3883,7 +4146,8 @@ public struct ChatItemVersion: Decodable { public var createdAt: Date } -public struct MemberDeliveryStatus: Decodable { +public struct MemberDeliveryStatus: Decodable, Hashable { public var groupMemberId: Int64 - public var memberDeliveryStatus: CIStatus + public var memberDeliveryStatus: GroupSndStatus + public var sentViaProxy: Bool? } diff --git a/apps/ios/SimpleXChat/ChatUtils.swift b/apps/ios/SimpleXChat/ChatUtils.swift new file mode 100644 index 0000000000..5f56180918 --- /dev/null +++ b/apps/ios/SimpleXChat/ChatUtils.swift @@ -0,0 +1,113 @@ +// +// ChatUtils.swift +// SimpleXChat +// +// Created by Levitating Pineapple on 15/07/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import Foundation + +public protocol ChatLike { + var chatInfo: ChatInfo { get} + var chatItems: [ChatItem] { get } + var chatStats: ChatStats { get } +} + +extension ChatLike { + public func groupFeatureEnabled(_ feature: GroupFeature) -> Bool { + if case let .group(groupInfo) = self.chatInfo { + let p = groupInfo.fullGroupPreferences + return switch feature { + case .timedMessages: p.timedMessages.on + case .directMessages: p.directMessages.on(for: groupInfo.membership) + case .fullDelete: p.fullDelete.on + case .reactions: p.reactions.on + case .voice: p.voice.on(for: groupInfo.membership) + case .files: p.files.on(for: groupInfo.membership) + case .simplexLinks: p.simplexLinks.on(for: groupInfo.membership) + case .history: p.history.on + } + } else { + return true + } + } + + public func prohibitedByPref( + hasSimplexLink: Bool, + isMediaOrFileAttachment: Bool, + isVoice: Bool + ) -> Bool { + // preference checks should match checks in compose view + let simplexLinkProhibited = hasSimplexLink && !groupFeatureEnabled(.simplexLinks) + let fileProhibited = isMediaOrFileAttachment && !groupFeatureEnabled(.files) + let voiceProhibited = isVoice && !chatInfo.featureEnabled(.voice) + return switch chatInfo { + case .direct: voiceProhibited + case .group: simplexLinkProhibited || fileProhibited || voiceProhibited + case .local: false + case .contactRequest: false + case .contactConnection: false + case .invalidJSON: false + } + } +} + +public func filterChatsToForwardTo(chats: [C]) -> [C] { + var filteredChats = chats.filter { c in + c.chatInfo.chatType != .local && canForwardToChat(c.chatInfo) + } + if let privateNotes = chats.first(where: { $0.chatInfo.chatType == .local }) { + filteredChats.insert(privateNotes, at: 0) + } + return filteredChats +} + +public func foundChat(_ chat: ChatLike, _ searchStr: String) -> Bool { + let cInfo = chat.chatInfo + return switch cInfo { + case let .direct(contact): + viewNameContains(cInfo, searchStr) || + contact.profile.displayName.localizedLowercase.contains(searchStr) || + contact.fullName.localizedLowercase.contains(searchStr) + default: + viewNameContains(cInfo, searchStr) + } + + func viewNameContains(_ cInfo: ChatInfo, _ s: String) -> Bool { + cInfo.chatViewName.localizedLowercase.contains(s) + } +} + +private func canForwardToChat(_ cInfo: ChatInfo) -> Bool { + switch cInfo { + case let .direct(contact): contact.sendMsgEnabled && !contact.nextSendGrpInv + case let .group(groupInfo): groupInfo.sendMsgEnabled + case let .local(noteFolder): noteFolder.sendMsgEnabled + case .contactRequest: false + case .contactConnection: false + case .invalidJSON: false + } +} + +public func chatIconName(_ cInfo: ChatInfo) -> String { + switch cInfo { + case .direct: "person.crop.circle.fill" + case .group: "person.2.circle.fill" + case .local: "folder.circle.fill" + case .contactRequest: "person.crop.circle.fill" + default: "circle.fill" + } +} + +public func hasSimplexLink(_ text: String?) -> Bool { + if let text, let parsedMsg = parseSimpleXMarkdown(text) { + parsedMsgHasSimplexLink(parsedMsg) + } else { + false + } +} + +public func parsedMsgHasSimplexLink(_ parsedMsg: [FormattedText]) -> Bool { + parsedMsg.contains(where: { ft in ft.format?.isSimplexLink ?? false }) +} diff --git a/apps/ios/SimpleXChat/ErrorAlert.swift b/apps/ios/SimpleXChat/ErrorAlert.swift new file mode 100644 index 0000000000..5b9acc4fca --- /dev/null +++ b/apps/ios/SimpleXChat/ErrorAlert.swift @@ -0,0 +1,159 @@ +// +// ErrorAlert.swift +// SimpleXChat +// +// Created by Levitating Pineapple on 20/07/2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import SwiftUI + +public struct ErrorAlert: Error { + public let title: LocalizedStringKey + public let message: LocalizedStringKey? + public let actions: Optional<() -> AnyView> + + public init( + title: LocalizedStringKey, + message: LocalizedStringKey? = nil + ) { + self.title = title + self.message = message + self.actions = nil + } + + public init( + title: LocalizedStringKey, + message: LocalizedStringKey? = nil, + @ViewBuilder actions: @escaping () -> A + ) { + self.title = title + self.message = message + self.actions = { AnyView(actions()) } + } + + public init(_ title: LocalizedStringKey) { + self = ErrorAlert(title: title) + } + + public init(_ error: any Error) { + self = if let chatResponse = error as? ChatResponse { + ErrorAlert(chatResponse) + } else { + ErrorAlert(LocalizedStringKey(error.localizedDescription)) + } + } + + public init(_ chatError: ChatError) { + self = ErrorAlert("\(chatErrorString(chatError))") + } + + public init(_ chatResponse: ChatResponse) { + self = if let networkErrorAlert = getNetworkErrorAlert(chatResponse) { + networkErrorAlert + } else { + ErrorAlert("\(responseError(chatResponse))") + } + } +} + +extension LocalizedStringKey: @unchecked Sendable { } + +extension View { + /// Bridges ``ErrorAlert`` to the generic alert API. + /// - Parameters: + /// - errorAlert: Binding to the Error, which is rendered in the alert + /// - actions: View Builder containing action buttons. + /// System defaults to `Ok` dismiss error action, when no actions are provided. + /// System implicitly adds `Cancel` action, if a destructive action is present + /// + /// - Returns: View, which displays ErrorAlert?, when set. + @ViewBuilder public func alert( + _ errorAlert: Binding, + @ViewBuilder actions: (ErrorAlert) -> A = { _ in EmptyView() } + ) -> some View { + alert( + errorAlert.wrappedValue?.title ?? "", + isPresented: Binding( + get: { errorAlert.wrappedValue != nil }, + set: { if !$0 { errorAlert.wrappedValue = nil } } + ), + actions: { + if let actions_ = errorAlert.wrappedValue?.actions { + actions_() + } else { + if let alert = errorAlert.wrappedValue { actions(alert) } + } + }, + message: { + if let message = errorAlert.wrappedValue?.message { + Text(message) + } + } + ) + } +} + +public func getNetworkErrorAlert(_ r: ChatResponse) -> ErrorAlert? { + switch r { + case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TIMEOUT))): + return ErrorAlert(title: "Connection timeout", message: "Please check your network connection with \(serverHostname(addr)) and try again.") + case let .chatCmdError(_, .errorAgent(.BROKER(addr, .NETWORK))): + return ErrorAlert(title: "Connection error", message: "Please check your network connection with \(serverHostname(addr)) and try again.") + case let .chatCmdError(_, .errorAgent(.BROKER(addr, .HOST))): + return ErrorAlert(title: "Connection error", message: "Server address is incompatible with network settings: \(serverHostname(addr)).") + case let .chatCmdError(_, .errorAgent(.BROKER(addr, .TRANSPORT(.version)))): + return ErrorAlert(title: "Connection error", message: "Server version is incompatible with your app: \(serverHostname(addr)).") + case let .chatCmdError(_, .errorAgent(.SMP(serverAddress, .PROXY(proxyErr)))): + return smpProxyErrorAlert(proxyErr, serverAddress) + case let .chatCmdError(_, .errorAgent(.PROXY(proxyServer, relayServer, .protocolError(.PROXY(proxyErr))))): + return proxyDestinationErrorAlert(proxyErr, proxyServer, relayServer) + default: + return nil + } +} + +private func smpProxyErrorAlert(_ proxyErr: ProxyError, _ srvAddr: String) -> ErrorAlert? { + switch proxyErr { + case .BROKER(brokerErr: .TIMEOUT): + return ErrorAlert(title: "Private routing error", message: "Error connecting to forwarding server \(serverHostname(srvAddr)). Please try later.") + case .BROKER(brokerErr: .NETWORK): + return ErrorAlert(title: "Private routing error", message: "Error connecting to forwarding server \(serverHostname(srvAddr)). Please try later.") + case .BROKER(brokerErr: .HOST): + return ErrorAlert(title: "Private routing error", message: "Forwarding server address is incompatible with network settings: \(serverHostname(srvAddr)).") + case .BROKER(brokerErr: .TRANSPORT(.version)): + return ErrorAlert(title: "Private routing error", message: "Forwarding server version is incompatible with network settings: \(serverHostname(srvAddr)).") + default: + return nil + } +} + +private func proxyDestinationErrorAlert(_ proxyErr: ProxyError, _ proxyServer: String, _ relayServer: String) -> ErrorAlert? { + switch proxyErr { + case .BROKER(brokerErr: .TIMEOUT): + return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.") + case .BROKER(brokerErr: .NETWORK): + return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.") + case .NO_SESSION: + return ErrorAlert(title: "Private routing error", message: "Forwarding server \(serverHostname(proxyServer)) failed to connect to destination server \(serverHostname(relayServer)). Please try later.") + case .BROKER(brokerErr: .HOST): + return ErrorAlert(title: "Private routing error", message: "Destination server address of \(serverHostname(relayServer)) is incompatible with forwarding server \(serverHostname(proxyServer)) settings.") + case .BROKER(brokerErr: .TRANSPORT(.version)): + return ErrorAlert(title: "Private routing error", message: "Destination server version of \(serverHostname(relayServer)) is incompatible with forwarding server \(serverHostname(proxyServer)).") + default: + return nil + } +} + +public func serverHostname(_ srv: String) -> String { + parseServerAddress(srv)?.hostnames.first ?? srv +} + +public func mtrErrorDescription(_ err: MTRError) -> LocalizedStringKey { + switch err { + case let .noDown(dbMigrations): + "database version is newer than the app, but no down migration for: \(dbMigrations.joined(separator: ", "))" + case let .different(appMigration, dbMigration): + "different migration in the app/database: \(appMigration) / \(dbMigration)" + } +} diff --git a/apps/ios/SimpleXChat/FileUtils.swift b/apps/ios/SimpleXChat/FileUtils.swift index 125600f3f3..8b0d082aed 100644 --- a/apps/ios/SimpleXChat/FileUtils.swift +++ b/apps/ios/SimpleXChat/FileUtils.swift @@ -8,6 +8,7 @@ import Foundation import OSLog +import UIKit let logger = Logger() @@ -85,6 +86,8 @@ public func deleteAppDatabaseAndFiles() { try? fm.removeItem(at: getTempFilesDirectory()) try? fm.removeItem(at: getMigrationTempFilesDirectory()) try? fm.createDirectory(at: getTempFilesDirectory(), withIntermediateDirectories: true) + try? fm.removeItem(at: getWallpaperDirectory()) + try? fm.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true) deleteAppFiles() _ = kcDatabasePassword.remove() storeDBPassphraseGroupDefault.set(true) @@ -196,6 +199,14 @@ public func getAppFilePath(_ fileName: String) -> URL { getAppFilesDirectory().appendingPathComponent(fileName) } +public func getWallpaperDirectory() -> URL { + getAppDirectory().appendingPathComponent("assets", isDirectory: true).appendingPathComponent("wallpapers", isDirectory: true) +} + +public func getWallpaperFilePath(_ filename: String) -> URL { + getWallpaperDirectory().appendingPathComponent(filename) +} + public func saveFile(_ data: Data, _ fileName: String, encrypted: Bool) -> CryptoFile? { let filePath = getAppFilePath(fileName) do { diff --git a/apps/ios/Shared/Model/ImageUtils.swift b/apps/ios/SimpleXChat/ImageUtils.swift similarity index 60% rename from apps/ios/Shared/Model/ImageUtils.swift rename to apps/ios/SimpleXChat/ImageUtils.swift index 6437597b19..67218a781e 100644 --- a/apps/ios/Shared/Model/ImageUtils.swift +++ b/apps/ios/SimpleXChat/ImageUtils.swift @@ -7,18 +7,19 @@ // import Foundation -import SimpleXChat import SwiftUI import AVKit +import SwiftyGif +import LinkPresentation -func getLoadedFileSource(_ file: CIFile?) -> CryptoFile? { +public func getLoadedFileSource(_ file: CIFile?) -> CryptoFile? { if let file = file, file.loaded { return file.fileSource } return nil } -func getLoadedImage(_ file: CIFile?) -> UIImage? { +public func getLoadedImage(_ file: CIFile?) -> UIImage? { if let fileSource = getLoadedFileSource(file) { let filePath = getAppFilePath(fileSource.filePath) do { @@ -37,7 +38,7 @@ func getLoadedImage(_ file: CIFile?) -> UIImage? { return nil } -func getFileData(_ path: URL, _ cfArgs: CryptoFileArgs?) throws -> Data { +public func getFileData(_ path: URL, _ cfArgs: CryptoFileArgs?) throws -> Data { if let cfArgs = cfArgs { return try readCryptoFile(path: path.path, cryptoArgs: cfArgs) } else { @@ -45,7 +46,7 @@ func getFileData(_ path: URL, _ cfArgs: CryptoFileArgs?) throws -> Data { } } -func getLoadedVideo(_ file: CIFile?) -> URL? { +public func getLoadedVideo(_ file: CIFile?) -> URL? { if let fileSource = getLoadedFileSource(file) { let filePath = getAppFilePath(fileSource.filePath) if FileManager.default.fileExists(atPath: filePath.path) { @@ -55,13 +56,13 @@ func getLoadedVideo(_ file: CIFile?) -> URL? { return nil } -func saveAnimImage(_ image: UIImage) -> CryptoFile? { +public func saveAnimImage(_ image: UIImage) -> CryptoFile? { let fileName = generateNewFileName("IMG", "gif") guard let imageData = image.imageData else { return nil } return saveFile(imageData, fileName, encrypted: privacyEncryptLocalFilesGroupDefault.get()) } -func saveImage(_ uiImage: UIImage) -> CryptoFile? { +public func saveImage(_ uiImage: UIImage) -> CryptoFile? { let hasAlpha = imageHasAlpha(uiImage) let ext = hasAlpha ? "png" : "jpg" if let imageDataResized = resizeImageToDataSize(uiImage, maxDataSize: MAX_IMAGE_SIZE, hasAlpha: hasAlpha) { @@ -71,7 +72,7 @@ func saveImage(_ uiImage: UIImage) -> CryptoFile? { return nil } -func cropToSquare(_ image: UIImage) -> UIImage { +public func cropToSquare(_ image: UIImage) -> UIImage { let size = image.size let side = min(size.width, size.height) let newSize = CGSize(width: side, height: side) @@ -84,7 +85,7 @@ func cropToSquare(_ image: UIImage) -> UIImage { return resizeImage(image, newBounds: CGRect(origin: .zero, size: newSize), drawIn: CGRect(origin: origin, size: size), hasAlpha: imageHasAlpha(image)) } -func resizeImageToDataSize(_ image: UIImage, maxDataSize: Int64, hasAlpha: Bool) -> Data? { +public func resizeImageToDataSize(_ image: UIImage, maxDataSize: Int64, hasAlpha: Bool) -> Data? { var img = image var data = hasAlpha ? img.pngData() : img.jpegData(compressionQuality: 0.85) var dataSize = data?.count ?? 0 @@ -99,7 +100,7 @@ func resizeImageToDataSize(_ image: UIImage, maxDataSize: Int64, hasAlpha: Bool) return data } -func resizeImageToStrSize(_ image: UIImage, maxDataSize: Int64) -> String? { +public func resizeImageToStrSize(_ image: UIImage, maxDataSize: Int64) -> String? { var img = image let hasAlpha = imageHasAlpha(image) var str = compressImageStr(img, hasAlpha: hasAlpha) @@ -115,7 +116,7 @@ func resizeImageToStrSize(_ image: UIImage, maxDataSize: Int64) -> String? { return str } -func compressImageStr(_ image: UIImage, _ compressionQuality: CGFloat = 0.85, hasAlpha: Bool) -> String? { +public func compressImageStr(_ image: UIImage, _ compressionQuality: CGFloat = 0.85, hasAlpha: Bool) -> String? { let ext = hasAlpha ? "png" : "jpg" if let data = hasAlpha ? image.pngData() : image.jpegData(compressionQuality: compressionQuality) { return "data:image/\(ext);base64,\(data.base64EncodedString())" @@ -138,7 +139,7 @@ private func resizeImage(_ image: UIImage, newBounds: CGRect, drawIn: CGRect, ha } } -func imageHasAlpha(_ img: UIImage) -> Bool { +public func imageHasAlpha(_ img: UIImage) -> Bool { if let cgImage = img.cgImage { let colorSpace = CGColorSpaceCreateDeviceRGB() let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue) @@ -158,7 +159,35 @@ func imageHasAlpha(_ img: UIImage) -> Bool { return false } -func saveFileFromURL(_ url: URL) -> CryptoFile? { +/// Reduces image size, while consuming less RAM +/// +/// Used by ShareExtension to downsize large images +/// before passing them to regular image processing pipeline +/// to avoid exceeding 120MB memory +/// +/// - Parameters: +/// - url: Location of the image data +/// - size: Maximum dimension (width or height) +/// - Returns: Downsampled image or `nil`, if the image can't be located +public func downsampleImage(at url: URL, to size: Int64) -> UIImage? { + autoreleasepool { + if let source = CGImageSourceCreateWithURL(url as CFURL, nil) { + CGImageSourceCreateThumbnailAtIndex( + source, + 0, + [ + kCGImageSourceCreateThumbnailFromImageAlways: true, + kCGImageSourceShouldCacheImmediately: true, + kCGImageSourceCreateThumbnailWithTransform: true, + kCGImageSourceThumbnailMaxPixelSize: String(size) as CFString + ] as CFDictionary + ) + .map { UIImage(cgImage: $0) } + } else { nil } + } +} + +public func saveFileFromURL(_ url: URL) -> CryptoFile? { let encrypted = privacyEncryptLocalFilesGroupDefault.get() let savedFile: CryptoFile? if url.startAccessingSecurityScopedResource() { @@ -184,7 +213,7 @@ func saveFileFromURL(_ url: URL) -> CryptoFile? { return savedFile } -func moveTempFileFromURL(_ url: URL) -> CryptoFile? { +public func moveTempFileFromURL(_ url: URL) -> CryptoFile? { do { let encrypted = privacyEncryptLocalFilesGroupDefault.get() let fileName = uniqueCombine(url.lastPathComponent) @@ -197,7 +226,6 @@ func moveTempFileFromURL(_ url: URL) -> CryptoFile? { try FileManager.default.moveItem(at: url, to: getAppFilePath(fileName)) savedFile = CryptoFile.plain(fileName) } - ChatModel.shared.filesToDelete.remove(url) return savedFile } catch { logger.error("ImageUtils.moveTempFileFromURL error: \(error.localizedDescription)") @@ -205,7 +233,44 @@ func moveTempFileFromURL(_ url: URL) -> CryptoFile? { } } -func generateNewFileName(_ prefix: String, _ ext: String, fullPath: Bool = false) -> String { +public func saveWallpaperFile(url: URL) -> String? { + let destFile = URL(fileURLWithPath: generateNewFileName(getWallpaperDirectory().path + "/" + "wallpaper", "jpg", fullPath: true)) + do { + try FileManager.default.copyItem(atPath: url.path, toPath: destFile.path) + return destFile.lastPathComponent + } catch { + logger.error("FileUtils.saveWallpaperFile error: \(error.localizedDescription)") + return nil + } +} + +public func saveWallpaperFile(image: UIImage) -> String? { + let hasAlpha = imageHasAlpha(image) + let destFile = URL(fileURLWithPath: generateNewFileName(getWallpaperDirectory().path + "/" + "wallpaper", hasAlpha ? "png" : "jpg", fullPath: true)) + let dataResized = resizeImageToDataSize(image, maxDataSize: 5_000_000, hasAlpha: hasAlpha) + do { + try dataResized!.write(to: destFile) + return destFile.lastPathComponent + } catch { + logger.error("FileUtils.saveWallpaperFile error: \(error.localizedDescription)") + return nil + } +} + +public func removeWallpaperFile(fileName: String? = nil) { + do { + try FileManager.default.contentsOfDirectory(atPath: getWallpaperDirectory().path).forEach { + if URL(fileURLWithPath: $0).lastPathComponent == fileName { try FileManager.default.removeItem(atPath: $0) } + } + } catch { + logger.error("FileUtils.removeWallpaperFile error: \(error.localizedDescription)") + } + if let fileName { + WallpaperType.cachedImages.removeValue(forKey: fileName) + } +} + +public func generateNewFileName(_ prefix: String, _ ext: String, fullPath: Bool = false) -> String { uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)", fullPath: fullPath) } @@ -237,7 +302,7 @@ private func getTimestamp() -> String { return df.string(from: Date()) } -func dropImagePrefix(_ s: String) -> String { +public func dropImagePrefix(_ s: String) -> String { dropPrefix(dropPrefix(s, "data:image/png;base64,"), "data:image/jpg;base64,") } @@ -245,8 +310,23 @@ private func dropPrefix(_ s: String, _ prefix: String) -> String { s.hasPrefix(prefix) ? String(s.dropFirst(prefix.count)) : s } +public 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 +} + extension AVAsset { - func generatePreview() -> (UIImage, Int)? { + public func generatePreview() -> (UIImage, Int)? { let generator = AVAssetImageGenerator(asset: self) generator.appliesPreferredTrackTransform = true var actualTime = CMTimeMake(value: 0, timescale: 0) @@ -258,7 +338,7 @@ extension AVAsset { } extension UIImage { - func replaceColor(_ from: UIColor, _ to: UIColor) -> UIImage { + public func replaceColor(_ from: UIColor, _ to: UIColor) -> UIImage { if let cgImage = cgImage { let colorSpace = CGColorSpaceCreateDeviceRGB() let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue) @@ -303,4 +383,75 @@ extension UIImage { } return self } + + public convenience init?(base64Encoded: String?) { + if let base64Encoded, let data = Data(base64Encoded: dropImagePrefix(base64Encoded)) { + self.init(data: data) + } else { + return nil + } + } +} + +public func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) { + logger.debug("getLinkMetadata: fetching URL preview") + LPMetadataProvider().startFetchingMetadata(for: url){ metadata, error in + if let e = error { + logger.error("Error retrieving link metadata: \(e.localizedDescription)") + } + if let metadata = metadata, + let imageProvider = metadata.imageProvider, + imageProvider.canLoadObject(ofClass: UIImage.self) { + imageProvider.loadObject(ofClass: UIImage.self){ object, error in + var linkPreview: LinkPreview? = nil + if let error = error { + logger.error("Couldn't load image preview from link metadata with error: \(error.localizedDescription)") + } else { + if let image = object as? UIImage, + let resized = resizeImageToStrSize(image, maxDataSize: 14000), + let title = metadata.title, + let uri = metadata.originalURL { + linkPreview = LinkPreview(uri: uri, title: title, image: resized) + } + } + cb(linkPreview) + } + } else { + logger.error("Could not load link preview image") + cb(nil) + } + } +} + +public func getLinkPreview(for url: URL) async -> LinkPreview? { + await withCheckedContinuation { cont in + getLinkPreview(url: url) { cont.resume(returning: $0) } + } +} + +private let squareToCircleRatio = 0.935 + +private let radiusFactor = (1 - squareToCircleRatio) / 50 + +@ViewBuilder public func clipProfileImage(_ img: Image, size: CGFloat, radius: Double, blurred: Bool = false) -> some View { + if radius >= 50 { + blurredFrame(img, size, blurred).clipShape(Circle()) + } else if radius <= 0 { + let sz = size * squareToCircleRatio + blurredFrame(img, sz, blurred).padding((size - sz) / 2) + } else { + let sz = size * (squareToCircleRatio + radius * radiusFactor) + blurredFrame(img, sz, blurred) + .clipShape(RoundedRectangle(cornerRadius: sz * radius / 100, style: .continuous)) + .padding((size - sz) / 2) + } +} + +@ViewBuilder private func blurredFrame(_ img: Image, _ size: CGFloat, _ blurred: Bool) -> some View { + let v = img.resizable().frame(width: size, height: size) + if blurred { + v.blur(radius: size / 4) + } else { + v + } } diff --git a/apps/ios/SimpleXChat/Notifications.swift b/apps/ios/SimpleXChat/Notifications.swift index bc959cb34b..4b43595372 100644 --- a/apps/ios/SimpleXChat/Notifications.swift +++ b/apps/ios/SimpleXChat/Notifications.swift @@ -47,7 +47,7 @@ public func createContactConnectedNtf(_ user: any UserLike, _ contact: Contact) hideContent ? NSLocalizedString("A new contact", comment: "notification title") : contact.displayName ), body: String.localizedStringWithFormat( - NSLocalizedString("You can now send messages to %@", comment: "notification body"), + NSLocalizedString("You can now chat with %@", comment: "notification body"), hideContent ? NSLocalizedString("this contact", comment: "notification title") : contact.chatViewName ), targetContentIdentifier: contact.id, diff --git a/apps/ios/SimpleXChat/SharedFileSubscriber.swift b/apps/ios/SimpleXChat/SharedFileSubscriber.swift index f496e6999e..bf5997f40b 100644 --- a/apps/ios/SimpleXChat/SharedFileSubscriber.swift +++ b/apps/ios/SimpleXChat/SharedFileSubscriber.swift @@ -12,6 +12,8 @@ public typealias AppSubscriber = SharedFileSubscriber> +public typealias SESubscriber = SharedFileSubscriber> + public class SharedFileSubscriber: NSObject, NSFilePresenter { var fileURL: URL public var presentedItemURL: URL? @@ -57,6 +59,8 @@ let appMessagesSharedFile = getGroupContainerDirectory().appendingPathComponent( let nseMessagesSharedFile = getGroupContainerDirectory().appendingPathComponent("chat.simplex.app.SimpleX-NSE.messages", isDirectory: false) +let seMessagesSharedFile = getGroupContainerDirectory().appendingPathComponent("chat.simplex.app.SimpleX-SE.messages", isDirectory: false) + public struct ProcessMessage: Codable { var createdAt: Date = Date.now var message: Message @@ -70,6 +74,10 @@ public enum NSEProcessMessage: Codable { case state(state: NSEState) } +public enum SEProcessMessage: Codable { + case state(state: SEState) +} + public func sendAppProcessMessage(_ message: AppProcessMessage) { SharedFileSubscriber.notify(url: appMessagesSharedFile, message: ProcessMessage(message: message)) } @@ -78,6 +86,10 @@ public func sendNSEProcessMessage(_ message: NSEProcessMessage) { SharedFileSubscriber.notify(url: nseMessagesSharedFile, message: ProcessMessage(message: message)) } +public func sendSEProcessMessage(_ message: SEProcessMessage) { + SharedFileSubscriber.notify(url: seMessagesSharedFile, message: ProcessMessage(message: message)) +} + public func appMessageSubscriber(onMessage: @escaping (AppProcessMessage) -> Void) -> AppSubscriber { SharedFileSubscriber(fileURL: appMessagesSharedFile) { (msg: ProcessMessage) in onMessage(msg.message) @@ -90,6 +102,12 @@ public func nseMessageSubscriber(onMessage: @escaping (NSEProcessMessage) -> Voi } } +public func seMessageSubscriber(onMessage: @escaping (SEProcessMessage) -> Void) -> SESubscriber { + SharedFileSubscriber(fileURL: seMessagesSharedFile) { (msg: ProcessMessage) in + onMessage(msg.message) + } +} + public func sendAppState(_ state: AppState) { sendAppProcessMessage(.state(state: state)) } @@ -97,3 +115,7 @@ public func sendAppState(_ state: AppState) { public func sendNSEState(_ state: NSEState) { sendNSEProcessMessage(.state(state: state)) } + +public func sendSEState(_ state: SEState) { + sendSEProcessMessage(.state(state: state)) +} diff --git a/apps/ios/SimpleXChat/Theme/ChatWallpaperTypes.swift b/apps/ios/SimpleXChat/Theme/ChatWallpaperTypes.swift new file mode 100644 index 0000000000..662f8b43d1 --- /dev/null +++ b/apps/ios/SimpleXChat/Theme/ChatWallpaperTypes.swift @@ -0,0 +1,402 @@ +// +// ChatWallpaper.swift +// SimpleX (iOS) +// +// Created by Avently on 06.06.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import Foundation +import SwiftUI + +public enum PresetWallpaper: CaseIterable { + case cats + case flowers + case hearts + case kids + case school + case travel + + var res: UIImage { + UIImage(named: "wallpaper_\(filename)")! + } + + public var filename: String { + switch self { + case .cats: "cats" + case .flowers: "flowers" + case .hearts: "hearts" + case .kids: "kids" + case .school: "school" + case .travel: "travel" + } + } + + public var scale: Float { + switch self { + case .cats: 0.63 + case .flowers: 0.53 + case .hearts: 0.59 + case .kids: 0.53 + case .school: 0.53 + case .travel: 0.68 + } + } + + public var background: [DefaultTheme: Color] { + switch self { + case .cats: wallpaperBackgrounds(light: "#ffF8F6EA") + case .flowers: wallpaperBackgrounds(light: "#ffE2FFE4") + case .hearts: wallpaperBackgrounds(light: "#ffFDECEC") + case .kids: wallpaperBackgrounds(light: "#ffdbfdfb") + case .school: wallpaperBackgrounds(light: "#ffE7F5FF") + case .travel: wallpaperBackgrounds(light: "#fff9eeff") + } + } + + public var tint: [DefaultTheme: Color] { + switch self { + case .cats: [ + DefaultTheme.LIGHT: "#ffefdca6".colorFromReadableHex(), + DefaultTheme.DARK: "#ff4b3b0e".colorFromReadableHex(), + DefaultTheme.SIMPLEX: "#ff51400f".colorFromReadableHex(), + DefaultTheme.BLACK: "#ff4b3b0e".colorFromReadableHex() + ] + case .flowers: [ + DefaultTheme.LIGHT: "#ff9CEA59".colorFromReadableHex(), + DefaultTheme.DARK: "#ff31560D".colorFromReadableHex(), + DefaultTheme.SIMPLEX: "#ff36600f".colorFromReadableHex(), + DefaultTheme.BLACK: "#ff31560D".colorFromReadableHex() + ] + case .hearts: [ + DefaultTheme.LIGHT: "#fffde0e0".colorFromReadableHex(), + DefaultTheme.DARK: "#ff3c0f0f".colorFromReadableHex(), + DefaultTheme.SIMPLEX: "#ff411010".colorFromReadableHex(), + DefaultTheme.BLACK: "#ff3C0F0F".colorFromReadableHex() + ] + case .kids: [ + DefaultTheme.LIGHT: "#ffadeffc".colorFromReadableHex(), + DefaultTheme.DARK: "#ff16404B".colorFromReadableHex(), + DefaultTheme.SIMPLEX: "#ff184753".colorFromReadableHex(), + DefaultTheme.BLACK: "#ff16404B".colorFromReadableHex() + ] + case .school: [ + DefaultTheme.LIGHT: "#ffCEEBFF".colorFromReadableHex(), + DefaultTheme.DARK: "#ff0F293B".colorFromReadableHex(), + DefaultTheme.SIMPLEX: "#ff112f43".colorFromReadableHex(), + DefaultTheme.BLACK: "#ff0F293B".colorFromReadableHex() + ] + case .travel: [ + DefaultTheme.LIGHT: "#ffeedbfe".colorFromReadableHex(), + DefaultTheme.DARK: "#ff311E48".colorFromReadableHex(), + DefaultTheme.SIMPLEX: "#ff35204e".colorFromReadableHex(), + DefaultTheme.BLACK: "#ff311E48".colorFromReadableHex() + ] + } + } + + public var colors: [DefaultTheme: ThemeColors] { + switch self { + case .cats: [ + DefaultTheme.LIGHT: ThemeColors.from( + sentMessage: "#fffffaed", + sentQuote: "#fffaf0d6", + receivedMessage: "#ffF8F7F4", + receivedQuote: "#ffefede9" + ), + DefaultTheme.DARK: ThemeColors.from( + sentMessage: "#ff2f2919", + sentQuote: "#ff473a1d", + receivedMessage: "#ff272624", + receivedQuote: "#ff373633" + ), + DefaultTheme.SIMPLEX: ThemeColors.from( + sentMessage: "#ff41371b", + sentQuote: "#ff654f1c", + receivedMessage: "#ff272624", + receivedQuote: "#ff373633" + ), + DefaultTheme.BLACK: ThemeColors.from( + sentMessage: "#ff41371b", + sentQuote: "#ff654f1c", + receivedMessage: "#ff1f1e1b", + receivedQuote: "#ff2f2d27" + ) + ] + case .flowers: [ + DefaultTheme.LIGHT: ThemeColors.from( + sentMessage: "#fff1ffe5", + sentQuote: "#ffdcf9c4", + receivedMessage: "#ffF4F8F2", + receivedQuote: "#ffe7ece7" + ), + DefaultTheme.DARK: ThemeColors.from( + sentMessage: "#ff163521", + sentQuote: "#ff1B5330", + receivedMessage: "#ff242523", + receivedQuote: "#ff353733" + ), + DefaultTheme.SIMPLEX: ThemeColors.from( + sentMessage: "#ff184739", + sentQuote: "#ff1F6F4B", + receivedMessage: "#ff242523", + receivedQuote: "#ff353733" + ), + DefaultTheme.BLACK: ThemeColors.from( + sentMessage: "#ff184739", + sentQuote: "#ff1F6F4B", + receivedMessage: "#ff1c1f1a", + receivedQuote: "#ff282b25" + ) + ] + case .hearts: [ + DefaultTheme.LIGHT: ThemeColors.from( + sentMessage: "#fffff4f4", + sentQuote: "#ffffdfdf", + receivedMessage: "#fff8f6f6", + receivedQuote: "#ffefebeb" + ), + DefaultTheme.DARK: ThemeColors.from( + sentMessage: "#ff301515", + sentQuote: "#ff4C1818", + receivedMessage: "#ff242121", + receivedQuote: "#ff3b3535" + ), + DefaultTheme.SIMPLEX: ThemeColors.from( + sentMessage: "#ff491A28", + sentQuote: "#ff761F29", + receivedMessage: "#ff242121", + receivedQuote: "#ff3b3535" + ), + DefaultTheme.BLACK: ThemeColors.from( + sentMessage: "#ff491A28", + sentQuote: "#ff761F29", + receivedMessage: "#ff1f1b1b", + receivedQuote: "#ff2e2626" + ) + ] + case .kids: [ + DefaultTheme.LIGHT: ThemeColors.from( + sentMessage: "#ffeafeff", + sentQuote: "#ffcbf4f7", + receivedMessage: "#fff3fafa", + receivedQuote: "#ffe4efef" + ), + DefaultTheme.DARK: ThemeColors.from( + sentMessage: "#ff16302F", + sentQuote: "#ff1a4a49", + receivedMessage: "#ff252626", + receivedQuote: "#ff373A39" + ), + DefaultTheme.SIMPLEX: ThemeColors.from( + sentMessage: "#ff1a4745", + sentQuote: "#ff1d6b69", + receivedMessage: "#ff252626", + receivedQuote: "#ff373a39" + ), + DefaultTheme.BLACK: ThemeColors.from( + sentMessage: "#ff1a4745", + sentQuote: "#ff1d6b69", + receivedMessage: "#ff1e1f1f", + receivedQuote: "#ff262b29" + ) + ] + case .school: [ + DefaultTheme.LIGHT: ThemeColors.from( + sentMessage: "#ffeef9ff", + sentQuote: "#ffD6EDFA", + receivedMessage: "#ffF3F5F9", + receivedQuote: "#ffe4e8ee" + ), + DefaultTheme.DARK: ThemeColors.from( + sentMessage: "#ff172833", + sentQuote: "#ff1C3E4F", + receivedMessage: "#ff26282c", + receivedQuote: "#ff393c40" + ), + DefaultTheme.SIMPLEX: ThemeColors.from( + sentMessage: "#ff1A3C5D", + sentQuote: "#ff235b80", + receivedMessage: "#ff26282c", + receivedQuote: "#ff393c40" + ), + DefaultTheme.BLACK: ThemeColors.from( + sentMessage: "#ff1A3C5D", + sentQuote: "#ff235b80", + receivedMessage: "#ff1d1e22", + receivedQuote: "#ff292b2f" + ) + ] + case .travel: [ + DefaultTheme.LIGHT: ThemeColors.from( + sentMessage: "#fffcf6ff", + sentQuote: "#fff2e0fc", + receivedMessage: "#ffF6F4F7", + receivedQuote: "#ffede9ee" + ), + DefaultTheme.DARK: ThemeColors.from( + sentMessage: "#ff33263B", + sentQuote: "#ff53385E", + receivedMessage: "#ff272528", + receivedQuote: "#ff3B373E" + ), + DefaultTheme.SIMPLEX: ThemeColors.from( + sentMessage: "#ff3C255D", + sentQuote: "#ff623485", + receivedMessage: "#ff26273B", + receivedQuote: "#ff3A394F" + ), + DefaultTheme.BLACK: ThemeColors.from( + sentMessage: "#ff3C255D", + sentQuote: "#ff623485", + receivedMessage: "#ff231f23", + receivedQuote: "#ff2c2931" + ) + ] + } + } + + public static func from(_ filename: String) -> PresetWallpaper? { + switch filename { + case PresetWallpaper.cats.filename: PresetWallpaper.cats + case PresetWallpaper.flowers.filename: PresetWallpaper.flowers + case PresetWallpaper.hearts.filename: PresetWallpaper.hearts + case PresetWallpaper.kids.filename: PresetWallpaper.kids + case PresetWallpaper.school.filename: PresetWallpaper.school + case PresetWallpaper.travel.filename: PresetWallpaper.travel + default: nil + } + } +} + +func wallpaperBackgrounds(light: String) -> [DefaultTheme : Color] { + [ + DefaultTheme.LIGHT: light.colorFromReadableHex(), + DefaultTheme.DARK: "#ff121212".colorFromReadableHex(), + DefaultTheme.SIMPLEX: "#ff111528".colorFromReadableHex(), + DefaultTheme.BLACK: "#ff070707".colorFromReadableHex() + ] +} + +public enum WallpaperScaleType: String, Codable, CaseIterable { + case fill + case fit + case `repeat` + + public var text: String { + switch self { + case .fill: "Fill" + case .fit: "Fit" + case .repeat: "Repeat" + } + } + + public func computeScaleFactor(_ srcSize: CGSize, _ dstSize: CGSize) -> (CGFloat, CGFloat) { + switch self { + case .fill: + let widthScale = dstSize.width / srcSize.width + let heightScale = dstSize.height / srcSize.height + return (max(widthScale, heightScale), max(widthScale, heightScale)) + case .fit: fallthrough + case .repeat: + let widthScale = dstSize.width / srcSize.width + let heightScale = dstSize.height / srcSize.height + return (min(widthScale, heightScale), min(widthScale, heightScale)) + } + } +} + +public enum WallpaperType: Equatable { + public var image: SwiftUI.Image? { + if let uiImage { + return SwiftUI.Image(uiImage: uiImage) + } + return nil + } + + public var uiImage: UIImage? { + let filename: String + switch self { + case let .preset(f, _): filename = f + case let .image(f, _, _): filename = f + default: return nil + } + if filename == "" { return nil } + if let image = WallpaperType.cachedImages[filename] { + return image + } else { + let res: UIImage? + if case let .preset(filename, _) = self { + res = (PresetWallpaper.from(filename) ?? PresetWallpaper.cats).res + } else { + // In case of unintentional image deletion don't crash the app + res = UIImage(contentsOfFile: getWallpaperFilePath(filename).path) + } + if let res { + WallpaperType.cachedImages[filename] = res + } + return res + } + } + + public func sameType(_ other: WallpaperType?) -> Bool { + if case let .preset(filename, _) = self, case let .preset(otherFilename, _) = other { filename == otherFilename } + else if case .image = self, case .image = other { true } + else if case .empty = self, case .empty = other { true } + else { false } + } + + public var isPreset: Bool { switch self { case .preset: true; default: false } } + + public var isImage: Bool { switch self { case .image: true; default: false } } + + public var isEmpty: Bool { switch self { case .empty: true; default: false } } + + public var scale: Float { + switch self { + case let .preset(_, scale): scale ?? 1 + case let .image(_, scale, _): scale ?? 1 + case .empty: 1 + } + } + + public func samePreset(other: PresetWallpaper?) -> Bool { if case let .preset(filename, _) = self, filename == other?.filename { true } else { false } } + + case preset(_ filename: String, _ scale: Float?) + + case image(_ filename: String, _ scale: Float?, _ scaleType: WallpaperScaleType?) + + case empty + + public func defaultBackgroundColor(_ theme: DefaultTheme, _ themeBackground: Color) -> Color { + if case let .preset(filename, _) = self { + (PresetWallpaper.from(filename) ?? PresetWallpaper.cats).background[theme]! + } else { + themeBackground + } + } + + public func defaultTintColor(_ theme: DefaultTheme) -> Color { + if case let .preset(filename, _) = self { + (PresetWallpaper.from(filename) ?? PresetWallpaper.cats).tint[theme]! + } else if case let .image(_, _, scaleType) = self, scaleType == WallpaperScaleType.repeat { + Color.clear + } else { + Color.clear + } + } + + public static var cachedImages: [String: UIImage] = [:] + + public static func from(_ wallpaper: ThemeWallpaper?) -> WallpaperType? { + if wallpaper == nil { + return nil + } else if let preset = wallpaper?.preset { + return WallpaperType.preset(preset, wallpaper?.scale) + } else if let imageFile = wallpaper?.imageFile { + return WallpaperType.image(imageFile, wallpaper?.scale, wallpaper?.scaleType) + } else { + return WallpaperType.empty + } + } +} diff --git a/apps/ios/SimpleXChat/Theme/Color.swift b/apps/ios/SimpleXChat/Theme/Color.swift new file mode 100644 index 0000000000..3e8fe1b6e7 --- /dev/null +++ b/apps/ios/SimpleXChat/Theme/Color.swift @@ -0,0 +1,114 @@ +// +// Color.swift +// SimpleX (iOS) +// +// Created by Avently on 05.06.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import Foundation +import SwiftUI + +//let Purple200 = Color(0xFFBB86FC) +//let Purple500 = Color(0xFF6200EE) +//let Purple700 = Color(0xFF3700B3) +//let Teal200 = Color(0xFF03DAC5) +//let Gray = Color(0x22222222) +//let Indigo = Color(0xFF9966FF) +let SimplexBlue = Color(0, 136, 255, a: 255) +//let SimplexGreen = Color(77, 218, 103, a: 255) +//let SecretColor = Color(0x40808080) +let LightGray = Color(241, 242, 246, a: 255) +let DarkGray = Color(43, 44, 46, a: 255) +let HighOrLowlight = Color(139, 135, 134, a: 255) +//let MessagePreviewDark = Color(179, 175, 174, a: 255) +//let MessagePreviewLight = Color(49, 45, 44, a: 255) +//let ToolbarLight = Color(220, 220, 220, a: 12) +//let ToolbarDark = Color(80, 80, 80, a: 12) +//let SettingsSecondaryLight = Color(200, 196, 195, a: 90) +//let GroupDark = Color(80, 80, 80, a: 60) +//let IncomingCallLight = Color(239, 237, 236, a: 255) +//let WarningOrange = Color(255, 127, 0, a: 255) +//let WarningYellow = Color(255, 192, 0, a: 255) +//let FileLight = Color(183, 190, 199, a: 255) +//let FileDark = Color(101, 101, 106, a: 255) + +extension Color { + public init(_ argb: Int64) { + let a = Double((argb & 0xFF000000) >> 24) / 255.0 + let r = Double((argb & 0xFF0000) >> 16) / 255.0 + let g = Double((argb & 0xFF00) >> 8) / 255.0 + let b = Double((argb & 0xFF)) / 255.0 + self.init(.sRGB, red: r, green: g, blue: b, opacity: a) + } + + public init(_ r: Int, _ g: Int, _ b: Int, a: Int) { + self.init(.sRGB, red: Double(r) / 255.0, green: Double(g) / 255.0, blue: Double(b) / 255.0, opacity: Double(a) / 255.0) + } + + public func toReadableHex() -> String { + let uiColor: UIColor = .init(self) + var (r, g, b, a): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0) + uiColor.getRed(&r, green: &g, blue: &b, alpha: &a) + // Can be negative values and more than 1. Extended color range, making it normal + r = min(1, max(0, r)) + g = min(1, max(0, g)) + b = min(1, max(0, b)) + a = min(1, max(0, a)) + return String(format: "#%02x%02x%02x%02x", + Int((a * 255).rounded()), + Int((r * 255).rounded()), + Int((g * 255).rounded()), + Int((b * 255).rounded()) + ) + } + + public func darker(_ factor: CGFloat = 0.1) -> Color { + var (r, g, b, a): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0) + UIColor(self).getRed(&r, green: &g, blue: &b, alpha: &a) + return Color(.sRGB, red: max(r * (1 - factor), 0), green: max(g * (1 - factor), 0), blue: max(b * (1 - factor), 0), opacity: a) + } + + public func lighter(_ factor: CGFloat = 0.1) -> Color { + var (r, g, b, a): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0) + UIColor(self).getRed(&r, green: &g, blue: &b, alpha: &a) + return Color(.sRGB, red: min(r * (1 + factor), 1), green: min(g * (1 + factor), 1), blue: min(b * (1 + factor), 1), opacity: a) + } + + public func asGroupedBackground(_ mode: DefaultThemeMode) -> Color { + let uiColor: UIColor = .init(self) + var (r, g, b, a): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0) + uiColor.getRed(&r, green: &g, blue: &b, alpha: &a) + return mode == DefaultThemeMode.light + ? Color(.sRGB, red: max(0, r - 0.052), green: max(0, g - 0.051), blue: max(0, b - 0.032), opacity: a) + : Color(.sRGB, red: min(1, r + 0.11), green: min(1, g + 0.11), blue: min(1, b + 0.115), opacity: a) + } +} + +extension String { + func colorFromReadableHex() -> Color { + // https://stackoverflow.com/a/56874327 + let hex = self.trimmingCharacters(in: ["#", " "]) + var int: UInt64 = 0 + Scanner(string: hex).scanHexInt64(&int) + let a, r, g, b: UInt64 + switch hex.count { + case 3: // RGB (12-bit) + (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) + case 6: // RGB (24-bit) + (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) + case 8: // ARGB (32-bit) + (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) + default: + (a, r, g, b) = (1, 1, 1, 0) + } + + return Color( + .sRGB, + red: Double(r) / 255, + green: Double(g) / 255, + blue: Double(b) / 255, + opacity: Double(a) / 255 + ) + } +} diff --git a/apps/ios/SimpleXChat/Theme/ThemeTypes.swift b/apps/ios/SimpleXChat/Theme/ThemeTypes.swift new file mode 100644 index 0000000000..4074382543 --- /dev/null +++ b/apps/ios/SimpleXChat/Theme/ThemeTypes.swift @@ -0,0 +1,736 @@ +// +// Theme.swift +// SimpleX (iOS) +// +// Created by Avently on 03.06.2024. +// Copyright © 2024 SimpleX Chat. All rights reserved. +// + +import Foundation +import SwiftUI + +public enum DefaultTheme: String, Codable, Equatable { + case LIGHT + case DARK + case SIMPLEX + case BLACK + + public static let SYSTEM_THEME_NAME: String = "SYSTEM" + + public var themeName: String { self.rawValue } + + public var mode: DefaultThemeMode { + self == .LIGHT + ? DefaultThemeMode.light + : DefaultThemeMode.dark + } + + public func hasChangedAnyColor(_ overrides: ThemeOverrides?) -> Bool { + if let overrides { + overrides.colors != ThemeColors() || (overrides.wallpaper != nil && (overrides.wallpaper?.background != nil || overrides.wallpaper?.tint != nil)) + } else { + false + } + } +} + +public enum DefaultThemeMode: String, Codable { + case light + case dark +} + +public class Colors: ObservableObject, NSCopying, Equatable { + @Published public var primary: Color + @Published public var primaryVariant: Color + @Published public var secondary: Color + @Published public var secondaryVariant: Color + @Published public var background: Color + @Published public var surface: Color + @Published public var error: Color + @Published public var onBackground: Color + @Published public var onSurface: Color + @Published public var isLight: Bool + + public init(primary: Color, primaryVariant: Color, secondary: Color, secondaryVariant: Color, background: Color, surface: Color, error: Color, onBackground: Color, onSurface: Color, isLight: Bool) { + self.primary = primary + self.primaryVariant = primaryVariant + self.secondary = secondary + self.secondaryVariant = secondaryVariant + self.background = background + self.surface = surface + self.error = error + self.onBackground = onBackground + self.onSurface = onSurface + self.isLight = isLight + } + + public static func == (lhs: Colors, rhs: Colors) -> Bool { + lhs.primary == rhs.primary && + lhs.primaryVariant == rhs.primaryVariant && + lhs.secondary == rhs.secondary && + lhs.secondaryVariant == rhs.secondaryVariant && + lhs.background == rhs.background && + lhs.surface == rhs.surface && + lhs.error == rhs.error && + lhs.onBackground == rhs.onBackground && + lhs.onSurface == rhs.onSurface && + lhs.isLight == rhs.isLight + } + + public func copy(with zone: NSZone? = nil) -> Any { + Colors(primary: self.primary, primaryVariant: self.primaryVariant, secondary: self.secondary, secondaryVariant: self.secondaryVariant, background: self.background, surface: self.surface, error: self.error, onBackground: self.onBackground, onSurface: self.onSurface, isLight: self.isLight) + } + + public func clone() -> Colors { copy() as! Colors } +} + +public class AppColors: ObservableObject, NSCopying, Equatable { + @Published public var title: Color + @Published public var primaryVariant2: Color + @Published public var sentMessage: Color + @Published public var sentQuote: Color + @Published public var receivedMessage: Color + @Published public var receivedQuote: Color + + public init(title: Color, primaryVariant2: Color, sentMessage: Color, sentQuote: Color, receivedMessage: Color, receivedQuote: Color) { + self.title = title + self.primaryVariant2 = primaryVariant2 + self.sentMessage = sentMessage + self.sentQuote = sentQuote + self.receivedMessage = receivedMessage + self.receivedQuote = receivedQuote + } + + public static func == (lhs: AppColors, rhs: AppColors) -> Bool { + lhs.title == rhs.title && + lhs.primaryVariant2 == rhs.primaryVariant2 && + lhs.sentMessage == rhs.sentMessage && + lhs.sentQuote == rhs.sentQuote && + lhs.receivedQuote == rhs.receivedMessage && + lhs.receivedQuote == rhs.receivedQuote + } + + public func copy(with zone: NSZone? = nil) -> Any { + AppColors(title: self.title, primaryVariant2: self.primaryVariant2, sentMessage: self.sentMessage, sentQuote: self.sentQuote, receivedMessage: self.receivedMessage, receivedQuote: self.receivedQuote) + } + + public func clone() -> AppColors { copy() as! AppColors } + + public func copy( + title: Color?, + primaryVariant2: Color?, + sentMessage: Color?, + sentQuote: Color?, + receivedMessage: Color?, + receivedQuote: Color? + ) -> AppColors { + AppColors( + title: title ?? self.title, + primaryVariant2: primaryVariant2 ?? self.primaryVariant2, + sentMessage: sentMessage ?? self.sentMessage, + sentQuote: sentQuote ?? self.sentQuote, + receivedMessage: receivedMessage ?? self.receivedMessage, + receivedQuote: receivedQuote ?? self.receivedQuote + ) + } +} + +public class AppWallpaper: ObservableObject, NSCopying, Equatable { + public static func == (lhs: AppWallpaper, rhs: AppWallpaper) -> Bool { + lhs.background == rhs.background && + lhs.tint == rhs.tint && + lhs.type == rhs.type + } + + @Published public var background: Color? = nil + @Published public var tint: Color? = nil + @Published public var type: WallpaperType = WallpaperType.empty + + public init(background: Color?, tint: Color?, type: WallpaperType) { + self.background = background + self.tint = tint + self.type = type + } + + public func copy(with zone: NSZone? = nil) -> Any { + AppWallpaper(background: self.background, tint: self.tint, type: self.type) + } + + public func clone() -> AppWallpaper { copy() as! AppWallpaper } + + public func copyWithoutDefault(_ background: Color?, _ tint: Color?, _ type: WallpaperType) -> AppWallpaper { + AppWallpaper( + background: background, + tint: tint, + type: type + ) + } +} + +public enum ThemeColor { + case primary + case primaryVariant + case secondary + case secondaryVariant + case background + case surface + case title + case sentMessage + case sentQuote + case receivedMessage + case receivedQuote + case primaryVariant2 + case wallpaperBackground + case wallpaperTint + + public func fromColors(_ colors: Colors, _ appColors: AppColors, _ appWallpaper: AppWallpaper) -> Color? { + switch (self) { + case .primary: colors.primary + case .primaryVariant: colors.primaryVariant + case .secondary: colors.secondary + case .secondaryVariant: colors.secondaryVariant + case .background: colors.background + case .surface: colors.surface + case .title: appColors.title + case .primaryVariant2: appColors.primaryVariant2 + case .sentMessage: appColors.sentMessage + case .sentQuote: appColors.sentQuote + case .receivedMessage: appColors.receivedMessage + case .receivedQuote: appColors.receivedQuote + case .wallpaperBackground: appWallpaper.background + case .wallpaperTint: appWallpaper.tint + } + } + + public var text: LocalizedStringKey { + switch (self) { + case .primary: "Accent" + case .primaryVariant: "Additional accent" + case .secondary: "Secondary" + case .secondaryVariant: "Additional secondary" + case .background: "Background" + case .surface: "Menus" + case .title: "Title" + case .primaryVariant2: "Additional accent 2" + case .sentMessage: "Sent message" + case .sentQuote: "Sent reply" + case .receivedMessage: "Received message" + case .receivedQuote: "Received reply" + case .wallpaperBackground: "Wallpaper background" + case .wallpaperTint: "Wallpaper accent" + } + } +} + +public struct ThemeColors: Codable, Equatable, Hashable { + public var primary: String? = nil + public var primaryVariant: String? = nil + public var secondary: String? = nil + public var secondaryVariant: String? = nil + public var background: String? = nil + public var surface: String? = nil + public var title: String? = nil + public var primaryVariant2: String? = nil + public var sentMessage: String? = nil + public var sentQuote: String? = nil + public var receivedMessage: String? = nil + public var receivedQuote: String? = nil + + public init(primary: String? = nil, primaryVariant: String? = nil, secondary: String? = nil, secondaryVariant: String? = nil, background: String? = nil, surface: String? = nil, title: String? = nil, primaryVariant2: String? = nil, sentMessage: String? = nil, sentQuote: String? = nil, receivedMessage: String? = nil, receivedQuote: String? = nil) { + self.primary = primary + self.primaryVariant = primaryVariant + self.secondary = secondary + self.secondaryVariant = secondaryVariant + self.background = background + self.surface = surface + self.title = title + self.primaryVariant2 = primaryVariant2 + self.sentMessage = sentMessage + self.sentQuote = sentQuote + self.receivedMessage = receivedMessage + self.receivedQuote = receivedQuote + } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case primary = "accent" + case primaryVariant = "accentVariant" + case secondary + case secondaryVariant + case background + case surface = "menus" + case title + case primaryVariant2 = "accentVariant2" + case sentMessage + case sentQuote = "sentReply" + case receivedMessage + case receivedQuote = "receivedReply" + } + + public static func from(sentMessage: String, sentQuote: String, receivedMessage: String, receivedQuote: String) -> ThemeColors { + var c = ThemeColors() + c.sentMessage = sentMessage + c.sentQuote = sentQuote + c.receivedMessage = receivedMessage + c.receivedQuote = receivedQuote + return c + } + + public static func from(_ colors: Colors, _ appColors: AppColors) -> ThemeColors { + ThemeColors( + primary: colors.primary.toReadableHex(), + primaryVariant: colors.primaryVariant.toReadableHex(), + secondary: colors.secondary.toReadableHex(), + secondaryVariant: colors.secondaryVariant.toReadableHex(), + background: colors.background.toReadableHex(), + surface: colors.surface.toReadableHex(), + title: appColors.title.toReadableHex(), + primaryVariant2: appColors.primaryVariant2.toReadableHex(), + sentMessage: appColors.sentMessage.toReadableHex(), + sentQuote: appColors.sentQuote.toReadableHex(), + receivedMessage: appColors.receivedMessage.toReadableHex(), + receivedQuote: appColors.receivedQuote.toReadableHex() + ) + } +} + +public struct ThemeWallpaper: Codable, Equatable, Hashable { + public var preset: String? + public var scale: Float? + public var scaleType: WallpaperScaleType? + public var background: String? + public var tint: String? + public var image: String? + public var imageFile: String? + + public init(preset: String? = nil, scale: Float? = nil, scaleType: WallpaperScaleType? = nil, background: String? = nil, tint: String? = nil, image: String? = nil, imageFile: String? = nil) { + self.preset = preset + self.scale = scale + self.scaleType = scaleType + self.background = background + self.tint = tint + self.image = image + self.imageFile = imageFile + } + + public enum CodingKeys: String, CodingKey, CaseIterable { + case preset + case scale + case scaleType + case background + case tint + case image + case imageFile + } + + public func toAppWallpaper() -> AppWallpaper { + AppWallpaper ( + background: background?.colorFromReadableHex(), + tint: tint?.colorFromReadableHex(), + type: WallpaperType.from(self) ?? WallpaperType.empty + ) + } + + public func withFilledWallpaperPath() -> ThemeWallpaper { + let aw = toAppWallpaper() + let type = aw.type + let preset: String? = if case let WallpaperType.preset(filename, _) = type { filename } else { nil } + let scale: Float? = if scale == nil { nil } else { + if case let WallpaperType.preset(_, scale) = type { + scale + } else if case let WallpaperType.image(_, scale, _) = type { + scale + } else { + nil + } + } + let scaleType: WallpaperScaleType? = if scaleType == nil { nil } else if case let WallpaperType.image(_, _, scaleType) = type { scaleType } else { nil } + let imageFile: String? = if case let WallpaperType.image(filename, _, _) = type { filename } else { nil } + return ThemeWallpaper ( + preset: preset, + scale: scale, + scaleType: scaleType, + background: aw.background?.toReadableHex(), + tint: aw.tint?.toReadableHex(), + image: nil, + imageFile: imageFile + ) + } + + public static func from(_ type: WallpaperType, _ background: String?, _ tint: String?) -> ThemeWallpaper { + let preset: String? = if case let WallpaperType.preset(filename, _) = type { filename } else { nil } + let scale: Float? = if case let WallpaperType.preset(_, scale) = type { scale } else if case let WallpaperType.image(_, scale, _) = type { scale } else { nil } + let scaleType: WallpaperScaleType? = if case let WallpaperType.image(_, _, scaleType) = type { scaleType } else { nil } + let imageFile: String? = if case let WallpaperType.image(filename, _, _) = type { filename } else { nil } + return ThemeWallpaper( + preset: preset, + scale: scale, + scaleType: scaleType, + background: background, + tint: tint, + image: nil, + imageFile: imageFile + ) + } +} + +/// If you add new properties, make sure they serialized to YAML correctly, see: +/// encodeThemeOverrides() +public struct ThemeOverrides: Codable, Equatable, Hashable { + public var themeId: String = UUID().uuidString + public var base: DefaultTheme + public var colors: ThemeColors = ThemeColors() + public var wallpaper: ThemeWallpaper? = nil + + public init(themeId: String = UUID().uuidString, base: DefaultTheme, colors: ThemeColors = ThemeColors(), wallpaper: ThemeWallpaper? = nil) { + self.themeId = themeId + self.base = base + self.colors = colors + self.wallpaper = wallpaper + } + + public func isSame(_ type: WallpaperType?, _ themeName: String) -> Bool { + if base.themeName != themeName { + return false + } + return if let preset = wallpaper?.preset, let type, case let WallpaperType.preset(filename, _) = type, preset == filename { + true + } else if wallpaper?.imageFile != nil, let type, case WallpaperType.image = type { + true + } else if wallpaper?.preset == nil && wallpaper?.imageFile == nil && type == nil { + true + } else if wallpaper?.preset == nil && wallpaper?.imageFile == nil, let type, case WallpaperType.empty = type { + true + } else { + false + } + } + + public func withUpdatedColor(_ name: ThemeColor, _ color: String?) -> ThemeOverrides { + var c = colors + var w = wallpaper + switch name { + case ThemeColor.primary: c.primary = color + case ThemeColor.primaryVariant: c.primaryVariant = color + case ThemeColor.secondary: c.secondary = color + case ThemeColor.secondaryVariant: c.secondaryVariant = color + case ThemeColor.background: c.background = color + case ThemeColor.surface: c.surface = color + case ThemeColor.title: c.title = color + case ThemeColor.primaryVariant2: c.primaryVariant2 = color + case ThemeColor.sentMessage: c.sentMessage = color + case ThemeColor.sentQuote: c.sentQuote = color + case ThemeColor.receivedMessage: c.receivedMessage = color + case ThemeColor.receivedQuote: c.receivedQuote = color + case ThemeColor.wallpaperBackground: w?.background = color + case ThemeColor.wallpaperTint: w?.tint = color + } + return ThemeOverrides(themeId: themeId, base: base, colors: c, wallpaper: w) + } + + public func toColors(_ base: DefaultTheme, _ perChatTheme: ThemeColors?, _ perUserTheme: ThemeColors?, _ presetWallpaperTheme: ThemeColors?) -> Colors { + let baseColors = switch base { + case DefaultTheme.LIGHT: LightColorPalette + case DefaultTheme.DARK: DarkColorPalette + case DefaultTheme.SIMPLEX: SimplexColorPalette + case DefaultTheme.BLACK: BlackColorPalette + } + let c = baseColors.clone() + c.primary = perChatTheme?.primary?.colorFromReadableHex() ?? perUserTheme?.primary?.colorFromReadableHex() ?? colors.primary?.colorFromReadableHex() ?? presetWallpaperTheme?.primary?.colorFromReadableHex() ?? baseColors.primary + c.primaryVariant = perChatTheme?.primaryVariant?.colorFromReadableHex() ?? perUserTheme?.primaryVariant?.colorFromReadableHex() ?? colors.primaryVariant?.colorFromReadableHex() ?? presetWallpaperTheme?.primaryVariant?.colorFromReadableHex() ?? baseColors.primaryVariant + c.secondary = perChatTheme?.secondary?.colorFromReadableHex() ?? perUserTheme?.secondary?.colorFromReadableHex() ?? colors.secondary?.colorFromReadableHex() ?? presetWallpaperTheme?.secondary?.colorFromReadableHex() ?? baseColors.secondary + c.secondaryVariant = perChatTheme?.secondaryVariant?.colorFromReadableHex() ?? perUserTheme?.secondaryVariant?.colorFromReadableHex() ?? colors.secondaryVariant?.colorFromReadableHex() ?? presetWallpaperTheme?.secondaryVariant?.colorFromReadableHex() ?? baseColors.secondaryVariant + c.background = perChatTheme?.background?.colorFromReadableHex() ?? perUserTheme?.background?.colorFromReadableHex() ?? colors.background?.colorFromReadableHex() ?? presetWallpaperTheme?.background?.colorFromReadableHex() ?? baseColors.background + c.surface = perChatTheme?.surface?.colorFromReadableHex() ?? perUserTheme?.surface?.colorFromReadableHex() ?? colors.surface?.colorFromReadableHex() ?? presetWallpaperTheme?.surface?.colorFromReadableHex() ?? baseColors.surface + return c + } + + public func toAppColors(_ base: DefaultTheme, _ perChatTheme: ThemeColors?, _ perChatWallpaperType: WallpaperType?, _ perUserTheme: ThemeColors?, _ perUserWallpaperType: WallpaperType?, _ presetWallpaperTheme: ThemeColors?) -> AppColors { + let baseColors = switch base { + case DefaultTheme.LIGHT: LightColorPaletteApp + case DefaultTheme.DARK: DarkColorPaletteApp + case DefaultTheme.SIMPLEX: SimplexColorPaletteApp + case DefaultTheme.BLACK: BlackColorPaletteApp + } + + let sentMessageFallback = colors.sentMessage?.colorFromReadableHex() ?? presetWallpaperTheme?.sentMessage?.colorFromReadableHex() ?? baseColors.sentMessage + let sentQuoteFallback = colors.sentQuote?.colorFromReadableHex() ?? presetWallpaperTheme?.sentQuote?.colorFromReadableHex() ?? baseColors.sentQuote + let receivedMessageFallback = colors.receivedMessage?.colorFromReadableHex() ?? presetWallpaperTheme?.receivedMessage?.colorFromReadableHex() ?? baseColors.receivedMessage + let receivedQuoteFallback = colors.receivedQuote?.colorFromReadableHex() ?? presetWallpaperTheme?.receivedQuote?.colorFromReadableHex() ?? baseColors.receivedQuote + + let c = baseColors.clone() + c.title = perChatTheme?.title?.colorFromReadableHex() ?? perUserTheme?.title?.colorFromReadableHex() ?? colors.title?.colorFromReadableHex() ?? presetWallpaperTheme?.title?.colorFromReadableHex() ?? baseColors.title + c.primaryVariant2 = perChatTheme?.primaryVariant2?.colorFromReadableHex() ?? perUserTheme?.primaryVariant2?.colorFromReadableHex() ?? colors.primaryVariant2?.colorFromReadableHex() ?? presetWallpaperTheme?.primaryVariant2?.colorFromReadableHex() ?? baseColors.primaryVariant2 + c.sentMessage = if let c = perChatTheme?.sentMessage { c.colorFromReadableHex() } else if let perUserTheme, (perChatWallpaperType == nil || perUserWallpaperType == nil || perChatWallpaperType!.sameType(perUserWallpaperType)) { perUserTheme.sentMessage?.colorFromReadableHex() ?? sentMessageFallback } else { sentMessageFallback } + c.sentQuote = if let c = perChatTheme?.sentQuote { c.colorFromReadableHex() } else if let perUserTheme, (perChatWallpaperType == nil || perUserWallpaperType == nil || perChatWallpaperType!.sameType(perUserWallpaperType)) { perUserTheme.sentQuote?.colorFromReadableHex() ?? sentQuoteFallback } else { sentQuoteFallback } + c.receivedMessage = if let c = perChatTheme?.receivedMessage { c.colorFromReadableHex() } else if let perUserTheme, (perChatWallpaperType == nil || perUserWallpaperType == nil || perChatWallpaperType!.sameType(perUserWallpaperType)) { perUserTheme.receivedMessage?.colorFromReadableHex() ?? receivedMessageFallback } + else { receivedMessageFallback } + c.receivedQuote = if let c = perChatTheme?.receivedQuote { c.colorFromReadableHex() } else if let perUserTheme, (perChatWallpaperType == nil || perUserWallpaperType == nil || perChatWallpaperType!.sameType(perUserWallpaperType)) { perUserTheme.receivedQuote?.colorFromReadableHex() ?? receivedQuoteFallback } else { receivedQuoteFallback } + return c + } + + public func toAppWallpaper(_ themeOverridesForType: WallpaperType?, _ perChatTheme: ThemeModeOverride?, _ perUserTheme: ThemeModeOverride?, _ themeBackgroundColor: Color) -> AppWallpaper { + let mainType: WallpaperType + if let t = themeOverridesForType { mainType = t } + // type can be nil if override is empty `"wallpaper": "{}"`, in this case no wallpaper is needed, empty. + // It's not nil to override upper level wallpaper + else if let w = perChatTheme?.wallpaper { mainType = w.toAppWallpaper().type } + else if let w = perUserTheme?.wallpaper { mainType = w.toAppWallpaper().type } + else if let w = wallpaper { mainType = w.toAppWallpaper().type } + else { return AppWallpaper(background: nil, tint: nil, type: WallpaperType.empty) } + + let first: ThemeWallpaper? = if mainType.sameType(perChatTheme?.wallpaper?.toAppWallpaper().type) { perChatTheme?.wallpaper } else { nil } + let second: ThemeWallpaper? = if mainType.sameType(perUserTheme?.wallpaper?.toAppWallpaper().type) { perUserTheme?.wallpaper } else { nil } + let third: ThemeWallpaper? = if mainType.sameType(self.wallpaper?.toAppWallpaper().type) { self.wallpaper } else { nil } + + let wallpaper: WallpaperType + switch mainType { + case let WallpaperType.preset(preset, scale): + let scale = if themeOverridesForType == nil { scale ?? first?.scale ?? second?.scale ?? third?.scale } else { second?.scale ?? third?.scale ?? scale } + wallpaper = WallpaperType.preset(preset, scale) + case let WallpaperType.image(filename, scale, scaleType): + let scale = if themeOverridesForType == nil { scale ?? first?.scale ?? second?.scale ?? third?.scale } else { second?.scale ?? third?.scale ?? scale } + let scaleType = if themeOverridesForType == nil { scaleType ?? first?.scaleType ?? second?.scaleType ?? third?.scaleType } else { second?.scaleType ?? third?.scaleType ?? scaleType } + let imageFile = if themeOverridesForType == nil { filename } else { first?.imageFile ?? second?.imageFile ?? third?.imageFile ?? filename } + wallpaper = WallpaperType.image(imageFile, scale, scaleType) + case WallpaperType.empty: + wallpaper = WallpaperType.empty + } + let background = (first?.background ?? second?.background ?? third?.background)?.colorFromReadableHex() ?? mainType.defaultBackgroundColor(base, themeBackgroundColor) + let tint = (first?.tint ?? second?.tint ?? third?.tint)?.colorFromReadableHex() ?? mainType.defaultTintColor(base) + + return AppWallpaper(background: background, tint: tint, type: wallpaper) + } + + public func withFilledColors(_ base: DefaultTheme, _ perChatTheme: ThemeColors?, _ perChatWallpaperType: WallpaperType?, _ perUserTheme: ThemeColors?, _ perUserWallpaperType: WallpaperType?, _ presetWallpaperTheme: ThemeColors?) -> ThemeColors { + let c = toColors(base, perChatTheme, perUserTheme, presetWallpaperTheme) + let ac = toAppColors(base, perChatTheme, perChatWallpaperType, perUserTheme, perUserWallpaperType, presetWallpaperTheme) + return ThemeColors( + primary: c.primary.toReadableHex(), + primaryVariant: c.primaryVariant.toReadableHex(), + secondary: c.secondary.toReadableHex(), + secondaryVariant: c.secondaryVariant.toReadableHex(), + background: c.background.toReadableHex(), + surface: c.surface.toReadableHex(), + title: ac.title.toReadableHex(), + primaryVariant2: ac.primaryVariant2.toReadableHex(), + sentMessage: ac.sentMessage.toReadableHex(), + sentQuote: ac.sentQuote.toReadableHex(), + receivedMessage: ac.receivedMessage.toReadableHex(), + receivedQuote: ac.receivedQuote.toReadableHex() + ) + } +} + +extension [ThemeOverrides] { + public func getTheme(_ themeId: String?) -> ThemeOverrides? { + self.first { $0.themeId == themeId } + } + + public func getTheme(_ themeId: String?, _ type: WallpaperType?, _ base: DefaultTheme) -> ThemeOverrides? { + self.first { $0.themeId == themeId || $0.isSame(type, base.themeName) } + } + + public func replace(_ theme: ThemeOverrides) -> [ThemeOverrides] { + let index = self.firstIndex { $0.themeId == theme.themeId || + // prevent situation when two themes has the same type but different theme id (maybe something was changed in prefs by hand) + $0.isSame(WallpaperType.from(theme.wallpaper), theme.base.themeName) + } + var a = self.map { $0 } + if let index { + a[index] = theme + } else { + a.append(theme) + } + return a + } + + public func sameTheme(_ type: WallpaperType?, _ themeName: String) -> ThemeOverrides? { first { $0.isSame(type, themeName) } } + + public func skipDuplicates() -> [ThemeOverrides] { + var res: [ThemeOverrides] = [] + self.forEach { theme in + let themeType = WallpaperType.from(theme.wallpaper) + if !res.contains(where: { $0.themeId == theme.themeId || $0.isSame(themeType, theme.base.themeName) }) { + res.append(theme) + } + } + return res + } + +} + +public struct ThemeModeOverrides: Codable, Hashable { + public var light: ThemeModeOverride? = nil + public var dark: ThemeModeOverride? = nil + + public init(light: ThemeModeOverride? = nil, dark: ThemeModeOverride? = nil) { + self.light = light + self.dark = dark + } + + public func preferredMode(_ darkTheme: Bool) -> ThemeModeOverride? { + darkTheme ? dark : light + } +} + +public struct ThemeModeOverride: Codable, Equatable, Hashable { + public var mode: DefaultThemeMode// = CurrentColors.base.mode + public var colors: ThemeColors = ThemeColors() + public var wallpaper: ThemeWallpaper? = nil + + public init(mode: DefaultThemeMode, colors: ThemeColors = ThemeColors(), wallpaper: ThemeWallpaper? = nil) { + self.mode = mode + self.colors = colors + self.wallpaper = wallpaper + } + + public var type: WallpaperType? { WallpaperType.from(wallpaper) } + + public func withUpdatedColor(_ name: ThemeColor, _ color: String?) -> ThemeModeOverride { + var c = colors + var w = wallpaper + switch (name) { + case ThemeColor.primary: c.primary = color + case ThemeColor.primaryVariant: c.primaryVariant = color + case ThemeColor.secondary: c.secondary = color + case ThemeColor.secondaryVariant: c.secondaryVariant = color + case ThemeColor.background: c.background = color + case ThemeColor.surface: c.surface = color + case ThemeColor.title: c.title = color + case ThemeColor.primaryVariant2: c.primaryVariant2 = color + case ThemeColor.sentMessage: c.sentMessage = color + case ThemeColor.sentQuote: c.sentQuote = color + case ThemeColor.receivedMessage: c.receivedMessage = color + case ThemeColor.receivedQuote: c.receivedQuote = color + case ThemeColor.wallpaperBackground: w?.background = color + case ThemeColor.wallpaperTint: w?.tint = color + } + return ThemeModeOverride(mode: mode, colors: c, wallpaper: w) + } + + public static func withFilledAppDefaults(_ mode: DefaultThemeMode, _ base: DefaultTheme) -> ThemeModeOverride { + ThemeModeOverride( + mode: mode, + colors: ThemeOverrides(base: base).withFilledColors(base, nil, nil, nil, nil, nil), + wallpaper: ThemeWallpaper(preset: PresetWallpaper.school.filename) + ) + } +} + +public let DarkColorPalette = Colors( + primary: SimplexBlue, + primaryVariant: SimplexBlue, + secondary: HighOrLowlight, + secondaryVariant: DarkGray, + background: Color.black, + surface: Color(0xFF222222), + error: Color.red, + onBackground: Color.white, + onSurface: Color.white, + isLight: false +) +public let DarkColorPaletteApp = AppColors( + title: .white, + primaryVariant2: Color(0xFF18262E), + sentMessage: Color(0xFF18262E), + sentQuote: Color(0xFF1D3847), + receivedMessage: Color(0xff262627), + receivedQuote: Color(0xff373739) +) + +public let LightColorPalette = Colors ( + primary: SimplexBlue, + primaryVariant: SimplexBlue, + secondary: HighOrLowlight, + secondaryVariant: LightGray, + background: Color.white, + surface: Color.white, + error: Color.red, + onBackground: Color.black, + onSurface: Color.black, + isLight: true +) +public let LightColorPaletteApp = AppColors( + title: .black, + primaryVariant2: Color(0xFFE9F7FF), + sentMessage: Color(0xFFE9F7FF), + sentQuote: Color(0xFFD6F0FF), + receivedMessage: Color(0xfff5f5f6), + receivedQuote: Color(0xffececee) +) + +public let SimplexColorPalette = Colors( + primary: Color(0xFF70F0F9), + primaryVariant: Color(0xFF1298A5), + secondary: HighOrLowlight, + secondaryVariant: Color(0xFF2C464D), + background: Color(0xFF111528), + surface: Color(0xFF121C37), + error: Color.red, + onBackground: Color.white, + onSurface: Color.white, + isLight: false +) +public let SimplexColorPaletteApp = AppColors( + title: .white, + primaryVariant2: Color(0xFF172941), + sentMessage: Color(0xFF172941), + sentQuote: Color(0xFF1C3A57), + receivedMessage: Color(0xff25283a), + receivedQuote: Color(0xff36394a) +) + +public let BlackColorPalette = Colors( + primary: Color(0xff0077e0), + primaryVariant: Color(0xff0077e0), + secondary: HighOrLowlight, + secondaryVariant: DarkGray, + background: Color(0xff070707), + surface: Color(0xff161617), + error: Color.red, + onBackground: Color.white, + onSurface: Color.white, + isLight: false +) +public let BlackColorPaletteApp = AppColors( + title: .white, + primaryVariant2: Color(0xff243747), + sentMessage: Color(0xFF18262E), + sentQuote: Color(0xFF1D3847), + receivedMessage: Color(0xff1b1b1b), + receivedQuote: Color(0xff29292b) +) + +extension Colors { + public func updateColorsFrom(_ other: Colors) { + primary = other.primary + primaryVariant = other.primaryVariant + secondary = other.secondary + secondaryVariant = other.secondaryVariant + background = other.background + surface = other.surface + error = other.error + onBackground = other.onBackground + onSurface = other.onSurface + isLight = other.isLight + } +} + +extension AppColors { + public func updateColorsFrom(_ other: AppColors) { + title = other.title + primaryVariant2 = other.primaryVariant2 + sentMessage = other.sentMessage + sentQuote = other.sentQuote + receivedMessage = other.receivedMessage + receivedQuote = other.receivedQuote + } +} + +extension AppWallpaper { + public func updateWallpaperFrom(_ other: AppWallpaper) { + background = other.background + tint = other.tint + type = other.type + } +} diff --git a/apps/ios/SimpleXChat/hs_init.c b/apps/ios/SimpleXChat/hs_init.c index adacd57310..4731e7b829 100644 --- a/apps/ios/SimpleXChat/hs_init.c +++ b/apps/ios/SimpleXChat/hs_init.c @@ -39,3 +39,19 @@ void haskell_init_nse(void) { char **pargv = argv; hs_init_with_rtsopts(&argc, &pargv); } + +void haskell_init_se(void) { + int argc = 7; + char *argv[] = { + "simplex", + "+RTS", // requires `hs_init_with_rtsopts` + "-A1m", // chunk size for new allocations + "-H1m", // initial heap size + "-F0.5", // heap growth triggering GC + "-Fd1", // memory return + "-c", // compacting garbage collector + 0 + }; + char **pargv = argv; + hs_init_with_rtsopts(&argc, &pargv); +} diff --git a/apps/ios/SimpleXChat/hs_init.h b/apps/ios/SimpleXChat/hs_init.h index a732fd7113..40be4fc263 100644 --- a/apps/ios/SimpleXChat/hs_init.h +++ b/apps/ios/SimpleXChat/hs_init.h @@ -13,4 +13,6 @@ void haskell_init(void); void haskell_init_nse(void); +void haskell_init_se(void); + #endif /* hs_init_h */ diff --git a/apps/ios/bg.lproj/Localizable.strings b/apps/ios/bg.lproj/Localizable.strings index 06d9f1f43e..aa43902c81 100644 --- a/apps/ios/bg.lproj/Localizable.strings +++ b/apps/ios/bg.lproj/Localizable.strings @@ -154,9 +154,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ е потвърдено"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ сървъри"; - /* No comment provided by engineer. */ "%@ uploaded" = "%@ качено"; @@ -337,11 +334,9 @@ /* No comment provided by engineer. */ "above, then choose:" = "по-горе, след това избери:"; -/* No comment provided by engineer. */ -"Accent color" = "Основен цвят"; - /* accept contact request via notification - accept incoming call via notification */ + accept incoming call via notification + swipe action */ "Accept" = "Приеми"; /* No comment provided by engineer. */ @@ -350,7 +345,8 @@ /* notification body */ "Accept contact request from %@?" = "Приемане на заявка за контакт от %@?"; -/* accept contact request via notification */ +/* accept contact request via notification + swipe action */ "Accept incognito" = "Приеми инкогнито"; /* call status */ @@ -369,7 +365,7 @@ "Add profile" = "Добави профил"; /* No comment provided by engineer. */ -"Add server…" = "Добави сървър…"; +"Add server" = "Добави сървър"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Добави сървъри чрез сканиране на QR кодове."; @@ -813,7 +809,7 @@ /* No comment provided by engineer. */ "Choose from library" = "Избери от библиотеката"; -/* No comment provided by engineer. */ +/* swipe action */ "Clear" = "Изчисти"; /* No comment provided by engineer. */ @@ -831,9 +827,6 @@ /* No comment provided by engineer. */ "colored" = "цветен"; -/* No comment provided by engineer. */ -"Colors" = "Цветове"; - /* server test step */ "Compare file" = "Сравни файл"; @@ -993,9 +986,6 @@ /* notification */ "Contact is connected" = "Контактът е свързан"; -/* No comment provided by engineer. */ -"Contact is not connected yet!" = "Контактът все още не е свързан!"; - /* No comment provided by engineer. */ "Contact name" = "Име на контакт"; @@ -1011,7 +1001,7 @@ /* No comment provided by engineer. */ "Continue" = "Продължи"; -/* chat item action */ +/* No comment provided by engineer. */ "Copy" = "Копирай"; /* No comment provided by engineer. */ @@ -1170,7 +1160,8 @@ /* No comment provided by engineer. */ "default (yes)" = "по подразбиране (да)"; -/* chat item action */ +/* chat item action + swipe action */ "Delete" = "Изтрий"; /* No comment provided by engineer. */ @@ -1209,12 +1200,6 @@ /* No comment provided by engineer. */ "Delete contact" = "Изтрий контакт"; -/* No comment provided by engineer. */ -"Delete Contact" = "Изтрий контакт"; - -/* No comment provided by engineer. */ -"Delete contact?\nThis cannot be undone!" = "Изтрий контакт?\nТова не може да бъде отменено!"; - /* No comment provided by engineer. */ "Delete database" = "Изтрий базата данни"; @@ -1269,9 +1254,6 @@ /* No comment provided by engineer. */ "Delete old database?" = "Изтрий старата база данни?"; -/* No comment provided by engineer. */ -"Delete pending connection" = "Изтрий предстоящата връзка"; - /* No comment provided by engineer. */ "Delete pending connection?" = "Изтрий предстоящата връзка?"; @@ -1662,9 +1644,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "Грешка при изтриване на връзката"; -/* No comment provided by engineer. */ -"Error deleting contact" = "Грешка при изтриване на контакт"; - /* No comment provided by engineer. */ "Error deleting database" = "Грешка при изтриване на базата данни"; @@ -1779,7 +1758,8 @@ /* No comment provided by engineer. */ "Error: " = "Грешка: "; -/* No comment provided by engineer. */ +/* file error text + snd error text */ "Error: %@" = "Грешка: %@"; /* No comment provided by engineer. */ @@ -1824,7 +1804,7 @@ /* No comment provided by engineer. */ "Faster joining and more reliable messages." = "По-бързо присъединяване и по-надеждни съобщения."; -/* No comment provided by engineer. */ +/* swipe action */ "Favorite" = "Любим"; /* No comment provided by engineer. */ @@ -2292,7 +2272,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Японски интерфейс"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Присъединяване"; /* No comment provided by engineer. */ @@ -2343,7 +2323,7 @@ /* No comment provided by engineer. */ "Learn more" = "Научете повече"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "Напусни"; /* No comment provided by engineer. */ @@ -2568,19 +2548,16 @@ /* item status description */ "Most likely this connection is deleted." = "Най-вероятно тази връзка е изтрита."; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Най-вероятно този контакт е изтрил връзката с вас."; - /* No comment provided by engineer. */ "Multiple chat profiles" = "Множество профили за чат"; -/* No comment provided by engineer. */ +/* swipe action */ "Mute" = "Без звук"; /* No comment provided by engineer. */ "Muted when inactive!" = "Без звук при неактивност!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "Име"; /* No comment provided by engineer. */ @@ -2702,7 +2679,7 @@ time to disappear */ "off" = "изключено"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Изключено"; /* feature offered item */ @@ -2730,10 +2707,10 @@ "One-time invitation link" = "Линк за еднократна покана"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "За свързване ще са необходими Onion хостове. Изисква се активиране на VPN."; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "За свързване ще са **необходими** Onion хостове.\nИзисква се активиране на VPN."; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "Ще се използват Onion хостове, когато са налични. Изисква се активиране на VPN."; +"Onion hosts will be used when available.\nRequires compatible VPN." = "Ще се използват Onion хостове, когато са налични.\nИзисква се активиране на VPN."; /* No comment provided by engineer. */ "Onion hosts will not be used." = "Няма се използват Onion хостове."; @@ -3032,7 +3009,7 @@ /* chat item menu */ "React…" = "Реагирай…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "Прочетено"; /* No comment provided by engineer. */ @@ -3077,9 +3054,6 @@ /* No comment provided by engineer. */ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Получаващият адрес ще бъде променен към друг сървър. Промяната на адреса ще завърши, след като подателят е онлайн."; -/* No comment provided by engineer. */ -"Receiving concurrency" = "Паралелност на получаване"; - /* No comment provided by engineer. */ "Receiving file will be stopped." = "Получаващият се файл ще бъде спрян."; @@ -3110,7 +3084,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Намалена консумация на батерията"; -/* reject incoming call via notification */ +/* reject incoming call via notification + swipe action */ "Reject" = "Отхвърляне"; /* No comment provided by engineer. */ @@ -3218,9 +3193,6 @@ /* chat item action */ "Reveal" = "Покажи"; -/* No comment provided by engineer. */ -"Revert" = "Отмени промените"; - /* No comment provided by engineer. */ "Revoke" = "Отзови"; @@ -3350,7 +3322,7 @@ /* chat item text */ "security code changed" = "кодът за сигурност е променен"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "Избери"; /* No comment provided by engineer. */ @@ -3377,9 +3349,6 @@ /* No comment provided by engineer. */ "send direct message" = "изпрати лично съобщение"; -/* No comment provided by engineer. */ -"Send direct message" = "Изпрати лично съобщение"; - /* No comment provided by engineer. */ "Send direct message to connect" = "Изпрати лично съобщение за свързване"; @@ -3602,9 +3571,6 @@ /* No comment provided by engineer. */ "Small groups (max 20)" = "Малки групи (максимум 20)"; -/* No comment provided by engineer. */ -"SMP servers" = "SMP сървъри"; - /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "Някои не-фатални грешки са възникнали по време на импортиране - може да видите конзолата за повече подробности."; @@ -3704,9 +3670,6 @@ /* No comment provided by engineer. */ "Tap to scan" = "Докосни за сканиране"; -/* No comment provided by engineer. */ -"Tap to start a new chat" = "Докосни за започване на нов чат"; - /* No comment provided by engineer. */ "TCP connection timeout" = "Времето на изчакване за установяване на TCP връзка"; @@ -3797,9 +3760,6 @@ /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "Текстът, който поставихте, не е SimpleX линк за връзка."; -/* No comment provided by engineer. */ -"Theme" = "Тема"; - /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Тези настройки са за текущия ви профил **%@**."; @@ -3920,13 +3880,10 @@ /* rcv group event chat item */ "unblocked %@" = "отблокиран %@"; -/* item status description */ -"Unexpected error: %@" = "Неочаквана грешка: %@"; - /* No comment provided by engineer. */ "Unexpected migration state" = "Неочаквано състояние на миграция"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "Премахни от любимите"; /* No comment provided by engineer. */ @@ -3974,10 +3931,10 @@ /* authentication reason */ "Unlock app" = "Отключи приложението"; -/* No comment provided by engineer. */ +/* swipe action */ "Unmute" = "Уведомявай"; -/* No comment provided by engineer. */ +/* swipe action */ "Unread" = "Непрочетено"; /* No comment provided by engineer. */ @@ -3986,18 +3943,12 @@ /* No comment provided by engineer. */ "Update" = "Актуализация"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = "Актуализиране на настройката за .onion хостове?"; - /* No comment provided by engineer. */ "Update database passphrase" = "Актуализирай паролата на базата данни"; /* No comment provided by engineer. */ "Update network settings?" = "Актуализиране на мрежовите настройки?"; -/* No comment provided by engineer. */ -"Update transport isolation mode?" = "Актуализиране на режима на изолация на транспорта?"; - /* rcv group event chat item */ "updated group profile" = "актуализиран профил на групата"; @@ -4007,9 +3958,6 @@ /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "Актуализирането на настройките ще свърже отново клиента към всички сървъри."; -/* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "Актуализирането на тази настройка ще свърже повторно клиента към всички сървъри."; - /* No comment provided by engineer. */ "Upgrade and open chat" = "Актуализирай и отвори чата"; @@ -4058,9 +4006,6 @@ /* No comment provided by engineer. */ "User profile" = "Потребителски профил"; -/* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "Използването на .onion хостове изисква съвместим VPN доставчик."; - /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "Използват се сървърите на SimpleX Chat."; @@ -4229,9 +4174,6 @@ /* No comment provided by engineer. */ "Wrong passphrase!" = "Грешна парола!"; -/* No comment provided by engineer. */ -"XFTP servers" = "XFTP сървъри"; - /* pref value */ "yes" = "да"; @@ -4314,7 +4256,7 @@ "You can make it visible to your SimpleX contacts via Settings." = "Можете да го направите видим за вашите контакти в SimpleX чрез Настройки."; /* notification body */ -"You can now send messages to %@" = "Вече можете да изпращате съобщения до %@"; +"You can now chat with %@" = "Вече можете да изпращате съобщения до %@"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Можете да зададете визуализация на известията на заключен екран през настройките."; @@ -4367,9 +4309,6 @@ /* No comment provided by engineer. */ "You have already requested connection!\nRepeat connection request?" = "Вече сте направили заявката за връзка!\nИзпрати отново заявката за свързване?"; -/* No comment provided by engineer. */ -"You have no chats" = "Нямате чатове"; - /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "Трябва да въвеждате парола при всяко стартиране на приложението - тя не се съхранява на устройството."; @@ -4460,9 +4399,6 @@ /* No comment provided by engineer. */ "Your chat profiles" = "Вашите чат профили"; -/* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Вашият контакт трябва да бъде онлайн, за да осъществите връзката.\nМожете да откажете тази връзка и да премахнете контакта (и да опитате по -късно с нов линк)."; - /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Вашият контакт изпрати файл, който е по-голям от поддържания в момента максимален размер (%@)."; diff --git a/apps/ios/cs.lproj/Localizable.strings b/apps/ios/cs.lproj/Localizable.strings index e2087547bd..220550c682 100644 --- a/apps/ios/cs.lproj/Localizable.strings +++ b/apps/ios/cs.lproj/Localizable.strings @@ -130,9 +130,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ je ověřený"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ servery"; - /* notification title */ "%@ wants to connect!" = "%@ se chce připojit!"; @@ -289,11 +286,9 @@ /* No comment provided by engineer. */ "above, then choose:" = "výše, pak vyberte:"; -/* No comment provided by engineer. */ -"Accent color" = "Zbarvení"; - /* accept contact request via notification - accept incoming call via notification */ + accept incoming call via notification + swipe action */ "Accept" = "Přijmout"; /* No comment provided by engineer. */ @@ -302,7 +297,8 @@ /* notification body */ "Accept contact request from %@?" = "Přijmout žádost o kontakt od %@?"; -/* accept contact request via notification */ +/* accept contact request via notification + swipe action */ "Accept incognito" = "Přijmout inkognito"; /* call status */ @@ -318,7 +314,7 @@ "Add profile" = "Přidat profil"; /* No comment provided by engineer. */ -"Add server…" = "Přidat server…"; +"Add server" = "Přidat server"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Přidejte servery skenováním QR kódů."; @@ -663,7 +659,7 @@ /* No comment provided by engineer. */ "Choose from library" = "Vybrat z knihovny"; -/* No comment provided by engineer. */ +/* swipe action */ "Clear" = "Vyčistit"; /* No comment provided by engineer. */ @@ -678,9 +674,6 @@ /* No comment provided by engineer. */ "colored" = "barevné"; -/* No comment provided by engineer. */ -"Colors" = "Barvy"; - /* server test step */ "Compare file" = "Porovnat soubor"; @@ -795,9 +788,6 @@ /* notification */ "Contact is connected" = "Kontakt je připojen"; -/* No comment provided by engineer. */ -"Contact is not connected yet!" = "Kontakt ještě není připojen!"; - /* No comment provided by engineer. */ "Contact name" = "Jméno kontaktu"; @@ -813,7 +803,7 @@ /* No comment provided by engineer. */ "Continue" = "Pokračovat"; -/* chat item action */ +/* No comment provided by engineer. */ "Copy" = "Kopírovat"; /* No comment provided by engineer. */ @@ -948,7 +938,8 @@ /* No comment provided by engineer. */ "default (yes)" = "výchozí (ano)"; -/* chat item action */ +/* chat item action + swipe action */ "Delete" = "Smazat"; /* No comment provided by engineer. */ @@ -981,9 +972,6 @@ /* No comment provided by engineer. */ "Delete contact" = "Smazat kontakt"; -/* No comment provided by engineer. */ -"Delete Contact" = "Smazat kontakt"; - /* No comment provided by engineer. */ "Delete database" = "Odstranění databáze"; @@ -1035,9 +1023,6 @@ /* No comment provided by engineer. */ "Delete old database?" = "Smazat starou databázi?"; -/* No comment provided by engineer. */ -"Delete pending connection" = "Smazat čekající připojení"; - /* No comment provided by engineer. */ "Delete pending connection?" = "Smazat čekající připojení?"; @@ -1362,9 +1347,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "Chyba při mazání připojení"; -/* No comment provided by engineer. */ -"Error deleting contact" = "Chyba mazání kontaktu"; - /* No comment provided by engineer. */ "Error deleting database" = "Chyba při mazání databáze"; @@ -1461,7 +1443,8 @@ /* No comment provided by engineer. */ "Error: " = "Chyba: "; -/* No comment provided by engineer. */ +/* file error text + snd error text */ "Error: %@" = "Chyba: %@"; /* No comment provided by engineer. */ @@ -1494,7 +1477,7 @@ /* No comment provided by engineer. */ "Fast and no wait until the sender is online!" = "Rychle a bez čekání, než bude odesílatel online!"; -/* No comment provided by engineer. */ +/* swipe action */ "Favorite" = "Oblíbené"; /* No comment provided by engineer. */ @@ -1878,7 +1861,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Japonské rozhraní"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Připojte se na"; /* No comment provided by engineer. */ @@ -1908,7 +1891,7 @@ /* No comment provided by engineer. */ "Learn more" = "Zjistit více"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "Opustit"; /* No comment provided by engineer. */ @@ -2082,19 +2065,16 @@ /* item status description */ "Most likely this connection is deleted." = "Pravděpodobně je toto spojení smazáno."; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Tento kontakt s největší pravděpodobností smazal spojení s vámi."; - /* No comment provided by engineer. */ "Multiple chat profiles" = "Více chatovacích profilů"; -/* No comment provided by engineer. */ +/* swipe action */ "Mute" = "Ztlumit"; /* No comment provided by engineer. */ "Muted when inactive!" = "Ztlumit při neaktivitě!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "Jméno"; /* No comment provided by engineer. */ @@ -2201,7 +2181,7 @@ time to disappear */ "off" = "vypnuto"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Vypnout"; /* feature offered item */ @@ -2226,10 +2206,10 @@ "One-time invitation link" = "Jednorázový zvací odkaz"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "Pro připojení budou vyžadováni Onion hostitelé. Vyžaduje povolení sítě VPN."; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Pro připojení budou vyžadováni Onion hostitelé.\nVyžaduje povolení sítě VPN."; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "Onion hostitelé budou použiti, pokud jsou k dispozici. Vyžaduje povolení sítě VPN."; +"Onion hosts will be used when available.\nRequires compatible VPN." = "Onion hostitelé budou použiti, pokud jsou k dispozici.\nVyžaduje povolení sítě VPN."; /* No comment provided by engineer. */ "Onion hosts will not be used." = "Onion hostitelé nebudou použiti."; @@ -2456,7 +2436,7 @@ /* chat item menu */ "React…" = "Reagovat…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "Číst"; /* No comment provided by engineer. */ @@ -2522,7 +2502,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Snížení spotřeby baterie"; -/* reject incoming call via notification */ +/* reject incoming call via notification + swipe action */ "Reject" = "Odmítnout"; /* No comment provided by engineer. */ @@ -2606,9 +2587,6 @@ /* chat item action */ "Reveal" = "Odhalit"; -/* No comment provided by engineer. */ -"Revert" = "Vrátit"; - /* No comment provided by engineer. */ "Revoke" = "Odvolat"; @@ -2711,7 +2689,7 @@ /* chat item text */ "security code changed" = "bezpečnostní kód změněn"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "Vybrat"; /* No comment provided by engineer. */ @@ -2738,9 +2716,6 @@ /* No comment provided by engineer. */ "send direct message" = "odeslat přímou zprávu"; -/* No comment provided by engineer. */ -"Send direct message" = "Odeslat přímou zprávu"; - /* No comment provided by engineer. */ "Send direct message to connect" = "Odeslat přímou zprávu pro připojení"; @@ -2933,9 +2908,6 @@ /* No comment provided by engineer. */ "Small groups (max 20)" = "Malé skupiny (max. 20)"; -/* No comment provided by engineer. */ -"SMP servers" = "SMP servery"; - /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "Během importu došlo k nezávažným chybám - podrobnosti naleznete v chat konzoli."; @@ -3011,9 +2983,6 @@ /* No comment provided by engineer. */ "Tap to join incognito" = "Klepnutím se připojíte inkognito"; -/* No comment provided by engineer. */ -"Tap to start a new chat" = "Klepnutím na zahájíte nový chat"; - /* No comment provided by engineer. */ "TCP connection timeout" = "Časový limit připojení TCP"; @@ -3098,9 +3067,6 @@ /* No comment provided by engineer. */ "The servers for new connections of your current chat profile **%@**." = "Servery pro nová připojení vašeho aktuálního chat profilu **%@**."; -/* No comment provided by engineer. */ -"Theme" = "Téma"; - /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Toto nastavení je pro váš aktuální profil **%@**."; @@ -3179,13 +3145,10 @@ /* No comment provided by engineer. */ "Unable to record voice message" = "Nelze nahrát hlasovou zprávu"; -/* item status description */ -"Unexpected error: %@" = "Neočekávaná chyba: %@"; - /* No comment provided by engineer. */ "Unexpected migration state" = "Neočekávaný stav přenášení"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "Odobl."; /* No comment provided by engineer. */ @@ -3224,36 +3187,27 @@ /* authentication reason */ "Unlock app" = "Odemknout aplikaci"; -/* No comment provided by engineer. */ +/* swipe action */ "Unmute" = "Zrušit ztlumení"; -/* No comment provided by engineer. */ +/* swipe action */ "Unread" = "Nepřečtený"; /* No comment provided by engineer. */ "Update" = "Aktualizovat"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = "Aktualizovat nastavení hostitelů .onion?"; - /* No comment provided by engineer. */ "Update database passphrase" = "Aktualizovat přístupovou frázi databáze"; /* No comment provided by engineer. */ "Update network settings?" = "Aktualizovat nastavení sítě?"; -/* No comment provided by engineer. */ -"Update transport isolation mode?" = "Aktualizovat režim dopravní izolace?"; - /* rcv group event chat item */ "updated group profile" = "aktualizoval profil skupiny"; /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "Aktualizací nastavení se klient znovu připojí ke všem serverům."; -/* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "Aktualizace tohoto nastavení znovu připojí klienta ke všem serverům."; - /* No comment provided by engineer. */ "Upgrade and open chat" = "Zvýšit a otevřít chat"; @@ -3287,9 +3241,6 @@ /* No comment provided by engineer. */ "User profile" = "Profil uživatele"; -/* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "Použití hostitelů .onion vyžaduje kompatibilního poskytovatele VPN."; - /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "Používat servery SimpleX Chat."; @@ -3404,9 +3355,6 @@ /* No comment provided by engineer. */ "Wrong passphrase!" = "Špatná přístupová fráze!"; -/* No comment provided by engineer. */ -"XFTP servers" = "XFTP servery"; - /* pref value */ "yes" = "ano"; @@ -3453,7 +3401,7 @@ "You can hide or mute a user profile - swipe it to the right." = "Profil uživatele můžete skrýt nebo ztlumit - přejeďte prstem doprava."; /* notification body */ -"You can now send messages to %@" = "Nyní můžete posílat zprávy %@"; +"You can now chat with %@" = "Nyní můžete posílat zprávy %@"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Náhled oznámení na zamykací obrazovce můžete změnit v nastavení."; @@ -3497,9 +3445,6 @@ /* No comment provided by engineer. */ "You could not be verified; please try again." = "Nemohli jste být ověřeni; Zkuste to prosím znovu."; -/* No comment provided by engineer. */ -"You have no chats" = "Nemáte žádné konverzace"; - /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "Musíte zadat přístupovou frázi při každém spuštění aplikace - není uložena v zařízení."; @@ -3581,9 +3526,6 @@ /* No comment provided by engineer. */ "Your chat profiles" = "Vaše chat profily"; -/* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "K dokončení připojení, musí být váš kontakt online.\nToto připojení můžete zrušit a kontakt odebrat (a zkusit to později s novým odkazem)."; - /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Kontakt odeslal soubor, který je větší než aktuálně podporovaná maximální velikost (%@)."; diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index 5fc393fa1f..bab14eda6b 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -154,9 +154,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ wurde erfolgreich überprüft"; -/* No comment provided by engineer. */ -"%@ servers" = "%@-Server"; - /* No comment provided by engineer. */ "%@ uploaded" = "%@ hochgeladen"; @@ -317,13 +314,13 @@ "A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." = "**Für jeden Kontakt und jedes Gruppenmitglied** wird eine separate TCP-Verbindung genutzt.\n**Bitte beachten Sie**: Wenn Sie viele Verbindungen haben, kann der Batterieverbrauch und die Datennutzung wesentlich höher sein und einige Verbindungen können scheitern."; /* No comment provided by engineer. */ -"Abort" = "Abbrechen"; +"Abort" = "Beenden"; /* No comment provided by engineer. */ -"Abort changing address" = "Wechsel der Empfängeradresse abbrechen"; +"Abort changing address" = "Wechsel der Empfängeradresse beenden"; /* No comment provided by engineer. */ -"Abort changing address?" = "Wechsel der Empfängeradresse abbrechen?"; +"Abort changing address?" = "Wechsel der Empfängeradresse beenden?"; /* No comment provided by engineer. */ "About SimpleX" = "Über SimpleX"; @@ -338,10 +335,11 @@ "above, then choose:" = "Danach die gewünschte Aktion auswählen:"; /* No comment provided by engineer. */ -"Accent color" = "Akzentfarbe"; +"Accent" = "Akzent"; /* accept contact request via notification - accept incoming call via notification */ + accept incoming call via notification + swipe action */ "Accept" = "Annehmen"; /* No comment provided by engineer. */ @@ -350,12 +348,22 @@ /* notification body */ "Accept contact request from %@?" = "Die Kontaktanfrage von %@ annehmen?"; -/* accept contact request via notification */ +/* accept contact request via notification + swipe action */ "Accept incognito" = "Inkognito akzeptieren"; /* call status */ "accepted call" = "Anruf angenommen"; +/* No comment provided by engineer. */ +"Acknowledged" = "Bestätigt"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Fehler bei der Bestätigung"; + +/* No comment provided by engineer. */ +"Active connections" = "Aktive Verbindungen"; + /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Fügen Sie die Adresse Ihrem Profil hinzu, damit Ihre Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet."; @@ -369,7 +377,7 @@ "Add profile" = "Profil hinzufügen"; /* No comment provided by engineer. */ -"Add server…" = "Füge Server hinzu…"; +"Add server" = "Füge Server hinzu"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Fügen Sie Server durch Scannen der QR Codes hinzu."; @@ -380,11 +388,20 @@ /* No comment provided by engineer. */ "Add welcome message" = "Begrüßungsmeldung hinzufügen"; +/* No comment provided by engineer. */ +"Additional accent" = "Erste Akzentfarbe"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Zusätzlicher Akzent 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "Zweite Akzentfarbe"; + /* No comment provided by engineer. */ "Address" = "Adresse"; /* No comment provided by engineer. */ -"Address change will be aborted. Old receiving address will be used." = "Der Wechsel der Empfängeradresse wird abgebrochen. Die bisherige Adresse wird weiter verwendet."; +"Address change will be aborted. Old receiving address will be used." = "Der Wechsel der Empfängeradresse wird beendet. Die bisherige Adresse wird weiter verwendet."; /* member role */ "admin" = "Admin"; @@ -401,6 +418,9 @@ /* No comment provided by engineer. */ "Advanced network settings" = "Erweiterte Netzwerkeinstellungen"; +/* No comment provided by engineer. */ +"Advanced settings" = "Erweiterte Einstellungen"; + /* chat item text */ "agreeing encryption for %@…" = "Verschlüsselung von %@ zustimmen…"; @@ -411,11 +431,14 @@ "All app data is deleted." = "Werden die App-Daten komplett gelöscht."; /* No comment provided by engineer. */ -"All chats and messages will be deleted - this cannot be undone!" = "Alle Chats und Nachrichten werden gelöscht! Dies kann nicht rückgängig gemacht werden!"; +"All chats and messages will be deleted - this cannot be undone!" = "Es werden alle Chats und Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden!"; /* No comment provided by engineer. */ "All data is erased when it is entered." = "Alle Daten werden gelöscht, sobald dieser eingegeben wird."; +/* No comment provided by engineer. */ +"All data is private to your device." = "Alle Daten werden nur auf Ihrem Gerät gespeichert."; + /* No comment provided by engineer. */ "All group members will remain connected." = "Alle Gruppenmitglieder bleiben verbunden."; @@ -423,14 +446,17 @@ "all members" = "Alle Mitglieder"; /* No comment provided by engineer. */ -"All messages will be deleted - this cannot be undone!" = "Es werden alle Nachrichten gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden!"; +"All messages will be deleted - this cannot be undone!" = "Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden!"; /* No comment provided by engineer. */ -"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Alle Nachrichten werden gelöscht - dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht."; +"All messages will be deleted - this cannot be undone! The messages will be deleted ONLY for you." = "Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht."; /* No comment provided by engineer. */ "All new messages from %@ will be hidden!" = "Von %@ werden alle neuen Nachrichten ausgeblendet!"; +/* No comment provided by engineer. */ +"All profiles" = "Alle Profile"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Alle Ihre Kontakte bleiben verbunden."; @@ -438,7 +464,7 @@ "All your contacts will remain connected. Profile update will be sent to your contacts." = "Alle Ihre Kontakte bleiben verbunden. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet."; /* No comment provided by engineer. */ -"All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Alle Ihre Kontakte, Unterhaltungen und Dateien werden sicher verschlüsselt und in Daten-Paketen auf die konfigurierten XTFP-Server hochgeladen."; +"All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Alle Ihre Kontakte, Unterhaltungen und Dateien werden sicher verschlüsselt und in Daten-Paketen auf die konfigurierten XTFP-Relais hochgeladen."; /* No comment provided by engineer. */ "Allow" = "Erlauben"; @@ -446,9 +472,15 @@ /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "Erlauben Sie Anrufe nur dann, wenn es Ihr Kontakt ebenfalls erlaubt."; +/* No comment provided by engineer. */ +"Allow calls?" = "Anrufe erlauben?"; + /* No comment provided by engineer. */ "Allow disappearing messages only if your contact allows it to you." = "Erlauben Sie verschwindende Nachrichten nur dann, wenn es Ihr Kontakt ebenfalls erlaubt."; +/* No comment provided by engineer. */ +"Allow downgrade" = "Herabstufung erlauben"; + /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Erlauben Sie das unwiederbringliche Löschen von Nachrichten nur dann, wenn es Ihnen Ihr Kontakt ebenfalls erlaubt. (24 Stunden)"; @@ -464,6 +496,9 @@ /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Das Senden von verschwindenden Nachrichten erlauben."; +/* No comment provided by engineer. */ +"Allow sharing" = "Teilen erlauben"; + /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Unwiederbringliches löschen von gesendeten Nachrichten erlauben. (24 Stunden)"; @@ -509,6 +544,9 @@ /* pref value */ "always" = "Immer"; +/* No comment provided by engineer. */ +"Always use private routing." = "Sie nutzen immer privates Routing."; + /* No comment provided by engineer. */ "Always use relay" = "Über ein Relais verbinden"; @@ -551,15 +589,27 @@ /* No comment provided by engineer. */ "Apply" = "Anwenden"; +/* No comment provided by engineer. */ +"Apply to" = "Anwenden auf"; + /* No comment provided by engineer. */ "Archive and upload" = "Archivieren und Hochladen"; +/* No comment provided by engineer. */ +"Archive contacts to chat later." = "Kontakte für spätere Chats archivieren."; + +/* No comment provided by engineer. */ +"Archived contacts" = "Archivierte Kontakte"; + /* No comment provided by engineer. */ "Archiving database" = "Datenbank wird archiviert"; /* No comment provided by engineer. */ "Attach" = "Anhängen"; +/* No comment provided by engineer. */ +"attempts" = "Versuche"; + /* No comment provided by engineer. */ "Audio & video calls" = "Audio- & Videoanrufe"; @@ -602,6 +652,9 @@ /* No comment provided by engineer. */ "Back" = "Zurück"; +/* No comment provided by engineer. */ +"Background" = "Hintergrund-Farbe"; + /* No comment provided by engineer. */ "Bad desktop address" = "Falsche Desktop-Adresse"; @@ -623,6 +676,12 @@ /* No comment provided by engineer. */ "Better messages" = "Verbesserungen bei Nachrichten"; +/* No comment provided by engineer. */ +"Better networking" = "Kontrollieren Sie Ihr Netzwerk"; + +/* No comment provided by engineer. */ +"Black" = "Schwarz"; + /* No comment provided by engineer. */ "Block" = "Blockieren"; @@ -653,6 +712,12 @@ /* No comment provided by engineer. */ "Blocked by admin" = "wurde vom Administrator blockiert"; +/* No comment provided by engineer. */ +"Blur for better privacy." = "Für bessere Privatsphäre verpixeln."; + +/* No comment provided by engineer. */ +"Blur media" = "Medium unscharf machen"; + /* No comment provided by engineer. */ "bold" = "fett"; @@ -677,6 +742,9 @@ /* No comment provided by engineer. */ "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Per Chat-Profil (Voreinstellung) oder [per Verbindung](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; +/* No comment provided by engineer. */ +"call" = "Anrufen"; + /* No comment provided by engineer. */ "Call already ended!" = "Anruf ist bereits beendet!"; @@ -692,15 +760,27 @@ /* No comment provided by engineer. */ "Calls" = "Anrufe"; +/* No comment provided by engineer. */ +"Calls prohibited!" = "Anrufe nicht zugelassen!"; + /* No comment provided by engineer. */ "Camera not available" = "Kamera nicht verfügbar"; +/* No comment provided by engineer. */ +"Can't call contact" = "Kontakt kann nicht angerufen werden"; + +/* No comment provided by engineer. */ +"Can't call member" = "Mitglied kann nicht angerufen werden"; + /* No comment provided by engineer. */ "Can't invite contact!" = "Kontakt kann nicht eingeladen werden!"; /* No comment provided by engineer. */ "Can't invite contacts!" = "Kontakte können nicht eingeladen werden!"; +/* No comment provided by engineer. */ +"Can't message member" = "Mitglied kann nicht benachrichtigt werden"; + /* No comment provided by engineer. */ "Cancel" = "Abbrechen"; @@ -714,10 +794,16 @@ "Cannot access keychain to save database password" = "Die App kann nicht auf den Schlüsselbund zugreifen, um das Datenbank-Passwort zu speichern"; /* No comment provided by engineer. */ -"Cannot receive file" = "Datei kann nicht empfangen werden"; +"Cannot forward message" = "Die Nachricht kann nicht weitergeleitet werden"; /* No comment provided by engineer. */ -"Cellular" = "Zellulär"; +"Cannot receive file" = "Datei kann nicht empfangen werden"; + +/* snd error text */ +"Capacity exceeded - recipient did not receive previously sent messages." = "Kapazität überschritten - der Empfänger hat die zuvor gesendeten Nachrichten nicht empfangen."; + +/* No comment provided by engineer. */ +"Cellular" = "Mobilfunknetz"; /* No comment provided by engineer. */ "Change" = "Ändern"; @@ -768,6 +854,9 @@ /* No comment provided by engineer. */ "Chat archive" = "Datenbank Archiv"; +/* No comment provided by engineer. */ +"Chat colors" = "Chat-Farben"; + /* No comment provided by engineer. */ "Chat console" = "Chat-Konsole"; @@ -777,6 +866,9 @@ /* No comment provided by engineer. */ "Chat database deleted" = "Chat-Datenbank gelöscht"; +/* No comment provided by engineer. */ +"Chat database exported" = "Chat-Datenbank wurde exportiert"; + /* No comment provided by engineer. */ "Chat database imported" = "Chat-Datenbank importiert"; @@ -789,12 +881,18 @@ /* No comment provided by engineer. */ "Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." = "Der Chat ist angehalten. Wenn Sie diese Datenbank bereits auf einem anderen Gerät genutzt haben, sollten Sie diese vor dem Starten des Chats wieder zurückspielen."; +/* No comment provided by engineer. */ +"Chat list" = "Chat-Liste"; + /* No comment provided by engineer. */ "Chat migrated!" = "Chat wurde migriert!"; /* No comment provided by engineer. */ "Chat preferences" = "Chat-Präferenzen"; +/* No comment provided by engineer. */ +"Chat theme" = "Chat-Design"; + /* No comment provided by engineer. */ "Chats" = "Chats"; @@ -814,6 +912,15 @@ "Choose from library" = "Aus dem Fotoalbum auswählen"; /* No comment provided by engineer. */ +"Chunks deleted" = "Daten-Pakete gelöscht"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Daten-Pakete heruntergeladen"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Daten-Pakete hochgeladen"; + +/* swipe action */ "Clear" = "Löschen"; /* No comment provided by engineer. */ @@ -829,10 +936,13 @@ "Clear verification" = "Überprüfung zurücknehmen"; /* No comment provided by engineer. */ -"colored" = "farbig"; +"Color chats with the new themes." = "Farbige Chats mit neuen Designs."; /* No comment provided by engineer. */ -"Colors" = "Farben"; +"Color mode" = "Farbvariante"; + +/* No comment provided by engineer. */ +"colored" = "farbig"; /* server test step */ "Compare file" = "Datei vergleichen"; @@ -843,15 +953,27 @@ /* No comment provided by engineer. */ "complete" = "vollständig"; +/* No comment provided by engineer. */ +"Completed" = "Abgeschlossen"; + /* No comment provided by engineer. */ "Configure ICE servers" = "ICE-Server konfigurieren"; +/* No comment provided by engineer. */ +"Configured %@ servers" = "Konfigurierte %@ Server"; + /* No comment provided by engineer. */ "Confirm" = "Bestätigen"; +/* No comment provided by engineer. */ +"Confirm contact deletion?" = "Löschen des Kontakts bestätigen?"; + /* No comment provided by engineer. */ "Confirm database upgrades" = "Datenbank-Aktualisierungen bestätigen"; +/* No comment provided by engineer. */ +"Confirm files from unknown servers." = "Dateien von unbekannten Servern bestätigen."; + /* No comment provided by engineer. */ "Confirm network settings" = "Bestätigen Sie die Netzwerkeinstellungen"; @@ -885,6 +1007,9 @@ /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "Mit den SimpleX Chat-Entwicklern verbinden."; +/* No comment provided by engineer. */ +"Connect to your friends faster." = "Schneller mit Ihren Freunden verbinden."; + /* No comment provided by engineer. */ "Connect to yourself?" = "Mit Ihnen selbst verbinden?"; @@ -909,18 +1034,27 @@ /* No comment provided by engineer. */ "connected" = "Verbunden"; +/* No comment provided by engineer. */ +"Connected" = "Verbunden"; + /* No comment provided by engineer. */ "Connected desktop" = "Verbundener Desktop"; /* rcv group event chat item */ "connected directly" = "Direkt miteinander verbunden"; +/* No comment provided by engineer. */ +"Connected servers" = "Verbundene Server"; + /* No comment provided by engineer. */ "Connected to desktop" = "Mit dem Desktop verbunden"; /* No comment provided by engineer. */ "connecting" = "verbinde"; +/* No comment provided by engineer. */ +"Connecting" = "Verbinden"; + /* No comment provided by engineer. */ "connecting (accepted)" = "Verbindung (angenommen)"; @@ -942,6 +1076,9 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "Mit dem Server verbinden… (Fehler: %@)"; +/* No comment provided by engineer. */ +"Connecting to contact, please wait or check later!" = "Verbinde mit Kontakt, bitte warten oder später erneut überprüfen!"; + /* No comment provided by engineer. */ "Connecting to desktop" = "Mit dem Desktop verbinden"; @@ -951,6 +1088,9 @@ /* No comment provided by engineer. */ "Connection" = "Verbindung"; +/* No comment provided by engineer. */ +"Connection and servers status." = "Verbindungs- und Server-Status."; + /* No comment provided by engineer. */ "Connection error" = "Verbindungsfehler"; @@ -960,6 +1100,9 @@ /* chat list item title (it should not be shown */ "connection established" = "Verbindung hergestellt"; +/* No comment provided by engineer. */ +"Connection notifications" = "Verbindungsbenachrichtigungen"; + /* No comment provided by engineer. */ "Connection request sent!" = "Verbindungsanfrage wurde gesendet!"; @@ -969,9 +1112,15 @@ /* No comment provided by engineer. */ "Connection timeout" = "Verbindungszeitüberschreitung"; +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "Die Verbindung mit dem Desktop wurde gestoppt"; + /* connection information */ "connection:%@" = "Verbindung:%@"; +/* No comment provided by engineer. */ +"Connections" = "Verbindungen"; + /* profile update event chat item */ "contact %@ changed to %@" = "Der Kontaktname wurde von %1$@ auf %2$@ geändert"; @@ -981,6 +1130,9 @@ /* No comment provided by engineer. */ "Contact already exists" = "Der Kontakt ist bereits vorhanden"; +/* No comment provided by engineer. */ +"Contact deleted!" = "Kontakt gelöscht!"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "Kontakt nutzt E2E-Verschlüsselung"; @@ -994,7 +1146,7 @@ "Contact is connected" = "Mit Ihrem Kontakt verbunden"; /* No comment provided by engineer. */ -"Contact is not connected yet!" = "Ihr Kontakt ist noch nicht verbunden!"; +"Contact is deleted." = "Kontakt wurde gelöscht."; /* No comment provided by engineer. */ "Contact name" = "Kontaktname"; @@ -1002,6 +1154,9 @@ /* No comment provided by engineer. */ "Contact preferences" = "Kontakt-Präferenzen"; +/* No comment provided by engineer. */ +"Contact will be deleted - this cannot be undone!" = "Kontakt wird gelöscht. Dies kann nicht rückgängig gemacht werden!"; + /* No comment provided by engineer. */ "Contacts" = "Kontakte"; @@ -1011,9 +1166,15 @@ /* No comment provided by engineer. */ "Continue" = "Weiter"; -/* chat item action */ +/* No comment provided by engineer. */ +"Conversation deleted!" = "Unterhaltung gelöscht!"; + +/* No comment provided by engineer. */ "Copy" = "Kopieren"; +/* No comment provided by engineer. */ +"Copy error" = "Fehlermeldung kopieren"; + /* No comment provided by engineer. */ "Core version: v%@" = "Core Version: v%@"; @@ -1059,6 +1220,9 @@ /* No comment provided by engineer. */ "Create your profile" = "Erstellen Sie Ihr Profil"; +/* No comment provided by engineer. */ +"Created" = "Erstellt"; + /* No comment provided by engineer. */ "Created at" = "Erstellt um"; @@ -1083,6 +1247,9 @@ /* No comment provided by engineer. */ "Current passphrase…" = "Aktuelles Passwort…"; +/* No comment provided by engineer. */ +"Current profile" = "Aktueller Profil"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "Die derzeit maximal unterstützte Dateigröße beträgt %@."; @@ -1092,9 +1259,15 @@ /* No comment provided by engineer. */ "Custom time" = "Zeit anpassen"; +/* No comment provided by engineer. */ +"Customize theme" = "Design anpassen"; + /* No comment provided by engineer. */ "Dark" = "Dunkel"; +/* No comment provided by engineer. */ +"Dark mode colors" = "Farben für die dunkle Variante"; + /* No comment provided by engineer. */ "Database downgrade" = "Datenbank auf alte Version herabstufen"; @@ -1155,12 +1328,18 @@ /* time unit */ "days" = "Tage"; +/* No comment provided by engineer. */ +"Debug delivery" = "Debugging-Zustellung"; + /* No comment provided by engineer. */ "Decentralized" = "Dezentral"; /* message decrypt error item */ "Decryption error" = "Entschlüsselungsfehler"; +/* No comment provided by engineer. */ +"decryption errors" = "Entschlüsselungs-Fehler"; + /* pref value */ "default (%@)" = "Voreinstellung (%@)"; @@ -1170,9 +1349,13 @@ /* No comment provided by engineer. */ "default (yes)" = "Voreinstellung (Ja)"; -/* chat item action */ +/* chat item action + swipe action */ "Delete" = "Löschen"; +/* No comment provided by engineer. */ +"Delete %lld messages of members?" = "%lld Nachrichten der Mitglieder löschen?"; + /* No comment provided by engineer. */ "Delete %lld messages?" = "%lld Nachrichten löschen?"; @@ -1210,10 +1393,7 @@ "Delete contact" = "Kontakt löschen"; /* No comment provided by engineer. */ -"Delete Contact" = "Kontakt löschen"; - -/* No comment provided by engineer. */ -"Delete contact?\nThis cannot be undone!" = "Kontakt löschen?\nDas kann nicht rückgängig gemacht werden!"; +"Delete contact?" = "Kontakt löschen?"; /* No comment provided by engineer. */ "Delete database" = "Datenbank löschen"; @@ -1270,10 +1450,7 @@ "Delete old database?" = "Alte Datenbank löschen?"; /* No comment provided by engineer. */ -"Delete pending connection" = "Ausstehende Verbindung löschen"; - -/* No comment provided by engineer. */ -"Delete pending connection?" = "Die ausstehende Verbindung löschen?"; +"Delete pending connection?" = "Ausstehende Verbindung löschen?"; /* No comment provided by engineer. */ "Delete profile" = "Profil löschen"; @@ -1281,12 +1458,21 @@ /* server test step */ "Delete queue" = "Lösche Warteschlange"; +/* No comment provided by engineer. */ +"Delete up to 20 messages at once." = "Löschen Sie bis zu 20 Nachrichten auf einmal."; + /* No comment provided by engineer. */ "Delete user profile?" = "Benutzerprofil löschen?"; +/* No comment provided by engineer. */ +"Delete without notification" = "Ohne Benachrichtigung löschen"; + /* deleted chat item */ "deleted" = "Gelöscht"; +/* No comment provided by engineer. */ +"Deleted" = "Gelöscht"; + /* No comment provided by engineer. */ "Deleted at" = "Gelöscht um"; @@ -1299,6 +1485,9 @@ /* rcv group event chat item */ "deleted group" = "Gruppe gelöscht"; +/* No comment provided by engineer. */ +"Deletion errors" = "Fehler beim Löschen"; + /* No comment provided by engineer. */ "Delivery" = "Zustellung"; @@ -1320,9 +1509,27 @@ /* No comment provided by engineer. */ "Desktop devices" = "Desktop-Geräte"; +/* No comment provided by engineer. */ +"Destination server address of %@ is incompatible with forwarding server %@ settings." = "Adresse des Zielservers von %@ ist nicht kompatibel mit den Einstellungen des Weiterleitungsservers %@."; + +/* snd error text */ +"Destination server error: %@" = "Zielserver-Fehler: %@"; + +/* No comment provided by engineer. */ +"Destination server version of %@ is incompatible with forwarding server %@." = "Die Version des Zielservers %@ ist nicht kompatibel mit dem Weiterleitungsserver %@."; + +/* No comment provided by engineer. */ +"Detailed statistics" = "Detaillierte Statistiken"; + +/* No comment provided by engineer. */ +"Details" = "Details"; + /* No comment provided by engineer. */ "Develop" = "Entwicklung"; +/* No comment provided by engineer. */ +"Developer options" = "Optionen für Entwickler"; + /* No comment provided by engineer. */ "Developer tools" = "Entwicklertools"; @@ -1362,6 +1569,9 @@ /* No comment provided by engineer. */ "disabled" = "deaktiviert"; +/* No comment provided by engineer. */ +"Disabled" = "Deaktiviert"; + /* No comment provided by engineer. */ "Disappearing message" = "Verschwindende Nachricht"; @@ -1399,7 +1609,13 @@ "Do not send history to new members." = "Den Nachrichtenverlauf nicht an neue Mitglieder senden."; /* No comment provided by engineer. */ -"Do NOT use SimpleX for emergency calls." = "Nutzen Sie SimpleX nicht für Notrufe."; +"Do NOT send messages directly, even if your or destination server does not support private routing." = "Nachrichten werden nicht direkt versendet, selbst wenn Ihr oder der Zielserver kein privates Routing unterstützt."; + +/* No comment provided by engineer. */ +"Do NOT use private routing." = "Sie nutzen KEIN privates Routing."; + +/* No comment provided by engineer. */ +"Do NOT use SimpleX for emergency calls." = "SimpleX NICHT für Notrufe nutzen."; /* No comment provided by engineer. */ "Don't create address" = "Keine Adresse erstellt"; @@ -1416,12 +1632,21 @@ /* chat item action */ "Download" = "Herunterladen"; +/* No comment provided by engineer. */ +"Download errors" = "Fehler beim Herunterladen"; + /* No comment provided by engineer. */ "Download failed" = "Herunterladen fehlgeschlagen"; /* server test step */ "Download file" = "Datei herunterladen"; +/* No comment provided by engineer. */ +"Downloaded" = "Heruntergeladen"; + +/* No comment provided by engineer. */ +"Downloaded files" = "Heruntergeladene Dateien"; + /* No comment provided by engineer. */ "Downloading archive" = "Archiv wird heruntergeladen"; @@ -1434,6 +1659,9 @@ /* integrity error chat item */ "duplicate message" = "Doppelte Nachricht"; +/* No comment provided by engineer. */ +"duplicates" = "Duplikate"; + /* No comment provided by engineer. */ "Duration" = "Dauer"; @@ -1491,6 +1719,9 @@ /* enabled status */ "enabled" = "Aktiviert"; +/* No comment provided by engineer. */ +"Enabled" = "Aktiviert"; + /* No comment provided by engineer. */ "Enabled for" = "Aktiviert für"; @@ -1612,7 +1843,7 @@ "Error" = "Fehler"; /* No comment provided by engineer. */ -"Error aborting address change" = "Fehler beim Abbrechen des Adresswechsels"; +"Error aborting address change" = "Fehler beim Beenden des Adresswechsels"; /* No comment provided by engineer. */ "Error accepting contact request" = "Fehler beim Annehmen der Kontaktanfrage"; @@ -1632,6 +1863,9 @@ /* No comment provided by engineer. */ "Error changing setting" = "Fehler beim Ändern der Einstellung"; +/* No comment provided by engineer. */ +"Error connecting to forwarding server %@. Please try later." = "Fehler beim Verbinden mit dem Weiterleitungsserver %@. Bitte versuchen Sie es später erneut."; + /* No comment provided by engineer. */ "Error creating address" = "Fehler beim Erstellen der Adresse"; @@ -1662,9 +1896,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "Fehler beim Löschen der Verbindung"; -/* No comment provided by engineer. */ -"Error deleting contact" = "Fehler beim Löschen des Kontakts"; - /* No comment provided by engineer. */ "Error deleting database" = "Fehler beim Löschen der Datenbank"; @@ -1692,6 +1923,9 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "Fehler beim Exportieren der Chat-Datenbank"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Fehler beim Exportieren des Designs: %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "Fehler beim Importieren der Chat-Datenbank"; @@ -1707,9 +1941,18 @@ /* No comment provided by engineer. */ "Error receiving file" = "Fehler beim Empfangen der Datei"; +/* No comment provided by engineer. */ +"Error reconnecting server" = "Fehler beim Wiederherstellen der Verbindung zum Server"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "Fehler beim Wiederherstellen der Verbindungen zu den Servern"; + /* No comment provided by engineer. */ "Error removing member" = "Fehler beim Entfernen des Mitglieds"; +/* No comment provided by engineer. */ +"Error resetting statistics" = "Fehler beim Zurücksetzen der Statistiken"; + /* No comment provided by engineer. */ "Error saving %@ servers" = "Fehler beim Speichern der %@-Server"; @@ -1779,7 +2022,8 @@ /* No comment provided by engineer. */ "Error: " = "Fehler: "; -/* No comment provided by engineer. */ +/* file error text + snd error text */ "Error: %@" = "Fehler: %@"; /* No comment provided by engineer. */ @@ -1788,6 +2032,9 @@ /* No comment provided by engineer. */ "Error: URL is invalid" = "Fehler: URL ist ungültig"; +/* No comment provided by engineer. */ +"Errors" = "Fehler"; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Auch wenn sie im Chat deaktiviert sind."; @@ -1800,12 +2047,18 @@ /* chat item action */ "Expand" = "Erweitern"; +/* No comment provided by engineer. */ +"expired" = "abgelaufen"; + /* No comment provided by engineer. */ "Export database" = "Datenbank exportieren"; /* No comment provided by engineer. */ "Export error:" = "Fehler beim Export:"; +/* No comment provided by engineer. */ +"Export theme" = "Design exportieren"; + /* No comment provided by engineer. */ "Exported database archive." = "Exportiertes Datenbankarchiv."; @@ -1824,9 +2077,24 @@ /* No comment provided by engineer. */ "Faster joining and more reliable messages." = "Schnellerer Gruppenbeitritt und zuverlässigere Nachrichtenzustellung."; -/* No comment provided by engineer. */ +/* swipe action */ "Favorite" = "Favorit"; +/* No comment provided by engineer. */ +"File error" = "Datei-Fehler"; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "Datei nicht gefunden - höchstwahrscheinlich wurde die Datei gelöscht oder der Transfer abgebrochen."; + +/* file error text */ +"File server error: %@" = "Datei-Server Fehler: %@"; + +/* No comment provided by engineer. */ +"File status" = "Datei-Status"; + +/* copied message info */ +"File status: %@" = "Datei-Status: %@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "Die Datei wird von den Servern gelöscht."; @@ -1839,6 +2107,9 @@ /* No comment provided by engineer. */ "File: %@" = "Datei: %@"; +/* No comment provided by engineer. */ +"Files" = "Dateien"; + /* No comment provided by engineer. */ "Files & media" = "Dateien & Medien"; @@ -1905,6 +2176,21 @@ /* No comment provided by engineer. */ "Forwarded from" = "Weitergeleitet aus"; +/* No comment provided by engineer. */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "Weiterleitungsserver %@ konnte sich nicht mit dem Zielserver %@ verbinden. Bitte versuchen Sie es später erneut."; + +/* No comment provided by engineer. */ +"Forwarding server address is incompatible with network settings: %@." = "Adresse des Weiterleitungsservers ist nicht kompatibel mit den Netzwerkeinstellungen: %@."; + +/* No comment provided by engineer. */ +"Forwarding server version is incompatible with network settings: %@." = "Version des Weiterleitungsservers ist nicht kompatibel mit den Netzwerkeinstellungen: %@."; + +/* snd error text */ +"Forwarding server: %@\nDestination server error: %@" = "Weiterleitungsserver: %1$@\nZielserver Fehler: %2$@"; + +/* snd error text */ +"Forwarding server: %@\nError: %@" = "Weiterleitungsserver: %1$@\nFehler: %2$@"; + /* No comment provided by engineer. */ "Found desktop" = "Gefundener Desktop"; @@ -1932,6 +2218,12 @@ /* No comment provided by engineer. */ "GIFs and stickers" = "GIFs und Sticker"; +/* message preview */ +"Good afternoon!" = "Guten Nachmittag!"; + +/* message preview */ +"Good morning!" = "Guten Morgen!"; + /* No comment provided by engineer. */ "Group" = "Gruppe"; @@ -2011,10 +2303,10 @@ "Group welcome message" = "Gruppen-Begrüßungsmeldung"; /* No comment provided by engineer. */ -"Group will be deleted for all members - this cannot be undone!" = "Die Gruppe wird für alle Mitglieder gelöscht - dies kann nicht rückgängig gemacht werden!"; +"Group will be deleted for all members - this cannot be undone!" = "Die Gruppe wird für alle Mitglieder gelöscht. Dies kann nicht rückgängig gemacht werden!"; /* No comment provided by engineer. */ -"Group will be deleted for you - this cannot be undone!" = "Die Gruppe wird für Sie gelöscht - dies kann nicht rückgängig gemacht werden!"; +"Group will be deleted for you - this cannot be undone!" = "Die Gruppe wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden!"; /* No comment provided by engineer. */ "Help" = "Hilfe"; @@ -2109,6 +2401,9 @@ /* No comment provided by engineer. */ "Import failed" = "Import ist fehlgeschlagen"; +/* No comment provided by engineer. */ +"Import theme" = "Design importieren"; + /* No comment provided by engineer. */ "Importing archive" = "Archiv wird importiert"; @@ -2130,6 +2425,9 @@ /* No comment provided by engineer. */ "In-call sounds" = "Klingeltöne"; +/* No comment provided by engineer. */ +"inactive" = "Inaktiv"; + /* No comment provided by engineer. */ "Incognito" = "Inkognito"; @@ -2161,10 +2459,10 @@ "Incoming video call" = "Eingehender Videoanruf"; /* No comment provided by engineer. */ -"Incompatible database version" = "Inkompatible Datenbank-Version"; +"Incompatible database version" = "Datenbank-Version nicht kompatibel"; /* No comment provided by engineer. */ -"Incompatible version" = "Inkompatible Version"; +"Incompatible version" = "Version nicht kompatibel"; /* PIN entry */ "Incorrect passcode" = "Zugangscode ist falsch"; @@ -2193,6 +2491,9 @@ /* No comment provided by engineer. */ "Interface" = "Schnittstelle"; +/* No comment provided by engineer. */ +"Interface colors" = "Interface-Farben"; + /* invalid chat data */ "invalid chat" = "Ungültiger Chat"; @@ -2235,6 +2536,9 @@ /* group name */ "invitation to group %@" = "Einladung zur Gruppe %@"; +/* No comment provided by engineer. */ +"invite" = "Einladen"; + /* No comment provided by engineer. */ "Invite friends" = "Freunde einladen"; @@ -2280,6 +2584,9 @@ /* No comment provided by engineer. */ "It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Dies kann unter folgenden Umständen passieren:\n1. Die Nachrichten verfallen auf dem sendenden Client-System nach 2 Tagen oder auf dem Server nach 30 Tagen.\n2. Die Nachrichten-Entschlüsselung ist fehlgeschlagen, da von Ihnen oder Ihrem Kontakt ein altes Datenbank-Backup genutzt wurde.\n3. Die Verbindung wurde kompromittiert."; +/* No comment provided by engineer. */ +"It protects your IP address and connections." = "Ihre IP-Adresse und Verbindungen werden geschützt."; + /* No comment provided by engineer. */ "It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Es sieht so aus, als ob Sie bereits über diesen Link verbunden sind. Wenn das nicht der Fall ist, gab es einen Fehler (%@)."; @@ -2292,7 +2599,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Japanische Benutzeroberfläche"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Beitreten"; /* No comment provided by engineer. */ @@ -2322,6 +2629,9 @@ /* No comment provided by engineer. */ "Keep" = "Behalten"; +/* No comment provided by engineer. */ +"Keep conversation" = "Unterhaltung behalten"; + /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Die App muss geöffnet bleiben, um sie vom Desktop aus nutzen zu können"; @@ -2343,7 +2653,7 @@ /* No comment provided by engineer. */ "Learn more" = "Mehr erfahren"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "Verlassen"; /* No comment provided by engineer. */ @@ -2433,6 +2743,12 @@ /* No comment provided by engineer. */ "Max 30 seconds, received instantly." = "Max. 30 Sekunden, sofort erhalten."; +/* No comment provided by engineer. */ +"Media & file servers" = "Medien- und Datei-Server"; + +/* blur media */ +"Medium" = "Medium"; + /* member role */ "member" = "Mitglied"; @@ -2445,6 +2761,9 @@ /* rcv group event chat item */ "member connected" = "ist der Gruppe beigetreten"; +/* item status text */ +"Member inactive" = "Mitglied inaktiv"; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "Die Mitgliederrolle wird auf \"%@\" geändert. Alle Mitglieder der Gruppe werden benachrichtigt."; @@ -2452,7 +2771,13 @@ "Member role will be changed to \"%@\". The member will receive a new invitation." = "Die Mitgliederrolle wird auf \"%@\" geändert. Das Mitglied wird eine neue Einladung erhalten."; /* No comment provided by engineer. */ -"Member will be removed from group - this cannot be undone!" = "Das Mitglied wird aus der Gruppe entfernt - dies kann nicht rückgängig gemacht werden!"; +"Member will be removed from group - this cannot be undone!" = "Das Mitglied wird aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden!"; + +/* No comment provided by engineer. */ +"Menus" = "Menüs"; + +/* No comment provided by engineer. */ +"message" = "Nachricht"; /* item status text */ "Message delivery error" = "Fehler bei der Nachrichtenzustellung"; @@ -2460,9 +2785,21 @@ /* No comment provided by engineer. */ "Message delivery receipts!" = "Empfangsbestätigungen für Nachrichten!"; +/* item status text */ +"Message delivery warning" = "Warnung bei der Nachrichtenzustellung"; + /* No comment provided by engineer. */ "Message draft" = "Nachrichtenentwurf"; +/* item status text */ +"Message forwarded" = "Nachricht weitergeleitet"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "Die Nachricht kann später zugestellt werden, wenn das Mitglied aktiv wird."; + +/* No comment provided by engineer. */ +"Message queue info" = "Nachrichten-Warteschlangen-Information"; + /* chat feature */ "Message reactions" = "Reaktionen auf Nachrichten"; @@ -2475,9 +2812,21 @@ /* notification */ "message received" = "Nachricht empfangen"; +/* No comment provided by engineer. */ +"Message reception" = "Nachrichtenempfang"; + +/* No comment provided by engineer. */ +"Message servers" = "Nachrichten-Server"; + /* No comment provided by engineer. */ "Message source remains private." = "Die Nachrichtenquelle bleibt privat."; +/* No comment provided by engineer. */ +"Message status" = "Nachrichten-Status"; + +/* copied message info */ +"Message status: %@" = "Nachrichten-Status: %@"; + /* No comment provided by engineer. */ "Message text" = "Nachrichtentext"; @@ -2493,6 +2842,12 @@ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "Die Nachrichten von %@ werden angezeigt!"; +/* No comment provided by engineer. */ +"Messages received" = "Empfangene Nachrichten"; + +/* No comment provided by engineer. */ +"Messages sent" = "Gesendete Nachrichten"; + /* No comment provided by engineer. */ "Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Nachrichten, Dateien und Anrufe sind durch **Ende-zu-Ende-Verschlüsselung** mit Perfect Forward Secrecy, Ablehnung und Einbruchs-Wiederherstellung geschützt."; @@ -2568,19 +2923,19 @@ /* item status description */ "Most likely this connection is deleted." = "Wahrscheinlich ist diese Verbindung gelöscht worden."; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Dieser Kontakt hat sehr wahrscheinlich die Verbindung mit Ihnen gelöscht."; - /* No comment provided by engineer. */ "Multiple chat profiles" = "Mehrere Chat-Profile"; /* No comment provided by engineer. */ +"mute" = "Stummschalten"; + +/* swipe action */ "Mute" = "Stummschalten"; /* No comment provided by engineer. */ "Muted when inactive!" = "Bei Inaktivität stummgeschaltet!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "Name"; /* No comment provided by engineer. */ @@ -2589,6 +2944,9 @@ /* No comment provided by engineer. */ "Network connection" = "Netzwerkverbindung"; +/* snd error text */ +"Network issues - message expired after many attempts to send it." = "Netzwerk-Fehler - die Nachricht ist nach vielen Sende-Versuchen abgelaufen."; + /* No comment provided by engineer. */ "Network management" = "Netzwerk-Verwaltung"; @@ -2604,6 +2962,9 @@ /* No comment provided by engineer. */ "New chat" = "Neuer Chat"; +/* No comment provided by engineer. */ +"New chat experience 🎉" = "Neue Chat-Erfahrung 🎉"; + /* notification */ "New contact request" = "Neue Kontaktanfrage"; @@ -2622,6 +2983,9 @@ /* No comment provided by engineer. */ "New in %@" = "Neu in %@"; +/* No comment provided by engineer. */ +"New media options" = "Neue Medien-Optionen"; + /* No comment provided by engineer. */ "New member role" = "Neue Mitgliedsrolle"; @@ -2658,6 +3022,9 @@ /* No comment provided by engineer. */ "No device token!" = "Kein Geräte-Token!"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Bisher keine direkte Verbindung. Nachricht wird von einem Admin weitergeleitet."; + /* No comment provided by engineer. */ "no e2e encryption" = "Keine E2E-Verschlüsselung"; @@ -2670,6 +3037,9 @@ /* No comment provided by engineer. */ "No history" = "Kein Nachrichtenverlauf"; +/* No comment provided by engineer. */ +"No info, try to reload" = "Keine Information - es wird versucht neu zu laden"; + /* No comment provided by engineer. */ "No network connection" = "Keine Netzwerkverbindung"; @@ -2685,6 +3055,9 @@ /* No comment provided by engineer. */ "Not compatible!" = "Nicht kompatibel!"; +/* No comment provided by engineer. */ +"Nothing selected" = "Nichts ausgewählt"; + /* No comment provided by engineer. */ "Notifications" = "Benachrichtigungen"; @@ -2702,7 +3075,7 @@ time to disappear */ "off" = "Aus"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Aus"; /* feature offered item */ @@ -2730,10 +3103,10 @@ "One-time invitation link" = "Einmal-Einladungslink"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "Für die Verbindung werden Onion-Hosts benötigt. Dies erfordert die Aktivierung eines VPNs."; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Für diese Verbindung werden Onion-Hosts benötigt.\nDies erfordert die Aktivierung eines VPNs."; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "Onion-Hosts werden verwendet, sobald sie verfügbar sind. Dies erfordert die Aktivierung eines VPNs."; +"Onion hosts will be used when available.\nRequires compatible VPN." = "Wenn Onion-Hosts verfügbar sind, werden sie verwendet.\nDies erfordert die Aktivierung eines VPNs."; /* No comment provided by engineer. */ "Onion hosts will not be used." = "Onion-Hosts werden nicht verwendet."; @@ -2741,6 +3114,9 @@ /* No comment provided by engineer. */ "Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Nur die Endgeräte speichern die Benutzerprofile, Kontakte, Gruppen und Nachrichten, welche über eine **2-Schichten Ende-zu-Ende-Verschlüsselung** gesendet werden."; +/* No comment provided by engineer. */ +"Only delete conversation" = "Nur die Unterhaltung löschen"; + /* No comment provided by engineer. */ "Only group owners can change group preferences." = "Gruppen-Präferenzen können nur von Gruppen-Eigentümern geändert werden."; @@ -2795,6 +3171,9 @@ /* authentication reason */ "Open migration to another device" = "Migration auf ein anderes Gerät öffnen"; +/* No comment provided by engineer. */ +"Open server settings" = "Server-Einstellungen öffnen"; + /* No comment provided by engineer. */ "Open Settings" = "Geräte-Einstellungen öffnen"; @@ -2819,9 +3198,18 @@ /* No comment provided by engineer. */ "Or show this code" = "Oder diesen QR-Code anzeigen"; +/* No comment provided by engineer. */ +"other" = "andere"; + /* No comment provided by engineer. */ "Other" = "Andere"; +/* No comment provided by engineer. */ +"Other %@ servers" = "Andere %@ Server"; + +/* No comment provided by engineer. */ +"other errors" = "Andere Fehler"; + /* member role */ "owner" = "Eigentümer"; @@ -2864,6 +3252,9 @@ /* No comment provided by engineer. */ "peer-to-peer" = "Peer-to-Peer"; +/* No comment provided by engineer. */ +"Pending" = "Ausstehend"; + /* No comment provided by engineer. */ "People can connect to you only via the links you share." = "Verbindungen mit Kontakten sind nur über Links möglich, die Sie oder Ihre Kontakte untereinander teilen."; @@ -2882,9 +3273,18 @@ /* No comment provided by engineer. */ "PING interval" = "PING-Intervall"; +/* No comment provided by engineer. */ +"Play from the chat list." = "Direkt aus der Chat-Liste abspielen."; + +/* No comment provided by engineer. */ +"Please ask your contact to enable calls." = "Bitten Sie Ihren Kontakt darum, Anrufe zu aktivieren."; + /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "Bitten Sie Ihren Kontakt darum, das Senden von Sprachnachrichten zu aktivieren."; +/* No comment provided by engineer. */ +"Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.\nPlease share any other issues with the developers." = "Bitte überprüfen Sie, ob sich das Mobiltelefon und die Desktop-App im gleichen lokalen Netzwerk befinden, und die Desktop-Firewall die Verbindung erlaubt.\nBitte teilen Sie weitere mögliche Probleme den Entwicklern mit."; + /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "Überprüfen Sie bitte, ob Sie den richtigen Link genutzt haben oder bitten Sie Ihren Kontakt nochmal darum, Ihnen einen Link zuzusenden."; @@ -2942,6 +3342,9 @@ /* No comment provided by engineer. */ "Preview" = "Vorschau"; +/* No comment provided by engineer. */ +"Previously connected servers" = "Bisher verbundene Server"; + /* No comment provided by engineer. */ "Privacy & security" = "Datenschutz & Sicherheit"; @@ -2951,9 +3354,21 @@ /* No comment provided by engineer. */ "Private filenames" = "Neutrale Dateinamen"; +/* No comment provided by engineer. */ +"Private message routing" = "Privates Nachrichten-Routing"; + +/* No comment provided by engineer. */ +"Private message routing 🚀" = "Privates Nachrichten-Routing 🚀"; + /* name of notes to self */ "Private notes" = "Private Notizen"; +/* No comment provided by engineer. */ +"Private routing" = "Privates Routing"; + +/* No comment provided by engineer. */ +"Private routing error" = "Fehler beim privaten Routing"; + /* No comment provided by engineer. */ "Profile and server connections" = "Profil und Serververbindungen"; @@ -2972,6 +3387,9 @@ /* No comment provided by engineer. */ "Profile password" = "Passwort für Profil"; +/* No comment provided by engineer. */ +"Profile theme" = "Profil-Design"; + /* No comment provided by engineer. */ "Profile update will be sent to your contacts." = "Profil-Aktualisierung wird an Ihre Kontakte gesendet."; @@ -3005,15 +3423,27 @@ /* No comment provided by engineer. */ "Protect app screen" = "App-Bildschirm schützen"; +/* No comment provided by engineer. */ +"Protect IP address" = "IP-Adresse schützen"; + /* No comment provided by engineer. */ "Protect your chat profiles with a password!" = "Ihre Chat-Profile mit einem Passwort schützen!"; +/* No comment provided by engineer. */ +"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Schützen Sie Ihre IP-Adresse vor den Nachrichten-Relais, die Ihre Kontakte ausgewählt haben.\nAktivieren Sie es in den *Netzwerk & Server* Einstellungen."; + /* No comment provided by engineer. */ "Protocol timeout" = "Protokollzeitüberschreitung"; /* No comment provided by engineer. */ "Protocol timeout per KB" = "Protokollzeitüberschreitung pro kB"; +/* No comment provided by engineer. */ +"Proxied" = "Proxied"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Proxy-Server"; + /* No comment provided by engineer. */ "Push notifications" = "Push-Benachrichtigungen"; @@ -3029,10 +3459,13 @@ /* No comment provided by engineer. */ "Rate the app" = "Bewerten Sie die App"; +/* No comment provided by engineer. */ +"Reachable chat toolbar" = "Erreichbare Chat-Symbolleiste"; + /* chat item menu */ "React…" = "Reagiere…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "Gelesen"; /* No comment provided by engineer. */ @@ -3056,6 +3489,9 @@ /* No comment provided by engineer. */ "Receipts are disabled" = "Bestätigungen sind deaktiviert"; +/* No comment provided by engineer. */ +"Receive errors" = "Fehler beim Empfang"; + /* No comment provided by engineer. */ "received answer…" = "Antwort erhalten…"; @@ -3075,10 +3511,16 @@ "Received message" = "Empfangene Nachricht"; /* No comment provided by engineer. */ -"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Die Empfängeradresse wird auf einen anderen Server geändert. Der Adresswechsel wird abgeschlossen, wenn der Absender wieder online ist."; +"Received messages" = "Empfangene Nachrichten"; /* No comment provided by engineer. */ -"Receiving concurrency" = "Gleichzeitiger Empfang"; +"Received reply" = "Empfangene Antwort"; + +/* No comment provided by engineer. */ +"Received total" = "Summe aller empfangenen Nachrichten"; + +/* No comment provided by engineer. */ +"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Die Empfängeradresse wird auf einen anderen Server geändert. Der Adresswechsel wird abgeschlossen, wenn der Absender wieder online ist."; /* No comment provided by engineer. */ "Receiving file will be stopped." = "Der Empfang der Datei wird beendet."; @@ -3095,9 +3537,24 @@ /* No comment provided by engineer. */ "Recipients see updates as you type them." = "Die Empfänger sehen Nachrichtenaktualisierungen, während Sie sie eingeben."; +/* No comment provided by engineer. */ +"Reconnect" = "Neu verbinden"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Alle verbundenen Server werden neu verbunden, um die Zustellung der Nachricht zu erzwingen. Dies verursacht zusätzlichen Datenverkehr."; +/* No comment provided by engineer. */ +"Reconnect all servers" = "Alle Server neu verbinden"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "Alle Server neu verbinden?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "Um die Auslieferung von Nachrichten zu erzwingen, wird der Server neu verbunden. Dafür wird weiterer Datenverkehr benötigt."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "Server neu verbinden?"; + /* No comment provided by engineer. */ "Reconnect servers?" = "Die Server neu verbinden?"; @@ -3110,7 +3567,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Reduzierter Batterieverbrauch"; -/* reject incoming call via notification */ +/* reject incoming call via notification + swipe action */ "Reject" = "Ablehnen"; /* No comment provided by engineer. */ @@ -3131,6 +3589,9 @@ /* No comment provided by engineer. */ "Remove" = "Entfernen"; +/* No comment provided by engineer. */ +"Remove image" = "Bild entfernen"; + /* No comment provided by engineer. */ "Remove member" = "Mitglied entfernen"; @@ -3188,12 +3649,27 @@ /* No comment provided by engineer. */ "Reset" = "Zurücksetzen"; +/* No comment provided by engineer. */ +"Reset all hints" = "Alle Hinweise zurücksetzen"; + +/* No comment provided by engineer. */ +"Reset all statistics" = "Alle Statistiken zurücksetzen"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "Alle Statistiken zurücksetzen?"; + /* No comment provided by engineer. */ "Reset colors" = "Farben zurücksetzen"; +/* No comment provided by engineer. */ +"Reset to app theme" = "Auf das App-Design zurücksetzen"; + /* No comment provided by engineer. */ "Reset to defaults" = "Auf Voreinstellungen zurücksetzen"; +/* No comment provided by engineer. */ +"Reset to user theme" = "Auf das Benutzer-spezifische Design zurücksetzen"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Um ein neues Chat-Profil zu erstellen, starten Sie die App neu"; @@ -3218,9 +3694,6 @@ /* chat item action */ "Reveal" = "Aufdecken"; -/* No comment provided by engineer. */ -"Revert" = "Zurückkehren"; - /* No comment provided by engineer. */ "Revoke" = "Widerrufen"; @@ -3236,6 +3709,9 @@ /* No comment provided by engineer. */ "Run chat" = "Chat starten"; +/* No comment provided by engineer. */ +"Safely receive files" = "Dateien sicher empfangen"; + /* No comment provided by engineer. */ "Safer groups" = "Sicherere Gruppen"; @@ -3251,6 +3727,9 @@ /* No comment provided by engineer. */ "Save and notify group members" = "Speichern und Gruppenmitglieder benachrichtigen"; +/* No comment provided by engineer. */ +"Save and reconnect" = "Speichern und neu verbinden"; + /* No comment provided by engineer. */ "Save and update group profile" = "Gruppen-Profil sichern und aktualisieren"; @@ -3305,6 +3784,12 @@ /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "Gespeicherte WebRTC ICE-Server werden entfernt"; +/* No comment provided by engineer. */ +"Scale" = "Skalieren"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Link scannen / einfügen"; + /* No comment provided by engineer. */ "Scan code" = "Code scannen"; @@ -3320,6 +3805,9 @@ /* No comment provided by engineer. */ "Scan server QR code" = "Scannen Sie den QR-Code des Servers"; +/* No comment provided by engineer. */ +"search" = "Suchen"; + /* No comment provided by engineer. */ "Search" = "Suche"; @@ -3332,6 +3820,9 @@ /* network option */ "sec" = "sek"; +/* No comment provided by engineer. */ +"Secondary" = "Zweite Farbe"; + /* time unit */ "seconds" = "Sekunden"; @@ -3341,6 +3832,9 @@ /* server test step */ "Secure queue" = "Sichere Warteschlange"; +/* No comment provided by engineer. */ +"Secured" = "Abgesichert"; + /* No comment provided by engineer. */ "Security assessment" = "Sicherheits-Gutachten"; @@ -3350,9 +3844,15 @@ /* chat item text */ "security code changed" = "Sicherheitscode wurde geändert"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "Auswählen"; +/* No comment provided by engineer. */ +"Selected %lld" = "%lld ausgewählt"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Diese Nachricht ist wegen der gewählten Chat-Einstellungen nicht erlaubt."; + /* No comment provided by engineer. */ "Self-destruct" = "Selbstzerstörung"; @@ -3377,21 +3877,30 @@ /* No comment provided by engineer. */ "send direct message" = "Direktnachricht senden"; -/* No comment provided by engineer. */ -"Send direct message" = "Direktnachricht senden"; - /* No comment provided by engineer. */ "Send direct message to connect" = "Eine Direktnachricht zum Verbinden senden"; /* No comment provided by engineer. */ "Send disappearing message" = "Verschwindende Nachricht senden"; +/* No comment provided by engineer. */ +"Send errors" = "Fehler beim Senden"; + /* No comment provided by engineer. */ "Send link previews" = "Link-Vorschau senden"; /* No comment provided by engineer. */ "Send live message" = "Live Nachricht senden"; +/* No comment provided by engineer. */ +"Send message to enable calls." = "Nachricht senden, um Anrufe zu aktivieren."; + +/* No comment provided by engineer. */ +"Send messages directly when IP address is protected and your or destination server does not support private routing." = "Nachrichten werden direkt versendet, wenn die IP-Adresse geschützt ist, und Ihr oder der Zielserver kein privates Routing unterstützt."; + +/* No comment provided by engineer. */ +"Send messages directly when your or destination server does not support private routing." = "Nachrichten werden direkt versendet, wenn Ihr oder der Zielserver kein privates Routing unterstützt."; + /* No comment provided by engineer. */ "Send notifications" = "Benachrichtigungen senden"; @@ -3446,15 +3955,42 @@ /* copied message info */ "Sent at: %@" = "Gesendet um: %@"; +/* No comment provided by engineer. */ +"Sent directly" = "Direkt gesendet"; + /* notification */ "Sent file event" = "Datei-Ereignis wurde gesendet"; /* message info title */ "Sent message" = "Gesendete Nachricht"; +/* No comment provided by engineer. */ +"Sent messages" = "Gesendete Nachrichten"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "Gesendete Nachrichten werden nach der eingestellten Zeit gelöscht."; +/* No comment provided by engineer. */ +"Sent reply" = "Gesendete Antwort"; + +/* No comment provided by engineer. */ +"Sent total" = "Summe aller gesendeten Nachrichten"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Über einen Proxy gesendet"; + +/* No comment provided by engineer. */ +"Server address" = "Server-Adresse"; + +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "Die Server-Adresse ist nicht mit den Netzwerkeinstellungen kompatibel: %@."; + +/* srv error text. */ +"Server address is incompatible with network settings." = "Die Server-Adresse ist nicht mit den Netzwerkeinstellungen kompatibel."; + +/* queue info */ +"server queue info: %@\n\nlast received msg: %@" = "Server-Warteschlangen-Information: %1$@\n\nZuletzt empfangene Nachricht: %2$@"; + /* server test error */ "Server requires authorization to create queues, check password" = "Um Warteschlangen zu erzeugen benötigt der Server eine Authentifizierung. Bitte überprüfen Sie das Passwort"; @@ -3464,9 +4000,24 @@ /* No comment provided by engineer. */ "Server test failed!" = "Server Test ist fehlgeschlagen!"; +/* No comment provided by engineer. */ +"Server type" = "Server-Typ"; + +/* srv error text */ +"Server version is incompatible with network settings." = "Die Server-Version ist nicht mit den Netzwerkeinstellungen kompatibel."; + +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "Die Server-Version ist nicht mit Ihrer App kompatibel: %@."; + /* No comment provided by engineer. */ "Servers" = "Server"; +/* No comment provided by engineer. */ +"Servers info" = "Server-Informationen"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "Die Serverstatistiken werden zurückgesetzt. Dies kann nicht rückgängig gemacht werden!"; + /* No comment provided by engineer. */ "Session code" = "Sitzungscode"; @@ -3476,6 +4027,9 @@ /* No comment provided by engineer. */ "Set contact name…" = "Kontaktname festlegen…"; +/* No comment provided by engineer. */ +"Set default theme" = "Default-Design einstellen"; + /* No comment provided by engineer. */ "Set group preferences" = "Gruppen-Präferenzen einstellen"; @@ -3521,15 +4075,24 @@ /* No comment provided by engineer. */ "Share address with contacts?" = "Die Adresse mit Kontakten teilen?"; +/* No comment provided by engineer. */ +"Share from other apps." = "Aus anderen Apps heraus teilen."; + /* No comment provided by engineer. */ "Share link" = "Link teilen"; /* No comment provided by engineer. */ "Share this 1-time invite link" = "Teilen Sie diesen Einmal-Einladungslink"; +/* No comment provided by engineer. */ +"Share to SimpleX" = "Mit SimpleX teilen"; + /* No comment provided by engineer. */ "Share with contacts" = "Mit Kontakten teilen"; +/* No comment provided by engineer. */ +"Show → on messages sent via private routing." = "Bei Nachrichten, die über privates Routing versendet wurden, → anzeigen."; + /* No comment provided by engineer. */ "Show calls in phone history" = "Anrufliste anzeigen"; @@ -3539,6 +4102,12 @@ /* No comment provided by engineer. */ "Show last messages" = "Letzte Nachrichten anzeigen"; +/* No comment provided by engineer. */ +"Show message status" = "Nachrichtenstatus anzeigen"; + +/* No comment provided by engineer. */ +"Show percentage" = "Prozentualen Anteil anzeigen"; + /* No comment provided by engineer. */ "Show preview" = "Vorschau anzeigen"; @@ -3548,6 +4117,9 @@ /* No comment provided by engineer. */ "Show:" = "Anzeigen:"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "SimpleX-Adresse"; @@ -3593,6 +4165,9 @@ /* No comment provided by engineer. */ "Simplified incognito mode" = "Vereinfachter Inkognito-Modus"; +/* No comment provided by engineer. */ +"Size" = "Größe"; + /* No comment provided by engineer. */ "Skip" = "Überspringen"; @@ -3603,11 +4178,20 @@ "Small groups (max 20)" = "Kleine Gruppen (max. 20)"; /* No comment provided by engineer. */ -"SMP servers" = "SMP-Server"; +"SMP server" = "SMP-Server"; + +/* blur media */ +"Soft" = "Weich"; + +/* No comment provided by engineer. */ +"Some file(s) were not exported:" = "Einzelne Datei(en) wurde(n) nicht exportiert:"; /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "Während des Imports sind einige nicht schwerwiegende Fehler aufgetreten - in der Chat-Konsole finden Sie weitere Einzelheiten."; +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import:" = "Während des Imports traten ein paar nicht schwerwiegende Fehler auf:"; + /* notification title */ "Somebody" = "Jemand"; @@ -3626,9 +4210,15 @@ /* No comment provided by engineer. */ "Start migration" = "Starten Sie die Migration"; +/* No comment provided by engineer. */ +"Starting from %@." = "Beginnend mit %@."; + /* No comment provided by engineer. */ "starting…" = "Verbindung wird gestartet…"; +/* No comment provided by engineer. */ +"Statistics" = "Statistiken"; + /* No comment provided by engineer. */ "Stop" = "Beenden"; @@ -3668,9 +4258,21 @@ /* No comment provided by engineer. */ "strike" = "durchstreichen"; +/* blur media */ +"Strong" = "Hart"; + /* No comment provided by engineer. */ "Submit" = "Bestätigen"; +/* No comment provided by engineer. */ +"Subscribed" = "Abonniert"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Fehler beim Abonnieren"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Nicht beachtete Abonnements"; + /* No comment provided by engineer. */ "Support SimpleX Chat" = "Unterstützung von SimpleX Chat"; @@ -3705,7 +4307,7 @@ "Tap to scan" = "Zum Scannen tippen"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Zum Starten eines neuen Chats tippen"; +"TCP connection" = "TCP-Verbindung"; /* No comment provided by engineer. */ "TCP connection timeout" = "Timeout der TCP-Verbindung"; @@ -3719,6 +4321,9 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* No comment provided by engineer. */ +"Temporary file error" = "Temporärer Datei-Fehler"; + /* server test failure */ "Test failed at step %@." = "Der Test ist beim Schritt %@ fehlgeschlagen."; @@ -3746,6 +4351,9 @@ /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Wenn sie Nachrichten oder Kontaktanfragen empfangen, kann Sie die App benachrichtigen - Um dies zu aktivieren, öffnen Sie bitte die Einstellungen."; +/* No comment provided by engineer. */ +"The app will ask to confirm downloads from unknown file servers (except .onion)." = "Die App wird eine Bestätigung bei Downloads von unbekannten Datei-Servern anfordern (außer bei .onion)."; + /* No comment provided by engineer. */ "The attempt to change database passphrase was not completed." = "Die Änderung des Datenbank-Passworts konnte nicht abgeschlossen werden."; @@ -3776,6 +4384,12 @@ /* No comment provided by engineer. */ "The message will be marked as moderated for all members." = "Diese Nachricht wird für alle Mitglieder als moderiert gekennzeichnet."; +/* No comment provided by engineer. */ +"The messages will be deleted for all members." = "Die Nachrichten werden für alle Mitglieder gelöscht werden."; + +/* No comment provided by engineer. */ +"The messages will be marked as moderated for all members." = "Die Nachrichten werden für alle Mitglieder als moderiert gekennzeichnet werden."; + /* No comment provided by engineer. */ "The next generation of private messaging" = "Die nächste Generation von privatem Messaging"; @@ -3798,7 +4412,7 @@ "The text you pasted is not a SimpleX link." = "Der von Ihnen eingefügte Text ist kein SimpleX-Link."; /* No comment provided by engineer. */ -"Theme" = "Design"; +"Themes" = "Design"; /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Diese Einstellungen betreffen Ihr aktuelles Profil **%@**."; @@ -3807,10 +4421,10 @@ "They can be overridden in contact and group settings." = "Sie können in den Kontakteinstellungen überschrieben werden."; /* No comment provided by engineer. */ -"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Diese Aktion kann nicht rückgängig gemacht werden! Alle empfangenen und gesendeten Dateien und Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten."; +"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Diese Aktion kann nicht rückgängig gemacht werden! Es werden alle empfangenen und gesendeten Dateien und Medien gelöscht. Bilder mit niedriger Auflösung bleiben erhalten."; /* No comment provided by engineer. */ -"This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Diese Aktion kann nicht rückgängig gemacht werden! Alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, werden gelöscht. Dieser Vorgang kann mehrere Minuten dauern."; +"This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Diese Aktion kann nicht rückgängig gemacht werden! Es werden alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, gelöscht. Dieser Vorgang kann mehrere Minuten dauern."; /* No comment provided by engineer. */ "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "Diese Aktion kann nicht rückgängig gemacht werden! Ihr Profil und Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren."; @@ -3842,9 +4456,15 @@ /* No comment provided by engineer. */ "This is your own SimpleX address!" = "Das ist Ihre eigene SimpleX-Adresse!"; +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "Dieser Link wurde schon mit einem anderen Mobiltelefon genutzt. Bitte erstellen sie einen neuen Link in der Desktop-App."; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Diese Einstellung gilt für Nachrichten in Ihrem aktuellen Chat-Profil **%@**."; +/* No comment provided by engineer. */ +"Title" = "Bezeichnung"; + /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Um Fragen zu stellen und aktuelle Informationen zu erhalten:"; @@ -3866,6 +4486,9 @@ /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Um Ihre Informationen zu schützen, schalten Sie die SimpleX-Sperre ein.\nSie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funktion aktiviert wird."; +/* No comment provided by engineer. */ +"To protect your IP address, private routing uses your SMP servers to deliver messages." = "Zum Schutz Ihrer IP-Adresse, wird für die Nachrichten-Auslieferung privates Routing über Ihre konfigurierten SMP-Server genutzt."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Bitte erlauben Sie die Nutzung des Mikrofons, um Sprachnachrichten aufnehmen zu können."; @@ -3878,12 +4501,24 @@ /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Um die Ende-zu-Ende-Verschlüsselung mit Ihrem Kontakt zu überprüfen, müssen Sie den Sicherheitscode in Ihren Apps vergleichen oder scannen."; +/* No comment provided by engineer. */ +"Toggle chat list:" = "Chat-Liste umschalten:"; + /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Inkognito beim Verbinden einschalten."; +/* No comment provided by engineer. */ +"Toolbar opacity" = "Deckkraft der Symbolleiste"; + +/* No comment provided by engineer. */ +"Total" = "Summe aller Abonnements"; + /* No comment provided by engineer. */ "Transport isolation" = "Transport-Isolation"; +/* No comment provided by engineer. */ +"Transport sessions" = "Transport-Sitzungen"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Beim Versuch die Verbindung mit dem Server aufzunehmen, der für den Empfang von Nachrichten mit diesem Kontakt genutzt wird, ist ein Fehler aufgetreten (Fehler: %@)."; @@ -3920,13 +4555,10 @@ /* rcv group event chat item */ "unblocked %@" = "%@ wurde freigegeben"; -/* item status description */ -"Unexpected error: %@" = "Unerwarteter Fehler: %@"; - /* No comment provided by engineer. */ "Unexpected migration state" = "Unerwarteter Migrationsstatus"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "Fav. entf."; /* No comment provided by engineer. */ @@ -3953,6 +4585,12 @@ /* No comment provided by engineer. */ "Unknown error" = "Unbekannter Fehler"; +/* No comment provided by engineer. */ +"unknown servers" = "Unbekannte Relais"; + +/* No comment provided by engineer. */ +"Unknown servers!" = "Unbekannte Server!"; + /* No comment provided by engineer. */ "unknown status" = "unbekannter Gruppenmitglieds-Status"; @@ -3975,9 +4613,15 @@ "Unlock app" = "App entsperren"; /* No comment provided by engineer. */ +"unmute" = "Stummschaltung aufheben"; + +/* swipe action */ "Unmute" = "Stummschaltung aufheben"; /* No comment provided by engineer. */ +"unprotected" = "Ungeschützt"; + +/* swipe action */ "Unread" = "Ungelesen"; /* No comment provided by engineer. */ @@ -3986,9 +4630,6 @@ /* No comment provided by engineer. */ "Update" = "Aktualisieren"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = "Einstellung für .onion-Hosts aktualisieren?"; - /* No comment provided by engineer. */ "Update database passphrase" = "Datenbank-Passwort aktualisieren"; @@ -3996,7 +4637,7 @@ "Update network settings?" = "Netzwerkeinstellungen aktualisieren?"; /* No comment provided by engineer. */ -"Update transport isolation mode?" = "Transport-Isolations-Modus aktualisieren?"; +"Update settings?" = "Einstellungen aktualisieren?"; /* rcv group event chat item */ "updated group profile" = "Aktualisiertes Gruppenprofil"; @@ -4008,10 +4649,10 @@ "Updating settings will re-connect the client to all servers." = "Die Aktualisierung der Einstellungen wird den Client wieder mit allen Servern verbinden."; /* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "Die Aktualisierung dieser Einstellung wird den Client wieder mit allen Servern verbinden."; +"Upgrade and open chat" = "Aktualisieren und den Chat öffnen"; /* No comment provided by engineer. */ -"Upgrade and open chat" = "Aktualisieren und den Chat öffnen"; +"Upload errors" = "Fehler beim Hochladen"; /* No comment provided by engineer. */ "Upload failed" = "Hochladen fehlgeschlagen"; @@ -4019,6 +4660,12 @@ /* server test step */ "Upload file" = "Datei hochladen"; +/* No comment provided by engineer. */ +"Uploaded" = "Hochgeladen"; + +/* No comment provided by engineer. */ +"Uploaded files" = "Hochgeladene Dateien"; + /* No comment provided by engineer. */ "Uploading archive" = "Archiv wird hochgeladen"; @@ -4029,7 +4676,7 @@ "Use chat" = "Verwenden Sie Chat"; /* No comment provided by engineer. */ -"Use current profile" = "Das aktuelle Profil nutzen"; +"Use current profile" = "Aktuelles Profil nutzen"; /* No comment provided by engineer. */ "Use for new connections" = "Für neue Verbindungen nutzen"; @@ -4041,11 +4688,17 @@ "Use iOS call interface" = "iOS Anrufschnittstelle nutzen"; /* No comment provided by engineer. */ -"Use new incognito profile" = "Ein neues Inkognito-Profil nutzen"; +"Use new incognito profile" = "Neues Inkognito-Profil nutzen"; /* No comment provided by engineer. */ "Use only local notifications?" = "Nur lokale Benachrichtigungen nutzen?"; +/* No comment provided by engineer. */ +"Use private routing with unknown servers when IP address is not protected." = "Sie nutzen privates Routing mit unbekannten Servern, wenn Ihre IP-Adresse nicht geschützt ist."; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers." = "Sie nutzen privates Routing mit unbekannten Servern."; + /* No comment provided by engineer. */ "Use server" = "Server nutzen"; @@ -4055,11 +4708,14 @@ /* No comment provided by engineer. */ "Use the app while in the call." = "Die App kann während eines Anrufs genutzt werden."; +/* No comment provided by engineer. */ +"Use the app with one hand." = "Die App mit einer Hand nutzen."; + /* No comment provided by engineer. */ "User profile" = "Benutzerprofil"; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "Für die Nutzung von .onion-Hosts sind kompatible VPN-Anbieter erforderlich."; +"User selection" = "Benutzer-Auswahl"; /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "Verwendung von SimpleX-Chat-Servern."; @@ -4109,6 +4765,9 @@ /* No comment provided by engineer. */ "Via secure quantum resistant protocol." = "Über ein sicheres quantenbeständiges Protokoll."; +/* No comment provided by engineer. */ +"video" = "Video"; + /* No comment provided by engineer. */ "Video call" = "Videoanruf"; @@ -4166,6 +4825,12 @@ /* No comment provided by engineer. */ "Waiting for video" = "Auf das Video warten"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "Wallpaper-Akzent"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Wallpaper-Hintergrund"; + /* No comment provided by engineer. */ "wants to connect to you!" = "möchte sich mit Ihnen verbinden!"; @@ -4199,6 +4864,9 @@ /* No comment provided by engineer. */ "When connecting audio and video calls." = "Bei der Verbindung über Audio- und Video-Anrufe."; +/* No comment provided by engineer. */ +"when IP hidden" = "Wenn die IP-Adresse versteckt ist"; + /* No comment provided by engineer. */ "When people request to connect, you can accept or reject it." = "Wenn Personen eine Verbindung anfordern, können Sie diese annehmen oder ablehnen."; @@ -4223,14 +4891,26 @@ /* No comment provided by engineer. */ "With reduced battery usage." = "Mit reduziertem Akkuverbrauch."; +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to file servers." = "Ohne Tor- oder VPN-Nutzung wird Ihre IP-Adresse für Datei-Server sichtbar sein."; + +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Ohne Tor- oder VPN-Nutzung wird Ihre IP-Adresse für diese XFTP-Relais sichtbar sein: %@."; + /* No comment provided by engineer. */ "Wrong database passphrase" = "Falsches Datenbank-Passwort"; +/* snd error text */ +"Wrong key or unknown connection - most likely this connection is deleted." = "Falscher Schlüssel oder unbekannte Verbindung - höchstwahrscheinlich ist diese Verbindung gelöscht worden."; + +/* file error text */ +"Wrong key or unknown file chunk address - most likely file is deleted." = "Falscher Schlüssel oder unbekannte Daten-Paketadresse der Datei - höchstwahrscheinlich wurde die Datei gelöscht."; + /* No comment provided by engineer. */ "Wrong passphrase!" = "Falsches Passwort!"; /* No comment provided by engineer. */ -"XFTP servers" = "XFTP-Server"; +"XFTP server" = "XFTP-Server"; /* pref value */ "yes" = "Ja"; @@ -4286,6 +4966,9 @@ /* No comment provided by engineer. */ "You are invited to group" = "Sie sind zu der Gruppe eingeladen"; +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "Sie sind nicht mit diesen Servern verbunden. Zur Auslieferung von Nachrichten an diese Server wird privates Routing genutzt."; + /* No comment provided by engineer. */ "you are observer" = "Sie sind Beobachter"; @@ -4295,6 +4978,9 @@ /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "Sie können Anrufe ohne Geräte- und App-Authentifizierung vom Sperrbildschirm aus annehmen."; +/* No comment provided by engineer. */ +"You can change it in Appearance settings." = "Kann von Ihnen in den Erscheinungsbild-Einstellungen geändert werden."; + /* No comment provided by engineer. */ "You can create it later" = "Sie können dies später erstellen"; @@ -4314,7 +5000,10 @@ "You can make it visible to your SimpleX contacts via Settings." = "Sie können sie über Einstellungen für Ihre SimpleX-Kontakte sichtbar machen."; /* notification body */ -"You can now send messages to %@" = "Sie können nun Nachrichten an %@ versenden"; +"You can now chat with %@" = "Sie können nun Nachrichten an %@ versenden"; + +/* No comment provided by engineer. */ +"You can send messages to %@ from Archived contacts." = "Sie können aus den archivierten Kontakten heraus Nachrichten an %@ versenden."; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Über die Geräte-Einstellungen können Sie die Benachrichtigungsvorschau im Sperrbildschirm erlauben."; @@ -4331,6 +5020,9 @@ /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "Sie können den Chat über die App-Einstellungen / Datenbank oder durch Neustart der App starten"; +/* No comment provided by engineer. */ +"You can still view conversation with %@ in the list of chats." = "Sie können in der Chatliste weiterhin die Unterhaltung mit %@ einsehen."; + /* No comment provided by engineer. */ "You can turn on SimpleX Lock via Settings." = "Sie können die SimpleX-Sperre über die Einstellungen aktivieren."; @@ -4367,9 +5059,6 @@ /* No comment provided by engineer. */ "You have already requested connection!\nRepeat connection request?" = "Sie haben bereits ein Verbindungsanfrage beantragt!\nVerbindungsanfrage wiederholen?"; -/* No comment provided by engineer. */ -"You have no chats" = "Sie haben keine Chats"; - /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "Sie müssen das Passwort jedes Mal eingeben, wenn die App startet. Es wird nicht auf dem Gerät gespeichert."; @@ -4385,9 +5074,18 @@ /* snd group event chat item */ "you left" = "Sie haben verlassen"; +/* No comment provided by engineer. */ +"You may migrate the exported database." = "Sie können die exportierte Datenbank migrieren."; + +/* No comment provided by engineer. */ +"You may save the exported archive." = "Sie können das exportierte Archiv speichern."; + /* No comment provided by engineer. */ "You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." = "Sie dürfen die neueste Version Ihrer Chat-Datenbank NUR auf einem Gerät verwenden, andernfalls erhalten Sie möglicherweise keine Nachrichten mehr von einigen Ihrer Kontakte."; +/* No comment provided by engineer. */ +"You need to allow your contact to call to be able to call them." = "Sie müssen Ihrem Kontakt Anrufe zu Ihnen erlauben, bevor Sie ihn selbst anrufen können."; + /* No comment provided by engineer. */ "You need to allow your contact to send voice messages to be able to send them." = "Sie müssen Ihrem Kontakt das Senden von Sprachnachrichten erlauben, um diese senden zu können."; @@ -4460,9 +5158,6 @@ /* No comment provided by engineer. */ "Your chat profiles" = "Ihre Chat-Profile"; -/* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Damit die Verbindung hergestellt werden kann, muss Ihr Kontakt online sein.\nSie können diese Verbindung abbrechen und den Kontakt entfernen (und es später nochmals mit einem neuen Link versuchen)."; - /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Ihr Kontakt hat eine Datei gesendet, die größer ist als die derzeit unterstützte maximale Größe (%@)."; diff --git a/apps/ios/de.lproj/SimpleX--iOS--InfoPlist.strings b/apps/ios/de.lproj/SimpleX--iOS--InfoPlist.strings index 5fe2ef2d09..0dee85ad95 100644 --- a/apps/ios/de.lproj/SimpleX--iOS--InfoPlist.strings +++ b/apps/ios/de.lproj/SimpleX--iOS--InfoPlist.strings @@ -2,7 +2,7 @@ "CFBundleName" = "SimpleX"; /* Privacy - Camera Usage Description */ -"NSCameraUsageDescription" = "SimpleX benötigt Zugriff auf die Kamera, um QR Codes für die Verbindung mit anderen Nutzern zu scannen und Videoanrufe durchzuführen."; +"NSCameraUsageDescription" = "SimpleX benötigt Zugriff auf die Kamera, um QR Codes für die Verbindung mit anderen Benutzern zu scannen und Videoanrufe durchzuführen."; /* Privacy - Face ID Usage Description */ "NSFaceIDUsageDescription" = "Face ID wird von SimpleX für die lokale Authentifizierung genutzt"; diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index 8a61d6c438..3dce4e4474 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -154,9 +154,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ está verificado"; -/* No comment provided by engineer. */ -"%@ servers" = "Servidores %@"; - /* No comment provided by engineer. */ "%@ uploaded" = "%@ subido"; @@ -338,10 +335,11 @@ "above, then choose:" = "y después elige:"; /* No comment provided by engineer. */ -"Accent color" = "Color"; +"Accent" = "Color"; /* accept contact request via notification - accept incoming call via notification */ + accept incoming call via notification + swipe action */ "Accept" = "Aceptar"; /* No comment provided by engineer. */ @@ -350,12 +348,22 @@ /* notification body */ "Accept contact request from %@?" = "¿Aceptar solicitud de contacto de %@?"; -/* accept contact request via notification */ +/* accept contact request via notification + swipe action */ "Accept incognito" = "Aceptar incógnito"; /* call status */ "accepted call" = "llamada aceptada"; +/* No comment provided by engineer. */ +"Acknowledged" = "Confirmaciones"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Errores de confirmación"; + +/* No comment provided by engineer. */ +"Active connections" = "Conexiones activas"; + /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Añade la dirección a tu perfil para que tus contactos puedan compartirla con otros. La actualización del perfil se enviará a tus contactos."; @@ -369,7 +377,7 @@ "Add profile" = "Añadir perfil"; /* No comment provided by engineer. */ -"Add server…" = "Añadir servidor…"; +"Add server" = "Añadir servidor"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Añadir servidores mediante el escaneo de códigos QR."; @@ -380,6 +388,15 @@ /* No comment provided by engineer. */ "Add welcome message" = "Añadir mensaje de bienvenida"; +/* No comment provided by engineer. */ +"Additional accent" = "Acento adicional"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Color adicional 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "Secundario adicional"; + /* No comment provided by engineer. */ "Address" = "Dirección"; @@ -401,6 +418,9 @@ /* No comment provided by engineer. */ "Advanced network settings" = "Configuración avanzada de red"; +/* No comment provided by engineer. */ +"Advanced settings" = "Configuración avanzada"; + /* chat item text */ "agreeing encryption for %@…" = "acordando cifrado para %@…"; @@ -416,6 +436,9 @@ /* No comment provided by engineer. */ "All data is erased when it is entered." = "Al introducirlo todos los datos son eliminados."; +/* No comment provided by engineer. */ +"All data is private to your device." = "Todos los datos son privados y están en tu dispositivo."; + /* No comment provided by engineer. */ "All group members will remain connected." = "Todos los miembros del grupo permanecerán conectados."; @@ -431,6 +454,9 @@ /* No comment provided by engineer. */ "All new messages from %@ will be hidden!" = "¡Los mensajes nuevos de %@ estarán ocultos!"; +/* No comment provided by engineer. */ +"All profiles" = "Todos los perfiles"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Todos tus contactos permanecerán conectados."; @@ -446,9 +472,15 @@ /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "Se permiten las llamadas pero sólo si tu contacto también las permite."; +/* No comment provided by engineer. */ +"Allow calls?" = "¿Permitir llamadas?"; + /* No comment provided by engineer. */ "Allow disappearing messages only if your contact allows it to you." = "Se permiten los mensajes temporales pero sólo si tu contacto también los permite para tí."; +/* No comment provided by engineer. */ +"Allow downgrade" = "Permitir versión anterior"; + /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Se permite la eliminación irreversible de mensajes pero sólo si tu contacto también la permite para tí. (24 horas)"; @@ -464,6 +496,9 @@ /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Permites el envío de mensajes temporales."; +/* No comment provided by engineer. */ +"Allow sharing" = "Permitir compartir"; + /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Se permite la eliminación irreversible de mensajes. (24 horas)"; @@ -471,7 +506,7 @@ "Allow to send files and media." = "Se permite enviar archivos y multimedia."; /* No comment provided by engineer. */ -"Allow to send SimpleX links." = "Permitir enviar enlaces SimpleX."; +"Allow to send SimpleX links." = "Se permite enviar enlaces SimpleX."; /* No comment provided by engineer. */ "Allow to send voice messages." = "Permites enviar mensajes de voz."; @@ -509,6 +544,9 @@ /* pref value */ "always" = "siempre"; +/* No comment provided by engineer. */ +"Always use private routing." = "Usar siempre enrutamiento privado."; + /* No comment provided by engineer. */ "Always use relay" = "Usar siempre retransmisor"; @@ -551,15 +589,27 @@ /* No comment provided by engineer. */ "Apply" = "Aplicar"; +/* No comment provided by engineer. */ +"Apply to" = "Aplicar a"; + /* No comment provided by engineer. */ "Archive and upload" = "Archivar y subir"; +/* No comment provided by engineer. */ +"Archive contacts to chat later." = "Archiva contactos para charlar más tarde."; + +/* No comment provided by engineer. */ +"Archived contacts" = "Contactos archivados"; + /* No comment provided by engineer. */ "Archiving database" = "Archivando base de datos"; /* No comment provided by engineer. */ "Attach" = "Adjuntar"; +/* No comment provided by engineer. */ +"attempts" = "intentos"; + /* No comment provided by engineer. */ "Audio & video calls" = "Llamadas y videollamadas"; @@ -602,6 +652,9 @@ /* No comment provided by engineer. */ "Back" = "Volver"; +/* No comment provided by engineer. */ +"Background" = "Fondo"; + /* No comment provided by engineer. */ "Bad desktop address" = "Dirección ordenador incorrecta"; @@ -623,6 +676,12 @@ /* No comment provided by engineer. */ "Better messages" = "Mensajes mejorados"; +/* No comment provided by engineer. */ +"Better networking" = "Uso de red mejorado"; + +/* No comment provided by engineer. */ +"Black" = "Negro"; + /* No comment provided by engineer. */ "Block" = "Bloquear"; @@ -653,6 +712,12 @@ /* No comment provided by engineer. */ "Blocked by admin" = "Bloqueado por administrador"; +/* No comment provided by engineer. */ +"Blur for better privacy." = "Difumina para mayor privacidad."; + +/* No comment provided by engineer. */ +"Blur media" = "Difuminar multimedia"; + /* No comment provided by engineer. */ "bold" = "negrita"; @@ -675,7 +740,10 @@ "Bulgarian, Finnish, Thai and Ukrainian - thanks to the users and [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!" = "Búlgaro, Finlandés, Tailandés y Ucraniano - gracias a los usuarios y [Weblate](https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat)!"; /* No comment provided by engineer. */ -"By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Mediante perfil (por defecto) o [por conexión](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; +"By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Mediante perfil (predeterminado) o [por conexión](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; + +/* No comment provided by engineer. */ +"call" = "llamada"; /* No comment provided by engineer. */ "Call already ended!" = "¡La llamada ha terminado!"; @@ -692,15 +760,27 @@ /* No comment provided by engineer. */ "Calls" = "Llamadas"; +/* No comment provided by engineer. */ +"Calls prohibited!" = "¡Llamadas no permitidas!"; + /* No comment provided by engineer. */ "Camera not available" = "Cámara no disponible"; +/* No comment provided by engineer. */ +"Can't call contact" = "No se puede llamar al contacto"; + +/* No comment provided by engineer. */ +"Can't call member" = "No se puede llamar al miembro"; + /* No comment provided by engineer. */ "Can't invite contact!" = "¡No se puede invitar el contacto!"; /* No comment provided by engineer. */ "Can't invite contacts!" = "¡No se pueden invitar contactos!"; +/* No comment provided by engineer. */ +"Can't message member" = "No se pueden enviar mensajes al miembro"; + /* No comment provided by engineer. */ "Cancel" = "Cancelar"; @@ -713,9 +793,15 @@ /* No comment provided by engineer. */ "Cannot access keychain to save database password" = "Keychain inaccesible para guardar la contraseña de la base de datos"; +/* No comment provided by engineer. */ +"Cannot forward message" = "No se puede reenviar el mensaje"; + /* No comment provided by engineer. */ "Cannot receive file" = "No se puede recibir el archivo"; +/* snd error text */ +"Capacity exceeded - recipient did not receive previously sent messages." = "Capacidad excedida - el destinatario no ha recibido los mensajes previos."; + /* No comment provided by engineer. */ "Cellular" = "Móvil"; @@ -768,26 +854,35 @@ /* No comment provided by engineer. */ "Chat archive" = "Archivo del chat"; +/* No comment provided by engineer. */ +"Chat colors" = "Colores del chat"; + /* No comment provided by engineer. */ "Chat console" = "Consola de Chat"; /* No comment provided by engineer. */ -"Chat database" = "Base de datos del chat"; +"Chat database" = "Base de datos de SimpleX"; /* No comment provided by engineer. */ "Chat database deleted" = "Base de datos eliminada"; +/* No comment provided by engineer. */ +"Chat database exported" = "Base de datos exportada"; + /* No comment provided by engineer. */ "Chat database imported" = "Base de datos importada"; /* No comment provided by engineer. */ -"Chat is running" = "Chat está en ejecución"; +"Chat is running" = "SimpleX está en ejecución"; /* No comment provided by engineer. */ -"Chat is stopped" = "Chat está parado"; +"Chat is stopped" = "SimpleX está parado"; /* No comment provided by engineer. */ -"Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." = "Chat parado. Si has usado esta base de datos en otro dispositivo, debes transferirla de vuelta antes de iniciar Chat."; +"Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." = "SimpleX está parado. Si has usado esta base de datos en otro dispositivo, debes transferirla de vuelta antes de iniciar SimpleX."; + +/* No comment provided by engineer. */ +"Chat list" = "Lista de chats"; /* No comment provided by engineer. */ "Chat migrated!" = "¡Chat migrado!"; @@ -795,6 +890,9 @@ /* No comment provided by engineer. */ "Chat preferences" = "Preferencias de Chat"; +/* No comment provided by engineer. */ +"Chat theme" = "Tema de chat"; + /* No comment provided by engineer. */ "Chats" = "Chats"; @@ -814,6 +912,15 @@ "Choose from library" = "Elige de la biblioteca"; /* No comment provided by engineer. */ +"Chunks deleted" = "Bloques eliminados"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Bloques descargados"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Bloques subidos"; + +/* swipe action */ "Clear" = "Vaciar"; /* No comment provided by engineer. */ @@ -829,10 +936,13 @@ "Clear verification" = "Eliminar verificación"; /* No comment provided by engineer. */ -"colored" = "coloreado"; +"Color chats with the new themes." = "Colorea los chats con los nuevos temas."; /* No comment provided by engineer. */ -"Colors" = "Colores"; +"Color mode" = "Modo de color"; + +/* No comment provided by engineer. */ +"colored" = "coloreado"; /* server test step */ "Compare file" = "Comparar archivo"; @@ -843,15 +953,27 @@ /* No comment provided by engineer. */ "complete" = "completado"; +/* No comment provided by engineer. */ +"Completed" = "Completadas"; + /* No comment provided by engineer. */ "Configure ICE servers" = "Configure servidores ICE"; +/* No comment provided by engineer. */ +"Configured %@ servers" = "%@ servidores configurados"; + /* No comment provided by engineer. */ "Confirm" = "Confirmar"; +/* No comment provided by engineer. */ +"Confirm contact deletion?" = "¿Confirmas la eliminación del contacto?"; + /* No comment provided by engineer. */ "Confirm database upgrades" = "Confirmar actualizaciones de la bases de datos"; +/* No comment provided by engineer. */ +"Confirm files from unknown servers." = "Confirma archivos de servidores desconocidos."; + /* No comment provided by engineer. */ "Confirm network settings" = "Confirmar configuración de red"; @@ -865,7 +987,7 @@ "Confirm password" = "Confirmar contraseña"; /* No comment provided by engineer. */ -"Confirm that you remember database passphrase to migrate it." = "Confirma que recuerdas la frase de contraseña de la base de datos para migrarla."; +"Confirm that you remember database passphrase to migrate it." = "Para migrar confirma que recuerdas la frase de contraseña de la base de datos."; /* No comment provided by engineer. */ "Confirm upload" = "Confirmar subida"; @@ -885,6 +1007,9 @@ /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "contacta con los desarrolladores de SimpleX Chat."; +/* No comment provided by engineer. */ +"Connect to your friends faster." = "Conecta más rápido con tus amigos."; + /* No comment provided by engineer. */ "Connect to yourself?" = "¿Conectarte a tí mismo?"; @@ -909,18 +1034,27 @@ /* No comment provided by engineer. */ "connected" = "conectado"; +/* No comment provided by engineer. */ +"Connected" = "Conectadas"; + /* No comment provided by engineer. */ "Connected desktop" = "Ordenador conectado"; /* rcv group event chat item */ "connected directly" = "conectado directamente"; +/* No comment provided by engineer. */ +"Connected servers" = "Servidores conectados"; + /* No comment provided by engineer. */ "Connected to desktop" = "Conectado con ordenador"; /* No comment provided by engineer. */ "connecting" = "conectando"; +/* No comment provided by engineer. */ +"Connecting" = "Conectando"; + /* No comment provided by engineer. */ "connecting (accepted)" = "conectando (aceptado)"; @@ -942,6 +1076,9 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "Conectando con el servidor... (error: %@)"; +/* No comment provided by engineer. */ +"Connecting to contact, please wait or check later!" = "Conectando con el contacto, por favor espera o revisa más tarde."; + /* No comment provided by engineer. */ "Connecting to desktop" = "Conectando con ordenador"; @@ -951,15 +1088,21 @@ /* No comment provided by engineer. */ "Connection" = "Conexión"; +/* No comment provided by engineer. */ +"Connection and servers status." = "Estado de tu conexión y servidores."; + /* No comment provided by engineer. */ "Connection error" = "Error conexión"; /* No comment provided by engineer. */ -"Connection error (AUTH)" = "Error conexión (Autenticación)"; +"Connection error (AUTH)" = "Error de conexión (Autenticación)"; /* chat list item title (it should not be shown */ "connection established" = "conexión establecida"; +/* No comment provided by engineer. */ +"Connection notifications" = "Notificaciones de conexión"; + /* No comment provided by engineer. */ "Connection request sent!" = "¡Solicitud de conexión enviada!"; @@ -967,11 +1110,17 @@ "Connection terminated" = "Conexión finalizada"; /* No comment provided by engineer. */ -"Connection timeout" = "Tiempo de conexión expirado"; +"Connection timeout" = "Tiempo de conexión agotado"; + +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "La conexión con el escritorio (desktop) se ha parado"; /* connection information */ "connection:%@" = "conexión: % @"; +/* No comment provided by engineer. */ +"Connections" = "Conexiones"; + /* profile update event chat item */ "contact %@ changed to %@" = "el contacto %1$@ ha cambiado a %2$@"; @@ -981,6 +1130,9 @@ /* No comment provided by engineer. */ "Contact already exists" = "El contácto ya existe"; +/* No comment provided by engineer. */ +"Contact deleted!" = "¡Contacto eliminado!"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "el contacto dispone de cifrado de extremo a extremo"; @@ -994,7 +1146,7 @@ "Contact is connected" = "El contacto está en línea"; /* No comment provided by engineer. */ -"Contact is not connected yet!" = "¡El contacto aun no se ha conectado!"; +"Contact is deleted." = "El contacto está eliminado."; /* No comment provided by engineer. */ "Contact name" = "Contacto"; @@ -1002,6 +1154,9 @@ /* No comment provided by engineer. */ "Contact preferences" = "Preferencias de contacto"; +/* No comment provided by engineer. */ +"Contact will be deleted - this cannot be undone!" = "El contacto será eliminado. ¡No podrá deshacerse!"; + /* No comment provided by engineer. */ "Contacts" = "Contactos"; @@ -1011,9 +1166,15 @@ /* No comment provided by engineer. */ "Continue" = "Continuar"; -/* chat item action */ +/* No comment provided by engineer. */ +"Conversation deleted!" = "¡Conversación eliminada!"; + +/* No comment provided by engineer. */ "Copy" = "Copiar"; +/* No comment provided by engineer. */ +"Copy error" = "Copiar error"; + /* No comment provided by engineer. */ "Core version: v%@" = "Versión Core: v%@"; @@ -1059,6 +1220,9 @@ /* No comment provided by engineer. */ "Create your profile" = "Crea tu perfil"; +/* No comment provided by engineer. */ +"Created" = "Creadas"; + /* No comment provided by engineer. */ "Created at" = "Creado"; @@ -1083,6 +1247,9 @@ /* No comment provided by engineer. */ "Current passphrase…" = "Contraseña actual…"; +/* No comment provided by engineer. */ +"Current profile" = "Perfil actual"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "El tamaño máximo de archivo admitido es %@."; @@ -1092,9 +1259,15 @@ /* No comment provided by engineer. */ "Custom time" = "Tiempo personalizado"; +/* No comment provided by engineer. */ +"Customize theme" = "Personalizar tema"; + /* No comment provided by engineer. */ "Dark" = "Oscuro"; +/* No comment provided by engineer. */ +"Dark mode colors" = "Colores en modo oscuro"; + /* No comment provided by engineer. */ "Database downgrade" = "Degradación de la base de datos"; @@ -1155,26 +1328,36 @@ /* time unit */ "days" = "días"; +/* No comment provided by engineer. */ +"Debug delivery" = "Informe debug"; + /* No comment provided by engineer. */ "Decentralized" = "Descentralizada"; /* message decrypt error item */ "Decryption error" = "Error descifrado"; +/* No comment provided by engineer. */ +"decryption errors" = "errores de descifrado"; + /* pref value */ -"default (%@)" = "por defecto (%@)"; +"default (%@)" = "predeterminado (%@)"; /* No comment provided by engineer. */ -"default (no)" = "por defecto (no)"; +"default (no)" = "predeterminado (no)"; /* No comment provided by engineer. */ -"default (yes)" = "por defecto (sí)"; +"default (yes)" = "predeterminado (sí)"; -/* chat item action */ +/* chat item action + swipe action */ "Delete" = "Eliminar"; /* No comment provided by engineer. */ -"Delete %lld messages?" = "¿Elimina %lld mensajes?"; +"Delete %lld messages of members?" = "¿Eliminar %lld mensajes de miembros?"; + +/* No comment provided by engineer. */ +"Delete %lld messages?" = "¿Eliminar %lld mensajes?"; /* No comment provided by engineer. */ "Delete address" = "Eliminar dirección"; @@ -1210,10 +1393,7 @@ "Delete contact" = "Eliminar contacto"; /* No comment provided by engineer. */ -"Delete Contact" = "Eliminar contacto"; - -/* No comment provided by engineer. */ -"Delete contact?\nThis cannot be undone!" = "¿Eliminar contacto?\n¡No podrá deshacerse!"; +"Delete contact?" = "¿Eliminar contacto?"; /* No comment provided by engineer. */ "Delete database" = "Eliminar base de datos"; @@ -1269,9 +1449,6 @@ /* No comment provided by engineer. */ "Delete old database?" = "¿Eliminar base de datos antigua?"; -/* No comment provided by engineer. */ -"Delete pending connection" = "Eliminar conexión pendiente"; - /* No comment provided by engineer. */ "Delete pending connection?" = "¿Eliminar conexión pendiente?"; @@ -1281,12 +1458,21 @@ /* server test step */ "Delete queue" = "Eliminar cola"; +/* No comment provided by engineer. */ +"Delete up to 20 messages at once." = "Elimina hasta 20 mensajes a la vez."; + /* No comment provided by engineer. */ "Delete user profile?" = "¿Eliminar perfil de usuario?"; +/* No comment provided by engineer. */ +"Delete without notification" = "Elimina sin notificar"; + /* deleted chat item */ "deleted" = "eliminado"; +/* No comment provided by engineer. */ +"Deleted" = "Eliminadas"; + /* No comment provided by engineer. */ "Deleted at" = "Eliminado"; @@ -1299,6 +1485,9 @@ /* rcv group event chat item */ "deleted group" = "grupo eliminado"; +/* No comment provided by engineer. */ +"Deletion errors" = "Errores de eliminación"; + /* No comment provided by engineer. */ "Delivery" = "Entrega"; @@ -1320,9 +1509,27 @@ /* No comment provided by engineer. */ "Desktop devices" = "Ordenadores"; +/* No comment provided by engineer. */ +"Destination server address of %@ is incompatible with forwarding server %@ settings." = "La dirección del servidor de destino de %@ es incompatible con la configuración del servidor de reenvío %@."; + +/* snd error text */ +"Destination server error: %@" = "Error del servidor de destino: %@"; + +/* No comment provided by engineer. */ +"Destination server version of %@ is incompatible with forwarding server %@." = "La versión del servidor de destino de %@ es incompatible con el servidor de reenvío %@."; + +/* No comment provided by engineer. */ +"Detailed statistics" = "Estadísticas detalladas"; + +/* No comment provided by engineer. */ +"Details" = "Detalles"; + /* No comment provided by engineer. */ "Develop" = "Desarrollo"; +/* No comment provided by engineer. */ +"Developer options" = "Opciones desarrollador"; + /* No comment provided by engineer. */ "Developer tools" = "Herramientas desarrollo"; @@ -1362,6 +1569,9 @@ /* No comment provided by engineer. */ "disabled" = "desactivado"; +/* No comment provided by engineer. */ +"Disabled" = "Desactivado"; + /* No comment provided by engineer. */ "Disappearing message" = "Mensaje temporal"; @@ -1396,7 +1606,13 @@ "Do it later" = "Hacer más tarde"; /* No comment provided by engineer. */ -"Do not send history to new members." = "No enviar historial a miembros nuevos."; +"Do not send history to new members." = "No se envía el historial a los miembros nuevos."; + +/* No comment provided by engineer. */ +"Do NOT send messages directly, even if your or destination server does not support private routing." = "NO enviar mensajes directamente incluso si tu servidor o el de destino no soportan enrutamiento privado."; + +/* No comment provided by engineer. */ +"Do NOT use private routing." = "NO usar enrutamiento privado."; /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "NO uses SimpleX para llamadas de emergencia."; @@ -1416,12 +1632,21 @@ /* chat item action */ "Download" = "Descargar"; +/* No comment provided by engineer. */ +"Download errors" = "Errores en la descarga"; + /* No comment provided by engineer. */ "Download failed" = "Descarga fallida"; /* server test step */ "Download file" = "Descargar archivo"; +/* No comment provided by engineer. */ +"Downloaded" = "Descargado"; + +/* No comment provided by engineer. */ +"Downloaded files" = "Archivos descargados"; + /* No comment provided by engineer. */ "Downloading archive" = "Descargando archivo"; @@ -1434,6 +1659,9 @@ /* integrity error chat item */ "duplicate message" = "mensaje duplicado"; +/* No comment provided by engineer. */ +"duplicates" = "duplicados"; + /* No comment provided by engineer. */ "Duration" = "Duración"; @@ -1492,7 +1720,10 @@ "enabled" = "activado"; /* No comment provided by engineer. */ -"Enabled for" = "Activar para"; +"Enabled" = "Activado"; + +/* No comment provided by engineer. */ +"Enabled for" = "Activado para"; /* enabled status */ "enabled for contact" = "activado para el contacto"; @@ -1632,6 +1863,9 @@ /* No comment provided by engineer. */ "Error changing setting" = "Error cambiando configuración"; +/* No comment provided by engineer. */ +"Error connecting to forwarding server %@. Please try later." = "Error al conectar con el servidor de reenvío %@. Por favor, inténtalo más tarde."; + /* No comment provided by engineer. */ "Error creating address" = "Error al crear dirección"; @@ -1662,9 +1896,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "Error al eliminar conexión"; -/* No comment provided by engineer. */ -"Error deleting contact" = "Error al eliminar contacto"; - /* No comment provided by engineer. */ "Error deleting database" = "Error al eliminar base de datos"; @@ -1692,6 +1923,9 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "Error al exportar base de datos"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Error al exportar tema: %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "Error al importar base de datos"; @@ -1707,9 +1941,18 @@ /* No comment provided by engineer. */ "Error receiving file" = "Error al recibir archivo"; +/* No comment provided by engineer. */ +"Error reconnecting server" = "Error al reconectar con el servidor"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "Error al reconectar con los servidores"; + /* No comment provided by engineer. */ "Error removing member" = "Error al eliminar miembro"; +/* No comment provided by engineer. */ +"Error resetting statistics" = "Error al restablecer las estadísticas"; + /* No comment provided by engineer. */ "Error saving %@ servers" = "Error al guardar servidores %@"; @@ -1750,7 +1993,7 @@ "Error starting chat" = "Error al iniciar Chat"; /* No comment provided by engineer. */ -"Error stopping chat" = "Error al parar Chat"; +"Error stopping chat" = "Error al parar SimpleX"; /* No comment provided by engineer. */ "Error switching profile!" = "¡Error al cambiar perfil!"; @@ -1779,7 +2022,8 @@ /* No comment provided by engineer. */ "Error: " = "Error: "; -/* No comment provided by engineer. */ +/* file error text + snd error text */ "Error: %@" = "Error: %@"; /* No comment provided by engineer. */ @@ -1788,6 +2032,9 @@ /* No comment provided by engineer. */ "Error: URL is invalid" = "Error: la URL no es válida"; +/* No comment provided by engineer. */ +"Errors" = "Errores"; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Incluso si está desactivado para la conversación."; @@ -1800,12 +2047,18 @@ /* chat item action */ "Expand" = "Expandir"; +/* No comment provided by engineer. */ +"expired" = "expirados"; + /* No comment provided by engineer. */ "Export database" = "Exportar base de datos"; /* No comment provided by engineer. */ "Export error:" = "Error al exportar:"; +/* No comment provided by engineer. */ +"Export theme" = "Exportar tema"; + /* No comment provided by engineer. */ "Exported database archive." = "Archivo de base de datos exportado."; @@ -1824,9 +2077,24 @@ /* No comment provided by engineer. */ "Faster joining and more reliable messages." = "Mensajería más segura y conexión más rápida."; -/* No comment provided by engineer. */ +/* swipe action */ "Favorite" = "Favoritos"; +/* No comment provided by engineer. */ +"File error" = "Error de archivo"; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "Archivo no encontrado, probablemente haya sido borrado o cancelado."; + +/* file error text */ +"File server error: %@" = "Error del servidor de archivos: %@"; + +/* No comment provided by engineer. */ +"File status" = "Estado del archivo"; + +/* copied message info */ +"File status: %@" = "Estado del archivo: %@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "El archivo será eliminado de los servidores."; @@ -1834,11 +2102,14 @@ "File will be received when your contact completes uploading it." = "El archivo se recibirá cuando el contacto termine de subirlo."; /* No comment provided by engineer. */ -"File will be received when your contact is online, please wait or check later!" = "El archivo se recibirá cuando el contacto esté en línea, ¡por favor espera o compruébalo más tarde!"; +"File will be received when your contact is online, please wait or check later!" = "El archivo se recibirá cuando el contacto esté en línea, ¡por favor espera o revisa más tarde!"; /* No comment provided by engineer. */ "File: %@" = "Archivo: %@"; +/* No comment provided by engineer. */ +"Files" = "Archivos"; + /* No comment provided by engineer. */ "Files & media" = "Archivos y multimedia"; @@ -1905,6 +2176,21 @@ /* No comment provided by engineer. */ "Forwarded from" = "Reenviado por"; +/* No comment provided by engineer. */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "El servidor de reenvío %@ no ha podido conectarse al servidor de destino %@. Por favor, intentalo más tarde."; + +/* No comment provided by engineer. */ +"Forwarding server address is incompatible with network settings: %@." = "La dirección del servidor de reenvío es incompatible con la configuración de red: %@."; + +/* No comment provided by engineer. */ +"Forwarding server version is incompatible with network settings: %@." = "La versión del servidor de reenvío es incompatible con la configuración de red: %@."; + +/* snd error text */ +"Forwarding server: %@\nDestination server error: %@" = "Servidor de reenvío: %1$@\nError del servidor de destino: %2$@"; + +/* snd error text */ +"Forwarding server: %@\nError: %@" = "Servidor de reenvío: %1$@\nError: %2$@"; + /* No comment provided by engineer. */ "Found desktop" = "Ordenador encontrado"; @@ -1921,7 +2207,7 @@ "Full name:" = "Nombre completo:"; /* No comment provided by engineer. */ -"Fully decentralized – visible only to members." = "Completamente descentralizado: sólo visible a los miembros."; +"Fully decentralized – visible only to members." = "Completamente descentralizado y sólo visible para los miembros."; /* No comment provided by engineer. */ "Fully re-implemented - work in background!" = "Completamente reimplementado: ¡funciona en segundo plano!"; @@ -1932,6 +2218,12 @@ /* No comment provided by engineer. */ "GIFs and stickers" = "GIFs y stickers"; +/* message preview */ +"Good afternoon!" = "¡Buenas tardes!"; + +/* message preview */ +"Good morning!" = "¡Buenos días!"; + /* No comment provided by engineer. */ "Group" = "Grupo"; @@ -2071,7 +2363,7 @@ "ICE servers (one per line)" = "Servidores ICE (uno por línea)"; /* No comment provided by engineer. */ -"If you can't meet in person, show QR code in a video call, or share the link." = "Si no puedes reunirte en persona, muestra el código QR por videollamada, o comparte el enlace."; +"If you can't meet in person, show QR code in a video call, or share the link." = "Si no puedes reunirte en persona, muestra el código QR por videollamada o comparte el enlace."; /* No comment provided by engineer. */ "If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "¡Si introduces este código al abrir la aplicación, todos los datos de la misma se eliminarán de forma irreversible!"; @@ -2089,7 +2381,7 @@ "Image will be received when your contact completes uploading it." = "La imagen se recibirá cuando el contacto termine de subirla."; /* No comment provided by engineer. */ -"Image will be received when your contact is online, please wait or check later!" = "La imagen se recibirá cuando el contacto esté en línea, ¡por favor espera o compruébalo más tarde!"; +"Image will be received when your contact is online, please wait or check later!" = "La imagen se recibirá cuando el contacto esté en línea, ¡por favor espera o revisa más tarde!"; /* No comment provided by engineer. */ "Immediately" = "Inmediatamente"; @@ -2109,6 +2401,9 @@ /* No comment provided by engineer. */ "Import failed" = "Error de importación"; +/* No comment provided by engineer. */ +"Import theme" = "Importar tema"; + /* No comment provided by engineer. */ "Importing archive" = "Importando archivo"; @@ -2122,7 +2417,7 @@ "Improved server configuration" = "Configuración del servidor mejorada"; /* No comment provided by engineer. */ -"In order to continue, chat should be stopped." = "Para continuar, Chat debe estar parado."; +"In order to continue, chat should be stopped." = "Para continuar, SimpleX debe estar parado."; /* No comment provided by engineer. */ "In reply to" = "En respuesta a"; @@ -2130,6 +2425,9 @@ /* No comment provided by engineer. */ "In-call sounds" = "Sonido de llamada"; +/* No comment provided by engineer. */ +"inactive" = "inactivo"; + /* No comment provided by engineer. */ "Incognito" = "Incógnito"; @@ -2193,6 +2491,9 @@ /* No comment provided by engineer. */ "Interface" = "Interfaz"; +/* No comment provided by engineer. */ +"Interface colors" = "Colores del interfaz"; + /* invalid chat data */ "invalid chat" = "chat no válido"; @@ -2235,6 +2536,9 @@ /* group name */ "invitation to group %@" = "invitación al grupo %@"; +/* No comment provided by engineer. */ +"invite" = "Invitar"; + /* No comment provided by engineer. */ "Invite friends" = "Invitar amigos"; @@ -2280,6 +2584,9 @@ /* No comment provided by engineer. */ "It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Esto puede suceder cuando:\n1. Los mensajes caducan tras 2 días en el cliente saliente o tras 30 días en el servidor.\n2. El descifrado ha fallado porque tu o tu contacto estáis usando una copia de seguridad antigua de la base de datos.\n3. La conexión ha sido comprometida."; +/* No comment provided by engineer. */ +"It protects your IP address and connections." = "Protege tu dirección IP y tus conexiones."; + /* No comment provided by engineer. */ "It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Parece que ya estás conectado mediante este enlace. Si no es así ha habido un error (%@)."; @@ -2292,7 +2599,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Interfáz en japonés"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Unirte"; /* No comment provided by engineer. */ @@ -2322,6 +2629,9 @@ /* No comment provided by engineer. */ "Keep" = "Guardar"; +/* No comment provided by engineer. */ +"Keep conversation" = "Conservar conversación"; + /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Mantén la aplicación abierta para usarla desde el ordenador"; @@ -2343,7 +2653,7 @@ /* No comment provided by engineer. */ "Learn more" = "Más información"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "Salir"; /* No comment provided by engineer. */ @@ -2433,6 +2743,12 @@ /* No comment provided by engineer. */ "Max 30 seconds, received instantly." = "Máximo 30 segundos, recibido al instante."; +/* No comment provided by engineer. */ +"Media & file servers" = "Servidores de archivos y multimedia"; + +/* blur media */ +"Medium" = "Medio"; + /* member role */ "member" = "miembro"; @@ -2445,6 +2761,9 @@ /* rcv group event chat item */ "member connected" = "conectado"; +/* item status text */ +"Member inactive" = "Miembro inactivo"; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "El rol del miembro cambiará a \"%@\" y se notificará al grupo."; @@ -2454,15 +2773,33 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "El miembro será expulsado del grupo. ¡No podrá deshacerse!"; +/* No comment provided by engineer. */ +"Menus" = "Menus"; + +/* No comment provided by engineer. */ +"message" = "mensaje"; + /* item status text */ "Message delivery error" = "Error en la entrega del mensaje"; /* No comment provided by engineer. */ "Message delivery receipts!" = "¡Confirmación de entrega de mensajes!"; +/* item status text */ +"Message delivery warning" = "Aviso de entrega de mensaje"; + /* No comment provided by engineer. */ "Message draft" = "Borrador de mensaje"; +/* item status text */ +"Message forwarded" = "Mensaje reenviado"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "El mensaje podría ser entregado más tarde si el miembro vuelve a estar activo."; + +/* No comment provided by engineer. */ +"Message queue info" = "Información cola de mensajes"; + /* chat feature */ "Message reactions" = "Reacciones a mensajes"; @@ -2475,9 +2812,21 @@ /* notification */ "message received" = "mensaje recibido"; +/* No comment provided by engineer. */ +"Message reception" = "Recepción de mensaje"; + +/* No comment provided by engineer. */ +"Message servers" = "Servidores de mensajes"; + /* No comment provided by engineer. */ "Message source remains private." = "El autor del mensaje se mantiene privado."; +/* No comment provided by engineer. */ +"Message status" = "Estado del mensaje"; + +/* copied message info */ +"Message status: %@" = "Estado del mensaje: %@"; + /* No comment provided by engineer. */ "Message text" = "Contacto y texto"; @@ -2493,6 +2842,12 @@ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "¡Los mensajes de %@ serán mostrados!"; +/* No comment provided by engineer. */ +"Messages received" = "Mensajes recibidos"; + +/* No comment provided by engineer. */ +"Messages sent" = "Mensajes enviados"; + /* No comment provided by engineer. */ "Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Los mensajes, archivos y llamadas están protegidos mediante **cifrado de extremo a extremo** con secreto perfecto hacía adelante, repudio y recuperación tras ataque."; @@ -2568,19 +2923,19 @@ /* item status description */ "Most likely this connection is deleted." = "Probablemente la conexión ha sido eliminada."; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Lo más probable es que este contacto haya eliminado la conexión contigo."; - /* No comment provided by engineer. */ "Multiple chat profiles" = "Múltiples perfiles"; /* No comment provided by engineer. */ +"mute" = "silenciar"; + +/* swipe action */ "Mute" = "Silenciar"; /* No comment provided by engineer. */ "Muted when inactive!" = "¡Silenciado cuando está inactivo!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "Nombre"; /* No comment provided by engineer. */ @@ -2589,6 +2944,9 @@ /* No comment provided by engineer. */ "Network connection" = "Conexión de red"; +/* snd error text */ +"Network issues - message expired after many attempts to send it." = "Problema en la red - el mensaje ha expirado tras muchos intentos de envío."; + /* No comment provided by engineer. */ "Network management" = "Gestión de la red"; @@ -2604,6 +2962,9 @@ /* No comment provided by engineer. */ "New chat" = "Nuevo chat"; +/* No comment provided by engineer. */ +"New chat experience 🎉" = "Nueva experiencia de chat 🎉"; + /* notification */ "New contact request" = "Nueva solicitud de contacto"; @@ -2622,6 +2983,9 @@ /* No comment provided by engineer. */ "New in %@" = "Nuevo en %@"; +/* No comment provided by engineer. */ +"New media options" = "Nuevas opciones multimedia"; + /* No comment provided by engineer. */ "New member role" = "Nuevo rol de miembro"; @@ -2658,6 +3022,9 @@ /* No comment provided by engineer. */ "No device token!" = "¡Sin dispositivo token!"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Aún no hay conexión directa con este miembro, el mensaje es reenviado por el administrador."; + /* No comment provided by engineer. */ "no e2e encryption" = "sin cifrar"; @@ -2670,6 +3037,9 @@ /* No comment provided by engineer. */ "No history" = "Sin historial"; +/* No comment provided by engineer. */ +"No info, try to reload" = "No hay información, intenta recargar"; + /* No comment provided by engineer. */ "No network connection" = "Sin conexión de red"; @@ -2685,6 +3055,9 @@ /* No comment provided by engineer. */ "Not compatible!" = "¡No compatible!"; +/* No comment provided by engineer. */ +"Nothing selected" = "Nada seleccionado"; + /* No comment provided by engineer. */ "Notifications" = "Notificaciones"; @@ -2702,7 +3075,7 @@ time to disappear */ "off" = "desactivado"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Desactivado"; /* feature offered item */ @@ -2727,13 +3100,13 @@ "on" = "Activado"; /* No comment provided by engineer. */ -"One-time invitation link" = "Enlace único de invitación de un uso"; +"One-time invitation link" = "Enlace de invitación de un solo uso"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "Se requieren hosts .onion para la conexión. Requiere activación de la VPN."; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Se **requieren** hosts .onion para la conexión.\nRequiere activación de la VPN."; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "Se usarán hosts .onion si están disponibles. Requiere activación de la VPN."; +"Onion hosts will be used when available.\nRequires compatible VPN." = "Se usarán hosts .onion si están disponibles.\nRequiere activación de la VPN."; /* No comment provided by engineer. */ "Onion hosts will not be used." = "No se usarán hosts .onion."; @@ -2741,6 +3114,9 @@ /* No comment provided by engineer. */ "Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Sólo los dispositivos cliente almacenan perfiles de usuario, contactos, grupos y mensajes enviados con **cifrado de extremo a extremo de 2 capas**."; +/* No comment provided by engineer. */ +"Only delete conversation" = "Sólo borrar la conversación"; + /* No comment provided by engineer. */ "Only group owners can change group preferences." = "Sólo los propietarios pueden modificar las preferencias del grupo."; @@ -2795,6 +3171,9 @@ /* authentication reason */ "Open migration to another device" = "Abrir menú migración a otro dispositivo"; +/* No comment provided by engineer. */ +"Open server settings" = "Abrir configuración del servidor"; + /* No comment provided by engineer. */ "Open Settings" = "Abrir Configuración"; @@ -2811,17 +3190,26 @@ "Or paste archive link" = "O pegar enlace del archivo"; /* No comment provided by engineer. */ -"Or scan QR code" = "O escanear código QR"; +"Or scan QR code" = "O escanea el código QR"; /* No comment provided by engineer. */ "Or securely share this file link" = "O comparte de forma segura este enlace al archivo"; /* No comment provided by engineer. */ -"Or show this code" = "O mostrar este código"; +"Or show this code" = "O muestra este código QR"; + +/* No comment provided by engineer. */ +"other" = "otros"; /* No comment provided by engineer. */ "Other" = "Otro"; +/* No comment provided by engineer. */ +"Other %@ servers" = "Otros servidores %@"; + +/* No comment provided by engineer. */ +"other errors" = "otros errores"; + /* member role */ "owner" = "propietario"; @@ -2859,16 +3247,19 @@ "Paste link to connect!" = "Pegar enlace para conectar!"; /* No comment provided by engineer. */ -"Paste the link you received" = "Pegar el enlace recibido"; +"Paste the link you received" = "Pega el enlace recibido"; /* No comment provided by engineer. */ "peer-to-peer" = "p2p"; +/* No comment provided by engineer. */ +"Pending" = "Pendientes"; + /* No comment provided by engineer. */ "People can connect to you only via the links you share." = "Las personas pueden conectarse contigo solo mediante los enlaces que compartes."; /* No comment provided by engineer. */ -"Periodically" = "Periódico"; +"Periodically" = "Periódicamente"; /* message decrypt error item */ "Permanent decryption error" = "Error permanente descifrado"; @@ -2882,9 +3273,18 @@ /* No comment provided by engineer. */ "PING interval" = "Intervalo PING"; +/* No comment provided by engineer. */ +"Play from the chat list." = "Reproduce desde la lista de chats."; + +/* No comment provided by engineer. */ +"Please ask your contact to enable calls." = "Por favor, pide a tu contacto que active las llamadas."; + /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "Solicita que tu contacto habilite el envío de mensajes de voz."; +/* No comment provided by engineer. */ +"Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.\nPlease share any other issues with the developers." = "Comprueba que el móvil y el ordenador están conectados a la misma red local y que el cortafuegos del ordenador permite la conexión.\nPor favor, comparte cualquier otro problema con los desarrolladores."; + /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "Comprueba que has usado el enlace correcto o pide a tu contacto que te envíe otro."; @@ -2943,7 +3343,10 @@ "Preview" = "Vista previa"; /* No comment provided by engineer. */ -"Privacy & security" = "Privacidad y Seguridad"; +"Previously connected servers" = "Servidores conectados previamente"; + +/* No comment provided by engineer. */ +"Privacy & security" = "Seguridad y Privacidad"; /* No comment provided by engineer. */ "Privacy redefined" = "Privacidad redefinida"; @@ -2951,9 +3354,21 @@ /* No comment provided by engineer. */ "Private filenames" = "Nombres de archivos privados"; +/* No comment provided by engineer. */ +"Private message routing" = "Enrutamiento privado de mensajes"; + +/* No comment provided by engineer. */ +"Private message routing 🚀" = "Enrutamiento privado de mensajes 🚀"; + /* name of notes to self */ "Private notes" = "Notas privadas"; +/* No comment provided by engineer. */ +"Private routing" = "Enrutamiento privado"; + +/* No comment provided by engineer. */ +"Private routing error" = "Error de enrutamiento privado"; + /* No comment provided by engineer. */ "Profile and server connections" = "Datos del perfil y conexiones"; @@ -2961,7 +3376,7 @@ "Profile image" = "Imagen del perfil"; /* No comment provided by engineer. */ -"Profile images" = "Imágenes del perfil"; +"Profile images" = "Forma de los perfiles"; /* No comment provided by engineer. */ "Profile name" = "Nombre del perfil"; @@ -2972,6 +3387,9 @@ /* No comment provided by engineer. */ "Profile password" = "Contraseña del perfil"; +/* No comment provided by engineer. */ +"Profile theme" = "Tema del perfil"; + /* No comment provided by engineer. */ "Profile update will be sent to your contacts." = "La actualización del perfil se enviará a tus contactos."; @@ -2997,7 +3415,7 @@ "Prohibit sending files and media." = "No permitir el envío de archivos y multimedia."; /* No comment provided by engineer. */ -"Prohibit sending SimpleX links." = "No permitir el envío de enlaces SimpleX."; +"Prohibit sending SimpleX links." = "No se permite enviar enlaces SimpleX."; /* No comment provided by engineer. */ "Prohibit sending voice messages." = "No se permiten mensajes de voz."; @@ -3005,14 +3423,26 @@ /* No comment provided by engineer. */ "Protect app screen" = "Proteger la pantalla de la aplicación"; +/* No comment provided by engineer. */ +"Protect IP address" = "Proteger dirección IP"; + /* No comment provided by engineer. */ "Protect your chat profiles with a password!" = "¡Protege tus perfiles con contraseña!"; /* No comment provided by engineer. */ -"Protocol timeout" = "Tiempo de espera del protocolo"; +"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Protege tu dirección IP de los servidores de retransmisión elegidos por tus contactos.\nActívalo en ajustes de *Servidores y Redes*."; /* No comment provided by engineer. */ -"Protocol timeout per KB" = "Límite de espera del protocolo por KB"; +"Protocol timeout" = "Timeout protocolo"; + +/* No comment provided by engineer. */ +"Protocol timeout per KB" = "Timeout protocolo por KB"; + +/* No comment provided by engineer. */ +"Proxied" = "Como proxy"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Servidores con proxy"; /* No comment provided by engineer. */ "Push notifications" = "Notificaciones automáticas"; @@ -3029,33 +3459,39 @@ /* No comment provided by engineer. */ "Rate the app" = "Valora la aplicación"; +/* No comment provided by engineer. */ +"Reachable chat toolbar" = "Barra de herramientas accesible"; + /* chat item menu */ "React…" = "Reacciona…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "Leer"; /* No comment provided by engineer. */ -"Read more" = "Saber más"; +"Read more" = "Conoce más"; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Saber más en el [Manual del Usuario](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)." = "Conoce más en el [Manual del Usuario](https://simplex.chat/docs/guide/app-settings.html#your-simplex-contact-address)."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Saber más en [Guía de Usuario](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)." = "Conoce más en la [Guía del Usuario](https://simplex.chat/docs/guide/chat-profiles.html#incognito-mode)."; /* No comment provided by engineer. */ -"Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Saber más en el [Manual del Usuario](https://simplex.chat/docs/guide/readme.html#connect-to-friends)."; +"Read more in [User Guide](https://simplex.chat/docs/guide/readme.html#connect-to-friends)." = "Conoce más en el [Manual del Usuario](https://simplex.chat/docs/guide/readme.html#connect-to-friends)."; /* No comment provided by engineer. */ -"Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "Saber más en nuestro [repositorio GitHub](https://github.com/simplex-chat/simplex-chat#readme)."; +"Read more in our [GitHub repository](https://github.com/simplex-chat/simplex-chat#readme)." = "Conoce más en nuestro [repositorio GitHub](https://github.com/simplex-chat/simplex-chat#readme)."; /* No comment provided by engineer. */ -"Read more in our GitHub repository." = "Saber más en nuestro repositorio GitHub."; +"Read more in our GitHub repository." = "Conoce más en nuestro repositorio GitHub."; /* No comment provided by engineer. */ "Receipts are disabled" = "Las confirmaciones están desactivadas"; +/* No comment provided by engineer. */ +"Receive errors" = "Errores de recepción"; + /* No comment provided by engineer. */ "received answer…" = "respuesta recibida…"; @@ -3075,10 +3511,16 @@ "Received message" = "Mensaje entrante"; /* No comment provided by engineer. */ -"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "La dirección de recepción pasará a otro servidor. El cambio se completará cuando el remitente esté en línea."; +"Received messages" = "Mensajes recibidos"; /* No comment provided by engineer. */ -"Receiving concurrency" = "Concurrencia en la recepción"; +"Received reply" = "Respuesta recibida"; + +/* No comment provided by engineer. */ +"Received total" = "Total recibidos"; + +/* No comment provided by engineer. */ +"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "La dirección de recepción pasará a otro servidor. El cambio se completará cuando el remitente esté en línea."; /* No comment provided by engineer. */ "Receiving file will be stopped." = "Se detendrá la recepción del archivo."; @@ -3095,9 +3537,24 @@ /* No comment provided by engineer. */ "Recipients see updates as you type them." = "Los destinatarios ven actualizarse mientras escribes."; +/* No comment provided by engineer. */ +"Reconnect" = "Reconectar"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Reconectar todos los servidores conectados para forzar la entrega del mensaje. Se usa tráfico adicional."; +/* No comment provided by engineer. */ +"Reconnect all servers" = "Reconectar todos los servidores"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "¿Reconectar todos los servidores?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "Reconectar el servidor para forzar la entrega de mensajes. Usa tráfico adicional."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "¿Reconectar servidor?"; + /* No comment provided by engineer. */ "Reconnect servers?" = "¿Reconectar servidores?"; @@ -3110,7 +3567,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Reducción del uso de batería"; -/* reject incoming call via notification */ +/* reject incoming call via notification + swipe action */ "Reject" = "Rechazar"; /* No comment provided by engineer. */ @@ -3123,7 +3581,7 @@ "rejected call" = "llamada rechazada"; /* No comment provided by engineer. */ -"Relay server is only used if necessary. Another party can observe your IP address." = "El retransmisor sólo se usa en caso de necesidad. Un tercero podría ver tu IP."; +"Relay server is only used if necessary. Another party can observe your IP address." = "El servidor de retransmisión sólo se usa en caso de necesidad. Un tercero podría ver tu IP."; /* No comment provided by engineer. */ "Relay server protects your IP address, but it can observe the duration of the call." = "El servidor de retransmisión protege tu IP pero puede ver la duración de la llamada."; @@ -3131,6 +3589,9 @@ /* No comment provided by engineer. */ "Remove" = "Eliminar"; +/* No comment provided by engineer. */ +"Remove image" = "Eliminar imagen"; + /* No comment provided by engineer. */ "Remove member" = "Expulsar miembro"; @@ -3188,11 +3649,26 @@ /* No comment provided by engineer. */ "Reset" = "Restablecer"; +/* No comment provided by engineer. */ +"Reset all hints" = "Reiniciar todas las pistas"; + +/* No comment provided by engineer. */ +"Reset all statistics" = "Restablecer todas las estadísticas"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "¿Restablecer todas las estadísticas?"; + /* No comment provided by engineer. */ "Reset colors" = "Restablecer colores"; /* No comment provided by engineer. */ -"Reset to defaults" = "Restablecer valores por defecto"; +"Reset to app theme" = "Restablecer al tema de la aplicación"; + +/* No comment provided by engineer. */ +"Reset to defaults" = "Restablecer valores predetarminados"; + +/* No comment provided by engineer. */ +"Reset to user theme" = "Restablecer al tema del usuario"; /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Reinicia la aplicación para crear un perfil nuevo"; @@ -3218,9 +3694,6 @@ /* chat item action */ "Reveal" = "Revelar"; -/* No comment provided by engineer. */ -"Revert" = "Revertir"; - /* No comment provided by engineer. */ "Revoke" = "Revocar"; @@ -3234,7 +3707,10 @@ "Role" = "Rol"; /* No comment provided by engineer. */ -"Run chat" = "Ejecutar chat"; +"Run chat" = "Ejecutar SimpleX"; + +/* No comment provided by engineer. */ +"Safely receive files" = "Recibe archivos de forma segura"; /* No comment provided by engineer. */ "Safer groups" = "Grupos más seguros"; @@ -3251,6 +3727,9 @@ /* No comment provided by engineer. */ "Save and notify group members" = "Guardar y notificar grupo"; +/* No comment provided by engineer. */ +"Save and reconnect" = "Guardar y reconectar"; + /* No comment provided by engineer. */ "Save and update group profile" = "Guardar y actualizar perfil del grupo"; @@ -3305,6 +3784,12 @@ /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "Los servidores WebRTC ICE guardados serán eliminados"; +/* No comment provided by engineer. */ +"Scale" = "Escala"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Escanear / Pegar enlace"; + /* No comment provided by engineer. */ "Scan code" = "Escanear código"; @@ -3320,6 +3805,9 @@ /* No comment provided by engineer. */ "Scan server QR code" = "Escanear código QR del servidor"; +/* No comment provided by engineer. */ +"search" = "buscar"; + /* No comment provided by engineer. */ "Search" = "Buscar"; @@ -3332,6 +3820,9 @@ /* network option */ "sec" = "seg"; +/* No comment provided by engineer. */ +"Secondary" = "Secundario"; + /* time unit */ "seconds" = "segundos"; @@ -3341,6 +3832,9 @@ /* server test step */ "Secure queue" = "Cola segura"; +/* No comment provided by engineer. */ +"Secured" = "Aseguradas"; + /* No comment provided by engineer. */ "Security assessment" = "Evaluación de la seguridad"; @@ -3350,9 +3844,15 @@ /* chat item text */ "security code changed" = "código de seguridad cambiado"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "Seleccionar"; +/* No comment provided by engineer. */ +"Selected %lld" = "Seleccionados %lld"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Las preferencias seleccionadas no permiten este mensaje."; + /* No comment provided by engineer. */ "Self-destruct" = "Autodestrucción"; @@ -3378,20 +3878,29 @@ "send direct message" = "Enviar mensaje directo"; /* No comment provided by engineer. */ -"Send direct message" = "Enviar mensaje directo"; - -/* No comment provided by engineer. */ -"Send direct message to connect" = "Enviar mensaje directo para conectar"; +"Send direct message to connect" = "Envia un mensaje para conectar"; /* No comment provided by engineer. */ "Send disappearing message" = "Enviar mensaje temporal"; +/* No comment provided by engineer. */ +"Send errors" = "Errores de envío"; + /* No comment provided by engineer. */ "Send link previews" = "Enviar previsualizacion de enlaces"; /* No comment provided by engineer. */ "Send live message" = "Mensaje en vivo"; +/* No comment provided by engineer. */ +"Send message to enable calls." = "Enviar mensaje para activar llamadas."; + +/* No comment provided by engineer. */ +"Send messages directly when IP address is protected and your or destination server does not support private routing." = "Enviar mensajes directamente cuando tu dirección IP está protegida y tu servidor o el de destino no admitan enrutamiento privado."; + +/* No comment provided by engineer. */ +"Send messages directly when your or destination server does not support private routing." = "Enviar mensajes directamente cuando tu servidor o el de destino no admitan enrutamiento privado."; + /* No comment provided by engineer. */ "Send notifications" = "Enviar notificaciones"; @@ -3408,7 +3917,7 @@ "Send them from gallery or custom keyboards." = "Envíalos desde la galería o desde teclados personalizados."; /* No comment provided by engineer. */ -"Send up to 100 last messages to new members." = "Enviar hasta 100 últimos mensajes a los miembros nuevos."; +"Send up to 100 last messages to new members." = "Se envían hasta 100 mensajes más recientes a los miembros nuevos."; /* No comment provided by engineer. */ "Sender cancelled file transfer." = "El remitente ha cancelado la transferencia de archivos."; @@ -3446,15 +3955,42 @@ /* copied message info */ "Sent at: %@" = "Enviado: %@"; +/* No comment provided by engineer. */ +"Sent directly" = "Directamente"; + /* notification */ "Sent file event" = "Evento de archivo enviado"; /* message info title */ "Sent message" = "Mensaje saliente"; +/* No comment provided by engineer. */ +"Sent messages" = "Mensajes enviados"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "Los mensajes enviados se eliminarán una vez transcurrido el tiempo establecido."; +/* No comment provided by engineer. */ +"Sent reply" = "Respuesta enviada"; + +/* No comment provided by engineer. */ +"Sent total" = "Total enviados"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Mediante proxy"; + +/* No comment provided by engineer. */ +"Server address" = "Dirección del servidor"; + +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "La dirección del servidor es incompatible con la configuración de la red: %@."; + +/* srv error text. */ +"Server address is incompatible with network settings." = "La dirección del servidor es incompatible con la configuración de la red."; + +/* queue info */ +"server queue info: %@\n\nlast received msg: %@" = "información cola del servidor: %1$@\n\núltimo mensaje recibido: %2$@"; + /* server test error */ "Server requires authorization to create queues, check password" = "El servidor requiere autorización para crear colas, comprueba la contraseña"; @@ -3464,9 +4000,24 @@ /* No comment provided by engineer. */ "Server test failed!" = "¡Error en prueba del servidor!"; +/* No comment provided by engineer. */ +"Server type" = "Tipo de servidor"; + +/* srv error text */ +"Server version is incompatible with network settings." = "La versión del servidor es incompatible con la configuración de red."; + +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "La versión del servidor es incompatible con tu aplicación: %@."; + /* No comment provided by engineer. */ "Servers" = "Servidores"; +/* No comment provided by engineer. */ +"Servers info" = "Info servidores"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "Las estadísticas de los servidores serán restablecidas. ¡No podrá deshacerse!"; + /* No comment provided by engineer. */ "Session code" = "Código de sesión"; @@ -3476,6 +4027,9 @@ /* No comment provided by engineer. */ "Set contact name…" = "Escribe el nombre del contacto…"; +/* No comment provided by engineer. */ +"Set default theme" = "Establecer tema predeterminado"; + /* No comment provided by engineer. */ "Set group preferences" = "Establecer preferencias de grupo"; @@ -3521,15 +4075,24 @@ /* No comment provided by engineer. */ "Share address with contacts?" = "¿Compartir la dirección con los contactos?"; +/* No comment provided by engineer. */ +"Share from other apps." = "Comparte desde otras aplicaciones."; + /* No comment provided by engineer. */ "Share link" = "Compartir enlace"; /* No comment provided by engineer. */ -"Share this 1-time invite link" = "Compartir este enlace de un uso"; +"Share this 1-time invite link" = "Comparte este enlace de un solo uso"; + +/* No comment provided by engineer. */ +"Share to SimpleX" = "Compartir con Simplex"; /* No comment provided by engineer. */ "Share with contacts" = "Compartir con contactos"; +/* No comment provided by engineer. */ +"Show → on messages sent via private routing." = "Mostrar → en mensajes con enrutamiento privado."; + /* No comment provided by engineer. */ "Show calls in phone history" = "Mostrar llamadas en el historial del teléfono"; @@ -3539,6 +4102,12 @@ /* No comment provided by engineer. */ "Show last messages" = "Mostrar último mensaje"; +/* No comment provided by engineer. */ +"Show message status" = "Estado del mensaje"; + +/* No comment provided by engineer. */ +"Show percentage" = "Mostrar porcentajes"; + /* No comment provided by engineer. */ "Show preview" = "Mostrar vista previa"; @@ -3548,6 +4117,9 @@ /* No comment provided by engineer. */ "Show:" = "Mostrar:"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "Dirección SimpleX"; @@ -3593,6 +4165,9 @@ /* No comment provided by engineer. */ "Simplified incognito mode" = "Modo incógnito simplificado"; +/* No comment provided by engineer. */ +"Size" = "Tamaño"; + /* No comment provided by engineer. */ "Skip" = "Omitir"; @@ -3603,11 +4178,20 @@ "Small groups (max 20)" = "Grupos pequeños (máx. 20)"; /* No comment provided by engineer. */ -"SMP servers" = "Servidores SMP"; +"SMP server" = "Servidor SMP"; + +/* blur media */ +"Soft" = "Suave"; + +/* No comment provided by engineer. */ +"Some file(s) were not exported:" = "Algunos archivos no han sido exportados:"; /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "Algunos errores no críticos ocurrieron durante la importación - para más detalles puedes ver la consola de Chat."; +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import:" = "Han ocurrido algunos errores no críticos durante la importación:"; + /* notification title */ "Somebody" = "Alguien"; @@ -3626,20 +4210,26 @@ /* No comment provided by engineer. */ "Start migration" = "Iniciar migración"; +/* No comment provided by engineer. */ +"Starting from %@." = "Iniciado el %@."; + /* No comment provided by engineer. */ "starting…" = "inicializando…"; +/* No comment provided by engineer. */ +"Statistics" = "Estadísticas"; + /* No comment provided by engineer. */ "Stop" = "Parar"; /* No comment provided by engineer. */ -"Stop chat" = "Parar chat"; +"Stop chat" = "Parar SimpleX"; /* No comment provided by engineer. */ -"Stop chat to enable database actions" = "Para habilitar las acciones sobre la base de datos, debes parar Chat"; +"Stop chat to enable database actions" = "Para habilitar las acciones sobre la base de datos, debes parar SimpleX"; /* No comment provided by engineer. */ -"Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Para poder exportar, importar o eliminar la base de datos primero debes parar Chat. Mientras tanto no podrás recibir ni enviar mensajes."; +"Stop chat to export, import or delete chat database. You will not be able to receive and send messages while the chat is stopped." = "Para poder exportar, importar o eliminar la base de datos primero debes parar SimpleX. Mientras tanto no podrás recibir ni enviar mensajes."; /* No comment provided by engineer. */ "Stop chat?" = "¿Parar Chat?"; @@ -3668,9 +4258,21 @@ /* No comment provided by engineer. */ "strike" = "tachado"; +/* blur media */ +"Strong" = "Fuerte"; + /* No comment provided by engineer. */ "Submit" = "Enviar"; +/* No comment provided by engineer. */ +"Subscribed" = "Suscrito"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Errores de suscripción"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Suscripciones ignoradas"; + /* No comment provided by engineer. */ "Support SimpleX Chat" = "Soporte SimpleX Chat"; @@ -3699,16 +4301,16 @@ "Tap to join incognito" = "Pulsa para unirte en modo incógnito"; /* No comment provided by engineer. */ -"Tap to paste link" = "Pulsa para pegar enlace"; +"Tap to paste link" = "Pulsa para pegar el enlacePulsa para pegar enlace"; /* No comment provided by engineer. */ "Tap to scan" = "Pulsa para escanear"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Pulsa para iniciar chat nuevo"; +"TCP connection" = "Conexión TCP"; /* No comment provided by engineer. */ -"TCP connection timeout" = "Tiempo de espera de la conexión TCP agotado"; +"TCP connection timeout" = "Timeout de la conexión TCP"; /* No comment provided by engineer. */ "TCP_KEEPCNT" = "TCP_KEEPCNT"; @@ -3719,6 +4321,9 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* No comment provided by engineer. */ +"Temporary file error" = "Error en archivo temporal"; + /* server test failure */ "Test failed at step %@." = "La prueba ha fallado en el paso %@."; @@ -3746,6 +4351,9 @@ /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "La aplicación puede notificarte cuando recibas mensajes o solicitudes de contacto: por favor, abre la configuración para activarlo."; +/* No comment provided by engineer. */ +"The app will ask to confirm downloads from unknown file servers (except .onion)." = "La aplicación pedirá que confirmes las descargas desde servidores de archivos desconocidos (excepto si son .onion)."; + /* No comment provided by engineer. */ "The attempt to change database passphrase was not completed." = "El intento de cambiar la contraseña de la base de datos no se ha completado."; @@ -3776,6 +4384,12 @@ /* No comment provided by engineer. */ "The message will be marked as moderated for all members." = "El mensaje será marcado como moderado para todos los miembros."; +/* No comment provided by engineer. */ +"The messages will be deleted for all members." = "Los mensajes serán eliminados para todos los miembros."; + +/* No comment provided by engineer. */ +"The messages will be marked as moderated for all members." = "Los mensajes serán marcados como moderados para todos los miembros."; + /* No comment provided by engineer. */ "The next generation of private messaging" = "La nueva generación de mensajería privada"; @@ -3798,13 +4412,13 @@ "The text you pasted is not a SimpleX link." = "El texto pegado no es un enlace SimpleX."; /* No comment provided by engineer. */ -"Theme" = "Tema"; +"Themes" = "Temas"; /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Esta configuración afecta a tu perfil actual **%@**."; /* No comment provided by engineer. */ -"They can be overridden in contact and group settings." = "Se pueden anular en la configuración de contactos."; +"They can be overridden in contact and group settings." = "Se puede modificar desde la configuración particular de cada grupo y contacto."; /* No comment provided by engineer. */ "This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Esta acción es irreversible. Se eliminarán todos los archivos y multimedia recibidos y enviados. Las imágenes de baja resolución permanecerán."; @@ -3842,9 +4456,15 @@ /* No comment provided by engineer. */ "This is your own SimpleX address!" = "¡Esta es tu propia dirección SimpleX!"; +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "Este enlace ha sido usado en otro dispositivo móvil, por favor crea un enlace nuevo en el ordenador."; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Esta configuración se aplica a los mensajes del perfil actual **%@**."; +/* No comment provided by engineer. */ +"Title" = "Título"; + /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Para consultar cualquier duda y recibir actualizaciones:"; @@ -3866,6 +4486,9 @@ /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Para proteger tu información, activa el Bloqueo SimpleX.\nSe te pedirá que completes la autenticación antes de activar esta función."; +/* No comment provided by engineer. */ +"To protect your IP address, private routing uses your SMP servers to deliver messages." = "Para proteger tu dirección IP, el enrutamiento privado usa tu lista de servidores SMP para enviar mensajes."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Para grabar el mensaje de voz concede permiso para usar el micrófono."; @@ -3878,12 +4501,24 @@ /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Para verificar el cifrado de extremo a extremo con tu contacto, compara (o escanea) el código en ambos dispositivos."; +/* No comment provided by engineer. */ +"Toggle chat list:" = "Alternar lista de chats:"; + /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Activa incógnito al conectar."; +/* No comment provided by engineer. */ +"Toolbar opacity" = "Opacidad barra"; + +/* No comment provided by engineer. */ +"Total" = "Total"; + /* No comment provided by engineer. */ "Transport isolation" = "Aislamiento de transporte"; +/* No comment provided by engineer. */ +"Transport sessions" = "Sesiones de transporte"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Intentando conectar con el servidor usado para recibir mensajes de este contacto (error: %@)."; @@ -3920,13 +4555,10 @@ /* rcv group event chat item */ "unblocked %@" = "ha desbloqueado a %@"; -/* item status description */ -"Unexpected error: %@" = "Error inesperado: %@"; - /* No comment provided by engineer. */ "Unexpected migration state" = "Estado de migración inesperado"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "No fav."; /* No comment provided by engineer. */ @@ -3953,6 +4585,12 @@ /* No comment provided by engineer. */ "Unknown error" = "Error desconocido"; +/* No comment provided by engineer. */ +"unknown servers" = "con servidores desconocidos"; + +/* No comment provided by engineer. */ +"Unknown servers!" = "¡Servidores desconocidos!"; + /* No comment provided by engineer. */ "unknown status" = "estado desconocido"; @@ -3960,7 +4598,7 @@ "Unless you use iOS call interface, enable Do Not Disturb mode to avoid interruptions." = "A menos que utilices la interfaz de llamadas de iOS, activa el modo No molestar para evitar interrupciones."; /* No comment provided by engineer. */ -"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "A menos que tu contacto haya eliminado la conexión o\nque este enlace ya se haya usado, podría ser un error. Por favor, notifícalo.\nPara conectarte, pide a tu contacto que cree otro enlace de conexión y comprueba que tienes buena conexión de red."; +"Unless your contact deleted the connection or this link was already used, it might be a bug - please report it.\nTo connect, please ask your contact to create another connection link and check that you have a stable network connection." = "A menos que tu contacto haya eliminado la conexión o el enlace haya sido usado, podría ser un error. Por favor, notifícalo.\nPara conectarte pide a tu contacto que cree otro enlace y comprueba la conexión de red."; /* No comment provided by engineer. */ "Unlink" = "Desenlazar"; @@ -3975,9 +4613,15 @@ "Unlock app" = "Desbloquear aplicación"; /* No comment provided by engineer. */ +"unmute" = "activar sonido"; + +/* swipe action */ "Unmute" = "Activar audio"; /* No comment provided by engineer. */ +"unprotected" = "con IP desprotegida"; + +/* swipe action */ "Unread" = "No leído"; /* No comment provided by engineer. */ @@ -3986,9 +4630,6 @@ /* No comment provided by engineer. */ "Update" = "Actualizar"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = "¿Actualizar la configuración de los hosts .onion?"; - /* No comment provided by engineer. */ "Update database passphrase" = "Actualizar contraseña de la base de datos"; @@ -3996,7 +4637,7 @@ "Update network settings?" = "¿Actualizar la configuración de red?"; /* No comment provided by engineer. */ -"Update transport isolation mode?" = "¿Actualizar el modo de aislamiento de transporte?"; +"Update settings?" = "¿Actualizar configuración?"; /* rcv group event chat item */ "updated group profile" = "ha actualizado el perfil del grupo"; @@ -4008,10 +4649,10 @@ "Updating settings will re-connect the client to all servers." = "Al actualizar la configuración el cliente se reconectará a todos los servidores."; /* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "Al actualizar esta configuración el cliente se reconectará a todos los servidores."; +"Upgrade and open chat" = "Actualizar y abrir Chat"; /* No comment provided by engineer. */ -"Upgrade and open chat" = "Actualizar y abrir Chat"; +"Upload errors" = "Errores en subida"; /* No comment provided by engineer. */ "Upload failed" = "Error de subida"; @@ -4019,6 +4660,12 @@ /* server test step */ "Upload file" = "Subir archivo"; +/* No comment provided by engineer. */ +"Uploaded" = "Subido"; + +/* No comment provided by engineer. */ +"Uploaded files" = "Archivos subidos"; + /* No comment provided by engineer. */ "Uploading archive" = "Subiendo archivo"; @@ -4046,6 +4693,12 @@ /* No comment provided by engineer. */ "Use only local notifications?" = "¿Usar sólo notificaciones locales?"; +/* No comment provided by engineer. */ +"Use private routing with unknown servers when IP address is not protected." = "Usar enrutamiento privado con servidores desconocidos cuando tu dirección IP no está protegida."; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers." = "Usar enrutamiento privado con servidores de retransmisión desconocidos."; + /* No comment provided by engineer. */ "Use server" = "Usar servidor"; @@ -4055,11 +4708,14 @@ /* No comment provided by engineer. */ "Use the app while in the call." = "Usar la aplicación durante la llamada."; +/* No comment provided by engineer. */ +"Use the app with one hand." = "Usa la aplicación con una sola mano."; + /* No comment provided by engineer. */ "User profile" = "Perfil de usuario"; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "Usar hosts .onion requiere un proveedor VPN compatible."; +"User selection" = "Selección de usuarios"; /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "Usar servidores SimpleX Chat."; @@ -4109,6 +4765,9 @@ /* No comment provided by engineer. */ "Via secure quantum resistant protocol." = "Mediante protocolo seguro de resistencia cuántica."; +/* No comment provided by engineer. */ +"video" = "video"; + /* No comment provided by engineer. */ "Video call" = "Videollamada"; @@ -4119,7 +4778,7 @@ "Video will be received when your contact completes uploading it." = "El video se recibirá cuando el contacto termine de subirlo."; /* No comment provided by engineer. */ -"Video will be received when your contact is online, please wait or check later!" = "El vídeo se recibirá cuando el contacto esté en línea, por favor espera o compruébalo más tarde."; +"Video will be received when your contact is online, please wait or check later!" = "El vídeo se recibirá cuando el contacto esté en línea, por favor espera o revisa más tarde."; /* No comment provided by engineer. */ "Videos and files up to 1gb" = "Vídeos y archivos de hasta 1Gb"; @@ -4166,6 +4825,12 @@ /* No comment provided by engineer. */ "Waiting for video" = "Esperando el vídeo"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "Color imagen de fondo"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Color de fondo"; + /* No comment provided by engineer. */ "wants to connect to you!" = "¡quiere contactar contigo!"; @@ -4199,6 +4864,9 @@ /* No comment provided by engineer. */ "When connecting audio and video calls." = "Al iniciar llamadas de audio y vídeo."; +/* No comment provided by engineer. */ +"when IP hidden" = "con IP oculta"; + /* No comment provided by engineer. */ "When people request to connect, you can accept or reject it." = "Cuando alguien solicite conectarse podrás aceptar o rechazar la solicitud."; @@ -4223,14 +4891,26 @@ /* No comment provided by engineer. */ "With reduced battery usage." = "Con uso reducido de batería."; +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to file servers." = "Sin Tor o VPN, tu dirección IP será visible para los servidores de archivos."; + +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Sin Tor o VPN, tu dirección IP será visible para estos servidores XFTP: %@."; + /* No comment provided by engineer. */ "Wrong database passphrase" = "Contraseña de base de datos incorrecta"; +/* snd error text */ +"Wrong key or unknown connection - most likely this connection is deleted." = "Clave incorrecta o conexión desconocida - probablemente esta conexión fue eliminada."; + +/* file error text */ +"Wrong key or unknown file chunk address - most likely file is deleted." = "Clave incorrecta o dirección del bloque del archivo desconocida. Es probable que el archivo se haya eliminado."; + /* No comment provided by engineer. */ "Wrong passphrase!" = "¡Contraseña incorrecta!"; /* No comment provided by engineer. */ -"XFTP servers" = "Servidores XFTP"; +"XFTP server" = "Servidor XFTP"; /* pref value */ "yes" = "sí"; @@ -4286,6 +4966,9 @@ /* No comment provided by engineer. */ "You are invited to group" = "Has sido invitado a un grupo"; +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "No estás conectado a estos servidores. Para enviarles mensajes se usa el enrutamiento privado."; + /* No comment provided by engineer. */ "you are observer" = "Tu rol es observador"; @@ -4295,6 +4978,9 @@ /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "Puede aceptar llamadas desde la pantalla de bloqueo, sin autenticación de dispositivos y aplicaciones."; +/* No comment provided by engineer. */ +"You can change it in Appearance settings." = "Puedes cambiar la posición de la barra desde el menú Apariencia."; + /* No comment provided by engineer. */ "You can create it later" = "Puedes crearla más tarde"; @@ -4314,7 +5000,10 @@ "You can make it visible to your SimpleX contacts via Settings." = "Puedes hacerlo visible para tus contactos de SimpleX en Configuración."; /* notification body */ -"You can now send messages to %@" = "Ya puedes enviar mensajes a %@"; +"You can now chat with %@" = "Ya puedes chatear con %@"; + +/* No comment provided by engineer. */ +"You can send messages to %@ from Archived contacts." = "Puedes enviar mensajes a %@ desde Contactos archivados."; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Puedes configurar las notificaciones de la pantalla de bloqueo desde Configuración."; @@ -4331,6 +5020,9 @@ /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "Puede iniciar Chat a través de la Configuración / Base de datos de la aplicación o reiniciando la aplicación"; +/* No comment provided by engineer. */ +"You can still view conversation with %@ in the list of chats." = "Aún puedes ver la conversación con %@ en la lista de chats."; + /* No comment provided by engineer. */ "You can turn on SimpleX Lock via Settings." = "Puedes activar el Bloqueo SimpleX a través de Configuración."; @@ -4367,9 +5059,6 @@ /* No comment provided by engineer. */ "You have already requested connection!\nRepeat connection request?" = "Ya has solicitado la conexión\n¿Repetir solicitud?"; -/* No comment provided by engineer. */ -"You have no chats" = "No tienes chats"; - /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "La contraseña no se almacena en el dispositivo, tienes que introducirla cada vez que inicies la aplicación."; @@ -4385,9 +5074,18 @@ /* snd group event chat item */ "you left" = "has salido"; +/* No comment provided by engineer. */ +"You may migrate the exported database." = "Puedes migrar la base de datos exportada."; + +/* No comment provided by engineer. */ +"You may save the exported archive." = "Puedes guardar el archivo exportado."; + /* No comment provided by engineer. */ "You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." = "Debes usar la versión más reciente de tu base de datos ÚNICAMENTE en un dispositivo, de lo contrario podrías dejar de recibir mensajes de algunos contactos."; +/* No comment provided by engineer. */ +"You need to allow your contact to call to be able to call them." = "Necesitas permitir que tus contacto llamen para poder llamarles."; + /* No comment provided by engineer. */ "You need to allow your contact to send voice messages to be able to send them." = "Para poder enviar mensajes de voz antes debes permitir que tu contacto pueda enviarlos."; @@ -4410,19 +5108,19 @@ "you unblocked %@" = "has desbloqueado a %@"; /* No comment provided by engineer. */ -"You will be connected to group when the group host's device is online, please wait or check later!" = "Te conectarás al grupo cuando el dispositivo del anfitrión esté en línea, por favor espera o compruébalo más tarde."; +"You will be connected to group when the group host's device is online, please wait or check later!" = "Te conectarás al grupo cuando el dispositivo del anfitrión esté en línea, por favor espera o revisa más tarde."; /* No comment provided by engineer. */ -"You will be connected when group link host's device is online, please wait or check later!" = "Te conectarás cuando el dispositivo propietario del grupo esté en línea, por favor espera o compruébalo más tarde."; +"You will be connected when group link host's device is online, please wait or check later!" = "Te conectarás cuando el dispositivo propietario del grupo esté en línea, por favor espera o revisa más tarde."; /* No comment provided by engineer. */ -"You will be connected when your connection request is accepted, please wait or check later!" = "Te conectarás cuando tu solicitud se acepte, por favor espera o compruébalo más tarde."; +"You will be connected when your connection request is accepted, please wait or check later!" = "Te conectarás cuando tu solicitud se acepte, por favor espera o revisa más tarde."; /* No comment provided by engineer. */ -"You will be connected when your contact's device is online, please wait or check later!" = "Te conectarás cuando el dispositivo del contacto esté en línea, por favor espera o compruébalo más tarde."; +"You will be connected when your contact's device is online, please wait or check later!" = "Te conectarás cuando el dispositivo del contacto esté en línea, por favor espera o revisa más tarde."; /* No comment provided by engineer. */ -"You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Se te pedirá identificarte cuándo inicies o continues usando la aplicación tras 30 segundos en segundo plano."; +"You will be required to authenticate when you start or resume the app after 30 seconds in background." = "Se te pedirá autenticarte cuando inicies la aplicación o sigas usándola tras 30 segundos en segundo plano."; /* No comment provided by engineer. */ "You will connect to all group members." = "Te conectarás con todos los miembros del grupo."; @@ -4460,9 +5158,6 @@ /* No comment provided by engineer. */ "Your chat profiles" = "Mis perfiles"; -/* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "El contacto debe estar en línea para completar la conexión.\nPuedes cancelarla y eliminar el contacto (e intentarlo más tarde con un enlace nuevo)."; - /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "El contacto ha enviado un archivo mayor al máximo admitido (%@)."; @@ -4491,10 +5186,10 @@ "Your profile" = "Tu perfil"; /* No comment provided by engineer. */ -"Your profile **%@** will be shared." = "Tu perfil **%@** será compartido."; +"Your profile **%@** will be shared." = "El perfil **%@** será compartido."; /* No comment provided by engineer. */ -"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Tu perfil se almacena en tu dispositivo y sólo se comparte con tus contactos.\nLos servidores de SimpleX no pueden ver tu perfil."; +"Your profile is stored on your device and shared only with your contacts.\nSimpleX servers cannot see your profile." = "Tu perfil es almacenado en tu dispositivo y solamente se comparte con tus contactos.\nLos servidores SimpleX no pueden ver tu perfil."; /* No comment provided by engineer. */ "Your profile, contacts and delivered messages are stored on your device." = "Tu perfil, contactos y mensajes se almacenan en tu dispositivo."; diff --git a/apps/ios/fi.lproj/Localizable.strings b/apps/ios/fi.lproj/Localizable.strings index c71f9e089c..a7820e69b1 100644 --- a/apps/ios/fi.lproj/Localizable.strings +++ b/apps/ios/fi.lproj/Localizable.strings @@ -121,9 +121,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ on vahvistettu"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ palvelimet"; - /* notification title */ "%@ wants to connect!" = "%@ haluaa muodostaa yhteyden!"; @@ -280,11 +277,9 @@ /* No comment provided by engineer. */ "above, then choose:" = "edellä, valitse sitten:"; -/* No comment provided by engineer. */ -"Accent color" = "Korostusväri"; - /* accept contact request via notification - accept incoming call via notification */ + accept incoming call via notification + swipe action */ "Accept" = "Hyväksy"; /* No comment provided by engineer. */ @@ -293,7 +288,8 @@ /* notification body */ "Accept contact request from %@?" = "Hyväksy kontaktipyyntö %@:ltä?"; -/* accept contact request via notification */ +/* accept contact request via notification + swipe action */ "Accept incognito" = "Hyväksy tuntematon"; /* call status */ @@ -309,7 +305,7 @@ "Add profile" = "Lisää profiili"; /* No comment provided by engineer. */ -"Add server…" = "Lisää palvelin…"; +"Add server" = "Lisää palvelin"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Lisää palvelimia skannaamalla QR-koodeja."; @@ -648,7 +644,7 @@ /* No comment provided by engineer. */ "Choose from library" = "Valitse kirjastosta"; -/* No comment provided by engineer. */ +/* swipe action */ "Clear" = "Tyhjennä"; /* No comment provided by engineer. */ @@ -663,9 +659,6 @@ /* No comment provided by engineer. */ "colored" = "värillinen"; -/* No comment provided by engineer. */ -"Colors" = "Värit"; - /* server test step */ "Compare file" = "Vertaa tiedostoa"; @@ -777,9 +770,6 @@ /* notification */ "Contact is connected" = "Kontakti on yhdistetty"; -/* No comment provided by engineer. */ -"Contact is not connected yet!" = "Kontaktia ei ole vielä yhdistetty!"; - /* No comment provided by engineer. */ "Contact name" = "Kontaktin nimi"; @@ -795,7 +785,7 @@ /* No comment provided by engineer. */ "Continue" = "Jatka"; -/* chat item action */ +/* No comment provided by engineer. */ "Copy" = "Kopioi"; /* No comment provided by engineer. */ @@ -930,7 +920,8 @@ /* No comment provided by engineer. */ "default (yes)" = "oletusarvo (kyllä)"; -/* chat item action */ +/* chat item action + swipe action */ "Delete" = "Poista"; /* No comment provided by engineer. */ @@ -963,9 +954,6 @@ /* No comment provided by engineer. */ "Delete contact" = "Poista kontakti"; -/* No comment provided by engineer. */ -"Delete Contact" = "Poista kontakti"; - /* No comment provided by engineer. */ "Delete database" = "Poista tietokanta"; @@ -1017,9 +1005,6 @@ /* No comment provided by engineer. */ "Delete old database?" = "Poista vanha tietokanta?"; -/* No comment provided by engineer. */ -"Delete pending connection" = "Poista vireillä oleva yhteys"; - /* No comment provided by engineer. */ "Delete pending connection?" = "Poistetaanko odottava yhteys?"; @@ -1338,9 +1323,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "Virhe yhteyden poistamisessa"; -/* No comment provided by engineer. */ -"Error deleting contact" = "Virhe kontaktin poistamisessa"; - /* No comment provided by engineer. */ "Error deleting database" = "Virhe tietokannan poistamisessa"; @@ -1434,7 +1416,8 @@ /* No comment provided by engineer. */ "Error: " = "Virhe: "; -/* No comment provided by engineer. */ +/* file error text + snd error text */ "Error: %@" = "Virhe: %@"; /* No comment provided by engineer. */ @@ -1470,7 +1453,7 @@ /* No comment provided by engineer. */ "Fast and no wait until the sender is online!" = "Nopea ja ei odotusta, kunnes lähettäjä on online-tilassa!"; -/* No comment provided by engineer. */ +/* swipe action */ "Favorite" = "Suosikki"; /* No comment provided by engineer. */ @@ -1854,7 +1837,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Japanilainen käyttöliittymä"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Liity"; /* No comment provided by engineer. */ @@ -1884,7 +1867,7 @@ /* No comment provided by engineer. */ "Learn more" = "Lue lisää"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "Poistu"; /* No comment provided by engineer. */ @@ -2058,19 +2041,16 @@ /* item status description */ "Most likely this connection is deleted." = "Todennäköisesti tämä yhteys on poistettu."; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Todennäköisesti tämä kontakti on poistanut yhteyden sinuun."; - /* No comment provided by engineer. */ "Multiple chat profiles" = "Useita keskusteluprofiileja"; -/* No comment provided by engineer. */ +/* swipe action */ "Mute" = "Mykistä"; /* No comment provided by engineer. */ "Muted when inactive!" = "Mykistetty ei-aktiivisena!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "Nimi"; /* No comment provided by engineer. */ @@ -2174,7 +2154,7 @@ time to disappear */ "off" = "pois"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Pois"; /* feature offered item */ @@ -2199,10 +2179,10 @@ "One-time invitation link" = "Kertakutsulinkki"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "Yhteyden muodostamiseen tarvitaan Onion-isäntiä. Edellyttää VPN:n sallimista."; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Yhteyden muodostamiseen tarvitaan Onion-isäntiä.\nEdellyttää VPN:n sallimista."; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "Onion-isäntiä käytetään, kun niitä on saatavilla. Edellyttää VPN:n sallimista."; +"Onion hosts will be used when available.\nRequires compatible VPN." = "Onion-isäntiä käytetään, kun niitä on saatavilla.\nEdellyttää VPN:n sallimista."; /* No comment provided by engineer. */ "Onion hosts will not be used." = "Onion-isäntiä ei käytetä."; @@ -2426,7 +2406,7 @@ /* chat item menu */ "React…" = "Reagoi…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "Lue"; /* No comment provided by engineer. */ @@ -2492,7 +2472,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Pienempi akun käyttö"; -/* reject incoming call via notification */ +/* reject incoming call via notification + swipe action */ "Reject" = "Hylkää"; /* No comment provided by engineer. */ @@ -2576,9 +2557,6 @@ /* chat item action */ "Reveal" = "Paljasta"; -/* No comment provided by engineer. */ -"Revert" = "Palauta"; - /* No comment provided by engineer. */ "Revoke" = "Peruuta"; @@ -2681,7 +2659,7 @@ /* chat item text */ "security code changed" = "turvakoodi on muuttunut"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "Valitse"; /* No comment provided by engineer. */ @@ -2705,9 +2683,6 @@ /* No comment provided by engineer. */ "Send delivery receipts to" = "Lähetä toimituskuittaukset vastaanottajalle"; -/* No comment provided by engineer. */ -"Send direct message" = "Lähetä yksityisviesti"; - /* No comment provided by engineer. */ "Send disappearing message" = "Lähetä katoava viesti"; @@ -2894,9 +2869,6 @@ /* No comment provided by engineer. */ "Small groups (max 20)" = "Pienryhmät (max 20)"; -/* No comment provided by engineer. */ -"SMP servers" = "SMP-palvelimet"; - /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "Tuonnin aikana tapahtui joitakin ei-vakavia virheitä – saatat nähdä Chat-konsolissa lisätietoja."; @@ -2972,9 +2944,6 @@ /* No comment provided by engineer. */ "Tap to join incognito" = "Napauta liittyäksesi incognito-tilassa"; -/* No comment provided by engineer. */ -"Tap to start a new chat" = "Aloita uusi keskustelu napauttamalla"; - /* No comment provided by engineer. */ "TCP connection timeout" = "TCP-yhteyden aikakatkaisu"; @@ -3059,9 +3028,6 @@ /* No comment provided by engineer. */ "The servers for new connections of your current chat profile **%@**." = "Palvelimet nykyisen keskusteluprofiilisi uusille yhteyksille **%@**."; -/* No comment provided by engineer. */ -"Theme" = "Teema"; - /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Nämä asetukset koskevat nykyistä profiiliasi **%@**."; @@ -3137,13 +3103,10 @@ /* No comment provided by engineer. */ "Unable to record voice message" = "Ääniviestiä ei voi tallentaa"; -/* item status description */ -"Unexpected error: %@" = "Odottamaton virhe: %@"; - /* No comment provided by engineer. */ "Unexpected migration state" = "Odottamaton siirtotila"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "Epäsuotuisa."; /* No comment provided by engineer. */ @@ -3182,36 +3145,27 @@ /* authentication reason */ "Unlock app" = "Avaa sovellus"; -/* No comment provided by engineer. */ +/* swipe action */ "Unmute" = "Poista mykistys"; -/* No comment provided by engineer. */ +/* swipe action */ "Unread" = "Lukematon"; /* No comment provided by engineer. */ "Update" = "Päivitä"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = "Päivitä .onion-isäntien asetus?"; - /* No comment provided by engineer. */ "Update database passphrase" = "Päivitä tietokannan tunnuslause"; /* No comment provided by engineer. */ "Update network settings?" = "Päivitä verkkoasetukset?"; -/* No comment provided by engineer. */ -"Update transport isolation mode?" = "Päivitä kuljetuksen eristystila?"; - /* rcv group event chat item */ "updated group profile" = "päivitetty ryhmäprofiili"; /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "Asetusten päivittäminen yhdistää asiakkaan uudelleen kaikkiin palvelimiin."; -/* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "Tämän asetuksen päivittäminen yhdistää asiakkaan uudelleen kaikkiin palvelimiin."; - /* No comment provided by engineer. */ "Upgrade and open chat" = "Päivitä ja avaa keskustelu"; @@ -3245,9 +3199,6 @@ /* No comment provided by engineer. */ "User profile" = "Käyttäjäprofiili"; -/* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = ".onion-isäntien käyttäminen vaatii yhteensopivan VPN-palveluntarjoajan."; - /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "Käyttää SimpleX Chat -palvelimia."; @@ -3362,9 +3313,6 @@ /* No comment provided by engineer. */ "Wrong passphrase!" = "Väärä tunnuslause!"; -/* No comment provided by engineer. */ -"XFTP servers" = "XFTP-palvelimet"; - /* pref value */ "yes" = "kyllä"; @@ -3411,7 +3359,7 @@ "You can hide or mute a user profile - swipe it to the right." = "Voit piilottaa tai mykistää käyttäjäprofiilin pyyhkäisemällä sitä oikealle."; /* notification body */ -"You can now send messages to %@" = "Voit nyt lähettää viestejä %@:lle"; +"You can now chat with %@" = "Voit nyt lähettää viestejä %@:lle"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Voit määrittää lukitusnäytön ilmoituksen esikatselun asetuksista."; @@ -3455,9 +3403,6 @@ /* No comment provided by engineer. */ "You could not be verified; please try again." = "Sinua ei voitu todentaa; yritä uudelleen."; -/* No comment provided by engineer. */ -"You have no chats" = "Sinulla ei ole keskusteluja"; - /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "Sinun on annettava tunnuslause aina, kun sovellus käynnistyy - sitä ei tallenneta laitteeseen."; @@ -3539,9 +3484,6 @@ /* No comment provided by engineer. */ "Your chat profiles" = "Keskusteluprofiilisi"; -/* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Kontaktin tulee olla online-tilassa, jotta yhteys voidaan muodostaa.\nVoit peruuttaa tämän yhteyden ja poistaa kontaktin (ja yrittää myöhemmin uudella linkillä)."; - /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Yhteyshenkilösi lähetti tiedoston, joka on suurempi kuin tällä hetkellä tuettu enimmäiskoko (%@)."; diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index 5e6c9c1b40..b4f256762c 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -154,9 +154,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ est vérifié·e"; -/* No comment provided by engineer. */ -"%@ servers" = "Serveurs %@"; - /* No comment provided by engineer. */ "%@ uploaded" = "%@ envoyé"; @@ -317,7 +314,7 @@ "A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." = "Une connexion TCP distincte sera utilisée **pour chaque contact et membre de groupe**.\n**Veuillez noter** : si vous avez de nombreuses connexions, votre consommation de batterie et de réseau peut être nettement plus élevée et certaines liaisons peuvent échouer."; /* No comment provided by engineer. */ -"Abort" = "Annuler"; +"Abort" = "Abandonner"; /* No comment provided by engineer. */ "Abort changing address" = "Annuler le changement d'adresse"; @@ -338,10 +335,11 @@ "above, then choose:" = "ci-dessus, puis choisissez :"; /* No comment provided by engineer. */ -"Accent color" = "Couleur principale"; +"Accent" = "Principale"; /* accept contact request via notification - accept incoming call via notification */ + accept incoming call via notification + swipe action */ "Accept" = "Accepter"; /* No comment provided by engineer. */ @@ -350,12 +348,22 @@ /* notification body */ "Accept contact request from %@?" = "Accepter la demande de contact de %@ ?"; -/* accept contact request via notification */ +/* accept contact request via notification + swipe action */ "Accept incognito" = "Accepter en incognito"; /* call status */ "accepted call" = "appel accepté"; +/* No comment provided by engineer. */ +"Acknowledged" = "Reçu avec accusé de réception"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Erreur d'accusé de réception"; + +/* No comment provided by engineer. */ +"Active connections" = "Connections actives"; + /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Ajoutez une adresse à votre profil, afin que vos contacts puissent la partager avec d'autres personnes. La mise à jour du profil sera envoyée à vos contacts."; @@ -369,7 +377,7 @@ "Add profile" = "Ajouter un profil"; /* No comment provided by engineer. */ -"Add server…" = "Ajouter un serveur…"; +"Add server" = "Ajouter un serveur"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Ajoutez des serveurs en scannant des codes QR."; @@ -380,6 +388,15 @@ /* No comment provided by engineer. */ "Add welcome message" = "Ajouter un message d'accueil"; +/* No comment provided by engineer. */ +"Additional accent" = "Accent additionnel"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Accent additionnel 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "Accent secondaire"; + /* No comment provided by engineer. */ "Address" = "Adresse"; @@ -401,6 +418,9 @@ /* No comment provided by engineer. */ "Advanced network settings" = "Paramètres réseau avancés"; +/* No comment provided by engineer. */ +"Advanced settings" = "Paramètres avancés"; + /* chat item text */ "agreeing encryption for %@…" = "négociation du chiffrement avec %@…"; @@ -416,6 +436,9 @@ /* No comment provided by engineer. */ "All data is erased when it is entered." = "Toutes les données sont effacées lorsqu'il est saisi."; +/* No comment provided by engineer. */ +"All data is private to your device." = "Toutes les données restent confinées dans votre appareil."; + /* No comment provided by engineer. */ "All group members will remain connected." = "Tous les membres du groupe resteront connectés."; @@ -431,6 +454,9 @@ /* No comment provided by engineer. */ "All new messages from %@ will be hidden!" = "Tous les nouveaux messages de %@ seront cachés !"; +/* No comment provided by engineer. */ +"All profiles" = "Tous les profiles"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Tous vos contacts resteront connectés."; @@ -446,9 +472,15 @@ /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "Autoriser les appels que si votre contact les autorise."; +/* No comment provided by engineer. */ +"Allow calls?" = "Autoriser les appels ?"; + /* No comment provided by engineer. */ "Allow disappearing messages only if your contact allows it to you." = "Autorise les messages éphémères seulement si votre contact vous l’autorise."; +/* No comment provided by engineer. */ +"Allow downgrade" = "Autoriser la rétrogradation"; + /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Autoriser la suppression irréversible des messages uniquement si votre contact vous l'autorise. (24 heures)"; @@ -464,6 +496,9 @@ /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Autorise l’envoi de messages éphémères."; +/* No comment provided by engineer. */ +"Allow sharing" = "Autoriser le partage"; + /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Autoriser la suppression irréversible de messages envoyés. (24 heures)"; @@ -509,6 +544,9 @@ /* pref value */ "always" = "toujours"; +/* No comment provided by engineer. */ +"Always use private routing." = "Toujours utiliser le routage privé."; + /* No comment provided by engineer. */ "Always use relay" = "Se connecter via relais"; @@ -552,7 +590,16 @@ "Apply" = "Appliquer"; /* No comment provided by engineer. */ -"Archive and upload" = "Archiver et transférer"; +"Apply to" = "Appliquer à"; + +/* No comment provided by engineer. */ +"Archive and upload" = "Archiver et téléverser"; + +/* No comment provided by engineer. */ +"Archive contacts to chat later." = "Archiver les contacts pour discuter plus tard."; + +/* No comment provided by engineer. */ +"Archived contacts" = "Contacts archivés"; /* No comment provided by engineer. */ "Archiving database" = "Archivage de la base de données"; @@ -560,6 +607,9 @@ /* No comment provided by engineer. */ "Attach" = "Attacher"; +/* No comment provided by engineer. */ +"attempts" = "tentatives"; + /* No comment provided by engineer. */ "Audio & video calls" = "Appels audio et vidéo"; @@ -602,6 +652,9 @@ /* No comment provided by engineer. */ "Back" = "Retour"; +/* No comment provided by engineer. */ +"Background" = "Fond"; + /* No comment provided by engineer. */ "Bad desktop address" = "Mauvaise adresse de bureau"; @@ -623,6 +676,12 @@ /* No comment provided by engineer. */ "Better messages" = "Meilleurs messages"; +/* No comment provided by engineer. */ +"Better networking" = "Meilleure gestion de réseau"; + +/* No comment provided by engineer. */ +"Black" = "Noir"; + /* No comment provided by engineer. */ "Block" = "Bloquer"; @@ -653,6 +712,12 @@ /* No comment provided by engineer. */ "Blocked by admin" = "Bloqué par l'administrateur"; +/* No comment provided by engineer. */ +"Blur for better privacy." = "Rendez les images floues et protégez-les contre les regards indiscrets."; + +/* No comment provided by engineer. */ +"Blur media" = "Flouter les médias"; + /* No comment provided by engineer. */ "bold" = "gras"; @@ -677,6 +742,9 @@ /* No comment provided by engineer. */ "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Par profil de chat (par défaut) ou [par connexion](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; +/* No comment provided by engineer. */ +"call" = "appeler"; + /* No comment provided by engineer. */ "Call already ended!" = "Appel déjà terminé !"; @@ -692,15 +760,27 @@ /* No comment provided by engineer. */ "Calls" = "Appels"; +/* No comment provided by engineer. */ +"Calls prohibited!" = "Les appels ne sont pas autorisés !"; + /* No comment provided by engineer. */ "Camera not available" = "Caméra non disponible"; +/* No comment provided by engineer. */ +"Can't call contact" = "Impossible d'appeler le contact"; + +/* No comment provided by engineer. */ +"Can't call member" = "Impossible d'appeler le membre"; + /* No comment provided by engineer. */ "Can't invite contact!" = "Impossible d'inviter le contact !"; /* No comment provided by engineer. */ "Can't invite contacts!" = "Impossible d'inviter les contacts !"; +/* No comment provided by engineer. */ +"Can't message member" = "Impossible d'envoyer un message à ce membre"; + /* No comment provided by engineer. */ "Cancel" = "Annuler"; @@ -713,9 +793,15 @@ /* No comment provided by engineer. */ "Cannot access keychain to save database password" = "Impossible d'accéder à la keychain pour enregistrer le mot de passe de la base de données"; +/* No comment provided by engineer. */ +"Cannot forward message" = "Impossible de transférer le message"; + /* No comment provided by engineer. */ "Cannot receive file" = "Impossible de recevoir le fichier"; +/* snd error text */ +"Capacity exceeded - recipient did not receive previously sent messages." = "Capacité dépassée - le destinataire n'a pas pu recevoir les messages envoyés précédemment."; + /* No comment provided by engineer. */ "Cellular" = "Cellulaire"; @@ -768,6 +854,9 @@ /* No comment provided by engineer. */ "Chat archive" = "Archives du chat"; +/* No comment provided by engineer. */ +"Chat colors" = "Couleurs de chat"; + /* No comment provided by engineer. */ "Chat console" = "Console du chat"; @@ -777,6 +866,9 @@ /* No comment provided by engineer. */ "Chat database deleted" = "Base de données du chat supprimée"; +/* No comment provided by engineer. */ +"Chat database exported" = "Exportation de la base de données des discussions"; + /* No comment provided by engineer. */ "Chat database imported" = "Base de données du chat importée"; @@ -789,12 +881,18 @@ /* No comment provided by engineer. */ "Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." = "Le chat est arrêté. Si vous avez déjà utilisé cette base de données sur un autre appareil, vous devez la transférer à nouveau avant de démarrer le chat."; +/* No comment provided by engineer. */ +"Chat list" = "Liste de discussion"; + /* No comment provided by engineer. */ "Chat migrated!" = "Messagerie transférée !"; /* No comment provided by engineer. */ "Chat preferences" = "Préférences de chat"; +/* No comment provided by engineer. */ +"Chat theme" = "Thème de chat"; + /* No comment provided by engineer. */ "Chats" = "Discussions"; @@ -814,6 +912,15 @@ "Choose from library" = "Choisir dans la photothèque"; /* No comment provided by engineer. */ +"Chunks deleted" = "Chunks supprimés"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Chunks téléchargés"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Chunks téléversés"; + +/* swipe action */ "Clear" = "Effacer"; /* No comment provided by engineer. */ @@ -829,10 +936,13 @@ "Clear verification" = "Retirer la vérification"; /* No comment provided by engineer. */ -"colored" = "coloré"; +"Color chats with the new themes." = "Colorez vos discussions avec les nouveaux thèmes."; /* No comment provided by engineer. */ -"Colors" = "Couleurs"; +"Color mode" = "Mode de couleur"; + +/* No comment provided by engineer. */ +"colored" = "coloré"; /* server test step */ "Compare file" = "Comparer le fichier"; @@ -843,15 +953,27 @@ /* No comment provided by engineer. */ "complete" = "complet"; +/* No comment provided by engineer. */ +"Completed" = "Complétées"; + /* No comment provided by engineer. */ "Configure ICE servers" = "Configurer les serveurs ICE"; +/* No comment provided by engineer. */ +"Configured %@ servers" = "%@ serveurs configurés"; + /* No comment provided by engineer. */ "Confirm" = "Confirmer"; +/* No comment provided by engineer. */ +"Confirm contact deletion?" = "Confirmer la suppression du contact ?"; + /* No comment provided by engineer. */ "Confirm database upgrades" = "Confirmer la mise à niveau de la base de données"; +/* No comment provided by engineer. */ +"Confirm files from unknown servers." = "Confirmer les fichiers provenant de serveurs inconnus."; + /* No comment provided by engineer. */ "Confirm network settings" = "Confirmer les paramètres réseau"; @@ -885,6 +1007,9 @@ /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "se connecter aux developpeurs de SimpleX Chat."; +/* No comment provided by engineer. */ +"Connect to your friends faster." = "Connectez-vous à vos amis plus rapidement."; + /* No comment provided by engineer. */ "Connect to yourself?" = "Se connecter à soi-même ?"; @@ -909,18 +1034,27 @@ /* No comment provided by engineer. */ "connected" = "connecté"; +/* No comment provided by engineer. */ +"Connected" = "Connecté"; + /* No comment provided by engineer. */ "Connected desktop" = "Bureau connecté"; /* rcv group event chat item */ "connected directly" = "s'est connecté.e de manière directe"; +/* No comment provided by engineer. */ +"Connected servers" = "Serveurs connectés"; + /* No comment provided by engineer. */ "Connected to desktop" = "Connecté au bureau"; /* No comment provided by engineer. */ "connecting" = "connexion"; +/* No comment provided by engineer. */ +"Connecting" = "Connexion"; + /* No comment provided by engineer. */ "connecting (accepted)" = "connexion (acceptée)"; @@ -942,6 +1076,9 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "Connexion au serveur… (erreur : %@)"; +/* No comment provided by engineer. */ +"Connecting to contact, please wait or check later!" = "Connexion au contact, veuillez patienter ou vérifier plus tard !"; + /* No comment provided by engineer. */ "Connecting to desktop" = "Connexion au bureau"; @@ -951,6 +1088,9 @@ /* No comment provided by engineer. */ "Connection" = "Connexion"; +/* No comment provided by engineer. */ +"Connection and servers status." = "État de la connexion et des serveurs."; + /* No comment provided by engineer. */ "Connection error" = "Erreur de connexion"; @@ -960,6 +1100,9 @@ /* chat list item title (it should not be shown */ "connection established" = "connexion établie"; +/* No comment provided by engineer. */ +"Connection notifications" = "Notifications de connexion"; + /* No comment provided by engineer. */ "Connection request sent!" = "Demande de connexion envoyée !"; @@ -969,9 +1112,15 @@ /* No comment provided by engineer. */ "Connection timeout" = "Délai de connexion"; +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "La connexion avec le bureau s'est arrêtée"; + /* connection information */ "connection:%@" = "connexion : %@"; +/* No comment provided by engineer. */ +"Connections" = "Connexions"; + /* profile update event chat item */ "contact %@ changed to %@" = "le contact %1$@ est devenu %2$@"; @@ -981,6 +1130,9 @@ /* No comment provided by engineer. */ "Contact already exists" = "Contact déjà existant"; +/* No comment provided by engineer. */ +"Contact deleted!" = "Contact supprimé !"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "Ce contact a le chiffrement de bout en bout"; @@ -994,7 +1146,7 @@ "Contact is connected" = "Le contact est connecté"; /* No comment provided by engineer. */ -"Contact is not connected yet!" = "Le contact n'est pas encore connecté !"; +"Contact is deleted." = "Le contact est supprimé."; /* No comment provided by engineer. */ "Contact name" = "Nom du contact"; @@ -1002,6 +1154,9 @@ /* No comment provided by engineer. */ "Contact preferences" = "Préférences de contact"; +/* No comment provided by engineer. */ +"Contact will be deleted - this cannot be undone!" = "Le contact sera supprimé - il n'est pas possible de revenir en arrière !"; + /* No comment provided by engineer. */ "Contacts" = "Contacts"; @@ -1011,9 +1166,15 @@ /* No comment provided by engineer. */ "Continue" = "Continuer"; -/* chat item action */ +/* No comment provided by engineer. */ +"Conversation deleted!" = "Conversation supprimée !"; + +/* No comment provided by engineer. */ "Copy" = "Copier"; +/* No comment provided by engineer. */ +"Copy error" = "Erreur de copie"; + /* No comment provided by engineer. */ "Core version: v%@" = "Version du cœur : v%@"; @@ -1059,6 +1220,9 @@ /* No comment provided by engineer. */ "Create your profile" = "Créez votre profil"; +/* No comment provided by engineer. */ +"Created" = "Créées"; + /* No comment provided by engineer. */ "Created at" = "Créé à"; @@ -1083,6 +1247,9 @@ /* No comment provided by engineer. */ "Current passphrase…" = "Phrase secrète actuelle…"; +/* No comment provided by engineer. */ +"Current profile" = "Profil actuel"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "Actuellement, la taille maximale des fichiers supportés est de %@."; @@ -1092,9 +1259,15 @@ /* No comment provided by engineer. */ "Custom time" = "Délai personnalisé"; +/* No comment provided by engineer. */ +"Customize theme" = "Personnaliser le thème"; + /* No comment provided by engineer. */ "Dark" = "Sombre"; +/* No comment provided by engineer. */ +"Dark mode colors" = "Couleurs en mode sombre"; + /* No comment provided by engineer. */ "Database downgrade" = "Rétrogradation de la base de données"; @@ -1155,12 +1328,18 @@ /* time unit */ "days" = "jours"; +/* No comment provided by engineer. */ +"Debug delivery" = "Livraison de débogage"; + /* No comment provided by engineer. */ "Decentralized" = "Décentralisé"; /* message decrypt error item */ "Decryption error" = "Erreur de déchiffrement"; +/* No comment provided by engineer. */ +"decryption errors" = "Erreurs de déchiffrement"; + /* pref value */ "default (%@)" = "défaut (%@)"; @@ -1170,9 +1349,13 @@ /* No comment provided by engineer. */ "default (yes)" = "par défaut (oui)"; -/* chat item action */ +/* chat item action + swipe action */ "Delete" = "Supprimer"; +/* No comment provided by engineer. */ +"Delete %lld messages of members?" = "Supprimer %lld messages de membres ?"; + /* No comment provided by engineer. */ "Delete %lld messages?" = "Supprimer %lld messages ?"; @@ -1210,10 +1393,7 @@ "Delete contact" = "Supprimer le contact"; /* No comment provided by engineer. */ -"Delete Contact" = "Supprimer le contact"; - -/* No comment provided by engineer. */ -"Delete contact?\nThis cannot be undone!" = "Supprimer le contact ?\nCette opération ne peut être annulée !"; +"Delete contact?" = "Supprimer le contact ?"; /* No comment provided by engineer. */ "Delete database" = "Supprimer la base de données"; @@ -1269,9 +1449,6 @@ /* No comment provided by engineer. */ "Delete old database?" = "Supprimer l'ancienne base de données ?"; -/* No comment provided by engineer. */ -"Delete pending connection" = "Supprimer la connexion en attente"; - /* No comment provided by engineer. */ "Delete pending connection?" = "Supprimer la connexion en attente ?"; @@ -1281,12 +1458,21 @@ /* server test step */ "Delete queue" = "Supprimer la file d'attente"; +/* No comment provided by engineer. */ +"Delete up to 20 messages at once." = "Supprimez jusqu'à 20 messages à la fois."; + /* No comment provided by engineer. */ "Delete user profile?" = "Supprimer le profil utilisateur ?"; +/* No comment provided by engineer. */ +"Delete without notification" = "Supprimer sans notification"; + /* deleted chat item */ "deleted" = "supprimé"; +/* No comment provided by engineer. */ +"Deleted" = "Supprimées"; + /* No comment provided by engineer. */ "Deleted at" = "Supprimé à"; @@ -1299,6 +1485,9 @@ /* rcv group event chat item */ "deleted group" = "groupe supprimé"; +/* No comment provided by engineer. */ +"Deletion errors" = "Erreurs de suppression"; + /* No comment provided by engineer. */ "Delivery" = "Distribution"; @@ -1306,7 +1495,7 @@ "Delivery receipts are disabled!" = "Les accusés de réception sont désactivés !"; /* No comment provided by engineer. */ -"Delivery receipts!" = "Justificatifs de réception!"; +"Delivery receipts!" = "Justificatifs de réception !"; /* No comment provided by engineer. */ "Description" = "Description"; @@ -1320,9 +1509,27 @@ /* No comment provided by engineer. */ "Desktop devices" = "Appareils de bureau"; +/* No comment provided by engineer. */ +"Destination server address of %@ is incompatible with forwarding server %@ settings." = "L'adresse du serveur de destination %@ est incompatible avec les paramètres du serveur de redirection %@."; + +/* snd error text */ +"Destination server error: %@" = "Erreur du serveur de destination : %@"; + +/* No comment provided by engineer. */ +"Destination server version of %@ is incompatible with forwarding server %@." = "La version du serveur de destination %@ est incompatible avec le serveur de redirection %@."; + +/* No comment provided by engineer. */ +"Detailed statistics" = "Statistiques détaillées"; + +/* No comment provided by engineer. */ +"Details" = "Détails"; + /* No comment provided by engineer. */ "Develop" = "Développer"; +/* No comment provided by engineer. */ +"Developer options" = "Options pour les développeurs"; + /* No comment provided by engineer. */ "Developer tools" = "Outils du développeur"; @@ -1362,6 +1569,9 @@ /* No comment provided by engineer. */ "disabled" = "désactivé"; +/* No comment provided by engineer. */ +"Disabled" = "Désactivé"; + /* No comment provided by engineer. */ "Disappearing message" = "Message éphémère"; @@ -1398,6 +1608,12 @@ /* No comment provided by engineer. */ "Do not send history to new members." = "Ne pas envoyer d'historique aux nouveaux membres."; +/* No comment provided by engineer. */ +"Do NOT send messages directly, even if your or destination server does not support private routing." = "Ne pas envoyer de messages directement, même si votre serveur ou le serveur de destination ne prend pas en charge le routage privé."; + +/* No comment provided by engineer. */ +"Do NOT use private routing." = "Ne pas utiliser de routage privé."; + /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "N'utilisez PAS SimpleX pour les appels d'urgence."; @@ -1416,12 +1632,21 @@ /* chat item action */ "Download" = "Télécharger"; +/* No comment provided by engineer. */ +"Download errors" = "Erreurs de téléchargement"; + /* No comment provided by engineer. */ "Download failed" = "Échec du téléchargement"; /* server test step */ "Download file" = "Télécharger le fichier"; +/* No comment provided by engineer. */ +"Downloaded" = "Téléchargé"; + +/* No comment provided by engineer. */ +"Downloaded files" = "Fichiers téléchargés"; + /* No comment provided by engineer. */ "Downloading archive" = "Téléchargement de l'archive"; @@ -1434,6 +1659,9 @@ /* integrity error chat item */ "duplicate message" = "message dupliqué"; +/* No comment provided by engineer. */ +"duplicates" = "doublons"; + /* No comment provided by engineer. */ "Duration" = "Durée"; @@ -1491,6 +1719,9 @@ /* enabled status */ "enabled" = "activé"; +/* No comment provided by engineer. */ +"Enabled" = "Activé"; + /* No comment provided by engineer. */ "Enabled for" = "Activé pour"; @@ -1632,6 +1863,9 @@ /* No comment provided by engineer. */ "Error changing setting" = "Erreur de changement de paramètre"; +/* No comment provided by engineer. */ +"Error connecting to forwarding server %@. Please try later." = "Erreur de connexion au serveur de redirection %@. Veuillez réessayer plus tard."; + /* No comment provided by engineer. */ "Error creating address" = "Erreur lors de la création de l'adresse"; @@ -1662,9 +1896,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "Erreur lors de la suppression de la connexion"; -/* No comment provided by engineer. */ -"Error deleting contact" = "Erreur lors de la suppression du contact"; - /* No comment provided by engineer. */ "Error deleting database" = "Erreur lors de la suppression de la base de données"; @@ -1692,6 +1923,9 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "Erreur lors de l'exportation de la base de données du chat"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Erreur d'exportation du thème : %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "Erreur lors de l'importation de la base de données du chat"; @@ -1707,9 +1941,18 @@ /* No comment provided by engineer. */ "Error receiving file" = "Erreur lors de la réception du fichier"; +/* No comment provided by engineer. */ +"Error reconnecting server" = "Erreur de reconnexion du serveur"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "Erreur de reconnexion des serveurs"; + /* No comment provided by engineer. */ "Error removing member" = "Erreur lors de la suppression d'un membre"; +/* No comment provided by engineer. */ +"Error resetting statistics" = "Erreur de réinitialisation des statistiques"; + /* No comment provided by engineer. */ "Error saving %@ servers" = "Erreur lors de la sauvegarde des serveurs %@"; @@ -1779,7 +2022,8 @@ /* No comment provided by engineer. */ "Error: " = "Erreur : "; -/* No comment provided by engineer. */ +/* file error text + snd error text */ "Error: %@" = "Erreur : %@"; /* No comment provided by engineer. */ @@ -1788,6 +2032,9 @@ /* No comment provided by engineer. */ "Error: URL is invalid" = "Erreur : URL invalide"; +/* No comment provided by engineer. */ +"Errors" = "Erreurs"; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Même s'il est désactivé dans la conversation."; @@ -1800,12 +2047,18 @@ /* chat item action */ "Expand" = "Étendre"; +/* No comment provided by engineer. */ +"expired" = "expiré"; + /* No comment provided by engineer. */ "Export database" = "Exporter la base de données"; /* No comment provided by engineer. */ "Export error:" = "Erreur lors de l'exportation :"; +/* No comment provided by engineer. */ +"Export theme" = "Exporter le thème"; + /* No comment provided by engineer. */ "Exported database archive." = "Archive de la base de données exportée."; @@ -1824,9 +2077,24 @@ /* No comment provided by engineer. */ "Faster joining and more reliable messages." = "Connexion plus rapide et messages plus fiables."; -/* No comment provided by engineer. */ +/* swipe action */ "Favorite" = "Favoris"; +/* No comment provided by engineer. */ +"File error" = "Erreur de fichier"; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "Fichier introuvable - le fichier a probablement été supprimé ou annulé."; + +/* file error text */ +"File server error: %@" = "Erreur de serveur de fichiers : %@"; + +/* No comment provided by engineer. */ +"File status" = "Statut du fichier"; + +/* copied message info */ +"File status: %@" = "Statut du fichier : %@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "Le fichier sera supprimé des serveurs."; @@ -1839,6 +2107,9 @@ /* No comment provided by engineer. */ "File: %@" = "Fichier : %@"; +/* No comment provided by engineer. */ +"Files" = "Fichiers"; + /* No comment provided by engineer. */ "Files & media" = "Fichiers & médias"; @@ -1876,7 +2147,7 @@ "Fix connection" = "Réparer la connexion"; /* No comment provided by engineer. */ -"Fix connection?" = "Réparer la connexion?"; +"Fix connection?" = "Réparer la connexion ?"; /* No comment provided by engineer. */ "Fix encryption after restoring backups." = "Réparer le chiffrement après la restauration des sauvegardes."; @@ -1905,6 +2176,21 @@ /* No comment provided by engineer. */ "Forwarded from" = "Transféré depuis"; +/* No comment provided by engineer. */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "Le serveur de redirection %@ n'a pas réussi à se connecter au serveur de destination %@. Veuillez réessayer plus tard."; + +/* No comment provided by engineer. */ +"Forwarding server address is incompatible with network settings: %@." = "L'adresse du serveur de redirection est incompatible avec les paramètres du réseau : %@."; + +/* No comment provided by engineer. */ +"Forwarding server version is incompatible with network settings: %@." = "La version du serveur de redirection est incompatible avec les paramètres du réseau : %@."; + +/* snd error text */ +"Forwarding server: %@\nDestination server error: %@" = "Serveur de transfert : %1$@\nErreur du serveur de destination : %2$@"; + +/* snd error text */ +"Forwarding server: %@\nError: %@" = "Serveur de transfert : %1$@\nErreur : %2$@"; + /* No comment provided by engineer. */ "Found desktop" = "Bureau trouvé"; @@ -1932,6 +2218,12 @@ /* No comment provided by engineer. */ "GIFs and stickers" = "GIFs et stickers"; +/* message preview */ +"Good afternoon!" = "Bonjour !"; + +/* message preview */ +"Good morning!" = "Bonjour !"; + /* No comment provided by engineer. */ "Group" = "Groupe"; @@ -2109,6 +2401,9 @@ /* No comment provided by engineer. */ "Import failed" = "Échec de l'importation"; +/* No comment provided by engineer. */ +"Import theme" = "Importer un thème"; + /* No comment provided by engineer. */ "Importing archive" = "Importation de l'archive"; @@ -2130,6 +2425,9 @@ /* No comment provided by engineer. */ "In-call sounds" = "Sons d'appel"; +/* No comment provided by engineer. */ +"inactive" = "inactif"; + /* No comment provided by engineer. */ "Incognito" = "Incognito"; @@ -2193,6 +2491,9 @@ /* No comment provided by engineer. */ "Interface" = "Interface"; +/* No comment provided by engineer. */ +"Interface colors" = "Couleurs d'interface"; + /* invalid chat data */ "invalid chat" = "chat invalide"; @@ -2235,6 +2536,9 @@ /* group name */ "invitation to group %@" = "invitation au groupe %@"; +/* No comment provided by engineer. */ +"invite" = "inviter"; + /* No comment provided by engineer. */ "Invite friends" = "Inviter des amis"; @@ -2280,6 +2584,9 @@ /* No comment provided by engineer. */ "It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Cela peut arriver quand :\n1. Les messages ont expiré dans le client expéditeur après 2 jours ou sur le serveur après 30 jours.\n2. Le déchiffrement du message a échoué, car vous ou votre contact avez utilisé une ancienne sauvegarde de base de données.\n3. La connexion a été compromise."; +/* No comment provided by engineer. */ +"It protects your IP address and connections." = "Il protège votre adresse IP et vos connexions."; + /* No comment provided by engineer. */ "It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Il semblerait que vous êtes déjà connecté via ce lien. Si ce n'est pas le cas, il y a eu une erreur (%@)."; @@ -2292,7 +2599,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Interface en japonais"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Rejoindre"; /* No comment provided by engineer. */ @@ -2322,6 +2629,9 @@ /* No comment provided by engineer. */ "Keep" = "Conserver"; +/* No comment provided by engineer. */ +"Keep conversation" = "Garder la conversation"; + /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Garder l'application ouverte pour l'utiliser depuis le bureau"; @@ -2343,7 +2653,7 @@ /* No comment provided by engineer. */ "Learn more" = "En savoir plus"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "Quitter"; /* No comment provided by engineer. */ @@ -2433,6 +2743,12 @@ /* No comment provided by engineer. */ "Max 30 seconds, received instantly." = "Max 30 secondes, réception immédiate."; +/* No comment provided by engineer. */ +"Media & file servers" = "Serveurs de fichiers et de médias"; + +/* blur media */ +"Medium" = "Modéré"; + /* member role */ "member" = "membre"; @@ -2445,6 +2761,9 @@ /* rcv group event chat item */ "member connected" = "est connecté·e"; +/* item status text */ +"Member inactive" = "Membre inactif"; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "Le rôle du membre sera changé pour \"%@\". Tous les membres du groupe en seront informés."; @@ -2454,15 +2773,33 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Ce membre sera retiré du groupe - impossible de revenir en arrière !"; +/* No comment provided by engineer. */ +"Menus" = "Menus"; + +/* No comment provided by engineer. */ +"message" = "message"; + /* item status text */ "Message delivery error" = "Erreur de distribution du message"; /* No comment provided by engineer. */ "Message delivery receipts!" = "Accusés de réception des messages !"; +/* item status text */ +"Message delivery warning" = "Avertissement sur la distribution des messages"; + /* No comment provided by engineer. */ "Message draft" = "Brouillon de message"; +/* item status text */ +"Message forwarded" = "Message transféré"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "Le message peut être transmis plus tard si le membre devient actif."; + +/* No comment provided by engineer. */ +"Message queue info" = "Informations sur la file d'attente des messages"; + /* chat feature */ "Message reactions" = "Réactions aux messages"; @@ -2475,9 +2812,21 @@ /* notification */ "message received" = "message reçu"; +/* No comment provided by engineer. */ +"Message reception" = "Réception de message"; + +/* No comment provided by engineer. */ +"Message servers" = "Serveurs de messages"; + /* No comment provided by engineer. */ "Message source remains private." = "La source du message reste privée."; +/* No comment provided by engineer. */ +"Message status" = "Statut du message"; + +/* copied message info */ +"Message status: %@" = "Statut du message : %@"; + /* No comment provided by engineer. */ "Message text" = "Texte du message"; @@ -2493,6 +2842,12 @@ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "Les messages de %@ seront affichés !"; +/* No comment provided by engineer. */ +"Messages received" = "Messages reçus"; + +/* No comment provided by engineer. */ +"Messages sent" = "Messages envoyés"; + /* No comment provided by engineer. */ "Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Les messages, fichiers et appels sont protégés par un chiffrement **de bout en bout** avec une confidentialité persistante, une répudiation et une récupération en cas d'effraction."; @@ -2568,19 +2923,19 @@ /* item status description */ "Most likely this connection is deleted." = "Connexion probablement supprimée."; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Il est fort probable que ce contact ait supprimé la connexion avec vous."; - /* No comment provided by engineer. */ "Multiple chat profiles" = "Différents profils de chat"; /* No comment provided by engineer. */ +"mute" = "muet"; + +/* swipe action */ "Mute" = "Muet"; /* No comment provided by engineer. */ "Muted when inactive!" = "Mute en cas d'inactivité !"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "Nom"; /* No comment provided by engineer. */ @@ -2589,6 +2944,9 @@ /* No comment provided by engineer. */ "Network connection" = "Connexion au réseau"; +/* snd error text */ +"Network issues - message expired after many attempts to send it." = "Problèmes de réseau - le message a expiré après plusieurs tentatives d'envoi."; + /* No comment provided by engineer. */ "Network management" = "Gestion du réseau"; @@ -2604,6 +2962,9 @@ /* No comment provided by engineer. */ "New chat" = "Nouveau chat"; +/* No comment provided by engineer. */ +"New chat experience 🎉" = "Nouvelle expérience de discussion 🎉"; + /* notification */ "New contact request" = "Nouvelle demande de contact"; @@ -2622,6 +2983,9 @@ /* No comment provided by engineer. */ "New in %@" = "Nouveautés de la %@"; +/* No comment provided by engineer. */ +"New media options" = "Nouvelles options de médias"; + /* No comment provided by engineer. */ "New member role" = "Nouveau rôle"; @@ -2658,6 +3022,9 @@ /* No comment provided by engineer. */ "No device token!" = "Pas de token d'appareil !"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Pas de connexion directe pour l'instant, le message est transmis par l'administrateur."; + /* No comment provided by engineer. */ "no e2e encryption" = "sans chiffrement de bout en bout"; @@ -2670,6 +3037,9 @@ /* No comment provided by engineer. */ "No history" = "Aucun historique"; +/* No comment provided by engineer. */ +"No info, try to reload" = "Pas d'info, essayez de recharger"; + /* No comment provided by engineer. */ "No network connection" = "Pas de connexion au réseau"; @@ -2685,6 +3055,9 @@ /* No comment provided by engineer. */ "Not compatible!" = "Non compatible !"; +/* No comment provided by engineer. */ +"Nothing selected" = "Aucune sélection"; + /* No comment provided by engineer. */ "Notifications" = "Notifications"; @@ -2702,7 +3075,7 @@ time to disappear */ "off" = "off"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Off"; /* feature offered item */ @@ -2730,10 +3103,10 @@ "One-time invitation link" = "Lien d'invitation unique"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "Les hôtes .onion seront nécessaires pour la connexion. Nécessite l'activation d'un VPN."; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Les hôtes .onion seront **nécessaires** pour la connexion.\nNécessite l'activation d'un VPN."; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "Les hôtes .onion seront utilisés dès que possible. Nécessite l'activation d'un VPN."; +"Onion hosts will be used when available.\nRequires compatible VPN." = "Les hôtes .onion seront utilisés dès que possible.\nNécessite l'activation d'un VPN."; /* No comment provided by engineer. */ "Onion hosts will not be used." = "Les hôtes .onion ne seront pas utilisés."; @@ -2741,6 +3114,9 @@ /* No comment provided by engineer. */ "Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Seuls les appareils clients stockent les profils des utilisateurs, les contacts, les groupes et les messages envoyés avec un **chiffrement de bout en bout à deux couches**."; +/* No comment provided by engineer. */ +"Only delete conversation" = "Ne supprimer que la conversation"; + /* No comment provided by engineer. */ "Only group owners can change group preferences." = "Seuls les propriétaires du groupe peuvent modifier les préférences du groupe."; @@ -2795,6 +3171,9 @@ /* authentication reason */ "Open migration to another device" = "Ouvrir le transfert vers un autre appareil"; +/* No comment provided by engineer. */ +"Open server settings" = "Ouvrir les paramètres du serveur"; + /* No comment provided by engineer. */ "Open Settings" = "Ouvrir les Paramètres"; @@ -2819,9 +3198,18 @@ /* No comment provided by engineer. */ "Or show this code" = "Ou présenter ce code"; +/* No comment provided by engineer. */ +"other" = "autre"; + /* No comment provided by engineer. */ "Other" = "Autres"; +/* No comment provided by engineer. */ +"Other %@ servers" = "Autres serveurs %@"; + +/* No comment provided by engineer. */ +"other errors" = "autres erreurs"; + /* member role */ "owner" = "propriétaire"; @@ -2864,6 +3252,9 @@ /* No comment provided by engineer. */ "peer-to-peer" = "pair-à-pair"; +/* No comment provided by engineer. */ +"Pending" = "En attente"; + /* No comment provided by engineer. */ "People can connect to you only via the links you share." = "On ne peut se connecter à vous qu’avec les liens que vous partagez."; @@ -2882,9 +3273,18 @@ /* No comment provided by engineer. */ "PING interval" = "Intervalle de PING"; +/* No comment provided by engineer. */ +"Play from the chat list." = "Aperçu depuis la liste de conversation."; + +/* No comment provided by engineer. */ +"Please ask your contact to enable calls." = "Veuillez demander à votre contact d'autoriser les appels."; + /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "Veuillez demander à votre contact de permettre l'envoi de messages vocaux."; +/* No comment provided by engineer. */ +"Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.\nPlease share any other issues with the developers." = "Veuillez vérifier que le téléphone portable et l'ordinateur de bureau sont connectés au même réseau local et que le pare-feu de l'ordinateur de bureau autorise la connexion.\nVeuillez faire part de tout autre problème aux développeurs."; + /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "Veuillez vérifier que vous avez utilisé le bon lien ou demandez à votre contact de vous en envoyer un autre."; @@ -2942,6 +3342,9 @@ /* No comment provided by engineer. */ "Preview" = "Aperçu"; +/* No comment provided by engineer. */ +"Previously connected servers" = "Serveurs précédemment connectés"; + /* No comment provided by engineer. */ "Privacy & security" = "Vie privée et sécurité"; @@ -2951,9 +3354,21 @@ /* No comment provided by engineer. */ "Private filenames" = "Noms de fichiers privés"; +/* No comment provided by engineer. */ +"Private message routing" = "Routage privé des messages"; + +/* No comment provided by engineer. */ +"Private message routing 🚀" = "Routage privé des messages 🚀"; + /* name of notes to self */ "Private notes" = "Notes privées"; +/* No comment provided by engineer. */ +"Private routing" = "Routage privé"; + +/* No comment provided by engineer. */ +"Private routing error" = "Erreur de routage privé"; + /* No comment provided by engineer. */ "Profile and server connections" = "Profil et connexions au serveur"; @@ -2972,6 +3387,9 @@ /* No comment provided by engineer. */ "Profile password" = "Mot de passe de profil"; +/* No comment provided by engineer. */ +"Profile theme" = "Thème de profil"; + /* No comment provided by engineer. */ "Profile update will be sent to your contacts." = "La mise à jour du profil sera envoyée à vos contacts."; @@ -3005,15 +3423,27 @@ /* No comment provided by engineer. */ "Protect app screen" = "Protéger l'écran de l'app"; +/* No comment provided by engineer. */ +"Protect IP address" = "Protéger l'adresse IP"; + /* No comment provided by engineer. */ "Protect your chat profiles with a password!" = "Protégez vos profils de chat par un mot de passe !"; +/* No comment provided by engineer. */ +"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Protégez votre adresse IP des relais de messagerie choisis par vos contacts.\nActivez-le dans les paramètres *Réseau et serveurs*."; + /* No comment provided by engineer. */ "Protocol timeout" = "Délai du protocole"; /* No comment provided by engineer. */ "Protocol timeout per KB" = "Délai d'attente du protocole par KB"; +/* No comment provided by engineer. */ +"Proxied" = "Routé via un proxy"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Serveurs routés via des proxy"; + /* No comment provided by engineer. */ "Push notifications" = "Notifications push"; @@ -3029,10 +3459,13 @@ /* No comment provided by engineer. */ "Rate the app" = "Évaluer l'app"; +/* No comment provided by engineer. */ +"Reachable chat toolbar" = "Barre d'outils accessible"; + /* chat item menu */ "React…" = "Réagissez…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "Lire"; /* No comment provided by engineer. */ @@ -3056,6 +3489,9 @@ /* No comment provided by engineer. */ "Receipts are disabled" = "Les accusés de réception sont désactivés"; +/* No comment provided by engineer. */ +"Receive errors" = "Erreurs reçues"; + /* No comment provided by engineer. */ "received answer…" = "réponse reçu…"; @@ -3075,10 +3511,16 @@ "Received message" = "Message reçu"; /* No comment provided by engineer. */ -"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "L'adresse de réception sera changée pour un autre serveur. Le changement d'adresse sera terminé lorsque l'expéditeur sera en ligne."; +"Received messages" = "Messages reçus"; /* No comment provided by engineer. */ -"Receiving concurrency" = "Réception simultanée"; +"Received reply" = "Réponse reçue"; + +/* No comment provided by engineer. */ +"Received total" = "Total reçu"; + +/* No comment provided by engineer. */ +"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "L'adresse de réception sera changée pour un autre serveur. Le changement d'adresse sera terminé lorsque l'expéditeur sera en ligne."; /* No comment provided by engineer. */ "Receiving file will be stopped." = "La réception du fichier sera interrompue."; @@ -3095,11 +3537,26 @@ /* No comment provided by engineer. */ "Recipients see updates as you type them." = "Les destinataires voient les mises à jour au fur et à mesure que vous leur écrivez."; +/* No comment provided by engineer. */ +"Reconnect" = "Reconnecter"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Reconnecter tous les serveurs connectés pour forcer la livraison des messages. Cette méthode utilise du trafic supplémentaire."; /* No comment provided by engineer. */ -"Reconnect servers?" = "Reconnecter les serveurs?"; +"Reconnect all servers" = "Reconnecter tous les serveurs"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "Reconnecter tous les serveurs ?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "Reconnecter le serveur pour forcer la livraison des messages. Utilise du trafic supplémentaire."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "Reconnecter le serveur ?"; + +/* No comment provided by engineer. */ +"Reconnect servers?" = "Reconnecter les serveurs ?"; /* No comment provided by engineer. */ "Record updated at" = "Enregistrement mis à jour le"; @@ -3110,7 +3567,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Réduction de la consommation de batterie"; -/* reject incoming call via notification */ +/* reject incoming call via notification + swipe action */ "Reject" = "Rejeter"; /* No comment provided by engineer. */ @@ -3131,6 +3589,9 @@ /* No comment provided by engineer. */ "Remove" = "Supprimer"; +/* No comment provided by engineer. */ +"Remove image" = "Enlever l'image"; + /* No comment provided by engineer. */ "Remove member" = "Retirer le membre"; @@ -3162,7 +3623,7 @@ "Renegotiate encryption" = "Renégocier le chiffrement"; /* No comment provided by engineer. */ -"Renegotiate encryption?" = "Renégocier le chiffrement?"; +"Renegotiate encryption?" = "Renégocier le chiffrement ?"; /* No comment provided by engineer. */ "Repeat connection request?" = "Répéter la demande de connexion ?"; @@ -3188,12 +3649,27 @@ /* No comment provided by engineer. */ "Reset" = "Réinitialisation"; +/* No comment provided by engineer. */ +"Reset all hints" = "Rétablir tous les conseils"; + +/* No comment provided by engineer. */ +"Reset all statistics" = "Réinitialiser toutes les statistiques"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "Réinitialiser toutes les statistiques ?"; + /* No comment provided by engineer. */ "Reset colors" = "Réinitialisation des couleurs"; +/* No comment provided by engineer. */ +"Reset to app theme" = "Réinitialisation au thème de l'appli"; + /* No comment provided by engineer. */ "Reset to defaults" = "Réinitialisation des valeurs par défaut"; +/* No comment provided by engineer. */ +"Reset to user theme" = "Réinitialisation au thème de l'utilisateur"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Redémarrez l'application pour créer un nouveau profil de chat"; @@ -3218,9 +3694,6 @@ /* chat item action */ "Reveal" = "Révéler"; -/* No comment provided by engineer. */ -"Revert" = "Revenir en arrière"; - /* No comment provided by engineer. */ "Revoke" = "Révoquer"; @@ -3236,6 +3709,9 @@ /* No comment provided by engineer. */ "Run chat" = "Exécuter le chat"; +/* No comment provided by engineer. */ +"Safely receive files" = "Réception de fichiers en toute sécurité"; + /* No comment provided by engineer. */ "Safer groups" = "Groupes plus sûrs"; @@ -3251,6 +3727,9 @@ /* No comment provided by engineer. */ "Save and notify group members" = "Enregistrer et en informer les membres du groupe"; +/* No comment provided by engineer. */ +"Save and reconnect" = "Sauvegarder et se reconnecter"; + /* No comment provided by engineer. */ "Save and update group profile" = "Enregistrer et mettre à jour le profil du groupe"; @@ -3305,6 +3784,12 @@ /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "Les serveurs WebRTC ICE sauvegardés seront supprimés"; +/* No comment provided by engineer. */ +"Scale" = "Échelle"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Scanner / Coller le lien"; + /* No comment provided by engineer. */ "Scan code" = "Scanner le code"; @@ -3320,6 +3805,9 @@ /* No comment provided by engineer. */ "Scan server QR code" = "Scanner un code QR de serveur"; +/* No comment provided by engineer. */ +"search" = "rechercher"; + /* No comment provided by engineer. */ "Search" = "Rechercher"; @@ -3332,6 +3820,9 @@ /* network option */ "sec" = "sec"; +/* No comment provided by engineer. */ +"Secondary" = "Secondaire"; + /* time unit */ "seconds" = "secondes"; @@ -3341,6 +3832,9 @@ /* server test step */ "Secure queue" = "File d'attente sécurisée"; +/* No comment provided by engineer. */ +"Secured" = "Sécurisées"; + /* No comment provided by engineer. */ "Security assessment" = "Évaluation de sécurité"; @@ -3350,9 +3844,15 @@ /* chat item text */ "security code changed" = "code de sécurité modifié"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "Choisir"; +/* No comment provided by engineer. */ +"Selected %lld" = "%lld sélectionné(s)"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Les préférences de chat sélectionnées interdisent ce message."; + /* No comment provided by engineer. */ "Self-destruct" = "Autodestruction"; @@ -3377,9 +3877,6 @@ /* No comment provided by engineer. */ "send direct message" = "envoyer un message direct"; -/* No comment provided by engineer. */ -"Send direct message" = "Envoyer un message direct"; - /* No comment provided by engineer. */ "Send direct message to connect" = "Envoyer un message direct pour vous connecter"; @@ -3387,11 +3884,23 @@ "Send disappearing message" = "Envoyer un message éphémère"; /* No comment provided by engineer. */ -"Send link previews" = "Envoi d'aperçus de liens"; +"Send errors" = "Erreurs d'envoi"; + +/* No comment provided by engineer. */ +"Send link previews" = "Aperçu des liens"; /* No comment provided by engineer. */ "Send live message" = "Envoyer un message dynamique"; +/* No comment provided by engineer. */ +"Send message to enable calls." = "Envoyer un message pour activer les appels."; + +/* No comment provided by engineer. */ +"Send messages directly when IP address is protected and your or destination server does not support private routing." = "Envoyer les messages de manière directe lorsque l'adresse IP est protégée et que votre serveur ou le serveur de destination ne prend pas en charge le routage privé."; + +/* No comment provided by engineer. */ +"Send messages directly when your or destination server does not support private routing." = "Envoyez les messages de manière directe lorsque votre serveur ou le serveur de destination ne prend pas en charge le routage privé."; + /* No comment provided by engineer. */ "Send notifications" = "Envoi de notifications"; @@ -3446,27 +3955,69 @@ /* copied message info */ "Sent at: %@" = "Envoyé le : %@"; +/* No comment provided by engineer. */ +"Sent directly" = "Envoyé directement"; + /* notification */ "Sent file event" = "Événement de fichier envoyé"; /* message info title */ "Sent message" = "Message envoyé"; +/* No comment provided by engineer. */ +"Sent messages" = "Messages envoyés"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "Les messages envoyés seront supprimés après une durée déterminée."; +/* No comment provided by engineer. */ +"Sent reply" = "Réponse envoyée"; + +/* No comment provided by engineer. */ +"Sent total" = "Total envoyé"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Envoyé via le proxy"; + +/* No comment provided by engineer. */ +"Server address" = "Adresse du serveur"; + +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "L'adresse du serveur est incompatible avec les paramètres réseau : %@."; + +/* srv error text. */ +"Server address is incompatible with network settings." = "L'adresse du serveur est incompatible avec les paramètres du réseau."; + +/* queue info */ +"server queue info: %@\n\nlast received msg: %@" = "info sur la file d'attente du serveur : %1$@\n\ndernier message reçu : %2$@"; + /* server test error */ "Server requires authorization to create queues, check password" = "Le serveur requiert une autorisation pour créer des files d'attente, vérifiez le mot de passe"; /* server test error */ -"Server requires authorization to upload, check password" = "Le serveur requiert une autorisation pour uploader, vérifiez le mot de passe"; +"Server requires authorization to upload, check password" = "Le serveur requiert une autorisation pour téléverser, vérifiez le mot de passe"; /* No comment provided by engineer. */ "Server test failed!" = "Échec du test du serveur !"; +/* No comment provided by engineer. */ +"Server type" = "Type de serveur"; + +/* srv error text */ +"Server version is incompatible with network settings." = "La version du serveur est incompatible avec les paramètres du réseau."; + +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "La version du serveur est incompatible avec votre appli : %@."; + /* No comment provided by engineer. */ "Servers" = "Serveurs"; +/* No comment provided by engineer. */ +"Servers info" = "Infos serveurs"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "Les statistiques des serveurs seront réinitialisées - il n'est pas possible de revenir en arrière !"; + /* No comment provided by engineer. */ "Session code" = "Code de session"; @@ -3476,6 +4027,9 @@ /* No comment provided by engineer. */ "Set contact name…" = "Définir le nom du contact…"; +/* No comment provided by engineer. */ +"Set default theme" = "Définir le thème par défaut"; + /* No comment provided by engineer. */ "Set group preferences" = "Définir les préférences du groupe"; @@ -3521,15 +4075,24 @@ /* No comment provided by engineer. */ "Share address with contacts?" = "Partager l'adresse avec vos contacts ?"; +/* No comment provided by engineer. */ +"Share from other apps." = "Partager depuis d'autres applications."; + /* No comment provided by engineer. */ "Share link" = "Partager le lien"; /* No comment provided by engineer. */ "Share this 1-time invite link" = "Partager ce lien d'invitation unique"; +/* No comment provided by engineer. */ +"Share to SimpleX" = "Partager sur SimpleX"; + /* No comment provided by engineer. */ "Share with contacts" = "Partager avec vos contacts"; +/* No comment provided by engineer. */ +"Show → on messages sent via private routing." = "Afficher → sur les messages envoyés via le routage privé."; + /* No comment provided by engineer. */ "Show calls in phone history" = "Afficher les appels dans l'historique du téléphone"; @@ -3537,10 +4100,16 @@ "Show developer options" = "Afficher les options pour les développeurs"; /* No comment provided by engineer. */ -"Show last messages" = "Voir les derniers messages"; +"Show last messages" = "Aperçu des derniers messages"; /* No comment provided by engineer. */ -"Show preview" = "Afficher l'aperçu"; +"Show message status" = "Afficher le statut du message"; + +/* No comment provided by engineer. */ +"Show percentage" = "Afficher le pourcentage"; + +/* No comment provided by engineer. */ +"Show preview" = "Aperçu affiché"; /* No comment provided by engineer. */ "Show QR code" = "Afficher le code QR"; @@ -3548,6 +4117,9 @@ /* No comment provided by engineer. */ "Show:" = "Afficher :"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "Adresse SimpleX"; @@ -3593,6 +4165,9 @@ /* No comment provided by engineer. */ "Simplified incognito mode" = "Mode incognito simplifié"; +/* No comment provided by engineer. */ +"Size" = "Taille"; + /* No comment provided by engineer. */ "Skip" = "Passer"; @@ -3603,11 +4178,20 @@ "Small groups (max 20)" = "Petits groupes (max 20)"; /* No comment provided by engineer. */ -"SMP servers" = "Serveurs SMP"; +"SMP server" = "Serveur SMP"; + +/* blur media */ +"Soft" = "Léger"; + +/* No comment provided by engineer. */ +"Some file(s) were not exported:" = "Certains fichiers n'ont pas été exportés :"; /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "Des erreurs non fatales se sont produites lors de l'importation - vous pouvez consulter la console de chat pour plus de détails."; +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import:" = "L'importation a entraîné des erreurs non fatales :"; + /* notification title */ "Somebody" = "Quelqu'un"; @@ -3626,9 +4210,15 @@ /* No comment provided by engineer. */ "Start migration" = "Démarrer la migration"; +/* No comment provided by engineer. */ +"Starting from %@." = "À partir de %@."; + /* No comment provided by engineer. */ "starting…" = "lancement…"; +/* No comment provided by engineer. */ +"Statistics" = "Statistiques"; + /* No comment provided by engineer. */ "Stop" = "Arrêter"; @@ -3668,9 +4258,21 @@ /* No comment provided by engineer. */ "strike" = "barré"; +/* blur media */ +"Strong" = "Fort"; + /* No comment provided by engineer. */ "Submit" = "Soumettre"; +/* No comment provided by engineer. */ +"Subscribed" = "Inscriptions"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Erreurs d'inscription"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Inscriptions ignorées"; + /* No comment provided by engineer. */ "Support SimpleX Chat" = "Supporter SimpleX Chat"; @@ -3705,7 +4307,7 @@ "Tap to scan" = "Appuyez pour scanner"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Appuyez ici pour démarrer une nouvelle discussion"; +"TCP connection" = "Connexion TCP"; /* No comment provided by engineer. */ "TCP connection timeout" = "Délai de connexion TCP"; @@ -3719,6 +4321,9 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* No comment provided by engineer. */ +"Temporary file error" = "Erreur de fichier temporaire"; + /* server test failure */ "Test failed at step %@." = "Échec du test à l'étape %@."; @@ -3746,6 +4351,9 @@ /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "L'application peut vous avertir lorsque vous recevez des messages ou des demandes de contact - veuillez ouvrir les paramètres pour les activer."; +/* No comment provided by engineer. */ +"The app will ask to confirm downloads from unknown file servers (except .onion)." = "L'application demandera de confirmer les téléchargements à partir de serveurs de fichiers inconnus (sauf .onion)."; + /* No comment provided by engineer. */ "The attempt to change database passphrase was not completed." = "La tentative de modification de la phrase secrète de la base de données n'a pas abouti."; @@ -3776,6 +4384,12 @@ /* No comment provided by engineer. */ "The message will be marked as moderated for all members." = "Le message sera marqué comme modéré pour tous les membres."; +/* No comment provided by engineer. */ +"The messages will be deleted for all members." = "Les messages seront supprimés pour tous les membres."; + +/* No comment provided by engineer. */ +"The messages will be marked as moderated for all members." = "Les messages seront marqués comme modérés pour tous les membres."; + /* No comment provided by engineer. */ "The next generation of private messaging" = "La nouvelle génération de messagerie privée"; @@ -3798,7 +4412,7 @@ "The text you pasted is not a SimpleX link." = "Le texte collé n'est pas un lien SimpleX."; /* No comment provided by engineer. */ -"Theme" = "Thème"; +"Themes" = "Thèmes"; /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Ces paramètres s'appliquent à votre profil actuel **%@**."; @@ -3842,9 +4456,15 @@ /* No comment provided by engineer. */ "This is your own SimpleX address!" = "Voici votre propre adresse SimpleX !"; +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "Ce lien a été utilisé avec un autre appareil mobile, veuillez créer un nouveau lien sur le bureau."; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Ce paramètre s'applique aux messages de votre profil de chat actuel **%@**."; +/* No comment provided by engineer. */ +"Title" = "Titre"; + /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Si vous avez des questions et que vous souhaitez des réponses :"; @@ -3866,6 +4486,9 @@ /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Pour protéger vos informations, activez la fonction SimpleX Lock.\nVous serez invité à confirmer l'authentification avant que cette fonction ne soit activée."; +/* No comment provided by engineer. */ +"To protect your IP address, private routing uses your SMP servers to deliver messages." = "Pour protéger votre adresse IP, le routage privé utilise vos serveurs SMP pour délivrer les messages."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Pour enregistrer un message vocal, veuillez accorder la permission d'utiliser le microphone."; @@ -3878,12 +4501,24 @@ /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Pour vérifier le chiffrement de bout en bout avec votre contact, comparez (ou scannez) le code sur vos appareils."; +/* No comment provided by engineer. */ +"Toggle chat list:" = "Afficher la liste des conversations :"; + /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Basculer en mode incognito lors de la connexion."; +/* No comment provided by engineer. */ +"Toolbar opacity" = "Opacité de la barre d'outils"; + +/* No comment provided by engineer. */ +"Total" = "Total"; + /* No comment provided by engineer. */ "Transport isolation" = "Transport isolé"; +/* No comment provided by engineer. */ +"Transport sessions" = "Sessions de transport"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Tentative de connexion au serveur utilisé pour recevoir les messages de ce contact (erreur : %@)."; @@ -3920,13 +4555,10 @@ /* rcv group event chat item */ "unblocked %@" = "%@ débloqué"; -/* item status description */ -"Unexpected error: %@" = "Erreur inattendue : %@"; - /* No comment provided by engineer. */ "Unexpected migration state" = "État de la migration inattendu"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "Unfav."; /* No comment provided by engineer. */ @@ -3953,6 +4585,12 @@ /* No comment provided by engineer. */ "Unknown error" = "Erreur inconnue"; +/* No comment provided by engineer. */ +"unknown servers" = "relais inconnus"; + +/* No comment provided by engineer. */ +"Unknown servers!" = "Serveurs inconnus !"; + /* No comment provided by engineer. */ "unknown status" = "statut inconnu"; @@ -3975,9 +4613,15 @@ "Unlock app" = "Déverrouiller l'app"; /* No comment provided by engineer. */ +"unmute" = "démuter"; + +/* swipe action */ "Unmute" = "Démute"; /* No comment provided by engineer. */ +"unprotected" = "non protégé"; + +/* swipe action */ "Unread" = "Non lu"; /* No comment provided by engineer. */ @@ -3986,9 +4630,6 @@ /* No comment provided by engineer. */ "Update" = "Mise à jour"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = "Mettre à jour le paramètre des hôtes .onion ?"; - /* No comment provided by engineer. */ "Update database passphrase" = "Mise à jour de la phrase secrète de la base de données"; @@ -3996,7 +4637,7 @@ "Update network settings?" = "Mettre à jour les paramètres réseau ?"; /* No comment provided by engineer. */ -"Update transport isolation mode?" = "Mettre à jour le mode d'isolement du transport ?"; +"Update settings?" = "Mettre à jour les paramètres ?"; /* rcv group event chat item */ "updated group profile" = "mise à jour du profil de groupe"; @@ -4008,16 +4649,22 @@ "Updating settings will re-connect the client to all servers." = "La mise à jour des ces paramètres reconnectera le client à tous les serveurs."; /* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "La mise à jour de ce paramètre reconnectera le client à tous les serveurs."; +"Upgrade and open chat" = "Mettre à niveau et ouvrir le chat"; /* No comment provided by engineer. */ -"Upgrade and open chat" = "Mettre à niveau et ouvrir le chat"; +"Upload errors" = "Erreurs de téléversement"; /* No comment provided by engineer. */ "Upload failed" = "Échec de l'envoi"; /* server test step */ -"Upload file" = "Transférer le fichier"; +"Upload file" = "Téléverser le fichier"; + +/* No comment provided by engineer. */ +"Uploaded" = "Téléversé"; + +/* No comment provided by engineer. */ +"Uploaded files" = "Fichiers téléversés"; /* No comment provided by engineer. */ "Uploading archive" = "Envoi de l'archive"; @@ -4046,6 +4693,12 @@ /* No comment provided by engineer. */ "Use only local notifications?" = "Utilisation de notifications locales uniquement ?"; +/* No comment provided by engineer. */ +"Use private routing with unknown servers when IP address is not protected." = "Utiliser le routage privé avec des serveurs inconnus lorsque l'adresse IP n'est pas protégée."; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers." = "Utiliser le routage privé avec des serveurs inconnus."; + /* No comment provided by engineer. */ "Use server" = "Utiliser ce serveur"; @@ -4055,11 +4708,14 @@ /* No comment provided by engineer. */ "Use the app while in the call." = "Utiliser l'application pendant l'appel."; +/* No comment provided by engineer. */ +"Use the app with one hand." = "Utiliser l'application d'une main."; + /* No comment provided by engineer. */ "User profile" = "Profil d'utilisateur"; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "L'utilisation des hôtes .onion nécessite un fournisseur VPN compatible."; +"User selection" = "Sélection de l'utilisateur"; /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "Vous utilisez les serveurs SimpleX."; @@ -4109,6 +4765,9 @@ /* No comment provided by engineer. */ "Via secure quantum resistant protocol." = "Via un protocole sécurisé de cryptographie post-quantique."; +/* No comment provided by engineer. */ +"video" = "vidéo"; + /* No comment provided by engineer. */ "Video call" = "Appel vidéo"; @@ -4116,7 +4775,7 @@ "video call (not e2e encrypted)" = "appel vidéo (sans chiffrement)"; /* No comment provided by engineer. */ -"Video will be received when your contact completes uploading it." = "La vidéo ne sera reçue que lorsque votre contact aura fini de la transférer."; +"Video will be received when your contact completes uploading it." = "La vidéo ne sera reçue que lorsque votre contact aura fini la mettre en ligne."; /* No comment provided by engineer. */ "Video will be received when your contact is online, please wait or check later!" = "La vidéo ne sera reçue que lorsque votre contact sera en ligne. Veuillez patienter ou vérifier plus tard !"; @@ -4166,11 +4825,17 @@ /* No comment provided by engineer. */ "Waiting for video" = "En attente de la vidéo"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "Accentuation du papier-peint"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Fond d'écran"; + /* No comment provided by engineer. */ "wants to connect to you!" = "veut établir une connexion !"; /* No comment provided by engineer. */ -"Warning: starting chat on multiple devices is not supported and will cause message delivery failures" = "Attention: démarrer une session de chat sur plusieurs appareils n'est pas pris en charge et entraînera des dysfonctionnements au niveau de la transmission des messages"; +"Warning: starting chat on multiple devices is not supported and will cause message delivery failures" = "Attention : démarrer une session de chat sur plusieurs appareils n'est pas pris en charge et entraînera des dysfonctionnements au niveau de la transmission des messages"; /* No comment provided by engineer. */ "Warning: you may lose some data!" = "Attention : vous risquez de perdre des données !"; @@ -4199,6 +4864,9 @@ /* No comment provided by engineer. */ "When connecting audio and video calls." = "Lors des appels audio et vidéo."; +/* No comment provided by engineer. */ +"when IP hidden" = "lorsque l'IP est masquée"; + /* No comment provided by engineer. */ "When people request to connect, you can accept or reject it." = "Vous pouvez accepter ou refuser les demandes de contacts."; @@ -4223,14 +4891,26 @@ /* No comment provided by engineer. */ "With reduced battery usage." = "Consommation réduite de la batterie."; +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to file servers." = "Sans Tor ou un VPN, votre adresse IP sera visible par les serveurs de fichiers."; + +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Sans Tor ni VPN, votre adresse IP sera visible par ces relais XFTP : %@."; + /* No comment provided by engineer. */ "Wrong database passphrase" = "Mauvaise phrase secrète pour la base de données"; +/* snd error text */ +"Wrong key or unknown connection - most likely this connection is deleted." = "Clé erronée ou connexion non identifiée - il est très probable que cette connexion soit supprimée."; + +/* file error text */ +"Wrong key or unknown file chunk address - most likely file is deleted." = "Mauvaise clé ou adresse inconnue du bloc de données du fichier - le fichier est probablement supprimé."; + /* No comment provided by engineer. */ "Wrong passphrase!" = "Mauvaise phrase secrète !"; /* No comment provided by engineer. */ -"XFTP servers" = "Serveurs XFTP"; +"XFTP server" = "Serveur XFTP"; /* pref value */ "yes" = "oui"; @@ -4286,6 +4966,9 @@ /* No comment provided by engineer. */ "You are invited to group" = "Vous êtes invité·e au groupe"; +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "Vous n'êtes pas connecté à ces serveurs. Le routage privé est utilisé pour leur délivrer des messages."; + /* No comment provided by engineer. */ "you are observer" = "vous êtes observateur"; @@ -4295,6 +4978,9 @@ /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "Vous pouvez accepter des appels à partir de l'écran de verrouillage, sans authentification de l'appareil ou de l'application."; +/* No comment provided by engineer. */ +"You can change it in Appearance settings." = "Vous pouvez choisir de le modifier dans les paramètres d'apparence."; + /* No comment provided by engineer. */ "You can create it later" = "Vous pouvez la créer plus tard"; @@ -4314,7 +5000,10 @@ "You can make it visible to your SimpleX contacts via Settings." = "Vous pouvez le rendre visible à vos contacts SimpleX via Paramètres."; /* notification body */ -"You can now send messages to %@" = "Vous pouvez maintenant envoyer des messages à %@"; +"You can now chat with %@" = "Vous pouvez maintenant envoyer des messages à %@"; + +/* No comment provided by engineer. */ +"You can send messages to %@ from Archived contacts." = "Vous pouvez envoyer des messages à %@ à partir des contacts archivés."; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Vous pouvez configurer l'aperçu des notifications sur l'écran de verrouillage via les paramètres."; @@ -4331,6 +5020,9 @@ /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "Vous pouvez lancer le chat via Paramètres / Base de données ou en redémarrant l'app"; +/* No comment provided by engineer. */ +"You can still view conversation with %@ in the list of chats." = "Vous pouvez toujours voir la conversation avec %@ dans la liste des discussions."; + /* No comment provided by engineer. */ "You can turn on SimpleX Lock via Settings." = "Vous pouvez activer SimpleX Lock dans les Paramètres."; @@ -4367,9 +5059,6 @@ /* No comment provided by engineer. */ "You have already requested connection!\nRepeat connection request?" = "Vous avez déjà demandé une connexion !\nRépéter la demande de connexion ?"; -/* No comment provided by engineer. */ -"You have no chats" = "Vous n'avez aucune discussion"; - /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "Vous devez saisir la phrase secrète à chaque fois que l'application démarre - elle n'est pas stockée sur l'appareil."; @@ -4385,9 +5074,18 @@ /* snd group event chat item */ "you left" = "vous avez quitté"; +/* No comment provided by engineer. */ +"You may migrate the exported database." = "Vous pouvez migrer la base de données exportée."; + +/* No comment provided by engineer. */ +"You may save the exported archive." = "Vous pouvez enregistrer l'archive exportée."; + /* No comment provided by engineer. */ "You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." = "Vous devez utiliser la version la plus récente de votre base de données de chat sur un seul appareil UNIQUEMENT, sinon vous risquez de ne plus recevoir les messages de certains contacts."; +/* No comment provided by engineer. */ +"You need to allow your contact to call to be able to call them." = "Vous devez autoriser votre contact à appeler pour pouvoir l'appeler."; + /* No comment provided by engineer. */ "You need to allow your contact to send voice messages to be able to send them." = "Vous devez autoriser votre contact à envoyer des messages vocaux pour pouvoir en envoyer."; @@ -4460,9 +5158,6 @@ /* No comment provided by engineer. */ "Your chat profiles" = "Vos profils de chat"; -/* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Votre contact a besoin d'être en ligne pour completer la connexion.\nVous pouvez annuler la connexion et supprimer le contact (et réessayer plus tard avec un autre lien)."; - /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Votre contact a envoyé un fichier plus grand que la taille maximale supportée actuellement(%@)."; diff --git a/apps/ios/hu.lproj/Localizable.strings b/apps/ios/hu.lproj/Localizable.strings index b590785606..8c70bcd626 100644 --- a/apps/ios/hu.lproj/Localizable.strings +++ b/apps/ios/hu.lproj/Localizable.strings @@ -68,7 +68,7 @@ "**Add contact**: to create a new invitation link, or connect via a link you received." = "**Ismerős hozzáadása**: új meghívó hivatkozás létrehozásához, vagy egy kapott hivatkozáson keresztül történő kapcsolódáshoz."; /* No comment provided by engineer. */ -"**Add new contact**: to create your one-time QR Code for your contact." = "**Új ismerős hozzáadása**: egyszer használatos QR-kód vagy hivatkozás létrehozása a kapcsolattartóhoz."; +"**Add new contact**: to create your one-time QR Code for your contact." = "**Új ismerős hozzáadása**: egyszer használatos QR-kód vagy hivatkozás létrehozása az ismerőse számára."; /* No comment provided by engineer. */ "**Create group**: to create a new group." = "**Csoport létrehozása**: új csoport létrehozásához."; @@ -83,19 +83,19 @@ "**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." = "**Privátabb**: 20 percenként ellenőrzi az új üzeneteket. Az eszköztoken megosztásra kerül a SimpleX Chat kiszolgálóval, de az nem, hogy hány ismerőse vagy üzenete van."; /* No comment provided by engineer. */ -"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Legprivátabb**: ne használja a SimpleX Chat értesítési szervert, rendszeresen ellenőrizze az üzeneteket a háttérben (attól függően, hogy milyen gyakran használja az alkalmazást)."; +"**Most private**: do not use SimpleX Chat notifications server, check messages periodically in the background (depends on how often you use the app)." = "**Legprivátabb**: ne használja a SimpleX Chat értesítési kiszolgálót, rendszeresen ellenőrizze az üzeneteket a háttérben (attól függően, hogy milyen gyakran használja az alkalmazást)."; /* No comment provided by engineer. */ -"**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Megjegyzés**: ha két eszközön is ugyanazt az adatbázist használja, akkor biztonsági védelemként megszakítja a kapcsolataiból érkező üzenetek visszafejtését."; +"**Please note**: using the same database on two devices will break the decryption of messages from your connections, as a security protection." = "**Megjegyzés**: ha két eszközön is ugyanazt az adatbázist használja, akkor biztonsági védelemként megszakítja az ismerőseitől érkező üzenetek visszafejtését."; /* No comment provided by engineer. */ "**Please note**: you will NOT be able to recover or change passphrase if you lose it." = "**Figyelem**: NEM tudja visszaállítani vagy megváltoztatni jelmondatát, ha elveszíti azt."; /* No comment provided by engineer. */ -"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Javasolt**: az eszköztoken és az értesítések elküldésre kerülnek a SimpleX Chat értesítési szerverre, kivéve az üzenet tartalma, mérete vagy az, hogy kitől származik."; +"**Recommended**: device token and notifications are sent to SimpleX Chat notification server, but not the message content, size or who it is from." = "**Javasolt**: az eszköztoken és az értesítések elküldésre kerülnek a SimpleX Chat értesítési kiszolgálóra, kivéve az üzenet tartalma, mérete vagy az, hogy kitől származik."; /* No comment provided by engineer. */ -"**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Figyelmeztetés**: Az azonnali push-értesítésekhez a kulcstárolóban tárolt jelmondat megadása szükséges."; +"**Warning**: Instant push notifications require passphrase saved in Keychain." = "**Figyelmeztetés**: Az azonnali push-értesítésekhez a kulcstartóban tárolt jelmondat megadása szükséges."; /* No comment provided by engineer. */ "**Warning**: the archive will be removed." = "**Figyelem**: az archívum törlésre kerül."; @@ -154,9 +154,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ ellenőrizve"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ kiszolgáló"; - /* No comment provided by engineer. */ "%@ uploaded" = "%@ feltöltve"; @@ -212,10 +209,10 @@ "%lld members" = "%lld tag"; /* No comment provided by engineer. */ -"%lld messages blocked" = "%lld üzenet blokkolva"; +"%lld messages blocked" = "%lld üzenet letiltva"; /* No comment provided by engineer. */ -"%lld messages blocked by admin" = "%lld üzenet blokkolva az admin által"; +"%lld messages blocked by admin" = "%lld üzenet letiltva az admin által"; /* No comment provided by engineer. */ "%lld messages marked deleted" = "%lld törlésre megjelölt üzenet"; @@ -266,7 +263,7 @@ "`a + b`" = "a + b"; /* email text */ -"

Hi!

\n

Connect to me via SimpleX Chat

" = "

Üdvözlöm!

\n

Csatlakozzon hozzám a SimpleX Chaten

"; +"

Hi!

\n

Connect to me via SimpleX Chat

" = "

Üdvözlöm!

\n

Csatlakozzon hozzám a SimpleX Chaten

"; /* No comment provided by engineer. */ "~strike~" = "\\~áthúzott~"; @@ -326,38 +323,49 @@ "Abort changing address?" = "Címváltoztatás megszakítása??"; /* No comment provided by engineer. */ -"About SimpleX" = "A SimpleX névjegye"; +"About SimpleX" = "A SimpleX-ről"; /* No comment provided by engineer. */ -"About SimpleX address" = "A SimpleX azonosítóról"; +"About SimpleX address" = "A SimpleX címről"; /* No comment provided by engineer. */ -"About SimpleX Chat" = "A SimpleX Chat névjegye"; +"About SimpleX Chat" = "A SimpleX Chat-ről"; /* No comment provided by engineer. */ "above, then choose:" = "gombra fent, majd válassza ki:"; /* No comment provided by engineer. */ -"Accent color" = "Kiemelő szín"; +"Accent" = "Kiemelés"; /* accept contact request via notification - accept incoming call via notification */ + accept incoming call via notification + swipe action */ "Accept" = "Elfogadás"; /* No comment provided by engineer. */ -"Accept connection request?" = "Kapcsolatfelvétel elfogadása?"; +"Accept connection request?" = "Kapcsolódási kérelem elfogadása?"; /* notification body */ "Accept contact request from %@?" = "Elfogadja %@ kapcsolat kérését?"; -/* accept contact request via notification */ +/* accept contact request via notification + swipe action */ "Accept incognito" = "Fogadás inkognítóban"; /* call status */ "accepted call" = "elfogadott hívás"; /* No comment provided by engineer. */ -"Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Azonosító hozzáadása a profilhoz, hogy az ismerősei megoszthassák másokkal. A profilfrissítés elküldésre kerül az ismerősei számára."; +"Acknowledged" = "Nyugtázva"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Nyugtázott hibák"; + +/* No comment provided by engineer. */ +"Active connections" = "Aktív kapcsolatok száma"; + +/* No comment provided by engineer. */ +"Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Cím hozzáadása a profilhoz, hogy az ismerősei megoszthassák másokkal. A profilfrissítés elküldésre kerül az ismerősei számára."; /* No comment provided by engineer. */ "Add contact" = "Ismerős hozzáadása"; @@ -369,7 +377,7 @@ "Add profile" = "Profil hozzáadása"; /* No comment provided by engineer. */ -"Add server…" = "Kiszolgáló hozzáadása…"; +"Add server" = "Kiszolgáló hozzáadása"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Kiszolgáló hozzáadása QR-kód beolvasásával."; @@ -380,6 +388,15 @@ /* No comment provided by engineer. */ "Add welcome message" = "Üdvözlő üzenet hozzáadása"; +/* No comment provided by engineer. */ +"Additional accent" = "További kiemelés"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "További kiemelés 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "További másodlagos"; + /* No comment provided by engineer. */ "Address" = "Cím"; @@ -401,6 +418,9 @@ /* No comment provided by engineer. */ "Advanced network settings" = "Speciális hálózati beállítások"; +/* No comment provided by engineer. */ +"Advanced settings" = "Haladó beállítások"; + /* chat item text */ "agreeing encryption for %@…" = "titkosítás jóváhagyása %@ számára…"; @@ -416,6 +436,9 @@ /* No comment provided by engineer. */ "All data is erased when it is entered." = "A jelkód megadása után minden adat törlésre kerül."; +/* No comment provided by engineer. */ +"All data is private to your device." = "Minden adat biztonságban van a készülékén."; + /* No comment provided by engineer. */ "All group members will remain connected." = "Minden csoporttag kapcsolódva marad."; @@ -431,6 +454,9 @@ /* No comment provided by engineer. */ "All new messages from %@ will be hidden!" = "Minden új üzenet elrejtésre kerül tőle: %@!"; +/* No comment provided by engineer. */ +"All profiles" = "Minden profil"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Minden ismerős kapcsolódva marad."; @@ -444,25 +470,34 @@ "Allow" = "Engedélyezés"; /* No comment provided by engineer. */ -"Allow calls only if your contact allows them." = "Hívások engedélyezése kizárólag abban az esetben, ha ismerőse is engedélyezi."; +"Allow calls only if your contact allows them." = "A hívások kezdeményezése kizárólag abban az esetben van engedélyezve, ha az ismerőse is engedélyezi."; /* No comment provided by engineer. */ -"Allow disappearing messages only if your contact allows it to you." = "Eltűnő üzenetek engedélyezése kizárólag abban az esetben, ha ismerőse is engedélyezi az ön számára."; +"Allow calls?" = "Hívások engedélyezése?"; /* No comment provided by engineer. */ -"Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Üzenet végleges törlésének engedélyezése kizárólag abban az esetben, ha ismerőse is engedélyezi. (24 óra)"; +"Allow disappearing messages only if your contact allows it to you." = "Az eltűnő üzenetek küldése kizárólag abban az esetben van engedélyezve, ha az ismerőse is engedélyezi az ön számára."; /* No comment provided by engineer. */ -"Allow message reactions only if your contact allows them." = "Üzenetreakciók engedélyezése kizárólag abban az esetben, ha ismerőse is engedélyezi."; +"Allow downgrade" = "Visszafejlesztés engedélyezése"; + +/* No comment provided by engineer. */ +"Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Az üzenetek végleges törlése kizárólag abban az esetben van engedélyezve, ha az ismerőse is engedélyezi. (24 óra)"; + +/* No comment provided by engineer. */ +"Allow message reactions only if your contact allows them." = "Az üzenetreakciók küldése kizárólag abban az esetben van engedélyezve, ha az ismerőse is engedélyezi."; /* No comment provided by engineer. */ "Allow message reactions." = "Üzenetreakciók engedélyezése."; /* No comment provided by engineer. */ -"Allow sending direct messages to members." = "Közvetlen üzenetek küldésének engedélyezése a tagok számára."; +"Allow sending direct messages to members." = "A közvetlen üzenetek küldése a tagok között engedélyezve van."; /* No comment provided by engineer. */ -"Allow sending disappearing messages." = "Eltűnő üzenetek küldésének engedélyezése."; +"Allow sending disappearing messages." = "Az eltűnő üzenetek küldése engedélyezve van."; + +/* No comment provided by engineer. */ +"Allow sharing" = "Megosztás engedélyezése"; /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Elküldött üzenetek végleges törlésének engedélyezése. (24 óra)"; @@ -477,25 +512,25 @@ "Allow to send voice messages." = "Hangüzenetek küldésének engedélyezése."; /* No comment provided by engineer. */ -"Allow voice messages only if your contact allows them." = "Hangüzenetek küldésének engedélyezése kizárólag abban az esetben, ha ismerőse is engedélyezi."; +"Allow voice messages only if your contact allows them." = "A hangüzenetek küldése kizárólag abban az esetben van engedélyezve, ha az ismerőse is engedélyezi."; /* No comment provided by engineer. */ "Allow voice messages?" = "Hangüzenetek engedélyezése?"; /* No comment provided by engineer. */ -"Allow your contacts adding message reactions." = "Ismerősök általi üzenetreakciók hozzáadásának engedélyezése."; +"Allow your contacts adding message reactions." = "Az üzenetreakciók küldése engedélyezve van az ismerősei számára."; /* No comment provided by engineer. */ -"Allow your contacts to call you." = "Hívások engedélyezése ismerősök számára."; +"Allow your contacts to call you." = "A hívások kezdeményezése engedélyezve van az ismerősei számára."; /* No comment provided by engineer. */ -"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "Elküldött üzenetek végleges törlésének engedélyezése az ismerősök számára. (24 óra)"; +"Allow your contacts to irreversibly delete sent messages. (24 hours)" = "Az elküldött üzenetek végleges törlése engedélyezve van az ismerősei számára. (24 óra)"; /* No comment provided by engineer. */ -"Allow your contacts to send disappearing messages." = "Eltűnő üzenetek engedélyezése ismerősök számára."; +"Allow your contacts to send disappearing messages." = "Az eltűnő üzenetek küldésének engedélyezése az ismerősei számára."; /* No comment provided by engineer. */ -"Allow your contacts to send voice messages." = "Hangüzenetek küldésének engedélyezése ismerősök számára."; +"Allow your contacts to send voice messages." = "A hangüzenetek küldése engedélyezve van az ismerősei számára."; /* No comment provided by engineer. */ "Already connected?" = "Már kapcsolódott?"; @@ -509,6 +544,9 @@ /* pref value */ "always" = "mindig"; +/* No comment provided by engineer. */ +"Always use private routing." = "Mindig használjon privát útválasztást."; + /* No comment provided by engineer. */ "Always use relay" = "Mindig használjon átjátszó kiszolgálót"; @@ -516,7 +554,7 @@ "An empty chat profile with the provided name is created, and the app opens as usual." = "Egy üres csevegési profil jön létre a megadott névvel, és az alkalmazás a szokásos módon megnyílik."; /* No comment provided by engineer. */ -"and %lld other events" = "és %lld további esemény"; +"and %lld other events" = "és további %lld esemény"; /* No comment provided by engineer. */ "Answer call" = "Hívás fogadása"; @@ -551,15 +589,27 @@ /* No comment provided by engineer. */ "Apply" = "Alkalmaz"; +/* No comment provided by engineer. */ +"Apply to" = "Alkalmazás erre"; + /* No comment provided by engineer. */ "Archive and upload" = "Archiválás és feltöltés"; +/* No comment provided by engineer. */ +"Archive contacts to chat later." = "Ismerősök archiválása a későbbi csevegéshez."; + +/* No comment provided by engineer. */ +"Archived contacts" = "Archivált ismerősök"; + /* No comment provided by engineer. */ "Archiving database" = "Adatbázis archiválása"; /* No comment provided by engineer. */ "Attach" = "Csatolás"; +/* No comment provided by engineer. */ +"attempts" = "próbálkozások"; + /* No comment provided by engineer. */ "Audio & video calls" = "Hang- és videóhívások"; @@ -573,10 +623,10 @@ "Audio/video calls" = "Hang-/videóhívások"; /* No comment provided by engineer. */ -"Audio/video calls are prohibited." = "A hang- és videóhívások le vannak tiltva."; +"Audio/video calls are prohibited." = "A hívások kezdeményezése le van tiltva ebben a csevegésben."; /* PIN entry */ -"Authentication cancelled" = "Hitelesítés megszakítva"; +"Authentication cancelled" = "Hitelesítés visszavonva"; /* No comment provided by engineer. */ "Authentication failed" = "Sikertelen hitelesítés"; @@ -594,22 +644,25 @@ "Auto-accept" = "Automatikus elfogadás"; /* No comment provided by engineer. */ -"Auto-accept contact requests" = "Ismerős jelölések automatikus elfogadása"; +"Auto-accept contact requests" = "Kapcsolódási kérelmek automatikus elfogadása"; /* No comment provided by engineer. */ -"Auto-accept images" = "Fotók automatikus elfogadása"; +"Auto-accept images" = "Képek automatikus elfogadása"; /* No comment provided by engineer. */ "Back" = "Vissza"; /* No comment provided by engineer. */ -"Bad desktop address" = "Hibás számítógép azonosító"; - -/* integrity error chat item */ -"bad message hash" = "téves üzenet hash"; +"Background" = "Háttér"; /* No comment provided by engineer. */ -"Bad message hash" = "Téves üzenet hash"; +"Bad desktop address" = "Hibás számítógép cím"; + +/* integrity error chat item */ +"bad message hash" = "hibás az üzenet ellenőrzőösszege"; + +/* No comment provided by engineer. */ +"Bad message hash" = "Hibás az üzenet ellenőrzőösszege"; /* integrity error chat item */ "bad message ID" = "téves üzenet ID"; @@ -624,28 +677,34 @@ "Better messages" = "Jobb üzenetek"; /* No comment provided by engineer. */ -"Block" = "Blokkolás"; +"Better networking" = "Jobb hálózatkezelés"; /* No comment provided by engineer. */ -"Block for all" = "Mindenki számára letiltva"; +"Black" = "Fekete"; /* No comment provided by engineer. */ -"Block group members" = "Csoporttagok blokkolása"; +"Block" = "Letiltás"; /* No comment provided by engineer. */ -"Block member" = "Tag blokkolása"; +"Block for all" = "Letiltás mindenki számára"; /* No comment provided by engineer. */ -"Block member for all?" = "Tag letiltása mindenki számára?"; +"Block group members" = "Csoporttagok letiltása"; /* No comment provided by engineer. */ -"Block member?" = "Tag blokkolása?"; +"Block member" = "Tag letiltása"; + +/* No comment provided by engineer. */ +"Block member for all?" = "Mindenki számára letiltja ezt a tagot?"; + +/* No comment provided by engineer. */ +"Block member?" = "Tag letiltása?"; /* marked deleted chat item preview text */ -"blocked" = "blokkolva"; +"blocked" = "letiltva"; /* rcv group event chat item */ -"blocked %@" = "%@ letiltva"; +"blocked %@" = "letiltotta %@-t"; /* marked deleted chat item preview text */ "blocked by admin" = "letiltva az admin által"; @@ -653,6 +712,12 @@ /* No comment provided by engineer. */ "Blocked by admin" = "Letiltva az admin által"; +/* No comment provided by engineer. */ +"Blur for better privacy." = "Elhomályosítás a jobb adatvédelemért."; + +/* No comment provided by engineer. */ +"Blur media" = "Média elhomályosítása"; + /* No comment provided by engineer. */ "bold" = "félkövér"; @@ -663,7 +728,7 @@ "Both you and your contact can irreversibly delete sent messages. (24 hours)" = "Mindkét fél törölheti véglegesen az elküldött üzeneteket. (24 óra)"; /* No comment provided by engineer. */ -"Both you and your contact can make calls." = "Mindkét fél tud hívásokat indítani."; +"Both you and your contact can make calls." = "Mindkét fél tud hívásokat kezdeményezni."; /* No comment provided by engineer. */ "Both you and your contact can send disappearing messages." = "Mindkét fél küldhet eltűnő üzeneteket."; @@ -677,6 +742,9 @@ /* No comment provided by engineer. */ "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Csevegési profil (alapértelmezett) vagy [kapcsolat alapján] (https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BÉTA)."; +/* No comment provided by engineer. */ +"call" = "hívás"; + /* No comment provided by engineer. */ "Call already ended!" = "A hívás már befejeződött!"; @@ -693,7 +761,16 @@ "Calls" = "Hívások"; /* No comment provided by engineer. */ -"Camera not available" = "A fényképező nem elérhető"; +"Calls prohibited!" = "A hívások le vannak tiltva!"; + +/* No comment provided by engineer. */ +"Camera not available" = "A kamera nem elérhető"; + +/* No comment provided by engineer. */ +"Can't call contact" = "Nem lehet felhívni az ismerőst"; + +/* No comment provided by engineer. */ +"Can't call member" = "Nem lehet felhívni a tagot"; /* No comment provided by engineer. */ "Can't invite contact!" = "Ismerősök meghívása le van tiltva!"; @@ -701,6 +778,9 @@ /* No comment provided by engineer. */ "Can't invite contacts!" = "Ismerősök meghívása nem lehetséges!"; +/* No comment provided by engineer. */ +"Can't message member" = "Nem lehet üzenetet küldeni a tagnak"; + /* No comment provided by engineer. */ "Cancel" = "Mégse"; @@ -708,14 +788,20 @@ "Cancel migration" = "Átköltöztetés visszavonása"; /* feature offered item */ -"cancelled %@" = "%@ törölve"; +"cancelled %@" = "%@ visszavonva"; /* No comment provided by engineer. */ "Cannot access keychain to save database password" = "Nem lehet hozzáférni a kulcstartóhoz az adatbázis jelszavának mentéséhez"; +/* No comment provided by engineer. */ +"Cannot forward message" = "Nem lehet továbbítani az üzenetet"; + /* No comment provided by engineer. */ "Cannot receive file" = "Nem lehet fogadni a fájlt"; +/* snd error text */ +"Capacity exceeded - recipient did not receive previously sent messages." = "Kapacitás túllépés - a címzett nem kapta meg a korábban elküldött üzeneteket."; + /* No comment provided by engineer. */ "Cellular" = "Mobilhálózat"; @@ -760,14 +846,17 @@ "changed your role to %@" = "megváltoztatta a szerepkörét erre: %@"; /* chat item text */ -"changing address for %@…" = "cím módosítása %@ számára…"; +"changing address for %@…" = "cím megváltoztatása nála: %@…"; /* chat item text */ -"changing address…" = "azonosító megváltoztatása…"; +"changing address…" = "cím megváltoztatása…"; /* No comment provided by engineer. */ "Chat archive" = "Csevegési archívum"; +/* No comment provided by engineer. */ +"Chat colors" = "Csevegés színei"; + /* No comment provided by engineer. */ "Chat console" = "Csevegési konzol"; @@ -777,6 +866,9 @@ /* No comment provided by engineer. */ "Chat database deleted" = "Csevegési adatbázis törölve"; +/* No comment provided by engineer. */ +"Chat database exported" = "Csevegési adatbázis exportálva"; + /* No comment provided by engineer. */ "Chat database imported" = "Csevegési adatbázis importálva"; @@ -789,12 +881,18 @@ /* No comment provided by engineer. */ "Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." = "A csevegés leállt. Ha már használta ezt az adatbázist egy másik eszközön, úgy visszaállítás szükséges a csevegés megkezdése előtt."; +/* No comment provided by engineer. */ +"Chat list" = "Csevegőlista"; + /* No comment provided by engineer. */ "Chat migrated!" = "A csevegés átköltöztetve!"; /* No comment provided by engineer. */ "Chat preferences" = "Csevegési beállítások"; +/* No comment provided by engineer. */ +"Chat theme" = "Csevegés témája"; + /* No comment provided by engineer. */ "Chats" = "Csevegések"; @@ -805,7 +903,7 @@ "Chinese and Spanish interface" = "Kínai és spanyol kezelőfelület"; /* No comment provided by engineer. */ -"Choose _Migrate from another device_ on the new device and scan QR code." = "Válassza az _Átköltöztetés egy másik eszközről_ opciót az új eszközön és szkennelje be a QR-kódot."; +"Choose _Migrate from another device_ on the new device and scan QR code." = "Válassza az _Átköltöztetés egy másik eszközről_ opciót az új eszközön és olvassa be a QR-kódot."; /* No comment provided by engineer. */ "Choose file" = "Fájl kiválasztása"; @@ -814,25 +912,37 @@ "Choose from library" = "Választás a könyvtárból"; /* No comment provided by engineer. */ +"Chunks deleted" = "Törölt fájltöredékek"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Letöltött fájltöredékek"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Feltöltött fájltöredékek"; + +/* swipe action */ "Clear" = "Kiürítés"; /* No comment provided by engineer. */ -"Clear conversation" = "Beszélgetés kiürítése"; +"Clear conversation" = "Üzenetek kiürítése"; /* No comment provided by engineer. */ -"Clear conversation?" = "Beszélgetés kiürítése?"; +"Clear conversation?" = "Üzenetek kiürítése?"; /* No comment provided by engineer. */ -"Clear private notes?" = "Privát jegyzetek törlése?"; +"Clear private notes?" = "Privát jegyzetek kiürítése?"; /* No comment provided by engineer. */ "Clear verification" = "Hitelesítés törlése"; /* No comment provided by engineer. */ -"colored" = "színes"; +"Color chats with the new themes." = "Csevegések színezése új témákkal."; /* No comment provided by engineer. */ -"Colors" = "Színek"; +"Color mode" = "Színmód"; + +/* No comment provided by engineer. */ +"colored" = "színes"; /* server test step */ "Compare file" = "Fájl összehasonlítás"; @@ -843,14 +953,26 @@ /* No comment provided by engineer. */ "complete" = "befejezett"; +/* No comment provided by engineer. */ +"Completed" = "Elkészült"; + /* No comment provided by engineer. */ "Configure ICE servers" = "ICE kiszolgálók beállítása"; +/* No comment provided by engineer. */ +"Configured %@ servers" = "Beállított %@ kiszolgálók"; + /* No comment provided by engineer. */ "Confirm" = "Megerősítés"; /* No comment provided by engineer. */ -"Confirm database upgrades" = "Adatbázis frissítés megerősítése"; +"Confirm contact deletion?" = "Biztosan törli az ismerőst?"; + +/* No comment provided by engineer. */ +"Confirm database upgrades" = "Adatbázis fejlesztésének megerősítése"; + +/* No comment provided by engineer. */ +"Confirm files from unknown servers." = "Ismeretlen kiszolgálókról származó fájlok jóváhagyása."; /* No comment provided by engineer. */ "Confirm network settings" = "Hálózati beállítások megerősítése"; @@ -885,6 +1007,9 @@ /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "Kapcsolódás a SimpleX Chat fejlesztőkhöz."; +/* No comment provided by engineer. */ +"Connect to your friends faster." = "Kapcsolódjon gyorsabban az ismerőseihez."; + /* No comment provided by engineer. */ "Connect to yourself?" = "Kapcsolódás saját magához?"; @@ -892,10 +1017,10 @@ "Connect to yourself?\nThis is your own one-time link!" = "Kapcsolódás saját magához?\nEz az egyszer használatos hivatkozása!"; /* No comment provided by engineer. */ -"Connect to yourself?\nThis is your own SimpleX address!" = "Kapcsolódás saját magához?\nEz a SimpleX azonosítója!"; +"Connect to yourself?\nThis is your own SimpleX address!" = "Kapcsolódás saját magához?\nEz az ön SimpleX címe!"; /* No comment provided by engineer. */ -"Connect via contact address" = "Kapcsolódás a kapcsolattartási azonosítón keresztül"; +"Connect via contact address" = "Kapcsolódás a kapcsolattartási címen keresztül"; /* No comment provided by engineer. */ "Connect via link" = "Kapcsolódás egy hivatkozáson keresztül"; @@ -909,18 +1034,27 @@ /* No comment provided by engineer. */ "connected" = "kapcsolódva"; +/* No comment provided by engineer. */ +"Connected" = "Kapcsolódva"; + /* No comment provided by engineer. */ "Connected desktop" = "Csatlakoztatott számítógép"; /* rcv group event chat item */ "connected directly" = "közvetlenül kapcsolódva"; +/* No comment provided by engineer. */ +"Connected servers" = "Kapcsolódott kiszolgálók"; + /* No comment provided by engineer. */ "Connected to desktop" = "Kapcsolódva a számítógéphez"; /* No comment provided by engineer. */ "connecting" = "kapcsolódás"; +/* No comment provided by engineer. */ +"Connecting" = "Kapcsolódás"; + /* No comment provided by engineer. */ "connecting (accepted)" = "kapcsolódás (elfogadva)"; @@ -942,6 +1076,9 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "Kapcsolódás a kiszolgálóhoz... (hiba: %@)"; +/* No comment provided by engineer. */ +"Connecting to contact, please wait or check later!" = "Kapcsolódás az ismerőshöz, várjon vagy ellenőrizze később!"; + /* No comment provided by engineer. */ "Connecting to desktop" = "Kapcsolódás a számítógéphez"; @@ -951,6 +1088,9 @@ /* No comment provided by engineer. */ "Connection" = "Kapcsolat"; +/* No comment provided by engineer. */ +"Connection and servers status." = "Kapcsolatok- és kiszolgálók állapotának megjelenítése."; + /* No comment provided by engineer. */ "Connection error" = "Kapcsolódási hiba"; @@ -960,6 +1100,9 @@ /* chat list item title (it should not be shown */ "connection established" = "kapcsolat létrehozva"; +/* No comment provided by engineer. */ +"Connection notifications" = "Kapcsolódási értesítések"; + /* No comment provided by engineer. */ "Connection request sent!" = "Kapcsolódási kérés elküldve!"; @@ -969,9 +1112,15 @@ /* No comment provided by engineer. */ "Connection timeout" = "Kapcsolat időtúllépés"; +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "A kapcsolat a számítógéppel megszakadt"; + /* connection information */ "connection:%@" = "kapcsolat: %@"; +/* No comment provided by engineer. */ +"Connections" = "Kapcsolatok"; + /* profile update event chat item */ "contact %@ changed to %@" = "%1$@ megváltoztatta a nevét erre: %2$@"; @@ -981,6 +1130,9 @@ /* No comment provided by engineer. */ "Contact already exists" = "Létező ismerős"; +/* No comment provided by engineer. */ +"Contact deleted!" = "Ismerős törölve!"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "az ismerősnél az e2e titkosítás elérhető"; @@ -994,7 +1146,7 @@ "Contact is connected" = "Ismerőse kapcsolódott"; /* No comment provided by engineer. */ -"Contact is not connected yet!" = "Az ismerőse még nem kapcsolódott!"; +"Contact is deleted." = "Törölt ismerős."; /* No comment provided by engineer. */ "Contact name" = "Ismerős neve"; @@ -1002,18 +1154,27 @@ /* No comment provided by engineer. */ "Contact preferences" = "Ismerős beállításai"; +/* No comment provided by engineer. */ +"Contact will be deleted - this cannot be undone!" = "Az ismerős törlésre fog kerülni - ez a művelet nem vonható vissza!"; + /* No comment provided by engineer. */ "Contacts" = "Ismerősök"; /* No comment provided by engineer. */ -"Contacts can mark messages for deletion; you will be able to view them." = "Az ismerősök törlésre jelölhetnek üzeneteket ; megtekintheti őket."; +"Contacts can mark messages for deletion; you will be able to view them." = "Az ismerősei törlésre jelölhetnek üzeneteket; ön majd meg tudja nézni azokat."; /* No comment provided by engineer. */ "Continue" = "Folytatás"; -/* chat item action */ +/* No comment provided by engineer. */ +"Conversation deleted!" = "Beszélgetés törölve!"; + +/* No comment provided by engineer. */ "Copy" = "Másolás"; +/* No comment provided by engineer. */ +"Copy error" = "Másolási hiba"; + /* No comment provided by engineer. */ "Core version: v%@" = "Alapverziószám: v%@"; @@ -1027,7 +1188,7 @@ "Create a group using a random profile." = "Csoport létrehozása véletlenszerűen létrehozott profillal."; /* No comment provided by engineer. */ -"Create an address to let people connect with you." = "Azonosító létrehozása, hogy az emberek kapcsolatba léphessenek önnel."; +"Create an address to let people connect with you." = "Cím létrehozása, hogy az emberek kapcsolatba léphessenek önnel."; /* server test step */ "Create file" = "Fájl létrehozása"; @@ -1054,11 +1215,14 @@ "Create secret group" = "Titkos csoport létrehozása"; /* No comment provided by engineer. */ -"Create SimpleX address" = "SimpleX azonosító létrehozása"; +"Create SimpleX address" = "SimpleX cím létrehozása"; /* No comment provided by engineer. */ "Create your profile" = "Saját profil létrehozása"; +/* No comment provided by engineer. */ +"Created" = "Létrehozva"; + /* No comment provided by engineer. */ "Created at" = "Létrehozva ekkor:"; @@ -1075,7 +1239,7 @@ "Creating link…" = "Hivatkozás létrehozása…"; /* No comment provided by engineer. */ -"creator" = "szerző"; +"creator" = "készítő"; /* No comment provided by engineer. */ "Current Passcode" = "Jelenlegi jelkód"; @@ -1083,6 +1247,9 @@ /* No comment provided by engineer. */ "Current passphrase…" = "Jelenlegi jelmondat…"; +/* No comment provided by engineer. */ +"Current profile" = "Jelenlegi profil"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "Jelenleg a maximális támogatott fájlméret %@."; @@ -1092,17 +1259,23 @@ /* No comment provided by engineer. */ "Custom time" = "Személyreszabott idő"; +/* No comment provided by engineer. */ +"Customize theme" = "Téma személyre szabása"; + /* No comment provided by engineer. */ "Dark" = "Sötét"; /* No comment provided by engineer. */ -"Database downgrade" = "Visszatérés a korábbi adatbázis verzióra"; +"Dark mode colors" = "Sötét mód színei"; + +/* No comment provided by engineer. */ +"Database downgrade" = "Adatbázis visszafejlesztése"; /* No comment provided by engineer. */ "Database encrypted!" = "Adatbázis titkosítva!"; /* No comment provided by engineer. */ -"Database encryption passphrase will be updated and stored in the keychain.\n" = "Az adatbázis titkosítási jelmondata frissül és tárolódik a kulcstárolóban.\n"; +"Database encryption passphrase will be updated and stored in the keychain.\n" = "Az adatbázis titkosítási jelmondata frissül és tárolódik a kulcstartóban.\n"; /* No comment provided by engineer. */ "Database encryption passphrase will be updated.\n" = "Adatbázis titkosítási jelmondat frissítve lesz.\n"; @@ -1132,10 +1305,10 @@ "Database passphrase & export" = "Adatbázis jelmondat és exportálás"; /* No comment provided by engineer. */ -"Database passphrase is different from saved in the keychain." = "Az adatbázis jelmondata eltér a kulcstárlóban mentettől."; +"Database passphrase is different from saved in the keychain." = "Az adatbázis jelmondata eltér a kulcstartóban mentettől."; /* No comment provided by engineer. */ -"Database passphrase is required to open chat." = "Adatbázis jelmondat szükséges a csevegés megnyitásához."; +"Database passphrase is required to open chat." = "A csevegés megnyitásához adja meg az adatbázis jelmondatát."; /* No comment provided by engineer. */ "Database upgrade" = "Adatbázis fejlesztése"; @@ -1144,7 +1317,7 @@ "database version is newer than the app, but no down migration for: %@" = "az adatbázis verziója újabb, mint az alkalmazásé, de nincs visszafelé átköltöztetés ehhez: %@"; /* No comment provided by engineer. */ -"Database will be encrypted and the passphrase stored in the keychain.\n" = "Az adatbázis titkosítva lesz, a jelmondat pedig a kulcstárolóban lesz tárolva.\n"; +"Database will be encrypted and the passphrase stored in the keychain.\n" = "Az adatbázis titkosítva lesz, a jelmondat pedig a kulcstartóban lesz tárolva.\n"; /* No comment provided by engineer. */ "Database will be encrypted.\n" = "Az adatbázis titkosításra kerül.\n"; @@ -1155,12 +1328,18 @@ /* time unit */ "days" = "nap"; +/* No comment provided by engineer. */ +"Debug delivery" = "Kézbesítési hibák felderítése"; + /* No comment provided by engineer. */ "Decentralized" = "Decentralizált"; /* message decrypt error item */ "Decryption error" = "Titkosítás visszafejtési hiba"; +/* No comment provided by engineer. */ +"decryption errors" = "visszafejtési hibák"; + /* pref value */ "default (%@)" = "alapértelmezett (%@)"; @@ -1170,26 +1349,30 @@ /* No comment provided by engineer. */ "default (yes)" = "alapértelmezett (igen)"; -/* chat item action */ +/* chat item action + swipe action */ "Delete" = "Törlés"; +/* No comment provided by engineer. */ +"Delete %lld messages of members?" = "Tagok %lld üzenetének törlése?"; + /* No comment provided by engineer. */ "Delete %lld messages?" = "Töröl %lld üzenetet?"; /* No comment provided by engineer. */ -"Delete address" = "Azonosító törlése"; +"Delete address" = "Cím törlése"; /* No comment provided by engineer. */ -"Delete address?" = "Azonosító törlése?"; +"Delete address?" = "Cím törlése?"; /* No comment provided by engineer. */ -"Delete after" = "Törlés miután"; +"Delete after" = "Törlés ennyi idő után"; /* No comment provided by engineer. */ "Delete all files" = "Minden fájl törlése"; /* No comment provided by engineer. */ -"Delete and notify contact" = "Törlés és ismerős értesítése"; +"Delete and notify contact" = "Törlés, és az ismerős értesítése"; /* No comment provided by engineer. */ "Delete archive" = "Archívum törlése"; @@ -1210,10 +1393,7 @@ "Delete contact" = "Ismerős törlése"; /* No comment provided by engineer. */ -"Delete Contact" = "Ismerős törlése"; - -/* No comment provided by engineer. */ -"Delete contact?\nThis cannot be undone!" = "Ismerős törlése?\nEz a művelet nem vonható vissza!"; +"Delete contact?" = "Ismerős törlése?"; /* No comment provided by engineer. */ "Delete database" = "Adatbázis törlése"; @@ -1234,7 +1414,7 @@ "Delete for everyone" = "Törlés mindenkinél"; /* No comment provided by engineer. */ -"Delete for me" = "Törlés nálam"; +"Delete for me" = "Csak nálam"; /* No comment provided by engineer. */ "Delete group" = "Csoport törlése"; @@ -1252,7 +1432,7 @@ "Delete link?" = "Hivatkozás törlése?"; /* No comment provided by engineer. */ -"Delete member message?" = "Csoporttag üzenet törlése?"; +"Delete member message?" = "Csoporttag üzenetének törlése?"; /* No comment provided by engineer. */ "Delete message?" = "Üzenet törlése?"; @@ -1261,7 +1441,7 @@ "Delete messages" = "Üzenetek törlése"; /* No comment provided by engineer. */ -"Delete messages after" = "Üzenetek törlése miután"; +"Delete messages after" = "Üzenetek törlése ennyi idő után"; /* No comment provided by engineer. */ "Delete old database" = "Régi adatbázis törlése"; @@ -1269,9 +1449,6 @@ /* No comment provided by engineer. */ "Delete old database?" = "Régi adatbázis törlése?"; -/* No comment provided by engineer. */ -"Delete pending connection" = "Függőben lévő kapcsolat törlése"; - /* No comment provided by engineer. */ "Delete pending connection?" = "Függő kapcsolatfelvételi kérések törlése?"; @@ -1281,12 +1458,21 @@ /* server test step */ "Delete queue" = "Várólista törlése"; +/* No comment provided by engineer. */ +"Delete up to 20 messages at once." = "Legfeljebb 20 üzenet törlése egyszerre."; + /* No comment provided by engineer. */ "Delete user profile?" = "Felhasználói profil törlése?"; +/* No comment provided by engineer. */ +"Delete without notification" = "Törlés értesítés nélkül"; + /* deleted chat item */ "deleted" = "törölve"; +/* No comment provided by engineer. */ +"Deleted" = "Törölve"; + /* No comment provided by engineer. */ "Deleted at" = "Törölve ekkor:"; @@ -1299,20 +1485,23 @@ /* rcv group event chat item */ "deleted group" = "törölt csoport"; +/* No comment provided by engineer. */ +"Deletion errors" = "Törlési hibák"; + /* No comment provided by engineer. */ "Delivery" = "Kézbesítés"; /* No comment provided by engineer. */ -"Delivery receipts are disabled!" = "Kézbesítési igazolások kikapcsolva!"; +"Delivery receipts are disabled!" = "A kézbesítési jelentések ki vannak kapcsolva!"; /* No comment provided by engineer. */ -"Delivery receipts!" = "Kézbesítési igazolások!"; +"Delivery receipts!" = "Üzenet kézbesítési jelentések!"; /* No comment provided by engineer. */ "Description" = "Leírás"; /* No comment provided by engineer. */ -"Desktop address" = "Számítógép azonosítója"; +"Desktop address" = "Számítógép címe"; /* No comment provided by engineer. */ "Desktop app version %@ is not compatible with this app." = "Az asztali kliens verziója %@ nem kompatibilis ezzel az alkalmazással."; @@ -1320,9 +1509,27 @@ /* No comment provided by engineer. */ "Desktop devices" = "Számítógépek"; +/* No comment provided by engineer. */ +"Destination server address of %@ is incompatible with forwarding server %@ settings." = "A(z) %@ célkiszolgáló címe nem kompatibilis a(z) %@ továbbító kiszolgáló beállításaival."; + +/* snd error text */ +"Destination server error: %@" = "Célkiszolgáló hiba: %@"; + +/* No comment provided by engineer. */ +"Destination server version of %@ is incompatible with forwarding server %@." = "A(z) %@ célkiszolgáló verziója nem kompatibilis a(z) %@ továbbító kiszolgálóval."; + +/* No comment provided by engineer. */ +"Detailed statistics" = "Részletes statisztikák"; + +/* No comment provided by engineer. */ +"Details" = "Részletek"; + /* No comment provided by engineer. */ "Develop" = "Fejlesztés"; +/* No comment provided by engineer. */ +"Developer options" = "Fejlesztői beállítások"; + /* No comment provided by engineer. */ "Developer tools" = "Fejlesztői eszközök"; @@ -1330,10 +1537,10 @@ "Device" = "Eszköz"; /* No comment provided by engineer. */ -"Device authentication is disabled. Turning off SimpleX Lock." = "Eszközhitelesítés kikapcsolva. SimpleX zárolás kikapcsolása."; +"Device authentication is disabled. Turning off SimpleX Lock." = "A készüléken nincs beállítva a képernyőzár. A SimpleX zár ki van kapcsolva."; /* No comment provided by engineer. */ -"Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." = "Eszközhitelesítés nem engedélyezett.A SimpleX zárolás bekapcsolható a Beállításokon keresztül, miután az eszköz hitelesítés engedélyezésre került."; +"Device authentication is not enabled. You can turn on SimpleX Lock via Settings, once you enable device authentication." = "A készüléken nincs beállítva a képernyőzár. A SimpleX zár az „Adatvédelem és biztonság” menüben kapcsolható be, miután beállította a képernyőzárat az eszközén."; /* No comment provided by engineer. */ "different migration in the app/database: %@ / %@" = "különböző átköltöztetések az alkalmazásban/adatbázisban: %@ / %@"; @@ -1348,7 +1555,7 @@ "Direct messages" = "Közvetlen üzenetek"; /* No comment provided by engineer. */ -"Direct messages between members are prohibited in this group." = "Ebben a csoportban tiltott a tagok közötti közvetlen üzenetek küldése."; +"Direct messages between members are prohibited in this group." = "A közvetlen üzenetek küldése a tagok között le van tiltva ebben a csoportban."; /* No comment provided by engineer. */ "Disable (keep overrides)" = "Letiltás (felülírások megtartásával)"; @@ -1357,11 +1564,14 @@ "Disable for all" = "Letiltás mindenki számára"; /* authentication reason */ -"Disable SimpleX Lock" = "SimpleX zárolás kikapcsolása"; +"Disable SimpleX Lock" = "SimpleX zár kikapcsolása"; /* No comment provided by engineer. */ "disabled" = "letiltva"; +/* No comment provided by engineer. */ +"Disabled" = "Letiltva"; + /* No comment provided by engineer. */ "Disappearing message" = "Eltűnő üzenet"; @@ -1369,7 +1579,7 @@ "Disappearing messages" = "Eltűnő üzenetek"; /* No comment provided by engineer. */ -"Disappearing messages are prohibited in this chat." = "Az eltűnő üzenetek le vannak tiltva ebben a csevegésben."; +"Disappearing messages are prohibited in this chat." = "Az eltűnő üzenetek küldése le van tiltva ebben a csevegésben."; /* No comment provided by engineer. */ "Disappearing messages are prohibited in this group." = "Az eltűnő üzenetek küldése le van tiltva ebben a csoportban."; @@ -1398,11 +1608,17 @@ /* No comment provided by engineer. */ "Do not send history to new members." = "Az előzmények ne kerüljenek elküldésre az új tagok számára."; +/* No comment provided by engineer. */ +"Do NOT send messages directly, even if your or destination server does not support private routing." = "Ne küldjön üzeneteket közvetlenül, még akkor sem, ha az ön kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást."; + +/* No comment provided by engineer. */ +"Do NOT use private routing." = "Ne használjon privát útválasztást."; + /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "NE használja a SimpleX-et segélyhívásokhoz."; /* No comment provided by engineer. */ -"Don't create address" = "Ne hozzon létre azonosítót"; +"Don't create address" = "Ne hozzon létre címet"; /* No comment provided by engineer. */ "Don't enable" = "Ne engedélyezze"; @@ -1411,17 +1627,26 @@ "Don't show again" = "Ne mutasd újra"; /* No comment provided by engineer. */ -"Downgrade and open chat" = "Visszatérés a korábbi verzióra és a csevegés megnyitása"; +"Downgrade and open chat" = "Visszafejlesztés és a csevegés megnyitása"; /* chat item action */ "Download" = "Letöltés"; +/* No comment provided by engineer. */ +"Download errors" = "Letöltési hibák"; + /* No comment provided by engineer. */ "Download failed" = "Sikertelen letöltés"; /* server test step */ "Download file" = "Fájl letöltése"; +/* No comment provided by engineer. */ +"Downloaded" = "Letöltve"; + +/* No comment provided by engineer. */ +"Downloaded files" = "Letöltött fájlok"; + /* No comment provided by engineer. */ "Downloading archive" = "Archívum letöltése"; @@ -1432,7 +1657,10 @@ "Duplicate display name!" = "Duplikált megjelenítési név!"; /* integrity error chat item */ -"duplicate message" = "duplikálódott üzenet"; +"duplicate message" = "duplikált üzenet"; + +/* No comment provided by engineer. */ +"duplicates" = "duplikációk"; /* No comment provided by engineer. */ "Duration" = "Időtartam"; @@ -1483,14 +1711,17 @@ "Enable self-destruct passcode" = "Önmegsemmisítő jelkód engedélyezése"; /* authentication reason */ -"Enable SimpleX Lock" = "SimpleX zárolás engedélyezése"; +"Enable SimpleX Lock" = "SimpleX zár bekapcsolása"; /* No comment provided by engineer. */ -"Enable TCP keep-alive" = "TCP életben tartásának engedélyezése"; +"Enable TCP keep-alive" = "TCP életben tartása"; /* enabled status */ "enabled" = "engedélyezve"; +/* No comment provided by engineer. */ +"Enabled" = "Engedélyezve"; + /* No comment provided by engineer. */ "Enabled for" = "Engedélyezve"; @@ -1528,7 +1759,7 @@ "Encrypted message: database migration error" = "Titkosított üzenet: adatbázis-átköltöztetés hiba"; /* notification */ -"Encrypted message: keychain error" = "Titkosított üzenet: kulcstároló hiba"; +"Encrypted message: keychain error" = "Titkosított üzenet: kulcstartó hiba"; /* notification */ "Encrypted message: no passphrase" = "Titkosított üzenet: nincs jelmondat"; @@ -1597,10 +1828,10 @@ "Enter this device name…" = "Eszköznév megadása…"; /* placeholder */ -"Enter welcome message…" = "Üdvözlő üzenetet megadása…"; +"Enter welcome message…" = "Üdvözlő üzenet megadása…"; /* placeholder */ -"Enter welcome message… (optional)" = "Üdvözlő üzenetet megadása… (opcionális)"; +"Enter welcome message… (optional)" = "Üdvözlő üzenet megadása… (opcionális)"; /* No comment provided by engineer. */ "Enter your name…" = "Adjon meg egy nevet…"; @@ -1612,7 +1843,7 @@ "Error" = "Hiba"; /* No comment provided by engineer. */ -"Error aborting address change" = "Hiba az azonosító megváltoztatásának megszakításakor"; +"Error aborting address change" = "Hiba a cím megváltoztatásának megszakításakor"; /* No comment provided by engineer. */ "Error accepting contact request" = "Hiba történt a kapcsolatfelvételi kérelem elfogadásakor"; @@ -1624,7 +1855,7 @@ "Error adding member(s)" = "Hiba a tag(-ok) hozzáadásakor"; /* No comment provided by engineer. */ -"Error changing address" = "Hiba az azonosító megváltoztatásakor"; +"Error changing address" = "Hiba a cím megváltoztatásakor"; /* No comment provided by engineer. */ "Error changing role" = "Hiba a szerepkör megváltoztatásakor"; @@ -1633,7 +1864,10 @@ "Error changing setting" = "Hiba a beállítás megváltoztatásakor"; /* No comment provided by engineer. */ -"Error creating address" = "Hiba az azonosító létrehozásakor"; +"Error connecting to forwarding server %@. Please try later." = "Hiba a(z) %@ továbbító kiszolgálóhoz való kapcsolódáskor. Próbálja meg később."; + +/* No comment provided by engineer. */ +"Error creating address" = "Hiba a cím létrehozásakor"; /* No comment provided by engineer. */ "Error creating group" = "Hiba a csoport létrehozásakor"; @@ -1662,9 +1896,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "Hiba a kapcsolat törlésekor"; -/* No comment provided by engineer. */ -"Error deleting contact" = "Hiba az ismerős törlésekor"; - /* No comment provided by engineer. */ "Error deleting database" = "Hiba az adatbázis törlésekor"; @@ -1692,6 +1923,9 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "Hiba a csevegési adatbázis exportálásakor"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Hiba a téma exportálásakor: %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "Hiba a csevegési adatbázis importálásakor"; @@ -1707,14 +1941,23 @@ /* No comment provided by engineer. */ "Error receiving file" = "Hiba a fájl fogadásakor"; +/* No comment provided by engineer. */ +"Error reconnecting server" = "Hiba a kiszolgálóhoz való újrakapcsolódáskor"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "Hiba a kiszolgálókhoz való újrakapcsolódáskor"; + /* No comment provided by engineer. */ "Error removing member" = "Hiba a tag eltávolításakor"; +/* No comment provided by engineer. */ +"Error resetting statistics" = "Hiba a statisztikák visszaállításakor"; + /* No comment provided by engineer. */ "Error saving %@ servers" = "Hiba történt a %@ kiszolgálók mentése közben"; /* No comment provided by engineer. */ -"Error saving group profile" = "Hiba a csoport profil mentésekor"; +"Error saving group profile" = "Hiba a csoportprofil mentésekor"; /* No comment provided by engineer. */ "Error saving ICE servers" = "Hiba az ICE kiszolgálók mentésekor"; @@ -1723,7 +1966,7 @@ "Error saving passcode" = "Hiba a jelkód mentése közben"; /* No comment provided by engineer. */ -"Error saving passphrase to keychain" = "Hiba a jelmondat kulcstárolóba történő mentésekor"; +"Error saving passphrase to keychain" = "Hiba a jelmondat kulcstartóba történő mentésekor"; /* when migrating */ "Error saving settings" = "Hiba a beállítások mentésekor"; @@ -1756,7 +1999,7 @@ "Error switching profile!" = "Hiba a profil váltásakor!"; /* No comment provided by engineer. */ -"Error synchronizing connection" = "Hiba a kapcsolat szinkronizálása során"; +"Error synchronizing connection" = "Hiba a kapcsolat szinkronizálása közben"; /* No comment provided by engineer. */ "Error updating group link" = "Hiba a csoport hivatkozás frissítésekor"; @@ -1779,7 +2022,8 @@ /* No comment provided by engineer. */ "Error: " = "Hiba: "; -/* No comment provided by engineer. */ +/* file error text + snd error text */ "Error: %@" = "Hiba: %@"; /* No comment provided by engineer. */ @@ -1788,6 +2032,9 @@ /* No comment provided by engineer. */ "Error: URL is invalid" = "Hiba: az URL érvénytelen"; +/* No comment provided by engineer. */ +"Errors" = "Hibák"; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Akkor is, ha le van tiltva a beszélgetésben."; @@ -1800,12 +2047,18 @@ /* chat item action */ "Expand" = "Kibontás"; +/* No comment provided by engineer. */ +"expired" = "lejárt"; + /* No comment provided by engineer. */ "Export database" = "Adatbázis exportálása"; /* No comment provided by engineer. */ "Export error:" = "Exportálási hiba:"; +/* No comment provided by engineer. */ +"Export theme" = "Téma exportálása"; + /* No comment provided by engineer. */ "Exported database archive." = "Exportált adatbázis-archívum."; @@ -1824,9 +2077,24 @@ /* No comment provided by engineer. */ "Faster joining and more reliable messages." = "Gyorsabb csatlakozás és megbízhatóbb üzenet kézbesítés."; -/* No comment provided by engineer. */ +/* swipe action */ "Favorite" = "Kedvenc"; +/* No comment provided by engineer. */ +"File error" = "Fájlhiba"; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "A fájl nem található - valószínűleg a fájlt törölték vagy visszavonták."; + +/* file error text */ +"File server error: %@" = "Fájlkiszolgáló hiba: %@"; + +/* No comment provided by engineer. */ +"File status" = "Fájlállapot"; + +/* copied message info */ +"File status: %@" = "Fájlállapot: %@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "A fájl törölve lesz a kiszolgálóról."; @@ -1839,6 +2107,9 @@ /* No comment provided by engineer. */ "File: %@" = "Fájl: %@"; +/* No comment provided by engineer. */ +"Files" = "Fájlok"; + /* No comment provided by engineer. */ "Files & media" = "Fájlok és média"; @@ -1905,6 +2176,21 @@ /* No comment provided by engineer. */ "Forwarded from" = "Továbbítva innen:"; +/* No comment provided by engineer. */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "A(z) %@ továbbító kiszolgáló nem tudott csatlakozni a(z) %@ célkiszolgálóhoz. Próbálja meg később."; + +/* No comment provided by engineer. */ +"Forwarding server address is incompatible with network settings: %@." = "A továbbító kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %@."; + +/* No comment provided by engineer. */ +"Forwarding server version is incompatible with network settings: %@." = "A továbbító kiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %@."; + +/* snd error text */ +"Forwarding server: %@\nDestination server error: %@" = "Továbbító kiszolgáló: %1$@\nCélkiszolgáló hiba:%2$@"; + +/* snd error text */ +"Forwarding server: %@\nError: %@" = "Továbbító kiszolgáló: %1$@\nHiba: %2$@"; + /* No comment provided by engineer. */ "Found desktop" = "Megtalált számítógép"; @@ -1932,6 +2218,12 @@ /* No comment provided by engineer. */ "GIFs and stickers" = "GIF-ek és matricák"; +/* message preview */ +"Good afternoon!" = "Jó napot!"; + +/* message preview */ +"Good morning!" = "Jó reggelt!"; + /* No comment provided by engineer. */ "Group" = "Csoport"; @@ -1999,13 +2291,13 @@ "Group preferences" = "Csoport beállítások"; /* No comment provided by engineer. */ -"Group profile" = "Csoport profil"; +"Group profile" = "Csoportprofil"; /* No comment provided by engineer. */ "Group profile is stored on members' devices, not on the servers." = "A csoport profilja a tagok eszközein tárolódik, nem a kiszolgálókon."; /* snd group event chat item */ -"group profile updated" = "csoport profil frissítve"; +"group profile updated" = "csoportprofil frissítve"; /* No comment provided by engineer. */ "Group welcome message" = "Csoport üdvözlő üzenete"; @@ -2071,13 +2363,13 @@ "ICE servers (one per line)" = "ICE-kiszolgálók (soronként egy)"; /* No comment provided by engineer. */ -"If you can't meet in person, show QR code in a video call, or share the link." = "Ha nem tud személyesen találkozni, mutassa meg a QR-kódot egy videohívás során, vagy ossza meg a hivatkozást."; +"If you can't meet in person, show QR code in a video call, or share the link." = "Ha nem tud személyesen találkozni, mutassa meg a QR-kódot egy videohívás közben, vagy ossza meg a hivatkozást."; /* No comment provided by engineer. */ "If you enter this passcode when opening the app, all app data will be irreversibly removed!" = "Ha az alkalmazás megnyitásakor megadja ezt a jelkódot, az összes alkalmazásadat véglegesen törlődik!"; /* No comment provided by engineer. */ -"If you enter your self-destruct passcode while opening the app:" = "Ha az alkalmazás megnyitásakor az önmegsemmisítő jelkódot megadásra kerül:"; +"If you enter your self-destruct passcode while opening the app:" = "Ha az alkalmazás megnyitásakor megadja az önmegsemmisítő jelkódot:"; /* No comment provided by engineer. */ "If you need to use the chat now tap **Do it later** below (you will be offered to migrate the database when you restart the app)." = "Ha most kell használnia a csevegést, koppintson a ** Csináld később** elemre (az alkalmazás újraindításakor felajánlásra kerül az adatbázis áttelepítése)."; @@ -2109,6 +2401,9 @@ /* No comment provided by engineer. */ "Import failed" = "Sikertelen importálás"; +/* No comment provided by engineer. */ +"Import theme" = "Téma importálása"; + /* No comment provided by engineer. */ "Importing archive" = "Archívum importálása"; @@ -2130,6 +2425,9 @@ /* No comment provided by engineer. */ "In-call sounds" = "Bejövő hívás csengőhangja"; +/* No comment provided by engineer. */ +"inactive" = "inaktív"; + /* No comment provided by engineer. */ "Incognito" = "Inkognitó"; @@ -2193,6 +2491,9 @@ /* No comment provided by engineer. */ "Interface" = "Felület"; +/* No comment provided by engineer. */ +"Interface colors" = "Kezelőfelület színei"; + /* invalid chat data */ "invalid chat" = "érvénytelen csevegés"; @@ -2235,6 +2536,9 @@ /* group name */ "invitation to group %@" = "meghívás a(z) %@ csoportba"; +/* No comment provided by engineer. */ +"invite" = "meghívás"; + /* No comment provided by engineer. */ "Invite friends" = "Barátok meghívása"; @@ -2245,41 +2549,44 @@ "Invite to group" = "Meghívás a csoportba"; /* No comment provided by engineer. */ -"invited" = "meghívta"; +"invited" = "meghíva"; /* rcv group event chat item */ -"invited %@" = "meghívta %@-t"; +"invited %@" = "meghívta őt: %@"; /* chat list item title */ "invited to connect" = "meghívta, hogy csatlakozzon"; /* rcv group event chat item */ -"invited via your group link" = "meghívta a csoport hivatkozásán keresztül"; +"invited via your group link" = "meghíva az ön csoport hivatkozásán keresztül"; /* No comment provided by engineer. */ -"iOS Keychain is used to securely store passphrase - it allows receiving push notifications." = "Az iOS kulcstár a jelmondat biztonságos tárolására szolgál - lehetővé teszi a push-értesítések fogadását."; +"iOS Keychain is used to securely store passphrase - it allows receiving push notifications." = "Az iOS kulcstartó a jelmondat biztonságos tárolására szolgál - lehetővé teszi a push-értesítések fogadását."; /* No comment provided by engineer. */ -"iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." = "Az iOS kulcstár az alkalmazás újraindítása, vagy a jelmondat módosítása után a jelmondat biztonságos tárolására szolgál - lehetővé teszi a push-értesítések fogadását."; +"iOS Keychain will be used to securely store passphrase after you restart the app or change passphrase - it will allow receiving push notifications." = "Az iOS kulcstartó az alkalmazás újraindítása, vagy a jelmondat módosítása után a jelmondat biztonságos tárolására szolgál - lehetővé teszi a push-értesítések fogadását."; /* No comment provided by engineer. */ "Irreversible message deletion" = "Végleges üzenettörlés"; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this chat." = "Ebben a csevegésben az üzenetek végleges törlése le van tiltva."; +"Irreversible message deletion is prohibited in this chat." = "Az üzenetek végleges törlése le van tiltva ebben a csevegésben."; /* No comment provided by engineer. */ -"Irreversible message deletion is prohibited in this group." = "Ebben a csoportban az üzenetek végleges törlése le van tiltva."; +"Irreversible message deletion is prohibited in this group." = "Az üzenetek végleges törlése le van tiltva ebben a csoportban."; /* No comment provided by engineer. */ "It allows having many anonymous connections without any shared data between them in a single chat profile." = "Lehetővé teszi, hogy egyetlen csevegőprofilon belül több anonim kapcsolat legyen, anélkül, hogy megosztott adatok lennének közöttük."; /* No comment provided by engineer. */ -"It can happen when you or your connection used the old database backup." = "Ez akkor fordulhat elő, ha ön vagy a kapcsolata régi adatbázis biztonsági mentést használt."; +"It can happen when you or your connection used the old database backup." = "Ez akkor fordulhat elő, ha ön vagy az ismerőse régi adatbázis biztonsági mentést használt."; /* No comment provided by engineer. */ "It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Ez akkor fordulhat elő, ha:\n1. Az üzenetek 2 nap után, vagy a kiszolgálón 30 nap után lejártak.\n2. Az üzenet visszafejtése sikertelen volt, mert vagy az ismerőse régebbi adatbázis biztonsági mentést használt.\n3. A kapcsolat sérült."; +/* No comment provided by engineer. */ +"It protects your IP address and connections." = "Védi az IP-címét és a kapcsolatokat."; + /* No comment provided by engineer. */ "It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Úgy tűnik, már kapcsolódott ezen a hivatkozáson keresztül. Ha ez nem így van, akkor hiba történt (%@)."; @@ -2292,7 +2599,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Japán kezelőfelület"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Csatlakozás"; /* No comment provided by engineer. */ @@ -2322,6 +2629,9 @@ /* No comment provided by engineer. */ "Keep" = "Megtart"; +/* No comment provided by engineer. */ +"Keep conversation" = "Beszélgetés megtartása"; + /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "A számítógépről való használathoz tartsd nyitva az alkalmazást"; @@ -2332,10 +2642,10 @@ "Keep your connections" = "Kapcsolatok megtartása"; /* No comment provided by engineer. */ -"Keychain error" = "Kulcstároló hiba"; +"Keychain error" = "Kulcstartó hiba"; /* No comment provided by engineer. */ -"KeyChain error" = "Kulcstároló hiba"; +"KeyChain error" = "Kulcstartó hiba"; /* No comment provided by engineer. */ "Large file!" = "Nagy fájl!"; @@ -2343,8 +2653,8 @@ /* No comment provided by engineer. */ "Learn more" = "Tudjon meg többet"; -/* No comment provided by engineer. */ -"Leave" = "Elhagy"; +/* swipe action */ +"Leave" = "Elhagyás"; /* No comment provided by engineer. */ "Leave group" = "Csoport elhagyása"; @@ -2407,10 +2717,10 @@ "Make profile private!" = "Tegye priváttá a profilját!"; /* No comment provided by engineer. */ -"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Győződjön meg arról, hogy a %@ szervercímek megfelelő formátumúak, sorszeparáltak és nem duplikáltak (%@)."; +"Make sure %@ server addresses are in correct format, line separated and are not duplicated (%@)." = "Győződjön meg arról, hogy a %@ kiszolgálócímek megfelelő formátumúak, sorszeparáltak és nincsenek duplikálva (%@)."; /* No comment provided by engineer. */ -"Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Győződjön meg arról, hogy a WebRTC ICE-kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nem duplikáltak."; +"Make sure WebRTC ICE server addresses are in correct format, line separated and are not duplicated." = "Győződjön meg arról, hogy a WebRTC ICE-kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nincsenek duplikálva."; /* No comment provided by engineer. */ "Many people asked: *if SimpleX has no user identifiers, how can it deliver messages?*" = "Sokan kérdezték: *ha a SimpleX-nek nincsenek felhasználói azonosítói, akkor hogyan tud üzeneteket kézbesíteni?*"; @@ -2419,10 +2729,10 @@ "Mark deleted for everyone" = "Jelölje meg mindenki számára töröltként"; /* No comment provided by engineer. */ -"Mark read" = "Megjelölés olvasottként"; +"Mark read" = "Olvasottnak jelölés"; /* No comment provided by engineer. */ -"Mark verified" = "Ellenőrzöttként jelölve"; +"Mark verified" = "Hitelesítés"; /* No comment provided by engineer. */ "Markdown in messages" = "Markdown az üzenetekben"; @@ -2433,6 +2743,12 @@ /* No comment provided by engineer. */ "Max 30 seconds, received instantly." = "Max. 30 másodperc, azonnal érkezett."; +/* No comment provided by engineer. */ +"Media & file servers" = "Média és fájlkiszolgálók"; + +/* blur media */ +"Medium" = "Közepes"; + /* member role */ "member" = "tag"; @@ -2445,39 +2761,72 @@ /* rcv group event chat item */ "member connected" = "kapcsolódott"; -/* No comment provided by engineer. */ -"Member role will be changed to \"%@\". All group members will be notified." = "A tag szerepköre meg fog változni erre: \"%@\". A csoport minden tagja értesítést kap róla."; +/* item status text */ +"Member inactive" = "Inaktív tag"; /* No comment provided by engineer. */ -"Member role will be changed to \"%@\". The member will receive a new invitation." = "A tag szerepköre meg fog változni erre: \"%@\". A tag új meghívást fog kapni."; +"Member role will be changed to \"%@\". All group members will be notified." = "A tag szerepköre meg fog változni erre: „%@”. A csoport minden tagja értesítést kap róla."; + +/* No comment provided by engineer. */ +"Member role will be changed to \"%@\". The member will receive a new invitation." = "A tag szerepköre meg fog változni erre: „%@”. A tag új meghívást fog kapni."; /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "A tag eltávolítása a csoportból - ez a művelet nem vonható vissza!"; +/* No comment provided by engineer. */ +"Menus" = "Menük"; + +/* No comment provided by engineer. */ +"message" = "üzenet"; + /* item status text */ "Message delivery error" = "Üzenetkézbesítési hiba"; /* No comment provided by engineer. */ -"Message delivery receipts!" = "Üzenetkézbesítési bizonylatok!"; +"Message delivery receipts!" = "Üzenet kézbesítési jelentések!"; + +/* item status text */ +"Message delivery warning" = "Üzenet kézbesítési figyelmeztetés"; /* No comment provided by engineer. */ "Message draft" = "Üzenetvázlat"; +/* item status text */ +"Message forwarded" = "Továbbított üzenet"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "Az üzenet később is kézbesíthető, ha a tag aktívvá válik."; + +/* No comment provided by engineer. */ +"Message queue info" = "Üzenet-várakoztatási információ"; + /* chat feature */ "Message reactions" = "Üzenetreakciók"; /* No comment provided by engineer. */ -"Message reactions are prohibited in this chat." = "Az üzenetreakciók ebben a csevegésben le vannak tiltva."; +"Message reactions are prohibited in this chat." = "Az üzenetreakciók küldése le van tiltva ebben a csevegésben."; /* No comment provided by engineer. */ -"Message reactions are prohibited in this group." = "Ebben a csoportban az üzenetreakciók le vannak tiltva."; +"Message reactions are prohibited in this group." = "Az üzenetreakciók küldése le van tiltva ebben a csoportban."; /* notification */ "message received" = "üzenet érkezett"; +/* No comment provided by engineer. */ +"Message reception" = "Üzenet kézbesítési jelentés"; + +/* No comment provided by engineer. */ +"Message servers" = "Üzenetkiszolgálók"; + /* No comment provided by engineer. */ "Message source remains private." = "Az üzenet forrása titokban marad."; +/* No comment provided by engineer. */ +"Message status" = "Üzenetállapot"; + +/* copied message info */ +"Message status: %@" = "Üzenetállapot: %@"; + /* No comment provided by engineer. */ "Message text" = "Üzenet szövege"; @@ -2494,10 +2843,16 @@ "Messages from %@ will be shown!" = "A(z) %@ által írt üzenetek megjelennek!"; /* No comment provided by engineer. */ -"Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Az üzeneteket, fájlokat és hívásokat **végpontok közötti titkosítással** és sérülés utáni titkosságvédelemmel, visszautasítással és sérülés utáni helyreállítással védi."; +"Messages received" = "Fogadott üzenetek"; /* No comment provided by engineer. */ -"Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Az üzeneteket, fájlokat és hívásokat **végpontok közötti kvantumrezisztens titkosítással** és sérülés utáni titkosságvédelemmel, visszautasítással és sérülés utáni helyreállítással védi."; +"Messages sent" = "Elküldött üzenetek"; + +/* No comment provided by engineer. */ +"Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Az üzeneteket, fájlokat és hívásokat **végpontok közötti titkosítással**, sérülés utáni titkosság-védelemmel és -helyreállítással, továbbá visszautasítással védi."; + +/* No comment provided by engineer. */ +"Messages, files and calls are protected by **quantum resistant e2e encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Az üzeneteket, fájlokat és hívásokat **végpontok közötti kvantumrezisztens titkosítással**, sérülés utáni titkosság-védelemmel és -helyreállítással, továbbá visszautasítással védi."; /* No comment provided by engineer. */ "Migrate device" = "Eszköz átköltöztetése"; @@ -2568,19 +2923,19 @@ /* item status description */ "Most likely this connection is deleted." = "Valószínűleg ez a kapcsolat törlésre került."; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Valószínűleg ez az ismerős törölte önnel a kapcsolatot."; - /* No comment provided by engineer. */ "Multiple chat profiles" = "Több csevegőprofil"; /* No comment provided by engineer. */ -"Mute" = "Elnémítás"; +"mute" = "némítás"; + +/* swipe action */ +"Mute" = "Némítás"; /* No comment provided by engineer. */ "Muted when inactive!" = "Némítás, ha inaktív!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "Név"; /* No comment provided by engineer. */ @@ -2589,6 +2944,9 @@ /* No comment provided by engineer. */ "Network connection" = "Internetkapcsolat"; +/* snd error text */ +"Network issues - message expired after many attempts to send it." = "Hálózati problémák - az üzenet többszöri elküldési kísérlet után lejárt."; + /* No comment provided by engineer. */ "Network management" = "Hálózatkezelés"; @@ -2604,6 +2962,9 @@ /* No comment provided by engineer. */ "New chat" = "Új beszélgetés"; +/* No comment provided by engineer. */ +"New chat experience 🎉" = "Új csevegési élmény 🎉"; + /* notification */ "New contact request" = "Új kapcsolattartási kérelem"; @@ -2622,6 +2983,9 @@ /* No comment provided by engineer. */ "New in %@" = "Újdonságok a(z) %@ verzióban"; +/* No comment provided by engineer. */ +"New media options" = "Új médiabeállítások"; + /* No comment provided by engineer. */ "New member role" = "Új tag szerepköre"; @@ -2658,6 +3022,9 @@ /* No comment provided by engineer. */ "No device token!" = "Nincs eszköztoken!"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Még nincs közvetlen kapcsolat, az üzenetet az admin továbbítja."; + /* No comment provided by engineer. */ "no e2e encryption" = "nincs e2e titkosítás"; @@ -2670,6 +3037,9 @@ /* No comment provided by engineer. */ "No history" = "Nincsenek előzmények"; +/* No comment provided by engineer. */ +"No info, try to reload" = "Nincs információ, próbálja meg újratölteni"; + /* No comment provided by engineer. */ "No network connection" = "Nincs hálózati kapcsolat"; @@ -2685,6 +3055,9 @@ /* No comment provided by engineer. */ "Not compatible!" = "Nem kompatibilis!"; +/* No comment provided by engineer. */ +"Nothing selected" = "Semmi sincs kiválasztva"; + /* No comment provided by engineer. */ "Notifications" = "Értesítések"; @@ -2692,7 +3065,7 @@ "Notifications are disabled!" = "Az értesítések le vannak tiltva!"; /* No comment provided by engineer. */ -"Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Most már az adminok is:\n- törölhetik a tagok üzeneteit.\n- letilthatnak tagokat (\"megfigyelő\" szerepkör)"; +"Now admins can:\n- delete members' messages.\n- disable members (\"observer\" role)" = "Most már az adminok is:\n- törölhetik a tagok üzeneteit.\n- letilthatnak tagokat („megfigyelő” szerepkör)"; /* member role */ "observer" = "megfigyelő"; @@ -2700,10 +3073,10 @@ /* enabled status group pref value time to disappear */ -"off" = "ki"; +"off" = "kikapcsolva"; -/* No comment provided by engineer. */ -"Off" = "Ki"; +/* blur media */ +"Off" = "Kikapcsolva"; /* feature offered item */ "offered %@" = "%@ ajánlotta"; @@ -2724,16 +3097,16 @@ "Old database archive" = "Régi adatbázis archívum"; /* group pref value */ -"on" = "be"; +"on" = "bekapcsolva"; /* No comment provided by engineer. */ "One-time invitation link" = "Egyszer használatos meghívó hivatkozás"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "A kapcsolódáshoz Onion kiszolgálókra lesz szükség. VPN engedélyezése szükséges."; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "A kapcsolódáshoz Onion kiszolgálókra lesz szükség.\nVPN engedélyezése szükséges."; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "Onion kiszolgálók használata, ha azok rendelkezésre állnak. VPN engedélyezése szükséges."; +"Onion hosts will be used when available.\nRequires compatible VPN." = "Onion kiszolgálók használata, ha azok rendelkezésre állnak.\nVPN engedélyezése szükséges."; /* No comment provided by engineer. */ "Onion hosts will not be used." = "Onion kiszolgálók nem lesznek használva."; @@ -2741,6 +3114,9 @@ /* No comment provided by engineer. */ "Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Csak a klienseszközök tárolják a felhasználói profilokat, névjegyeket, csoportokat és a **2 rétegű végponttól-végpontig titkosítással** küldött üzeneteket."; +/* No comment provided by engineer. */ +"Only delete conversation" = "Csak a beszélgetés törlése"; + /* No comment provided by engineer. */ "Only group owners can change group preferences." = "Csak a csoporttulajdonosok módosíthatják a csoportbeállításokat."; @@ -2775,7 +3151,7 @@ "Only your contact can make calls." = "Csak az ismerős tud hívást indítani."; /* No comment provided by engineer. */ -"Only your contact can send disappearing messages." = "Csak az ismerős tud eltűnő üzeneteket küldeni."; +"Only your contact can send disappearing messages." = "Csak az ismerőse tud eltűnő üzeneteket küldeni."; /* No comment provided by engineer. */ "Only your contact can send voice messages." = "Csak az ismerős tud hangüzeneteket küldeni."; @@ -2795,6 +3171,9 @@ /* authentication reason */ "Open migration to another device" = "Átköltöztetés megkezdése egy másik eszközre"; +/* No comment provided by engineer. */ +"Open server settings" = "Kiszolgáló beállításainak megnyitása"; + /* No comment provided by engineer. */ "Open Settings" = "Beállítások megnyitása"; @@ -2819,9 +3198,18 @@ /* No comment provided by engineer. */ "Or show this code" = "Vagy mutassa meg ezt a kódot"; +/* No comment provided by engineer. */ +"other" = "egyéb"; + /* No comment provided by engineer. */ "Other" = "További"; +/* No comment provided by engineer. */ +"Other %@ servers" = "További %@ kiszolgálók"; + +/* No comment provided by engineer. */ +"other errors" = "egyéb hibák"; + /* member role */ "owner" = "tulajdonos"; @@ -2847,10 +3235,10 @@ "Password to show" = "Jelszó megjelenítése"; /* past/unknown group member */ -"Past member %@" = "Korábbi csoport tag %@"; +"Past member %@" = "Már nem tag - %@"; /* No comment provided by engineer. */ -"Paste desktop address" = "Számítógép azonosítójának beillesztése"; +"Paste desktop address" = "Számítógép címének beillesztése"; /* No comment provided by engineer. */ "Paste image" = "Kép beillesztése"; @@ -2864,6 +3252,9 @@ /* No comment provided by engineer. */ "peer-to-peer" = "ponttól-pontig"; +/* No comment provided by engineer. */ +"Pending" = "Függő"; + /* No comment provided by engineer. */ "People can connect to you only via the links you share." = "Az emberek csak az ön által megosztott hivatkozáson keresztül kapcsolódhatnak."; @@ -2882,9 +3273,18 @@ /* No comment provided by engineer. */ "PING interval" = "PING időköze"; +/* No comment provided by engineer. */ +"Play from the chat list." = "Lejátszás a csevegési listából."; + +/* No comment provided by engineer. */ +"Please ask your contact to enable calls." = "Kérje meg az ismerősét, hogy engedélyezze a hívásokat."; + /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "Ismerős felkérése, hogy engedélyezze a hangüzenetek küldését."; +/* No comment provided by engineer. */ +"Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.\nPlease share any other issues with the developers." = "Ellenőrizze, hogy a mobil és az asztali számítógép ugyanahhoz a helyi hálózathoz csatlakozik-e, valamint az asztali számítógép tűzfalában engedélyezve van-e a kapcsolat.\nMinden további problémát osszon meg a fejlesztőkkel."; + /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "Ellenőrizze, hogy a megfelelő hivatkozást használta-e, vagy kérje meg ismerősét, hogy küldjön egy másikat."; @@ -2942,6 +3342,9 @@ /* No comment provided by engineer. */ "Preview" = "Előnézet"; +/* No comment provided by engineer. */ +"Previously connected servers" = "Korábban kapcsolódott kiszolgálók"; + /* No comment provided by engineer. */ "Privacy & security" = "Adatvédelem és biztonság"; @@ -2951,9 +3354,21 @@ /* No comment provided by engineer. */ "Private filenames" = "Privát fájl nevek"; +/* No comment provided by engineer. */ +"Private message routing" = "Privát üzenet útválasztás"; + +/* No comment provided by engineer. */ +"Private message routing 🚀" = "Privát üzenet útválasztás 🚀"; + /* name of notes to self */ "Private notes" = "Privát jegyzetek"; +/* No comment provided by engineer. */ +"Private routing" = "Privát útválasztás"; + +/* No comment provided by engineer. */ +"Private routing error" = "Privát útválasztási hiba"; + /* No comment provided by engineer. */ "Profile and server connections" = "Profil és kiszolgálókapcsolatok"; @@ -2972,26 +3387,29 @@ /* No comment provided by engineer. */ "Profile password" = "Profiljelszó"; +/* No comment provided by engineer. */ +"Profile theme" = "Profiltéma"; + /* No comment provided by engineer. */ "Profile update will be sent to your contacts." = "A profilfrissítés elküldésre került az ismerősök számára."; /* No comment provided by engineer. */ -"Prohibit audio/video calls." = "Hang- és videóhívások tiltása."; +"Prohibit audio/video calls." = "A hívások kezdeményezése le van tiltva."; /* No comment provided by engineer. */ "Prohibit irreversible message deletion." = "Az üzenetek véglegesen való törlése le van tiltva."; /* No comment provided by engineer. */ -"Prohibit message reactions." = "Üzenetreakciók tiltása."; +"Prohibit message reactions." = "Az üzenetreakciók küldése le van tiltva."; /* No comment provided by engineer. */ "Prohibit messages reactions." = "Az üzenetreakciók tiltása."; /* No comment provided by engineer. */ -"Prohibit sending direct messages to members." = "Közvetlen üzenetek küldésének letiltása a tagok számára."; +"Prohibit sending direct messages to members." = "A közvetlen üzenetek küldése a tagok között le van tiltva."; /* No comment provided by engineer. */ -"Prohibit sending disappearing messages." = "Eltűnő üzenetek küldésének letiltása."; +"Prohibit sending disappearing messages." = "Az eltűnő üzenetek küldése le van tiltva."; /* No comment provided by engineer. */ "Prohibit sending files and media." = "Fájlok- és a médiatartalom küldés letiltása."; @@ -3000,20 +3418,32 @@ "Prohibit sending SimpleX links." = "A SimpleX hivatkozások küldése le van tiltva."; /* No comment provided by engineer. */ -"Prohibit sending voice messages." = "Hangüzenetek küldésének letiltása."; +"Prohibit sending voice messages." = "A hangüzenetek küldése le van tiltva."; /* No comment provided by engineer. */ "Protect app screen" = "Alkalmazás képernyőjének védelme"; +/* No comment provided by engineer. */ +"Protect IP address" = "IP-cím védelem"; + /* No comment provided by engineer. */ "Protect your chat profiles with a password!" = "Csevegési profiljok védelme jelszóval!"; +/* No comment provided by engineer. */ +"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Védje IP-címét az ismerősei által kiválasztott üzenetküldő átjátszókkal szemben.\nEngedélyezze a Beállítások / Hálózat és kiszolgálók menüben."; + /* No comment provided by engineer. */ "Protocol timeout" = "Protokoll időtúllépés"; /* No comment provided by engineer. */ "Protocol timeout per KB" = "Protokoll időkorlát KB-onként"; +/* No comment provided by engineer. */ +"Proxied" = "Proxyzott"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Proxyzott kiszolgálók"; + /* No comment provided by engineer. */ "Push notifications" = "Push értesítések"; @@ -3029,10 +3459,13 @@ /* No comment provided by engineer. */ "Rate the app" = "Értékelje az alkalmazást"; +/* No comment provided by engineer. */ +"Reachable chat toolbar" = "Könnyen elérhető eszköztár"; + /* chat item menu */ "React…" = "Reagálj…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "Olvasd el"; /* No comment provided by engineer. */ @@ -3056,6 +3489,9 @@ /* No comment provided by engineer. */ "Receipts are disabled" = "Üzenet kézbesítési jelentés letiltva"; +/* No comment provided by engineer. */ +"Receive errors" = "Üzenetfogadási hibák"; + /* No comment provided by engineer. */ "received answer…" = "fogadott válasz…"; @@ -3075,10 +3511,16 @@ "Received message" = "Fogadott üzenet"; /* No comment provided by engineer. */ -"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "A fogadó cím egy másik kiszolgálóra változik. A címváltoztatás a feladó online állapotba kerülése után fejeződik be."; +"Received messages" = "Fogadott üzenetek"; /* No comment provided by engineer. */ -"Receiving concurrency" = "Egyidejű fogadás"; +"Received reply" = "Fogadott válasz"; + +/* No comment provided by engineer. */ +"Received total" = "Összes fogadott"; + +/* No comment provided by engineer. */ +"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "A fogadó cím egy másik kiszolgálóra változik. A címváltoztatás a feladó online állapotba kerülése után fejeződik be."; /* No comment provided by engineer. */ "Receiving file will be stopped." = "A fájl fogadása leállt."; @@ -3095,9 +3537,24 @@ /* No comment provided by engineer. */ "Recipients see updates as you type them." = "A címzettek a beírás közben látják a frissítéseket."; +/* No comment provided by engineer. */ +"Reconnect" = "Újrakapcsolás"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Újrakapcsolódás az összes kiszolgálóhoz az üzenetek kézbesítésének kikényszerítéséhez. Ez további forgalmat használ."; +/* No comment provided by engineer. */ +"Reconnect all servers" = "Újrakapcsolódás minden kiszolgálóhoz"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "Újrakapcsolódás minden kiszolgálóhoz?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "A kiszolgálóhoz való újrakapcsolódás az üzenet kézbesítésének kikényszerítéséhez. Ez további adatforgalmat használ."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "Újrakapcsolódás a kiszolgálóhoz?"; + /* No comment provided by engineer. */ "Reconnect servers?" = "Újrakapcsolódás a kiszolgálókhoz?"; @@ -3110,7 +3567,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Csökkentett akkumulátorhasználat"; -/* reject incoming call via notification */ +/* reject incoming call via notification + swipe action */ "Reject" = "Elutasítás"; /* No comment provided by engineer. */ @@ -3123,34 +3581,37 @@ "rejected call" = "elutasított hívás"; /* No comment provided by engineer. */ -"Relay server is only used if necessary. Another party can observe your IP address." = "Az átjátszó kiszolgáló csak szükség esetén kerül használatra. Egy másik fél megfigyelheti az IP-címét."; +"Relay server is only used if necessary. Another party can observe your IP address." = "Az átjátszó kiszolgáló csak szükség esetén kerül használatra. Egy másik fél megfigyelheti az IP-címet."; /* No comment provided by engineer. */ -"Relay server protects your IP address, but it can observe the duration of the call." = "Az átjátszó kiszolgáló megvédi IP-címét, de megfigyelheti a hívás időtartamát."; +"Relay server protects your IP address, but it can observe the duration of the call." = "Az átjátszó kiszolgáló megvédi az IP-címet, de megfigyelheti a hívás időtartamát."; /* No comment provided by engineer. */ "Remove" = "Eltávolítás"; /* No comment provided by engineer. */ -"Remove member" = "Tag eltávolítása"; +"Remove image" = "Kép eltávolítása"; /* No comment provided by engineer. */ -"Remove member?" = "Tag eltávolítása?"; +"Remove member" = "Eltávolítás"; /* No comment provided by engineer. */ -"Remove passphrase from keychain?" = "Jelmondat eltávolítása a kulcstárolóból?"; +"Remove member?" = "Biztosan eltávolítja?"; + +/* No comment provided by engineer. */ +"Remove passphrase from keychain?" = "Jelmondat eltávolítása a kulcstartóból?"; /* No comment provided by engineer. */ "removed" = "eltávolítva"; /* rcv group event chat item */ -"removed %@" = "%@ eltávolítva"; +"removed %@" = "eltávolította őt: %@"; /* profile update event chat item */ -"removed contact address" = "törölt kapcsolattartási azonosító"; +"removed contact address" = "törölt kapcsolattartási cím"; /* profile update event chat item */ -"removed profile picture" = "törölt profilkép"; +"removed profile picture" = "törölte a profilképét"; /* rcv group event chat item */ "removed you" = "eltávolítottak"; @@ -3183,17 +3644,32 @@ "Reply" = "Válasz"; /* No comment provided by engineer. */ -"Required" = "Megkövetelt"; +"Required" = "Szükséges"; /* No comment provided by engineer. */ "Reset" = "Alaphelyzetbe állítás"; +/* No comment provided by engineer. */ +"Reset all hints" = "Tippek visszaállítása"; + +/* No comment provided by engineer. */ +"Reset all statistics" = "Minden statisztika visszaállítása"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "Minden statisztika visszaállítása?"; + /* No comment provided by engineer. */ "Reset colors" = "Színek alaphelyzetbe állítása"; +/* No comment provided by engineer. */ +"Reset to app theme" = "Alkalmazás témájának visszaállítása"; + /* No comment provided by engineer. */ "Reset to defaults" = "Alaphelyzetbe állítás"; +/* No comment provided by engineer. */ +"Reset to user theme" = "Felhasználó által létrehozott téma visszaállítása"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Új csevegési profil létrehozásához indítsa újra az alkalmazást"; @@ -3218,9 +3694,6 @@ /* chat item action */ "Reveal" = "Felfedés"; -/* No comment provided by engineer. */ -"Revert" = "Visszaállít"; - /* No comment provided by engineer. */ "Revoke" = "Visszavonás"; @@ -3236,6 +3709,9 @@ /* No comment provided by engineer. */ "Run chat" = "Csevegési szolgáltatás indítása"; +/* No comment provided by engineer. */ +"Safely receive files" = "Fájlok biztonságos fogadása"; + /* No comment provided by engineer. */ "Safer groups" = "Biztonságosabb csoportok"; @@ -3252,7 +3728,10 @@ "Save and notify group members" = "Mentés és a csoporttagok értesítése"; /* No comment provided by engineer. */ -"Save and update group profile" = "Mentés és a csoport profil frissítése"; +"Save and reconnect" = "Mentés és újrakapcsolódás"; + +/* No comment provided by engineer. */ +"Save and update group profile" = "Mentés és csoportprofil frissítése"; /* No comment provided by engineer. */ "Save archive" = "Archívum mentése"; @@ -3261,13 +3740,13 @@ "Save auto-accept settings" = "Automatikus elfogadási beállítások mentése"; /* No comment provided by engineer. */ -"Save group profile" = "Csoport profil elmentése"; +"Save group profile" = "Csoportprofil elmentése"; /* No comment provided by engineer. */ "Save passphrase and open chat" = "Jelmondat elmentése és csevegés megnyitása"; /* No comment provided by engineer. */ -"Save passphrase in Keychain" = "Jelmondat mentése a kulcstárban"; +"Save passphrase in Keychain" = "Jelmondat mentése a kulcstartóba"; /* No comment provided by engineer. */ "Save preferences?" = "Beállítások mentése?"; @@ -3306,7 +3785,13 @@ "Saved WebRTC ICE servers will be removed" = "A mentett WebRTC ICE kiszolgálók eltávolításra kerülnek"; /* No comment provided by engineer. */ -"Scan code" = "Kód beolvasása"; +"Scale" = "Méretezés"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Hivatkozás beolvasása / beillesztése"; + +/* No comment provided by engineer. */ +"Scan code" = "Beolvasás"; /* No comment provided by engineer. */ "Scan QR code" = "QR-kód beolvasása"; @@ -3320,6 +3805,9 @@ /* No comment provided by engineer. */ "Scan server QR code" = "A kiszolgáló QR-kódjának beolvasása"; +/* No comment provided by engineer. */ +"search" = "keresés"; + /* No comment provided by engineer. */ "Search" = "Keresés"; @@ -3332,6 +3820,9 @@ /* network option */ "sec" = "mp"; +/* No comment provided by engineer. */ +"Secondary" = "Másodlagos"; + /* time unit */ "seconds" = "másodperc"; @@ -3341,6 +3832,9 @@ /* server test step */ "Secure queue" = "Biztonságos várólista"; +/* No comment provided by engineer. */ +"Secured" = "Biztosítva"; + /* No comment provided by engineer. */ "Security assessment" = "Biztonsági kiértékelés"; @@ -3350,9 +3844,15 @@ /* chat item text */ "security code changed" = "a biztonsági kód megváltozott"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "Választás"; +/* No comment provided by engineer. */ +"Selected %lld" = "%lld kiválasztva"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "A kiválasztott csevegési beállítások tiltják ezt az üzenetet."; + /* No comment provided by engineer. */ "Self-destruct" = "Önmegsemmisítés"; @@ -3377,21 +3877,30 @@ /* No comment provided by engineer. */ "send direct message" = "közvetlen üzenet küldése"; -/* No comment provided by engineer. */ -"Send direct message" = "Közvetlen üzenet küldése"; - /* No comment provided by engineer. */ "Send direct message to connect" = "Közvetlen üzenet küldése a kapcsolódáshoz"; /* No comment provided by engineer. */ "Send disappearing message" = "Eltűnő üzenet küldése"; +/* No comment provided by engineer. */ +"Send errors" = "Üzenetküldési hibák"; + /* No comment provided by engineer. */ "Send link previews" = "Hivatkozás előnézetek küldése"; /* No comment provided by engineer. */ "Send live message" = "Élő üzenet küldése"; +/* No comment provided by engineer. */ +"Send message to enable calls." = "Üzenet küldése a hívások engedélyezéséhez."; + +/* No comment provided by engineer. */ +"Send messages directly when IP address is protected and your or destination server does not support private routing." = "Közvetlen üzenetküldés, ha az IP-cím védett és az ön kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást."; + +/* No comment provided by engineer. */ +"Send messages directly when your or destination server does not support private routing." = "Közvetlen üzenetküldés, ha az ön kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást."; + /* No comment provided by engineer. */ "Send notifications" = "Értesítések küldése"; @@ -3411,7 +3920,7 @@ "Send up to 100 last messages to new members." = "Az utolsó 100 üzenet elküldése az új tagoknak."; /* No comment provided by engineer. */ -"Sender cancelled file transfer." = "A küldő megszakította a fájl átvitelt."; +"Sender cancelled file transfer." = "A fájl küldője visszavonta az átvitelt."; /* No comment provided by engineer. */ "Sender may have deleted the connection request." = "A küldő törölhette a kapcsolódási kérelmet."; @@ -3446,15 +3955,42 @@ /* copied message info */ "Sent at: %@" = "Elküldve ekkor: %@"; +/* No comment provided by engineer. */ +"Sent directly" = "Közvetlenül küldött"; + /* notification */ "Sent file event" = "Elküldött fájl esemény"; /* message info title */ "Sent message" = "Elküldött üzenet"; +/* No comment provided by engineer. */ +"Sent messages" = "Elküldött üzenetek"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "Az elküldött üzenetek törlésre kerülnek a beállított idő után."; +/* No comment provided by engineer. */ +"Sent reply" = "Elküldött válasz"; + +/* No comment provided by engineer. */ +"Sent total" = "Összes elküldött"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Proxyn keresztül küldve"; + +/* No comment provided by engineer. */ +"Server address" = "Kiszolgáló címe"; + +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "A kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %@."; + +/* srv error text. */ +"Server address is incompatible with network settings." = "A kiszolgáló címe nem kompatibilis a hálózati beállításokkal."; + +/* queue info */ +"server queue info: %@\n\nlast received msg: %@" = "kiszolgáló üzenet-várakotatási információ: %1$@\n\nutoljára fogadott üzenet: %2$@"; + /* server test error */ "Server requires authorization to create queues, check password" = "A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze jelszavát"; @@ -3462,11 +3998,26 @@ "Server requires authorization to upload, check password" = "A kiszolgálónak engedélyre van szüksége a várólisták feltöltéséhez, ellenőrizze jelszavát"; /* No comment provided by engineer. */ -"Server test failed!" = "Sikertelen kiszolgáló-teszt!"; +"Server test failed!" = "Sikertelen kiszolgáló teszt!"; + +/* No comment provided by engineer. */ +"Server type" = "Kiszolgáló típusa"; + +/* srv error text */ +"Server version is incompatible with network settings." = "A kiszolgáló verziója nem kompatibilis a hálózati beállításokkal."; + +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "A kiszolgáló verziója nem kompatibilis az alkalmazással: %@."; /* No comment provided by engineer. */ "Servers" = "Kiszolgálók"; +/* No comment provided by engineer. */ +"Servers info" = "információk a kiszolgálókról"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "A kiszolgálók statisztikái visszaállnak - ez nem vonható vissza!"; + /* No comment provided by engineer. */ "Session code" = "Munkamenet kód"; @@ -3476,6 +4027,9 @@ /* No comment provided by engineer. */ "Set contact name…" = "Ismerős nevének beállítása…"; +/* No comment provided by engineer. */ +"Set default theme" = "Alapértelmezett téma beállítása"; + /* No comment provided by engineer. */ "Set group preferences" = "Csoportbeállítások megadása"; @@ -3483,10 +4037,10 @@ "Set it instead of system authentication." = "Rendszerhitelesítés helyetti beállítás."; /* profile update event chat item */ -"set new contact address" = "új kapcsolattartási azonosító beállítása"; +"set new contact address" = "új kapcsolattartási cím beállítása"; /* profile update event chat item */ -"set new profile picture" = "új profilkép beállítása"; +"set new profile picture" = "új profilképet állított be"; /* No comment provided by engineer. */ "Set passcode" = "Jelkód beállítása"; @@ -3516,10 +4070,13 @@ "Share 1-time link" = "Egyszer használatos hivatkozás megosztása"; /* No comment provided by engineer. */ -"Share address" = "Azonosító megosztása"; +"Share address" = "Cím megosztása"; /* No comment provided by engineer. */ -"Share address with contacts?" = "Megosztja az azonosítót az ismerőseivel?"; +"Share address with contacts?" = "Megosztja a címet az ismerőseivel?"; + +/* No comment provided by engineer. */ +"Share from other apps." = "Megosztás más alkalmazásokból."; /* No comment provided by engineer. */ "Share link" = "Hivatkozás megosztása"; @@ -3528,7 +4085,13 @@ "Share this 1-time invite link" = "Egyszer használatos meghívó hivatkozás megosztása"; /* No comment provided by engineer. */ -"Share with contacts" = "Megosztás ismerősökkel"; +"Share to SimpleX" = "Megosztás a SimpleX-ben"; + +/* No comment provided by engineer. */ +"Share with contacts" = "Megosztás az ismerősökkel"; + +/* No comment provided by engineer. */ +"Show → on messages sent via private routing." = "Egy „→” jel megjelenítése a privát útválasztáson keresztül küldött üzeneteknél."; /* No comment provided by engineer. */ "Show calls in phone history" = "Hívások megjelenítése a híváslistában"; @@ -3539,6 +4102,12 @@ /* No comment provided by engineer. */ "Show last messages" = "Utolsó üzenetek megjelenítése"; +/* No comment provided by engineer. */ +"Show message status" = "Üzenet állapot megjelenítése"; + +/* No comment provided by engineer. */ +"Show percentage" = "Százalék megjelenítése"; + /* No comment provided by engineer. */ "Show preview" = "Előnézet megjelenítése"; @@ -3549,16 +4118,19 @@ "Show:" = "Megjelenítés:"; /* No comment provided by engineer. */ -"SimpleX address" = "SimpleX azonosító"; +"SimpleX" = "SimpleX"; /* No comment provided by engineer. */ -"SimpleX Address" = "SimpleX azonosító"; +"SimpleX address" = "SimpleX cím"; + +/* No comment provided by engineer. */ +"SimpleX Address" = "SimpleX cím"; /* No comment provided by engineer. */ "SimpleX Chat security was audited by Trail of Bits." = "A SimpleX Chat biztonsága a Trail of Bits által lett auditálva."; /* simplex link type */ -"SimpleX contact address" = "SimpleX kapcsolattartási azonosító"; +"SimpleX contact address" = "SimpleX kapcsolattartási cím"; /* notification */ "SimpleX encrypted message or connection event" = "SimpleX titkosított üzenet vagy kapcsolati esemény"; @@ -3570,22 +4142,22 @@ "SimpleX links" = "SimpleX hivatkozások"; /* No comment provided by engineer. */ -"SimpleX links are prohibited in this group." = "A SimpleX hivatkozások küldése ebben a csoportban le van tiltva."; +"SimpleX links are prohibited in this group." = "A SimpleX hivatkozások küldése le van tiltva ebben a csoportban."; /* No comment provided by engineer. */ "SimpleX links not allowed" = "A SimpleX hivatkozások küldése le van tiltva"; /* No comment provided by engineer. */ -"SimpleX Lock" = "SimpleX zárolás"; +"SimpleX Lock" = "SimpleX zár"; /* No comment provided by engineer. */ -"SimpleX Lock mode" = "SimpleX zárolási mód"; +"SimpleX Lock mode" = "Zárolási mód"; /* No comment provided by engineer. */ -"SimpleX Lock not enabled!" = "SimpleX zárolás nincs engedélyezve!"; +"SimpleX Lock not enabled!" = "A SimpleX zár nincs bekapcsolva!"; /* No comment provided by engineer. */ -"SimpleX Lock turned on" = "SimpleX zárolás bekapcsolva"; +"SimpleX Lock turned on" = "SimpleX zár bekapcsolva"; /* simplex link type */ "SimpleX one-time invitation" = "SimpleX egyszer használatos meghívó"; @@ -3593,6 +4165,9 @@ /* No comment provided by engineer. */ "Simplified incognito mode" = "Egyszerűsített inkognító mód"; +/* No comment provided by engineer. */ +"Size" = "Méret"; + /* No comment provided by engineer. */ "Skip" = "Kihagyás"; @@ -3603,10 +4178,19 @@ "Small groups (max 20)" = "Kis csoportok (max. 20 tag)"; /* No comment provided by engineer. */ -"SMP servers" = "Üzenetküldő (SMP) kiszolgálók"; +"SMP server" = "SMP-kiszolgáló"; + +/* blur media */ +"Soft" = "Enyhe"; /* No comment provided by engineer. */ -"Some non-fatal errors occurred during import - you may see Chat console for more details." = "Néhány nem végzetes hiba történt az importálás során – további részletekért a csevegési konzolban olvashat."; +"Some file(s) were not exported:" = "Néhány fájl nem került exportálásra:"; + +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import - you may see Chat console for more details." = "Néhány nem végzetes hiba történt az importálás közben – további részletekért a csevegési konzolban olvashat."; + +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import:" = "Néhány nem végzetes hiba történt az importálás közben:"; /* notification title */ "Somebody" = "Valaki"; @@ -3626,9 +4210,15 @@ /* No comment provided by engineer. */ "Start migration" = "Átköltöztetés indítása"; +/* No comment provided by engineer. */ +"Starting from %@." = "Kezdve ettől %@."; + /* No comment provided by engineer. */ "starting…" = "indítás…"; +/* No comment provided by engineer. */ +"Statistics" = "Statisztikák"; + /* No comment provided by engineer. */ "Stop" = "Megállítás"; @@ -3668,9 +4258,21 @@ /* No comment provided by engineer. */ "strike" = "áthúzott"; +/* blur media */ +"Strong" = "Erős"; + /* No comment provided by engineer. */ "Submit" = "Elküldés"; +/* No comment provided by engineer. */ +"Subscribed" = "Feliratkozva"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Feliratkozási hibák"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Elutasított feliratkozások"; + /* No comment provided by engineer. */ "Support SimpleX Chat" = "Támogassa a SimpleX Chatet"; @@ -3681,7 +4283,7 @@ "System authentication" = "Rendszerhitelesítés"; /* No comment provided by engineer. */ -"Take picture" = "Fotó készítése"; +"Take picture" = "Kép készítése"; /* No comment provided by engineer. */ "Tap button " = "Koppintson a "; @@ -3705,7 +4307,7 @@ "Tap to scan" = "Koppintson a beolvasáshoz"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Koppintson az új csevegés indításához"; +"TCP connection" = "TCP kapcsolat"; /* No comment provided by engineer. */ "TCP connection timeout" = "TCP kapcsolat időtúllépés"; @@ -3719,6 +4321,9 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* No comment provided by engineer. */ +"Temporary file error" = "Ideiglenes fájlhiba"; + /* server test failure */ "Test failed at step %@." = "A teszt sikertelen volt a(z) %@ lépésnél."; @@ -3746,6 +4351,9 @@ /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Az alkalmazás értesíteni fogja, amikor üzeneteket vagy kapcsolatfelvételi kéréseket kap – beállítások megnyitása az engedélyezéshez."; +/* No comment provided by engineer. */ +"The app will ask to confirm downloads from unknown file servers (except .onion)." = "Az alkalmazás kérni fogja az ismeretlen fájlkiszolgálókról (kivéve .onion) történő letöltések megerősítését."; + /* No comment provided by engineer. */ "The attempt to change database passphrase was not completed." = "Az adatbázis jelmondatának megváltoztatására tett kísérlet nem fejeződött be."; @@ -3753,19 +4361,19 @@ "The code you scanned is not a SimpleX link QR code." = "A beolvasott kód nem egy SimpleX hivatkozás QR-kód."; /* No comment provided by engineer. */ -"The connection you accepted will be cancelled!" = "Az ön által elfogadott kapcsolat megszakad!"; +"The connection you accepted will be cancelled!" = "Az ön által elfogadott kapcsolat vissza lesz vonva!"; /* No comment provided by engineer. */ "The contact you shared this link with will NOT be able to connect!" = "Ismerőse, akivel megosztotta ezt a hivatkozást, NEM fog tudni kapcsolódni!"; /* No comment provided by engineer. */ -"The created archive is available via app Settings / Database / Old database archive." = "A létrehozott archívum a Beállítások / Adatbázis / Régi adatbázis-archívum menüpontban érhető el."; +"The created archive is available via app Settings / Database / Old database archive." = "A létrehozott archívum a Beállítások / Adatbázis / Régi adatbázis-archívum menüben érhető el."; /* No comment provided by engineer. */ "The encryption is working and the new encryption agreement is not required. It may result in connection errors!" = "A titkosítás működik, és új titkosítási egyezményre nincs szükség. Ez kapcsolati hibákat eredményezhet!"; /* No comment provided by engineer. */ -"The hash of the previous message is different." = "Az előző üzenet hash-e más."; +"The hash of the previous message is different." = "Az előző üzenet ellenőrzőösszege különbözik."; /* No comment provided by engineer. */ "The ID of the next message is incorrect (less or equal to the previous).\nIt can happen because of some bug or when the connection is compromised." = "A következő üzenet azonosítója hibás (kisebb vagy egyenlő az előzővel).\nEz valamilyen hiba, vagy sérült kapcsolat esetén fordulhat elő."; @@ -3776,11 +4384,17 @@ /* No comment provided by engineer. */ "The message will be marked as moderated for all members." = "Az üzenet minden tag számára moderáltként lesz megjelölve."; +/* No comment provided by engineer. */ +"The messages will be deleted for all members." = "Az üzenetek minden tag számára törlésre kerülnek."; + +/* No comment provided by engineer. */ +"The messages will be marked as moderated for all members." = "Az üzenetek moderáltként lesznek megjelölve minden tag számára."; + /* No comment provided by engineer. */ "The next generation of private messaging" = "A privát üzenetküldés következő generációja"; /* No comment provided by engineer. */ -"The old database was not removed during the migration, it can be deleted." = "A régi adatbázis nem került eltávolításra az átköltöztetés során, így törölhető."; +"The old database was not removed during the migration, it can be deleted." = "A régi adatbázis nem került eltávolításra az átköltöztetés közben, így törölhető."; /* No comment provided by engineer. */ "The profile is only shared with your contacts." = "Profilja csak az ismerőseivel kerül megosztásra."; @@ -3798,7 +4412,7 @@ "The text you pasted is not a SimpleX link." = "A beillesztett szöveg nem egy SimpleX hivatkozás."; /* No comment provided by engineer. */ -"Theme" = "Téma"; +"Themes" = "Témák"; /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Ezek a beállítások a jelenlegi **%@** profiljára vonatkoznak."; @@ -3807,7 +4421,7 @@ "They can be overridden in contact and group settings." = "Ezek felülbírálhatóak az ismerős- és csoportbeállításokban."; /* No comment provided by engineer. */ -"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Ez a művelet nem vonható vissza - az összes fogadott és küldött fájl a médiatartalommal együtt törlésre kerülnek. Az alacsony felbontású fotók viszont megmaradnak."; +"This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain." = "Ez a művelet nem vonható vissza - az összes fogadott és küldött fájl a médiatartalommal együtt törlésre kerülnek. Az alacsony felbontású képek viszont megmaradnak."; /* No comment provided by engineer. */ "This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes." = "Ez a művelet nem vonható vissza - a kiválasztottnál korábban küldött és fogadott üzenetek törlésre kerülnek. Ez több percet is igénybe vehet."; @@ -3840,11 +4454,17 @@ "This is your own one-time link!" = "Ez az egyszer használatos hivatkozása!"; /* No comment provided by engineer. */ -"This is your own SimpleX address!" = "Ez a SimpleX azonosítója!"; +"This is your own SimpleX address!" = "Ez az ön SimpleX címe!"; + +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "Ezt a hivatkozást egy másik mobilleszközön már használták, hozzon létre egy új hivatkozást az asztali számítógépén."; /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Ez a beállítás a jelenlegi **%@** profiljában lévő üzenetekre érvényes."; +/* No comment provided by engineer. */ +"Title" = "Cím"; + /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Bármilyen kérdés feltevéséhez és a frissítésekért:"; @@ -3864,26 +4484,41 @@ "To protect timezone, image/voice files use UTC." = "Az időzóna védelme érdekében a kép-/hangfájlok UTC-t használnak."; /* No comment provided by engineer. */ -"To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Az adatavédelem érdekében kapcsolja be a SimpleX zárolás funkciót.\nA funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befejezésére."; +"To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "A biztonsága érdekében kapcsolja be a SimpleX zár funkciót.\nA funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beállítására az eszközén."; + +/* No comment provided by engineer. */ +"To protect your IP address, private routing uses your SMP servers to deliver messages." = "Az IP-cím védelmének érdekében a privát útválasztás az SMP kiszolgálókat használja az üzenetek kézbesítéséhez."; /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Hangüzenet rögzítéséhez adjon engedélyt a mikrofon használathoz."; /* No comment provided by engineer. */ -"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Rejtett profilja feltárásához írja be a teljes jelszót a keresőmezőbe a **Csevegési profiljai** oldalon."; +"To reveal your hidden profile, enter a full password into a search field in **Your chat profiles** page." = "Rejtett profilja megjelenítéséhez írja be a teljes jelszavát a keresőmezőbe a **Csevegési profilok** menüben."; /* No comment provided by engineer. */ "To support instant push notifications the chat database has to be migrated." = "Az azonnali push értesítések támogatásához a csevegési adatbázis migrálása szükséges."; /* No comment provided by engineer. */ -"To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "A végpontok közötti titkosítás ellenőrzéséhez ismerősével hasonlítsa össze (vagy szkennelje be) az eszközén lévő kódot."; +"To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "A végpontok közötti titkosítás ellenőrzéséhez hasonlítsa össze (vagy olvassa be a QR-kódot) az ismerőse eszközén lévő kóddal."; + +/* No comment provided by engineer. */ +"Toggle chat list:" = "Csevegőlista átváltása:"; /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Inkognitó mód kapcsolódáskor."; +/* No comment provided by engineer. */ +"Toolbar opacity" = "Eszköztár átlátszatlansága"; + +/* No comment provided by engineer. */ +"Total" = "Összesen"; + /* No comment provided by engineer. */ "Transport isolation" = "Kapcsolat izolációs mód"; +/* No comment provided by engineer. */ +"Transport sessions" = "Munkamenetek átvitele"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Kapcsolódási kísérlet ahhoz a kiszolgálóhoz, amely az adott ismerőstől érkező üzenetek fogadására szolgál (hiba: %@)."; @@ -3918,15 +4553,12 @@ "Unblock member?" = "Tag feloldása?"; /* rcv group event chat item */ -"unblocked %@" = "%@ feloldva"; - -/* item status description */ -"Unexpected error: %@" = "Váratlan hiba: %@"; +"unblocked %@" = "feloldotta %@ letiltását"; /* No comment provided by engineer. */ "Unexpected migration state" = "Váratlan átköltöztetési állapot"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "Nem kedvelt."; /* No comment provided by engineer. */ @@ -3953,6 +4585,12 @@ /* No comment provided by engineer. */ "Unknown error" = "Ismeretlen hiba"; +/* No comment provided by engineer. */ +"unknown servers" = "ismeretlen átjátszók"; + +/* No comment provided by engineer. */ +"Unknown servers!" = "Ismeretlen kiszolgálók!"; + /* No comment provided by engineer. */ "unknown status" = "ismeretlen státusz"; @@ -3975,9 +4613,15 @@ "Unlock app" = "Alkalmazás feloldása"; /* No comment provided by engineer. */ +"unmute" = "némítás feloldása"; + +/* swipe action */ "Unmute" = "Némítás feloldása"; /* No comment provided by engineer. */ +"unprotected" = "nem védett"; + +/* swipe action */ "Unread" = "Olvasatlan"; /* No comment provided by engineer. */ @@ -3986,9 +4630,6 @@ /* No comment provided by engineer. */ "Update" = "Frissítés"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = "Tor .onion kiszolgálók beállításainak frissítése?"; - /* No comment provided by engineer. */ "Update database passphrase" = "Adatbázis jelmondat megváltoztatása"; @@ -3996,22 +4637,22 @@ "Update network settings?" = "Hálózati beállítások megváltoztatása?"; /* No comment provided by engineer. */ -"Update transport isolation mode?" = "Kapcsolat izolációs mód frissítése?"; +"Update settings?" = "Beállítások frissítése?"; /* rcv group event chat item */ -"updated group profile" = "módosított csoport profil"; +"updated group profile" = "frissítette a csoport profilját"; /* profile update event chat item */ "updated profile" = "frissített profil"; /* No comment provided by engineer. */ -"Updating settings will re-connect the client to all servers." = "A beállítások frissítése a szerverekhez újra kapcsolódással jár."; +"Updating settings will re-connect the client to all servers." = "A beállítások frissítése a kiszolgálókhoz való újra kapcsolódással jár."; /* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "A beállítás frissítésével a kliens újrakapcsolódik az összes kiszolgálóhoz."; +"Upgrade and open chat" = "Fejlesztés és a csevegés megnyitása"; /* No comment provided by engineer. */ -"Upgrade and open chat" = "A csevegés frissítése és megnyitása"; +"Upload errors" = "Feltöltési hibák"; /* No comment provided by engineer. */ "Upload failed" = "Sikertelen feltöltés"; @@ -4019,6 +4660,12 @@ /* server test step */ "Upload file" = "Fájl feltöltése"; +/* No comment provided by engineer. */ +"Uploaded" = "Feltöltve"; + +/* No comment provided by engineer. */ +"Uploaded files" = "Feltöltött fájlok"; + /* No comment provided by engineer. */ "Uploading archive" = "Archívum feltöltése"; @@ -4046,6 +4693,12 @@ /* No comment provided by engineer. */ "Use only local notifications?" = "Csak helyi értesítések használata?"; +/* No comment provided by engineer. */ +"Use private routing with unknown servers when IP address is not protected." = "Privát útválasztás használata ismeretlen kiszolgálókkal, ha az IP-cím nem védett."; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers." = "Használjon privát útválasztást ismeretlen kiszolgálókkal."; + /* No comment provided by engineer. */ "Use server" = "Kiszolgáló használata"; @@ -4055,11 +4708,14 @@ /* No comment provided by engineer. */ "Use the app while in the call." = "Használja az alkalmazást hívás közben."; +/* No comment provided by engineer. */ +"Use the app with one hand." = "Használja az alkalmazást egy kézzel."; + /* No comment provided by engineer. */ "User profile" = "Felhasználói profil"; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "A .onion kiszolgálók használatához kompatibilis VPN szolgáltatóra van szükség."; +"User selection" = "Felhasználó kiválasztása"; /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "SimpleX Chat kiszolgálók használatban."; @@ -4095,7 +4751,7 @@ "Via browser" = "Böngészőn keresztül"; /* chat list item description */ -"via contact address link" = "kapcsolattartási azonosító-hivatkozáson keresztül"; +"via contact address link" = "kapcsolattartási cím-hivatkozáson keresztül"; /* chat list item description */ "via group link" = "csoport hivatkozáson keresztül"; @@ -4109,6 +4765,9 @@ /* No comment provided by engineer. */ "Via secure quantum resistant protocol." = "Biztonságos kvantum ellenálló protokoll által."; +/* No comment provided by engineer. */ +"video" = "videó"; + /* No comment provided by engineer. */ "Video call" = "Videóhívás"; @@ -4137,7 +4796,7 @@ "Voice messages" = "Hangüzenetek"; /* No comment provided by engineer. */ -"Voice messages are prohibited in this chat." = "A hangüzenetek le vannak tiltva ebben a csevegésben."; +"Voice messages are prohibited in this chat." = "A hangüzenetek küldése le van tiltva ebben a csevegésben."; /* No comment provided by engineer. */ "Voice messages are prohibited in this group." = "A hangüzenetek küldése le van tiltva ebben a csoportban."; @@ -4149,7 +4808,7 @@ "Voice messages prohibited!" = "A hangüzenetek le vannak tilva!"; /* No comment provided by engineer. */ -"waiting for answer…" = "várakozás válaszra…"; +"waiting for answer…" = "várakozás a válaszra…"; /* No comment provided by engineer. */ "waiting for confirmation…" = "várakozás a visszaigazolásra…"; @@ -4166,6 +4825,12 @@ /* No comment provided by engineer. */ "Waiting for video" = "Videóra várakozás"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "Háttérkép kiemelés"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Háttérkép háttérszíne"; + /* No comment provided by engineer. */ "wants to connect to you!" = "kapcsolatba akar lépni önnel!"; @@ -4199,6 +4864,9 @@ /* No comment provided by engineer. */ "When connecting audio and video calls." = "Amikor egy bejövő hang- vagy videóhívás érkezik."; +/* No comment provided by engineer. */ +"when IP hidden" = "ha az IP-cím rejtett"; + /* No comment provided by engineer. */ "When people request to connect, you can accept or reject it." = "Amikor az emberek kapcsolódást kérelmeznek, ön elfogadhatja vagy elutasíthatja azokat."; @@ -4224,13 +4892,25 @@ "With reduced battery usage." = "Csökkentett akkumulátorhasználattal."; /* No comment provided by engineer. */ -"Wrong database passphrase" = "Téves adatbázis jelmondat"; +"Without Tor or VPN, your IP address will be visible to file servers." = "Tor vagy VPN nélkül az IP-címe látható lesz a fájlkiszolgálók számára."; /* No comment provided by engineer. */ -"Wrong passphrase!" = "Téves jelmondat!"; +"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Tor vagy VPN nélkül az IP-címe látható lesz ezen XFTP átjátszók számára: %@."; /* No comment provided by engineer. */ -"XFTP servers" = "XFTP kiszolgálók"; +"Wrong database passphrase" = "Hibás adatbázis jelmondat"; + +/* snd error text */ +"Wrong key or unknown connection - most likely this connection is deleted." = "Hibás kulcs vagy ismeretlen kapcsolat - valószínűleg ez a kapcsolat törlődött."; + +/* file error text */ +"Wrong key or unknown file chunk address - most likely file is deleted." = "Hibás kulcs vagy ismeretlen fájltöredék cím - valószínűleg a fájl törlődött."; + +/* No comment provided by engineer. */ +"Wrong passphrase!" = "Hibás jelmondat!"; + +/* No comment provided by engineer. */ +"XFTP server" = "XFTP-kiszolgáló"; /* pref value */ "yes" = "igen"; @@ -4245,7 +4925,7 @@ "You **must not** use the same database on two devices." = "**Nem szabad** ugyanazt az adatbázist használni egyszerre két eszközön."; /* No comment provided by engineer. */ -"You accepted connection" = "Kapcsolódás elfogadva"; +"You accepted connection" = "Kapcsolat létrehozása"; /* No comment provided by engineer. */ "You allow" = "Engedélyezte"; @@ -4286,15 +4966,21 @@ /* No comment provided by engineer. */ "You are invited to group" = "Meghívást kapott a csoportba"; +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "Ön nem kapcsolódik ezekhez a kiszolgálókhoz. A privát útválasztás az üzenetek kézbesítésére szolgál."; + /* No comment provided by engineer. */ "you are observer" = "megfigyelő szerep"; /* snd group event chat item */ -"you blocked %@" = "blokkolta őt: %@"; +"you blocked %@" = "ön letiltotta %@-t"; /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "Hívásokat fogadhat a lezárási képernyőről, eszköz- és alkalmazáshitelesítés nélkül."; +/* No comment provided by engineer. */ +"You can change it in Appearance settings." = "Ezt a „Megjelenés” menüben módosíthatja."; + /* No comment provided by engineer. */ "You can create it later" = "Létrehozás később"; @@ -4302,19 +4988,22 @@ "You can enable later via Settings" = "Később engedélyezheti a Beállításokban"; /* No comment provided by engineer. */ -"You can enable them later via app Privacy & Security settings." = "Később engedélyezheti őket az alkalmazás Adatvédelem és biztonság menüpontban."; +"You can enable them later via app Privacy & Security settings." = "Később engedélyezheti őket az alkalmazás „Adatvédelem és biztonság” menüben."; /* No comment provided by engineer. */ "You can give another try." = "Megpróbálhatja még egyszer."; /* No comment provided by engineer. */ -"You can hide or mute a user profile - swipe it to the right." = "Elrejthet vagy némíthat egy felhasználói profilt – csúsztasson jobbra."; +"You can hide or mute a user profile - swipe it to the right." = "Elrejtheti vagy lenémíthatja a felhasználó profiljait - csúsztassa jobbra a profilt."; /* No comment provided by engineer. */ "You can make it visible to your SimpleX contacts via Settings." = "Láthatóvá teheti SimpleX ismerősök számára a Beállításokban."; /* notification body */ -"You can now send messages to %@" = "Mostantól küldhet üzeneteket %@ számára"; +"You can now chat with %@" = "Mostantól küldhet üzeneteket %@ számára"; + +/* No comment provided by engineer. */ +"You can send messages to %@ from Archived contacts." = "Az „Archivált ismerősökből” továbbra is küldhet üzeneteket neki: %@."; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "A beállításokon keresztül beállíthatja a lezárási képernyő értesítési előnézetét."; @@ -4323,16 +5012,19 @@ "You can share a link or a QR code - anybody will be able to join the group. You won't lose members of the group if you later delete it." = "Megoszthat egy hivatkozást vagy QR-kódot - így bárki csatlakozhat a csoporthoz. Ha a csoport később törlésre kerül, akkor nem fogja elveszíteni annak tagjait."; /* No comment provided by engineer. */ -"You can share this address with your contacts to let them connect with **%@**." = "Megoszthatja ezt az azonosítót az ismerőseivel, hogy kapcsolatba léphessenek önnel a **%@** nevű profilján keresztül."; +"You can share this address with your contacts to let them connect with **%@**." = "Megoszthatja ezt a címet az ismerőseivel, hogy kapcsolatba léphessenek önnel a(z) **%@** nevű profilján keresztül."; /* No comment provided by engineer. */ -"You can share your address as a link or QR code - anybody can connect to you." = "Megoszthatja azonosítóját hivatkozásként vagy QR-kódként – így bárki kapcsolódhat önhöz."; +"You can share your address as a link or QR code - anybody can connect to you." = "Megoszthatja a címét egy hivatkozásként vagy QR-kódként – így bárki kapcsolódhat önhöz."; /* No comment provided by engineer. */ -"You can start chat via app Settings / Database or by restarting the app" = "A csevegést az alkalmazás Beállítások / Adatbázis menü segítségével vagy az alkalmazás újraindításával indíthatja el"; +"You can start chat via app Settings / Database or by restarting the app" = "A csevegést az alkalmazás Beállítások / Adatbázis menüben vagy az alkalmazás újraindításával indíthatja el"; /* No comment provided by engineer. */ -"You can turn on SimpleX Lock via Settings." = "A SimpleX zárolás a Beállításokon keresztül kapcsolható be."; +"You can still view conversation with %@ in the list of chats." = "A(z) %@ nevű ismerősével folytatott beszélgetéseit továbbra is megtekintheti a csevegések listájában."; + +/* No comment provided by engineer. */ +"You can turn on SimpleX Lock via Settings." = "A SimpleX zár az „Adatvédelem és biztonság” menüben kapcsolható be."; /* No comment provided by engineer. */ "You can use markdown to format messages:" = "Üzenetek formázása a szövegbe szúrt speciális karakterekkel:"; @@ -4344,10 +5036,10 @@ "You can't send messages!" = "Nem lehet üzeneteket küldeni!"; /* chat item text */ -"you changed address" = "azonosítója megváltoztatva"; +"you changed address" = "cím megváltoztatva"; /* chat item text */ -"you changed address for %@" = "%@ azonosítója megváltoztatva"; +"you changed address for %@" = "cím megváltoztatva nála: %@"; /* snd group event chat item */ "you changed role for yourself to %@" = "saját szerepkör megváltoztatva erre: %@"; @@ -4356,20 +5048,17 @@ "you changed role of %@ to %@" = "%1$@ szerepkörét megváltoztatta erre: %@"; /* No comment provided by engineer. */ -"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Ön szabályozhatja, hogy mely kiszogál(ók)ón keresztül **kapja** az üzeneteket, az ismerősöket - az üzenetküldéshez használt szervereken."; +"You control through which server(s) **to receive** the messages, your contacts – the servers you use to message them." = "Ön szabályozhatja, hogy mely kiszogál(ók)ón keresztül **kapja** az üzeneteket, az ismerősöket - az üzenetküldéshez használt kiszolgálókon."; /* No comment provided by engineer. */ "You could not be verified; please try again." = "Nem lehetett ellenőrizni; próbálja meg újra."; /* No comment provided by engineer. */ -"You have already requested connection via this address!" = "Már kért egy kapcsolódási kérelmet ezen az azonosítón keresztül!"; +"You have already requested connection via this address!" = "Már kért egy kapcsolódási kérelmet ezen a címen keresztül!"; /* No comment provided by engineer. */ "You have already requested connection!\nRepeat connection request?" = "Már kért egy kapcsolódási kérelmet!\nKapcsolódási kérés megismétlése?"; -/* No comment provided by engineer. */ -"You have no chats" = "Nincsenek csevegési üzenetek"; - /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "A jelmondatot minden alkalommal meg kell adnia, amikor az alkalmazás elindul - nem az eszközön kerül tárolásra."; @@ -4385,9 +5074,18 @@ /* snd group event chat item */ "you left" = "elhagyta a csoportot"; +/* No comment provided by engineer. */ +"You may migrate the exported database." = "Az exportált adatbázist átköltöztetheti."; + +/* No comment provided by engineer. */ +"You may save the exported archive." = "Az exportált archívumot elmentheti."; + /* No comment provided by engineer. */ "You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." = "A csevegési adatbázis legfrissebb verzióját CSAK egy eszközön kell használnia, ellenkező esetben előfordulhat, hogy az üzeneteket nem fogja megkapni valamennyi ismerőstől."; +/* No comment provided by engineer. */ +"You need to allow your contact to call to be able to call them." = "Engedélyeznie kell a hívásokat az ismerőse számára, hogy fel tudják hívni egymást."; + /* No comment provided by engineer. */ "You need to allow your contact to send voice messages to be able to send them." = "Hangüzeneteket küldéséhez engedélyeznie kell azok küldését az ismerősök számára."; @@ -4407,7 +5105,7 @@ "you shared one-time link incognito" = "egyszer használatos hivatkozást osztott meg inkognitóban"; /* snd group event chat item */ -"you unblocked %@" = "feloldotta %@ blokkolását"; +"you unblocked %@" = "ön feloldotta %@ letiltását"; /* No comment provided by engineer. */ "You will be connected to group when the group host's device is online, please wait or check later!" = "Akkor lesz kapcsolódva a csoporthoz, amikor a csoport tulajdonosának eszköze online lesz, várjon, vagy ellenőrizze később!"; @@ -4434,7 +5132,7 @@ "You will stop receiving messages from this group. Chat history will be preserved." = "Ettől a csoporttól nem fog értesítéseket kapni. A csevegési előzmények megmaradnak."; /* No comment provided by engineer. */ -"You won't lose your contacts if you later delete your address." = "Nem veszíti el az ismerőseit, ha később törli az azonosítóját."; +"You won't lose your contacts if you later delete your address." = "Nem veszíti el az ismerőseit, ha később törli a címét."; /* No comment provided by engineer. */ "you: " = "ön: "; @@ -4458,10 +5156,7 @@ "Your chat database is not encrypted - set passphrase to encrypt it." = "A csevegési adatbázis nincs titkosítva – adjon meg egy jelmondatot a titkosításhoz."; /* No comment provided by engineer. */ -"Your chat profiles" = "Csevegési profiljai"; - -/* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Az ismerősnek online kell lennie ahhoz, hogy a kapcsolat létrejöjjön.\nMegszakíthatja ezt a kapcsolatfelvételt és törölheti az ismerőst (ezt később ismét megpróbálhatja egy új hivatkozással)."; +"Your chat profiles" = "Csevegési profilok"; /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Ismerőse olyan fájlt küldött, amely meghaladja a jelenleg támogatott maximális méretet (%@)."; @@ -4512,7 +5207,7 @@ "Your settings" = "Beállítások"; /* No comment provided by engineer. */ -"Your SimpleX address" = "SimpleX azonosítója"; +"Your SimpleX address" = "Az ön SimpleX címe"; /* No comment provided by engineer. */ "Your SMP servers" = "SMP kiszolgálók"; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index e27b2e04a8..f3fa0424cc 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -154,9 +154,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ è verificato/a"; -/* No comment provided by engineer. */ -"%@ servers" = "Server %@"; - /* No comment provided by engineer. */ "%@ uploaded" = "%@ caricati"; @@ -338,10 +335,11 @@ "above, then choose:" = "sopra, quindi scegli:"; /* No comment provided by engineer. */ -"Accent color" = "Colore principale"; +"Accent" = "Principale"; /* accept contact request via notification - accept incoming call via notification */ + accept incoming call via notification + swipe action */ "Accept" = "Accetta"; /* No comment provided by engineer. */ @@ -350,12 +348,22 @@ /* notification body */ "Accept contact request from %@?" = "Accettare la richiesta di contatto da %@?"; -/* accept contact request via notification */ +/* accept contact request via notification + swipe action */ "Accept incognito" = "Accetta in incognito"; /* call status */ "accepted call" = "chiamata accettata"; +/* No comment provided by engineer. */ +"Acknowledged" = "Riconosciuto"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Errori di riconoscimento"; + +/* No comment provided by engineer. */ +"Active connections" = "Connessioni attive"; + /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Aggiungi l'indirizzo al tuo profilo, in modo che i tuoi contatti possano condividerlo con altre persone. L'aggiornamento del profilo verrà inviato ai tuoi contatti."; @@ -369,7 +377,7 @@ "Add profile" = "Aggiungi profilo"; /* No comment provided by engineer. */ -"Add server…" = "Aggiungi server…"; +"Add server" = "Aggiungi server"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Aggiungi server scansionando codici QR."; @@ -380,6 +388,15 @@ /* No comment provided by engineer. */ "Add welcome message" = "Aggiungi messaggio di benvenuto"; +/* No comment provided by engineer. */ +"Additional accent" = "Principale aggiuntivo"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Principale aggiuntivo 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "Secondario aggiuntivo"; + /* No comment provided by engineer. */ "Address" = "Indirizzo"; @@ -401,6 +418,9 @@ /* No comment provided by engineer. */ "Advanced network settings" = "Impostazioni di rete avanzate"; +/* No comment provided by engineer. */ +"Advanced settings" = "Impostazioni avanzate"; + /* chat item text */ "agreeing encryption for %@…" = "concordando la crittografia per %@…"; @@ -416,6 +436,9 @@ /* No comment provided by engineer. */ "All data is erased when it is entered." = "Tutti i dati vengono cancellati quando inserito."; +/* No comment provided by engineer. */ +"All data is private to your device." = "Tutti i dati sono privati, nel tuo dispositivo."; + /* No comment provided by engineer. */ "All group members will remain connected." = "Tutti i membri del gruppo resteranno connessi."; @@ -431,6 +454,9 @@ /* No comment provided by engineer. */ "All new messages from %@ will be hidden!" = "Tutti i nuovi messaggi da %@ verrranno nascosti!"; +/* No comment provided by engineer. */ +"All profiles" = "Tutti gli profili"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Tutti i tuoi contatti resteranno connessi."; @@ -446,9 +472,15 @@ /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "Consenti le chiamate solo se il tuo contatto le consente."; +/* No comment provided by engineer. */ +"Allow calls?" = "Consentire le chiamate?"; + /* No comment provided by engineer. */ "Allow disappearing messages only if your contact allows it to you." = "Consenti i messaggi a tempo solo se il contatto li consente a te."; +/* No comment provided by engineer. */ +"Allow downgrade" = "Consenti downgrade"; + /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Consenti l'eliminazione irreversibile dei messaggi solo se il contatto la consente a te. (24 ore)"; @@ -464,6 +496,9 @@ /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Permetti l'invio di messaggi a tempo."; +/* No comment provided by engineer. */ +"Allow sharing" = "Consenti la condivisione"; + /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Permetti di eliminare irreversibilmente i messaggi inviati. (24 ore)"; @@ -509,6 +544,9 @@ /* pref value */ "always" = "sempre"; +/* No comment provided by engineer. */ +"Always use private routing." = "Usa sempre l'instradamento privato."; + /* No comment provided by engineer. */ "Always use relay" = "Connetti via relay"; @@ -551,15 +589,27 @@ /* No comment provided by engineer. */ "Apply" = "Applica"; +/* No comment provided by engineer. */ +"Apply to" = "Applica a"; + /* No comment provided by engineer. */ "Archive and upload" = "Archivia e carica"; +/* No comment provided by engineer. */ +"Archive contacts to chat later." = "Archivia contatti per chattare più tardi."; + +/* No comment provided by engineer. */ +"Archived contacts" = "Contatti archiviati"; + /* No comment provided by engineer. */ "Archiving database" = "Archiviazione del database"; /* No comment provided by engineer. */ "Attach" = "Allega"; +/* No comment provided by engineer. */ +"attempts" = "tentativi"; + /* No comment provided by engineer. */ "Audio & video calls" = "Chiamate audio e video"; @@ -602,6 +652,9 @@ /* No comment provided by engineer. */ "Back" = "Indietro"; +/* No comment provided by engineer. */ +"Background" = "Sfondo"; + /* No comment provided by engineer. */ "Bad desktop address" = "Indirizzo desktop errato"; @@ -623,6 +676,12 @@ /* No comment provided by engineer. */ "Better messages" = "Messaggi migliorati"; +/* No comment provided by engineer. */ +"Better networking" = "Rete migliorata"; + +/* No comment provided by engineer. */ +"Black" = "Nero"; + /* No comment provided by engineer. */ "Block" = "Blocca"; @@ -653,6 +712,12 @@ /* No comment provided by engineer. */ "Blocked by admin" = "Bloccato dall'amministratore"; +/* No comment provided by engineer. */ +"Blur for better privacy." = "Sfoca per una privacy maggiore."; + +/* No comment provided by engineer. */ +"Blur media" = "Sfocatura file multimediali"; + /* No comment provided by engineer. */ "bold" = "grassetto"; @@ -677,6 +742,9 @@ /* No comment provided by engineer. */ "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Per profilo di chat (predefinito) o [per connessione](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; +/* No comment provided by engineer. */ +"call" = "chiama"; + /* No comment provided by engineer. */ "Call already ended!" = "Chiamata già terminata!"; @@ -692,15 +760,27 @@ /* No comment provided by engineer. */ "Calls" = "Chiamate"; +/* No comment provided by engineer. */ +"Calls prohibited!" = "Chiamate proibite!"; + /* No comment provided by engineer. */ "Camera not available" = "Fotocamera non disponibile"; +/* No comment provided by engineer. */ +"Can't call contact" = "Impossibile chiamare il contatto"; + +/* No comment provided by engineer. */ +"Can't call member" = "Impossibile chiamare il membro"; + /* No comment provided by engineer. */ "Can't invite contact!" = "Impossibile invitare il contatto!"; /* No comment provided by engineer. */ "Can't invite contacts!" = "Impossibile invitare i contatti!"; +/* No comment provided by engineer. */ +"Can't message member" = "Impossibile inviare un messaggio al membro"; + /* No comment provided by engineer. */ "Cancel" = "Annulla"; @@ -713,9 +793,15 @@ /* No comment provided by engineer. */ "Cannot access keychain to save database password" = "Impossibile accedere al portachiavi per salvare la password del database"; +/* No comment provided by engineer. */ +"Cannot forward message" = "Impossibile inoltrare il messaggio"; + /* No comment provided by engineer. */ "Cannot receive file" = "Impossibile ricevere il file"; +/* snd error text */ +"Capacity exceeded - recipient did not receive previously sent messages." = "Quota superata - il destinatario non ha ricevuto i messaggi precedentemente inviati."; + /* No comment provided by engineer. */ "Cellular" = "Mobile"; @@ -768,6 +854,9 @@ /* No comment provided by engineer. */ "Chat archive" = "Archivio chat"; +/* No comment provided by engineer. */ +"Chat colors" = "Colori della chat"; + /* No comment provided by engineer. */ "Chat console" = "Console della chat"; @@ -777,6 +866,9 @@ /* No comment provided by engineer. */ "Chat database deleted" = "Database della chat eliminato"; +/* No comment provided by engineer. */ +"Chat database exported" = "Database della chat esportato"; + /* No comment provided by engineer. */ "Chat database imported" = "Database della chat importato"; @@ -789,12 +881,18 @@ /* No comment provided by engineer. */ "Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." = "La chat è ferma. Se hai già usato questo database su un altro dispositivo, dovresti trasferirlo prima di avviare la chat."; +/* No comment provided by engineer. */ +"Chat list" = "Elenco delle chat"; + /* No comment provided by engineer. */ "Chat migrated!" = "Chat migrata!"; /* No comment provided by engineer. */ "Chat preferences" = "Preferenze della chat"; +/* No comment provided by engineer. */ +"Chat theme" = "Tema della chat"; + /* No comment provided by engineer. */ "Chats" = "Chat"; @@ -814,6 +912,15 @@ "Choose from library" = "Scegli dalla libreria"; /* No comment provided by engineer. */ +"Chunks deleted" = "Blocchi eliminati"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Blocchi scaricati"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Blocchi inviati"; + +/* swipe action */ "Clear" = "Svuota"; /* No comment provided by engineer. */ @@ -829,10 +936,13 @@ "Clear verification" = "Annulla la verifica"; /* No comment provided by engineer. */ -"colored" = "colorato"; +"Color chats with the new themes." = "Colora le chat con i nuovi temi."; /* No comment provided by engineer. */ -"Colors" = "Colori"; +"Color mode" = "Modalità di colore"; + +/* No comment provided by engineer. */ +"colored" = "colorato"; /* server test step */ "Compare file" = "Confronta file"; @@ -843,15 +953,27 @@ /* No comment provided by engineer. */ "complete" = "completo"; +/* No comment provided by engineer. */ +"Completed" = "Completato"; + /* No comment provided by engineer. */ "Configure ICE servers" = "Configura server ICE"; +/* No comment provided by engineer. */ +"Configured %@ servers" = "Configurati %@ server"; + /* No comment provided by engineer. */ "Confirm" = "Conferma"; +/* No comment provided by engineer. */ +"Confirm contact deletion?" = "Confermare l'eliminazione del contatto?"; + /* No comment provided by engineer. */ "Confirm database upgrades" = "Conferma aggiornamenti database"; +/* No comment provided by engineer. */ +"Confirm files from unknown servers." = "Conferma i file da server sconosciuti."; + /* No comment provided by engineer. */ "Confirm network settings" = "Conferma le impostazioni di rete"; @@ -885,6 +1007,9 @@ /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "connettiti agli sviluppatori di SimpleX Chat."; +/* No comment provided by engineer. */ +"Connect to your friends faster." = "Connettiti più velocemente ai tuoi amici."; + /* No comment provided by engineer. */ "Connect to yourself?" = "Connettersi a te stesso?"; @@ -909,18 +1034,27 @@ /* No comment provided by engineer. */ "connected" = "connesso/a"; +/* No comment provided by engineer. */ +"Connected" = "Connesso"; + /* No comment provided by engineer. */ "Connected desktop" = "Desktop connesso"; /* rcv group event chat item */ "connected directly" = "si è connesso/a direttamente"; +/* No comment provided by engineer. */ +"Connected servers" = "Server connessi"; + /* No comment provided by engineer. */ "Connected to desktop" = "Connesso al desktop"; /* No comment provided by engineer. */ "connecting" = "in connessione"; +/* No comment provided by engineer. */ +"Connecting" = "In connessione"; + /* No comment provided by engineer. */ "connecting (accepted)" = "in connessione (accettato)"; @@ -942,6 +1076,9 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "Connessione al server… (errore: %@)"; +/* No comment provided by engineer. */ +"Connecting to contact, please wait or check later!" = "In collegamento con il contatto, attendi o controlla più tardi!"; + /* No comment provided by engineer. */ "Connecting to desktop" = "Connessione al desktop"; @@ -951,6 +1088,9 @@ /* No comment provided by engineer. */ "Connection" = "Connessione"; +/* No comment provided by engineer. */ +"Connection and servers status." = "Stato della connessione e dei server."; + /* No comment provided by engineer. */ "Connection error" = "Errore di connessione"; @@ -960,6 +1100,9 @@ /* chat list item title (it should not be shown */ "connection established" = "connessione stabilita"; +/* No comment provided by engineer. */ +"Connection notifications" = "Notifiche di connessione"; + /* No comment provided by engineer. */ "Connection request sent!" = "Richiesta di connessione inviata!"; @@ -969,9 +1112,15 @@ /* No comment provided by engineer. */ "Connection timeout" = "Connessione scaduta"; +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "Connessione con il desktop fermata"; + /* connection information */ "connection:%@" = "connessione:% @"; +/* No comment provided by engineer. */ +"Connections" = "Connessioni"; + /* profile update event chat item */ "contact %@ changed to %@" = "contatto %1$@ cambiato in %2$@"; @@ -981,6 +1130,9 @@ /* No comment provided by engineer. */ "Contact already exists" = "Il contatto esiste già"; +/* No comment provided by engineer. */ +"Contact deleted!" = "Contatto eliminato!"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "il contatto ha la crittografia e2e"; @@ -994,7 +1146,7 @@ "Contact is connected" = "Il contatto è connesso"; /* No comment provided by engineer. */ -"Contact is not connected yet!" = "Il contatto non è ancora connesso!"; +"Contact is deleted." = "Il contatto è stato eliminato."; /* No comment provided by engineer. */ "Contact name" = "Nome del contatto"; @@ -1002,6 +1154,9 @@ /* No comment provided by engineer. */ "Contact preferences" = "Preferenze del contatto"; +/* No comment provided by engineer. */ +"Contact will be deleted - this cannot be undone!" = "Il contatto verrà eliminato - non è reversibile!"; + /* No comment provided by engineer. */ "Contacts" = "Contatti"; @@ -1011,9 +1166,15 @@ /* No comment provided by engineer. */ "Continue" = "Continua"; -/* chat item action */ +/* No comment provided by engineer. */ +"Conversation deleted!" = "Conversazione eliminata!"; + +/* No comment provided by engineer. */ "Copy" = "Copia"; +/* No comment provided by engineer. */ +"Copy error" = "Copia errore"; + /* No comment provided by engineer. */ "Core version: v%@" = "Versione core: v%@"; @@ -1059,6 +1220,9 @@ /* No comment provided by engineer. */ "Create your profile" = "Crea il tuo profilo"; +/* No comment provided by engineer. */ +"Created" = "Creato"; + /* No comment provided by engineer. */ "Created at" = "Creato il"; @@ -1083,6 +1247,9 @@ /* No comment provided by engineer. */ "Current passphrase…" = "Password attuale…"; +/* No comment provided by engineer. */ +"Current profile" = "Profilo attuale"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "Attualmente la dimensione massima supportata è di %@."; @@ -1092,9 +1259,15 @@ /* No comment provided by engineer. */ "Custom time" = "Tempo personalizzato"; +/* No comment provided by engineer. */ +"Customize theme" = "Personalizza il tema"; + /* No comment provided by engineer. */ "Dark" = "Scuro"; +/* No comment provided by engineer. */ +"Dark mode colors" = "Colori modalità scura"; + /* No comment provided by engineer. */ "Database downgrade" = "Downgrade del database"; @@ -1155,12 +1328,18 @@ /* time unit */ "days" = "giorni"; +/* No comment provided by engineer. */ +"Debug delivery" = "Debug della consegna"; + /* No comment provided by engineer. */ "Decentralized" = "Decentralizzato"; /* message decrypt error item */ "Decryption error" = "Errore di decifrazione"; +/* No comment provided by engineer. */ +"decryption errors" = "errori di decifrazione"; + /* pref value */ "default (%@)" = "predefinito (%@)"; @@ -1170,9 +1349,13 @@ /* No comment provided by engineer. */ "default (yes)" = "predefinito (sì)"; -/* chat item action */ +/* chat item action + swipe action */ "Delete" = "Elimina"; +/* No comment provided by engineer. */ +"Delete %lld messages of members?" = "Eliminare %lld messaggi dei membri?"; + /* No comment provided by engineer. */ "Delete %lld messages?" = "Eliminare %lld messaggi?"; @@ -1210,10 +1393,7 @@ "Delete contact" = "Elimina contatto"; /* No comment provided by engineer. */ -"Delete Contact" = "Elimina contatto"; - -/* No comment provided by engineer. */ -"Delete contact?\nThis cannot be undone!" = "Eliminare il contatto?\nNon è reversibile!"; +"Delete contact?" = "Eliminare il contatto?"; /* No comment provided by engineer. */ "Delete database" = "Elimina database"; @@ -1269,9 +1449,6 @@ /* No comment provided by engineer. */ "Delete old database?" = "Eliminare il database vecchio?"; -/* No comment provided by engineer. */ -"Delete pending connection" = "Elimina connessione in attesa"; - /* No comment provided by engineer. */ "Delete pending connection?" = "Eliminare la connessione in attesa?"; @@ -1281,12 +1458,21 @@ /* server test step */ "Delete queue" = "Elimina coda"; +/* No comment provided by engineer. */ +"Delete up to 20 messages at once." = "Elimina fino a 20 messaggi contemporaneamente."; + /* No comment provided by engineer. */ "Delete user profile?" = "Eliminare il profilo utente?"; +/* No comment provided by engineer. */ +"Delete without notification" = "Elimina senza avvisare"; + /* deleted chat item */ "deleted" = "eliminato"; +/* No comment provided by engineer. */ +"Deleted" = "Eliminato"; + /* No comment provided by engineer. */ "Deleted at" = "Eliminato il"; @@ -1299,6 +1485,9 @@ /* rcv group event chat item */ "deleted group" = "gruppo eliminato"; +/* No comment provided by engineer. */ +"Deletion errors" = "Errori di eliminazione"; + /* No comment provided by engineer. */ "Delivery" = "Consegna"; @@ -1320,9 +1509,27 @@ /* No comment provided by engineer. */ "Desktop devices" = "Dispositivi desktop"; +/* No comment provided by engineer. */ +"Destination server address of %@ is incompatible with forwarding server %@ settings." = "L'indirizzo del server di destinazione di %@ è incompatibile con le impostazioni del server di inoltro %@."; + +/* snd error text */ +"Destination server error: %@" = "Errore del server di destinazione: %@"; + +/* No comment provided by engineer. */ +"Destination server version of %@ is incompatible with forwarding server %@." = "La versione del server di destinazione di %@ è incompatibile con il server di inoltro %@."; + +/* No comment provided by engineer. */ +"Detailed statistics" = "Statistiche dettagliate"; + +/* No comment provided by engineer. */ +"Details" = "Dettagli"; + /* No comment provided by engineer. */ "Develop" = "Sviluppa"; +/* No comment provided by engineer. */ +"Developer options" = "Opzioni sviluppatore"; + /* No comment provided by engineer. */ "Developer tools" = "Strumenti di sviluppo"; @@ -1362,6 +1569,9 @@ /* No comment provided by engineer. */ "disabled" = "disattivato"; +/* No comment provided by engineer. */ +"Disabled" = "Disattivato"; + /* No comment provided by engineer. */ "Disappearing message" = "Messaggio a tempo"; @@ -1398,6 +1608,12 @@ /* No comment provided by engineer. */ "Do not send history to new members." = "Non inviare la cronologia ai nuovi membri."; +/* No comment provided by engineer. */ +"Do NOT send messages directly, even if your or destination server does not support private routing." = "NON inviare messaggi direttamente, anche se il tuo server o quello di destinazione non supporta l'instradamento privato."; + +/* No comment provided by engineer. */ +"Do NOT use private routing." = "NON usare l'instradamento privato."; + /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "NON usare SimpleX per chiamate di emergenza."; @@ -1416,12 +1632,21 @@ /* chat item action */ "Download" = "Scarica"; +/* No comment provided by engineer. */ +"Download errors" = "Errori di scaricamento"; + /* No comment provided by engineer. */ "Download failed" = "Scaricamento fallito"; /* server test step */ "Download file" = "Scarica file"; +/* No comment provided by engineer. */ +"Downloaded" = "Scaricato"; + +/* No comment provided by engineer. */ +"Downloaded files" = "File scaricati"; + /* No comment provided by engineer. */ "Downloading archive" = "Scaricamento archivio"; @@ -1434,6 +1659,9 @@ /* integrity error chat item */ "duplicate message" = "messaggio duplicato"; +/* No comment provided by engineer. */ +"duplicates" = "doppi"; + /* No comment provided by engineer. */ "Duration" = "Durata"; @@ -1491,6 +1719,9 @@ /* enabled status */ "enabled" = "attivato"; +/* No comment provided by engineer. */ +"Enabled" = "Attivato"; + /* No comment provided by engineer. */ "Enabled for" = "Attivo per"; @@ -1632,6 +1863,9 @@ /* No comment provided by engineer. */ "Error changing setting" = "Errore nella modifica dell'impostazione"; +/* No comment provided by engineer. */ +"Error connecting to forwarding server %@. Please try later." = "Errore di connessione al server di inoltro %@. Riprova più tardi."; + /* No comment provided by engineer. */ "Error creating address" = "Errore nella creazione dell'indirizzo"; @@ -1662,9 +1896,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "Errore nell'eliminazione della connessione"; -/* No comment provided by engineer. */ -"Error deleting contact" = "Errore nell'eliminazione del contatto"; - /* No comment provided by engineer. */ "Error deleting database" = "Errore nell'eliminazione del database"; @@ -1692,6 +1923,9 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "Errore nell'esportazione del database della chat"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Errore di esportazione del tema: %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "Errore nell'importazione del database della chat"; @@ -1707,9 +1941,18 @@ /* No comment provided by engineer. */ "Error receiving file" = "Errore nella ricezione del file"; +/* No comment provided by engineer. */ +"Error reconnecting server" = "Errore di riconnessione al server"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "Errore di riconnessione ai server"; + /* No comment provided by engineer. */ "Error removing member" = "Errore nella rimozione del membro"; +/* No comment provided by engineer. */ +"Error resetting statistics" = "Errore di azzeramento statistiche"; + /* No comment provided by engineer. */ "Error saving %@ servers" = "Errore nel salvataggio dei server %@"; @@ -1779,7 +2022,8 @@ /* No comment provided by engineer. */ "Error: " = "Errore: "; -/* No comment provided by engineer. */ +/* file error text + snd error text */ "Error: %@" = "Errore: %@"; /* No comment provided by engineer. */ @@ -1788,6 +2032,9 @@ /* No comment provided by engineer. */ "Error: URL is invalid" = "Errore: l'URL non è valido"; +/* No comment provided by engineer. */ +"Errors" = "Errori"; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Anche quando disattivato nella conversazione."; @@ -1800,12 +2047,18 @@ /* chat item action */ "Expand" = "Espandi"; +/* No comment provided by engineer. */ +"expired" = "scaduto"; + /* No comment provided by engineer. */ "Export database" = "Esporta database"; /* No comment provided by engineer. */ "Export error:" = "Errore di esportazione:"; +/* No comment provided by engineer. */ +"Export theme" = "Esporta tema"; + /* No comment provided by engineer. */ "Exported database archive." = "Archivio database esportato."; @@ -1824,9 +2077,24 @@ /* No comment provided by engineer. */ "Faster joining and more reliable messages." = "Ingresso più veloce e messaggi più affidabili."; -/* No comment provided by engineer. */ +/* swipe action */ "Favorite" = "Preferito"; +/* No comment provided by engineer. */ +"File error" = "Errore del file"; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "File non trovato - probabilmente è stato eliminato o annullato."; + +/* file error text */ +"File server error: %@" = "Errore del server dei file: %@"; + +/* No comment provided by engineer. */ +"File status" = "Stato del file"; + +/* copied message info */ +"File status: %@" = "Stato del file: %@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "Il file verrà eliminato dai server."; @@ -1839,6 +2107,9 @@ /* No comment provided by engineer. */ "File: %@" = "File: %@"; +/* No comment provided by engineer. */ +"Files" = "File"; + /* No comment provided by engineer. */ "Files & media" = "File e multimediali"; @@ -1905,6 +2176,21 @@ /* No comment provided by engineer. */ "Forwarded from" = "Inoltrato da"; +/* No comment provided by engineer. */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "Il server di inoltro %@ non è riuscito a connettersi al server di destinazione %@. Riprova più tardi."; + +/* No comment provided by engineer. */ +"Forwarding server address is incompatible with network settings: %@." = "L'indirizzo del server di inoltro è incompatibile con le impostazioni di rete: %@."; + +/* No comment provided by engineer. */ +"Forwarding server version is incompatible with network settings: %@." = "La versione del server di inoltro è incompatibile con le impostazioni di rete: %@."; + +/* snd error text */ +"Forwarding server: %@\nDestination server error: %@" = "Server di inoltro: %1$@\nErrore del server di destinazione: %2$@"; + +/* snd error text */ +"Forwarding server: %@\nError: %@" = "Server di inoltro: %1$@\nErrore: %2$@"; + /* No comment provided by engineer. */ "Found desktop" = "Desktop trovato"; @@ -1932,6 +2218,12 @@ /* No comment provided by engineer. */ "GIFs and stickers" = "GIF e adesivi"; +/* message preview */ +"Good afternoon!" = "Buon pomeriggio!"; + +/* message preview */ +"Good morning!" = "Buongiorno!"; + /* No comment provided by engineer. */ "Group" = "Gruppo"; @@ -2109,6 +2401,9 @@ /* No comment provided by engineer. */ "Import failed" = "Importazione fallita"; +/* No comment provided by engineer. */ +"Import theme" = "Importa tema"; + /* No comment provided by engineer. */ "Importing archive" = "Importazione archivio"; @@ -2130,6 +2425,9 @@ /* No comment provided by engineer. */ "In-call sounds" = "Suoni nelle chiamate"; +/* No comment provided by engineer. */ +"inactive" = "inattivo"; + /* No comment provided by engineer. */ "Incognito" = "Incognito"; @@ -2193,6 +2491,9 @@ /* No comment provided by engineer. */ "Interface" = "Interfaccia"; +/* No comment provided by engineer. */ +"Interface colors" = "Colori dell'interfaccia"; + /* invalid chat data */ "invalid chat" = "chat non valida"; @@ -2235,6 +2536,9 @@ /* group name */ "invitation to group %@" = "invito al gruppo %@"; +/* No comment provided by engineer. */ +"invite" = "invita"; + /* No comment provided by engineer. */ "Invite friends" = "Invita amici"; @@ -2280,6 +2584,9 @@ /* No comment provided by engineer. */ "It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Può accadere quando:\n1. I messaggi sono scaduti sul client mittente dopo 2 giorni o sul server dopo 30 giorni.\n2. La decifrazione del messaggio è fallita, perché tu o il tuo contatto avete usato un backup del database vecchio.\n3. La connessione è stata compromessa."; +/* No comment provided by engineer. */ +"It protects your IP address and connections." = "Protegge il tuo indirizzo IP e le connessioni."; + /* No comment provided by engineer. */ "It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Sembra che tu sia già connesso tramite questo link. In caso contrario, c'è stato un errore (%@)."; @@ -2292,7 +2599,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Interfaccia giapponese"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Entra"; /* No comment provided by engineer. */ @@ -2322,6 +2629,9 @@ /* No comment provided by engineer. */ "Keep" = "Tieni"; +/* No comment provided by engineer. */ +"Keep conversation" = "Tieni la conversazione"; + /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Tieni aperta l'app per usarla dal desktop"; @@ -2343,7 +2653,7 @@ /* No comment provided by engineer. */ "Learn more" = "Maggiori informazioni"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "Esci"; /* No comment provided by engineer. */ @@ -2433,6 +2743,12 @@ /* No comment provided by engineer. */ "Max 30 seconds, received instantly." = "Max 30 secondi, ricevuto istantaneamente."; +/* No comment provided by engineer. */ +"Media & file servers" = "Server di multimediali e file"; + +/* blur media */ +"Medium" = "Media"; + /* member role */ "member" = "membro"; @@ -2445,6 +2761,9 @@ /* rcv group event chat item */ "member connected" = "si è connesso/a"; +/* item status text */ +"Member inactive" = "Membro inattivo"; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "Il ruolo del membro verrà cambiato in \"%@\". Tutti i membri del gruppo verranno avvisati."; @@ -2454,15 +2773,33 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Il membro verrà rimosso dal gruppo, non è reversibile!"; +/* No comment provided by engineer. */ +"Menus" = "Menu"; + +/* No comment provided by engineer. */ +"message" = "messaggio"; + /* item status text */ "Message delivery error" = "Errore di recapito del messaggio"; /* No comment provided by engineer. */ "Message delivery receipts!" = "Ricevute di consegna dei messaggi!"; +/* item status text */ +"Message delivery warning" = "Avviso di consegna del messaggio"; + /* No comment provided by engineer. */ "Message draft" = "Bozza dei messaggi"; +/* item status text */ +"Message forwarded" = "Messaggio inoltrato"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "Il messaggio può essere consegnato più tardi se il membro diventa attivo."; + +/* No comment provided by engineer. */ +"Message queue info" = "Info coda messaggi"; + /* chat feature */ "Message reactions" = "Reazioni ai messaggi"; @@ -2475,9 +2812,21 @@ /* notification */ "message received" = "messaggio ricevuto"; +/* No comment provided by engineer. */ +"Message reception" = "Ricezione messaggi"; + +/* No comment provided by engineer. */ +"Message servers" = "Server dei messaggi"; + /* No comment provided by engineer. */ "Message source remains private." = "La fonte del messaggio resta privata."; +/* No comment provided by engineer. */ +"Message status" = "Stato del messaggio"; + +/* copied message info */ +"Message status: %@" = "Stato del messaggio: %@"; + /* No comment provided by engineer. */ "Message text" = "Testo del messaggio"; @@ -2493,6 +2842,12 @@ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "I messaggi da %@ verranno mostrati!"; +/* No comment provided by engineer. */ +"Messages received" = "Messaggi ricevuti"; + +/* No comment provided by engineer. */ +"Messages sent" = "Messaggi inviati"; + /* No comment provided by engineer. */ "Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "I messaggi, i file e le chiamate sono protetti da **crittografia end-to-end** con perfect forward secrecy, ripudio e recupero da intrusione."; @@ -2568,19 +2923,19 @@ /* item status description */ "Most likely this connection is deleted." = "Probabilmente questa connessione è stata eliminata."; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Probabilmente questo contatto ha eliminato la connessione con te."; - /* No comment provided by engineer. */ "Multiple chat profiles" = "Profili di chat multipli"; /* No comment provided by engineer. */ +"mute" = "silenzia"; + +/* swipe action */ "Mute" = "Silenzia"; /* No comment provided by engineer. */ "Muted when inactive!" = "Silenzioso quando inattivo!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "Nome"; /* No comment provided by engineer. */ @@ -2589,6 +2944,9 @@ /* No comment provided by engineer. */ "Network connection" = "Connessione di rete"; +/* snd error text */ +"Network issues - message expired after many attempts to send it." = "Problemi di rete - messaggio scaduto dopo molti tentativi di inviarlo."; + /* No comment provided by engineer. */ "Network management" = "Gestione della rete"; @@ -2604,6 +2962,9 @@ /* No comment provided by engineer. */ "New chat" = "Nuova chat"; +/* No comment provided by engineer. */ +"New chat experience 🎉" = "Una nuova esperienza di chat 🎉"; + /* notification */ "New contact request" = "Nuova richiesta di contatto"; @@ -2622,6 +2983,9 @@ /* No comment provided by engineer. */ "New in %@" = "Novità nella %@"; +/* No comment provided by engineer. */ +"New media options" = "Nuove opzioni multimediali"; + /* No comment provided by engineer. */ "New member role" = "Nuovo ruolo del membro"; @@ -2658,6 +3022,9 @@ /* No comment provided by engineer. */ "No device token!" = "Nessun token del dispositivo!"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Ancora nessuna connessione diretta, il messaggio viene inoltrato dall'amministratore."; + /* No comment provided by engineer. */ "no e2e encryption" = "nessuna crittografia e2e"; @@ -2670,6 +3037,9 @@ /* No comment provided by engineer. */ "No history" = "Nessuna cronologia"; +/* No comment provided by engineer. */ +"No info, try to reload" = "Nessuna informazione, prova a ricaricare"; + /* No comment provided by engineer. */ "No network connection" = "Nessuna connessione di rete"; @@ -2685,6 +3055,9 @@ /* No comment provided by engineer. */ "Not compatible!" = "Non compatibile!"; +/* No comment provided by engineer. */ +"Nothing selected" = "Nessuna selezione"; + /* No comment provided by engineer. */ "Notifications" = "Notifiche"; @@ -2702,7 +3075,7 @@ time to disappear */ "off" = "off"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Off"; /* feature offered item */ @@ -2730,10 +3103,10 @@ "One-time invitation link" = "Link di invito una tantum"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "Gli host Onion saranno necessari per la connessione. Richiede l'attivazione della VPN."; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Gli host Onion saranno **necessari** per la connessione.\nRichiede l'attivazione della VPN."; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "Gli host Onion verranno usati quando disponibili. Richiede l'attivazione della VPN."; +"Onion hosts will be used when available.\nRequires compatible VPN." = "Gli host Onion verranno usati quando disponibili.\nRichiede l'attivazione della VPN."; /* No comment provided by engineer. */ "Onion hosts will not be used." = "Gli host Onion non verranno usati."; @@ -2741,6 +3114,9 @@ /* No comment provided by engineer. */ "Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Solo i dispositivi client archiviano profili utente, i contatti, i gruppi e i messaggi inviati con la **crittografia end-to-end a 2 livelli**."; +/* No comment provided by engineer. */ +"Only delete conversation" = "Elimina solo la conversazione"; + /* No comment provided by engineer. */ "Only group owners can change group preferences." = "Solo i proprietari del gruppo possono modificarne le preferenze."; @@ -2795,6 +3171,9 @@ /* authentication reason */ "Open migration to another device" = "Apri migrazione ad un altro dispositivo"; +/* No comment provided by engineer. */ +"Open server settings" = "Apri impostazioni server"; + /* No comment provided by engineer. */ "Open Settings" = "Apri le impostazioni"; @@ -2819,9 +3198,18 @@ /* No comment provided by engineer. */ "Or show this code" = "O mostra questo codice"; +/* No comment provided by engineer. */ +"other" = "altro"; + /* No comment provided by engineer. */ "Other" = "Altro"; +/* No comment provided by engineer. */ +"Other %@ servers" = "Altri %@ server"; + +/* No comment provided by engineer. */ +"other errors" = "altri errori"; + /* member role */ "owner" = "proprietario"; @@ -2864,6 +3252,9 @@ /* No comment provided by engineer. */ "peer-to-peer" = "peer-to-peer"; +/* No comment provided by engineer. */ +"Pending" = "In attesa"; + /* No comment provided by engineer. */ "People can connect to you only via the links you share." = "Le persone possono connettersi a te solo tramite i link che condividi."; @@ -2882,9 +3273,18 @@ /* No comment provided by engineer. */ "PING interval" = "Intervallo PING"; +/* No comment provided by engineer. */ +"Play from the chat list." = "Riproduci dall'elenco delle chat."; + +/* No comment provided by engineer. */ +"Please ask your contact to enable calls." = "Chiedi al contatto di attivare le chiamate."; + /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "Chiedi al tuo contatto di attivare l'invio dei messaggi vocali."; +/* No comment provided by engineer. */ +"Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.\nPlease share any other issues with the developers." = "Controlla che mobile e desktop siano collegati alla stessa rete locale e che il firewall del desktop consenta la connessione.\nSi prega di condividere qualsiasi altro problema con gli sviluppatori."; + /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "Controlla di aver usato il link giusto o chiedi al tuo contatto di inviartene un altro."; @@ -2942,6 +3342,9 @@ /* No comment provided by engineer. */ "Preview" = "Anteprima"; +/* No comment provided by engineer. */ +"Previously connected servers" = "Server precedentemente connessi"; + /* No comment provided by engineer. */ "Privacy & security" = "Privacy e sicurezza"; @@ -2951,9 +3354,21 @@ /* No comment provided by engineer. */ "Private filenames" = "Nomi di file privati"; +/* No comment provided by engineer. */ +"Private message routing" = "Instradamento privato dei messaggi"; + +/* No comment provided by engineer. */ +"Private message routing 🚀" = "Instradamento privato dei messaggi 🚀"; + /* name of notes to self */ "Private notes" = "Note private"; +/* No comment provided by engineer. */ +"Private routing" = "Instradamento privato"; + +/* No comment provided by engineer. */ +"Private routing error" = "Errore di instradamento privato"; + /* No comment provided by engineer. */ "Profile and server connections" = "Profilo e connessioni al server"; @@ -2972,6 +3387,9 @@ /* No comment provided by engineer. */ "Profile password" = "Password del profilo"; +/* No comment provided by engineer. */ +"Profile theme" = "Tema del profilo"; + /* No comment provided by engineer. */ "Profile update will be sent to your contacts." = "L'aggiornamento del profilo verrà inviato ai tuoi contatti."; @@ -3005,15 +3423,27 @@ /* No comment provided by engineer. */ "Protect app screen" = "Proteggi la schermata dell'app"; +/* No comment provided by engineer. */ +"Protect IP address" = "Proteggi l'indirizzo IP"; + /* No comment provided by engineer. */ "Protect your chat profiles with a password!" = "Proteggi i tuoi profili di chat con una password!"; +/* No comment provided by engineer. */ +"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Proteggi il tuo indirizzo IP dai relay di messaggistica scelti dai tuoi contatti.\nAttivalo nelle impostazioni *Rete e server*."; + /* No comment provided by engineer. */ "Protocol timeout" = "Scadenza del protocollo"; /* No comment provided by engineer. */ "Protocol timeout per KB" = "Scadenza del protocollo per KB"; +/* No comment provided by engineer. */ +"Proxied" = "Via proxy"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Server via proxy"; + /* No comment provided by engineer. */ "Push notifications" = "Notifiche push"; @@ -3029,10 +3459,13 @@ /* No comment provided by engineer. */ "Rate the app" = "Valuta l'app"; +/* No comment provided by engineer. */ +"Reachable chat toolbar" = "Barra degli strumenti di chat accessibile"; + /* chat item menu */ "React…" = "Reagisci…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "Leggi"; /* No comment provided by engineer. */ @@ -3056,6 +3489,9 @@ /* No comment provided by engineer. */ "Receipts are disabled" = "Le ricevute sono disattivate"; +/* No comment provided by engineer. */ +"Receive errors" = "Errori di ricezione"; + /* No comment provided by engineer. */ "received answer…" = "risposta ricevuta…"; @@ -3075,10 +3511,16 @@ "Received message" = "Messaggio ricevuto"; /* No comment provided by engineer. */ -"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "L'indirizzo di ricezione verrà cambiato in un server diverso. La modifica dell'indirizzo verrà completata dopo che il mittente sarà in linea."; +"Received messages" = "Messaggi ricevuti"; /* No comment provided by engineer. */ -"Receiving concurrency" = "Ricezione concomitanza"; +"Received reply" = "Risposta ricevuta"; + +/* No comment provided by engineer. */ +"Received total" = "Totale ricevuto"; + +/* No comment provided by engineer. */ +"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "L'indirizzo di ricezione verrà cambiato in un server diverso. La modifica dell'indirizzo verrà completata dopo che il mittente sarà in linea."; /* No comment provided by engineer. */ "Receiving file will be stopped." = "La ricezione del file verrà interrotta."; @@ -3095,9 +3537,24 @@ /* No comment provided by engineer. */ "Recipients see updates as you type them." = "I destinatari vedono gli aggiornamenti mentre li digiti."; +/* No comment provided by engineer. */ +"Reconnect" = "Riconnetti"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Riconnetti tutti i server connessi per imporre il recapito dei messaggi. Utilizza traffico aggiuntivo."; +/* No comment provided by engineer. */ +"Reconnect all servers" = "Riconnetti tutti i server"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "Riconnettere tutti i server?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "Riconnetti il server per forzare la consegna dei messaggi. Usa traffico aggiuntivo."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "Riconnettere il server?"; + /* No comment provided by engineer. */ "Reconnect servers?" = "Riconnettere i server?"; @@ -3110,7 +3567,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Consumo di batteria ridotto"; -/* reject incoming call via notification */ +/* reject incoming call via notification + swipe action */ "Reject" = "Rifiuta"; /* No comment provided by engineer. */ @@ -3131,6 +3589,9 @@ /* No comment provided by engineer. */ "Remove" = "Rimuovi"; +/* No comment provided by engineer. */ +"Remove image" = "Rimuovi immagine"; + /* No comment provided by engineer. */ "Remove member" = "Rimuovi membro"; @@ -3188,12 +3649,27 @@ /* No comment provided by engineer. */ "Reset" = "Ripristina"; +/* No comment provided by engineer. */ +"Reset all hints" = "Ripristina tutti i suggerimenti"; + +/* No comment provided by engineer. */ +"Reset all statistics" = "Azzera tutte le statistiche"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "Azzerare tutte le statistiche?"; + /* No comment provided by engineer. */ "Reset colors" = "Ripristina i colori"; +/* No comment provided by engineer. */ +"Reset to app theme" = "Ripristina al tema dell'app"; + /* No comment provided by engineer. */ "Reset to defaults" = "Ripristina i predefiniti"; +/* No comment provided by engineer. */ +"Reset to user theme" = "Ripristina al tema dell'utente"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Riavvia l'app per creare un nuovo profilo di chat"; @@ -3218,9 +3694,6 @@ /* chat item action */ "Reveal" = "Rivela"; -/* No comment provided by engineer. */ -"Revert" = "Ripristina"; - /* No comment provided by engineer. */ "Revoke" = "Revoca"; @@ -3236,6 +3709,9 @@ /* No comment provided by engineer. */ "Run chat" = "Avvia chat"; +/* No comment provided by engineer. */ +"Safely receive files" = "Ricevi i file in sicurezza"; + /* No comment provided by engineer. */ "Safer groups" = "Gruppi più sicuri"; @@ -3251,6 +3727,9 @@ /* No comment provided by engineer. */ "Save and notify group members" = "Salva e avvisa i membri del gruppo"; +/* No comment provided by engineer. */ +"Save and reconnect" = "Salva e riconnetti"; + /* No comment provided by engineer. */ "Save and update group profile" = "Salva e aggiorna il profilo del gruppo"; @@ -3305,6 +3784,12 @@ /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "I server WebRTC ICE salvati verranno rimossi"; +/* No comment provided by engineer. */ +"Scale" = "Scala"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Scansiona / Incolla link"; + /* No comment provided by engineer. */ "Scan code" = "Scansiona codice"; @@ -3320,6 +3805,9 @@ /* No comment provided by engineer. */ "Scan server QR code" = "Scansiona codice QR del server"; +/* No comment provided by engineer. */ +"search" = "cerca"; + /* No comment provided by engineer. */ "Search" = "Cerca"; @@ -3332,6 +3820,9 @@ /* network option */ "sec" = "sec"; +/* No comment provided by engineer. */ +"Secondary" = "Secondario"; + /* time unit */ "seconds" = "secondi"; @@ -3341,6 +3832,9 @@ /* server test step */ "Secure queue" = "Coda sicura"; +/* No comment provided by engineer. */ +"Secured" = "Protetto"; + /* No comment provided by engineer. */ "Security assessment" = "Valutazione della sicurezza"; @@ -3350,9 +3844,15 @@ /* chat item text */ "security code changed" = "codice di sicurezza modificato"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "Seleziona"; +/* No comment provided by engineer. */ +"Selected %lld" = "%lld selezionato"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Le preferenze della chat selezionata vietano questo messaggio."; + /* No comment provided by engineer. */ "Self-destruct" = "Autodistruzione"; @@ -3377,21 +3877,30 @@ /* No comment provided by engineer. */ "send direct message" = "invia messaggio diretto"; -/* No comment provided by engineer. */ -"Send direct message" = "Invia messaggio diretto"; - /* No comment provided by engineer. */ "Send direct message to connect" = "Invia messaggio diretto per connetterti"; /* No comment provided by engineer. */ "Send disappearing message" = "Invia messaggio a tempo"; +/* No comment provided by engineer. */ +"Send errors" = "Errori di invio"; + /* No comment provided by engineer. */ "Send link previews" = "Invia anteprime dei link"; /* No comment provided by engineer. */ "Send live message" = "Invia messaggio in diretta"; +/* No comment provided by engineer. */ +"Send message to enable calls." = "Invia un messaggio per attivare le chiamate."; + +/* No comment provided by engineer. */ +"Send messages directly when IP address is protected and your or destination server does not support private routing." = "Invia messaggi direttamente quando l'indirizzo IP è protetto e il tuo server o quello di destinazione non supporta l'instradamento privato."; + +/* No comment provided by engineer. */ +"Send messages directly when your or destination server does not support private routing." = "Invia messaggi direttamente quando il tuo server o quello di destinazione non supporta l'instradamento privato."; + /* No comment provided by engineer. */ "Send notifications" = "Invia notifiche"; @@ -3446,15 +3955,42 @@ /* copied message info */ "Sent at: %@" = "Inviato il: %@"; +/* No comment provided by engineer. */ +"Sent directly" = "Inviato direttamente"; + /* notification */ "Sent file event" = "Evento file inviato"; /* message info title */ "Sent message" = "Messaggio inviato"; +/* No comment provided by engineer. */ +"Sent messages" = "Messaggi inviati"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "I messaggi inviati verranno eliminati dopo il tempo impostato."; +/* No comment provided by engineer. */ +"Sent reply" = "Risposta inviata"; + +/* No comment provided by engineer. */ +"Sent total" = "Totale inviato"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Inviato via proxy"; + +/* No comment provided by engineer. */ +"Server address" = "Indirizzo server"; + +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "L'indirizzo del server è incompatibile con le impostazioni di rete: %@."; + +/* srv error text. */ +"Server address is incompatible with network settings." = "L'indirizzo del server non è compatibile con le impostazioni di rete."; + +/* queue info */ +"server queue info: %@\n\nlast received msg: %@" = "info coda server: %1$@\n\nultimo msg ricevuto: %2$@"; + /* server test error */ "Server requires authorization to create queues, check password" = "Il server richiede l'autorizzazione di creare code, controlla la password"; @@ -3464,9 +4000,24 @@ /* No comment provided by engineer. */ "Server test failed!" = "Test del server fallito!"; +/* No comment provided by engineer. */ +"Server type" = "Tipo server"; + +/* srv error text */ +"Server version is incompatible with network settings." = "La versione del server non è compatibile con le impostazioni di rete."; + +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "La versione del server è incompatibile con la tua app: %@."; + /* No comment provided by engineer. */ "Servers" = "Server"; +/* No comment provided by engineer. */ +"Servers info" = "Info dei server"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "Le statistiche dei server verranno azzerate - è irreversibile!"; + /* No comment provided by engineer. */ "Session code" = "Codice di sessione"; @@ -3476,6 +4027,9 @@ /* No comment provided by engineer. */ "Set contact name…" = "Imposta nome del contatto…"; +/* No comment provided by engineer. */ +"Set default theme" = "Imposta tema predefinito"; + /* No comment provided by engineer. */ "Set group preferences" = "Imposta le preferenze del gruppo"; @@ -3521,15 +4075,24 @@ /* No comment provided by engineer. */ "Share address with contacts?" = "Condividere l'indirizzo con i contatti?"; +/* No comment provided by engineer. */ +"Share from other apps." = "Condividi da altre app."; + /* No comment provided by engineer. */ "Share link" = "Condividi link"; /* No comment provided by engineer. */ "Share this 1-time invite link" = "Condividi questo link di invito una tantum"; +/* No comment provided by engineer. */ +"Share to SimpleX" = "Condividi in SimpleX"; + /* No comment provided by engineer. */ "Share with contacts" = "Condividi con i contatti"; +/* No comment provided by engineer. */ +"Show → on messages sent via private routing." = "Mostra → nei messaggi inviati via instradamento privato."; + /* No comment provided by engineer. */ "Show calls in phone history" = "Mostra le chiamate nella cronologia del telefono"; @@ -3539,6 +4102,12 @@ /* No comment provided by engineer. */ "Show last messages" = "Mostra ultimi messaggi"; +/* No comment provided by engineer. */ +"Show message status" = "Mostra stato del messaggio"; + +/* No comment provided by engineer. */ +"Show percentage" = "Mostra percentuale"; + /* No comment provided by engineer. */ "Show preview" = "Mostra anteprima"; @@ -3548,6 +4117,9 @@ /* No comment provided by engineer. */ "Show:" = "Mostra:"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "Indirizzo SimpleX"; @@ -3593,6 +4165,9 @@ /* No comment provided by engineer. */ "Simplified incognito mode" = "Modalità incognito semplificata"; +/* No comment provided by engineer. */ +"Size" = "Dimensione"; + /* No comment provided by engineer. */ "Skip" = "Salta"; @@ -3603,11 +4178,20 @@ "Small groups (max 20)" = "Piccoli gruppi (max 20)"; /* No comment provided by engineer. */ -"SMP servers" = "Server SMP"; +"SMP server" = "Server SMP"; + +/* blur media */ +"Soft" = "Leggera"; + +/* No comment provided by engineer. */ +"Some file(s) were not exported:" = "Alcuni file non sono stati esportati:"; /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "Si sono verificati alcuni errori non gravi durante l'importazione: vedi la console della chat per i dettagli."; +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import:" = "Si sono verificati alcuni errori non fatali durante l'importazione:"; + /* notification title */ "Somebody" = "Qualcuno"; @@ -3626,9 +4210,15 @@ /* No comment provided by engineer. */ "Start migration" = "Avvia la migrazione"; +/* No comment provided by engineer. */ +"Starting from %@." = "Inizio da %@."; + /* No comment provided by engineer. */ "starting…" = "avvio…"; +/* No comment provided by engineer. */ +"Statistics" = "Statistiche"; + /* No comment provided by engineer. */ "Stop" = "Ferma"; @@ -3668,9 +4258,21 @@ /* No comment provided by engineer. */ "strike" = "barrato"; +/* blur media */ +"Strong" = "Forte"; + /* No comment provided by engineer. */ "Submit" = "Invia"; +/* No comment provided by engineer. */ +"Subscribed" = "Iscritto"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Errori di iscrizione"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Iscrizioni ignorate"; + /* No comment provided by engineer. */ "Support SimpleX Chat" = "Supporta SimpleX Chat"; @@ -3705,7 +4307,7 @@ "Tap to scan" = "Tocca per scansionare"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Tocca per iniziare una chat"; +"TCP connection" = "Connessione TCP"; /* No comment provided by engineer. */ "TCP connection timeout" = "Scadenza connessione TCP"; @@ -3719,6 +4321,9 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* No comment provided by engineer. */ +"Temporary file error" = "Errore del file temporaneo"; + /* server test failure */ "Test failed at step %@." = "Test fallito al passo %@."; @@ -3746,6 +4351,9 @@ /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "L'app può avvisarti quando ricevi messaggi o richieste di contatto: apri le impostazioni per attivare."; +/* No comment provided by engineer. */ +"The app will ask to confirm downloads from unknown file servers (except .onion)." = "L'app chiederà di confermare i download da server di file sconosciuti (eccetto .onion)."; + /* No comment provided by engineer. */ "The attempt to change database passphrase was not completed." = "Il tentativo di cambiare la password del database non è stato completato."; @@ -3776,6 +4384,12 @@ /* No comment provided by engineer. */ "The message will be marked as moderated for all members." = "Il messaggio sarà segnato come moderato per tutti i membri."; +/* No comment provided by engineer. */ +"The messages will be deleted for all members." = "I messaggi verranno eliminati per tutti i membri."; + +/* No comment provided by engineer. */ +"The messages will be marked as moderated for all members." = "I messaggi verranno contrassegnati come moderati per tutti i membri."; + /* No comment provided by engineer. */ "The next generation of private messaging" = "La nuova generazione di messaggistica privata"; @@ -3798,7 +4412,7 @@ "The text you pasted is not a SimpleX link." = "Il testo che hai incollato non è un link SimpleX."; /* No comment provided by engineer. */ -"Theme" = "Tema"; +"Themes" = "Temi"; /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Queste impostazioni sono per il tuo profilo attuale **%@**."; @@ -3842,9 +4456,15 @@ /* No comment provided by engineer. */ "This is your own SimpleX address!" = "Questo è il tuo indirizzo SimpleX!"; +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "Questo link è stato usato con un altro dispositivo mobile, creane uno nuovo sul desktop."; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Questa impostazione si applica ai messaggi del profilo di chat attuale **%@**."; +/* No comment provided by engineer. */ +"Title" = "Titoli"; + /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Per porre domande e ricevere aggiornamenti:"; @@ -3866,6 +4486,9 @@ /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Per proteggere le tue informazioni, attiva SimpleX Lock.\nTi verrà chiesto di completare l'autenticazione prima di attivare questa funzionalità."; +/* No comment provided by engineer. */ +"To protect your IP address, private routing uses your SMP servers to deliver messages." = "Per proteggere il tuo indirizzo IP, l'instradamento privato usa i tuoi server SMP per consegnare i messaggi."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Per registrare un messaggio vocale, concedi l'autorizzazione all'uso del microfono."; @@ -3878,12 +4501,24 @@ /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Per verificare la crittografia end-to-end con il tuo contatto, confrontate (o scansionate) il codice sui vostri dispositivi."; +/* No comment provided by engineer. */ +"Toggle chat list:" = "Cambia l'elenco delle chat:"; + /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Attiva/disattiva l'incognito quando ti colleghi."; +/* No comment provided by engineer. */ +"Toolbar opacity" = "Opacità barra degli strumenti"; + +/* No comment provided by engineer. */ +"Total" = "Totale"; + /* No comment provided by engineer. */ "Transport isolation" = "Isolamento del trasporto"; +/* No comment provided by engineer. */ +"Transport sessions" = "Sessioni di trasporto"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Tentativo di connessione al server usato per ricevere messaggi da questo contatto (errore: %@)."; @@ -3920,13 +4555,10 @@ /* rcv group event chat item */ "unblocked %@" = "ha sbloccato %@"; -/* item status description */ -"Unexpected error: %@" = "Errore imprevisto: % @"; - /* No comment provided by engineer. */ "Unexpected migration state" = "Stato di migrazione imprevisto"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "Non pref."; /* No comment provided by engineer. */ @@ -3953,6 +4585,12 @@ /* No comment provided by engineer. */ "Unknown error" = "Errore sconosciuto"; +/* No comment provided by engineer. */ +"unknown servers" = "relay sconosciuti"; + +/* No comment provided by engineer. */ +"Unknown servers!" = "Server sconosciuti!"; + /* No comment provided by engineer. */ "unknown status" = "stato sconosciuto"; @@ -3975,9 +4613,15 @@ "Unlock app" = "Sblocca l'app"; /* No comment provided by engineer. */ +"unmute" = "riattiva notifiche"; + +/* swipe action */ "Unmute" = "Riattiva notifiche"; /* No comment provided by engineer. */ +"unprotected" = "non protetto"; + +/* swipe action */ "Unread" = "Non letto"; /* No comment provided by engineer. */ @@ -3986,9 +4630,6 @@ /* No comment provided by engineer. */ "Update" = "Aggiorna"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = "Aggiornare l'impostazione degli host .onion?"; - /* No comment provided by engineer. */ "Update database passphrase" = "Aggiorna la password del database"; @@ -3996,7 +4637,7 @@ "Update network settings?" = "Aggiornare le impostazioni di rete?"; /* No comment provided by engineer. */ -"Update transport isolation mode?" = "Aggiornare la modalità di isolamento del trasporto?"; +"Update settings?" = "Aggiornare le impostazioni?"; /* rcv group event chat item */ "updated group profile" = "ha aggiornato il profilo del gruppo"; @@ -4008,10 +4649,10 @@ "Updating settings will re-connect the client to all servers." = "L'aggiornamento delle impostazioni riconnetterà il client a tutti i server."; /* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "L'aggiornamento di questa impostazione riconnetterà il client a tutti i server."; +"Upgrade and open chat" = "Aggiorna e apri chat"; /* No comment provided by engineer. */ -"Upgrade and open chat" = "Aggiorna e apri chat"; +"Upload errors" = "Errori di invio"; /* No comment provided by engineer. */ "Upload failed" = "Invio fallito"; @@ -4019,6 +4660,12 @@ /* server test step */ "Upload file" = "Invia file"; +/* No comment provided by engineer. */ +"Uploaded" = "Inviato"; + +/* No comment provided by engineer. */ +"Uploaded files" = "File inviati"; + /* No comment provided by engineer. */ "Uploading archive" = "Invio dell'archivio"; @@ -4046,6 +4693,12 @@ /* No comment provided by engineer. */ "Use only local notifications?" = "Usare solo notifiche locali?"; +/* No comment provided by engineer. */ +"Use private routing with unknown servers when IP address is not protected." = "Usa l'instradamento privato con server sconosciuti quando l'indirizzo IP non è protetto."; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers." = "Usa l'instradamento privato con server sconosciuti."; + /* No comment provided by engineer. */ "Use server" = "Usa il server"; @@ -4055,11 +4708,14 @@ /* No comment provided by engineer. */ "Use the app while in the call." = "Usa l'app mentre sei in chiamata."; +/* No comment provided by engineer. */ +"Use the app with one hand." = "Usa l'app con una mano sola."; + /* No comment provided by engineer. */ "User profile" = "Profilo utente"; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "L'uso di host .onion richiede un fornitore di VPN compatibile."; +"User selection" = "Selezione utente"; /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "Utilizzo dei server SimpleX Chat."; @@ -4109,6 +4765,9 @@ /* No comment provided by engineer. */ "Via secure quantum resistant protocol." = "Tramite protocollo sicuro resistente alla quantistica."; +/* No comment provided by engineer. */ +"video" = "video"; + /* No comment provided by engineer. */ "Video call" = "Videochiamata"; @@ -4166,6 +4825,12 @@ /* No comment provided by engineer. */ "Waiting for video" = "In attesa del video"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "Tinta dello sfondo"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Retro dello sfondo"; + /* No comment provided by engineer. */ "wants to connect to you!" = "vuole connettersi con te!"; @@ -4199,6 +4864,9 @@ /* No comment provided by engineer. */ "When connecting audio and video calls." = "Quando si connettono le chiamate audio e video."; +/* No comment provided by engineer. */ +"when IP hidden" = "quando l'IP è nascosto"; + /* No comment provided by engineer. */ "When people request to connect, you can accept or reject it." = "Quando le persone chiedono di connettersi, puoi accettare o rifiutare."; @@ -4223,14 +4891,26 @@ /* No comment provided by engineer. */ "With reduced battery usage." = "Con consumo di batteria ridotto."; +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to file servers." = "Senza Tor o VPN, il tuo indirizzo IP sarà visibile ai server di file."; + +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Senza Tor o VPN, il tuo indirizzo IP sarà visibile a questi relay XFTP: %@."; + /* No comment provided by engineer. */ "Wrong database passphrase" = "Password del database sbagliata"; +/* snd error text */ +"Wrong key or unknown connection - most likely this connection is deleted." = "Chiave sbagliata o connessione sconosciuta - molto probabilmente questa connessione è stata eliminata."; + +/* file error text */ +"Wrong key or unknown file chunk address - most likely file is deleted." = "Chiave sbagliata o indirizzo sconosciuto per frammento del file - probabilmente il file è stato eliminato."; + /* No comment provided by engineer. */ "Wrong passphrase!" = "Password sbagliata!"; /* No comment provided by engineer. */ -"XFTP servers" = "Server XFTP"; +"XFTP server" = "Server XFTP"; /* pref value */ "yes" = "sì"; @@ -4286,6 +4966,9 @@ /* No comment provided by engineer. */ "You are invited to group" = "Sei stato/a invitato/a al gruppo"; +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "Non sei connesso/a a questi server. L'instradamento privato è usato per consegnare loro i messaggi."; + /* No comment provided by engineer. */ "you are observer" = "sei un osservatore"; @@ -4295,6 +4978,9 @@ /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "Puoi accettare chiamate dalla schermata di blocco, senza l'autenticazione del dispositivo e dell'app."; +/* No comment provided by engineer. */ +"You can change it in Appearance settings." = "Puoi cambiarlo nelle impostazioni dell'aspetto."; + /* No comment provided by engineer. */ "You can create it later" = "Puoi crearlo più tardi"; @@ -4314,7 +5000,10 @@ "You can make it visible to your SimpleX contacts via Settings." = "Puoi renderlo visibile ai tuoi contatti SimpleX nelle impostazioni."; /* notification body */ -"You can now send messages to %@" = "Ora puoi inviare messaggi a %@"; +"You can now chat with %@" = "Ora puoi inviare messaggi a %@"; + +/* No comment provided by engineer. */ +"You can send messages to %@ from Archived contacts." = "Puoi inviare messaggi a %@ dai contatti archiviati."; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Puoi impostare l'anteprima della notifica nella schermata di blocco tramite le impostazioni."; @@ -4331,6 +5020,9 @@ /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "Puoi avviare la chat via Impostazioni / Database o riavviando l'app"; +/* No comment provided by engineer. */ +"You can still view conversation with %@ in the list of chats." = "Puoi ancora vedere la conversazione con %@ nell'elenco delle chat."; + /* No comment provided by engineer. */ "You can turn on SimpleX Lock via Settings." = "Puoi attivare SimpleX Lock tramite le impostazioni."; @@ -4367,9 +5059,6 @@ /* No comment provided by engineer. */ "You have already requested connection!\nRepeat connection request?" = "Hai già richiesto la connessione!\nRipetere la richiesta di connessione?"; -/* No comment provided by engineer. */ -"You have no chats" = "Non hai chat"; - /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "Devi inserire la password ogni volta che si avvia l'app: non viene memorizzata sul dispositivo."; @@ -4385,9 +5074,18 @@ /* snd group event chat item */ "you left" = "sei uscito/a"; +/* No comment provided by engineer. */ +"You may migrate the exported database." = "Puoi migrare il database esportato."; + +/* No comment provided by engineer. */ +"You may save the exported archive." = "Puoi salvare l'archivio esportato."; + /* No comment provided by engineer. */ "You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." = "Devi usare la versione più recente del tuo database della chat SOLO su un dispositivo, altrimenti potresti non ricevere più i messaggi da alcuni contatti."; +/* No comment provided by engineer. */ +"You need to allow your contact to call to be able to call them." = "Devi consentire le chiamate al tuo contatto per poterlo chiamare."; + /* No comment provided by engineer. */ "You need to allow your contact to send voice messages to be able to send them." = "Devi consentire al tuo contatto di inviare messaggi vocali per poterli inviare anche tu."; @@ -4460,9 +5158,6 @@ /* No comment provided by engineer. */ "Your chat profiles" = "I tuoi profili di chat"; -/* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Il tuo contatto deve essere in linea per completare la connessione.\nPuoi annullare questa connessione e rimuovere il contatto (e riprovare più tardi con un link nuovo)."; - /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Il tuo contatto ha inviato un file più grande della dimensione massima attualmente supportata (%@)."; diff --git a/apps/ios/ja.lproj/Localizable.strings b/apps/ios/ja.lproj/Localizable.strings index f8bacda7b8..2a924539c8 100644 --- a/apps/ios/ja.lproj/Localizable.strings +++ b/apps/ios/ja.lproj/Localizable.strings @@ -148,9 +148,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ は検証されています"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ サーバー"; - /* No comment provided by engineer. */ "%@ uploaded" = "%@ アップロード済"; @@ -331,11 +328,9 @@ /* No comment provided by engineer. */ "above, then choose:" = "上で選んでください:"; -/* No comment provided by engineer. */ -"Accent color" = "アクセントカラー"; - /* accept contact request via notification - accept incoming call via notification */ + accept incoming call via notification + swipe action */ "Accept" = "承諾"; /* No comment provided by engineer. */ @@ -344,7 +339,8 @@ /* notification body */ "Accept contact request from %@?" = "%@ からの連絡要求を受け入れますか?"; -/* accept contact request via notification */ +/* accept contact request via notification + swipe action */ "Accept incognito" = "シークレットモードで承諾"; /* call status */ @@ -360,7 +356,7 @@ "Add profile" = "プロフィールを追加"; /* No comment provided by engineer. */ -"Add server…" = "サーバを追加…"; +"Add server" = "サーバを追加"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "QRコードでサーバを追加する。"; @@ -720,7 +716,7 @@ /* No comment provided by engineer. */ "Choose from library" = "ライブラリから選択"; -/* No comment provided by engineer. */ +/* swipe action */ "Clear" = "消す"; /* No comment provided by engineer. */ @@ -735,9 +731,6 @@ /* No comment provided by engineer. */ "colored" = "色付き"; -/* No comment provided by engineer. */ -"Colors" = "色"; - /* server test step */ "Compare file" = "ファイルを比較"; @@ -849,9 +842,6 @@ /* notification */ "Contact is connected" = "連絡先は接続中"; -/* No comment provided by engineer. */ -"Contact is not connected yet!" = "連絡先がまだ繋がってません!"; - /* No comment provided by engineer. */ "Contact name" = "連絡先の名前"; @@ -867,7 +857,7 @@ /* No comment provided by engineer. */ "Continue" = "続ける"; -/* chat item action */ +/* No comment provided by engineer. */ "Copy" = "コピー"; /* No comment provided by engineer. */ @@ -1002,7 +992,8 @@ /* No comment provided by engineer. */ "default (yes)" = "デフォルト(はい)"; -/* chat item action */ +/* chat item action + swipe action */ "Delete" = "削除"; /* No comment provided by engineer. */ @@ -1035,9 +1026,6 @@ /* No comment provided by engineer. */ "Delete contact" = "連絡先を削除"; -/* No comment provided by engineer. */ -"Delete Contact" = "連絡先を削除"; - /* No comment provided by engineer. */ "Delete database" = "データベースを削除"; @@ -1089,9 +1077,6 @@ /* No comment provided by engineer. */ "Delete old database?" = "古いデータベースを削除しますか?"; -/* No comment provided by engineer. */ -"Delete pending connection" = "確認待ちの接続を削除"; - /* No comment provided by engineer. */ "Delete pending connection?" = "接続待ちの接続を削除しますか?"; @@ -1416,9 +1401,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "接続の削除エラー"; -/* No comment provided by engineer. */ -"Error deleting contact" = "連絡先の削除にエラー発生"; - /* No comment provided by engineer. */ "Error deleting database" = "データベースの削除にエラー発生"; @@ -1509,7 +1491,8 @@ /* No comment provided by engineer. */ "Error: " = "エラー : "; -/* No comment provided by engineer. */ +/* file error text + snd error text */ "Error: %@" = "エラー : %@"; /* No comment provided by engineer. */ @@ -1545,7 +1528,7 @@ /* No comment provided by engineer. */ "Fast and no wait until the sender is online!" = "送信者がオンラインになるまでの待ち時間がなく、速い!"; -/* No comment provided by engineer. */ +/* swipe action */ "Favorite" = "お気に入り"; /* No comment provided by engineer. */ @@ -1929,7 +1912,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "日本語UI"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "参加"; /* No comment provided by engineer. */ @@ -1959,7 +1942,7 @@ /* No comment provided by engineer. */ "Learn more" = "さらに詳しく"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "脱退"; /* No comment provided by engineer. */ @@ -2130,19 +2113,16 @@ /* item status description */ "Most likely this connection is deleted." = "おそらく、この接続は削除されています。"; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "恐らくこの連絡先があなたとの接続を削除しました。"; - /* No comment provided by engineer. */ "Multiple chat profiles" = "複数チャットのプロフィール"; -/* No comment provided by engineer. */ +/* swipe action */ "Mute" = "ミュート"; /* No comment provided by engineer. */ "Muted when inactive!" = "非アクティブ時はミュート!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "名前"; /* No comment provided by engineer. */ @@ -2249,7 +2229,7 @@ time to disappear */ "off" = "オフ"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "オフ"; /* feature offered item */ @@ -2274,10 +2254,10 @@ "One-time invitation link" = "使い捨ての招待リンク"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "接続にオニオンのホストが必要となります。VPN を有効にする必要があります。"; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "接続にオニオンのホストが必要となります。\nVPN を有効にする必要があります。"; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "オニオンのホストが利用可能時に使われます。VPN を有効にする必要があります。"; +"Onion hosts will be used when available.\nRequires compatible VPN." = "オニオンのホストが利用可能時に使われます。\nVPN を有効にする必要があります。"; /* No comment provided by engineer. */ "Onion hosts will not be used." = "オニオンのホストが使われません。"; @@ -2504,7 +2484,7 @@ /* chat item menu */ "React…" = "反応する…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "読む"; /* No comment provided by engineer. */ @@ -2567,7 +2547,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "電池使用量低減"; -/* reject incoming call via notification */ +/* reject incoming call via notification + swipe action */ "Reject" = "拒否"; /* No comment provided by engineer. */ @@ -2651,9 +2632,6 @@ /* chat item action */ "Reveal" = "開示する"; -/* No comment provided by engineer. */ -"Revert" = "元に戻す"; - /* No comment provided by engineer. */ "Revoke" = "取り消す"; @@ -2756,7 +2734,7 @@ /* chat item text */ "security code changed" = "セキュリティコードが変更されました"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "選択"; /* No comment provided by engineer. */ @@ -2777,9 +2755,6 @@ /* No comment provided by engineer. */ "Send a live message - it will update for the recipient(s) as you type it" = "ライブメッセージを送信 (入力しながら宛先の画面で更新される)"; -/* No comment provided by engineer. */ -"Send direct message" = "ダイレクトメッセージを送信"; - /* No comment provided by engineer. */ "Send direct message to connect" = "ダイレクトメッセージを送信して接続する"; @@ -2951,9 +2926,6 @@ /* No comment provided by engineer. */ "Small groups (max 20)" = "小グループ(最大20名)"; -/* No comment provided by engineer. */ -"SMP servers" = "SMPサーバ"; - /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "インポート中に致命的でないエラーが発生しました - 詳細はチャットコンソールを参照してください。"; @@ -3029,9 +3001,6 @@ /* No comment provided by engineer. */ "Tap to join incognito" = "タップしてシークレットモードで参加"; -/* No comment provided by engineer. */ -"Tap to start a new chat" = "タップして新しいチャットを始める"; - /* No comment provided by engineer. */ "TCP connection timeout" = "TCP接続タイムアウト"; @@ -3116,9 +3085,6 @@ /* No comment provided by engineer. */ "The servers for new connections of your current chat profile **%@**." = "現在のチャットプロフィールの新しい接続のサーバ **%@**。"; -/* No comment provided by engineer. */ -"Theme" = "テーマ"; - /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "これらの設定は現在のプロファイル **%@** 用です。"; @@ -3191,13 +3157,10 @@ /* No comment provided by engineer. */ "Unable to record voice message" = "音声メッセージを録音できません"; -/* item status description */ -"Unexpected error: %@" = "予期しないエラー: %@"; - /* No comment provided by engineer. */ "Unexpected migration state" = "予期しない移行状態"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "お気に入りを取り消す。"; /* No comment provided by engineer. */ @@ -3236,36 +3199,27 @@ /* authentication reason */ "Unlock app" = "アプリのロック解除"; -/* No comment provided by engineer. */ +/* swipe action */ "Unmute" = "ミュート解除"; -/* No comment provided by engineer. */ +/* swipe action */ "Unread" = "未読"; /* No comment provided by engineer. */ "Update" = "更新"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = ".onionのホスト設定を更新しますか?"; - /* No comment provided by engineer. */ "Update database passphrase" = "データベースのパスフレーズを更新"; /* No comment provided by engineer. */ "Update network settings?" = "ネットワーク設定を更新しますか?"; -/* No comment provided by engineer. */ -"Update transport isolation mode?" = "トランスポート隔離モードを更新しますか?"; - /* rcv group event chat item */ "updated group profile" = "グループプロフィールを更新しました"; /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "設定を更新すると、全サーバにクライントの再接続が行われます。"; -/* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "設定を更新すると、全サーバにクライントの再接続が行われます。"; - /* No comment provided by engineer. */ "Upgrade and open chat" = "アップグレードしてチャットを開く"; @@ -3299,9 +3253,6 @@ /* No comment provided by engineer. */ "User profile" = "ユーザープロフィール"; -/* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = ".onionホストを使用するには、互換性のあるVPNプロバイダーが必要です。"; - /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "SimpleX チャット サーバーを使用する。"; @@ -3416,9 +3367,6 @@ /* No comment provided by engineer. */ "Wrong passphrase!" = "パスフレーズが違います!"; -/* No comment provided by engineer. */ -"XFTP servers" = "XFTPサーバ"; - /* pref value */ "yes" = "はい"; @@ -3465,7 +3413,7 @@ "You can hide or mute a user profile - swipe it to the right." = "ユーザープロファイルを右にスワイプすると、非表示またはミュートにすることができます。"; /* notification body */ -"You can now send messages to %@" = "%@ にメッセージを送信できるようになりました"; +"You can now chat with %@" = "%@ にメッセージを送信できるようになりました"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "設定からロック画面の通知プレビューを設定できます。"; @@ -3509,9 +3457,6 @@ /* No comment provided by engineer. */ "You could not be verified; please try again." = "確認できませんでした。 もう一度お試しください。"; -/* No comment provided by engineer. */ -"You have no chats" = "あなたはチャットがありません"; - /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "アプリ起動時にパスフレーズを入力しなければなりません。端末に保存されてません。"; @@ -3593,9 +3538,6 @@ /* No comment provided by engineer. */ "Your chat profiles" = "あなたのチャットプロフィール"; -/* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "接続を完了するには、連絡相手がオンラインになる必要があります。\nこの接続をキャンセルして、連絡先を削除をすることもできます (後でやり直すこともできます)。"; - /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "連絡先が現在サポートされている最大サイズ (%@) より大きいファイルを送信しました。"; diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index e12b8058f6..7d452743c6 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -154,9 +154,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ is geverifieerd"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ servers"; - /* No comment provided by engineer. */ "%@ uploaded" = "%@ geüpload"; @@ -338,10 +335,11 @@ "above, then choose:" = "hier boven, kies dan:"; /* No comment provided by engineer. */ -"Accent color" = "Accent kleur"; +"Accent" = "Accent"; /* accept contact request via notification - accept incoming call via notification */ + accept incoming call via notification + swipe action */ "Accept" = "Accepteer"; /* No comment provided by engineer. */ @@ -350,12 +348,22 @@ /* notification body */ "Accept contact request from %@?" = "Accepteer contactverzoek van %@?"; -/* accept contact request via notification */ +/* accept contact request via notification + swipe action */ "Accept incognito" = "Accepteer incognito"; /* call status */ "accepted call" = "geaccepteerde oproep"; +/* No comment provided by engineer. */ +"Acknowledged" = "Erkend"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Bevestigingsfouten"; + +/* No comment provided by engineer. */ +"Active connections" = "Actieve verbindingen"; + /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Voeg een adres toe aan uw profiel, zodat uw contacten het met andere mensen kunnen delen. Profiel update wordt naar uw contacten verzonden."; @@ -369,7 +377,7 @@ "Add profile" = "Profiel toevoegen"; /* No comment provided by engineer. */ -"Add server…" = "Server toevoegen…"; +"Add server" = "Server toevoegen"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Servers toevoegen door QR-codes te scannen."; @@ -378,7 +386,16 @@ "Add to another device" = "Toevoegen aan een ander apparaat"; /* No comment provided by engineer. */ -"Add welcome message" = "Welkomst bericht toevoegen"; +"Add welcome message" = "Welkom bericht toevoegen"; + +/* No comment provided by engineer. */ +"Additional accent" = "Extra accent"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Extra accent 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "Extra secundair"; /* No comment provided by engineer. */ "Address" = "Adres"; @@ -401,6 +418,9 @@ /* No comment provided by engineer. */ "Advanced network settings" = "Geavanceerde netwerk instellingen"; +/* No comment provided by engineer. */ +"Advanced settings" = "Geavanceerde instellingen"; + /* chat item text */ "agreeing encryption for %@…" = "versleuteling overeenkomen voor %@…"; @@ -416,6 +436,9 @@ /* No comment provided by engineer. */ "All data is erased when it is entered." = "Alle gegevens worden bij het invoeren gewist."; +/* No comment provided by engineer. */ +"All data is private to your device." = "Alle gegevens zijn privé op uw apparaat."; + /* No comment provided by engineer. */ "All group members will remain connected." = "Alle groepsleden blijven verbonden."; @@ -431,6 +454,9 @@ /* No comment provided by engineer. */ "All new messages from %@ will be hidden!" = "Alle nieuwe berichten van %@ worden verborgen!"; +/* No comment provided by engineer. */ +"All profiles" = "Alle profielen"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Al uw contacten blijven verbonden."; @@ -446,17 +472,23 @@ /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "Sta oproepen alleen toe als uw contact dit toestaat."; +/* No comment provided by engineer. */ +"Allow calls?" = "Oproepen toestaan?"; + /* No comment provided by engineer. */ "Allow disappearing messages only if your contact allows it to you." = "Sta verdwijnende berichten alleen toe als uw contact dit toestaat."; +/* No comment provided by engineer. */ +"Allow downgrade" = "Downgraden toestaan"; + /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Sta het onomkeerbaar verwijderen van berichten alleen toe als uw contact dit toestaat. (24 uur)"; /* No comment provided by engineer. */ -"Allow message reactions only if your contact allows them." = "Sta berichtreacties alleen toe als uw contact dit toestaat."; +"Allow message reactions only if your contact allows them." = "Sta bericht reacties alleen toe als uw contact dit toestaat."; /* No comment provided by engineer. */ -"Allow message reactions." = "Sta berichtreacties toe."; +"Allow message reactions." = "Sta bericht reacties toe."; /* No comment provided by engineer. */ "Allow sending direct messages to members." = "Sta het verzenden van directe berichten naar leden toe."; @@ -464,6 +496,9 @@ /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Toestaan dat verdwijnende berichten worden verzonden."; +/* No comment provided by engineer. */ +"Allow sharing" = "Delen toestaan"; + /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Sta toe om verzonden berichten onomkeerbaar te verwijderen. (24 uur)"; @@ -483,7 +518,7 @@ "Allow voice messages?" = "Spraak berichten toestaan?"; /* No comment provided by engineer. */ -"Allow your contacts adding message reactions." = "Sta uw contactpersonen toe om berichtreacties toe te voegen."; +"Allow your contacts adding message reactions." = "Sta uw contactpersonen toe om bericht reacties toe te voegen."; /* No comment provided by engineer. */ "Allow your contacts to call you." = "Sta toe dat uw contacten u bellen."; @@ -509,6 +544,9 @@ /* pref value */ "always" = "altijd"; +/* No comment provided by engineer. */ +"Always use private routing." = "Gebruik altijd privéroutering."; + /* No comment provided by engineer. */ "Always use relay" = "Altijd relay gebruiken"; @@ -551,15 +589,27 @@ /* No comment provided by engineer. */ "Apply" = "Toepassen"; +/* No comment provided by engineer. */ +"Apply to" = "Toepassen op"; + /* No comment provided by engineer. */ "Archive and upload" = "Archiveren en uploaden"; +/* No comment provided by engineer. */ +"Archive contacts to chat later." = "Archiveer contacten om later te chatten."; + +/* No comment provided by engineer. */ +"Archived contacts" = "Gearchiveerde contacten"; + /* No comment provided by engineer. */ "Archiving database" = "Database archiveren"; /* No comment provided by engineer. */ "Attach" = "Bijvoegen"; +/* No comment provided by engineer. */ +"attempts" = "pogingen"; + /* No comment provided by engineer. */ "Audio & video calls" = "Audio en video gesprekken"; @@ -602,6 +652,9 @@ /* No comment provided by engineer. */ "Back" = "Terug"; +/* No comment provided by engineer. */ +"Background" = "Achtergrond"; + /* No comment provided by engineer. */ "Bad desktop address" = "Onjuist desktopadres"; @@ -623,6 +676,12 @@ /* No comment provided by engineer. */ "Better messages" = "Betere berichten"; +/* No comment provided by engineer. */ +"Better networking" = "Beter netwerk"; + +/* No comment provided by engineer. */ +"Black" = "Zwart"; + /* No comment provided by engineer. */ "Block" = "Blokkeren"; @@ -653,11 +712,17 @@ /* No comment provided by engineer. */ "Blocked by admin" = "Geblokkeerd door beheerder"; +/* No comment provided by engineer. */ +"Blur for better privacy." = "Vervagen voor betere privacy."; + +/* No comment provided by engineer. */ +"Blur media" = "Vervaag media"; + /* No comment provided by engineer. */ "bold" = "vetgedrukt"; /* No comment provided by engineer. */ -"Both you and your contact can add message reactions." = "Zowel u als uw contact kunnen berichtreacties toevoegen."; +"Both you and your contact can add message reactions." = "Zowel u als uw contact kunnen bericht reacties toevoegen."; /* No comment provided by engineer. */ "Both you and your contact can irreversibly delete sent messages. (24 hours)" = "Zowel jij als je contact kunnen verzonden berichten onherroepelijk verwijderen. (24 uur)"; @@ -677,6 +742,9 @@ /* No comment provided by engineer. */ "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Via chat profiel (standaard) of [via verbinding](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; +/* No comment provided by engineer. */ +"call" = "bellen"; + /* No comment provided by engineer. */ "Call already ended!" = "Oproep al beëindigd!"; @@ -692,15 +760,27 @@ /* No comment provided by engineer. */ "Calls" = "Oproepen"; +/* No comment provided by engineer. */ +"Calls prohibited!" = "Bellen verboden!"; + /* No comment provided by engineer. */ "Camera not available" = "Camera niet beschikbaar"; +/* No comment provided by engineer. */ +"Can't call contact" = "Kan contact niet bellen"; + +/* No comment provided by engineer. */ +"Can't call member" = "Kan lid niet bellen"; + /* No comment provided by engineer. */ "Can't invite contact!" = "Kan contact niet uitnodigen!"; /* No comment provided by engineer. */ "Can't invite contacts!" = "Kan geen contacten uitnodigen!"; +/* No comment provided by engineer. */ +"Can't message member" = "Kan geen bericht sturen naar lid"; + /* No comment provided by engineer. */ "Cancel" = "Annuleren"; @@ -713,9 +793,15 @@ /* No comment provided by engineer. */ "Cannot access keychain to save database password" = "Geen toegang tot de keychain om database wachtwoord op te slaan"; +/* No comment provided by engineer. */ +"Cannot forward message" = "Kan bericht niet doorsturen"; + /* No comment provided by engineer. */ "Cannot receive file" = "Kan bestand niet ontvangen"; +/* snd error text */ +"Capacity exceeded - recipient did not receive previously sent messages." = "Capaciteit overschreden - ontvanger heeft eerder verzonden berichten niet ontvangen."; + /* No comment provided by engineer. */ "Cellular" = "Mobiel"; @@ -768,6 +854,9 @@ /* No comment provided by engineer. */ "Chat archive" = "Gesprek archief"; +/* No comment provided by engineer. */ +"Chat colors" = "Chat kleuren"; + /* No comment provided by engineer. */ "Chat console" = "Chat console"; @@ -777,6 +866,9 @@ /* No comment provided by engineer. */ "Chat database deleted" = "Chat database verwijderd"; +/* No comment provided by engineer. */ +"Chat database exported" = "Chat database geëxporteerd"; + /* No comment provided by engineer. */ "Chat database imported" = "Chat database geïmporteerd"; @@ -789,12 +881,18 @@ /* No comment provided by engineer. */ "Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." = "Chat is gestopt. Als je deze database al op een ander apparaat hebt gebruikt, moet je deze terugzetten voordat je met chatten begint."; +/* No comment provided by engineer. */ +"Chat list" = "Chatlijst"; + /* No comment provided by engineer. */ "Chat migrated!" = "Chat gemigreerd!"; /* No comment provided by engineer. */ "Chat preferences" = "Gesprek voorkeuren"; +/* No comment provided by engineer. */ +"Chat theme" = "Chat thema"; + /* No comment provided by engineer. */ "Chats" = "Gesprekken"; @@ -814,6 +912,15 @@ "Choose from library" = "Kies uit bibliotheek"; /* No comment provided by engineer. */ +"Chunks deleted" = "Stukken verwijderd"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Stukken gedownload"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Stukken geüpload"; + +/* swipe action */ "Clear" = "Wissen"; /* No comment provided by engineer. */ @@ -829,10 +936,13 @@ "Clear verification" = "Verwijderd verificatie"; /* No comment provided by engineer. */ -"colored" = "gekleurd"; +"Color chats with the new themes." = "Kleurchats met de nieuwe thema's."; /* No comment provided by engineer. */ -"Colors" = "Kleuren"; +"Color mode" = "Kleur mode"; + +/* No comment provided by engineer. */ +"colored" = "gekleurd"; /* server test step */ "Compare file" = "Bestand vergelijken"; @@ -843,15 +953,27 @@ /* No comment provided by engineer. */ "complete" = "compleet"; +/* No comment provided by engineer. */ +"Completed" = "voltooid"; + /* No comment provided by engineer. */ "Configure ICE servers" = "ICE servers configureren"; +/* No comment provided by engineer. */ +"Configured %@ servers" = "%@ servers geconfigureerd"; + /* No comment provided by engineer. */ "Confirm" = "Bevestigen"; +/* No comment provided by engineer. */ +"Confirm contact deletion?" = "Contact verwijderen bevestigen?"; + /* No comment provided by engineer. */ "Confirm database upgrades" = "Bevestig database upgrades"; +/* No comment provided by engineer. */ +"Confirm files from unknown servers." = "Bevestig bestanden van onbekende servers."; + /* No comment provided by engineer. */ "Confirm network settings" = "Bevestig netwerk instellingen"; @@ -885,6 +1007,9 @@ /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "maak verbinding met SimpleX Chat-ontwikkelaars."; +/* No comment provided by engineer. */ +"Connect to your friends faster." = "Maak sneller verbinding met je vrienden."; + /* No comment provided by engineer. */ "Connect to yourself?" = "Verbinding maken met jezelf?"; @@ -909,18 +1034,27 @@ /* No comment provided by engineer. */ "connected" = "verbonden"; +/* No comment provided by engineer. */ +"Connected" = "Verbonden"; + /* No comment provided by engineer. */ "Connected desktop" = "Verbonden desktop"; /* rcv group event chat item */ "connected directly" = "direct verbonden"; +/* No comment provided by engineer. */ +"Connected servers" = "Verbonden servers"; + /* No comment provided by engineer. */ "Connected to desktop" = "Verbonden met desktop"; /* No comment provided by engineer. */ "connecting" = "Verbinden"; +/* No comment provided by engineer. */ +"Connecting" = "Verbinden"; + /* No comment provided by engineer. */ "connecting (accepted)" = "verbinden (geaccepteerd)"; @@ -942,6 +1076,9 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "Verbinden met server... (fout: %@)"; +/* No comment provided by engineer. */ +"Connecting to contact, please wait or check later!" = "Er wordt verbinding gemaakt met het contact. Even geduld of controleer het later!"; + /* No comment provided by engineer. */ "Connecting to desktop" = "Verbinding maken met desktop"; @@ -951,6 +1088,9 @@ /* No comment provided by engineer. */ "Connection" = "Verbinding"; +/* No comment provided by engineer. */ +"Connection and servers status." = "Verbindings- en serverstatus."; + /* No comment provided by engineer. */ "Connection error" = "Verbindingsfout"; @@ -960,6 +1100,9 @@ /* chat list item title (it should not be shown */ "connection established" = "verbinding gemaakt"; +/* No comment provided by engineer. */ +"Connection notifications" = "Verbindingsmeldingen"; + /* No comment provided by engineer. */ "Connection request sent!" = "Verbindingsverzoek verzonden!"; @@ -969,9 +1112,15 @@ /* No comment provided by engineer. */ "Connection timeout" = "Timeout verbinding"; +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "Verbinding met desktop is gestopt"; + /* connection information */ "connection:%@" = "verbinding:%@"; +/* No comment provided by engineer. */ +"Connections" = "Verbindingen"; + /* profile update event chat item */ "contact %@ changed to %@" = "contactpersoon %1$@ gewijzigd in %2$@"; @@ -981,6 +1130,9 @@ /* No comment provided by engineer. */ "Contact already exists" = "Contact bestaat al"; +/* No comment provided by engineer. */ +"Contact deleted!" = "Contact verwijderd!"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "contact heeft e2e-codering"; @@ -994,7 +1146,7 @@ "Contact is connected" = "Contact is verbonden"; /* No comment provided by engineer. */ -"Contact is not connected yet!" = "Contact is nog niet verbonden!"; +"Contact is deleted." = "Contact is verwijderd."; /* No comment provided by engineer. */ "Contact name" = "Contact naam"; @@ -1002,6 +1154,9 @@ /* No comment provided by engineer. */ "Contact preferences" = "Contact voorkeuren"; +/* No comment provided by engineer. */ +"Contact will be deleted - this cannot be undone!" = "Het contact wordt verwijderd. Dit kan niet ongedaan worden gemaakt!"; + /* No comment provided by engineer. */ "Contacts" = "Contacten"; @@ -1011,9 +1166,15 @@ /* No comment provided by engineer. */ "Continue" = "Doorgaan"; -/* chat item action */ +/* No comment provided by engineer. */ +"Conversation deleted!" = "Gesprek verwijderd!"; + +/* No comment provided by engineer. */ "Copy" = "Kopiëren"; +/* No comment provided by engineer. */ +"Copy error" = "Kopieerfout"; + /* No comment provided by engineer. */ "Core version: v%@" = "Core versie: v% @"; @@ -1059,6 +1220,9 @@ /* No comment provided by engineer. */ "Create your profile" = "Maak je profiel aan"; +/* No comment provided by engineer. */ +"Created" = "Gemaakt"; + /* No comment provided by engineer. */ "Created at" = "Gemaakt op"; @@ -1083,6 +1247,9 @@ /* No comment provided by engineer. */ "Current passphrase…" = "Huidige wachtwoord…"; +/* No comment provided by engineer. */ +"Current profile" = "Huidig profiel"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "De momenteel maximaal ondersteunde bestandsgrootte is %@."; @@ -1092,9 +1259,15 @@ /* No comment provided by engineer. */ "Custom time" = "Aangepaste tijd"; +/* No comment provided by engineer. */ +"Customize theme" = "Thema aanpassen"; + /* No comment provided by engineer. */ "Dark" = "Donker"; +/* No comment provided by engineer. */ +"Dark mode colors" = "Kleuren in donkere modus"; + /* No comment provided by engineer. */ "Database downgrade" = "Database downgraden"; @@ -1155,12 +1328,18 @@ /* time unit */ "days" = "dagen"; +/* No comment provided by engineer. */ +"Debug delivery" = "Foutopsporing bezorging"; + /* No comment provided by engineer. */ "Decentralized" = "Gedecentraliseerd"; /* message decrypt error item */ "Decryption error" = "Decodering fout"; +/* No comment provided by engineer. */ +"decryption errors" = "decoderingsfouten"; + /* pref value */ "default (%@)" = "standaard (%@)"; @@ -1170,9 +1349,13 @@ /* No comment provided by engineer. */ "default (yes)" = "standaard (ja)"; -/* chat item action */ +/* chat item action + swipe action */ "Delete" = "Verwijderen"; +/* No comment provided by engineer. */ +"Delete %lld messages of members?" = "%lld berichten van leden verwijderen?"; + /* No comment provided by engineer. */ "Delete %lld messages?" = "%lld berichten verwijderen?"; @@ -1210,10 +1393,7 @@ "Delete contact" = "Verwijder contact"; /* No comment provided by engineer. */ -"Delete Contact" = "Verwijder contact"; - -/* No comment provided by engineer. */ -"Delete contact?\nThis cannot be undone!" = "Verwijder contact?\nDit kan niet ongedaan gemaakt worden!"; +"Delete contact?" = "Verwijder contact?"; /* No comment provided by engineer. */ "Delete database" = "Database verwijderen"; @@ -1269,9 +1449,6 @@ /* No comment provided by engineer. */ "Delete old database?" = "Oude database verwijderen?"; -/* No comment provided by engineer. */ -"Delete pending connection" = "Wachtende verbinding verwijderen"; - /* No comment provided by engineer. */ "Delete pending connection?" = "Wachtende verbinding verwijderen?"; @@ -1281,12 +1458,21 @@ /* server test step */ "Delete queue" = "Wachtrij verwijderen"; +/* No comment provided by engineer. */ +"Delete up to 20 messages at once." = "Verwijder maximaal 20 berichten tegelijk."; + /* No comment provided by engineer. */ "Delete user profile?" = "Gebruikers profiel verwijderen?"; +/* No comment provided by engineer. */ +"Delete without notification" = "Verwijderen zonder melding"; + /* deleted chat item */ "deleted" = "verwijderd"; +/* No comment provided by engineer. */ +"Deleted" = "Verwijderd"; + /* No comment provided by engineer. */ "Deleted at" = "Verwijderd om"; @@ -1299,6 +1485,9 @@ /* rcv group event chat item */ "deleted group" = "verwijderde groep"; +/* No comment provided by engineer. */ +"Deletion errors" = "Verwijderingsfouten"; + /* No comment provided by engineer. */ "Delivery" = "Bezorging"; @@ -1320,9 +1509,27 @@ /* No comment provided by engineer. */ "Desktop devices" = "Desktop apparaten"; +/* No comment provided by engineer. */ +"Destination server address of %@ is incompatible with forwarding server %@ settings." = "Het bestemmingsserveradres van %@ is niet compatibel met de doorstuurserverinstellingen %@."; + +/* snd error text */ +"Destination server error: %@" = "Bestemmingsserverfout: %@"; + +/* No comment provided by engineer. */ +"Destination server version of %@ is incompatible with forwarding server %@." = "De versie van de bestemmingsserver %@ is niet compatibel met de doorstuurserver %@."; + +/* No comment provided by engineer. */ +"Detailed statistics" = "Gedetailleerde statistieken"; + +/* No comment provided by engineer. */ +"Details" = "Details"; + /* No comment provided by engineer. */ "Develop" = "Ontwikkelen"; +/* No comment provided by engineer. */ +"Developer options" = "Ontwikkelaars opties"; + /* No comment provided by engineer. */ "Developer tools" = "Ontwikkel gereedschap"; @@ -1362,6 +1569,9 @@ /* No comment provided by engineer. */ "disabled" = "uitgeschakeld"; +/* No comment provided by engineer. */ +"Disabled" = "Uitgeschakeld"; + /* No comment provided by engineer. */ "Disappearing message" = "Verdwijnend bericht"; @@ -1398,6 +1608,12 @@ /* No comment provided by engineer. */ "Do not send history to new members." = "Stuur geen geschiedenis naar nieuwe leden."; +/* No comment provided by engineer. */ +"Do NOT send messages directly, even if your or destination server does not support private routing." = "Stuur GEEN berichten rechtstreeks, zelfs als uw of de bestemmingsserver geen privéroutering ondersteunt."; + +/* No comment provided by engineer. */ +"Do NOT use private routing." = "Gebruik GEEN privéroutering."; + /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "Gebruik SimpleX NIET voor noodoproepen."; @@ -1416,12 +1632,21 @@ /* chat item action */ "Download" = "Downloaden"; +/* No comment provided by engineer. */ +"Download errors" = "Downloadfouten"; + /* No comment provided by engineer. */ "Download failed" = "Download mislukt"; /* server test step */ "Download file" = "Download bestand"; +/* No comment provided by engineer. */ +"Downloaded" = "Gedownload"; + +/* No comment provided by engineer. */ +"Downloaded files" = "Gedownloade bestanden"; + /* No comment provided by engineer. */ "Downloading archive" = "Archief downloaden"; @@ -1434,6 +1659,9 @@ /* integrity error chat item */ "duplicate message" = "dubbel bericht"; +/* No comment provided by engineer. */ +"duplicates" = "duplicaten"; + /* No comment provided by engineer. */ "Duration" = "Duur"; @@ -1491,6 +1719,9 @@ /* enabled status */ "enabled" = "ingeschakeld"; +/* No comment provided by engineer. */ +"Enabled" = "Ingeschakeld"; + /* No comment provided by engineer. */ "Enabled for" = "Ingeschakeld voor"; @@ -1597,10 +1828,10 @@ "Enter this device name…" = "Voer deze apparaatnaam in…"; /* placeholder */ -"Enter welcome message…" = "Welkomst bericht invoeren…"; +"Enter welcome message…" = "Welkom bericht invoeren…"; /* placeholder */ -"Enter welcome message… (optional)" = "Voer welkomst bericht in... (optioneel)"; +"Enter welcome message… (optional)" = "Voer welkom bericht in... (optioneel)"; /* No comment provided by engineer. */ "Enter your name…" = "Vul uw naam in…"; @@ -1632,6 +1863,9 @@ /* No comment provided by engineer. */ "Error changing setting" = "Fout bij wijzigen van instelling"; +/* No comment provided by engineer. */ +"Error connecting to forwarding server %@. Please try later." = "Fout bij het verbinden met doorstuurserver %@. Probeer het later opnieuw."; + /* No comment provided by engineer. */ "Error creating address" = "Fout bij aanmaken van adres"; @@ -1662,9 +1896,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "Fout bij verwijderen van verbinding"; -/* No comment provided by engineer. */ -"Error deleting contact" = "Fout bij het verwijderen van contact"; - /* No comment provided by engineer. */ "Error deleting database" = "Fout bij het verwijderen van de database"; @@ -1692,6 +1923,9 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "Fout bij het exporteren van de chat database"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Fout bij exporteren van thema: %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "Fout bij het importeren van de chat database"; @@ -1707,9 +1941,18 @@ /* No comment provided by engineer. */ "Error receiving file" = "Fout bij ontvangen van bestand"; +/* No comment provided by engineer. */ +"Error reconnecting server" = "Fout bij opnieuw verbinding maken met de server"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "Fout bij opnieuw verbinden van servers"; + /* No comment provided by engineer. */ "Error removing member" = "Fout bij verwijderen van lid"; +/* No comment provided by engineer. */ +"Error resetting statistics" = "Fout bij het resetten van statistieken"; + /* No comment provided by engineer. */ "Error saving %@ servers" = "Fout bij opslaan van %@ servers"; @@ -1779,7 +2022,8 @@ /* No comment provided by engineer. */ "Error: " = "Fout: "; -/* No comment provided by engineer. */ +/* file error text + snd error text */ "Error: %@" = "Fout: %@"; /* No comment provided by engineer. */ @@ -1788,6 +2032,9 @@ /* No comment provided by engineer. */ "Error: URL is invalid" = "Fout: URL is ongeldig"; +/* No comment provided by engineer. */ +"Errors" = "Fouten"; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Zelfs wanneer uitgeschakeld in het gesprek."; @@ -1800,12 +2047,18 @@ /* chat item action */ "Expand" = "Uitklappen"; +/* No comment provided by engineer. */ +"expired" = "verlopen"; + /* No comment provided by engineer. */ "Export database" = "Database exporteren"; /* No comment provided by engineer. */ "Export error:" = "Exportfout:"; +/* No comment provided by engineer. */ +"Export theme" = "Exporteer thema"; + /* No comment provided by engineer. */ "Exported database archive." = "Geëxporteerd database archief."; @@ -1824,9 +2077,24 @@ /* No comment provided by engineer. */ "Faster joining and more reliable messages." = "Snellere deelname en betrouwbaardere berichten."; -/* No comment provided by engineer. */ +/* swipe action */ "Favorite" = "Favoriet"; +/* No comment provided by engineer. */ +"File error" = "Bestandsfout"; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "Bestand niet gevonden - hoogstwaarschijnlijk is het bestand verwijderd of geannuleerd."; + +/* file error text */ +"File server error: %@" = "Bestandsserverfout: %@"; + +/* No comment provided by engineer. */ +"File status" = "Bestandsstatus"; + +/* copied message info */ +"File status: %@" = "Bestandsstatus: %@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "Het bestand wordt van de servers verwijderd."; @@ -1839,6 +2107,9 @@ /* No comment provided by engineer. */ "File: %@" = "Bestand: %@"; +/* No comment provided by engineer. */ +"Files" = "Bestanden"; + /* No comment provided by engineer. */ "Files & media" = "Bestanden en media"; @@ -1905,6 +2176,21 @@ /* No comment provided by engineer. */ "Forwarded from" = "Doorgestuurd vanuit"; +/* No comment provided by engineer. */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "De doorstuurserver %@ kon geen verbinding maken met de bestemmingsserver %@. Probeer het later opnieuw."; + +/* No comment provided by engineer. */ +"Forwarding server address is incompatible with network settings: %@." = "Het adres van de doorstuurserver is niet compatibel met de netwerkinstellingen: %@."; + +/* No comment provided by engineer. */ +"Forwarding server version is incompatible with network settings: %@." = "De doorstuurserverversie is niet compatibel met de netwerkinstellingen: %@."; + +/* snd error text */ +"Forwarding server: %@\nDestination server error: %@" = "Doorstuurserver: %1$@\nBestemmingsserverfout: %2$@"; + +/* snd error text */ +"Forwarding server: %@\nError: %@" = "Doorstuurserver: %1$@\nFout: %2$@"; + /* No comment provided by engineer. */ "Found desktop" = "Desktop gevonden"; @@ -1932,6 +2218,12 @@ /* No comment provided by engineer. */ "GIFs and stickers" = "GIF's en stickers"; +/* message preview */ +"Good afternoon!" = "Goedemiddag!"; + +/* message preview */ +"Good morning!" = "Goedemorgen!"; + /* No comment provided by engineer. */ "Group" = "Groep"; @@ -1969,7 +2261,7 @@ "Group links" = "Groep links"; /* No comment provided by engineer. */ -"Group members can add message reactions." = "Groepsleden kunnen berichtreacties toevoegen."; +"Group members can add message reactions." = "Groepsleden kunnen bericht reacties toevoegen."; /* No comment provided by engineer. */ "Group members can irreversibly delete sent messages. (24 hours)" = "Groepsleden kunnen verzonden berichten onherroepelijk verwijderen. (24 uur)"; @@ -2008,7 +2300,7 @@ "group profile updated" = "groep profiel bijgewerkt"; /* No comment provided by engineer. */ -"Group welcome message" = "Groep welkomst bericht"; +"Group welcome message" = "Groep welkom bericht"; /* No comment provided by engineer. */ "Group will be deleted for all members - this cannot be undone!" = "Groep wordt verwijderd voor alle leden, dit kan niet ongedaan worden gemaakt!"; @@ -2109,6 +2401,9 @@ /* No comment provided by engineer. */ "Import failed" = "Importeren is mislukt"; +/* No comment provided by engineer. */ +"Import theme" = "Thema importeren"; + /* No comment provided by engineer. */ "Importing archive" = "Archief importeren"; @@ -2130,6 +2425,9 @@ /* No comment provided by engineer. */ "In-call sounds" = "Geluiden tijdens het bellen"; +/* No comment provided by engineer. */ +"inactive" = "inactief"; + /* No comment provided by engineer. */ "Incognito" = "Incognito"; @@ -2193,6 +2491,9 @@ /* No comment provided by engineer. */ "Interface" = "Interface"; +/* No comment provided by engineer. */ +"Interface colors" = "Interface kleuren"; + /* invalid chat data */ "invalid chat" = "ongeldige gesprek"; @@ -2235,6 +2536,9 @@ /* group name */ "invitation to group %@" = "uitnodiging voor groep %@"; +/* No comment provided by engineer. */ +"invite" = "uitnodiging"; + /* No comment provided by engineer. */ "Invite friends" = "Nodig vrienden uit"; @@ -2280,6 +2584,9 @@ /* No comment provided by engineer. */ "It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Het kan gebeuren wanneer:\n1. De berichten zijn na 2 dagen verlopen bij de verzendende client of na 30 dagen op de server.\n2. Decodering van het bericht is mislukt, omdat u of uw contact een oude database back-up heeft gebruikt.\n3. De verbinding is verbroken."; +/* No comment provided by engineer. */ +"It protects your IP address and connections." = "Het beschermt uw IP-adres en verbindingen."; + /* No comment provided by engineer. */ "It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Het lijkt erop dat u al bent verbonden via deze link. Als dit niet het geval is, is er een fout opgetreden (%@)."; @@ -2292,7 +2599,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Japanse interface"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Word lid van"; /* No comment provided by engineer. */ @@ -2322,6 +2629,9 @@ /* No comment provided by engineer. */ "Keep" = "Bewaar"; +/* No comment provided by engineer. */ +"Keep conversation" = "Behoud het gesprek"; + /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Houd de app geopend om deze vanaf de desktop te gebruiken"; @@ -2343,7 +2653,7 @@ /* No comment provided by engineer. */ "Learn more" = "Kom meer te weten"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "Verlaten"; /* No comment provided by engineer. */ @@ -2433,6 +2743,12 @@ /* No comment provided by engineer. */ "Max 30 seconds, received instantly." = "Max 30 seconden, direct ontvangen."; +/* No comment provided by engineer. */ +"Media & file servers" = "Media- en bestandsservers"; + +/* blur media */ +"Medium" = "Medium"; + /* member role */ "member" = "lid"; @@ -2445,6 +2761,9 @@ /* rcv group event chat item */ "member connected" = "is toegetreden"; +/* item status text */ +"Member inactive" = "Lid inactief"; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "De rol van lid wordt gewijzigd in \"%@\". Alle groepsleden worden op de hoogte gebracht."; @@ -2454,15 +2773,33 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Lid wordt uit de groep verwijderd, dit kan niet ongedaan worden gemaakt!"; +/* No comment provided by engineer. */ +"Menus" = "Menu's"; + +/* No comment provided by engineer. */ +"message" = "bericht"; + /* item status text */ "Message delivery error" = "Fout bij bezorging van bericht"; /* No comment provided by engineer. */ "Message delivery receipts!" = "Ontvangst bevestiging voor berichten!"; +/* item status text */ +"Message delivery warning" = "Waarschuwing voor berichtbezorging"; + /* No comment provided by engineer. */ "Message draft" = "Concept bericht"; +/* item status text */ +"Message forwarded" = "Bericht doorgestuurd"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "Het bericht kan later worden bezorgd als het lid actief wordt."; + +/* No comment provided by engineer. */ +"Message queue info" = "Informatie over berichtenwachtrij"; + /* chat feature */ "Message reactions" = "Reacties op berichten"; @@ -2475,9 +2812,21 @@ /* notification */ "message received" = "bericht ontvangen"; +/* No comment provided by engineer. */ +"Message reception" = "Bericht ontvangst"; + +/* No comment provided by engineer. */ +"Message servers" = "Berichtservers"; + /* No comment provided by engineer. */ "Message source remains private." = "Berichtbron blijft privé."; +/* No comment provided by engineer. */ +"Message status" = "Berichtstatus"; + +/* copied message info */ +"Message status: %@" = "Berichtstatus: %@"; + /* No comment provided by engineer. */ "Message text" = "Bericht tekst"; @@ -2493,6 +2842,12 @@ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "Berichten van %@ worden getoond!"; +/* No comment provided by engineer. */ +"Messages received" = "Berichten ontvangen"; + +/* No comment provided by engineer. */ +"Messages sent" = "Berichten verzonden"; + /* No comment provided by engineer. */ "Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Berichten, bestanden en oproepen worden beschermd door **end-to-end codering** met perfecte voorwaartse geheimhouding, afwijzing en inbraakherstel."; @@ -2527,7 +2882,7 @@ "Migration error:" = "Migratiefout:"; /* No comment provided by engineer. */ -"Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." = "Migratie mislukt. Tik hieronder op **Overslaan** om de huidige database te blijven gebruiken. Meld het probleem aan de app-ontwikkelaars via chat of e-mail [chat@simplex.chat](mailto:chat@simplex.chat)."; +"Migration failed. Tap **Skip** below to continue using the current database. Please report the issue to the app developers via chat or email [chat@simplex.chat](mailto:chat@simplex.chat)." = "Migratie mislukt. Tik hier onder op **Overslaan** om de huidige database te blijven gebruiken. Meld het probleem aan de app-ontwikkelaars via chat of e-mail [chat@simplex.chat](mailto:chat@simplex.chat)."; /* No comment provided by engineer. */ "Migration is completed" = "Migratie is voltooid"; @@ -2568,19 +2923,19 @@ /* item status description */ "Most likely this connection is deleted." = "Hoogstwaarschijnlijk is deze verbinding verwijderd."; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Hoogstwaarschijnlijk heeft dit contact de verbinding met jou verwijderd."; - /* No comment provided by engineer. */ "Multiple chat profiles" = "Meerdere chat profielen"; /* No comment provided by engineer. */ +"mute" = "dempen"; + +/* swipe action */ "Mute" = "Dempen"; /* No comment provided by engineer. */ "Muted when inactive!" = "Gedempt wanneer inactief!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "Naam"; /* No comment provided by engineer. */ @@ -2589,6 +2944,9 @@ /* No comment provided by engineer. */ "Network connection" = "Netwerkverbinding"; +/* snd error text */ +"Network issues - message expired after many attempts to send it." = "Netwerkproblemen - bericht is verlopen na vele pogingen om het te verzenden."; + /* No comment provided by engineer. */ "Network management" = "Netwerkbeheer"; @@ -2604,6 +2962,9 @@ /* No comment provided by engineer. */ "New chat" = "Nieuw gesprek"; +/* No comment provided by engineer. */ +"New chat experience 🎉" = "Nieuwe chatervaring 🎉"; + /* notification */ "New contact request" = "Nieuw contactverzoek"; @@ -2622,6 +2983,9 @@ /* No comment provided by engineer. */ "New in %@" = "Nieuw in %@"; +/* No comment provided by engineer. */ +"New media options" = "Nieuwe media-opties"; + /* No comment provided by engineer. */ "New member role" = "Nieuwe leden rol"; @@ -2658,6 +3022,9 @@ /* No comment provided by engineer. */ "No device token!" = "Geen apparaattoken!"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Nog geen directe verbinding, bericht wordt doorgestuurd door beheerder."; + /* No comment provided by engineer. */ "no e2e encryption" = "geen e2e versleuteling"; @@ -2670,6 +3037,9 @@ /* No comment provided by engineer. */ "No history" = "Geen geschiedenis"; +/* No comment provided by engineer. */ +"No info, try to reload" = "Geen info, probeer opnieuw te laden"; + /* No comment provided by engineer. */ "No network connection" = "Geen netwerkverbinding"; @@ -2685,6 +3055,9 @@ /* No comment provided by engineer. */ "Not compatible!" = "Niet compatibel!"; +/* No comment provided by engineer. */ +"Nothing selected" = "Niets geselecteerd"; + /* No comment provided by engineer. */ "Notifications" = "Meldingen"; @@ -2702,7 +3075,7 @@ time to disappear */ "off" = "uit"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Uit"; /* feature offered item */ @@ -2730,10 +3103,10 @@ "One-time invitation link" = "Eenmalige uitnodiging link"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "Onion hosts zullen nodig zijn voor verbinding. Vereist het inschakelen van VPN."; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Onion hosts zullen nodig zijn voor verbinding.\nVereist het inschakelen van VPN."; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "Onion hosts worden gebruikt indien beschikbaar. Vereist het inschakelen van VPN."; +"Onion hosts will be used when available.\nRequires compatible VPN." = "Onion hosts worden gebruikt indien beschikbaar.\nVereist het inschakelen van VPN."; /* No comment provided by engineer. */ "Onion hosts will not be used." = "Onion hosts worden niet gebruikt."; @@ -2741,6 +3114,9 @@ /* No comment provided by engineer. */ "Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Alleen client apparaten slaan gebruikers profielen, contacten, groepen en berichten op die zijn verzonden met **2-laags end-to-end-codering**."; +/* No comment provided by engineer. */ +"Only delete conversation" = "Alleen conversatie verwijderen"; + /* No comment provided by engineer. */ "Only group owners can change group preferences." = "Alleen groep eigenaren kunnen groep voorkeuren wijzigen."; @@ -2751,7 +3127,7 @@ "Only group owners can enable voice messages." = "Alleen groep eigenaren kunnen spraak berichten inschakelen."; /* No comment provided by engineer. */ -"Only you can add message reactions." = "Alleen jij kunt berichtreacties toevoegen."; +"Only you can add message reactions." = "Alleen jij kunt bericht reacties toevoegen."; /* No comment provided by engineer. */ "Only you can irreversibly delete messages (your contact can mark them for deletion). (24 hours)" = "Alleen jij kunt berichten onomkeerbaar verwijderen (je contact kan ze markeren voor verwijdering). (24 uur)"; @@ -2766,7 +3142,7 @@ "Only you can send voice messages." = "Alleen jij kunt spraak berichten verzenden."; /* No comment provided by engineer. */ -"Only your contact can add message reactions." = "Alleen uw contact kan berichtreacties toevoegen."; +"Only your contact can add message reactions." = "Alleen uw contact kan bericht reacties toevoegen."; /* No comment provided by engineer. */ "Only your contact can irreversibly delete messages (you can mark them for deletion). (24 hours)" = "Alleen uw contact kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering). (24 uur)"; @@ -2795,6 +3171,9 @@ /* authentication reason */ "Open migration to another device" = "Open de migratie naar een ander apparaat"; +/* No comment provided by engineer. */ +"Open server settings" = "Server instellingen openen"; + /* No comment provided by engineer. */ "Open Settings" = "Open instellingen"; @@ -2819,9 +3198,18 @@ /* No comment provided by engineer. */ "Or show this code" = "Of laat deze code zien"; +/* No comment provided by engineer. */ +"other" = "overig"; + /* No comment provided by engineer. */ "Other" = "Ander"; +/* No comment provided by engineer. */ +"Other %@ servers" = "Andere %@ servers"; + +/* No comment provided by engineer. */ +"other errors" = "overige fouten"; + /* member role */ "owner" = "Eigenaar"; @@ -2864,6 +3252,9 @@ /* No comment provided by engineer. */ "peer-to-peer" = "peer-to-peer"; +/* No comment provided by engineer. */ +"Pending" = "in behandeling"; + /* No comment provided by engineer. */ "People can connect to you only via the links you share." = "Mensen kunnen alleen verbinding met u maken via de links die u deelt."; @@ -2882,9 +3273,18 @@ /* No comment provided by engineer. */ "PING interval" = "PING interval"; +/* No comment provided by engineer. */ +"Play from the chat list." = "Afspelen via de gesprekken lijst."; + +/* No comment provided by engineer. */ +"Please ask your contact to enable calls." = "Vraag uw contactpersoon om oproepen in te schakelen."; + /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "Vraag uw contact om het verzenden van spraak berichten in te schakelen."; +/* No comment provided by engineer. */ +"Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.\nPlease share any other issues with the developers." = "Controleer of mobiel en desktop met hetzelfde lokale netwerk zijn verbonden en of de desktopfirewall de verbinding toestaat.\nDeel eventuele andere problemen met de ontwikkelaars."; + /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "Controleer of u de juiste link heeft gebruikt of vraag uw contact om u een andere te sturen."; @@ -2942,6 +3342,9 @@ /* No comment provided by engineer. */ "Preview" = "Voorbeeld"; +/* No comment provided by engineer. */ +"Previously connected servers" = "Eerder verbonden servers"; + /* No comment provided by engineer. */ "Privacy & security" = "Privacy en beveiliging"; @@ -2951,9 +3354,21 @@ /* No comment provided by engineer. */ "Private filenames" = "Privé bestandsnamen"; +/* No comment provided by engineer. */ +"Private message routing" = "Routering van privéberichten"; + +/* No comment provided by engineer. */ +"Private message routing 🚀" = "Routing van privéberichten🚀"; + /* name of notes to self */ "Private notes" = "Privé notities"; +/* No comment provided by engineer. */ +"Private routing" = "Privéroutering"; + +/* No comment provided by engineer. */ +"Private routing error" = "Fout in privéroutering"; + /* No comment provided by engineer. */ "Profile and server connections" = "Profiel- en serververbindingen"; @@ -2972,6 +3387,9 @@ /* No comment provided by engineer. */ "Profile password" = "Profiel wachtwoord"; +/* No comment provided by engineer. */ +"Profile theme" = "Profiel thema"; + /* No comment provided by engineer. */ "Profile update will be sent to your contacts." = "Profiel update wordt naar uw contacten verzonden."; @@ -2982,7 +3400,7 @@ "Prohibit irreversible message deletion." = "Verbied het onomkeerbaar verwijderen van berichten."; /* No comment provided by engineer. */ -"Prohibit message reactions." = "Berichtreacties verbieden."; +"Prohibit message reactions." = "Bericht reacties verbieden."; /* No comment provided by engineer. */ "Prohibit messages reactions." = "Berichten reacties verbieden."; @@ -3005,15 +3423,27 @@ /* No comment provided by engineer. */ "Protect app screen" = "App scherm verbergen"; +/* No comment provided by engineer. */ +"Protect IP address" = "Bescherm het IP-adres"; + /* No comment provided by engineer. */ "Protect your chat profiles with a password!" = "Bescherm je chat profielen met een wachtwoord!"; +/* No comment provided by engineer. */ +"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Bescherm uw IP-adres tegen de berichtenrelais die door uw contacten zijn gekozen.\nSchakel dit in in *Netwerk en servers*-instellingen."; + /* No comment provided by engineer. */ "Protocol timeout" = "Protocol timeout"; /* No comment provided by engineer. */ "Protocol timeout per KB" = "Protocol timeout per KB"; +/* No comment provided by engineer. */ +"Proxied" = "Proxied"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Proxied servers"; + /* No comment provided by engineer. */ "Push notifications" = "Push meldingen"; @@ -3029,10 +3459,13 @@ /* No comment provided by engineer. */ "Rate the app" = "Beoordeel de app"; +/* No comment provided by engineer. */ +"Reachable chat toolbar" = "Toegankelijke chatwerkbalk"; + /* chat item menu */ "React…" = "Reageer…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "Lees"; /* No comment provided by engineer. */ @@ -3056,6 +3489,9 @@ /* No comment provided by engineer. */ "Receipts are disabled" = "Bevestigingen zijn uitgeschakeld"; +/* No comment provided by engineer. */ +"Receive errors" = "Fouten ontvangen"; + /* No comment provided by engineer. */ "received answer…" = "antwoord gekregen…"; @@ -3075,10 +3511,16 @@ "Received message" = "Ontvangen bericht"; /* No comment provided by engineer. */ -"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Het ontvangstadres wordt gewijzigd naar een andere server. Adres wijziging wordt voltooid nadat de afzender online is."; +"Received messages" = "Ontvangen berichten"; /* No comment provided by engineer. */ -"Receiving concurrency" = "Gelijktijdig ontvangen"; +"Received reply" = "Antwoord ontvangen"; + +/* No comment provided by engineer. */ +"Received total" = "Totaal ontvangen"; + +/* No comment provided by engineer. */ +"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Het ontvangstadres wordt gewijzigd naar een andere server. Adres wijziging wordt voltooid nadat de afzender online is."; /* No comment provided by engineer. */ "Receiving file will be stopped." = "Het ontvangen van het bestand wordt gestopt."; @@ -3095,9 +3537,24 @@ /* No comment provided by engineer. */ "Recipients see updates as you type them." = "Ontvangers zien updates terwijl u ze typt."; +/* No comment provided by engineer. */ +"Reconnect" = "opnieuw verbinden"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Verbind alle verbonden servers opnieuw om de bezorging van berichten af te dwingen. Het maakt gebruik van extra data."; +/* No comment provided by engineer. */ +"Reconnect all servers" = "Maak opnieuw verbinding met alle servers"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "Alle servers opnieuw verbinden?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "Maak opnieuw verbinding met de server om de bezorging van berichten te forceren. Er wordt gebruik gemaakt van extra verkeer.Maak opnieuw verbinding met de server om de bezorging van berichten te forceren. Er wordt gebruik gemaakt van extra data."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "Server opnieuw verbinden?"; + /* No comment provided by engineer. */ "Reconnect servers?" = "Servers opnieuw verbinden?"; @@ -3110,7 +3567,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Verminderd batterijgebruik"; -/* reject incoming call via notification */ +/* reject incoming call via notification + swipe action */ "Reject" = "Afwijzen"; /* No comment provided by engineer. */ @@ -3131,6 +3589,9 @@ /* No comment provided by engineer. */ "Remove" = "Verwijderen"; +/* No comment provided by engineer. */ +"Remove image" = "Verwijder afbeelding"; + /* No comment provided by engineer. */ "Remove member" = "Lid verwijderen"; @@ -3188,12 +3649,27 @@ /* No comment provided by engineer. */ "Reset" = "Resetten"; +/* No comment provided by engineer. */ +"Reset all hints" = "Alle hints resetten"; + +/* No comment provided by engineer. */ +"Reset all statistics" = "Reset alle statistieken"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "Alle statistieken resetten?"; + /* No comment provided by engineer. */ "Reset colors" = "Kleuren resetten"; +/* No comment provided by engineer. */ +"Reset to app theme" = "Terugzetten naar app thema"; + /* No comment provided by engineer. */ "Reset to defaults" = "Resetten naar standaardwaarden"; +/* No comment provided by engineer. */ +"Reset to user theme" = "Terugzetten naar gebruikersthema"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Start de app opnieuw om een nieuw chat profiel aan te maken"; @@ -3218,9 +3694,6 @@ /* chat item action */ "Reveal" = "Onthullen"; -/* No comment provided by engineer. */ -"Revert" = "Terugdraaien"; - /* No comment provided by engineer. */ "Revoke" = "Intrekken"; @@ -3236,6 +3709,9 @@ /* No comment provided by engineer. */ "Run chat" = "Chat uitvoeren"; +/* No comment provided by engineer. */ +"Safely receive files" = "Veilig bestanden ontvangen"; + /* No comment provided by engineer. */ "Safer groups" = "Veiligere groepen"; @@ -3251,6 +3727,9 @@ /* No comment provided by engineer. */ "Save and notify group members" = "Opslaan en groep leden melden"; +/* No comment provided by engineer. */ +"Save and reconnect" = "Opslaan en opnieuw verbinden"; + /* No comment provided by engineer. */ "Save and update group profile" = "Groep profiel opslaan en bijwerken"; @@ -3285,7 +3764,7 @@ "Save settings?" = "Instellingen opslaan?"; /* No comment provided by engineer. */ -"Save welcome message?" = "Welkomst bericht opslaan?"; +"Save welcome message?" = "Welkom bericht opslaan?"; /* No comment provided by engineer. */ "saved" = "opgeslagen"; @@ -3305,6 +3784,12 @@ /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "Opgeslagen WebRTC ICE servers worden verwijderd"; +/* No comment provided by engineer. */ +"Scale" = "Schaal"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Link scannen/plakken"; + /* No comment provided by engineer. */ "Scan code" = "Code scannen"; @@ -3320,6 +3805,9 @@ /* No comment provided by engineer. */ "Scan server QR code" = "Scan server QR-code"; +/* No comment provided by engineer. */ +"search" = "zoekopdracht"; + /* No comment provided by engineer. */ "Search" = "Zoeken"; @@ -3332,6 +3820,9 @@ /* network option */ "sec" = "sec"; +/* No comment provided by engineer. */ +"Secondary" = "Secundair"; + /* time unit */ "seconds" = "seconden"; @@ -3341,6 +3832,9 @@ /* server test step */ "Secure queue" = "Veilige wachtrij"; +/* No comment provided by engineer. */ +"Secured" = "Beveiligd"; + /* No comment provided by engineer. */ "Security assessment" = "Beveiligingsbeoordeling"; @@ -3350,9 +3844,15 @@ /* chat item text */ "security code changed" = "beveiligingscode gewijzigd"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "Selecteer"; +/* No comment provided by engineer. */ +"Selected %lld" = "%lld geselecteerd"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Geselecteerde chat voorkeuren verbieden dit bericht."; + /* No comment provided by engineer. */ "Self-destruct" = "Zelfvernietiging"; @@ -3377,21 +3877,30 @@ /* No comment provided by engineer. */ "send direct message" = "stuur een direct bericht"; -/* No comment provided by engineer. */ -"Send direct message" = "Direct bericht sturen"; - /* No comment provided by engineer. */ "Send direct message to connect" = "Stuur een direct bericht om verbinding te maken"; /* No comment provided by engineer. */ "Send disappearing message" = "Stuur een verdwijnend bericht"; +/* No comment provided by engineer. */ +"Send errors" = "Verzend fouten"; + /* No comment provided by engineer. */ "Send link previews" = "Link voorbeelden verzenden"; /* No comment provided by engineer. */ "Send live message" = "Stuur een livebericht"; +/* No comment provided by engineer. */ +"Send message to enable calls." = "Stuur een bericht om oproepen mogelijk te maken."; + +/* No comment provided by engineer. */ +"Send messages directly when IP address is protected and your or destination server does not support private routing." = "Stuur berichten rechtstreeks als het IP-adres beschermd is en uw of bestemmingsserver geen privéroutering ondersteunt."; + +/* No comment provided by engineer. */ +"Send messages directly when your or destination server does not support private routing." = "Stuur berichten rechtstreeks wanneer uw of de doelserver geen privéroutering ondersteunt."; + /* No comment provided by engineer. */ "Send notifications" = "Meldingen verzenden"; @@ -3446,15 +3955,42 @@ /* copied message info */ "Sent at: %@" = "Verzonden op: %@"; +/* No comment provided by engineer. */ +"Sent directly" = "Direct verzonden"; + /* notification */ "Sent file event" = "Verzonden bestandsgebeurtenis"; /* message info title */ "Sent message" = "Verzonden bericht"; +/* No comment provided by engineer. */ +"Sent messages" = "Verzonden berichten"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "Verzonden berichten worden na ingestelde tijd verwijderd."; +/* No comment provided by engineer. */ +"Sent reply" = "Antwoord verzonden"; + +/* No comment provided by engineer. */ +"Sent total" = "Totaal verzonden"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Verzonden via proxy"; + +/* No comment provided by engineer. */ +"Server address" = "Server adres"; + +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "Serveradres is incompatibel met netwerkinstellingen: %@."; + +/* srv error text. */ +"Server address is incompatible with network settings." = "Serveradres is niet compatibel met netwerkinstellingen."; + +/* queue info */ +"server queue info: %@\n\nlast received msg: %@" = "informatie over serverwachtrij: %1$@\n\nlaatst ontvangen bericht: %2$@"; + /* server test error */ "Server requires authorization to create queues, check password" = "Server vereist autorisatie om wachtrijen te maken, controleer wachtwoord"; @@ -3464,9 +4000,24 @@ /* No comment provided by engineer. */ "Server test failed!" = "Servertest mislukt!"; +/* No comment provided by engineer. */ +"Server type" = "Server type"; + +/* srv error text */ +"Server version is incompatible with network settings." = "Serverversie is incompatibel met netwerkinstellingen."; + +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "Serverversie is incompatibel met uw app: %@."; + /* No comment provided by engineer. */ "Servers" = "Servers"; +/* No comment provided by engineer. */ +"Servers info" = "Server informatie"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "Serverstatistieken worden gereset - dit kan niet ongedaan worden gemaakt!"; + /* No comment provided by engineer. */ "Session code" = "Sessie code"; @@ -3476,6 +4027,9 @@ /* No comment provided by engineer. */ "Set contact name…" = "Contactnaam instellen…"; +/* No comment provided by engineer. */ +"Set default theme" = "Stel het standaard thema in"; + /* No comment provided by engineer. */ "Set group preferences" = "Groep voorkeuren instellen"; @@ -3521,15 +4075,24 @@ /* No comment provided by engineer. */ "Share address with contacts?" = "Adres delen met contacten?"; +/* No comment provided by engineer. */ +"Share from other apps." = "Delen vanuit andere apps."; + /* No comment provided by engineer. */ "Share link" = "Deel link"; /* No comment provided by engineer. */ "Share this 1-time invite link" = "Deel deze eenmalige uitnodigingslink"; +/* No comment provided by engineer. */ +"Share to SimpleX" = "Delen op SimpleX"; + /* No comment provided by engineer. */ "Share with contacts" = "Delen met contacten"; +/* No comment provided by engineer. */ +"Show → on messages sent via private routing." = "Toon → bij berichten verzonden via privéroutering."; + /* No comment provided by engineer. */ "Show calls in phone history" = "Toon oproepen in de telefoongeschiedenis"; @@ -3539,6 +4102,12 @@ /* No comment provided by engineer. */ "Show last messages" = "Laat laatste berichten zien"; +/* No comment provided by engineer. */ +"Show message status" = "Toon berichtstatus"; + +/* No comment provided by engineer. */ +"Show percentage" = "Percentage weergeven"; + /* No comment provided by engineer. */ "Show preview" = "Toon voorbeeld"; @@ -3548,6 +4117,9 @@ /* No comment provided by engineer. */ "Show:" = "Toon:"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "SimpleX adres"; @@ -3593,6 +4165,9 @@ /* No comment provided by engineer. */ "Simplified incognito mode" = "Vereenvoudigde incognitomodus"; +/* No comment provided by engineer. */ +"Size" = "Maat"; + /* No comment provided by engineer. */ "Skip" = "Overslaan"; @@ -3603,11 +4178,20 @@ "Small groups (max 20)" = "Kleine groepen (max 20)"; /* No comment provided by engineer. */ -"SMP servers" = "SMP servers"; +"SMP server" = "SMP server"; + +/* blur media */ +"Soft" = "Soft"; + +/* No comment provided by engineer. */ +"Some file(s) were not exported:" = "Sommige bestanden zijn niet geëxporteerd:"; /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "Er zijn enkele niet-fatale fouten opgetreden tijdens het importeren - u kunt de Chat console raadplegen voor meer details."; +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import:" = "Er zijn enkele niet-fatale fouten opgetreden tijdens het importeren:"; + /* notification title */ "Somebody" = "Iemand"; @@ -3626,9 +4210,15 @@ /* No comment provided by engineer. */ "Start migration" = "Start migratie"; +/* No comment provided by engineer. */ +"Starting from %@." = "Beginnend vanaf %@."; + /* No comment provided by engineer. */ "starting…" = "beginnen…"; +/* No comment provided by engineer. */ +"Statistics" = "Statistieken"; + /* No comment provided by engineer. */ "Stop" = "Stop"; @@ -3668,9 +4258,21 @@ /* No comment provided by engineer. */ "strike" = "staking"; +/* blur media */ +"Strong" = "Krachtig"; + /* No comment provided by engineer. */ "Submit" = "Indienen"; +/* No comment provided by engineer. */ +"Subscribed" = "Ingeschreven"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Inschrijving fouten"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Inschrijvingen genegeerd"; + /* No comment provided by engineer. */ "Support SimpleX Chat" = "Ondersteuning van SimpleX Chat"; @@ -3687,25 +4289,25 @@ "Tap button " = "Tik op de knop "; /* No comment provided by engineer. */ -"Tap to activate profile." = "Tik om profiel te activeren."; +"Tap to activate profile." = "Tik hier om profiel te activeren."; /* No comment provided by engineer. */ -"Tap to Connect" = "Tik om verbinding te maken"; +"Tap to Connect" = "Tik hier om verbinding te maken"; /* No comment provided by engineer. */ -"Tap to join" = "Tik om lid te worden"; +"Tap to join" = "Tik hier om lid te worden"; /* No comment provided by engineer. */ -"Tap to join incognito" = "Tik om incognito lid te worden"; +"Tap to join incognito" = "Tik hier om incognito lid te worden"; /* No comment provided by engineer. */ -"Tap to paste link" = "Tik om de link te plakken"; +"Tap to paste link" = "Tik hier om de link te plakken"; /* No comment provided by engineer. */ -"Tap to scan" = "Tik om te scannen"; +"Tap to scan" = "Tik hier om te scannen"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Tik om een nieuw gesprek te starten"; +"TCP connection" = "TCP verbinding"; /* No comment provided by engineer. */ "TCP connection timeout" = "Timeout van TCP-verbinding"; @@ -3719,6 +4321,9 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* No comment provided by engineer. */ +"Temporary file error" = "Tijdelijke bestandsfout"; + /* server test failure */ "Test failed at step %@." = "Test mislukt bij stap %@."; @@ -3746,6 +4351,9 @@ /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "De app kan u op de hoogte stellen wanneer u berichten of contact verzoeken ontvangt - open de instellingen om dit in te schakelen."; +/* No comment provided by engineer. */ +"The app will ask to confirm downloads from unknown file servers (except .onion)." = "De app vraagt om downloads van onbekende bestandsservers (behalve .onion) te bevestigen."; + /* No comment provided by engineer. */ "The attempt to change database passphrase was not completed." = "De poging om het wachtwoord van de database te wijzigen is niet voltooid."; @@ -3776,6 +4384,12 @@ /* No comment provided by engineer. */ "The message will be marked as moderated for all members." = "Het bericht wordt gemarkeerd als gemodereerd voor alle leden."; +/* No comment provided by engineer. */ +"The messages will be deleted for all members." = "De berichten worden voor alle leden verwijderd."; + +/* No comment provided by engineer. */ +"The messages will be marked as moderated for all members." = "De berichten worden voor alle leden als gemodereerd gemarkeerd."; + /* No comment provided by engineer. */ "The next generation of private messaging" = "De volgende generatie privéberichten"; @@ -3798,7 +4412,7 @@ "The text you pasted is not a SimpleX link." = "De tekst die u hebt geplakt is geen SimpleX link."; /* No comment provided by engineer. */ -"Theme" = "Thema"; +"Themes" = "Thema's"; /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Deze instellingen zijn voor uw huidige profiel **%@**."; @@ -3842,9 +4456,15 @@ /* No comment provided by engineer. */ "This is your own SimpleX address!" = "Dit is uw eigen SimpleX adres!"; +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "Deze link is gebruikt met een ander mobiel apparaat. Maak een nieuwe link op de desktop."; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Deze instelling is van toepassing op berichten in je huidige chat profiel **%@**."; +/* No comment provided by engineer. */ +"Title" = "Titel"; + /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Om vragen te stellen en updates te ontvangen:"; @@ -3866,6 +4486,9 @@ /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Schakel SimpleX Vergrendelen om uw informatie te beschermen.\nU wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingeschakeld."; +/* No comment provided by engineer. */ +"To protect your IP address, private routing uses your SMP servers to deliver messages." = "Om uw IP-adres te beschermen, gebruikt privéroutering uw SMP-servers om berichten te bezorgen."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Geef toestemming om de microfoon te gebruiken om een spraakbericht op te nemen."; @@ -3878,12 +4501,24 @@ /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Vergelijk (of scan) de code op uw apparaten om end-to-end-codering met uw contact te verifiëren."; +/* No comment provided by engineer. */ +"Toggle chat list:" = "Chatlijst wisselen:"; + /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Schakel incognito in tijdens het verbinden."; +/* No comment provided by engineer. */ +"Toolbar opacity" = "De transparantie van de werkbalk"; + +/* No comment provided by engineer. */ +"Total" = "Totaal"; + /* No comment provided by engineer. */ "Transport isolation" = "Transport isolation"; +/* No comment provided by engineer. */ +"Transport sessions" = "Transportsessies"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Proberen verbinding te maken met de server die wordt gebruikt om berichten van dit contact te ontvangen (fout: %@)."; @@ -3920,13 +4555,10 @@ /* rcv group event chat item */ "unblocked %@" = "gedeblokkeerd %@"; -/* item status description */ -"Unexpected error: %@" = "Onverwachte fout: %@"; - /* No comment provided by engineer. */ "Unexpected migration state" = "Onverwachte migratiestatus"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "Niet fav."; /* No comment provided by engineer. */ @@ -3953,6 +4585,12 @@ /* No comment provided by engineer. */ "Unknown error" = "Onbekende fout"; +/* No comment provided by engineer. */ +"unknown servers" = "onbekende relays"; + +/* No comment provided by engineer. */ +"Unknown servers!" = "Onbekende servers!"; + /* No comment provided by engineer. */ "unknown status" = "onbekende status"; @@ -3975,9 +4613,15 @@ "Unlock app" = "Ontgrendel app"; /* No comment provided by engineer. */ +"unmute" = "dempen opheffen"; + +/* swipe action */ "Unmute" = "Dempen opheffen"; /* No comment provided by engineer. */ +"unprotected" = "onbeschermd"; + +/* swipe action */ "Unread" = "Ongelezen"; /* No comment provided by engineer. */ @@ -3986,9 +4630,6 @@ /* No comment provided by engineer. */ "Update" = "Update"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = ".onion hosts-instelling updaten?"; - /* No comment provided by engineer. */ "Update database passphrase" = "Database wachtwoord bijwerken"; @@ -3996,7 +4637,7 @@ "Update network settings?" = "Netwerk instellingen bijwerken?"; /* No comment provided by engineer. */ -"Update transport isolation mode?" = "Transportisolatiemodus updaten?"; +"Update settings?" = "Instellingen actualiseren?"; /* rcv group event chat item */ "updated group profile" = "bijgewerkt groep profiel"; @@ -4008,10 +4649,10 @@ "Updating settings will re-connect the client to all servers." = "Door de instellingen bij te werken, wordt de client opnieuw verbonden met alle servers."; /* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "Als u deze instelling bijwerkt, wordt de client opnieuw verbonden met alle servers."; +"Upgrade and open chat" = "Upgrade en open chat"; /* No comment provided by engineer. */ -"Upgrade and open chat" = "Upgrade en open chat"; +"Upload errors" = "Upload fouten"; /* No comment provided by engineer. */ "Upload failed" = "Upload mislukt"; @@ -4019,6 +4660,12 @@ /* server test step */ "Upload file" = "Upload bestand"; +/* No comment provided by engineer. */ +"Uploaded" = "Geüpload"; + +/* No comment provided by engineer. */ +"Uploaded files" = "Geüploade bestanden"; + /* No comment provided by engineer. */ "Uploading archive" = "Archief uploaden"; @@ -4046,6 +4693,12 @@ /* No comment provided by engineer. */ "Use only local notifications?" = "Alleen lokale meldingen gebruiken?"; +/* No comment provided by engineer. */ +"Use private routing with unknown servers when IP address is not protected." = "Gebruik privéroutering met onbekende servers wanneer het IP-adres niet beveiligd is."; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers." = "Gebruik privéroutering met onbekende servers."; + /* No comment provided by engineer. */ "Use server" = "Gebruik server"; @@ -4055,11 +4708,14 @@ /* No comment provided by engineer. */ "Use the app while in the call." = "Gebruik de app tijdens het gesprek."; +/* No comment provided by engineer. */ +"Use the app with one hand." = "Gebruik de app met één hand."; + /* No comment provided by engineer. */ "User profile" = "Gebruikers profiel"; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "Het gebruik van .onion-hosts vereist een compatibele VPN-provider."; +"User selection" = "Gebruikersselectie"; /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "SimpleX Chat servers gebruiken."; @@ -4109,6 +4765,9 @@ /* No comment provided by engineer. */ "Via secure quantum resistant protocol." = "Via een beveiligd kwantumbestendig protocol."; +/* No comment provided by engineer. */ +"video" = "video"; + /* No comment provided by engineer. */ "Video call" = "video oproep"; @@ -4166,6 +4825,12 @@ /* No comment provided by engineer. */ "Waiting for video" = "Wachten op video"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "Achtergrond accent"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Wallpaper achtergrond"; + /* No comment provided by engineer. */ "wants to connect to you!" = "wil met je in contact komen!"; @@ -4185,10 +4850,10 @@ "Welcome %@!" = "Welkom %@!"; /* No comment provided by engineer. */ -"Welcome message" = "Welkomst bericht"; +"Welcome message" = "Welkom bericht"; /* No comment provided by engineer. */ -"Welcome message is too long" = "Welkomstbericht is te lang"; +"Welcome message is too long" = "Welkom bericht is te lang"; /* No comment provided by engineer. */ "What's new" = "Wat is er nieuw"; @@ -4199,6 +4864,9 @@ /* No comment provided by engineer. */ "When connecting audio and video calls." = "Bij het verbinden van audio- en video-oproepen."; +/* No comment provided by engineer. */ +"when IP hidden" = "wanneer IP verborgen is"; + /* No comment provided by engineer. */ "When people request to connect, you can accept or reject it." = "Wanneer mensen vragen om verbinding te maken, kunt u dit accepteren of weigeren."; @@ -4218,19 +4886,31 @@ "With encrypted files and media." = "‐Met versleutelde bestanden en media."; /* No comment provided by engineer. */ -"With optional welcome message." = "Met optioneel welkomst bericht."; +"With optional welcome message." = "Met optioneel welkom bericht."; /* No comment provided by engineer. */ "With reduced battery usage." = "Met verminderd batterijgebruik."; +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to file servers." = "Zonder Tor of VPN is uw IP-adres zichtbaar voor bestandsservers."; + +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Zonder Tor of VPN zal uw IP-adres zichtbaar zijn voor deze XFTP-relays: %@."; + /* No comment provided by engineer. */ "Wrong database passphrase" = "Verkeerd wachtwoord voor de database"; +/* snd error text */ +"Wrong key or unknown connection - most likely this connection is deleted." = "Verkeerde sleutel of onbekende verbinding - hoogstwaarschijnlijk is deze verbinding verwijderd."; + +/* file error text */ +"Wrong key or unknown file chunk address - most likely file is deleted." = "Verkeerde sleutel of onbekend bestanddeeladres - hoogstwaarschijnlijk is het bestand verwijderd."; + /* No comment provided by engineer. */ "Wrong passphrase!" = "Verkeerd wachtwoord!"; /* No comment provided by engineer. */ -"XFTP servers" = "XFTP servers"; +"XFTP server" = "XFTP server"; /* pref value */ "yes" = "Ja"; @@ -4286,6 +4966,9 @@ /* No comment provided by engineer. */ "You are invited to group" = "Je bent uitgenodigd voor de groep"; +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "U bent niet verbonden met deze servers. Privéroutering wordt gebruikt om berichten bij hen af te leveren."; + /* No comment provided by engineer. */ "you are observer" = "jij bent waarnemer"; @@ -4295,6 +4978,9 @@ /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "U kunt oproepen van het vergrendelingsscherm accepteren, zonder apparaat- en app-verificatie."; +/* No comment provided by engineer. */ +"You can change it in Appearance settings." = "U kunt dit wijzigen in de instellingen onder uiterlijk."; + /* No comment provided by engineer. */ "You can create it later" = "U kan het later maken"; @@ -4314,7 +5000,10 @@ "You can make it visible to your SimpleX contacts via Settings." = "Je kunt het via Instellingen zichtbaar maken voor je SimpleX contacten."; /* notification body */ -"You can now send messages to %@" = "Je kunt nu berichten sturen naar %@"; +"You can now chat with %@" = "Je kunt nu berichten sturen naar %@"; + +/* No comment provided by engineer. */ +"You can send messages to %@ from Archived contacts." = "U kunt berichten naar %@ sturen vanuit gearchiveerde contacten."; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "U kunt een voorbeeld van een melding op het vergrendeld scherm instellen via instellingen."; @@ -4331,6 +5020,9 @@ /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "U kunt de chat starten via app Instellingen / Database of door de app opnieuw op te starten"; +/* No comment provided by engineer. */ +"You can still view conversation with %@ in the list of chats." = "Je kunt het gesprek met %@ nog steeds bekijken in de lijst met chats."; + /* No comment provided by engineer. */ "You can turn on SimpleX Lock via Settings." = "Je kunt SimpleX Vergrendeling aanzetten via Instellingen."; @@ -4367,9 +5059,6 @@ /* No comment provided by engineer. */ "You have already requested connection!\nRepeat connection request?" = "Je hebt al verbinding aangevraagd!\nVerbindingsverzoek herhalen?"; -/* No comment provided by engineer. */ -"You have no chats" = "Je hebt geen gesprekken"; - /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "U moet elke keer dat de app start het wachtwoord invoeren, deze wordt niet op het apparaat opgeslagen."; @@ -4385,9 +5074,18 @@ /* snd group event chat item */ "you left" = "jij bent vertrokken"; +/* No comment provided by engineer. */ +"You may migrate the exported database." = "U kunt de geëxporteerde database migreren."; + +/* No comment provided by engineer. */ +"You may save the exported archive." = "U kunt het geëxporteerde archief opslaan."; + /* No comment provided by engineer. */ "You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." = "U mag ALLEEN de meest recente versie van uw chat database op één apparaat gebruiken, anders ontvangt u mogelijk geen berichten meer van sommige contacten."; +/* No comment provided by engineer. */ +"You need to allow your contact to call to be able to call them." = "U moet uw contactpersoon toestemming geven om te bellen, zodat hij/zij je kan bellen."; + /* No comment provided by engineer. */ "You need to allow your contact to send voice messages to be able to send them." = "U moet uw contact toestemming geven om spraak berichten te verzenden om ze te kunnen verzenden."; @@ -4460,9 +5158,6 @@ /* No comment provided by engineer. */ "Your chat profiles" = "Uw chat profielen"; -/* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Uw contact moet online zijn om de verbinding te voltooien.\nU kunt deze verbinding verbreken en het contact verwijderen en later proberen met een nieuwe link."; - /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Uw contact heeft een bestand verzonden dat groter is dan de momenteel ondersteunde maximale grootte (%@)."; diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index 15262085eb..179b2d3848 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -154,9 +154,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ jest zweryfikowany"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ serwery"; - /* No comment provided by engineer. */ "%@ uploaded" = "%@ wgrane"; @@ -338,10 +335,11 @@ "above, then choose:" = "powyżej, a następnie wybierz:"; /* No comment provided by engineer. */ -"Accent color" = "Kolor akcentu"; +"Accent" = "Akcent"; /* accept contact request via notification - accept incoming call via notification */ + accept incoming call via notification + swipe action */ "Accept" = "Akceptuj"; /* No comment provided by engineer. */ @@ -350,12 +348,22 @@ /* notification body */ "Accept contact request from %@?" = "Zaakceptuj prośbę o kontakt od %@?"; -/* accept contact request via notification */ +/* accept contact request via notification + swipe action */ "Accept incognito" = "Akceptuj incognito"; /* call status */ "accepted call" = "zaakceptowane połączenie"; +/* No comment provided by engineer. */ +"Acknowledged" = "Potwierdzono"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Błędy potwierdzenia"; + +/* No comment provided by engineer. */ +"Active connections" = "Aktywne połączenia"; + /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Dodaj adres do swojego profilu, aby Twoje kontakty mogły go udostępnić innym osobom. Aktualizacja profilu zostanie wysłana do Twoich kontaktów."; @@ -369,7 +377,7 @@ "Add profile" = "Dodaj profil"; /* No comment provided by engineer. */ -"Add server…" = "Dodaj serwer…"; +"Add server" = "Dodaj serwer"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Dodaj serwery, skanując kody QR."; @@ -380,6 +388,15 @@ /* No comment provided by engineer. */ "Add welcome message" = "Dodaj wiadomość powitalną"; +/* No comment provided by engineer. */ +"Additional accent" = "Dodatkowy akcent"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Dodatkowy akcent 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "Dodatkowy drugorzędny"; + /* No comment provided by engineer. */ "Address" = "Adres"; @@ -401,6 +418,9 @@ /* No comment provided by engineer. */ "Advanced network settings" = "Zaawansowane ustawienia sieci"; +/* No comment provided by engineer. */ +"Advanced settings" = "Zaawansowane ustawienia"; + /* chat item text */ "agreeing encryption for %@…" = "uzgadnianie szyfrowania dla %@…"; @@ -416,6 +436,9 @@ /* No comment provided by engineer. */ "All data is erased when it is entered." = "Wszystkie dane są usuwane po jego wprowadzeniu."; +/* No comment provided by engineer. */ +"All data is private to your device." = "Wszystkie dane są prywatne na Twoim urządzeniu."; + /* No comment provided by engineer. */ "All group members will remain connected." = "Wszyscy członkowie grupy pozostaną połączeni."; @@ -431,6 +454,9 @@ /* No comment provided by engineer. */ "All new messages from %@ will be hidden!" = "Wszystkie nowe wiadomości z %@ zostaną ukryte!"; +/* No comment provided by engineer. */ +"All profiles" = "Wszystkie profile"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Wszystkie Twoje kontakty pozostaną połączone."; @@ -446,9 +472,15 @@ /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "Zezwalaj na połączenia tylko wtedy, gdy Twój kontakt na to pozwala."; +/* No comment provided by engineer. */ +"Allow calls?" = "Zezwolić na połączenia?"; + /* No comment provided by engineer. */ "Allow disappearing messages only if your contact allows it to you." = "Zezwól na znikające wiadomości tylko wtedy, gdy Twój kontakt Ci na to pozwoli."; +/* No comment provided by engineer. */ +"Allow downgrade" = "Zezwól na obniżenie wersji"; + /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Zezwalaj na nieodwracalne usuwanie wiadomości tylko wtedy, gdy Twój kontakt Ci na to pozwoli. (24 godziny)"; @@ -464,6 +496,9 @@ /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Zezwól na wysyłanie znikających wiadomości."; +/* No comment provided by engineer. */ +"Allow sharing" = "Zezwól na udostępnianie"; + /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Zezwól na nieodwracalne usunięcie wysłanych wiadomości. (24 godziny)"; @@ -509,6 +544,9 @@ /* pref value */ "always" = "zawsze"; +/* No comment provided by engineer. */ +"Always use private routing." = "Zawsze używaj prywatnego trasowania."; + /* No comment provided by engineer. */ "Always use relay" = "Zawsze używaj przekaźnika"; @@ -551,15 +589,27 @@ /* No comment provided by engineer. */ "Apply" = "Zastosuj"; +/* No comment provided by engineer. */ +"Apply to" = "Zastosuj dla"; + /* No comment provided by engineer. */ "Archive and upload" = "Archiwizuj i prześlij"; +/* No comment provided by engineer. */ +"Archive contacts to chat later." = "Archiwizuj kontakty aby porozmawiać później."; + +/* No comment provided by engineer. */ +"Archived contacts" = "Zarchiwizowane kontakty"; + /* No comment provided by engineer. */ "Archiving database" = "Archiwizowanie bazy danych"; /* No comment provided by engineer. */ "Attach" = "Dołącz"; +/* No comment provided by engineer. */ +"attempts" = "próby"; + /* No comment provided by engineer. */ "Audio & video calls" = "Połączenia audio i wideo"; @@ -602,6 +652,9 @@ /* No comment provided by engineer. */ "Back" = "Wstecz"; +/* No comment provided by engineer. */ +"Background" = "Tło"; + /* No comment provided by engineer. */ "Bad desktop address" = "Zły adres komputera"; @@ -623,6 +676,12 @@ /* No comment provided by engineer. */ "Better messages" = "Lepsze wiadomości"; +/* No comment provided by engineer. */ +"Better networking" = "Lepsze sieciowanie"; + +/* No comment provided by engineer. */ +"Black" = "Czarny"; + /* No comment provided by engineer. */ "Block" = "Zablokuj"; @@ -653,6 +712,12 @@ /* No comment provided by engineer. */ "Blocked by admin" = "Zablokowany przez admina"; +/* No comment provided by engineer. */ +"Blur for better privacy." = "Rozmycie dla lepszej prywatności."; + +/* No comment provided by engineer. */ +"Blur media" = "Rozmycie mediów"; + /* No comment provided by engineer. */ "bold" = "pogrubiona"; @@ -677,6 +742,9 @@ /* No comment provided by engineer. */ "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Według profilu czatu (domyślnie) lub [według połączenia](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; +/* No comment provided by engineer. */ +"call" = "zadzwoń"; + /* No comment provided by engineer. */ "Call already ended!" = "Połączenie już zakończone!"; @@ -692,15 +760,27 @@ /* No comment provided by engineer. */ "Calls" = "Połączenia"; +/* No comment provided by engineer. */ +"Calls prohibited!" = "Połączenia zakazane!"; + /* No comment provided by engineer. */ "Camera not available" = "Kamera nie dostępna"; +/* No comment provided by engineer. */ +"Can't call contact" = "Nie można zadzwonić do kontaktu"; + +/* No comment provided by engineer. */ +"Can't call member" = "Nie można zadzwonić do członka"; + /* No comment provided by engineer. */ "Can't invite contact!" = "Nie można zaprosić kontaktu!"; /* No comment provided by engineer. */ "Can't invite contacts!" = "Nie można zaprosić kontaktów!"; +/* No comment provided by engineer. */ +"Can't message member" = "Nie można wysłać wiadomości do członka"; + /* No comment provided by engineer. */ "Cancel" = "Anuluj"; @@ -713,9 +793,15 @@ /* No comment provided by engineer. */ "Cannot access keychain to save database password" = "Nie można uzyskać dostępu do pęku kluczy, aby zapisać hasło do bazy danych"; +/* No comment provided by engineer. */ +"Cannot forward message" = "Nie można przekazać wiadomości"; + /* No comment provided by engineer. */ "Cannot receive file" = "Nie można odebrać pliku"; +/* snd error text */ +"Capacity exceeded - recipient did not receive previously sent messages." = "Przekroczono pojemność - odbiorca nie otrzymał wcześniej wysłanych wiadomości."; + /* No comment provided by engineer. */ "Cellular" = "Sieć komórkowa"; @@ -768,6 +854,9 @@ /* No comment provided by engineer. */ "Chat archive" = "Archiwum czatu"; +/* No comment provided by engineer. */ +"Chat colors" = "Kolory czatu"; + /* No comment provided by engineer. */ "Chat console" = "Konsola czatu"; @@ -777,6 +866,9 @@ /* No comment provided by engineer. */ "Chat database deleted" = "Baza danych czatu usunięta"; +/* No comment provided by engineer. */ +"Chat database exported" = "Wyeksportowano bazę danych czatu"; + /* No comment provided by engineer. */ "Chat database imported" = "Zaimportowano bazę danych czatu"; @@ -789,12 +881,18 @@ /* No comment provided by engineer. */ "Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." = "Czat został zatrzymany. Jeśli korzystałeś już z tej bazy danych na innym urządzeniu, powinieneś przenieść ją z powrotem przed rozpoczęciem czatu."; +/* No comment provided by engineer. */ +"Chat list" = "Lista czatów"; + /* No comment provided by engineer. */ "Chat migrated!" = "Czat zmigrowany!"; /* No comment provided by engineer. */ "Chat preferences" = "Preferencje czatu"; +/* No comment provided by engineer. */ +"Chat theme" = "Motyw czatu"; + /* No comment provided by engineer. */ "Chats" = "Czaty"; @@ -814,6 +912,15 @@ "Choose from library" = "Wybierz z biblioteki"; /* No comment provided by engineer. */ +"Chunks deleted" = "Fragmenty usunięte"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Fragmenty pobrane"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Fragmenty przesłane"; + +/* swipe action */ "Clear" = "Wyczyść"; /* No comment provided by engineer. */ @@ -829,10 +936,13 @@ "Clear verification" = "Wyczyść weryfikację"; /* No comment provided by engineer. */ -"colored" = "kolorowy"; +"Color chats with the new themes." = "Koloruj czaty z nowymi motywami."; /* No comment provided by engineer. */ -"Colors" = "Kolory"; +"Color mode" = "Tryb koloru"; + +/* No comment provided by engineer. */ +"colored" = "kolorowy"; /* server test step */ "Compare file" = "Porównaj plik"; @@ -843,15 +953,27 @@ /* No comment provided by engineer. */ "complete" = "kompletny"; +/* No comment provided by engineer. */ +"Completed" = "Zakończono"; + /* No comment provided by engineer. */ "Configure ICE servers" = "Skonfiguruj serwery ICE"; +/* No comment provided by engineer. */ +"Configured %@ servers" = "Skonfigurowano %@ serwerów"; + /* No comment provided by engineer. */ "Confirm" = "Potwierdź"; +/* No comment provided by engineer. */ +"Confirm contact deletion?" = "Potwierdzić usunięcie kontaktu?"; + /* No comment provided by engineer. */ "Confirm database upgrades" = "Potwierdź aktualizacje bazy danych"; +/* No comment provided by engineer. */ +"Confirm files from unknown servers." = "Potwierdzaj pliki z nieznanych serwerów."; + /* No comment provided by engineer. */ "Confirm network settings" = "Potwierdź ustawienia sieciowe"; @@ -885,6 +1007,9 @@ /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "połącz się z deweloperami SimpleX Chat."; +/* No comment provided by engineer. */ +"Connect to your friends faster." = "Szybciej łącz się ze znajomymi."; + /* No comment provided by engineer. */ "Connect to yourself?" = "Połączyć się ze sobą?"; @@ -909,18 +1034,27 @@ /* No comment provided by engineer. */ "connected" = "połączony"; +/* No comment provided by engineer. */ +"Connected" = "Połączony"; + /* No comment provided by engineer. */ "Connected desktop" = "Połączony komputer"; /* rcv group event chat item */ "connected directly" = "połącz bezpośrednio"; +/* No comment provided by engineer. */ +"Connected servers" = "Połączone serwery"; + /* No comment provided by engineer. */ "Connected to desktop" = "Połączony do komputera"; /* No comment provided by engineer. */ "connecting" = "łączenie"; +/* No comment provided by engineer. */ +"Connecting" = "Łączenie"; + /* No comment provided by engineer. */ "connecting (accepted)" = "łączenie (zaakceptowane)"; @@ -942,6 +1076,9 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "Łączenie z serwerem... (błąd: %@)"; +/* No comment provided by engineer. */ +"Connecting to contact, please wait or check later!" = "Łączenie z kontaktem, poczekaj lub sprawdź później!"; + /* No comment provided by engineer. */ "Connecting to desktop" = "Łączenie z komputerem"; @@ -951,6 +1088,9 @@ /* No comment provided by engineer. */ "Connection" = "Połączenie"; +/* No comment provided by engineer. */ +"Connection and servers status." = "Stan połączenia i serwerów."; + /* No comment provided by engineer. */ "Connection error" = "Błąd połączenia"; @@ -960,6 +1100,9 @@ /* chat list item title (it should not be shown */ "connection established" = "połączenie ustanowione"; +/* No comment provided by engineer. */ +"Connection notifications" = "Powiadomienia o połączeniu"; + /* No comment provided by engineer. */ "Connection request sent!" = "Prośba o połączenie wysłana!"; @@ -969,9 +1112,15 @@ /* No comment provided by engineer. */ "Connection timeout" = "Czas połączenia minął"; +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "Połączenie z komputerem zakończone"; + /* connection information */ "connection:%@" = "połączenie: %@"; +/* No comment provided by engineer. */ +"Connections" = "Połączenia"; + /* profile update event chat item */ "contact %@ changed to %@" = "kontakt %1$@ zmieniony na %2$@"; @@ -981,6 +1130,9 @@ /* No comment provided by engineer. */ "Contact already exists" = "Kontakt już istnieje"; +/* No comment provided by engineer. */ +"Contact deleted!" = "Kontakt usunięty!"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "kontakt posiada szyfrowanie e2e"; @@ -994,7 +1146,7 @@ "Contact is connected" = "Kontakt jest połączony"; /* No comment provided by engineer. */ -"Contact is not connected yet!" = "Kontakt nie jest jeszcze połączony!"; +"Contact is deleted." = "Kontakt jest usunięty."; /* No comment provided by engineer. */ "Contact name" = "Nazwa kontaktu"; @@ -1002,6 +1154,9 @@ /* No comment provided by engineer. */ "Contact preferences" = "Preferencje kontaktu"; +/* No comment provided by engineer. */ +"Contact will be deleted - this cannot be undone!" = "Kontakt zostanie usunięty – nie można tego cofnąć!"; + /* No comment provided by engineer. */ "Contacts" = "Kontakty"; @@ -1011,9 +1166,15 @@ /* No comment provided by engineer. */ "Continue" = "Kontynuuj"; -/* chat item action */ +/* No comment provided by engineer. */ +"Conversation deleted!" = "Rozmowa usunięta!"; + +/* No comment provided by engineer. */ "Copy" = "Kopiuj"; +/* No comment provided by engineer. */ +"Copy error" = "Kopiuj błąd"; + /* No comment provided by engineer. */ "Core version: v%@" = "Wersja rdzenia: v%@"; @@ -1059,6 +1220,9 @@ /* No comment provided by engineer. */ "Create your profile" = "Utwórz swój profil"; +/* No comment provided by engineer. */ +"Created" = "Utworzono"; + /* No comment provided by engineer. */ "Created at" = "Utworzony o"; @@ -1083,6 +1247,9 @@ /* No comment provided by engineer. */ "Current passphrase…" = "Obecne hasło…"; +/* No comment provided by engineer. */ +"Current profile" = "Bieżący profil"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "Obecnie maksymalna obsługiwana wielkość pliku wynosi %@."; @@ -1092,9 +1259,15 @@ /* No comment provided by engineer. */ "Custom time" = "Niestandardowy czas"; +/* No comment provided by engineer. */ +"Customize theme" = "Dostosuj motyw"; + /* No comment provided by engineer. */ "Dark" = "Ciemny"; +/* No comment provided by engineer. */ +"Dark mode colors" = "Kolory ciemnego trybu"; + /* No comment provided by engineer. */ "Database downgrade" = "Obniż wersję bazy danych"; @@ -1155,12 +1328,18 @@ /* time unit */ "days" = "dni"; +/* No comment provided by engineer. */ +"Debug delivery" = "Dostarczenie debugowania"; + /* No comment provided by engineer. */ "Decentralized" = "Zdecentralizowane"; /* message decrypt error item */ "Decryption error" = "Błąd odszyfrowania"; +/* No comment provided by engineer. */ +"decryption errors" = "błąd odszyfrowywania"; + /* pref value */ "default (%@)" = "domyślne (%@)"; @@ -1170,9 +1349,13 @@ /* No comment provided by engineer. */ "default (yes)" = "domyślnie (tak)"; -/* chat item action */ +/* chat item action + swipe action */ "Delete" = "Usuń"; +/* No comment provided by engineer. */ +"Delete %lld messages of members?" = "Usunąć %lld wiadomości członków?"; + /* No comment provided by engineer. */ "Delete %lld messages?" = "Usunąć %lld wiadomości?"; @@ -1210,10 +1393,7 @@ "Delete contact" = "Usuń kontakt"; /* No comment provided by engineer. */ -"Delete Contact" = "Usuń Kontakt"; - -/* No comment provided by engineer. */ -"Delete contact?\nThis cannot be undone!" = "Usunąć kontakt?\nTo nie może być cofnięte!"; +"Delete contact?" = "Usunąć kontakt?"; /* No comment provided by engineer. */ "Delete database" = "Usuń bazę danych"; @@ -1269,9 +1449,6 @@ /* No comment provided by engineer. */ "Delete old database?" = "Usunąć starą bazę danych?"; -/* No comment provided by engineer. */ -"Delete pending connection" = "Usuń oczekujące połączenie"; - /* No comment provided by engineer. */ "Delete pending connection?" = "Usunąć oczekujące połączenie?"; @@ -1281,12 +1458,21 @@ /* server test step */ "Delete queue" = "Usuń kolejkę"; +/* No comment provided by engineer. */ +"Delete up to 20 messages at once." = "Usuń do 20 wiadomości na raz."; + /* No comment provided by engineer. */ "Delete user profile?" = "Usunąć profil użytkownika?"; +/* No comment provided by engineer. */ +"Delete without notification" = "Usuń bez powiadomienia"; + /* deleted chat item */ "deleted" = "usunięty"; +/* No comment provided by engineer. */ +"Deleted" = "Usunięto"; + /* No comment provided by engineer. */ "Deleted at" = "Usunięto o"; @@ -1299,6 +1485,9 @@ /* rcv group event chat item */ "deleted group" = "usunięta grupa"; +/* No comment provided by engineer. */ +"Deletion errors" = "Błędy usuwania"; + /* No comment provided by engineer. */ "Delivery" = "Dostarczenie"; @@ -1320,9 +1509,27 @@ /* No comment provided by engineer. */ "Desktop devices" = "Urządzenia komputerowe"; +/* No comment provided by engineer. */ +"Destination server address of %@ is incompatible with forwarding server %@ settings." = "Adres serwera docelowego %@ jest niekompatybilny z ustawieniami serwera przekazującego %@."; + +/* snd error text */ +"Destination server error: %@" = "Błąd docelowego serwera: %@"; + +/* No comment provided by engineer. */ +"Destination server version of %@ is incompatible with forwarding server %@." = "Wersja serwera docelowego %@ jest niekompatybilna z serwerem przekierowującym %@."; + +/* No comment provided by engineer. */ +"Detailed statistics" = "Szczegółowe statystyki"; + +/* No comment provided by engineer. */ +"Details" = "Szczegóły"; + /* No comment provided by engineer. */ "Develop" = "Deweloperskie"; +/* No comment provided by engineer. */ +"Developer options" = "Opcje deweloperskie"; + /* No comment provided by engineer. */ "Developer tools" = "Narzędzia deweloperskie"; @@ -1362,6 +1569,9 @@ /* No comment provided by engineer. */ "disabled" = "wyłączony"; +/* No comment provided by engineer. */ +"Disabled" = "Wyłączony"; + /* No comment provided by engineer. */ "Disappearing message" = "Znikająca wiadomość"; @@ -1398,6 +1608,12 @@ /* No comment provided by engineer. */ "Do not send history to new members." = "Nie wysyłaj historii do nowych członków."; +/* No comment provided by engineer. */ +"Do NOT send messages directly, even if your or destination server does not support private routing." = "NIE wysyłaj wiadomości bezpośrednio, nawet jeśli serwer docelowy nie obsługuje prywatnego trasowania."; + +/* No comment provided by engineer. */ +"Do NOT use private routing." = "NIE używaj prywatnego trasowania."; + /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "NIE używaj SimpleX do połączeń alarmowych."; @@ -1416,12 +1632,21 @@ /* chat item action */ "Download" = "Pobierz"; +/* No comment provided by engineer. */ +"Download errors" = "Błędy pobierania"; + /* No comment provided by engineer. */ "Download failed" = "Pobieranie nie udane"; /* server test step */ "Download file" = "Pobierz plik"; +/* No comment provided by engineer. */ +"Downloaded" = "Pobrane"; + +/* No comment provided by engineer. */ +"Downloaded files" = "Pobrane pliki"; + /* No comment provided by engineer. */ "Downloading archive" = "Pobieranie archiwum"; @@ -1434,6 +1659,9 @@ /* integrity error chat item */ "duplicate message" = "zduplikowana wiadomość"; +/* No comment provided by engineer. */ +"duplicates" = "duplikaty"; + /* No comment provided by engineer. */ "Duration" = "Czas trwania"; @@ -1491,6 +1719,9 @@ /* enabled status */ "enabled" = "włączone"; +/* No comment provided by engineer. */ +"Enabled" = "Włączony"; + /* No comment provided by engineer. */ "Enabled for" = "Włączony dla"; @@ -1632,6 +1863,9 @@ /* No comment provided by engineer. */ "Error changing setting" = "Błąd zmiany ustawienia"; +/* No comment provided by engineer. */ +"Error connecting to forwarding server %@. Please try later." = "Błąd połączenia z serwerem przekierowania %@. Spróbuj ponownie później."; + /* No comment provided by engineer. */ "Error creating address" = "Błąd tworzenia adresu"; @@ -1662,9 +1896,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "Błąd usuwania połączenia"; -/* No comment provided by engineer. */ -"Error deleting contact" = "Błąd usuwania kontaktu"; - /* No comment provided by engineer. */ "Error deleting database" = "Błąd usuwania bazy danych"; @@ -1692,6 +1923,9 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "Błąd eksportu bazy danych czatu"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Błąd eksportowania motywu: %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "Błąd importu bazy danych czatu"; @@ -1707,9 +1941,18 @@ /* No comment provided by engineer. */ "Error receiving file" = "Błąd odbioru pliku"; +/* No comment provided by engineer. */ +"Error reconnecting server" = "Błąd ponownego łączenia z serwerem"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "Błąd ponownego łączenia serwerów"; + /* No comment provided by engineer. */ "Error removing member" = "Błąd usuwania członka"; +/* No comment provided by engineer. */ +"Error resetting statistics" = "Błąd resetowania statystyk"; + /* No comment provided by engineer. */ "Error saving %@ servers" = "Błąd zapisu %@ serwerów"; @@ -1779,7 +2022,8 @@ /* No comment provided by engineer. */ "Error: " = "Błąd: "; -/* No comment provided by engineer. */ +/* file error text + snd error text */ "Error: %@" = "Błąd: %@"; /* No comment provided by engineer. */ @@ -1788,6 +2032,9 @@ /* No comment provided by engineer. */ "Error: URL is invalid" = "Błąd: URL jest nieprawidłowy"; +/* No comment provided by engineer. */ +"Errors" = "Błędy"; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Nawet po wyłączeniu w rozmowie."; @@ -1800,12 +2047,18 @@ /* chat item action */ "Expand" = "Rozszerz"; +/* No comment provided by engineer. */ +"expired" = "wygasły"; + /* No comment provided by engineer. */ "Export database" = "Eksportuj bazę danych"; /* No comment provided by engineer. */ "Export error:" = "Błąd eksportu:"; +/* No comment provided by engineer. */ +"Export theme" = "Eksportuj motyw"; + /* No comment provided by engineer. */ "Exported database archive." = "Wyeksportowane archiwum bazy danych."; @@ -1824,9 +2077,24 @@ /* No comment provided by engineer. */ "Faster joining and more reliable messages." = "Szybsze dołączenie i bardziej niezawodne wiadomości."; -/* No comment provided by engineer. */ +/* swipe action */ "Favorite" = "Ulubione"; +/* No comment provided by engineer. */ +"File error" = "Błąd pliku"; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "Nie odnaleziono pliku - najprawdopodobniej plik został usunięty lub anulowany."; + +/* file error text */ +"File server error: %@" = "Błąd serwera plików: %@"; + +/* No comment provided by engineer. */ +"File status" = "Status pliku"; + +/* copied message info */ +"File status: %@" = "Status pliku: %@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "Plik zostanie usunięty z serwerów."; @@ -1839,6 +2107,9 @@ /* No comment provided by engineer. */ "File: %@" = "Plik: %@"; +/* No comment provided by engineer. */ +"Files" = "Pliki"; + /* No comment provided by engineer. */ "Files & media" = "Pliki i media"; @@ -1905,6 +2176,21 @@ /* No comment provided by engineer. */ "Forwarded from" = "Przekazane dalej od"; +/* No comment provided by engineer. */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "Serwer przekazujący %@ nie mógł połączyć się z serwerem docelowym %@. Spróbuj ponownie później."; + +/* No comment provided by engineer. */ +"Forwarding server address is incompatible with network settings: %@." = "Adres serwera przekierowującego jest niekompatybilny z ustawieniami sieciowymi: %@."; + +/* No comment provided by engineer. */ +"Forwarding server version is incompatible with network settings: %@." = "Wersja serwera przekierowującego jest niekompatybilna z ustawieniami sieciowymi: %@."; + +/* snd error text */ +"Forwarding server: %@\nDestination server error: %@" = "Serwer przekazujący: %1$@\nBłąd serwera docelowego: %2$@"; + +/* snd error text */ +"Forwarding server: %@\nError: %@" = "Serwer przekazujący: %1$@\nBłąd: %2$@"; + /* No comment provided by engineer. */ "Found desktop" = "Znaleziono komputer"; @@ -1932,6 +2218,12 @@ /* No comment provided by engineer. */ "GIFs and stickers" = "GIF-y i naklejki"; +/* message preview */ +"Good afternoon!" = "Dzień dobry!"; + +/* message preview */ +"Good morning!" = "Dzień dobry!"; + /* No comment provided by engineer. */ "Group" = "Grupa"; @@ -2109,6 +2401,9 @@ /* No comment provided by engineer. */ "Import failed" = "Import nie udał się"; +/* No comment provided by engineer. */ +"Import theme" = "Importuj motyw"; + /* No comment provided by engineer. */ "Importing archive" = "Importowanie archiwum"; @@ -2130,6 +2425,9 @@ /* No comment provided by engineer. */ "In-call sounds" = "Dźwięki w rozmowie"; +/* No comment provided by engineer. */ +"inactive" = "nieaktywny"; + /* No comment provided by engineer. */ "Incognito" = "Incognito"; @@ -2193,6 +2491,9 @@ /* No comment provided by engineer. */ "Interface" = "Interfejs"; +/* No comment provided by engineer. */ +"Interface colors" = "Kolory interfejsu"; + /* invalid chat data */ "invalid chat" = "nieprawidłowy czat"; @@ -2235,6 +2536,9 @@ /* group name */ "invitation to group %@" = "zaproszenie do grupy %@"; +/* No comment provided by engineer. */ +"invite" = "zaproś"; + /* No comment provided by engineer. */ "Invite friends" = "Zaproś znajomych"; @@ -2280,6 +2584,9 @@ /* No comment provided by engineer. */ "It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Może to nastąpić, gdy:\n1. Wiadomości wygasły w wysyłającym kliencie po 2 dniach lub na serwerze po 30 dniach.\n2. Odszyfrowanie wiadomości nie powiodło się, ponieważ Ty lub Twój kontakt użyliście starej kopii zapasowej bazy danych.\n3. Połączenie zostało skompromitowane."; +/* No comment provided by engineer. */ +"It protects your IP address and connections." = "Chroni Twój adres IP i połączenia."; + /* No comment provided by engineer. */ "It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Wygląda na to, że jesteś już połączony przez ten link. Jeśli tak nie jest, wystąpił błąd (%@)."; @@ -2292,7 +2599,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Japoński interfejs"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Dołącz"; /* No comment provided by engineer. */ @@ -2322,6 +2629,9 @@ /* No comment provided by engineer. */ "Keep" = "Zachowaj"; +/* No comment provided by engineer. */ +"Keep conversation" = "Zachowaj rozmowę"; + /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Zostaw aplikację otwartą i używaj ją z komputera"; @@ -2343,7 +2653,7 @@ /* No comment provided by engineer. */ "Learn more" = "Dowiedz się więcej"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "Opuść"; /* No comment provided by engineer. */ @@ -2433,6 +2743,12 @@ /* No comment provided by engineer. */ "Max 30 seconds, received instantly." = "Maksymalnie 30 sekund, odbierane natychmiast."; +/* No comment provided by engineer. */ +"Media & file servers" = "Serwery mediów i plików"; + +/* blur media */ +"Medium" = "Średni"; + /* member role */ "member" = "członek"; @@ -2445,6 +2761,9 @@ /* rcv group event chat item */ "member connected" = "połączony"; +/* item status text */ +"Member inactive" = "Członek nieaktywny"; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "Rola członka grupy zostanie zmieniona na \"%@\". Wszyscy członkowie grupy zostaną powiadomieni."; @@ -2454,15 +2773,33 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Członek zostanie usunięty z grupy - nie można tego cofnąć!"; +/* No comment provided by engineer. */ +"Menus" = "Menu"; + +/* No comment provided by engineer. */ +"message" = "wiadomość"; + /* item status text */ "Message delivery error" = "Błąd dostarczenia wiadomości"; /* No comment provided by engineer. */ "Message delivery receipts!" = "Potwierdzenia dostarczenia wiadomości!"; +/* item status text */ +"Message delivery warning" = "Ostrzeżenie dostarczenia wiadomości"; + /* No comment provided by engineer. */ "Message draft" = "Wersja robocza wiadomości"; +/* item status text */ +"Message forwarded" = "Wiadomość przekazana"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "Wiadomość może zostać dostarczona później jeśli członek stanie się aktywny."; + +/* No comment provided by engineer. */ +"Message queue info" = "Informacje kolejki wiadomości"; + /* chat feature */ "Message reactions" = "Reakcje wiadomości"; @@ -2475,9 +2812,21 @@ /* notification */ "message received" = "wiadomość otrzymana"; +/* No comment provided by engineer. */ +"Message reception" = "Odebranie wiadomości"; + +/* No comment provided by engineer. */ +"Message servers" = "Serwery wiadomości"; + /* No comment provided by engineer. */ "Message source remains private." = "Źródło wiadomości pozostaje prywatne."; +/* No comment provided by engineer. */ +"Message status" = "Status wiadomości"; + +/* copied message info */ +"Message status: %@" = "Status wiadomości: %@"; + /* No comment provided by engineer. */ "Message text" = "Tekst wiadomości"; @@ -2493,6 +2842,12 @@ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "Wiadomości od %@ zostaną pokazane!"; +/* No comment provided by engineer. */ +"Messages received" = "Otrzymane wiadomości"; + +/* No comment provided by engineer. */ +"Messages sent" = "Wysłane wiadomości"; + /* No comment provided by engineer. */ "Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Wiadomości, pliki i połączenia są chronione przez **szyfrowanie end-to-end** z doskonałym utajnianiem z wyprzedzeniem i odzyskiem po złamaniu."; @@ -2568,19 +2923,19 @@ /* item status description */ "Most likely this connection is deleted." = "Najprawdopodobniej to połączenie jest usunięte."; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Najprawdopodobniej ten kontakt usunął połączenie z Tobą."; - /* No comment provided by engineer. */ "Multiple chat profiles" = "Wiele profili czatu"; /* No comment provided by engineer. */ +"mute" = "wycisz"; + +/* swipe action */ "Mute" = "Wycisz"; /* No comment provided by engineer. */ "Muted when inactive!" = "Wyciszony, gdy jest nieaktywny!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "Nazwa"; /* No comment provided by engineer. */ @@ -2589,6 +2944,9 @@ /* No comment provided by engineer. */ "Network connection" = "Połączenie z siecią"; +/* snd error text */ +"Network issues - message expired after many attempts to send it." = "Błąd sieciowy - wiadomość wygasła po wielu próbach wysłania jej."; + /* No comment provided by engineer. */ "Network management" = "Zarządzenie sieciowe"; @@ -2604,6 +2962,9 @@ /* No comment provided by engineer. */ "New chat" = "Nowy czat"; +/* No comment provided by engineer. */ +"New chat experience 🎉" = "Nowe możliwości czatu 🎉"; + /* notification */ "New contact request" = "Nowa prośba o kontakt"; @@ -2622,6 +2983,9 @@ /* No comment provided by engineer. */ "New in %@" = "Nowość w %@"; +/* No comment provided by engineer. */ +"New media options" = "Nowe opcje mediów"; + /* No comment provided by engineer. */ "New member role" = "Nowa rola członka"; @@ -2658,6 +3022,9 @@ /* No comment provided by engineer. */ "No device token!" = "Brak tokenu urządzenia!"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Brak bezpośredniego połączenia, wiadomość została przekazana przez administratora."; + /* No comment provided by engineer. */ "no e2e encryption" = "brak szyfrowania e2e"; @@ -2670,6 +3037,9 @@ /* No comment provided by engineer. */ "No history" = "Brak historii"; +/* No comment provided by engineer. */ +"No info, try to reload" = "Brak informacji, spróbuj przeładować"; + /* No comment provided by engineer. */ "No network connection" = "Brak połączenia z siecią"; @@ -2685,6 +3055,9 @@ /* No comment provided by engineer. */ "Not compatible!" = "Nie kompatybilny!"; +/* No comment provided by engineer. */ +"Nothing selected" = "Nic nie jest zaznaczone"; + /* No comment provided by engineer. */ "Notifications" = "Powiadomienia"; @@ -2702,7 +3075,7 @@ time to disappear */ "off" = "wyłączony"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Wyłączony"; /* feature offered item */ @@ -2730,10 +3103,10 @@ "One-time invitation link" = "Jednorazowy link zaproszenia"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "Hosty onion będą wymagane do połączenia. Wymaga włączenia VPN."; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Hosty onion będą wymagane do połączenia.\nWymaga włączenia VPN."; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "Hosty onion będą używane, gdy będą dostępne. Wymaga włączenia VPN."; +"Onion hosts will be used when available.\nRequires compatible VPN." = "Hosty onion będą używane, gdy będą dostępne.\nWymaga włączenia VPN."; /* No comment provided by engineer. */ "Onion hosts will not be used." = "Hosty onion nie będą używane."; @@ -2741,6 +3114,9 @@ /* No comment provided by engineer. */ "Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Tylko urządzenia klienckie przechowują profile użytkowników, kontakty, grupy i wiadomości wysyłane za pomocą **2-warstwowego szyfrowania end-to-end**."; +/* No comment provided by engineer. */ +"Only delete conversation" = "Usuń tylko rozmowę"; + /* No comment provided by engineer. */ "Only group owners can change group preferences." = "Tylko właściciele grup mogą zmieniać preferencje grupy."; @@ -2795,6 +3171,9 @@ /* authentication reason */ "Open migration to another device" = "Otwórz migrację na innym urządzeniu"; +/* No comment provided by engineer. */ +"Open server settings" = "Otwórz ustawienia serwera"; + /* No comment provided by engineer. */ "Open Settings" = "Otwórz Ustawienia"; @@ -2819,9 +3198,18 @@ /* No comment provided by engineer. */ "Or show this code" = "Lub pokaż ten kod"; +/* No comment provided by engineer. */ +"other" = "inne"; + /* No comment provided by engineer. */ "Other" = "Inne"; +/* No comment provided by engineer. */ +"Other %@ servers" = "Inne %@ serwery"; + +/* No comment provided by engineer. */ +"other errors" = "inne błędy"; + /* member role */ "owner" = "właściciel"; @@ -2864,6 +3252,9 @@ /* No comment provided by engineer. */ "peer-to-peer" = "peer-to-peer"; +/* No comment provided by engineer. */ +"Pending" = "Oczekujące"; + /* No comment provided by engineer. */ "People can connect to you only via the links you share." = "Ludzie mogą się z Tobą połączyć tylko poprzez linki, które udostępniasz."; @@ -2882,9 +3273,18 @@ /* No comment provided by engineer. */ "PING interval" = "Interwał PINGU"; +/* No comment provided by engineer. */ +"Play from the chat list." = "Odtwórz z listy czatów."; + +/* No comment provided by engineer. */ +"Please ask your contact to enable calls." = "Poproś kontakt o włącznie połączeń."; + /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "Poproś Twój kontakt o włączenie wysyłania wiadomości głosowych."; +/* No comment provided by engineer. */ +"Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.\nPlease share any other issues with the developers." = "Sprawdź, czy telefon i komputer są podłączone do tej samej sieci lokalnej i czy zapora sieciowa komputera umożliwia połączenie.\nProszę podzielić się innymi problemami z deweloperami."; + /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "Sprawdź, czy użyłeś prawidłowego linku lub poproś Twój kontakt o przesłanie innego."; @@ -2942,6 +3342,9 @@ /* No comment provided by engineer. */ "Preview" = "Podgląd"; +/* No comment provided by engineer. */ +"Previously connected servers" = "Wcześniej połączone serwery"; + /* No comment provided by engineer. */ "Privacy & security" = "Prywatność i bezpieczeństwo"; @@ -2951,9 +3354,21 @@ /* No comment provided by engineer. */ "Private filenames" = "Prywatne nazwy plików"; +/* No comment provided by engineer. */ +"Private message routing" = "Trasowanie prywatnych wiadomości"; + +/* No comment provided by engineer. */ +"Private message routing 🚀" = "Trasowanie prywatnych wiadomości🚀"; + /* name of notes to self */ "Private notes" = "Prywatne notatki"; +/* No comment provided by engineer. */ +"Private routing" = "Prywatne trasowanie"; + +/* No comment provided by engineer. */ +"Private routing error" = "Błąd prywatnego trasowania"; + /* No comment provided by engineer. */ "Profile and server connections" = "Profil i połączenia z serwerem"; @@ -2972,6 +3387,9 @@ /* No comment provided by engineer. */ "Profile password" = "Hasło profilu"; +/* No comment provided by engineer. */ +"Profile theme" = "Motyw profilu"; + /* No comment provided by engineer. */ "Profile update will be sent to your contacts." = "Aktualizacja profilu zostanie wysłana do Twoich kontaktów."; @@ -3005,15 +3423,27 @@ /* No comment provided by engineer. */ "Protect app screen" = "Chroń ekran aplikacji"; +/* No comment provided by engineer. */ +"Protect IP address" = "Chroń adres IP"; + /* No comment provided by engineer. */ "Protect your chat profiles with a password!" = "Chroń swoje profile czatu hasłem!"; +/* No comment provided by engineer. */ +"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Chroni Twój adres IP przed przekaźnikami wiadomości wybranych przez Twoje kontakty.\nWłącz w ustawianiach *Sieć i serwery* ."; + /* No comment provided by engineer. */ "Protocol timeout" = "Limit czasu protokołu"; /* No comment provided by engineer. */ "Protocol timeout per KB" = "Limit czasu protokołu na KB"; +/* No comment provided by engineer. */ +"Proxied" = "Trasowane przez proxy"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Serwery trasowane przez proxy"; + /* No comment provided by engineer. */ "Push notifications" = "Powiadomienia push"; @@ -3029,10 +3459,13 @@ /* No comment provided by engineer. */ "Rate the app" = "Oceń aplikację"; +/* No comment provided by engineer. */ +"Reachable chat toolbar" = "Osiągalny pasek narzędzi czatu"; + /* chat item menu */ "React…" = "Reaguj…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "Czytaj"; /* No comment provided by engineer. */ @@ -3056,6 +3489,9 @@ /* No comment provided by engineer. */ "Receipts are disabled" = "Potwierdzenia są wyłączone"; +/* No comment provided by engineer. */ +"Receive errors" = "Błędy otrzymania"; + /* No comment provided by engineer. */ "received answer…" = "otrzymano odpowiedź…"; @@ -3075,10 +3511,16 @@ "Received message" = "Otrzymano wiadomość"; /* No comment provided by engineer. */ -"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Adres odbiorczy zostanie zmieniony na inny serwer. Zmiana adresu zostanie zakończona gdy nadawca będzie online."; +"Received messages" = "Otrzymane wiadomości"; /* No comment provided by engineer. */ -"Receiving concurrency" = "Konkurencyjne odbieranie"; +"Received reply" = "Otrzymano odpowiedź"; + +/* No comment provided by engineer. */ +"Received total" = "Otrzymano łącznie"; + +/* No comment provided by engineer. */ +"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Adres odbiorczy zostanie zmieniony na inny serwer. Zmiana adresu zostanie zakończona gdy nadawca będzie online."; /* No comment provided by engineer. */ "Receiving file will be stopped." = "Odbieranie pliku zostanie przerwane."; @@ -3095,9 +3537,24 @@ /* No comment provided by engineer. */ "Recipients see updates as you type them." = "Odbiorcy widzą aktualizacje podczas ich wpisywania."; +/* No comment provided by engineer. */ +"Reconnect" = "Połącz ponownie"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Połącz ponownie wszystkie połączone serwery, aby wymusić dostarczanie wiadomości. Wykorzystuje dodatkowy ruch."; +/* No comment provided by engineer. */ +"Reconnect all servers" = "Połącz ponownie wszystkie serwery"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "Połączyć ponownie wszystkie serwery?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "Ponownie połącz z serwerem w celu wymuszenia dostarczenia wiadomości. Wykorzystuje to dodatkowy ruch."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "Połączyć ponownie serwer?"; + /* No comment provided by engineer. */ "Reconnect servers?" = "Ponownie połączyć serwery?"; @@ -3110,7 +3567,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Zmniejszone zużycie baterii"; -/* reject incoming call via notification */ +/* reject incoming call via notification + swipe action */ "Reject" = "Odrzuć"; /* No comment provided by engineer. */ @@ -3131,6 +3589,9 @@ /* No comment provided by engineer. */ "Remove" = "Usuń"; +/* No comment provided by engineer. */ +"Remove image" = "Usuń obraz"; + /* No comment provided by engineer. */ "Remove member" = "Usuń członka"; @@ -3188,12 +3649,27 @@ /* No comment provided by engineer. */ "Reset" = "Resetuj"; +/* No comment provided by engineer. */ +"Reset all hints" = "Zresetuj wszystkie wskazówki"; + +/* No comment provided by engineer. */ +"Reset all statistics" = "Resetuj wszystkie statystyki"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "Zresetować wszystkie statystyki?"; + /* No comment provided by engineer. */ "Reset colors" = "Resetuj kolory"; +/* No comment provided by engineer. */ +"Reset to app theme" = "Zresetuj do motywu aplikacji"; + /* No comment provided by engineer. */ "Reset to defaults" = "Przywróć wartości domyślne"; +/* No comment provided by engineer. */ +"Reset to user theme" = "Zresetuj do motywu użytkownika"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Uruchom ponownie aplikację, aby utworzyć nowy profil czatu"; @@ -3218,9 +3694,6 @@ /* chat item action */ "Reveal" = "Ujawnij"; -/* No comment provided by engineer. */ -"Revert" = "Przywrócić"; - /* No comment provided by engineer. */ "Revoke" = "Odwołaj"; @@ -3236,6 +3709,9 @@ /* No comment provided by engineer. */ "Run chat" = "Uruchom czat"; +/* No comment provided by engineer. */ +"Safely receive files" = "Bezpiecznie otrzymuj pliki"; + /* No comment provided by engineer. */ "Safer groups" = "Bezpieczniejsze grupy"; @@ -3251,6 +3727,9 @@ /* No comment provided by engineer. */ "Save and notify group members" = "Zapisz i powiadom członków grupy"; +/* No comment provided by engineer. */ +"Save and reconnect" = "Zapisz i połącz ponownie"; + /* No comment provided by engineer. */ "Save and update group profile" = "Zapisz i zaktualizuj profil grupowy"; @@ -3305,6 +3784,12 @@ /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "Zapisane serwery WebRTC ICE zostaną usunięte"; +/* No comment provided by engineer. */ +"Scale" = "Skaluj"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Skanuj / Wklej link"; + /* No comment provided by engineer. */ "Scan code" = "Zeskanuj kod"; @@ -3320,6 +3805,9 @@ /* No comment provided by engineer. */ "Scan server QR code" = "Zeskanuj kod QR serwera"; +/* No comment provided by engineer. */ +"search" = "szukaj"; + /* No comment provided by engineer. */ "Search" = "Szukaj"; @@ -3332,6 +3820,9 @@ /* network option */ "sec" = "sek"; +/* No comment provided by engineer. */ +"Secondary" = "Drugorzędny"; + /* time unit */ "seconds" = "sekundy"; @@ -3341,6 +3832,9 @@ /* server test step */ "Secure queue" = "Bezpieczna kolejka"; +/* No comment provided by engineer. */ +"Secured" = "Zabezpieczone"; + /* No comment provided by engineer. */ "Security assessment" = "Ocena bezpieczeństwa"; @@ -3350,9 +3844,15 @@ /* chat item text */ "security code changed" = "kod bezpieczeństwa zmieniony"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "Wybierz"; +/* No comment provided by engineer. */ +"Selected %lld" = "Zaznaczono %lld"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Wybrane preferencje czatu zabraniają tej wiadomości."; + /* No comment provided by engineer. */ "Self-destruct" = "Samozniszczenie"; @@ -3377,21 +3877,30 @@ /* No comment provided by engineer. */ "send direct message" = "wyślij wiadomość bezpośrednią"; -/* No comment provided by engineer. */ -"Send direct message" = "Wyślij wiadomość bezpośrednią"; - /* No comment provided by engineer. */ "Send direct message to connect" = "Wyślij wiadomość bezpośrednią aby połączyć"; /* No comment provided by engineer. */ "Send disappearing message" = "Wyślij znikającą wiadomość"; +/* No comment provided by engineer. */ +"Send errors" = "Wyślij błędy"; + /* No comment provided by engineer. */ "Send link previews" = "Wyślij podgląd linku"; /* No comment provided by engineer. */ "Send live message" = "Wyślij wiadomość na żywo"; +/* No comment provided by engineer. */ +"Send message to enable calls." = "Wyślij wiadomość aby włączyć połączenia."; + +/* No comment provided by engineer. */ +"Send messages directly when IP address is protected and your or destination server does not support private routing." = "Wysyłaj wiadomości bezpośrednio, gdy adres IP jest chroniony i Twój lub docelowy serwer nie obsługuje prywatnego trasowania."; + +/* No comment provided by engineer. */ +"Send messages directly when your or destination server does not support private routing." = "Wysyłaj wiadomości bezpośrednio, gdy Twój lub docelowy serwer nie obsługuje prywatnego trasowania."; + /* No comment provided by engineer. */ "Send notifications" = "Wyślij powiadomienia"; @@ -3446,15 +3955,42 @@ /* copied message info */ "Sent at: %@" = "Wysłano o: %@"; +/* No comment provided by engineer. */ +"Sent directly" = "Wysłano bezpośrednio"; + /* notification */ "Sent file event" = "Wyślij zdarzenie pliku"; /* message info title */ "Sent message" = "Wyślij wiadomość"; +/* No comment provided by engineer. */ +"Sent messages" = "Wysłane wiadomości"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "Wysłane wiadomości zostaną usunięte po ustawionym czasie."; +/* No comment provided by engineer. */ +"Sent reply" = "Wyślij odpowiedź"; + +/* No comment provided by engineer. */ +"Sent total" = "Wysłano łącznie"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Wysłano przez proxy"; + +/* No comment provided by engineer. */ +"Server address" = "Adres serwera"; + +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "Adres serwera jest niekompatybilny z ustawieniami sieci: %@."; + +/* srv error text. */ +"Server address is incompatible with network settings." = "Adres serwera jest niekompatybilny z ustawieniami sieciowymi."; + +/* queue info */ +"server queue info: %@\n\nlast received msg: %@" = "Informacje kolejki serwera: %1$@\n\nostatnia otrzymana wiadomość: %2$@"; + /* server test error */ "Server requires authorization to create queues, check password" = "Serwer wymaga autoryzacji do tworzenia kolejek, sprawdź hasło"; @@ -3464,9 +4000,24 @@ /* No comment provided by engineer. */ "Server test failed!" = "Test serwera nie powiódł się!"; +/* No comment provided by engineer. */ +"Server type" = "Typ serwera"; + +/* srv error text */ +"Server version is incompatible with network settings." = "Wersja serwera jest niekompatybilna z ustawieniami sieciowymi."; + +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "Wersja serwera jest niekompatybilna z aplikacją: %@."; + /* No comment provided by engineer. */ "Servers" = "Serwery"; +/* No comment provided by engineer. */ +"Servers info" = "Informacje o serwerach"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "Statystyki serwerów zostaną zresetowane - nie można tego cofnąć!"; + /* No comment provided by engineer. */ "Session code" = "Kod sesji"; @@ -3476,6 +4027,9 @@ /* No comment provided by engineer. */ "Set contact name…" = "Ustaw nazwę kontaktu…"; +/* No comment provided by engineer. */ +"Set default theme" = "Ustaw domyślny motyw"; + /* No comment provided by engineer. */ "Set group preferences" = "Ustaw preferencje grupy"; @@ -3521,15 +4075,24 @@ /* No comment provided by engineer. */ "Share address with contacts?" = "Udostępnić adres kontaktom?"; +/* No comment provided by engineer. */ +"Share from other apps." = "Udostępnij z innych aplikacji."; + /* No comment provided by engineer. */ "Share link" = "Udostępnij link"; /* No comment provided by engineer. */ "Share this 1-time invite link" = "Udostępnij ten jednorazowy link"; +/* No comment provided by engineer. */ +"Share to SimpleX" = "Udostępnij do SimpleX"; + /* No comment provided by engineer. */ "Share with contacts" = "Udostępnij kontaktom"; +/* No comment provided by engineer. */ +"Show → on messages sent via private routing." = "Pokaż → na wiadomościach wysłanych przez prywatne trasowanie."; + /* No comment provided by engineer. */ "Show calls in phone history" = "Pokaż połączenia w historii telefonu"; @@ -3539,6 +4102,12 @@ /* No comment provided by engineer. */ "Show last messages" = "Pokaż ostatnie wiadomości"; +/* No comment provided by engineer. */ +"Show message status" = "Pokaż status wiadomości"; + +/* No comment provided by engineer. */ +"Show percentage" = "Pokaż procent"; + /* No comment provided by engineer. */ "Show preview" = "Pokaż podgląd"; @@ -3548,6 +4117,9 @@ /* No comment provided by engineer. */ "Show:" = "Pokaż:"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "Adres SimpleX"; @@ -3593,6 +4165,9 @@ /* No comment provided by engineer. */ "Simplified incognito mode" = "Uproszczony tryb incognito"; +/* No comment provided by engineer. */ +"Size" = "Rozmiar"; + /* No comment provided by engineer. */ "Skip" = "Pomiń"; @@ -3603,11 +4178,20 @@ "Small groups (max 20)" = "Małe grupy (maks. 20)"; /* No comment provided by engineer. */ -"SMP servers" = "Serwery SMP"; +"SMP server" = "Serwer SMP"; + +/* blur media */ +"Soft" = "Łagodny"; + +/* No comment provided by engineer. */ +"Some file(s) were not exported:" = "Niektóre plik(i) nie zostały wyeksportowane:"; /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "Podczas importu wystąpiły niekrytyczne błędy - więcej szczegółów można znaleźć w konsoli czatu."; +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import:" = "Podczas importu wystąpiły niekrytyczne błędy:"; + /* notification title */ "Somebody" = "Ktoś"; @@ -3626,9 +4210,15 @@ /* No comment provided by engineer. */ "Start migration" = "Rozpocznij migrację"; +/* No comment provided by engineer. */ +"Starting from %@." = "Zaczynanie od %@."; + /* No comment provided by engineer. */ "starting…" = "uruchamianie…"; +/* No comment provided by engineer. */ +"Statistics" = "Statystyki"; + /* No comment provided by engineer. */ "Stop" = "Zatrzymaj"; @@ -3668,9 +4258,21 @@ /* No comment provided by engineer. */ "strike" = "strajk"; +/* blur media */ +"Strong" = "Silne"; + /* No comment provided by engineer. */ "Submit" = "Zatwierdź"; +/* No comment provided by engineer. */ +"Subscribed" = "Zasubskrybowano"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Błędy subskrypcji"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Subskrypcje zignorowane"; + /* No comment provided by engineer. */ "Support SimpleX Chat" = "Wspieraj SimpleX Chat"; @@ -3705,7 +4307,7 @@ "Tap to scan" = "Dotknij, aby zeskanować"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Dotknij, aby rozpocząć nowy czat"; +"TCP connection" = "Połączenie TCP"; /* No comment provided by engineer. */ "TCP connection timeout" = "Limit czasu połączenia TCP"; @@ -3719,6 +4321,9 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* No comment provided by engineer. */ +"Temporary file error" = "Tymczasowy błąd pliku"; + /* server test failure */ "Test failed at step %@." = "Test nie powiódł się na etapie %@."; @@ -3746,6 +4351,9 @@ /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Aplikacja może powiadamiać Cię, gdy otrzymujesz wiadomości lub prośby o kontakt — otwórz ustawienia, aby włączyć."; +/* No comment provided by engineer. */ +"The app will ask to confirm downloads from unknown file servers (except .onion)." = "Aplikacja zapyta o potwierdzenie pobierania od nieznanych serwerów plików (poza .onion)."; + /* No comment provided by engineer. */ "The attempt to change database passphrase was not completed." = "Próba zmiany hasła bazy danych nie została zakończona."; @@ -3776,6 +4384,12 @@ /* No comment provided by engineer. */ "The message will be marked as moderated for all members." = "Wiadomość zostanie oznaczona jako moderowana dla wszystkich członków."; +/* No comment provided by engineer. */ +"The messages will be deleted for all members." = "Wiadomości zostaną usunięte dla wszystkich członków."; + +/* No comment provided by engineer. */ +"The messages will be marked as moderated for all members." = "Wiadomości zostaną oznaczone jako moderowane dla wszystkich członków."; + /* No comment provided by engineer. */ "The next generation of private messaging" = "Następna generacja prywatnych wiadomości"; @@ -3798,7 +4412,7 @@ "The text you pasted is not a SimpleX link." = "Tekst, który wkleiłeś nie jest linkiem SimpleX."; /* No comment provided by engineer. */ -"Theme" = "Motyw"; +"Themes" = "Motywy"; /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Te ustawienia dotyczą Twojego bieżącego profilu **%@**."; @@ -3842,9 +4456,15 @@ /* No comment provided by engineer. */ "This is your own SimpleX address!" = "To jest twój własny adres SimpleX!"; +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "Ten link dostał użyty z innym urządzeniem mobilnym, proszę stworzyć nowy link na komputerze."; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "To ustawienie dotyczy wiadomości Twojego bieżącego profilu czatu **%@**."; +/* No comment provided by engineer. */ +"Title" = "Tytuł"; + /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Aby zadać wszelkie pytania i otrzymywać aktualizacje:"; @@ -3866,6 +4486,9 @@ /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Aby chronić swoje informacje, włącz funkcję blokady SimpleX.\nPrzed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania."; +/* No comment provided by engineer. */ +"To protect your IP address, private routing uses your SMP servers to deliver messages." = "Aby chronić Twój adres IP, prywatne trasowanie używa Twoich serwerów SMP, aby dostarczyć wiadomości."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Aby nagrać wiadomość głosową należy udzielić zgody na użycie Mikrofonu."; @@ -3878,12 +4501,24 @@ /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Aby zweryfikować szyfrowanie end-to-end z Twoim kontaktem porównaj (lub zeskanuj) kod na waszych urządzeniach."; +/* No comment provided by engineer. */ +"Toggle chat list:" = "Przełącz listę czatów:"; + /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Przełącz incognito przy połączeniu."; +/* No comment provided by engineer. */ +"Toolbar opacity" = "Nieprzezroczystość paska narzędzi"; + +/* No comment provided by engineer. */ +"Total" = "Łącznie"; + /* No comment provided by engineer. */ "Transport isolation" = "Izolacja transportu"; +/* No comment provided by engineer. */ +"Transport sessions" = "Sesje transportowe"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu (błąd: %@)."; @@ -3920,13 +4555,10 @@ /* rcv group event chat item */ "unblocked %@" = "odblokowano %@"; -/* item status description */ -"Unexpected error: %@" = "Nieoczekiwany błąd: %@"; - /* No comment provided by engineer. */ "Unexpected migration state" = "Nieoczekiwany stan migracji"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "Nie ulub."; /* No comment provided by engineer. */ @@ -3953,6 +4585,12 @@ /* No comment provided by engineer. */ "Unknown error" = "Nieznany błąd"; +/* No comment provided by engineer. */ +"unknown servers" = "nieznane przekaźniki"; + +/* No comment provided by engineer. */ +"Unknown servers!" = "Nieznane serwery!"; + /* No comment provided by engineer. */ "unknown status" = "nieznany status"; @@ -3975,9 +4613,15 @@ "Unlock app" = "Odblokuj aplikację"; /* No comment provided by engineer. */ +"unmute" = "wyłącz wyciszenie"; + +/* swipe action */ "Unmute" = "Wyłącz wyciszenie"; /* No comment provided by engineer. */ +"unprotected" = "niezabezpieczony"; + +/* swipe action */ "Unread" = "Nieprzeczytane"; /* No comment provided by engineer. */ @@ -3986,9 +4630,6 @@ /* No comment provided by engineer. */ "Update" = "Aktualizuj"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = "Zaktualizować ustawienie hostów .onion?"; - /* No comment provided by engineer. */ "Update database passphrase" = "Aktualizuj hasło do bazy danych"; @@ -3996,7 +4637,7 @@ "Update network settings?" = "Zaktualizować ustawienia sieci?"; /* No comment provided by engineer. */ -"Update transport isolation mode?" = "Zaktualizować tryb izolacji transportu?"; +"Update settings?" = "Zaktualizować ustawienia?"; /* rcv group event chat item */ "updated group profile" = "zaktualizowano profil grupy"; @@ -4008,10 +4649,10 @@ "Updating settings will re-connect the client to all servers." = "Aktualizacja ustawień spowoduje ponowne połączenie klienta ze wszystkimi serwerami."; /* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "Aktualizacja tych ustawień spowoduje ponowne połączenie klienta ze wszystkimi serwerami."; +"Upgrade and open chat" = "Zaktualizuj i otwórz czat"; /* No comment provided by engineer. */ -"Upgrade and open chat" = "Zaktualizuj i otwórz czat"; +"Upload errors" = "Błędy przesłania"; /* No comment provided by engineer. */ "Upload failed" = "Wgrywanie nie udane"; @@ -4019,6 +4660,12 @@ /* server test step */ "Upload file" = "Prześlij plik"; +/* No comment provided by engineer. */ +"Uploaded" = "Przesłane"; + +/* No comment provided by engineer. */ +"Uploaded files" = "Przesłane pliki"; + /* No comment provided by engineer. */ "Uploading archive" = "Wgrywanie archiwum"; @@ -4046,6 +4693,12 @@ /* No comment provided by engineer. */ "Use only local notifications?" = "Używać tylko lokalnych powiadomień?"; +/* No comment provided by engineer. */ +"Use private routing with unknown servers when IP address is not protected." = "Używaj prywatnego trasowania z nieznanymi serwerami, gdy adres IP nie jest chroniony."; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers." = "Używaj prywatnego trasowania z nieznanymi serwerami."; + /* No comment provided by engineer. */ "Use server" = "Użyj serwera"; @@ -4055,11 +4708,14 @@ /* No comment provided by engineer. */ "Use the app while in the call." = "Używaj aplikacji podczas połączenia."; +/* No comment provided by engineer. */ +"Use the app with one hand." = "Korzystaj z aplikacji jedną ręką."; + /* No comment provided by engineer. */ "User profile" = "Profil użytkownika"; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "Używanie hostów .onion wymaga kompatybilnego dostawcy VPN."; +"User selection" = "Wybór użytkownika"; /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "Używanie serwerów SimpleX Chat."; @@ -4109,6 +4765,9 @@ /* No comment provided by engineer. */ "Via secure quantum resistant protocol." = "Dzięki bezpiecznemu protokołowi odpornego kwantowo."; +/* No comment provided by engineer. */ +"video" = "wideo"; + /* No comment provided by engineer. */ "Video call" = "Połączenie wideo"; @@ -4166,6 +4825,12 @@ /* No comment provided by engineer. */ "Waiting for video" = "Oczekiwanie na film"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "Akcent tapety"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Tło tapety"; + /* No comment provided by engineer. */ "wants to connect to you!" = "chce się z Tobą połączyć!"; @@ -4199,6 +4864,9 @@ /* No comment provided by engineer. */ "When connecting audio and video calls." = "Podczas łączenia połączeń audio i wideo."; +/* No comment provided by engineer. */ +"when IP hidden" = "gdy IP ukryty"; + /* No comment provided by engineer. */ "When people request to connect, you can accept or reject it." = "Kiedy ludzie proszą o połączenie, możesz je zaakceptować lub odrzucić."; @@ -4223,14 +4891,26 @@ /* No comment provided by engineer. */ "With reduced battery usage." = "Ze zmniejszonym zużyciem baterii."; +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to file servers." = "Bez Tor lub VPN, Twój adres IP będzie widoczny do serwerów plików."; + +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Bez Tor lub VPN, Twój adres IP będzie widoczny dla tych przekaźników XFTP: %@."; + /* No comment provided by engineer. */ "Wrong database passphrase" = "Nieprawidłowe hasło bazy danych"; +/* snd error text */ +"Wrong key or unknown connection - most likely this connection is deleted." = "Zły klucz lub nieznane połączenie - najprawdopodobniej to połączenie jest usunięte."; + +/* file error text */ +"Wrong key or unknown file chunk address - most likely file is deleted." = "Zły klucz lub nieznany adres fragmentu pliku - najprawdopodobniej plik został usunięty."; + /* No comment provided by engineer. */ "Wrong passphrase!" = "Nieprawidłowe hasło!"; /* No comment provided by engineer. */ -"XFTP servers" = "Serwery XFTP"; +"XFTP server" = "Serwer XFTP"; /* pref value */ "yes" = "tak"; @@ -4286,6 +4966,9 @@ /* No comment provided by engineer. */ "You are invited to group" = "Jesteś zaproszony do grupy"; +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "Nie jesteś połączony z tymi serwerami. Prywatne trasowanie jest używane do dostarczania do nich wiadomości."; + /* No comment provided by engineer. */ "you are observer" = "jesteś obserwatorem"; @@ -4295,6 +4978,9 @@ /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "Możesz przyjmować połączenia z ekranu blokady, bez uwierzytelniania urządzenia i aplikacji."; +/* No comment provided by engineer. */ +"You can change it in Appearance settings." = "Możesz to zmienić w ustawieniach wyglądu."; + /* No comment provided by engineer. */ "You can create it later" = "Możesz go utworzyć później"; @@ -4314,7 +5000,10 @@ "You can make it visible to your SimpleX contacts via Settings." = "Możesz ustawić go jako widoczny dla swoich kontaktów SimpleX w Ustawieniach."; /* notification body */ -"You can now send messages to %@" = "Możesz teraz wysyłać wiadomości do %@"; +"You can now chat with %@" = "Możesz teraz wysyłać wiadomości do %@"; + +/* No comment provided by engineer. */ +"You can send messages to %@ from Archived contacts." = "Możesz wysyłać wiadomości do %@ ze zarchiwizowanych kontaktów."; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Podgląd powiadomień na ekranie blokady można ustawić w ustawieniach."; @@ -4331,6 +5020,9 @@ /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "Możesz rozpocząć czat poprzez Ustawienia aplikacji / Baza danych lub poprzez ponowne uruchomienie aplikacji"; +/* No comment provided by engineer. */ +"You can still view conversation with %@ in the list of chats." = "Nadal możesz przeglądać rozmowę z %@ na liście czatów."; + /* No comment provided by engineer. */ "You can turn on SimpleX Lock via Settings." = "Możesz włączyć blokadę SimpleX poprzez Ustawienia."; @@ -4367,9 +5059,6 @@ /* No comment provided by engineer. */ "You have already requested connection!\nRepeat connection request?" = "Już prosiłeś o połączenie!\nPowtórzyć prośbę połączenia?"; -/* No comment provided by engineer. */ -"You have no chats" = "Nie masz czatów"; - /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "Musisz wprowadzić hasło przy każdym uruchomieniu aplikacji - nie jest one przechowywane na urządzeniu."; @@ -4385,9 +5074,18 @@ /* snd group event chat item */ "you left" = "wyszedłeś"; +/* No comment provided by engineer. */ +"You may migrate the exported database." = "Możesz zmigrować wyeksportowaną bazy danych."; + +/* No comment provided by engineer. */ +"You may save the exported archive." = "Możesz zapisać wyeksportowane archiwum."; + /* No comment provided by engineer. */ "You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." = "Musisz używać najnowszej wersji bazy danych czatu TYLKO na jednym urządzeniu, w przeciwnym razie możesz przestać otrzymywać wiadomości od niektórych kontaktów."; +/* No comment provided by engineer. */ +"You need to allow your contact to call to be able to call them." = "Aby móc dzwonić, musisz zezwolić kontaktowi na połączenia."; + /* No comment provided by engineer. */ "You need to allow your contact to send voice messages to be able to send them." = "Musisz zezwolić Twojemu kontaktowi na wysyłanie wiadomości głosowych, aby móc je wysyłać."; @@ -4460,9 +5158,6 @@ /* No comment provided by engineer. */ "Your chat profiles" = "Twoje profile czatu"; -/* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Twój kontakt musi być online, aby połączenie zostało zakończone.\nMożesz anulować to połączenie i usunąć kontakt (i spróbować później z nowym linkiem)."; - /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Twój kontakt wysłał plik, który jest większy niż obecnie obsługiwany maksymalny rozmiar (%@)."; diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 2c703f0095..5d74647d07 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -154,9 +154,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ подтверждён"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ серверы"; - /* No comment provided by engineer. */ "%@ uploaded" = "%@ загружено"; @@ -338,10 +335,11 @@ "above, then choose:" = "наверху, затем выберите:"; /* No comment provided by engineer. */ -"Accent color" = "Основной цвет"; +"Accent" = "Акцент"; /* accept contact request via notification - accept incoming call via notification */ + accept incoming call via notification + swipe action */ "Accept" = "Принять"; /* No comment provided by engineer. */ @@ -350,12 +348,22 @@ /* notification body */ "Accept contact request from %@?" = "Принять запрос на соединение от %@?"; -/* accept contact request via notification */ +/* accept contact request via notification + swipe action */ "Accept incognito" = "Принять инкогнито"; /* call status */ "accepted call" = "принятый звонок"; +/* No comment provided by engineer. */ +"Acknowledged" = "Подтверждено"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Ошибки подтверждения"; + +/* No comment provided by engineer. */ +"Active connections" = "Активные соединения"; + /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Добавьте адрес в свой профиль, чтобы Ваши контакты могли поделиться им. Профиль будет отправлен Вашим контактам."; @@ -369,7 +377,7 @@ "Add profile" = "Добавить профиль"; /* No comment provided by engineer. */ -"Add server…" = "Добавить сервер…"; +"Add server" = "Добавить сервер"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Добавить серверы через QR код."; @@ -380,6 +388,15 @@ /* No comment provided by engineer. */ "Add welcome message" = "Добавить приветственное сообщение"; +/* No comment provided by engineer. */ +"Additional accent" = "Дополнительный акцент"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Дополнительный акцент 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "Вторичный 2"; + /* No comment provided by engineer. */ "Address" = "Адрес"; @@ -401,6 +418,9 @@ /* No comment provided by engineer. */ "Advanced network settings" = "Настройки сети"; +/* No comment provided by engineer. */ +"Advanced settings" = "Настройки сети"; + /* chat item text */ "agreeing encryption for %@…" = "шифрование согласовывается для %@…"; @@ -416,6 +436,9 @@ /* No comment provided by engineer. */ "All data is erased when it is entered." = "Все данные удаляются при его вводе."; +/* No comment provided by engineer. */ +"All data is private to your device." = "Все данные хранятся только на вашем устройстве."; + /* No comment provided by engineer. */ "All group members will remain connected." = "Все члены группы, которые соединились через эту ссылку, останутся в группе."; @@ -431,6 +454,9 @@ /* No comment provided by engineer. */ "All new messages from %@ will be hidden!" = "Все новые сообщения от %@ будут скрыты!"; +/* No comment provided by engineer. */ +"All profiles" = "Все профили"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Все контакты, которые соединились через этот адрес, сохранятся."; @@ -446,9 +472,15 @@ /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "Разрешить звонки, только если их разрешает Ваш контакт."; +/* No comment provided by engineer. */ +"Allow calls?" = "Разрешить звонки?"; + /* No comment provided by engineer. */ "Allow disappearing messages only if your contact allows it to you." = "Разрешить исчезающие сообщения, только если Ваш контакт разрешает их Вам."; +/* No comment provided by engineer. */ +"Allow downgrade" = "Разрешить прямую доставку"; + /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Разрешить необратимое удаление сообщений, только если Ваш контакт разрешает это Вам. (24 часа)"; @@ -464,6 +496,9 @@ /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Разрешить посылать исчезающие сообщения."; +/* No comment provided by engineer. */ +"Allow sharing" = "Разрешить поделиться"; + /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Разрешить необратимо удалять отправленные сообщения. (24 часа)"; @@ -509,6 +544,9 @@ /* pref value */ "always" = "всегда"; +/* No comment provided by engineer. */ +"Always use private routing." = "Всегда использовать конфиденциальную доставку."; + /* No comment provided by engineer. */ "Always use relay" = "Всегда соединяться через relay"; @@ -551,15 +589,27 @@ /* No comment provided by engineer. */ "Apply" = "Применить"; +/* No comment provided by engineer. */ +"Apply to" = "Применить к"; + /* No comment provided by engineer. */ "Archive and upload" = "Архивировать и загрузить"; +/* No comment provided by engineer. */ +"Archive contacts to chat later." = "Архивируйте контакты чтобы продолжить переписку."; + +/* No comment provided by engineer. */ +"Archived contacts" = "Архивированные контакты"; + /* No comment provided by engineer. */ "Archiving database" = "Подготовка архива"; /* No comment provided by engineer. */ "Attach" = "Прикрепить"; +/* No comment provided by engineer. */ +"attempts" = "попытки"; + /* No comment provided by engineer. */ "Audio & video calls" = "Аудио- и видеозвонки"; @@ -602,6 +652,9 @@ /* No comment provided by engineer. */ "Back" = "Назад"; +/* No comment provided by engineer. */ +"Background" = "Фон"; + /* No comment provided by engineer. */ "Bad desktop address" = "Неверный адрес компьютера"; @@ -623,6 +676,12 @@ /* No comment provided by engineer. */ "Better messages" = "Улучшенные сообщения"; +/* No comment provided by engineer. */ +"Better networking" = "Улучшенные сетевые функции"; + +/* No comment provided by engineer. */ +"Black" = "Черная"; + /* No comment provided by engineer. */ "Block" = "Заблокировать"; @@ -653,6 +712,12 @@ /* No comment provided by engineer. */ "Blocked by admin" = "Заблокирован администратором"; +/* No comment provided by engineer. */ +"Blur for better privacy." = "Размыть для конфиденциальности."; + +/* No comment provided by engineer. */ +"Blur media" = "Размытие изображений"; + /* No comment provided by engineer. */ "bold" = "жирный"; @@ -677,6 +742,9 @@ /* No comment provided by engineer. */ "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "По профилю чата или [по соединению](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (БЕТА)."; +/* No comment provided by engineer. */ +"call" = "звонок"; + /* No comment provided by engineer. */ "Call already ended!" = "Звонок уже завершен!"; @@ -692,15 +760,27 @@ /* No comment provided by engineer. */ "Calls" = "Звонки"; +/* No comment provided by engineer. */ +"Calls prohibited!" = "Звонки запрещены!"; + /* No comment provided by engineer. */ "Camera not available" = "Камера недоступна"; +/* No comment provided by engineer. */ +"Can't call contact" = "Не удается позвонить контакту"; + +/* No comment provided by engineer. */ +"Can't call member" = "Не удается позвонить члену группы"; + /* No comment provided by engineer. */ "Can't invite contact!" = "Нельзя пригласить контакт!"; /* No comment provided by engineer. */ "Can't invite contacts!" = "Нельзя пригласить контакты!"; +/* No comment provided by engineer. */ +"Can't message member" = "Не удается написать члену группы"; + /* No comment provided by engineer. */ "Cancel" = "Отменить"; @@ -713,9 +793,15 @@ /* No comment provided by engineer. */ "Cannot access keychain to save database password" = "Ошибка доступа к Keychain при сохранении пароля"; +/* No comment provided by engineer. */ +"Cannot forward message" = "Невозможно переслать сообщение"; + /* No comment provided by engineer. */ "Cannot receive file" = "Невозможно получить файл"; +/* snd error text */ +"Capacity exceeded - recipient did not receive previously sent messages." = "Превышено количество сообщений - предыдущие сообщения не доставлены."; + /* No comment provided by engineer. */ "Cellular" = "Мобильная сеть"; @@ -768,6 +854,9 @@ /* No comment provided by engineer. */ "Chat archive" = "Архив чата"; +/* No comment provided by engineer. */ +"Chat colors" = "Цвета чата"; + /* No comment provided by engineer. */ "Chat console" = "Консоль"; @@ -777,6 +866,9 @@ /* No comment provided by engineer. */ "Chat database deleted" = "Данные чата удалены"; +/* No comment provided by engineer. */ +"Chat database exported" = "Данные чата экспортированы"; + /* No comment provided by engineer. */ "Chat database imported" = "Архив чата импортирован"; @@ -789,12 +881,18 @@ /* No comment provided by engineer. */ "Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." = "Чат остановлен. Если вы уже использовали эту базу данных на другом устройстве, перенесите ее обратно до запуска чата."; +/* No comment provided by engineer. */ +"Chat list" = "Список чатов"; + /* No comment provided by engineer. */ "Chat migrated!" = "Чат мигрирован!"; /* No comment provided by engineer. */ "Chat preferences" = "Предпочтения"; +/* No comment provided by engineer. */ +"Chat theme" = "Тема чата"; + /* No comment provided by engineer. */ "Chats" = "Чаты"; @@ -814,6 +912,15 @@ "Choose from library" = "Выбрать из библиотеки"; /* No comment provided by engineer. */ +"Chunks deleted" = "Блоков удалено"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Блоков принято"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Блоков загружено"; + +/* swipe action */ "Clear" = "Очистить"; /* No comment provided by engineer. */ @@ -829,10 +936,13 @@ "Clear verification" = "Сбросить подтверждение"; /* No comment provided by engineer. */ -"colored" = "цвет"; +"Color chats with the new themes." = "Добавьте цвета к чатам в настройках."; /* No comment provided by engineer. */ -"Colors" = "Цвета"; +"Color mode" = "Режим цветов"; + +/* No comment provided by engineer. */ +"colored" = "цвет"; /* server test step */ "Compare file" = "Сравнение файла"; @@ -843,15 +953,27 @@ /* No comment provided by engineer. */ "complete" = "соединение завершено"; +/* No comment provided by engineer. */ +"Completed" = "Готово"; + /* No comment provided by engineer. */ "Configure ICE servers" = "Настройка ICE серверов"; +/* No comment provided by engineer. */ +"Configured %@ servers" = "Настроенные %@ серверы"; + /* No comment provided by engineer. */ "Confirm" = "Подтвердить"; +/* No comment provided by engineer. */ +"Confirm contact deletion?" = "Потвердить удаление контакта?"; + /* No comment provided by engineer. */ "Confirm database upgrades" = "Подтвердить обновление базы данных"; +/* No comment provided by engineer. */ +"Confirm files from unknown servers." = "Подтверждать файлы с неизвестных серверов."; + /* No comment provided by engineer. */ "Confirm network settings" = "Подтвердите настройки сети"; @@ -885,6 +1007,9 @@ /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "соединитесь с разработчиками."; +/* No comment provided by engineer. */ +"Connect to your friends faster." = "Соединяйтесь с друзьями быстрее."; + /* No comment provided by engineer. */ "Connect to yourself?" = "Соединиться с самим собой?"; @@ -909,18 +1034,27 @@ /* No comment provided by engineer. */ "connected" = "соединение установлено"; +/* No comment provided by engineer. */ +"Connected" = "Соединено"; + /* No comment provided by engineer. */ "Connected desktop" = "Подключенный компьютер"; /* rcv group event chat item */ "connected directly" = "соединен(а) напрямую"; +/* No comment provided by engineer. */ +"Connected servers" = "Подключенные серверы"; + /* No comment provided by engineer. */ "Connected to desktop" = "Компьютер подключен"; /* No comment provided by engineer. */ "connecting" = "соединяется"; +/* No comment provided by engineer. */ +"Connecting" = "Соединяется"; + /* No comment provided by engineer. */ "connecting (accepted)" = "соединяется (приглашение принято)"; @@ -942,6 +1076,9 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "Устанавливается соединение с сервером… (ошибка: %@)"; +/* No comment provided by engineer. */ +"Connecting to contact, please wait or check later!" = "Контакт соединяется, подождите или проверьте позже!"; + /* No comment provided by engineer. */ "Connecting to desktop" = "Подключение к компьютеру"; @@ -951,6 +1088,9 @@ /* No comment provided by engineer. */ "Connection" = "Соединение"; +/* No comment provided by engineer. */ +"Connection and servers status." = "Состояние соединения и серверов."; + /* No comment provided by engineer. */ "Connection error" = "Ошибка соединения"; @@ -960,6 +1100,9 @@ /* chat list item title (it should not be shown */ "connection established" = "соединение установлено"; +/* No comment provided by engineer. */ +"Connection notifications" = "Уведомления по соединениям"; + /* No comment provided by engineer. */ "Connection request sent!" = "Запрос на соединение отправлен!"; @@ -969,9 +1112,15 @@ /* No comment provided by engineer. */ "Connection timeout" = "Превышено время соединения"; +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "Соединение с компьютером остановлено"; + /* connection information */ "connection:%@" = "connection:%@"; +/* No comment provided by engineer. */ +"Connections" = "Соединения"; + /* profile update event chat item */ "contact %@ changed to %@" = "контакт %1$@ изменён на %2$@"; @@ -981,6 +1130,9 @@ /* No comment provided by engineer. */ "Contact already exists" = "Существующий контакт"; +/* No comment provided by engineer. */ +"Contact deleted!" = "Контакт удален!"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "у контакта есть e2e шифрование"; @@ -994,7 +1146,7 @@ "Contact is connected" = "Соединение с контактом установлено"; /* No comment provided by engineer. */ -"Contact is not connected yet!" = "Соединение еще не установлено!"; +"Contact is deleted." = "Контакт удален."; /* No comment provided by engineer. */ "Contact name" = "Имена контактов"; @@ -1002,6 +1154,9 @@ /* No comment provided by engineer. */ "Contact preferences" = "Предпочтения контакта"; +/* No comment provided by engineer. */ +"Contact will be deleted - this cannot be undone!" = "Контакт будет удален — это нельзя отменить!"; + /* No comment provided by engineer. */ "Contacts" = "Контакты"; @@ -1011,9 +1166,15 @@ /* No comment provided by engineer. */ "Continue" = "Продолжить"; -/* chat item action */ +/* No comment provided by engineer. */ +"Conversation deleted!" = "Разговор удален!"; + +/* No comment provided by engineer. */ "Copy" = "Скопировать"; +/* No comment provided by engineer. */ +"Copy error" = "Ошибка копирования"; + /* No comment provided by engineer. */ "Core version: v%@" = "Версия ядра: v%@"; @@ -1059,6 +1220,9 @@ /* No comment provided by engineer. */ "Create your profile" = "Создать профиль"; +/* No comment provided by engineer. */ +"Created" = "Создано"; + /* No comment provided by engineer. */ "Created at" = "Создано"; @@ -1083,6 +1247,9 @@ /* No comment provided by engineer. */ "Current passphrase…" = "Текущий пароль…"; +/* No comment provided by engineer. */ +"Current profile" = "Текущий профиль"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "Максимальный размер файла - %@."; @@ -1092,9 +1259,15 @@ /* No comment provided by engineer. */ "Custom time" = "Пользовательское время"; +/* No comment provided by engineer. */ +"Customize theme" = "Настроить тему"; + /* No comment provided by engineer. */ "Dark" = "Тёмная"; +/* No comment provided by engineer. */ +"Dark mode colors" = "Цвета тёмного режима"; + /* No comment provided by engineer. */ "Database downgrade" = "Откат базы данных"; @@ -1155,12 +1328,18 @@ /* time unit */ "days" = "дней"; +/* No comment provided by engineer. */ +"Debug delivery" = "Отладка доставки"; + /* No comment provided by engineer. */ "Decentralized" = "Децентрализованный"; /* message decrypt error item */ "Decryption error" = "Ошибка расшифровки"; +/* No comment provided by engineer. */ +"decryption errors" = "ошибки расшифровки"; + /* pref value */ "default (%@)" = "по умолчанию (%@)"; @@ -1170,9 +1349,13 @@ /* No comment provided by engineer. */ "default (yes)" = "по умолчанию (да)"; -/* chat item action */ +/* chat item action + swipe action */ "Delete" = "Удалить"; +/* No comment provided by engineer. */ +"Delete %lld messages of members?" = "Удалить %lld сообщений членов группы?"; + /* No comment provided by engineer. */ "Delete %lld messages?" = "Удалить %lld сообщений?"; @@ -1210,10 +1393,7 @@ "Delete contact" = "Удалить контакт"; /* No comment provided by engineer. */ -"Delete Contact" = "Удалить контакт"; - -/* No comment provided by engineer. */ -"Delete contact?\nThis cannot be undone!" = "Удалить контакт?\nЭто не может быть отменено!"; +"Delete contact?" = "Удалить контакт?"; /* No comment provided by engineer. */ "Delete database" = "Удалить данные чата"; @@ -1269,9 +1449,6 @@ /* No comment provided by engineer. */ "Delete old database?" = "Удалить предыдущую версию данных?"; -/* No comment provided by engineer. */ -"Delete pending connection" = "Удалить соединение"; - /* No comment provided by engineer. */ "Delete pending connection?" = "Удалить ожидаемое соединение?"; @@ -1281,12 +1458,21 @@ /* server test step */ "Delete queue" = "Удаление очереди"; +/* No comment provided by engineer. */ +"Delete up to 20 messages at once." = "Удаляйте до 20 сообщений за раз."; + /* No comment provided by engineer. */ "Delete user profile?" = "Удалить профиль пользователя?"; +/* No comment provided by engineer. */ +"Delete without notification" = "Удалить без уведомления"; + /* deleted chat item */ "deleted" = "удалено"; +/* No comment provided by engineer. */ +"Deleted" = "Удалено"; + /* No comment provided by engineer. */ "Deleted at" = "Удалено"; @@ -1299,6 +1485,9 @@ /* rcv group event chat item */ "deleted group" = "удалил(а) группу"; +/* No comment provided by engineer. */ +"Deletion errors" = "Ошибки удаления"; + /* No comment provided by engineer. */ "Delivery" = "Доставка"; @@ -1320,9 +1509,27 @@ /* No comment provided by engineer. */ "Desktop devices" = "Компьютеры"; +/* No comment provided by engineer. */ +"Destination server address of %@ is incompatible with forwarding server %@ settings." = "Адрес сервера назначения %@ несовместим с настройками пересылающего сервера %@."; + +/* snd error text */ +"Destination server error: %@" = "Ошибка сервера получателя: %@"; + +/* No comment provided by engineer. */ +"Destination server version of %@ is incompatible with forwarding server %@." = "Версия сервера назначения %@ несовместима с пересылающим сервером %@."; + +/* No comment provided by engineer. */ +"Detailed statistics" = "Подробная статистика"; + +/* No comment provided by engineer. */ +"Details" = "Подробности"; + /* No comment provided by engineer. */ "Develop" = "Для разработчиков"; +/* No comment provided by engineer. */ +"Developer options" = "Опции разработчика"; + /* No comment provided by engineer. */ "Developer tools" = "Инструменты разработчика"; @@ -1362,6 +1569,9 @@ /* No comment provided by engineer. */ "disabled" = "выключено"; +/* No comment provided by engineer. */ +"Disabled" = "Выключено"; + /* No comment provided by engineer. */ "Disappearing message" = "Исчезающее сообщение"; @@ -1398,6 +1608,12 @@ /* No comment provided by engineer. */ "Do not send history to new members." = "Не отправлять историю новым членам."; +/* No comment provided by engineer. */ +"Do NOT send messages directly, even if your or destination server does not support private routing." = "Не отправлять сообщения напрямую, даже если сервер получателя не поддерживает конфиденциальную доставку."; + +/* No comment provided by engineer. */ +"Do NOT use private routing." = "Не использовать конфиденциальную доставку."; + /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "Не используйте SimpleX для экстренных звонков."; @@ -1416,12 +1632,21 @@ /* chat item action */ "Download" = "Загрузить"; +/* No comment provided by engineer. */ +"Download errors" = "Ошибки приема"; + /* No comment provided by engineer. */ "Download failed" = "Ошибка загрузки"; /* server test step */ "Download file" = "Загрузка файла"; +/* No comment provided by engineer. */ +"Downloaded" = "Принято"; + +/* No comment provided by engineer. */ +"Downloaded files" = "Принятые файлы"; + /* No comment provided by engineer. */ "Downloading archive" = "Загрузка архива"; @@ -1434,6 +1659,9 @@ /* integrity error chat item */ "duplicate message" = "повторное сообщение"; +/* No comment provided by engineer. */ +"duplicates" = "дубликаты"; + /* No comment provided by engineer. */ "Duration" = "Длительность"; @@ -1491,6 +1719,9 @@ /* enabled status */ "enabled" = "включено"; +/* No comment provided by engineer. */ +"Enabled" = "Включено"; + /* No comment provided by engineer. */ "Enabled for" = "Включено для"; @@ -1632,6 +1863,9 @@ /* No comment provided by engineer. */ "Error changing setting" = "Ошибка при изменении настройки"; +/* No comment provided by engineer. */ +"Error connecting to forwarding server %@. Please try later." = "Ошибка подключения к пересылающему серверу %@. Попробуйте позже."; + /* No comment provided by engineer. */ "Error creating address" = "Ошибка при создании адреса"; @@ -1662,9 +1896,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "Ошибка при удалении соединения"; -/* No comment provided by engineer. */ -"Error deleting contact" = "Ошибка при удалении контакта"; - /* No comment provided by engineer. */ "Error deleting database" = "Ошибка при удалении данных чата"; @@ -1692,6 +1923,9 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "Ошибка при экспорте архива чата"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Ошибка экспорта темы: %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "Ошибка при импорте архива чата"; @@ -1707,9 +1941,18 @@ /* No comment provided by engineer. */ "Error receiving file" = "Ошибка при получении файла"; +/* No comment provided by engineer. */ +"Error reconnecting server" = "Ошибка переподключения к серверу"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "Ошибка переподключения к серверам"; + /* No comment provided by engineer. */ "Error removing member" = "Ошибка при удалении члена группы"; +/* No comment provided by engineer. */ +"Error resetting statistics" = "Ошибка сброса статистики"; + /* No comment provided by engineer. */ "Error saving %@ servers" = "Ошибка при сохранении %@ серверов"; @@ -1779,7 +2022,8 @@ /* No comment provided by engineer. */ "Error: " = "Ошибка: "; -/* No comment provided by engineer. */ +/* file error text + snd error text */ "Error: %@" = "Ошибка: %@"; /* No comment provided by engineer. */ @@ -1788,6 +2032,9 @@ /* No comment provided by engineer. */ "Error: URL is invalid" = "Ошибка: неверная ссылка"; +/* No comment provided by engineer. */ +"Errors" = "Ошибки"; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Даже когда они выключены в разговоре."; @@ -1800,12 +2047,18 @@ /* chat item action */ "Expand" = "Раскрыть"; +/* No comment provided by engineer. */ +"expired" = "истекло"; + /* No comment provided by engineer. */ "Export database" = "Экспорт архива чата"; /* No comment provided by engineer. */ "Export error:" = "Ошибка при экспорте:"; +/* No comment provided by engineer. */ +"Export theme" = "Экспорт темы"; + /* No comment provided by engineer. */ "Exported database archive." = "Архив чата экспортирован."; @@ -1824,9 +2077,24 @@ /* No comment provided by engineer. */ "Faster joining and more reliable messages." = "Быстрое вступление и надежная доставка сообщений."; -/* No comment provided by engineer. */ +/* swipe action */ "Favorite" = "Избранный"; +/* No comment provided by engineer. */ +"File error" = "Ошибка файла"; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "Файл не найден - скорее всего, файл был удален или отменен."; + +/* file error text */ +"File server error: %@" = "Ошибка сервера файлов: %@"; + +/* No comment provided by engineer. */ +"File status" = "Статус файла"; + +/* copied message info */ +"File status: %@" = "Статус файла: %@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "Файл будет удалён с серверов."; @@ -1839,6 +2107,9 @@ /* No comment provided by engineer. */ "File: %@" = "Файл: %@"; +/* No comment provided by engineer. */ +"Files" = "Файлы"; + /* No comment provided by engineer. */ "Files & media" = "Файлы и медиа"; @@ -1905,6 +2176,21 @@ /* No comment provided by engineer. */ "Forwarded from" = "Переслано из"; +/* No comment provided by engineer. */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "Пересылающий сервер %@ не смог подключиться к серверу назначения %@. Попробуйте позже."; + +/* No comment provided by engineer. */ +"Forwarding server address is incompatible with network settings: %@." = "Адрес пересылающего сервера несовместим с настройками сети: %@."; + +/* No comment provided by engineer. */ +"Forwarding server version is incompatible with network settings: %@." = "Версия пересылающего сервера несовместима с настройками сети: %@."; + +/* snd error text */ +"Forwarding server: %@\nDestination server error: %@" = "Пересылающий сервер: %1$@\nОшибка сервера получателя: %2$@"; + +/* snd error text */ +"Forwarding server: %@\nError: %@" = "Пересылающий сервер: %1$@\nОшибка: %2$@"; + /* No comment provided by engineer. */ "Found desktop" = "Компьютер найден"; @@ -1932,6 +2218,12 @@ /* No comment provided by engineer. */ "GIFs and stickers" = "ГИФ файлы и стикеры"; +/* message preview */ +"Good afternoon!" = "Добрый день!"; + +/* message preview */ +"Good morning!" = "Доброе утро!"; + /* No comment provided by engineer. */ "Group" = "Группа"; @@ -2109,6 +2401,9 @@ /* No comment provided by engineer. */ "Import failed" = "Ошибка импорта"; +/* No comment provided by engineer. */ +"Import theme" = "Импорт темы"; + /* No comment provided by engineer. */ "Importing archive" = "Импорт архива"; @@ -2130,6 +2425,9 @@ /* No comment provided by engineer. */ "In-call sounds" = "Звуки во время звонков"; +/* No comment provided by engineer. */ +"inactive" = "неактивен"; + /* No comment provided by engineer. */ "Incognito" = "Инкогнито"; @@ -2193,6 +2491,9 @@ /* No comment provided by engineer. */ "Interface" = "Интерфейс"; +/* No comment provided by engineer. */ +"Interface colors" = "Цвета интерфейса"; + /* invalid chat data */ "invalid chat" = "ошибка чата"; @@ -2235,6 +2536,9 @@ /* group name */ "invitation to group %@" = "приглашение в группу %@"; +/* No comment provided by engineer. */ +"invite" = "пригласить"; + /* No comment provided by engineer. */ "Invite friends" = "Пригласить друзей"; @@ -2280,6 +2584,9 @@ /* No comment provided by engineer. */ "It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Это может произойти, когда:\n1. Клиент отправителя удалил неотправленные сообщения через 2 дня, или сервер – через 30 дней.\n2. Расшифровка сообщения была невозможна, когда Вы или Ваш контакт использовали старую копию базы данных.\n3. Соединение компроментировано."; +/* No comment provided by engineer. */ +"It protects your IP address and connections." = "Защищает ваш IP адрес и соединения."; + /* No comment provided by engineer. */ "It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Возможно, Вы уже соединились через эту ссылку. Если это не так, то это ошибка (%@)."; @@ -2292,7 +2599,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Японский интерфейс"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Вступить"; /* No comment provided by engineer. */ @@ -2322,6 +2629,9 @@ /* No comment provided by engineer. */ "Keep" = "Оставить"; +/* No comment provided by engineer. */ +"Keep conversation" = "Оставить разговор"; + /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Оставьте приложение открытым, чтобы использовать его с компьютера"; @@ -2343,7 +2653,7 @@ /* No comment provided by engineer. */ "Learn more" = "Узнать больше"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "Выйти"; /* No comment provided by engineer. */ @@ -2433,6 +2743,12 @@ /* No comment provided by engineer. */ "Max 30 seconds, received instantly." = "Макс. 30 секунд, доставляются мгновенно."; +/* No comment provided by engineer. */ +"Media & file servers" = "Серверы файлов и медиа"; + +/* blur media */ +"Medium" = "Среднее"; + /* member role */ "member" = "член группы"; @@ -2445,6 +2761,9 @@ /* rcv group event chat item */ "member connected" = "соединен(а)"; +/* item status text */ +"Member inactive" = "Член неактивен"; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "Роль члена группы будет изменена на \"%@\". Все члены группы получат сообщение."; @@ -2454,15 +2773,33 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Член группы будет удален - это действие нельзя отменить!"; +/* No comment provided by engineer. */ +"Menus" = "Меню"; + +/* No comment provided by engineer. */ +"message" = "написать"; + /* item status text */ "Message delivery error" = "Ошибка доставки сообщения"; /* No comment provided by engineer. */ "Message delivery receipts!" = "Отчеты о доставке сообщений!"; +/* item status text */ +"Message delivery warning" = "Предупреждение доставки сообщения"; + /* No comment provided by engineer. */ "Message draft" = "Черновик сообщения"; +/* item status text */ +"Message forwarded" = "Сообщение переслано"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "Сообщение может быть доставлено позже, если член группы станет активным."; + +/* No comment provided by engineer. */ +"Message queue info" = "Информация об очереди сообщений"; + /* chat feature */ "Message reactions" = "Реакции на сообщения"; @@ -2475,9 +2812,21 @@ /* notification */ "message received" = "получено сообщение"; +/* No comment provided by engineer. */ +"Message reception" = "Прием сообщений"; + +/* No comment provided by engineer. */ +"Message servers" = "Серверы сообщений"; + /* No comment provided by engineer. */ "Message source remains private." = "Источник сообщения остаётся конфиденциальным."; +/* No comment provided by engineer. */ +"Message status" = "Статус сообщения"; + +/* copied message info */ +"Message status: %@" = "Статус сообщения: %@"; + /* No comment provided by engineer. */ "Message text" = "Текст сообщения"; @@ -2493,6 +2842,12 @@ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "Сообщения от %@ будут показаны!"; +/* No comment provided by engineer. */ +"Messages received" = "Получено сообщений"; + +/* No comment provided by engineer. */ +"Messages sent" = "Сообщений отправлено"; + /* No comment provided by engineer. */ "Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Сообщения, файлы и звонки защищены **end-to-end шифрованием** с прямой секретностью (PFS), правдоподобным отрицанием и восстановлением от взлома."; @@ -2568,19 +2923,19 @@ /* item status description */ "Most likely this connection is deleted." = "Скорее всего, соединение удалено."; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Скорее всего, этот контакт удалил соединение с Вами."; - /* No comment provided by engineer. */ "Multiple chat profiles" = "Много профилей чата"; /* No comment provided by engineer. */ +"mute" = "без звука"; + +/* swipe action */ "Mute" = "Без звука"; /* No comment provided by engineer. */ "Muted when inactive!" = "Без звука, когда не активный!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "Имя"; /* No comment provided by engineer. */ @@ -2589,6 +2944,9 @@ /* No comment provided by engineer. */ "Network connection" = "Интернет-соединение"; +/* snd error text */ +"Network issues - message expired after many attempts to send it." = "Ошибка сети - сообщение не было отправлено после многократных попыток."; + /* No comment provided by engineer. */ "Network management" = "Статус сети"; @@ -2604,6 +2962,9 @@ /* No comment provided by engineer. */ "New chat" = "Новый чат"; +/* No comment provided by engineer. */ +"New chat experience 🎉" = "Новый интерфейс 🎉"; + /* notification */ "New contact request" = "Новый запрос на соединение"; @@ -2622,6 +2983,9 @@ /* No comment provided by engineer. */ "New in %@" = "Новое в %@"; +/* No comment provided by engineer. */ +"New media options" = "Новые медиа-опции"; + /* No comment provided by engineer. */ "New member role" = "Роль члена группы"; @@ -2658,6 +3022,9 @@ /* No comment provided by engineer. */ "No device token!" = "Отсутствует токен устройства!"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Прямого соединения пока нет, сообщение переслано или будет переслано админом."; + /* No comment provided by engineer. */ "no e2e encryption" = "нет e2e шифрования"; @@ -2670,6 +3037,9 @@ /* No comment provided by engineer. */ "No history" = "Нет истории"; +/* No comment provided by engineer. */ +"No info, try to reload" = "Нет информации, попробуйте перезагрузить"; + /* No comment provided by engineer. */ "No network connection" = "Нет интернет-соединения"; @@ -2685,6 +3055,9 @@ /* No comment provided by engineer. */ "Not compatible!" = "Несовместимая версия!"; +/* No comment provided by engineer. */ +"Nothing selected" = "Ничего не выбрано"; + /* No comment provided by engineer. */ "Notifications" = "Уведомления"; @@ -2702,7 +3075,7 @@ time to disappear */ "off" = "нет"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Выключено"; /* feature offered item */ @@ -2730,10 +3103,10 @@ "One-time invitation link" = "Одноразовая ссылка"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "Подключаться только к onion хостам. Требуется включенный VPN."; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Подключаться только к **onion** хостам.\nТребуется совместимый VPN."; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "Onion хосты используются, если возможно. Требуется включенный VPN."; +"Onion hosts will be used when available.\nRequires compatible VPN." = "Onion хосты используются, если возможно.\nТребуется совместимый VPN."; /* No comment provided by engineer. */ "Onion hosts will not be used." = "Onion хосты не используются."; @@ -2741,6 +3114,9 @@ /* No comment provided by engineer. */ "Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Только пользовательские устройства хранят контакты, группы и сообщения, которые отправляются **с двухуровневым end-to-end шифрованием**."; +/* No comment provided by engineer. */ +"Only delete conversation" = "Удалить только разговор"; + /* No comment provided by engineer. */ "Only group owners can change group preferences." = "Только владельцы группы могут изменять предпочтения группы."; @@ -2795,6 +3171,9 @@ /* authentication reason */ "Open migration to another device" = "Открытие миграции на другое устройство"; +/* No comment provided by engineer. */ +"Open server settings" = "Открыть настройки серверов"; + /* No comment provided by engineer. */ "Open Settings" = "Открыть Настройки"; @@ -2819,9 +3198,18 @@ /* No comment provided by engineer. */ "Or show this code" = "Или покажите этот код"; +/* No comment provided by engineer. */ +"other" = "другое"; + /* No comment provided by engineer. */ "Other" = "Другaя сеть"; +/* No comment provided by engineer. */ +"Other %@ servers" = "Другие %@ серверы"; + +/* No comment provided by engineer. */ +"other errors" = "другие ошибки"; + /* member role */ "owner" = "владелец"; @@ -2864,6 +3252,9 @@ /* No comment provided by engineer. */ "peer-to-peer" = "peer-to-peer"; +/* No comment provided by engineer. */ +"Pending" = "В ожидании"; + /* No comment provided by engineer. */ "People can connect to you only via the links you share." = "С Вами можно соединиться только через созданные Вами ссылки."; @@ -2882,9 +3273,18 @@ /* No comment provided by engineer. */ "PING interval" = "Интервал PING"; +/* No comment provided by engineer. */ +"Play from the chat list." = "Открыть из списка чатов."; + +/* No comment provided by engineer. */ +"Please ask your contact to enable calls." = "Попросите Вашего контакта разрешить звонки."; + /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "Попросите у Вашего контакта разрешить отправку голосовых сообщений."; +/* No comment provided by engineer. */ +"Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.\nPlease share any other issues with the developers." = "Пожалуйста, проверьте, что мобильный и компьютер находятся в одной и той же локальной сети, и что брандмауэр компьютера разрешает подключение.\nПожалуйста, поделитесь любыми другими ошибками с разработчиками."; + /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "Пожалуйста, проверьте, что Вы использовали правильную ссылку или попросите, чтобы Ваш контакт отправил Вам другую ссылку."; @@ -2942,6 +3342,9 @@ /* No comment provided by engineer. */ "Preview" = "Просмотр"; +/* No comment provided by engineer. */ +"Previously connected servers" = "Ранее подключенные серверы"; + /* No comment provided by engineer. */ "Privacy & security" = "Конфиденциальность"; @@ -2951,9 +3354,21 @@ /* No comment provided by engineer. */ "Private filenames" = "Защищенные имена файлов"; +/* No comment provided by engineer. */ +"Private message routing" = "Конфиденциальная доставка сообщений"; + +/* No comment provided by engineer. */ +"Private message routing 🚀" = "Конфиденциальная доставка 🚀"; + /* name of notes to self */ "Private notes" = "Личные заметки"; +/* No comment provided by engineer. */ +"Private routing" = "Конфиденциальная доставка"; + +/* No comment provided by engineer. */ +"Private routing error" = "Ошибка конфиденциальной доставки"; + /* No comment provided by engineer. */ "Profile and server connections" = "Профиль и соединения на сервере"; @@ -2972,6 +3387,9 @@ /* No comment provided by engineer. */ "Profile password" = "Пароль профиля"; +/* No comment provided by engineer. */ +"Profile theme" = "Тема профиля"; + /* No comment provided by engineer. */ "Profile update will be sent to your contacts." = "Обновлённый профиль будет отправлен Вашим контактам."; @@ -3005,15 +3423,27 @@ /* No comment provided by engineer. */ "Protect app screen" = "Защитить экран приложения"; +/* No comment provided by engineer. */ +"Protect IP address" = "Защитить IP адрес"; + /* No comment provided by engineer. */ "Protect your chat profiles with a password!" = "Защитите Ваши профили чата паролем!"; +/* No comment provided by engineer. */ +"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Защитите ваш IP адрес от серверов сообщений, выбранных Вашими контактами.\nВключите в настройках *Сеть и серверы*."; + /* No comment provided by engineer. */ "Protocol timeout" = "Таймаут протокола"; /* No comment provided by engineer. */ "Protocol timeout per KB" = "Таймаут протокола на KB"; +/* No comment provided by engineer. */ +"Proxied" = "Проксировано"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Проксированные серверы"; + /* No comment provided by engineer. */ "Push notifications" = "Доставка уведомлений"; @@ -3029,10 +3459,13 @@ /* No comment provided by engineer. */ "Rate the app" = "Оценить приложение"; +/* No comment provided by engineer. */ +"Reachable chat toolbar" = "Доступная панель чата"; + /* chat item menu */ "React…" = "Реакция…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "Прочитано"; /* No comment provided by engineer. */ @@ -3056,6 +3489,9 @@ /* No comment provided by engineer. */ "Receipts are disabled" = "Отчёты о доставке выключены"; +/* No comment provided by engineer. */ +"Receive errors" = "Ошибки приема"; + /* No comment provided by engineer. */ "received answer…" = "получен ответ…"; @@ -3075,10 +3511,16 @@ "Received message" = "Полученное сообщение"; /* No comment provided by engineer. */ -"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Адрес получения сообщений будет перемещён на другой сервер. Изменение адреса завершится после того как отправитель будет онлайн."; +"Received messages" = "Полученные сообщения"; /* No comment provided by engineer. */ -"Receiving concurrency" = "Одновременный приём"; +"Received reply" = "Полученный ответ"; + +/* No comment provided by engineer. */ +"Received total" = "Всего получено"; + +/* No comment provided by engineer. */ +"Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Адрес получения сообщений будет перемещён на другой сервер. Изменение адреса завершится после того как отправитель будет онлайн."; /* No comment provided by engineer. */ "Receiving file will be stopped." = "Приём файла будет прекращён."; @@ -3095,9 +3537,24 @@ /* No comment provided by engineer. */ "Recipients see updates as you type them." = "Получатели видят их в то время как Вы их набираете."; +/* No comment provided by engineer. */ +"Reconnect" = "Переподключить"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Повторно подключите все серверы, чтобы принудительно доставить сообщения. Используется дополнительный трафик."; +/* No comment provided by engineer. */ +"Reconnect all servers" = "Переподключить все серверы"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "Переподключить все серверы?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "Переподключить сервер для устранения неполадок доставки сообщений. Это использует дополнительный трафик."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "Переподключить сервер?"; + /* No comment provided by engineer. */ "Reconnect servers?" = "Переподключить серверы?"; @@ -3110,7 +3567,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Уменьшенное потребление батареи"; -/* reject incoming call via notification */ +/* reject incoming call via notification + swipe action */ "Reject" = "Отклонить"; /* No comment provided by engineer. */ @@ -3131,6 +3589,9 @@ /* No comment provided by engineer. */ "Remove" = "Удалить"; +/* No comment provided by engineer. */ +"Remove image" = "Удалить изображение"; + /* No comment provided by engineer. */ "Remove member" = "Удалить члена группы"; @@ -3188,12 +3649,27 @@ /* No comment provided by engineer. */ "Reset" = "Сбросить"; +/* No comment provided by engineer. */ +"Reset all hints" = "Сбросить все подсказки"; + +/* No comment provided by engineer. */ +"Reset all statistics" = "Сбросить всю статистику"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "Сбросить всю статистику?"; + /* No comment provided by engineer. */ "Reset colors" = "Сбросить цвета"; +/* No comment provided by engineer. */ +"Reset to app theme" = "Сбросить на тему приложения"; + /* No comment provided by engineer. */ "Reset to defaults" = "Сбросить настройки"; +/* No comment provided by engineer. */ +"Reset to user theme" = "Сбросить на тему пользователя"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Перезапустите приложение, чтобы создать новый профиль."; @@ -3218,9 +3694,6 @@ /* chat item action */ "Reveal" = "Показать"; -/* No comment provided by engineer. */ -"Revert" = "Отменить изменения"; - /* No comment provided by engineer. */ "Revoke" = "Отозвать"; @@ -3236,6 +3709,9 @@ /* No comment provided by engineer. */ "Run chat" = "Запустить chat"; +/* No comment provided by engineer. */ +"Safely receive files" = "Получайте файлы безопасно"; + /* No comment provided by engineer. */ "Safer groups" = "Более безопасные группы"; @@ -3251,6 +3727,9 @@ /* No comment provided by engineer. */ "Save and notify group members" = "Сохранить и уведомить членов группы"; +/* No comment provided by engineer. */ +"Save and reconnect" = "Сохранить и переподключиться"; + /* No comment provided by engineer. */ "Save and update group profile" = "Сохранить сообщение и обновить группу"; @@ -3305,6 +3784,12 @@ /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "Сохраненные WebRTC ICE серверы будут удалены"; +/* No comment provided by engineer. */ +"Scale" = "Масштаб"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Сканировать / Вставить ссылку"; + /* No comment provided by engineer. */ "Scan code" = "Сканировать код"; @@ -3320,6 +3805,9 @@ /* No comment provided by engineer. */ "Scan server QR code" = "Сканировать QR код сервера"; +/* No comment provided by engineer. */ +"search" = "поиск"; + /* No comment provided by engineer. */ "Search" = "Поиск"; @@ -3332,6 +3820,9 @@ /* network option */ "sec" = "сек"; +/* No comment provided by engineer. */ +"Secondary" = "Вторичный"; + /* time unit */ "seconds" = "секунд"; @@ -3341,6 +3832,9 @@ /* server test step */ "Secure queue" = "Защита очереди"; +/* No comment provided by engineer. */ +"Secured" = "Защищено"; + /* No comment provided by engineer. */ "Security assessment" = "Аудит безопасности"; @@ -3350,9 +3844,15 @@ /* chat item text */ "security code changed" = "код безопасности изменился"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "Выбрать"; +/* No comment provided by engineer. */ +"Selected %lld" = "Выбрано %lld"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Выбранные настройки чата запрещают это сообщение."; + /* No comment provided by engineer. */ "Self-destruct" = "Самоуничтожение"; @@ -3377,21 +3877,30 @@ /* No comment provided by engineer. */ "send direct message" = "отправьте сообщение"; -/* No comment provided by engineer. */ -"Send direct message" = "Отправить сообщение"; - /* No comment provided by engineer. */ "Send direct message to connect" = "Отправьте сообщение чтобы соединиться"; /* No comment provided by engineer. */ "Send disappearing message" = "Отправить исчезающее сообщение"; +/* No comment provided by engineer. */ +"Send errors" = "Ошибки отправки"; + /* No comment provided by engineer. */ "Send link previews" = "Отправлять картинки ссылок"; /* No comment provided by engineer. */ "Send live message" = "Отправить живое сообщение"; +/* No comment provided by engineer. */ +"Send message to enable calls." = "Отправьте сообщение, чтобы включить звонки."; + +/* No comment provided by engineer. */ +"Send messages directly when IP address is protected and your or destination server does not support private routing." = "Отправлять сообщения напрямую, когда IP адрес защищен, и Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку."; + +/* No comment provided by engineer. */ +"Send messages directly when your or destination server does not support private routing." = "Отправлять сообщения напрямую, когда Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку."; + /* No comment provided by engineer. */ "Send notifications" = "Отправлять уведомления"; @@ -3446,15 +3955,42 @@ /* copied message info */ "Sent at: %@" = "Отправлено: %@"; +/* No comment provided by engineer. */ +"Sent directly" = "Отправлено напрямую"; + /* notification */ "Sent file event" = "Отправка файла"; /* message info title */ "Sent message" = "Отправленное сообщение"; +/* No comment provided by engineer. */ +"Sent messages" = "Отправленные сообщения"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "Отправленные сообщения будут удалены через заданное время."; +/* No comment provided by engineer. */ +"Sent reply" = "Отправленный ответ"; + +/* No comment provided by engineer. */ +"Sent total" = "Всего отправлено"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Отправлено через прокси"; + +/* No comment provided by engineer. */ +"Server address" = "Адрес сервера"; + +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "Адрес сервера несовместим с сетевыми настройками: %@."; + +/* srv error text. */ +"Server address is incompatible with network settings." = "Адрес сервера несовместим с настройками сети."; + +/* queue info */ +"server queue info: %@\n\nlast received msg: %@" = "информация сервера об очереди: %1$@\n\nпоследнее полученное сообщение: %2$@"; + /* server test error */ "Server requires authorization to create queues, check password" = "Сервер требует авторизации для создания очередей, проверьте пароль"; @@ -3464,9 +4000,24 @@ /* No comment provided by engineer. */ "Server test failed!" = "Ошибка теста сервера!"; +/* No comment provided by engineer. */ +"Server type" = "Тип сервера"; + +/* srv error text */ +"Server version is incompatible with network settings." = "Версия сервера несовместима с настройками сети."; + +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "Версия сервера несовместима с вашим приложением: %@."; + /* No comment provided by engineer. */ "Servers" = "Серверы"; +/* No comment provided by engineer. */ +"Servers info" = "Информация о серверах"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "Статистика серверов будет сброшена - это нельзя отменить!"; + /* No comment provided by engineer. */ "Session code" = "Код сессии"; @@ -3476,6 +4027,9 @@ /* No comment provided by engineer. */ "Set contact name…" = "Имя контакта…"; +/* No comment provided by engineer. */ +"Set default theme" = "Установить тему по умолчанию"; + /* No comment provided by engineer. */ "Set group preferences" = "Предпочтения группы"; @@ -3521,15 +4075,24 @@ /* No comment provided by engineer. */ "Share address with contacts?" = "Поделиться адресом с контактами?"; +/* No comment provided by engineer. */ +"Share from other apps." = "Поделитесь из других приложений."; + /* No comment provided by engineer. */ "Share link" = "Поделиться ссылкой"; /* No comment provided by engineer. */ "Share this 1-time invite link" = "Поделиться одноразовой ссылкой-приглашением"; +/* No comment provided by engineer. */ +"Share to SimpleX" = "Поделиться в SimpleX"; + /* No comment provided by engineer. */ "Share with contacts" = "Поделиться с контактами"; +/* No comment provided by engineer. */ +"Show → on messages sent via private routing." = "Показать → на сообщениях доставленных конфиденциально."; + /* No comment provided by engineer. */ "Show calls in phone history" = "Показать звонки в истории телефона"; @@ -3539,6 +4102,12 @@ /* No comment provided by engineer. */ "Show last messages" = "Показывать последние сообщения"; +/* No comment provided by engineer. */ +"Show message status" = "Показать статус сообщения"; + +/* No comment provided by engineer. */ +"Show percentage" = "Показать процент"; + /* No comment provided by engineer. */ "Show preview" = "Показывать уведомления"; @@ -3548,6 +4117,9 @@ /* No comment provided by engineer. */ "Show:" = "Показать:"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "Адрес SimpleX"; @@ -3593,6 +4165,9 @@ /* No comment provided by engineer. */ "Simplified incognito mode" = "Упрощенный режим Инкогнито"; +/* No comment provided by engineer. */ +"Size" = "Размер"; + /* No comment provided by engineer. */ "Skip" = "Пропустить"; @@ -3603,11 +4178,20 @@ "Small groups (max 20)" = "Маленькие группы (до 20)"; /* No comment provided by engineer. */ -"SMP servers" = "SMP серверы"; +"SMP server" = "SMP сервер"; + +/* blur media */ +"Soft" = "Слабое"; + +/* No comment provided by engineer. */ +"Some file(s) were not exported:" = "Некоторые файл(ы) не были экспортированы:"; /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "Во время импорта произошли некоторые ошибки - для получения более подробной информации вы можете обратиться к консоли."; +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import:" = "Во время импорта произошли некоторые ошибки:"; + /* notification title */ "Somebody" = "Контакт"; @@ -3626,9 +4210,15 @@ /* No comment provided by engineer. */ "Start migration" = "Запустить перемещение данных"; +/* No comment provided by engineer. */ +"Starting from %@." = "Начиная с %@."; + /* No comment provided by engineer. */ "starting…" = "инициализация…"; +/* No comment provided by engineer. */ +"Statistics" = "Статистика"; + /* No comment provided by engineer. */ "Stop" = "Остановить"; @@ -3668,9 +4258,21 @@ /* No comment provided by engineer. */ "strike" = "зачеркнуть"; +/* blur media */ +"Strong" = "Сильное"; + /* No comment provided by engineer. */ "Submit" = "Продолжить"; +/* No comment provided by engineer. */ +"Subscribed" = "Подписано"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Ошибки подписки"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Подписок игнорировано"; + /* No comment provided by engineer. */ "Support SimpleX Chat" = "Поддержать SimpleX Chat"; @@ -3705,7 +4307,7 @@ "Tap to scan" = "Нажмите, чтобы сканировать"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Нажмите, чтобы начать чат"; +"TCP connection" = "TCP-соединение"; /* No comment provided by engineer. */ "TCP connection timeout" = "Таймаут TCP соединения"; @@ -3719,6 +4321,9 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* No comment provided by engineer. */ +"Temporary file error" = "Временная ошибка файла"; + /* server test failure */ "Test failed at step %@." = "Ошибка теста на шаге %@."; @@ -3746,6 +4351,9 @@ /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Приложение может посылать Вам уведомления о сообщениях и запросах на соединение - уведомления можно включить в Настройках."; +/* No comment provided by engineer. */ +"The app will ask to confirm downloads from unknown file servers (except .onion)." = "Приложение будет запрашивать подтверждение загрузки с неизвестных серверов (за исключением .onion адресов)."; + /* No comment provided by engineer. */ "The attempt to change database passphrase was not completed." = "Попытка поменять пароль базы данных не была завершена."; @@ -3776,6 +4384,12 @@ /* No comment provided by engineer. */ "The message will be marked as moderated for all members." = "Сообщение будет помечено как удаленное для всех членов группы."; +/* No comment provided by engineer. */ +"The messages will be deleted for all members." = "Сообщения будут удалены для всех членов группы."; + +/* No comment provided by engineer. */ +"The messages will be marked as moderated for all members." = "Сообщения будут помечены как удаленные для всех членов группы."; + /* No comment provided by engineer. */ "The next generation of private messaging" = "Новое поколение приватных сообщений"; @@ -3798,7 +4412,7 @@ "The text you pasted is not a SimpleX link." = "Вставленный текст не является SimpleX-ссылкой."; /* No comment provided by engineer. */ -"Theme" = "Тема"; +"Themes" = "Темы"; /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Установки для Вашего активного профиля **%@**."; @@ -3842,9 +4456,15 @@ /* No comment provided by engineer. */ "This is your own SimpleX address!" = "Это ваш собственный адрес SimpleX!"; +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "Эта ссылка была использована на другом мобильном, пожалуйста, создайте новую ссылку на компьютере."; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Эта настройка применяется к сообщениям в Вашем текущем профиле чата **%@**."; +/* No comment provided by engineer. */ +"Title" = "Заголовок"; + /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Чтобы задать вопросы и получать уведомления о новых версиях,"; @@ -3866,6 +4486,9 @@ /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Чтобы защитить Вашу информацию, включите блокировку SimpleX Chat.\nВам будет нужно пройти аутентификацию для включения блокировки."; +/* No comment provided by engineer. */ +"To protect your IP address, private routing uses your SMP servers to deliver messages." = "Чтобы защитить ваш IP адрес, приложение использует Ваши SMP серверы для конфиденциальной доставки сообщений."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Для записи голосового сообщения, пожалуйста разрешите доступ к микрофону."; @@ -3878,12 +4501,24 @@ /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Чтобы подтвердить end-to-end шифрование с Вашим контактом сравните (или сканируйте) код безопасности на Ваших устройствах."; +/* No comment provided by engineer. */ +"Toggle chat list:" = "Переключите список чатов:"; + /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Установите режим Инкогнито при соединении."; +/* No comment provided by engineer. */ +"Toolbar opacity" = "Прозрачность тулбара"; + +/* No comment provided by engineer. */ +"Total" = "Всего"; + /* No comment provided by engineer. */ "Transport isolation" = "Отдельные сессии для"; +/* No comment provided by engineer. */ +"Transport sessions" = "Транспортные сессии"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Устанавливается соединение с сервером, через который Вы получаете сообщения от этого контакта (ошибка: %@)."; @@ -3920,13 +4555,10 @@ /* rcv group event chat item */ "unblocked %@" = "%@ разблокирован"; -/* item status description */ -"Unexpected error: %@" = "Неожиданная ошибка: %@"; - /* No comment provided by engineer. */ "Unexpected migration state" = "Неожиданная ошибка при перемещении данных чата"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "Не избр."; /* No comment provided by engineer. */ @@ -3953,6 +4585,12 @@ /* No comment provided by engineer. */ "Unknown error" = "Неизвестная ошибка"; +/* No comment provided by engineer. */ +"unknown servers" = "неизвестные серверы"; + +/* No comment provided by engineer. */ +"Unknown servers!" = "Неизвестные серверы!"; + /* No comment provided by engineer. */ "unknown status" = "неизвестный статус"; @@ -3975,9 +4613,15 @@ "Unlock app" = "Разблокировать"; /* No comment provided by engineer. */ +"unmute" = "уведомлять"; + +/* swipe action */ "Unmute" = "Уведомлять"; /* No comment provided by engineer. */ +"unprotected" = "незащищённый"; + +/* swipe action */ "Unread" = "Не прочитано"; /* No comment provided by engineer. */ @@ -3986,9 +4630,6 @@ /* No comment provided by engineer. */ "Update" = "Обновить"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = "Обновить настройки .onion хостов?"; - /* No comment provided by engineer. */ "Update database passphrase" = "Поменять пароль"; @@ -3996,7 +4637,7 @@ "Update network settings?" = "Обновить настройки сети?"; /* No comment provided by engineer. */ -"Update transport isolation mode?" = "Обновить режим отдельных сессий?"; +"Update settings?" = "Обновить настройки?"; /* rcv group event chat item */ "updated group profile" = "обновил(а) профиль группы"; @@ -4008,10 +4649,10 @@ "Updating settings will re-connect the client to all servers." = "Обновление настроек приведет к сбросу и установке нового соединения со всеми серверами."; /* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "Обновление этих настроек приведет к сбросу и установке нового соединения со всеми серверами."; +"Upgrade and open chat" = "Обновить и открыть чат"; /* No comment provided by engineer. */ -"Upgrade and open chat" = "Обновить и открыть чат"; +"Upload errors" = "Ошибки загрузки"; /* No comment provided by engineer. */ "Upload failed" = "Ошибка загрузки"; @@ -4019,6 +4660,12 @@ /* server test step */ "Upload file" = "Загрузка файла"; +/* No comment provided by engineer. */ +"Uploaded" = "Загружено"; + +/* No comment provided by engineer. */ +"Uploaded files" = "Отправленные файлы"; + /* No comment provided by engineer. */ "Uploading archive" = "Загрузка архива"; @@ -4046,6 +4693,12 @@ /* No comment provided by engineer. */ "Use only local notifications?" = "Использовать только локальные нотификации?"; +/* No comment provided by engineer. */ +"Use private routing with unknown servers when IP address is not protected." = "Использовать конфиденциальную доставку с неизвестными серверами, когда IP адрес не защищен."; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers." = "Использовать конфиденциальную доставку с неизвестными серверами."; + /* No comment provided by engineer. */ "Use server" = "Использовать сервер"; @@ -4055,11 +4708,14 @@ /* No comment provided by engineer. */ "Use the app while in the call." = "Используйте приложение во время звонка."; +/* No comment provided by engineer. */ +"Use the app with one hand." = "Используйте приложение одной рукой."; + /* No comment provided by engineer. */ "User profile" = "Профиль чата"; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "Для использования .onion хостов требуется совместимый VPN провайдер."; +"User selection" = "Выбор пользователя"; /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "Используются серверы, предоставленные SimpleX Chat."; @@ -4109,6 +4765,9 @@ /* No comment provided by engineer. */ "Via secure quantum resistant protocol." = "Через безопасный квантово-устойчивый протокол."; +/* No comment provided by engineer. */ +"video" = "видеозвонок"; + /* No comment provided by engineer. */ "Video call" = "Видеозвонок"; @@ -4166,6 +4825,12 @@ /* No comment provided by engineer. */ "Waiting for video" = "Ожидание видео"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "Рисунок обоев"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Фон обоев"; + /* No comment provided by engineer. */ "wants to connect to you!" = "хочет соединиться с Вами!"; @@ -4199,6 +4864,9 @@ /* No comment provided by engineer. */ "When connecting audio and video calls." = "Во время соединения аудио и видео звонков."; +/* No comment provided by engineer. */ +"when IP hidden" = "когда IP защищен"; + /* No comment provided by engineer. */ "When people request to connect, you can accept or reject it." = "Когда Вы получите запрос на соединение, Вы можете принять или отклонить его."; @@ -4223,14 +4891,26 @@ /* No comment provided by engineer. */ "With reduced battery usage." = "С уменьшенным потреблением батареи."; +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to file servers." = "Без Тора или ВПН, Ваш IP адрес будет доступен серверам файлов."; + +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Без Тора или ВПН, Ваш IP адрес будет доступен этим серверам файлов: %@."; + /* No comment provided by engineer. */ "Wrong database passphrase" = "Неправильный пароль базы данных"; +/* snd error text */ +"Wrong key or unknown connection - most likely this connection is deleted." = "Неверный ключ или неизвестное соединение - скорее всего, это соединение удалено."; + +/* file error text */ +"Wrong key or unknown file chunk address - most likely file is deleted." = "Неверный ключ или неизвестный адрес блока файла - скорее всего, файл удален."; + /* No comment provided by engineer. */ "Wrong passphrase!" = "Неправильный пароль!"; /* No comment provided by engineer. */ -"XFTP servers" = "XFTP серверы"; +"XFTP server" = "XFTP сервер"; /* pref value */ "yes" = "да"; @@ -4286,6 +4966,9 @@ /* No comment provided by engineer. */ "You are invited to group" = "Вы приглашены в группу"; +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "Вы не подключены к этим серверам. Для доставки сообщений на них используется конфиденциальная доставка."; + /* No comment provided by engineer. */ "you are observer" = "только чтение сообщений"; @@ -4295,6 +4978,9 @@ /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "Вы можете принимать звонки на экране блокировки, без аутентификации."; +/* No comment provided by engineer. */ +"You can change it in Appearance settings." = "Вы можете изменить это в настройках Интерфейса."; + /* No comment provided by engineer. */ "You can create it later" = "Вы можете создать его позже"; @@ -4314,7 +5000,10 @@ "You can make it visible to your SimpleX contacts via Settings." = "Вы можете сделать его видимым для ваших контактов в SimpleX через Настройки."; /* notification body */ -"You can now send messages to %@" = "Вы теперь можете отправлять сообщения %@"; +"You can now chat with %@" = "Вы теперь можете общаться с %@"; + +/* No comment provided by engineer. */ +"You can send messages to %@ from Archived contacts." = "Вы можете отправлять сообщения %@ из Архивированных контактов."; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Вы можете установить просмотр уведомлений на экране блокировки в настройках."; @@ -4331,6 +5020,9 @@ /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "Вы можете запустить чат через Настройки приложения или перезапустив приложение."; +/* No comment provided by engineer. */ +"You can still view conversation with %@ in the list of chats." = "Вы по-прежнему можете просмотреть разговор с %@ в списке чатов."; + /* No comment provided by engineer. */ "You can turn on SimpleX Lock via Settings." = "Вы можете включить Блокировку SimpleX через Настройки."; @@ -4367,9 +5059,6 @@ /* No comment provided by engineer. */ "You have already requested connection!\nRepeat connection request?" = "Вы уже запросили соединение!\nПовторить запрос?"; -/* No comment provided by engineer. */ -"You have no chats" = "У Вас нет чатов"; - /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "Пароль не сохранен на устройстве — Вы будете должны ввести его при каждом запуске чата."; @@ -4385,9 +5074,18 @@ /* snd group event chat item */ "you left" = "Вы покинули группу"; +/* No comment provided by engineer. */ +"You may migrate the exported database." = "Вы можете мигрировать экспортированную базу данных."; + +/* No comment provided by engineer. */ +"You may save the exported archive." = "Вы можете сохранить экспортированный архив."; + /* No comment provided by engineer. */ "You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." = "Вы должны всегда использовать самую новую версию данных чата, ТОЛЬКО на одном устройстве, иначе Вы можете перестать получать сообщения от каких то контактов."; +/* No comment provided by engineer. */ +"You need to allow your contact to call to be able to call them." = "Чтобы включить звонки, разрешите их Вашему контакту."; + /* No comment provided by engineer. */ "You need to allow your contact to send voice messages to be able to send them." = "Чтобы включить отправку голосовых сообщений, разрешите их Вашему контакту."; @@ -4460,9 +5158,6 @@ /* No comment provided by engineer. */ "Your chat profiles" = "Ваши профили чата"; -/* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Ваш контакт должен быть в сети чтобы установить соединение.\nВы можете отменить соединение и удалить контакт (и попробовать позже с другой ссылкой)."; - /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Ваш контакт отправил файл, размер которого превышает максимальный размер (%@)."; diff --git a/apps/ios/th.lproj/Localizable.strings b/apps/ios/th.lproj/Localizable.strings index 93c56a730d..9726441a1f 100644 --- a/apps/ios/th.lproj/Localizable.strings +++ b/apps/ios/th.lproj/Localizable.strings @@ -109,9 +109,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ ได้รับการตรวจสอบแล้ว"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ เซิร์ฟเวอร์"; - /* notification title */ "%@ wants to connect!" = "%@ อยากเชื่อมต่อ!"; @@ -259,17 +256,16 @@ /* No comment provided by engineer. */ "above, then choose:" = "ด้านบน จากนั้นเลือก:"; -/* No comment provided by engineer. */ -"Accent color" = "สีเน้น"; - /* accept contact request via notification - accept incoming call via notification */ + accept incoming call via notification + swipe action */ "Accept" = "รับ"; /* notification body */ "Accept contact request from %@?" = "รับการขอติดต่อจาก %@?"; -/* accept contact request via notification */ +/* accept contact request via notification + swipe action */ "Accept incognito" = "ยอมรับโหมดไม่ระบุตัวตน"; /* call status */ @@ -285,7 +281,7 @@ "Add profile" = "เพิ่มโปรไฟล์"; /* No comment provided by engineer. */ -"Add server…" = "เพิ่มเซิร์ฟเวอร์…"; +"Add server" = "เพิ่มเซิร์ฟเวอร์"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "เพิ่มเซิร์ฟเวอร์โดยการสแกนรหัสคิวอาร์โค้ด"; @@ -624,7 +620,7 @@ /* No comment provided by engineer. */ "Choose from library" = "เลือกจากอัลบั้ม"; -/* No comment provided by engineer. */ +/* swipe action */ "Clear" = "ลบ"; /* No comment provided by engineer. */ @@ -639,9 +635,6 @@ /* No comment provided by engineer. */ "colored" = "มีสี"; -/* No comment provided by engineer. */ -"Colors" = "สี"; - /* server test step */ "Compare file" = "เปรียบเทียบไฟล์"; @@ -747,9 +740,6 @@ /* notification */ "Contact is connected" = "เชื่อมต่อกับผู้ติดต่อแล้ว"; -/* No comment provided by engineer. */ -"Contact is not connected yet!" = "ผู้ติดต่อยังไม่ได้เชื่อมต่อ!"; - /* No comment provided by engineer. */ "Contact name" = "ชื่อผู้ติดต่อ"; @@ -765,7 +755,7 @@ /* No comment provided by engineer. */ "Continue" = "ดำเนินการต่อ"; -/* chat item action */ +/* No comment provided by engineer. */ "Copy" = "คัดลอก"; /* No comment provided by engineer. */ @@ -897,7 +887,8 @@ /* No comment provided by engineer. */ "default (yes)" = "ค่าเริ่มต้น (ใช่)"; -/* chat item action */ +/* chat item action + swipe action */ "Delete" = "ลบ"; /* No comment provided by engineer. */ @@ -930,9 +921,6 @@ /* No comment provided by engineer. */ "Delete contact" = "ลบผู้ติดต่อ"; -/* No comment provided by engineer. */ -"Delete Contact" = "ลบผู้ติดต่อ"; - /* No comment provided by engineer. */ "Delete database" = "ลบฐานข้อมูล"; @@ -984,9 +972,6 @@ /* No comment provided by engineer. */ "Delete old database?" = "ลบฐานข้อมูลเก่า?"; -/* No comment provided by engineer. */ -"Delete pending connection" = "ลบการเชื่อมต่อที่รอดำเนินการ"; - /* No comment provided by engineer. */ "Delete pending connection?" = "ลบการเชื่อมต่อที่รอดำเนินการหรือไม่?"; @@ -1290,9 +1275,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "เกิดข้อผิดพลาดในการลบการเชื่อมต่อ"; -/* No comment provided by engineer. */ -"Error deleting contact" = "เกิดข้อผิดพลาดในการลบผู้ติดต่อ"; - /* No comment provided by engineer. */ "Error deleting database" = "เกิดข้อผิดพลาดในการลบฐานข้อมูล"; @@ -1386,7 +1368,8 @@ /* No comment provided by engineer. */ "Error: " = "ผิดพลาด: "; -/* No comment provided by engineer. */ +/* file error text + snd error text */ "Error: %@" = "ข้อผิดพลาด: % @"; /* No comment provided by engineer. */ @@ -1419,7 +1402,7 @@ /* No comment provided by engineer. */ "Fast and no wait until the sender is online!" = "รวดเร็วและไม่ต้องรอจนกว่าผู้ส่งจะออนไลน์!"; -/* No comment provided by engineer. */ +/* swipe action */ "Favorite" = "ที่ชอบ"; /* No comment provided by engineer. */ @@ -1797,7 +1780,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "อินเทอร์เฟซภาษาญี่ปุ่น"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "เข้าร่วม"; /* No comment provided by engineer. */ @@ -1827,7 +1810,7 @@ /* No comment provided by engineer. */ "Learn more" = "ศึกษาเพิ่มเติม"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "ออกจาก"; /* No comment provided by engineer. */ @@ -1998,19 +1981,16 @@ /* No comment provided by engineer. */ "More improvements are coming soon!" = "การปรับปรุงเพิ่มเติมกำลังจะมาเร็ว ๆ นี้!"; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "เป็นไปได้มากว่าผู้ติดต่อนี้ได้ลบการเชื่อมต่อกับคุณ"; - /* No comment provided by engineer. */ "Multiple chat profiles" = "โปรไฟล์การแชทหลายรายการ"; -/* No comment provided by engineer. */ +/* swipe action */ "Mute" = "ปิดเสียง"; /* No comment provided by engineer. */ "Muted when inactive!" = "ปิดเสียงเมื่อไม่ได้ใช้งาน!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "ชื่อ"; /* No comment provided by engineer. */ @@ -2111,7 +2091,7 @@ time to disappear */ "off" = "ปิด"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "ปิด"; /* feature offered item */ @@ -2136,10 +2116,10 @@ "One-time invitation link" = "ลิงก์คำเชิญแบบใช้ครั้งเดียว"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ ต้องเปิดใช้งาน VPN สำหรับการเชื่อมต่อ"; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ ต้องเปิดใช้งาน VPN สำหรับการเชื่อมต่อ"; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ ต้องเปิดใช้งาน VPN สำหรับการเชื่อมต่อ"; +"Onion hosts will be used when available.\nRequires compatible VPN." = "จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ ต้องเปิดใช้งาน VPN สำหรับการเชื่อมต่อ"; /* No comment provided by engineer. */ "Onion hosts will not be used." = "โฮสต์หัวหอมจะไม่ถูกใช้"; @@ -2363,7 +2343,7 @@ /* chat item menu */ "React…" = "ตอบสนอง…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "อ่าน"; /* No comment provided by engineer. */ @@ -2426,7 +2406,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "ลดการใช้แบตเตอรี่"; -/* reject incoming call via notification */ +/* reject incoming call via notification + swipe action */ "Reject" = "ปฏิเสธ"; /* No comment provided by engineer. */ @@ -2507,9 +2488,6 @@ /* chat item action */ "Reveal" = "เปิดเผย"; -/* No comment provided by engineer. */ -"Revert" = "เปลี่ยนกลับ"; - /* No comment provided by engineer. */ "Revoke" = "ถอน"; @@ -2612,7 +2590,7 @@ /* chat item text */ "security code changed" = "เปลี่ยนรหัสความปลอดภัยแล้ว"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "เลือก"; /* No comment provided by engineer. */ @@ -2636,9 +2614,6 @@ /* No comment provided by engineer. */ "Send delivery receipts to" = "ส่งใบเสร็จรับการจัดส่งข้อความไปที่"; -/* No comment provided by engineer. */ -"Send direct message" = "ส่งข้อความโดยตรง"; - /* No comment provided by engineer. */ "Send disappearing message" = "ส่งข้อความแบบที่หายไป"; @@ -2813,9 +2788,6 @@ /* No comment provided by engineer. */ "Skipped messages" = "ข้อความที่ข้ามไป"; -/* No comment provided by engineer. */ -"SMP servers" = "เซิร์ฟเวอร์ SMP"; - /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "ข้อผิดพลาดที่ไม่ร้ายแรงบางอย่างเกิดขึ้นระหว่างการนำเข้า - คุณอาจดูรายละเอียดเพิ่มเติมได้ที่คอนโซล Chat"; @@ -2891,9 +2863,6 @@ /* No comment provided by engineer. */ "Tap to join incognito" = "แตะเพื่อเข้าร่วมโหมดไม่ระบุตัวตน"; -/* No comment provided by engineer. */ -"Tap to start a new chat" = "แตะเพื่อเริ่มแชทใหม่"; - /* No comment provided by engineer. */ "TCP connection timeout" = "หมดเวลาการเชื่อมต่อ TCP"; @@ -2978,9 +2947,6 @@ /* No comment provided by engineer. */ "The servers for new connections of your current chat profile **%@**." = "เซิร์ฟเวอร์สำหรับการเชื่อมต่อใหม่ของโปรไฟล์การแชทปัจจุบันของคุณ **%@**"; -/* No comment provided by engineer. */ -"Theme" = "ธีม"; - /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "การตั้งค่าเหล่านี้ใช้สำหรับโปรไฟล์ปัจจุบันของคุณ **%@**"; @@ -3050,13 +3016,10 @@ /* No comment provided by engineer. */ "Unable to record voice message" = "ไม่สามารถบันทึกข้อความเสียง"; -/* item status description */ -"Unexpected error: %@" = "ข้อผิดพลาดที่ไม่คาดคิด: %@"; - /* No comment provided by engineer. */ "Unexpected migration state" = "สถานะการย้ายข้อมูลที่ไม่คาดคิด"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "เลิกชอบ"; /* No comment provided by engineer. */ @@ -3095,36 +3058,27 @@ /* authentication reason */ "Unlock app" = "ปลดล็อคแอป"; -/* No comment provided by engineer. */ +/* swipe action */ "Unmute" = "เปิดเสียง"; -/* No comment provided by engineer. */ +/* swipe action */ "Unread" = "เปลี่ยนเป็นยังไม่ได้อ่าน"; /* No comment provided by engineer. */ "Update" = "อัปเดต"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = "อัปเดตการตั้งค่าโฮสต์ .onion ไหม?"; - /* No comment provided by engineer. */ "Update database passphrase" = "อัปเดตรหัสผ่านของฐานข้อมูล"; /* No comment provided by engineer. */ "Update network settings?" = "อัปเดตการตั้งค่าเครือข่ายไหม?"; -/* No comment provided by engineer. */ -"Update transport isolation mode?" = "อัปเดตโหมดการแยกการขนส่งไหม?"; - /* rcv group event chat item */ "updated group profile" = "อัปเดตโปรไฟล์กลุ่มแล้ว"; /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "การอัปเดตการตั้งค่าจะเชื่อมต่อไคลเอนต์กับเซิร์ฟเวอร์ทั้งหมดอีกครั้ง"; -/* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "การอัปเดตการตั้งค่านี้จะเชื่อมต่อไคลเอนต์กับเซิร์ฟเวอร์ทั้งหมดอีกครั้ง"; - /* No comment provided by engineer. */ "Upgrade and open chat" = "อัปเกรดและเปิดการแชท"; @@ -3152,9 +3106,6 @@ /* No comment provided by engineer. */ "User profile" = "โปรไฟล์ผู้ใช้"; -/* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "การใช้โฮสต์ .onion ต้องการผู้ให้บริการ VPN ที่เข้ากันได้"; - /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "กำลังใช้เซิร์ฟเวอร์ SimpleX Chat อยู่"; @@ -3269,9 +3220,6 @@ /* No comment provided by engineer. */ "Wrong passphrase!" = "รหัสผ่านผิด!"; -/* No comment provided by engineer. */ -"XFTP servers" = "เซิร์ฟเวอร์ XFTP"; - /* pref value */ "yes" = "ใช่"; @@ -3318,7 +3266,7 @@ "You can hide or mute a user profile - swipe it to the right." = "คุณสามารถซ่อนหรือปิดเสียงโปรไฟล์ผู้ใช้ - ปัดไปทางขวา"; /* notification body */ -"You can now send messages to %@" = "ตอนนี้คุณสามารถส่งข้อความถึง %@"; +"You can now chat with %@" = "ตอนนี้คุณสามารถส่งข้อความถึง %@"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "คุณสามารถตั้งค่าแสดงตัวอย่างการแจ้งเตือนบนหน้าจอล็อคผ่านการตั้งค่า"; @@ -3362,9 +3310,6 @@ /* No comment provided by engineer. */ "You could not be verified; please try again." = "เราไม่สามารถตรวจสอบคุณได้ กรุณาลองอีกครั้ง."; -/* No comment provided by engineer. */ -"You have no chats" = "คุณไม่มีการแชท"; - /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "คุณต้องใส่รหัสผ่านทุกครั้งที่เริ่มแอป - รหัสผ่านไม่ได้จัดเก็บไว้ในอุปกรณ์"; @@ -3443,9 +3388,6 @@ /* No comment provided by engineer. */ "Your chat profiles" = "โปรไฟล์แชทของคุณ"; -/* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "ผู้ติดต่อของคุณจะต้องออนไลน์เพื่อให้การเชื่อมต่อเสร็จสมบูรณ์\nคุณสามารถยกเลิกการเชื่อมต่อนี้และลบผู้ติดต่อออก (และลองใหม่ในภายหลังด้วยลิงก์ใหม่)"; - /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "ผู้ติดต่อของคุณส่งไฟล์ที่ใหญ่กว่าขนาดสูงสุดที่รองรับในปัจจุบัน (%@)"; diff --git a/apps/ios/tr.lproj/Localizable.strings b/apps/ios/tr.lproj/Localizable.strings index 714a1bc739..892f38fcbc 100644 --- a/apps/ios/tr.lproj/Localizable.strings +++ b/apps/ios/tr.lproj/Localizable.strings @@ -154,9 +154,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ onaylandı"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ sunucuları"; - /* No comment provided by engineer. */ "%@ uploaded" = "%@ yüklendi"; @@ -275,7 +272,7 @@ "0 sec" = "0 saniye"; /* No comment provided by engineer. */ -"0s" = "0 saniye"; +"0s" = "0sn"; /* time interval */ "1 day" = "1 gün"; @@ -337,11 +334,9 @@ /* No comment provided by engineer. */ "above, then choose:" = "yukarı çıkın, ardından seçin:"; -/* No comment provided by engineer. */ -"Accent color" = "Vurgu rengi"; - /* accept contact request via notification - accept incoming call via notification */ + accept incoming call via notification + swipe action */ "Accept" = "Kabul et"; /* No comment provided by engineer. */ @@ -350,7 +345,8 @@ /* notification body */ "Accept contact request from %@?" = "%@ 'den gelen iletişim isteği kabul edilsin mi?"; -/* accept contact request via notification */ +/* accept contact request via notification + swipe action */ "Accept incognito" = "Takma adla kabul et"; /* call status */ @@ -369,7 +365,7 @@ "Add profile" = "Profil ekle"; /* No comment provided by engineer. */ -"Add server…" = "Sunucu ekle…"; +"Add server" = "Sunucu ekle"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Karekod taratarak sunucuları ekleyin."; @@ -438,7 +434,7 @@ "All your contacts will remain connected. Profile update will be sent to your contacts." = "Tüm kişileriniz bağlı kalacaktır. Profil güncellemesi kişilerinize gönderilecektir."; /* No comment provided by engineer. */ -"All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Tüm kişileriniz, sohbetleriniz ve dosyalarınız güvenli bir şekilde şifrelenecek ve parçalar halinde yapılandırılmış XFTP rölelerine yüklenecektir."; +"All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "Tüm kişileriniz, konuşmalarınız ve dosyalarınız güvenli bir şekilde şifrelenir ve yapılandırılmış XFTP yönlendiricilerine parçalar halinde yüklenir."; /* No comment provided by engineer. */ "Allow" = "İzin ver"; @@ -449,6 +445,9 @@ /* No comment provided by engineer. */ "Allow disappearing messages only if your contact allows it to you." = "Eğer kişide izin verirse kaybolan mesajlara izin ver."; +/* No comment provided by engineer. */ +"Allow downgrade" = "Sürüm düşürmeye izin ver"; + /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Konuştuğun kişi, kalıcı olarak silinebilen mesajlara izin veriyorsa sen de ver. (24 saat içinde)"; @@ -459,7 +458,7 @@ "Allow message reactions." = "Mesaj tepkilerine izin ver."; /* No comment provided by engineer. */ -"Allow sending direct messages to members." = "Üyelere direkt mesaj göndermeye izin ver."; +"Allow sending direct messages to members." = "Üyelere doğrudan mesaj göndermeye izin ver."; /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Kendiliğinden yok olan mesajlar göndermeye izin ver."; @@ -509,6 +508,9 @@ /* pref value */ "always" = "her zaman"; +/* No comment provided by engineer. */ +"Always use private routing." = "Her zaman gizli yönlendirme kullan."; + /* No comment provided by engineer. */ "Always use relay" = "Her zaman yönlendirici kullan"; @@ -716,6 +718,9 @@ /* No comment provided by engineer. */ "Cannot receive file" = "Dosya alınamıyor"; +/* snd error text */ +"Capacity exceeded - recipient did not receive previously sent messages." = "Kapasite aşıldı - alıcı önceden gönderilen mesajları almadı."; + /* No comment provided by engineer. */ "Cellular" = "Hücresel Veri"; @@ -813,7 +818,7 @@ /* No comment provided by engineer. */ "Choose from library" = "Kütüphaneden seç"; -/* No comment provided by engineer. */ +/* swipe action */ "Clear" = "Temizle"; /* No comment provided by engineer. */ @@ -831,9 +836,6 @@ /* No comment provided by engineer. */ "colored" = "renklendirilmiş"; -/* No comment provided by engineer. */ -"Colors" = "Renkler"; - /* server test step */ "Compare file" = "Dosya karşılaştır"; @@ -852,6 +854,9 @@ /* No comment provided by engineer. */ "Confirm database upgrades" = "Veritabanı geliştirmelerini onayla"; +/* No comment provided by engineer. */ +"Confirm files from unknown servers." = "Bilinmeyen sunuculardan gelen dosyaları onayla."; + /* No comment provided by engineer. */ "Confirm network settings" = "Ağ ayarlarını onaylayın"; @@ -993,9 +998,6 @@ /* notification */ "Contact is connected" = "Kişi bağlandı"; -/* No comment provided by engineer. */ -"Contact is not connected yet!" = "Kişi şuan bağlanmadı!"; - /* No comment provided by engineer. */ "Contact name" = "Kişi adı"; @@ -1011,7 +1013,7 @@ /* No comment provided by engineer. */ "Continue" = "Devam et"; -/* chat item action */ +/* No comment provided by engineer. */ "Copy" = "Kopyala"; /* No comment provided by engineer. */ @@ -1155,6 +1157,9 @@ /* time unit */ "days" = "gün"; +/* No comment provided by engineer. */ +"Debug delivery" = "Hata ayıklama teslimatı"; + /* No comment provided by engineer. */ "Decentralized" = "Merkezi Olmayan"; @@ -1170,7 +1175,8 @@ /* No comment provided by engineer. */ "default (yes)" = "varsayılan (evet)"; -/* chat item action */ +/* chat item action + swipe action */ "Delete" = "Sil"; /* No comment provided by engineer. */ @@ -1209,12 +1215,6 @@ /* No comment provided by engineer. */ "Delete contact" = "Kişiyi sil"; -/* No comment provided by engineer. */ -"Delete Contact" = "Kişiyi sil"; - -/* No comment provided by engineer. */ -"Delete contact?\nThis cannot be undone!" = "Kişi silinsin mi?\nBu geri alınamaz!"; - /* No comment provided by engineer. */ "Delete database" = "Veritabanını sil"; @@ -1269,9 +1269,6 @@ /* No comment provided by engineer. */ "Delete old database?" = "Eski veritabanı silinsin mi?"; -/* No comment provided by engineer. */ -"Delete pending connection" = "Bekleyen bağlantıyı sil"; - /* No comment provided by engineer. */ "Delete pending connection?" = "Bekleyen bağlantı silinsin mi?"; @@ -1320,6 +1317,9 @@ /* No comment provided by engineer. */ "Desktop devices" = "Bilgisayar cihazları"; +/* snd error text */ +"Destination server error: %@" = "Hedef sunucu hatası: %@"; + /* No comment provided by engineer. */ "Develop" = "Geliştir"; @@ -1398,6 +1398,12 @@ /* No comment provided by engineer. */ "Do not send history to new members." = "Yeni üyelere geçmişi gönderme."; +/* No comment provided by engineer. */ +"Do NOT send messages directly, even if your or destination server does not support private routing." = "Sizin veya hedef sunucunun özel yönlendirmeyi desteklememesi durumunda bile mesajları doğrudan GÖNDERMEYİN."; + +/* No comment provided by engineer. */ +"Do NOT use private routing." = "Gizli yönlendirmeyi KULLANMA."; + /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "Acil aramalar için SimpleX'i KULLANMAYIN."; @@ -1662,9 +1668,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "Bağlantı silinirken hata oluştu"; -/* No comment provided by engineer. */ -"Error deleting contact" = "Kişi silinirken hata oluştu"; - /* No comment provided by engineer. */ "Error deleting database" = "Veritabanı silinirken hata oluştu"; @@ -1779,7 +1782,8 @@ /* No comment provided by engineer. */ "Error: " = "Hata: "; -/* No comment provided by engineer. */ +/* file error text + snd error text */ "Error: %@" = "Hata: %@"; /* No comment provided by engineer. */ @@ -1824,7 +1828,7 @@ /* No comment provided by engineer. */ "Faster joining and more reliable messages." = "Daha hızlı katılma ve daha güvenilir mesajlar."; -/* No comment provided by engineer. */ +/* swipe action */ "Favorite" = "Favori"; /* No comment provided by engineer. */ @@ -1839,6 +1843,9 @@ /* No comment provided by engineer. */ "File: %@" = "Dosya: %@"; +/* No comment provided by engineer. */ +"Files" = "Dosyalar"; + /* No comment provided by engineer. */ "Files & media" = "Dosyalar & medya"; @@ -1905,6 +1912,12 @@ /* No comment provided by engineer. */ "Forwarded from" = "Şuradan iletildi"; +/* snd error text */ +"Forwarding server: %@\nDestination server error: %@" = "Yönlendirme sunucusu: %1$@\nHedef sunucu hatası: %2$@"; + +/* snd error text */ +"Forwarding server: %@\nError: %@" = "Yönlendirme sunucusu: %1$@\nHata: %2$@"; + /* No comment provided by engineer. */ "Found desktop" = "Bilgisayar bulundu"; @@ -2292,7 +2305,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Japonca arayüz"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Katıl"; /* No comment provided by engineer. */ @@ -2343,7 +2356,7 @@ /* No comment provided by engineer. */ "Learn more" = "Daha fazlası"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "Ayrıl"; /* No comment provided by engineer. */ @@ -2460,9 +2473,15 @@ /* No comment provided by engineer. */ "Message delivery receipts!" = "Mesaj alındı bilgisi!"; +/* item status text */ +"Message delivery warning" = "Mesaj iletimi uyarısı"; + /* No comment provided by engineer. */ "Message draft" = "Mesaj taslağı"; +/* No comment provided by engineer. */ +"Message queue info" = "Mesaj kuyruğu bilgisi"; + /* chat feature */ "Message reactions" = "Mesaj tepkileri"; @@ -2568,19 +2587,16 @@ /* item status description */ "Most likely this connection is deleted." = "Büyük ihtimalle bu bağlantı silinmiş."; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Büyük ihtimalle bu kişi seninle bağlantını sildi."; - /* No comment provided by engineer. */ "Multiple chat profiles" = "Çoklu sohbet profili"; -/* No comment provided by engineer. */ +/* swipe action */ "Mute" = "Sustur"; /* No comment provided by engineer. */ "Muted when inactive!" = "Aktif değilken susturuldu!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "İsim"; /* No comment provided by engineer. */ @@ -2589,6 +2605,9 @@ /* No comment provided by engineer. */ "Network connection" = "Ağ bağlantısı"; +/* snd error text */ +"Network issues - message expired after many attempts to send it." = "Ağ sorunları - birçok gönderme denemesinden sonra mesajın süresi doldu."; + /* No comment provided by engineer. */ "Network management" = "Ağ yönetimi"; @@ -2702,7 +2721,7 @@ time to disappear */ "off" = "kapalı"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Kapalı"; /* feature offered item */ @@ -2730,10 +2749,10 @@ "One-time invitation link" = "Tek zamanlı bağlantı daveti"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "Bağlantı için Onion ana bilgisayarları gerekecektir. VPN'nin etkinleştirilmesi gerekir."; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Bağlantı için Onion ana bilgisayarları gerekecektir.\nVPN'nin etkinleştirilmesi gerekir."; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "Onion ana bilgisayarları mevcutsa kullanılacaktır. VPN'nin etkinleştirilmesi gerekir."; +"Onion hosts will be used when available.\nRequires compatible VPN." = "Onion ana bilgisayarları mevcutsa kullanılacaktır.\nVPN'nin etkinleştirilmesi gerekir."; /* No comment provided by engineer. */ "Onion hosts will not be used." = "Onion ana bilgisayarları kullanılmayacaktır."; @@ -2951,9 +2970,18 @@ /* No comment provided by engineer. */ "Private filenames" = "Gizli dosya adları"; +/* No comment provided by engineer. */ +"Private message routing" = "Gizli mesaj yönlendirme"; + +/* No comment provided by engineer. */ +"Private message routing 🚀" = "Gizli mesaj yönlendirme 🚀"; + /* name of notes to self */ "Private notes" = "Gizli notlar"; +/* No comment provided by engineer. */ +"Private routing" = "Gizli yönlendirme"; + /* No comment provided by engineer. */ "Profile and server connections" = "Profil ve sunucu bağlantıları"; @@ -2988,7 +3016,7 @@ "Prohibit messages reactions." = "Mesajlarda tepkileri yasakla."; /* No comment provided by engineer. */ -"Prohibit sending direct messages to members." = "Geri dönülmez mesaj silme işlemini yasakla."; +"Prohibit sending direct messages to members." = "Üyelere doğrudan mesaj göndermeyi yasakla."; /* No comment provided by engineer. */ "Prohibit sending disappearing messages." = "Kaybolan mesajların gönderimini yasakla."; @@ -3005,9 +3033,15 @@ /* No comment provided by engineer. */ "Protect app screen" = "Uygulama ekranını koru"; +/* No comment provided by engineer. */ +"Protect IP address" = "IP adresini koru"; + /* No comment provided by engineer. */ "Protect your chat profiles with a password!" = "Bir parolayla birlikte sohbet profillerini koru!"; +/* No comment provided by engineer. */ +"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "IP adresinizi kişileriniz tarafından seçilen mesajlaşma yönlendiricilerinden koruyun.\n*Ağ ve sunucular* ayarlarında etkinleştirin."; + /* No comment provided by engineer. */ "Protocol timeout" = "Protokol zaman aşımı"; @@ -3032,7 +3066,7 @@ /* chat item menu */ "React…" = "Tepki ver…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "Oku"; /* No comment provided by engineer. */ @@ -3077,9 +3111,6 @@ /* No comment provided by engineer. */ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Alıcı adresi farklı bir sunucuya değiştirilecektir. Gönderici çevrimiçi olduktan sonra adres değişikliği tamamlanacaktır."; -/* No comment provided by engineer. */ -"Receiving concurrency" = "Eşzamanlılık alınıyor"; - /* No comment provided by engineer. */ "Receiving file will be stopped." = "Dosya alımı durdurulacaktır."; @@ -3110,7 +3141,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Azaltılmış pil kullanımı"; -/* reject incoming call via notification */ +/* reject incoming call via notification + swipe action */ "Reject" = "Reddet"; /* No comment provided by engineer. */ @@ -3123,10 +3155,10 @@ "rejected call" = "geri çevrilmiş çağrı"; /* No comment provided by engineer. */ -"Relay server is only used if necessary. Another party can observe your IP address." = "Aktarma sunucusu yalnızca gerekli olduğunda kullanılır. Başka bir taraf IP adresinizi gözlemleyebilir."; +"Relay server is only used if necessary. Another party can observe your IP address." = "Yönlendirici sunucusu yalnızca gerekli olduğunda kullanılır. Başka bir taraf IP adresinizi gözlemleyebilir."; /* No comment provided by engineer. */ -"Relay server protects your IP address, but it can observe the duration of the call." = "Aktarıcı sunucu IP adresinizi korur, ancak aramanın süresini gözlemleyebilir."; +"Relay server protects your IP address, but it can observe the duration of the call." = "Yönlendirici sunucu IP adresinizi korur, ancak aramanın süresini gözlemleyebilir."; /* No comment provided by engineer. */ "Remove" = "Sil"; @@ -3218,9 +3250,6 @@ /* chat item action */ "Reveal" = "Göster"; -/* No comment provided by engineer. */ -"Revert" = "Geri al"; - /* No comment provided by engineer. */ "Revoke" = "İptal et"; @@ -3236,6 +3265,9 @@ /* No comment provided by engineer. */ "Run chat" = "Sohbeti çalıştır"; +/* No comment provided by engineer. */ +"Safely receive files" = "Dosyaları güvenle alın"; + /* No comment provided by engineer. */ "Safer groups" = "Daha güvenli gruplar"; @@ -3350,7 +3382,7 @@ /* chat item text */ "security code changed" = "güvenlik kodu değiştirildi"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "Seç"; /* No comment provided by engineer. */ @@ -3377,9 +3409,6 @@ /* No comment provided by engineer. */ "send direct message" = "doğrudan mesaj gönder"; -/* No comment provided by engineer. */ -"Send direct message" = "Doğrudan mesaj gönder"; - /* No comment provided by engineer. */ "Send direct message to connect" = "Bağlanmak için doğrudan mesaj gönder"; @@ -3392,6 +3421,12 @@ /* No comment provided by engineer. */ "Send live message" = "Canlı mesaj gönder"; +/* No comment provided by engineer. */ +"Send messages directly when IP address is protected and your or destination server does not support private routing." = "IP adresi korumalı olduğunda ve sizin veya hedef sunucunun özel yönlendirmeyi desteklemediği durumlarda mesajları doğrudan gönderin."; + +/* No comment provided by engineer. */ +"Send messages directly when your or destination server does not support private routing." = "Sizin veya hedef sunucunun özel yönlendirmeyi desteklemediği durumlarda mesajları doğrudan gönderin."; + /* No comment provided by engineer. */ "Send notifications" = "Bildirimler gönder"; @@ -3455,6 +3490,12 @@ /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "Gönderilen mesajlar ayarlanan süreden sonra silinecektir."; +/* srv error text. */ +"Server address is incompatible with network settings." = "Sunucu adresi ağ ayarlarıyla uyumlu değil."; + +/* queue info */ +"server queue info: %@\n\nlast received msg: %@" = "sunucu kuyruk bilgisi: %1$@\n\nson alınan msj: %2$@"; + /* server test error */ "Server requires authorization to create queues, check password" = "Sunucunun sıra oluşturması için yetki gereklidir, şifreyi kontrol edin"; @@ -3464,6 +3505,9 @@ /* No comment provided by engineer. */ "Server test failed!" = "Sunucu testinde hata oluştu!"; +/* srv error text */ +"Server version is incompatible with network settings." = "Sunucu sürümü ağ ayarlarıyla uyumlu değil."; + /* No comment provided by engineer. */ "Servers" = "Sunucular"; @@ -3530,6 +3574,9 @@ /* No comment provided by engineer. */ "Share with contacts" = "Kişilerle paylaş"; +/* No comment provided by engineer. */ +"Show → on messages sent via private routing." = "Gizli yönlendirme yoluyla gönderilen mesajlarda → işaretini göster."; + /* No comment provided by engineer. */ "Show calls in phone history" = "Telefon geçmişinde aramaları göster"; @@ -3539,6 +3586,9 @@ /* No comment provided by engineer. */ "Show last messages" = "Son mesajları göster"; +/* No comment provided by engineer. */ +"Show message status" = "Mesaj durumunu göster"; + /* No comment provided by engineer. */ "Show preview" = "Ön gösterimi göser"; @@ -3602,9 +3652,6 @@ /* No comment provided by engineer. */ "Small groups (max 20)" = "Küçük gruplar (en fazla 20 kişi)"; -/* No comment provided by engineer. */ -"SMP servers" = "SMP sunucuları"; - /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "İçe aktarma sırasında bazı ölümcül olmayan hatalar oluştu - daha fazla ayrıntı için Sohbet konsoluna bakabilirsiniz."; @@ -3704,9 +3751,6 @@ /* No comment provided by engineer. */ "Tap to scan" = "Taramak için tıkla"; -/* No comment provided by engineer. */ -"Tap to start a new chat" = "Yeni bir sohbet başlatmak için tıkla"; - /* No comment provided by engineer. */ "TCP connection timeout" = "TCP bağlantı zaman aşımı"; @@ -3746,6 +3790,9 @@ /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Uygulama, mesaj veya iletişim isteği aldığınızda sizi bilgilendirebilir - etkinleştirmek için lütfen ayarları açın."; +/* No comment provided by engineer. */ +"The app will ask to confirm downloads from unknown file servers (except .onion)." = "Uygulama bilinmeyen dosya sunucularından indirmeleri onaylamanızı isteyecektir (.onion hariç)."; + /* No comment provided by engineer. */ "The attempt to change database passphrase was not completed." = "Veritabanı parolasını değiştirme girişimi tamamlanmadı."; @@ -3797,9 +3844,6 @@ /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "Yapıştırdığın metin bir SimpleX bağlantısı değildir."; -/* No comment provided by engineer. */ -"Theme" = "Tema"; - /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Bu ayarlar mevcut profiliniz **%@** içindir."; @@ -3866,6 +3910,9 @@ /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Bilgilerinizi korumak için SimpleX Lock özelliğini açın.\nBu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenecektir."; +/* No comment provided by engineer. */ +"To protect your IP address, private routing uses your SMP servers to deliver messages." = "IP adresinizi korumak için,gizli yönlendirme mesajları iletmek için SMP sunucularınızı kullanır."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Sesli mesaj kaydetmek için lütfen Mikrofon kullanım izni verin."; @@ -3920,13 +3967,10 @@ /* rcv group event chat item */ "unblocked %@" = "engeli kaldırıldı %@"; -/* item status description */ -"Unexpected error: %@" = "Beklenmeyen hata: %@"; - /* No comment provided by engineer. */ "Unexpected migration state" = "Beklenmeyen geçiş durumu"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "Favorilerden çık."; /* No comment provided by engineer. */ @@ -3953,6 +3997,12 @@ /* No comment provided by engineer. */ "Unknown error" = "Bilinmeyen hata"; +/* No comment provided by engineer. */ +"unknown servers" = "bilinmeyen yönlendiriciler"; + +/* No comment provided by engineer. */ +"Unknown servers!" = "Bilinmeyen sunucular!"; + /* No comment provided by engineer. */ "unknown status" = "bilinmeyen durum"; @@ -3974,10 +4024,13 @@ /* authentication reason */ "Unlock app" = "Uygulama kilidini aç"; -/* No comment provided by engineer. */ +/* swipe action */ "Unmute" = "Susturmayı kaldır"; /* No comment provided by engineer. */ +"unprotected" = "korumasız"; + +/* swipe action */ "Unread" = "Okunmamış"; /* No comment provided by engineer. */ @@ -3986,18 +4039,12 @@ /* No comment provided by engineer. */ "Update" = "Güncelle"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = ".onion ana bilgisayarların ayarı güncellensin mi?"; - /* No comment provided by engineer. */ "Update database passphrase" = "Veritabanı parolasını güncelle"; /* No comment provided by engineer. */ "Update network settings?" = "Bağlantı ayarları güncellensin mi?"; -/* No comment provided by engineer. */ -"Update transport isolation mode?" = "Taşıma izolasyon modu güncellensin mi?"; - /* rcv group event chat item */ "updated group profile" = "grup profili güncellendi"; @@ -4007,9 +4054,6 @@ /* No comment provided by engineer. */ "Updating settings will re-connect the client to all servers." = "Ayarların güncellenmesi, istemciyi tüm sunuculara yeniden bağlayacaktır."; -/* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "Bu ayarın güncellenmesi, istemciyi tüm sunuculara yeniden bağlayacaktır."; - /* No comment provided by engineer. */ "Upgrade and open chat" = "Yükselt ve sohbeti aç"; @@ -4046,6 +4090,12 @@ /* No comment provided by engineer. */ "Use only local notifications?" = "Sadece yerel bildirimler kullanılsın mı?"; +/* No comment provided by engineer. */ +"Use private routing with unknown servers when IP address is not protected." = "IP adresi korunmadığında bilinmeyen sunucularla gizli yönlendirme kullan."; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers." = "Bilinmeyen sunucularla gizli yönlendirme kullan."; + /* No comment provided by engineer. */ "Use server" = "Sunucu kullan"; @@ -4058,9 +4108,6 @@ /* No comment provided by engineer. */ "User profile" = "Kullanıcı profili"; -/* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = ".onion ana bilgisayarlarını kullanmak için uyumlu VPN sağlayıcısı gerekir."; - /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "SimpleX Chat sunucuları kullanılıyor."; @@ -4199,6 +4246,9 @@ /* No comment provided by engineer. */ "When connecting audio and video calls." = "Sesli ve görüntülü aramalara bağlanırken."; +/* No comment provided by engineer. */ +"when IP hidden" = "IP gizliyken"; + /* No comment provided by engineer. */ "When people request to connect, you can accept or reject it." = "İnsanlar bağlantı talebinde bulunduğunda, kabul edebilir veya reddedebilirsiniz."; @@ -4223,15 +4273,21 @@ /* No comment provided by engineer. */ "With reduced battery usage." = "Azaltılmış pil kullanımı ile birlikte."; +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to file servers." = "Tor veya VPN olmadan, IP adresiniz dosya sunucularına görülebilir."; + +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Tor veya VPN olmadan, IP adresiniz bu XFTP aktarıcıları tarafından görülebilir: %@."; + /* No comment provided by engineer. */ "Wrong database passphrase" = "Yanlış veritabanı parolası"; +/* snd error text */ +"Wrong key or unknown connection - most likely this connection is deleted." = "Yanlış anahtar veya bilinmeyen bağlantı - büyük olasılıkla bu bağlantı silinmiştir."; + /* No comment provided by engineer. */ "Wrong passphrase!" = "Yanlış parola!"; -/* No comment provided by engineer. */ -"XFTP servers" = "XFTP sunucuları"; - /* pref value */ "yes" = "evet"; @@ -4314,7 +4370,7 @@ "You can make it visible to your SimpleX contacts via Settings." = "Ayarlardan SimpleX kişilerinize görünür yapabilirsiniz."; /* notification body */ -"You can now send messages to %@" = "Artık %@ adresine mesaj gönderebilirsin"; +"You can now chat with %@" = "Artık %@ adresine mesaj gönderebilirsin"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Kilit ekranı bildirim önizlemesini ayarlar üzerinden ayarlayabilirsiniz."; @@ -4367,9 +4423,6 @@ /* No comment provided by engineer. */ "You have already requested connection!\nRepeat connection request?" = "Zaten bağlantı isteğinde bulundunuz!\nBağlantı isteği tekrarlansın mı?"; -/* No comment provided by engineer. */ -"You have no chats" = "Hiç sohbetiniz yok"; - /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "Uygulama her başladığında parola girmeniz gerekir - parola cihazınızda saklanmaz."; @@ -4460,9 +4513,6 @@ /* No comment provided by engineer. */ "Your chat profiles" = "Sohbet profillerin"; -/* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Bağlantının tamamlanması için kişinizin çevrimiçi olması gerekir.\nBu bağlantıyı iptal edebilir ve kişiyi kaldırabilirsiniz (ve daha sonra yeni bir bağlantıyla deneyebilirsiniz)."; - /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Kişiniz şu anda desteklenen maksimum boyuttan (%@) daha büyük bir dosya gönderdi."; diff --git a/apps/ios/uk.lproj/Localizable.strings b/apps/ios/uk.lproj/Localizable.strings index d52efd2b65..1908d386a3 100644 --- a/apps/ios/uk.lproj/Localizable.strings +++ b/apps/ios/uk.lproj/Localizable.strings @@ -154,9 +154,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ перевірено"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ сервери"; - /* No comment provided by engineer. */ "%@ uploaded" = "%@ завантажено"; @@ -338,10 +335,11 @@ "above, then choose:" = "вище, а потім обирайте:"; /* No comment provided by engineer. */ -"Accent color" = "Акцентний колір"; +"Accent" = "Акцент"; /* accept contact request via notification - accept incoming call via notification */ + accept incoming call via notification + swipe action */ "Accept" = "Прийняти"; /* No comment provided by engineer. */ @@ -350,12 +348,22 @@ /* notification body */ "Accept contact request from %@?" = "Прийняти запит на контакт від %@?"; -/* accept contact request via notification */ +/* accept contact request via notification + swipe action */ "Accept incognito" = "Прийняти інкогніто"; /* call status */ "accepted call" = "прийнято виклик"; +/* No comment provided by engineer. */ +"Acknowledged" = "Визнано"; + +/* No comment provided by engineer. */ +"Acknowledgement errors" = "Помилки підтвердження"; + +/* No comment provided by engineer. */ +"Active connections" = "Активні з'єднання"; + /* No comment provided by engineer. */ "Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts." = "Додайте адресу до свого профілю, щоб ваші контакти могли поділитися нею з іншими людьми. Повідомлення про оновлення профілю буде надіслано вашим контактам."; @@ -369,7 +377,7 @@ "Add profile" = "Додати профіль"; /* No comment provided by engineer. */ -"Add server…" = "Додати сервер…"; +"Add server" = "Додати сервер"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "Додайте сервери, відсканувавши QR-код."; @@ -380,6 +388,15 @@ /* No comment provided by engineer. */ "Add welcome message" = "Додати вітальне повідомлення"; +/* No comment provided by engineer. */ +"Additional accent" = "Додатковий акцент"; + +/* No comment provided by engineer. */ +"Additional accent 2" = "Додатковий акцент 2"; + +/* No comment provided by engineer. */ +"Additional secondary" = "Додаткова вторинна"; + /* No comment provided by engineer. */ "Address" = "Адреса"; @@ -389,6 +406,9 @@ /* member role */ "admin" = "адмін"; +/* feature role */ +"admins" = "адміністратори"; + /* No comment provided by engineer. */ "Admins can block a member for all." = "Адміністратори можуть заблокувати користувача для всіх."; @@ -398,6 +418,9 @@ /* No comment provided by engineer. */ "Advanced network settings" = "Розширені налаштування мережі"; +/* No comment provided by engineer. */ +"Advanced settings" = "Додаткові налаштування"; + /* chat item text */ "agreeing encryption for %@…" = "узгодження шифрування для %@…"; @@ -413,9 +436,15 @@ /* No comment provided by engineer. */ "All data is erased when it is entered." = "Всі дані стираються при введенні."; +/* No comment provided by engineer. */ +"All data is private to your device." = "Всі дані є приватними для вашого пристрою."; + /* No comment provided by engineer. */ "All group members will remain connected." = "Всі учасники групи залишаться на зв'язку."; +/* feature role */ +"all members" = "всі учасники"; + /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone!" = "Усі повідомлення будуть видалені - цю дію не можна скасувати!"; @@ -425,6 +454,9 @@ /* No comment provided by engineer. */ "All new messages from %@ will be hidden!" = "Всі нові повідомлення від %@ будуть приховані!"; +/* No comment provided by engineer. */ +"All profiles" = "Всі профілі"; + /* No comment provided by engineer. */ "All your contacts will remain connected." = "Всі ваші контакти залишаться на зв'язку."; @@ -440,9 +472,15 @@ /* No comment provided by engineer. */ "Allow calls only if your contact allows them." = "Дозволяйте дзвінки, тільки якщо ваш контакт дозволяє їх."; +/* No comment provided by engineer. */ +"Allow calls?" = "Дозволити дзвінки?"; + /* No comment provided by engineer. */ "Allow disappearing messages only if your contact allows it to you." = "Дозволяйте зникати повідомленням, тільки якщо контакт дозволяє вам це робити."; +/* No comment provided by engineer. */ +"Allow downgrade" = "Дозволити пониження версії"; + /* No comment provided by engineer. */ "Allow irreversible message deletion only if your contact allows it to you. (24 hours)" = "Дозволяйте безповоротне видалення повідомлень, тільки якщо контакт дозволяє вам це зробити. (24 години)"; @@ -458,12 +496,18 @@ /* No comment provided by engineer. */ "Allow sending disappearing messages." = "Дозволити надсилання зникаючих повідомлень."; +/* No comment provided by engineer. */ +"Allow sharing" = "Дозволити спільний доступ"; + /* No comment provided by engineer. */ "Allow to irreversibly delete sent messages. (24 hours)" = "Дозволяє безповоротно видаляти надіслані повідомлення. (24 години)"; /* No comment provided by engineer. */ "Allow to send files and media." = "Дозволяє надсилати файли та медіа."; +/* No comment provided by engineer. */ +"Allow to send SimpleX links." = "Дозволити надсилати посилання SimpleX."; + /* No comment provided by engineer. */ "Allow to send voice messages." = "Дозволити надсилати голосові повідомлення."; @@ -500,6 +544,9 @@ /* pref value */ "always" = "завжди"; +/* No comment provided by engineer. */ +"Always use private routing." = "Завжди використовуйте приватну маршрутизацію."; + /* No comment provided by engineer. */ "Always use relay" = "Завжди використовуйте реле"; @@ -542,15 +589,27 @@ /* No comment provided by engineer. */ "Apply" = "Подати заявку"; +/* No comment provided by engineer. */ +"Apply to" = "Звертатися до"; + /* No comment provided by engineer. */ "Archive and upload" = "Архівування та завантаження"; +/* No comment provided by engineer. */ +"Archive contacts to chat later." = "Архівуйте контакти, щоб поспілкуватися пізніше."; + +/* No comment provided by engineer. */ +"Archived contacts" = "Архівні контакти"; + /* No comment provided by engineer. */ "Archiving database" = "Архівування бази даних"; /* No comment provided by engineer. */ "Attach" = "Прикріпити"; +/* No comment provided by engineer. */ +"attempts" = "спроби"; + /* No comment provided by engineer. */ "Audio & video calls" = "Аудіо та відео дзвінки"; @@ -593,6 +652,9 @@ /* No comment provided by engineer. */ "Back" = "Назад"; +/* No comment provided by engineer. */ +"Background" = "Фон"; + /* No comment provided by engineer. */ "Bad desktop address" = "Неправильна адреса робочого столу"; @@ -614,6 +676,12 @@ /* No comment provided by engineer. */ "Better messages" = "Кращі повідомлення"; +/* No comment provided by engineer. */ +"Better networking" = "Краща мережа"; + +/* No comment provided by engineer. */ +"Black" = "Чорний"; + /* No comment provided by engineer. */ "Block" = "Блокувати"; @@ -644,6 +712,12 @@ /* No comment provided by engineer. */ "Blocked by admin" = "Заблокований адміністратором"; +/* No comment provided by engineer. */ +"Blur for better privacy." = "Розмиття для кращої приватності."; + +/* No comment provided by engineer. */ +"Blur media" = "Розмиття медіа"; + /* No comment provided by engineer. */ "bold" = "жирний"; @@ -668,6 +742,9 @@ /* No comment provided by engineer. */ "By chat profile (default) or [by connection](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)." = "Через профіль чату (за замовчуванням) або [за з'єднанням](https://simplex.chat/blog/20230204-simplex-chat-v4-5-user-chat-profiles.html#transport-isolation) (BETA)."; +/* No comment provided by engineer. */ +"call" = "дзвонити"; + /* No comment provided by engineer. */ "Call already ended!" = "Дзвінок вже закінчився!"; @@ -683,15 +760,27 @@ /* No comment provided by engineer. */ "Calls" = "Дзвінки"; +/* No comment provided by engineer. */ +"Calls prohibited!" = "Дзвінки заборонені!"; + /* No comment provided by engineer. */ "Camera not available" = "Камера недоступна"; +/* No comment provided by engineer. */ +"Can't call contact" = "Не вдається додзвонитися до контакту"; + +/* No comment provided by engineer. */ +"Can't call member" = "Не вдається зателефонувати користувачеві"; + /* No comment provided by engineer. */ "Can't invite contact!" = "Не вдається запросити контакт!"; /* No comment provided by engineer. */ "Can't invite contacts!" = "Неможливо запросити контакти!"; +/* No comment provided by engineer. */ +"Can't message member" = "Не можу надіслати повідомлення користувачеві"; + /* No comment provided by engineer. */ "Cancel" = "Скасувати"; @@ -704,9 +793,18 @@ /* No comment provided by engineer. */ "Cannot access keychain to save database password" = "Не вдається отримати доступ до зв'язки ключів для збереження пароля до бази даних"; +/* No comment provided by engineer. */ +"Cannot forward message" = "Неможливо переслати повідомлення"; + /* No comment provided by engineer. */ "Cannot receive file" = "Не вдається отримати файл"; +/* snd error text */ +"Capacity exceeded - recipient did not receive previously sent messages." = "Перевищено ліміт - одержувач не отримав раніше надіслані повідомлення."; + +/* No comment provided by engineer. */ +"Cellular" = "Стільниковий"; + /* No comment provided by engineer. */ "Change" = "Зміна"; @@ -756,6 +854,9 @@ /* No comment provided by engineer. */ "Chat archive" = "Архів чату"; +/* No comment provided by engineer. */ +"Chat colors" = "Кольори чату"; + /* No comment provided by engineer. */ "Chat console" = "Консоль чату"; @@ -765,6 +866,9 @@ /* No comment provided by engineer. */ "Chat database deleted" = "Видалено базу даних чату"; +/* No comment provided by engineer. */ +"Chat database exported" = "Експортовано базу даних чату"; + /* No comment provided by engineer. */ "Chat database imported" = "Імпорт бази даних чату"; @@ -777,12 +881,18 @@ /* No comment provided by engineer. */ "Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." = "Чат зупинено. Якщо ви вже використовували цю базу даних на іншому пристрої, перенесіть її назад перед запуском чату."; +/* No comment provided by engineer. */ +"Chat list" = "Список чатів"; + /* No comment provided by engineer. */ "Chat migrated!" = "Чат перемістився!"; /* No comment provided by engineer. */ "Chat preferences" = "Налаштування чату"; +/* No comment provided by engineer. */ +"Chat theme" = "Тема чату"; + /* No comment provided by engineer. */ "Chats" = "Чати"; @@ -802,6 +912,15 @@ "Choose from library" = "Виберіть з бібліотеки"; /* No comment provided by engineer. */ +"Chunks deleted" = "Фрагменти видалено"; + +/* No comment provided by engineer. */ +"Chunks downloaded" = "Завантажено фрагменти"; + +/* No comment provided by engineer. */ +"Chunks uploaded" = "Завантажено фрагменти"; + +/* swipe action */ "Clear" = "Чисто"; /* No comment provided by engineer. */ @@ -817,10 +936,13 @@ "Clear verification" = "Очистити перевірку"; /* No comment provided by engineer. */ -"colored" = "кольоровий"; +"Color chats with the new themes." = "Кольорові чати з новими темами."; /* No comment provided by engineer. */ -"Colors" = "Кольори"; +"Color mode" = "Колірний режим"; + +/* No comment provided by engineer. */ +"colored" = "кольоровий"; /* server test step */ "Compare file" = "Порівняти файл"; @@ -831,15 +953,27 @@ /* No comment provided by engineer. */ "complete" = "завершено"; +/* No comment provided by engineer. */ +"Completed" = "Завершено"; + /* No comment provided by engineer. */ "Configure ICE servers" = "Налаштування серверів ICE"; +/* No comment provided by engineer. */ +"Configured %@ servers" = "Налаштовані сервери %@"; + /* No comment provided by engineer. */ "Confirm" = "Підтвердити"; +/* No comment provided by engineer. */ +"Confirm contact deletion?" = "Підтвердити видалення контакту?"; + /* No comment provided by engineer. */ "Confirm database upgrades" = "Підтвердити оновлення бази даних"; +/* No comment provided by engineer. */ +"Confirm files from unknown servers." = "Підтвердити файли з невідомих серверів."; + /* No comment provided by engineer. */ "Confirm network settings" = "Підтвердьте налаштування мережі"; @@ -873,6 +1007,9 @@ /* No comment provided by engineer. */ "connect to SimpleX Chat developers." = "зв'язатися з розробниками SimpleX Chat."; +/* No comment provided by engineer. */ +"Connect to your friends faster." = "Швидше спілкуйтеся з друзями."; + /* No comment provided by engineer. */ "Connect to yourself?" = "З'єднатися з самим собою?"; @@ -897,18 +1034,27 @@ /* No comment provided by engineer. */ "connected" = "з'єднаний"; +/* No comment provided by engineer. */ +"Connected" = "Підключено"; + /* No comment provided by engineer. */ "Connected desktop" = "Підключений робочий стіл"; /* rcv group event chat item */ "connected directly" = "з'єднані безпосередньо"; +/* No comment provided by engineer. */ +"Connected servers" = "Підключені сервери"; + /* No comment provided by engineer. */ "Connected to desktop" = "Підключено до настільного комп'ютера"; /* No comment provided by engineer. */ "connecting" = "з'єднання"; +/* No comment provided by engineer. */ +"Connecting" = "Підключення"; + /* No comment provided by engineer. */ "connecting (accepted)" = "з'єднання (прийнято)"; @@ -930,6 +1076,9 @@ /* No comment provided by engineer. */ "Connecting server… (error: %@)" = "Підключення до сервера... (помилка: %@)"; +/* No comment provided by engineer. */ +"Connecting to contact, please wait or check later!" = "З'єднання з контактом, будь ласка, зачекайте або перевірте пізніше!"; + /* No comment provided by engineer. */ "Connecting to desktop" = "Підключення до ПК"; @@ -939,6 +1088,9 @@ /* No comment provided by engineer. */ "Connection" = "Підключення"; +/* No comment provided by engineer. */ +"Connection and servers status." = "Стан з'єднання та серверів."; + /* No comment provided by engineer. */ "Connection error" = "Помилка підключення"; @@ -948,6 +1100,9 @@ /* chat list item title (it should not be shown */ "connection established" = "з'єднання встановлене"; +/* No comment provided by engineer. */ +"Connection notifications" = "Сповіщення про підключення"; + /* No comment provided by engineer. */ "Connection request sent!" = "Запит на підключення відправлено!"; @@ -957,9 +1112,15 @@ /* No comment provided by engineer. */ "Connection timeout" = "Тайм-аут з'єднання"; +/* No comment provided by engineer. */ +"Connection with desktop stopped" = "Припинено зв'язок з робочим столом"; + /* connection information */ "connection:%@" = "з'єднання:%@"; +/* No comment provided by engineer. */ +"Connections" = "З'єднання"; + /* profile update event chat item */ "contact %@ changed to %@" = "контакт %1$@ змінено на %2$@"; @@ -969,6 +1130,9 @@ /* No comment provided by engineer. */ "Contact already exists" = "Контакт вже існує"; +/* No comment provided by engineer. */ +"Contact deleted!" = "Контакт видалено!"; + /* No comment provided by engineer. */ "contact has e2e encryption" = "контакт має шифрування e2e"; @@ -982,7 +1146,7 @@ "Contact is connected" = "Контакт підключений"; /* No comment provided by engineer. */ -"Contact is not connected yet!" = "Контакт ще не підключено!"; +"Contact is deleted." = "Контакт видалено."; /* No comment provided by engineer. */ "Contact name" = "Ім'я контактної особи"; @@ -990,6 +1154,9 @@ /* No comment provided by engineer. */ "Contact preferences" = "Налаштування контактів"; +/* No comment provided by engineer. */ +"Contact will be deleted - this cannot be undone!" = "Контакт буде видалено - це неможливо скасувати!"; + /* No comment provided by engineer. */ "Contacts" = "Контакти"; @@ -999,9 +1166,15 @@ /* No comment provided by engineer. */ "Continue" = "Продовжуйте"; -/* chat item action */ +/* No comment provided by engineer. */ +"Conversation deleted!" = "Розмова видалена!"; + +/* No comment provided by engineer. */ "Copy" = "Копіювати"; +/* No comment provided by engineer. */ +"Copy error" = "Помилка копіювання"; + /* No comment provided by engineer. */ "Core version: v%@" = "Основна версія: v%@"; @@ -1047,6 +1220,9 @@ /* No comment provided by engineer. */ "Create your profile" = "Створіть свій профіль"; +/* No comment provided by engineer. */ +"Created" = "Створено"; + /* No comment provided by engineer. */ "Created at" = "Створено за адресою"; @@ -1071,6 +1247,9 @@ /* No comment provided by engineer. */ "Current passphrase…" = "Поточна парольна фраза…"; +/* No comment provided by engineer. */ +"Current profile" = "Поточний профіль"; + /* No comment provided by engineer. */ "Currently maximum supported file size is %@." = "Наразі максимальний підтримуваний розмір файлу - %@."; @@ -1080,9 +1259,15 @@ /* No comment provided by engineer. */ "Custom time" = "Індивідуальний час"; +/* No comment provided by engineer. */ +"Customize theme" = "Налаштувати тему"; + /* No comment provided by engineer. */ "Dark" = "Темний"; +/* No comment provided by engineer. */ +"Dark mode colors" = "Кольори темного режиму"; + /* No comment provided by engineer. */ "Database downgrade" = "Пониження версії бази даних"; @@ -1143,12 +1328,18 @@ /* time unit */ "days" = "днів"; +/* No comment provided by engineer. */ +"Debug delivery" = "Доставка налагодження"; + /* No comment provided by engineer. */ "Decentralized" = "Децентралізований"; /* message decrypt error item */ "Decryption error" = "Помилка розшифровки"; +/* No comment provided by engineer. */ +"decryption errors" = "помилки розшифровки"; + /* pref value */ "default (%@)" = "за замовчуванням (%@)"; @@ -1158,9 +1349,13 @@ /* No comment provided by engineer. */ "default (yes)" = "за замовчуванням (так)"; -/* chat item action */ +/* chat item action + swipe action */ "Delete" = "Видалити"; +/* No comment provided by engineer. */ +"Delete %lld messages of members?" = "Видалити %lld повідомлень користувачів?"; + /* No comment provided by engineer. */ "Delete %lld messages?" = "Видалити %lld повідомлень?"; @@ -1198,10 +1393,7 @@ "Delete contact" = "Видалити контакт"; /* No comment provided by engineer. */ -"Delete Contact" = "Видалити контакт"; - -/* No comment provided by engineer. */ -"Delete contact?\nThis cannot be undone!" = "Видалити контакт?\nЦе не можна скасувати!"; +"Delete contact?" = "Видалити контакт?"; /* No comment provided by engineer. */ "Delete database" = "Видалити базу даних"; @@ -1257,9 +1449,6 @@ /* No comment provided by engineer. */ "Delete old database?" = "Видалити стару базу даних?"; -/* No comment provided by engineer. */ -"Delete pending connection" = "Видалити очікуване з'єднання"; - /* No comment provided by engineer. */ "Delete pending connection?" = "Видалити очікуване з'єднання?"; @@ -1269,12 +1458,21 @@ /* server test step */ "Delete queue" = "Видалити чергу"; +/* No comment provided by engineer. */ +"Delete up to 20 messages at once." = "Видаляйте до 20 повідомлень одночасно."; + /* No comment provided by engineer. */ "Delete user profile?" = "Видалити профіль користувача?"; +/* No comment provided by engineer. */ +"Delete without notification" = "Видалення без попередження"; + /* deleted chat item */ "deleted" = "видалено"; +/* No comment provided by engineer. */ +"Deleted" = "Видалено"; + /* No comment provided by engineer. */ "Deleted at" = "Видалено за"; @@ -1287,6 +1485,9 @@ /* rcv group event chat item */ "deleted group" = "видалено групу"; +/* No comment provided by engineer. */ +"Deletion errors" = "Помилки видалення"; + /* No comment provided by engineer. */ "Delivery" = "Доставка"; @@ -1308,9 +1509,27 @@ /* No comment provided by engineer. */ "Desktop devices" = "Настільні пристрої"; +/* No comment provided by engineer. */ +"Destination server address of %@ is incompatible with forwarding server %@ settings." = "Адреса сервера призначення %@ несумісна з налаштуваннями сервера пересилання %@."; + +/* snd error text */ +"Destination server error: %@" = "Помилка сервера призначення: %@"; + +/* No comment provided by engineer. */ +"Destination server version of %@ is incompatible with forwarding server %@." = "Версія сервера призначення %@ несумісна з версією сервера переадресації %@."; + +/* No comment provided by engineer. */ +"Detailed statistics" = "Детальна статистика"; + +/* No comment provided by engineer. */ +"Details" = "Деталі"; + /* No comment provided by engineer. */ "Develop" = "Розробник"; +/* No comment provided by engineer. */ +"Developer options" = "Можливості для розробників"; + /* No comment provided by engineer. */ "Developer tools" = "Інструменти для розробників"; @@ -1350,6 +1569,9 @@ /* No comment provided by engineer. */ "disabled" = "вимкнено"; +/* No comment provided by engineer. */ +"Disabled" = "Вимкнено"; + /* No comment provided by engineer. */ "Disappearing message" = "Зникаюче повідомлення"; @@ -1386,6 +1608,12 @@ /* No comment provided by engineer. */ "Do not send history to new members." = "Не надсилайте історію новим користувачам."; +/* No comment provided by engineer. */ +"Do NOT send messages directly, even if your or destination server does not support private routing." = "НЕ надсилайте повідомлення напряму, навіть якщо ваш сервер або сервер призначення не підтримує приватну маршрутизацію."; + +/* No comment provided by engineer. */ +"Do NOT use private routing." = "НЕ використовуйте приватну маршрутизацію."; + /* No comment provided by engineer. */ "Do NOT use SimpleX for emergency calls." = "НЕ використовуйте SimpleX для екстрених викликів."; @@ -1401,12 +1629,24 @@ /* No comment provided by engineer. */ "Downgrade and open chat" = "Пониження та відкритий чат"; +/* chat item action */ +"Download" = "Завантажити"; + +/* No comment provided by engineer. */ +"Download errors" = "Помилки завантаження"; + /* No comment provided by engineer. */ "Download failed" = "Не вдалося завантажити"; /* server test step */ "Download file" = "Завантажити файл"; +/* No comment provided by engineer. */ +"Downloaded" = "Завантажено"; + +/* No comment provided by engineer. */ +"Downloaded files" = "Завантажені файли"; + /* No comment provided by engineer. */ "Downloading archive" = "Завантажити архів"; @@ -1419,6 +1659,9 @@ /* integrity error chat item */ "duplicate message" = "дублююче повідомлення"; +/* No comment provided by engineer. */ +"duplicates" = "дублікати"; + /* No comment provided by engineer. */ "Duration" = "Тривалість"; @@ -1476,6 +1719,12 @@ /* enabled status */ "enabled" = "увімкнено"; +/* No comment provided by engineer. */ +"Enabled" = "Увімкнено"; + +/* No comment provided by engineer. */ +"Enabled for" = "Увімкнено для"; + /* enabled status */ "enabled for contact" = "увімкнено для контакту"; @@ -1614,6 +1863,9 @@ /* No comment provided by engineer. */ "Error changing setting" = "Помилка зміни налаштування"; +/* No comment provided by engineer. */ +"Error connecting to forwarding server %@. Please try later." = "Помилка підключення до сервера переадресації %@. Спробуйте пізніше."; + /* No comment provided by engineer. */ "Error creating address" = "Помилка створення адреси"; @@ -1644,9 +1896,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "Помилка видалення з'єднання"; -/* No comment provided by engineer. */ -"Error deleting contact" = "Помилка видалення контакту"; - /* No comment provided by engineer. */ "Error deleting database" = "Помилка видалення бази даних"; @@ -1674,6 +1923,9 @@ /* No comment provided by engineer. */ "Error exporting chat database" = "Помилка експорту бази даних чату"; +/* No comment provided by engineer. */ +"Error exporting theme: %@" = "Помилка експорту теми: %@"; + /* No comment provided by engineer. */ "Error importing chat database" = "Помилка імпорту бази даних чату"; @@ -1689,9 +1941,18 @@ /* No comment provided by engineer. */ "Error receiving file" = "Помилка отримання файлу"; +/* No comment provided by engineer. */ +"Error reconnecting server" = "Помилка перепідключення сервера"; + +/* No comment provided by engineer. */ +"Error reconnecting servers" = "Помилка перепідключення серверів"; + /* No comment provided by engineer. */ "Error removing member" = "Помилка видалення учасника"; +/* No comment provided by engineer. */ +"Error resetting statistics" = "Статистика скидання помилок"; + /* No comment provided by engineer. */ "Error saving %@ servers" = "Помилка збереження %@ серверів"; @@ -1761,7 +2022,8 @@ /* No comment provided by engineer. */ "Error: " = "Помилка: "; -/* No comment provided by engineer. */ +/* file error text + snd error text */ "Error: %@" = "Помилка: %@"; /* No comment provided by engineer. */ @@ -1770,6 +2032,9 @@ /* No comment provided by engineer. */ "Error: URL is invalid" = "Помилка: URL-адреса невірна"; +/* No comment provided by engineer. */ +"Errors" = "Помилки"; + /* No comment provided by engineer. */ "Even when disabled in the conversation." = "Навіть коли вимкнений у розмові."; @@ -1782,12 +2047,18 @@ /* chat item action */ "Expand" = "Розгорнути"; +/* No comment provided by engineer. */ +"expired" = "закінчився"; + /* No comment provided by engineer. */ "Export database" = "Експорт бази даних"; /* No comment provided by engineer. */ "Export error:" = "Помилка експорту:"; +/* No comment provided by engineer. */ +"Export theme" = "Тема експорту"; + /* No comment provided by engineer. */ "Exported database archive." = "Експортований архів бази даних."; @@ -1806,9 +2077,24 @@ /* No comment provided by engineer. */ "Faster joining and more reliable messages." = "Швидше приєднання та надійніші повідомлення."; -/* No comment provided by engineer. */ +/* swipe action */ "Favorite" = "Улюблений"; +/* No comment provided by engineer. */ +"File error" = "Помилка файлу"; + +/* file error text */ +"File not found - most likely file was deleted or cancelled." = "Файл не знайдено - найімовірніше, файл було видалено або скасовано."; + +/* file error text */ +"File server error: %@" = "Помилка файлового сервера: %@"; + +/* No comment provided by engineer. */ +"File status" = "Статус файлу"; + +/* copied message info */ +"File status: %@" = "Статус файлу: %@"; + /* No comment provided by engineer. */ "File will be deleted from servers." = "Файл буде видалено з серверів."; @@ -1821,6 +2107,9 @@ /* No comment provided by engineer. */ "File: %@" = "Файл: %@"; +/* No comment provided by engineer. */ +"Files" = "Файли"; + /* No comment provided by engineer. */ "Files & media" = "Файли та медіа"; @@ -1830,6 +2119,9 @@ /* No comment provided by engineer. */ "Files and media are prohibited in this group." = "Файли та медіа в цій групі заборонені."; +/* No comment provided by engineer. */ +"Files and media not allowed" = "Файли та медіафайли заборонені"; + /* No comment provided by engineer. */ "Files and media prohibited!" = "Файли та медіа заборонені!"; @@ -1869,6 +2161,36 @@ /* No comment provided by engineer. */ "For console" = "Для консолі"; +/* chat item action */ +"Forward" = "Пересилання"; + +/* No comment provided by engineer. */ +"Forward and save messages" = "Пересилання та збереження повідомлень"; + +/* No comment provided by engineer. */ +"forwarded" = "переслано"; + +/* No comment provided by engineer. */ +"Forwarded" = "Переслано"; + +/* No comment provided by engineer. */ +"Forwarded from" = "Переслано з"; + +/* No comment provided by engineer. */ +"Forwarding server %@ failed to connect to destination server %@. Please try later." = "Серверу переадресації %@ не вдалося з'єднатися з сервером призначення %@. Спробуйте пізніше."; + +/* No comment provided by engineer. */ +"Forwarding server address is incompatible with network settings: %@." = "Адреса сервера переадресації несумісна з налаштуваннями мережі: %@."; + +/* No comment provided by engineer. */ +"Forwarding server version is incompatible with network settings: %@." = "Версія сервера переадресації несумісна з мережевими налаштуваннями: %@."; + +/* snd error text */ +"Forwarding server: %@\nDestination server error: %@" = "Сервер переадресації: %1$@\nПомилка сервера призначення: %2$@"; + +/* snd error text */ +"Forwarding server: %@\nError: %@" = "Сервер переадресації: %1$@\nПомилка: %2$@"; + /* No comment provided by engineer. */ "Found desktop" = "Знайдено робочий стіл"; @@ -1896,6 +2218,12 @@ /* No comment provided by engineer. */ "GIFs and stickers" = "GIF-файли та наклейки"; +/* message preview */ +"Good afternoon!" = "Доброго дня!"; + +/* message preview */ +"Good morning!" = "Доброго ранку!"; + /* No comment provided by engineer. */ "Group" = "Група"; @@ -1947,6 +2275,9 @@ /* No comment provided by engineer. */ "Group members can send files and media." = "Учасники групи можуть надсилати файли та медіа."; +/* No comment provided by engineer. */ +"Group members can send SimpleX links." = "Учасники групи можуть надсилати посилання SimpleX."; + /* No comment provided by engineer. */ "Group members can send voice messages." = "Учасники групи можуть надсилати голосові повідомлення."; @@ -2070,6 +2401,9 @@ /* No comment provided by engineer. */ "Import failed" = "Не вдалося імпортувати"; +/* No comment provided by engineer. */ +"Import theme" = "Імпорт теми"; + /* No comment provided by engineer. */ "Importing archive" = "Імпорт архіву"; @@ -2088,6 +2422,12 @@ /* No comment provided by engineer. */ "In reply to" = "У відповідь на"; +/* No comment provided by engineer. */ +"In-call sounds" = "Звуки вхідного дзвінка"; + +/* No comment provided by engineer. */ +"inactive" = "неактивний"; + /* No comment provided by engineer. */ "Incognito" = "Інкогніто"; @@ -2151,6 +2491,9 @@ /* No comment provided by engineer. */ "Interface" = "Інтерфейс"; +/* No comment provided by engineer. */ +"Interface colors" = "Кольори інтерфейсу"; + /* invalid chat data */ "invalid chat" = "недійсний чат"; @@ -2193,6 +2536,9 @@ /* group name */ "invitation to group %@" = "запрошення до групи %@"; +/* No comment provided by engineer. */ +"invite" = "запросити"; + /* No comment provided by engineer. */ "Invite friends" = "Запросити друзів"; @@ -2238,6 +2584,9 @@ /* No comment provided by engineer. */ "It can happen when:\n1. The messages expired in the sending client after 2 days or on the server after 30 days.\n2. Message decryption failed, because you or your contact used old database backup.\n3. The connection was compromised." = "Це може статися, коли:\n1. Термін дії повідомлень закінчився в клієнті-відправнику через 2 дні або на сервері через 30 днів.\n2. Не вдалося розшифрувати повідомлення, тому що ви або ваш контакт використовували стару резервну копію бази даних.\n3. З'єднання було скомпрометовано."; +/* No comment provided by engineer. */ +"It protects your IP address and connections." = "Він захищає вашу IP-адресу та з'єднання."; + /* No comment provided by engineer. */ "It seems like you are already connected via this link. If it is not the case, there was an error (%@)." = "Схоже, що ви вже підключені за цим посиланням. Якщо це не так, сталася помилка (%@)."; @@ -2250,7 +2599,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "Японський інтерфейс"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "Приєднуйтесь"; /* No comment provided by engineer. */ @@ -2280,6 +2629,9 @@ /* No comment provided by engineer. */ "Keep" = "Тримай"; +/* No comment provided by engineer. */ +"Keep conversation" = "Підтримуйте розмову"; + /* No comment provided by engineer. */ "Keep the app open to use it from desktop" = "Тримайте додаток відкритим, щоб використовувати його з робочого столу"; @@ -2301,7 +2653,7 @@ /* No comment provided by engineer. */ "Learn more" = "Дізнайтеся більше"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "Залишити"; /* No comment provided by engineer. */ @@ -2391,6 +2743,12 @@ /* No comment provided by engineer. */ "Max 30 seconds, received instantly." = "Максимум 30 секунд, отримується миттєво."; +/* No comment provided by engineer. */ +"Media & file servers" = "Медіа та файлові сервери"; + +/* blur media */ +"Medium" = "Середній"; + /* member role */ "member" = "учасник"; @@ -2403,6 +2761,9 @@ /* rcv group event chat item */ "member connected" = "з'єднаний"; +/* item status text */ +"Member inactive" = "Користувач неактивний"; + /* No comment provided by engineer. */ "Member role will be changed to \"%@\". All group members will be notified." = "Роль учасника буде змінено на \"%@\". Всі учасники групи будуть повідомлені про це."; @@ -2412,15 +2773,33 @@ /* No comment provided by engineer. */ "Member will be removed from group - this cannot be undone!" = "Учасник буде видалений з групи - це неможливо скасувати!"; +/* No comment provided by engineer. */ +"Menus" = "Меню"; + +/* No comment provided by engineer. */ +"message" = "повідомлення"; + /* item status text */ "Message delivery error" = "Помилка доставки повідомлення"; /* No comment provided by engineer. */ "Message delivery receipts!" = "Підтвердження доставки повідомлення!"; +/* item status text */ +"Message delivery warning" = "Попередження про доставку повідомлення"; + /* No comment provided by engineer. */ "Message draft" = "Чернетка повідомлення"; +/* item status text */ +"Message forwarded" = "Повідомлення переслано"; + +/* item status description */ +"Message may be delivered later if member becomes active." = "Повідомлення може бути доставлене пізніше, якщо користувач стане активним."; + +/* No comment provided by engineer. */ +"Message queue info" = "Інформація про чергу повідомлень"; + /* chat feature */ "Message reactions" = "Реакції на повідомлення"; @@ -2433,6 +2812,21 @@ /* notification */ "message received" = "повідомлення отримано"; +/* No comment provided by engineer. */ +"Message reception" = "Прийом повідомлень"; + +/* No comment provided by engineer. */ +"Message servers" = "Сервери повідомлень"; + +/* No comment provided by engineer. */ +"Message source remains private." = "Джерело повідомлення залишається приватним."; + +/* No comment provided by engineer. */ +"Message status" = "Статус повідомлення"; + +/* copied message info */ +"Message status: %@" = "Статус повідомлення: %@"; + /* No comment provided by engineer. */ "Message text" = "Текст повідомлення"; @@ -2448,6 +2842,12 @@ /* No comment provided by engineer. */ "Messages from %@ will be shown!" = "Повідомлення від %@ будуть показані!"; +/* No comment provided by engineer. */ +"Messages received" = "Отримані повідомлення"; + +/* No comment provided by engineer. */ +"Messages sent" = "Надіслані повідомлення"; + /* No comment provided by engineer. */ "Messages, files and calls are protected by **end-to-end encryption** with perfect forward secrecy, repudiation and break-in recovery." = "Повідомлення, файли та дзвінки захищені **наскрізним шифруванням** з ідеальною секретністю переадресації, відмовою та відновленням після злому."; @@ -2517,27 +2917,39 @@ /* No comment provided by engineer. */ "More improvements are coming soon!" = "Незабаром буде ще більше покращень!"; +/* No comment provided by engineer. */ +"More reliable network connection." = "Більш надійне з'єднання з мережею."; + /* item status description */ "Most likely this connection is deleted." = "Швидше за все, це з'єднання видалено."; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "Швидше за все, цей контакт видалив зв'язок з вами."; - /* No comment provided by engineer. */ "Multiple chat profiles" = "Кілька профілів чату"; /* No comment provided by engineer. */ +"mute" = "приглушити"; + +/* swipe action */ "Mute" = "Вимкнути звук"; /* No comment provided by engineer. */ "Muted when inactive!" = "Вимкнено, коли неактивний!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "Ім'я"; /* No comment provided by engineer. */ "Network & servers" = "Мережа та сервери"; +/* No comment provided by engineer. */ +"Network connection" = "Підключення до мережі"; + +/* snd error text */ +"Network issues - message expired after many attempts to send it." = "Проблеми з мережею - термін дії повідомлення закінчився після багатьох спроб надіслати його."; + +/* No comment provided by engineer. */ +"Network management" = "Керування мережею"; + /* No comment provided by engineer. */ "Network settings" = "Налаштування мережі"; @@ -2550,6 +2962,9 @@ /* No comment provided by engineer. */ "New chat" = "Новий чат"; +/* No comment provided by engineer. */ +"New chat experience 🎉" = "Новий досвід спілкування в чаті 🎉"; + /* notification */ "New contact request" = "Новий запит на контакт"; @@ -2568,6 +2983,9 @@ /* No comment provided by engineer. */ "New in %@" = "Нове в %@"; +/* No comment provided by engineer. */ +"New media options" = "Нові медіа-опції"; + /* No comment provided by engineer. */ "New member role" = "Нова роль учасника"; @@ -2604,6 +3022,9 @@ /* No comment provided by engineer. */ "No device token!" = "Токен пристрою відсутній!"; +/* item status description */ +"No direct connection yet, message is forwarded by admin." = "Прямого зв'язку ще немає, повідомлення пересилається адміністратором."; + /* No comment provided by engineer. */ "no e2e encryption" = "без шифрування e2e"; @@ -2616,6 +3037,12 @@ /* No comment provided by engineer. */ "No history" = "Немає історії"; +/* No comment provided by engineer. */ +"No info, try to reload" = "Немає інформації, спробуйте перезавантажити"; + +/* No comment provided by engineer. */ +"No network connection" = "Немає підключення до мережі"; + /* No comment provided by engineer. */ "No permission to record voice message" = "Немає дозволу на запис голосового повідомлення"; @@ -2628,6 +3055,9 @@ /* No comment provided by engineer. */ "Not compatible!" = "Не сумісні!"; +/* No comment provided by engineer. */ +"Nothing selected" = "Нічого не вибрано"; + /* No comment provided by engineer. */ "Notifications" = "Сповіщення"; @@ -2645,7 +3075,7 @@ time to disappear */ "off" = "вимкнено"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "Вимкнено"; /* feature offered item */ @@ -2673,10 +3103,10 @@ "One-time invitation link" = "Посилання на одноразове запрошення"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "Для підключення будуть потрібні хости onion. Потрібно увімкнути VPN."; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Для підключення будуть потрібні хости onion.\nПотрібно увімкнути VPN."; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "Onion хости будуть використовуватися, коли вони будуть доступні. Потрібно увімкнути VPN."; +"Onion hosts will be used when available.\nRequires compatible VPN." = "Onion хости будуть використовуватися, коли вони будуть доступні.\nПотрібно увімкнути VPN."; /* No comment provided by engineer. */ "Onion hosts will not be used." = "Onion хости не будуть використовуватися."; @@ -2684,6 +3114,9 @@ /* No comment provided by engineer. */ "Only client devices store user profiles, contacts, groups, and messages sent with **2-layer end-to-end encryption**." = "Тільки клієнтські пристрої зберігають профілі користувачів, контакти, групи та повідомлення, надіслані за допомогою **2-шарового наскрізного шифрування**."; +/* No comment provided by engineer. */ +"Only delete conversation" = "Видаляйте тільки розмови"; + /* No comment provided by engineer. */ "Only group owners can change group preferences." = "Тільки власники груп можуть змінювати налаштування групи."; @@ -2738,6 +3171,9 @@ /* authentication reason */ "Open migration to another device" = "Відкрита міграція на інший пристрій"; +/* No comment provided by engineer. */ +"Open server settings" = "Відкрити налаштування сервера"; + /* No comment provided by engineer. */ "Open Settings" = "Відкрийте Налаштування"; @@ -2762,9 +3198,24 @@ /* No comment provided by engineer. */ "Or show this code" = "Або покажіть цей код"; +/* No comment provided by engineer. */ +"other" = "інший"; + +/* No comment provided by engineer. */ +"Other" = "Інше"; + +/* No comment provided by engineer. */ +"Other %@ servers" = "Інші сервери %@"; + +/* No comment provided by engineer. */ +"other errors" = "інші помилки"; + /* member role */ "owner" = "власник"; +/* feature role */ +"owners" = "власники"; + /* No comment provided by engineer. */ "Passcode" = "Пароль"; @@ -2801,6 +3252,9 @@ /* No comment provided by engineer. */ "peer-to-peer" = "одноранговий"; +/* No comment provided by engineer. */ +"Pending" = "В очікуванні"; + /* No comment provided by engineer. */ "People can connect to you only via the links you share." = "Люди можуть зв'язатися з вами лише за посиланнями, якими ви ділитеся."; @@ -2819,9 +3273,18 @@ /* No comment provided by engineer. */ "PING interval" = "Інтервал PING"; +/* No comment provided by engineer. */ +"Play from the chat list." = "Грати зі списку чату."; + +/* No comment provided by engineer. */ +"Please ask your contact to enable calls." = "Будь ласка, попросіть свого контакту ввімкнути дзвінки."; + /* No comment provided by engineer. */ "Please ask your contact to enable sending voice messages." = "Будь ласка, попросіть вашого контакту увімкнути відправку голосових повідомлень."; +/* No comment provided by engineer. */ +"Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.\nPlease share any other issues with the developers." = "Переконайтеся, що мобільний і настільний комп'ютери підключені до однієї локальної мережі, і що брандмауер настільного комп'ютера дозволяє з'єднання.\nБудь ласка, повідомте про будь-які інші проблеми розробникам."; + /* No comment provided by engineer. */ "Please check that you used the correct link or ask your contact to send you another one." = "Будь ласка, перевірте, чи ви скористалися правильним посиланням, або попросіть контактну особу надіслати вам інше."; @@ -2879,6 +3342,9 @@ /* No comment provided by engineer. */ "Preview" = "Попередній перегляд"; +/* No comment provided by engineer. */ +"Previously connected servers" = "Раніше підключені сервери"; + /* No comment provided by engineer. */ "Privacy & security" = "Конфіденційність і безпека"; @@ -2888,15 +3354,30 @@ /* No comment provided by engineer. */ "Private filenames" = "Приватні імена файлів"; +/* No comment provided by engineer. */ +"Private message routing" = "Маршрутизація приватних повідомлень"; + +/* No comment provided by engineer. */ +"Private message routing 🚀" = "Маршрутизація приватних повідомлень 🚀"; + /* name of notes to self */ "Private notes" = "Приватні нотатки"; +/* No comment provided by engineer. */ +"Private routing" = "Приватна маршрутизація"; + +/* No comment provided by engineer. */ +"Private routing error" = "Помилка приватної маршрутизації"; + /* No comment provided by engineer. */ "Profile and server connections" = "З'єднання профілю та сервера"; /* No comment provided by engineer. */ "Profile image" = "Зображення профілю"; +/* No comment provided by engineer. */ +"Profile images" = "Зображення профілю"; + /* No comment provided by engineer. */ "Profile name" = "Назва профілю"; @@ -2906,6 +3387,9 @@ /* No comment provided by engineer. */ "Profile password" = "Пароль до профілю"; +/* No comment provided by engineer. */ +"Profile theme" = "Тема профілю"; + /* No comment provided by engineer. */ "Profile update will be sent to your contacts." = "Оновлення профілю буде надіслано вашим контактам."; @@ -2930,21 +3414,36 @@ /* No comment provided by engineer. */ "Prohibit sending files and media." = "Заборонити надсилання файлів і медіа."; +/* No comment provided by engineer. */ +"Prohibit sending SimpleX links." = "Заборонити надсилання посилань SimpleX."; + /* No comment provided by engineer. */ "Prohibit sending voice messages." = "Заборонити надсилання голосових повідомлень."; /* No comment provided by engineer. */ "Protect app screen" = "Захистіть екран програми"; +/* No comment provided by engineer. */ +"Protect IP address" = "Захист IP-адреси"; + /* No comment provided by engineer. */ "Protect your chat profiles with a password!" = "Захистіть свої профілі чату паролем!"; +/* No comment provided by engineer. */ +"Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings." = "Захистіть свою IP-адресу від ретрансляторів повідомлень, обраних вашими контактами.\nУвімкніть у налаштуваннях *Мережа та сервери*."; + /* No comment provided by engineer. */ "Protocol timeout" = "Тайм-аут протоколу"; /* No comment provided by engineer. */ "Protocol timeout per KB" = "Тайм-аут протоколу на КБ"; +/* No comment provided by engineer. */ +"Proxied" = "Проксі-сервер"; + +/* No comment provided by engineer. */ +"Proxied servers" = "Проксі-сервери"; + /* No comment provided by engineer. */ "Push notifications" = "Push-повідомлення"; @@ -2960,10 +3459,13 @@ /* No comment provided by engineer. */ "Rate the app" = "Оцініть додаток"; +/* No comment provided by engineer. */ +"Reachable chat toolbar" = "Доступна панель інструментів чату"; + /* chat item menu */ "React…" = "Реагуй…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "Читати"; /* No comment provided by engineer. */ @@ -2987,6 +3489,9 @@ /* No comment provided by engineer. */ "Receipts are disabled" = "Підтвердження виключені"; +/* No comment provided by engineer. */ +"Receive errors" = "Отримання помилок"; + /* No comment provided by engineer. */ "received answer…" = "отримали відповідь…"; @@ -3005,6 +3510,15 @@ /* message info title */ "Received message" = "Отримано повідомлення"; +/* No comment provided by engineer. */ +"Received messages" = "Отримані повідомлення"; + +/* No comment provided by engineer. */ +"Received reply" = "Отримано відповідь"; + +/* No comment provided by engineer. */ +"Received total" = "Отримано всього"; + /* No comment provided by engineer. */ "Receiving address will be changed to a different server. Address change will complete after sender comes online." = "Адреса отримувача буде змінена на інший сервер. Зміна адреси завершиться після того, як відправник з'явиться в мережі."; @@ -3017,12 +3531,30 @@ /* No comment provided by engineer. */ "Recent history and improved [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)." = "Нещодавня історія та покращення [directory bot](simplex:/contact#/?v=1-4&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FeXSPwqTkKyDO3px4fLf1wx3MvPdjdLW3%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAaiv6MkMH44L2TcYrt_CsX3ZvM11WgbMEUn0hkIKTOho%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion)."; +/* No comment provided by engineer. */ +"Recipient(s) can't see who this message is from." = "Одержувач(и) не бачить, від кого це повідомлення."; + /* No comment provided by engineer. */ "Recipients see updates as you type them." = "Одержувачі бачать оновлення, коли ви їх вводите."; +/* No comment provided by engineer. */ +"Reconnect" = "Повторне підключення"; + /* No comment provided by engineer. */ "Reconnect all connected servers to force message delivery. It uses additional traffic." = "Перепідключіть всі підключені сервери, щоб примусово доставити повідомлення. Це використовує додатковий трафік."; +/* No comment provided by engineer. */ +"Reconnect all servers" = "Перепідключіть усі сервери"; + +/* No comment provided by engineer. */ +"Reconnect all servers?" = "Перепідключити всі сервери?"; + +/* No comment provided by engineer. */ +"Reconnect server to force message delivery. It uses additional traffic." = "Перепідключити сервер для примусової доставки повідомлень. Використовує додатковий трафік."; + +/* No comment provided by engineer. */ +"Reconnect server?" = "Перепідключити сервер?"; + /* No comment provided by engineer. */ "Reconnect servers?" = "Перепідключити сервери?"; @@ -3035,7 +3567,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "Зменшення використання акумулятора"; -/* reject incoming call via notification */ +/* reject incoming call via notification + swipe action */ "Reject" = "Відхилити"; /* No comment provided by engineer. */ @@ -3056,6 +3589,9 @@ /* No comment provided by engineer. */ "Remove" = "Видалити"; +/* No comment provided by engineer. */ +"Remove image" = "Видалити зображення"; + /* No comment provided by engineer. */ "Remove member" = "Видалити учасника"; @@ -3113,12 +3649,27 @@ /* No comment provided by engineer. */ "Reset" = "Перезавантаження"; +/* No comment provided by engineer. */ +"Reset all hints" = "Скинути всі підказки"; + +/* No comment provided by engineer. */ +"Reset all statistics" = "Скинути всю статистику"; + +/* No comment provided by engineer. */ +"Reset all statistics?" = "Скинути всю статистику?"; + /* No comment provided by engineer. */ "Reset colors" = "Скинути кольори"; +/* No comment provided by engineer. */ +"Reset to app theme" = "Повернутися до теми програми"; + /* No comment provided by engineer. */ "Reset to defaults" = "Відновити налаштування за замовчуванням"; +/* No comment provided by engineer. */ +"Reset to user theme" = "Повернутися до теми користувача"; + /* No comment provided by engineer. */ "Restart the app to create a new chat profile" = "Перезапустіть програму, щоб створити новий профіль чату"; @@ -3143,9 +3694,6 @@ /* chat item action */ "Reveal" = "Показувати"; -/* No comment provided by engineer. */ -"Revert" = "Повернутися"; - /* No comment provided by engineer. */ "Revoke" = "Відкликати"; @@ -3161,6 +3709,9 @@ /* No comment provided by engineer. */ "Run chat" = "Запустити чат"; +/* No comment provided by engineer. */ +"Safely receive files" = "Безпечне отримання файлів"; + /* No comment provided by engineer. */ "Safer groups" = "Безпечніші групи"; @@ -3176,6 +3727,9 @@ /* No comment provided by engineer. */ "Save and notify group members" = "Зберегти та повідомити учасників групи"; +/* No comment provided by engineer. */ +"Save and reconnect" = "Збережіть і підключіться знову"; + /* No comment provided by engineer. */ "Save and update group profile" = "Збереження та оновлення профілю групи"; @@ -3212,12 +3766,30 @@ /* No comment provided by engineer. */ "Save welcome message?" = "Зберегти вітальне повідомлення?"; +/* No comment provided by engineer. */ +"saved" = "збережено"; + +/* No comment provided by engineer. */ +"Saved" = "Збережено"; + +/* No comment provided by engineer. */ +"Saved from" = "Збережено з"; + +/* No comment provided by engineer. */ +"saved from %@" = "збережено з %@"; + /* message info title */ "Saved message" = "Збережене повідомлення"; /* No comment provided by engineer. */ "Saved WebRTC ICE servers will be removed" = "Збережені сервери WebRTC ICE буде видалено"; +/* No comment provided by engineer. */ +"Scale" = "Масштаб"; + +/* No comment provided by engineer. */ +"Scan / Paste link" = "Відсканувати / Вставити посилання"; + /* No comment provided by engineer. */ "Scan code" = "Сканувати код"; @@ -3233,6 +3805,9 @@ /* No comment provided by engineer. */ "Scan server QR code" = "Відскануйте QR-код сервера"; +/* No comment provided by engineer. */ +"search" = "пошук"; + /* No comment provided by engineer. */ "Search" = "Пошук"; @@ -3245,6 +3820,9 @@ /* network option */ "sec" = "сек"; +/* No comment provided by engineer. */ +"Secondary" = "Вторинний"; + /* time unit */ "seconds" = "секунди"; @@ -3254,6 +3832,9 @@ /* server test step */ "Secure queue" = "Безпечна черга"; +/* No comment provided by engineer. */ +"Secured" = "Забезпечено"; + /* No comment provided by engineer. */ "Security assessment" = "Оцінка безпеки"; @@ -3263,9 +3844,15 @@ /* chat item text */ "security code changed" = "змінено код безпеки"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "Виберіть"; +/* No comment provided by engineer. */ +"Selected %lld" = "Вибрано %lld"; + +/* No comment provided by engineer. */ +"Selected chat preferences prohibit this message." = "Вибрані налаштування чату забороняють це повідомлення."; + /* No comment provided by engineer. */ "Self-destruct" = "Самознищення"; @@ -3290,21 +3877,30 @@ /* No comment provided by engineer. */ "send direct message" = "надіслати пряме повідомлення"; -/* No comment provided by engineer. */ -"Send direct message" = "Надішліть пряме повідомлення"; - /* No comment provided by engineer. */ "Send direct message to connect" = "Надішліть пряме повідомлення, щоб підключитися"; /* No comment provided by engineer. */ "Send disappearing message" = "Надіслати зникаюче повідомлення"; +/* No comment provided by engineer. */ +"Send errors" = "Помилки надсилання"; + /* No comment provided by engineer. */ "Send link previews" = "Надіслати попередній перегляд за посиланням"; /* No comment provided by engineer. */ "Send live message" = "Надіслати живе повідомлення"; +/* No comment provided by engineer. */ +"Send message to enable calls." = "Надішліть повідомлення, щоб увімкнути дзвінки."; + +/* No comment provided by engineer. */ +"Send messages directly when IP address is protected and your or destination server does not support private routing." = "Надсилайте повідомлення напряму, якщо IP-адреса захищена, а ваш сервер або сервер призначення не підтримує приватну маршрутизацію."; + +/* No comment provided by engineer. */ +"Send messages directly when your or destination server does not support private routing." = "Надсилайте повідомлення напряму, якщо ваш сервер або сервер призначення не підтримує приватну маршрутизацію."; + /* No comment provided by engineer. */ "Send notifications" = "Надсилати сповіщення"; @@ -3359,15 +3955,42 @@ /* copied message info */ "Sent at: %@" = "Надіслано за: %@"; +/* No comment provided by engineer. */ +"Sent directly" = "Відправлено напряму"; + /* notification */ "Sent file event" = "Подія надісланого файлу"; /* message info title */ "Sent message" = "Надіслано повідомлення"; +/* No comment provided by engineer. */ +"Sent messages" = "Надіслані повідомлення"; + /* No comment provided by engineer. */ "Sent messages will be deleted after set time." = "Надіслані повідомлення будуть видалені через встановлений час."; +/* No comment provided by engineer. */ +"Sent reply" = "Надіслано відповідь"; + +/* No comment provided by engineer. */ +"Sent total" = "Відправлено всього"; + +/* No comment provided by engineer. */ +"Sent via proxy" = "Відправлено через проксі"; + +/* No comment provided by engineer. */ +"Server address" = "Адреса сервера"; + +/* No comment provided by engineer. */ +"Server address is incompatible with network settings: %@." = "Адреса сервера несумісна з налаштуваннями мережі: %@."; + +/* srv error text. */ +"Server address is incompatible with network settings." = "Адреса сервера несумісна з налаштуваннями мережі."; + +/* queue info */ +"server queue info: %@\n\nlast received msg: %@" = "інформація про чергу на сервері: %1$@\n\nостаннє отримане повідомлення: %2$@"; + /* server test error */ "Server requires authorization to create queues, check password" = "Сервер вимагає авторизації для створення черг, перевірте пароль"; @@ -3377,9 +4000,24 @@ /* No comment provided by engineer. */ "Server test failed!" = "Тест сервера завершився невдало!"; +/* No comment provided by engineer. */ +"Server type" = "Тип сервера"; + +/* srv error text */ +"Server version is incompatible with network settings." = "Серверна версія несумісна з мережевими налаштуваннями."; + +/* No comment provided by engineer. */ +"Server version is incompatible with your app: %@." = "Версія сервера несумісна з вашим додатком: %@."; + /* No comment provided by engineer. */ "Servers" = "Сервери"; +/* No comment provided by engineer. */ +"Servers info" = "Інформація про сервери"; + +/* No comment provided by engineer. */ +"Servers statistics will be reset - this cannot be undone!" = "Статистика серверів буде скинута - це неможливо скасувати!"; + /* No comment provided by engineer. */ "Session code" = "Код сесії"; @@ -3389,6 +4027,9 @@ /* No comment provided by engineer. */ "Set contact name…" = "Встановити ім'я контакту…"; +/* No comment provided by engineer. */ +"Set default theme" = "Встановлення теми за замовчуванням"; + /* No comment provided by engineer. */ "Set group preferences" = "Встановіть налаштування групи"; @@ -3419,6 +4060,9 @@ /* No comment provided by engineer. */ "Settings" = "Налаштування"; +/* No comment provided by engineer. */ +"Shape profile images" = "Сформуйте зображення профілю"; + /* chat item action */ "Share" = "Поділіться"; @@ -3431,15 +4075,24 @@ /* No comment provided by engineer. */ "Share address with contacts?" = "Поділіться адресою з контактами?"; +/* No comment provided by engineer. */ +"Share from other apps." = "Діліться з інших програм."; + /* No comment provided by engineer. */ "Share link" = "Поділіться посиланням"; /* No comment provided by engineer. */ "Share this 1-time invite link" = "Поділіться цим одноразовим посиланням-запрошенням"; +/* No comment provided by engineer. */ +"Share to SimpleX" = "Поділіться з SimpleX"; + /* No comment provided by engineer. */ "Share with contacts" = "Поділіться з контактами"; +/* No comment provided by engineer. */ +"Show → on messages sent via private routing." = "Показувати → у повідомленнях, надісланих через приватну маршрутизацію."; + /* No comment provided by engineer. */ "Show calls in phone history" = "Показувати дзвінки в історії дзвінків"; @@ -3449,6 +4102,12 @@ /* No comment provided by engineer. */ "Show last messages" = "Показати останні повідомлення"; +/* No comment provided by engineer. */ +"Show message status" = "Показати статус повідомлення"; + +/* No comment provided by engineer. */ +"Show percentage" = "Показати відсоток"; + /* No comment provided by engineer. */ "Show preview" = "Показати попередній перегляд"; @@ -3458,6 +4117,9 @@ /* No comment provided by engineer. */ "Show:" = "Показати:"; +/* No comment provided by engineer. */ +"SimpleX" = "SimpleX"; + /* No comment provided by engineer. */ "SimpleX address" = "Адреса SimpleX"; @@ -3479,6 +4141,12 @@ /* chat feature */ "SimpleX links" = "Посилання SimpleX"; +/* No comment provided by engineer. */ +"SimpleX links are prohibited in this group." = "У цій групі заборонені посилання на SimpleX."; + +/* No comment provided by engineer. */ +"SimpleX links not allowed" = "Посилання SimpleX заборонені"; + /* No comment provided by engineer. */ "SimpleX Lock" = "SimpleX Lock"; @@ -3497,6 +4165,9 @@ /* No comment provided by engineer. */ "Simplified incognito mode" = "Спрощений режим інкогніто"; +/* No comment provided by engineer. */ +"Size" = "Розмір"; + /* No comment provided by engineer. */ "Skip" = "Пропустити"; @@ -3507,14 +4178,26 @@ "Small groups (max 20)" = "Невеликі групи (максимум 20 осіб)"; /* No comment provided by engineer. */ -"SMP servers" = "Сервери SMP"; +"SMP server" = "Сервер SMP"; + +/* blur media */ +"Soft" = "М'який"; + +/* No comment provided by engineer. */ +"Some file(s) were not exported:" = "Деякі файли не було експортовано:"; /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "Під час імпорту виникли деякі нефатальні помилки – ви можете переглянути консоль чату, щоб дізнатися більше."; +/* No comment provided by engineer. */ +"Some non-fatal errors occurred during import:" = "Під час імпорту виникли деякі несмертельні помилки:"; + /* notification title */ "Somebody" = "Хтось"; +/* No comment provided by engineer. */ +"Square, circle, or anything in between." = "Квадрат, коло або щось середнє між ними."; + /* chat item text */ "standard end-to-end encryption" = "стандартне наскрізне шифрування"; @@ -3527,9 +4210,15 @@ /* No comment provided by engineer. */ "Start migration" = "Почати міграцію"; +/* No comment provided by engineer. */ +"Starting from %@." = "Починаючи з %@."; + /* No comment provided by engineer. */ "starting…" = "починаючи…"; +/* No comment provided by engineer. */ +"Statistics" = "Статистика"; + /* No comment provided by engineer. */ "Stop" = "Зупинити"; @@ -3569,9 +4258,21 @@ /* No comment provided by engineer. */ "strike" = "закреслено"; +/* blur media */ +"Strong" = "Сильний"; + /* No comment provided by engineer. */ "Submit" = "Надіслати"; +/* No comment provided by engineer. */ +"Subscribed" = "Підписано"; + +/* No comment provided by engineer. */ +"Subscription errors" = "Помилки підписки"; + +/* No comment provided by engineer. */ +"Subscriptions ignored" = "Підписки ігноруються"; + /* No comment provided by engineer. */ "Support SimpleX Chat" = "Підтримка чату SimpleX"; @@ -3606,7 +4307,7 @@ "Tap to scan" = "Натисніть, щоб сканувати"; /* No comment provided by engineer. */ -"Tap to start a new chat" = "Натисніть, щоб почати новий чат"; +"TCP connection" = "TCP-з'єднання"; /* No comment provided by engineer. */ "TCP connection timeout" = "Тайм-аут TCP-з'єднання"; @@ -3620,6 +4321,9 @@ /* No comment provided by engineer. */ "TCP_KEEPINTVL" = "TCP_KEEPINTVL"; +/* No comment provided by engineer. */ +"Temporary file error" = "Тимчасова помилка файлу"; + /* server test failure */ "Test failed at step %@." = "Тест завершився невдало на кроці %@."; @@ -3647,6 +4351,9 @@ /* No comment provided by engineer. */ "The app can notify you when you receive messages or contact requests - please open settings to enable." = "Додаток може сповіщати вас, коли ви отримуєте повідомлення або запити на контакт - будь ласка, відкрийте налаштування, щоб увімкнути цю функцію."; +/* No comment provided by engineer. */ +"The app will ask to confirm downloads from unknown file servers (except .onion)." = "Програма попросить підтвердити завантаження з невідомих файлових серверів (крім .onion)."; + /* No comment provided by engineer. */ "The attempt to change database passphrase was not completed." = "Спроба змінити пароль до бази даних не була завершена."; @@ -3677,6 +4384,12 @@ /* No comment provided by engineer. */ "The message will be marked as moderated for all members." = "Повідомлення буде позначено як модероване для всіх учасників."; +/* No comment provided by engineer. */ +"The messages will be deleted for all members." = "Повідомлення будуть видалені для всіх учасників."; + +/* No comment provided by engineer. */ +"The messages will be marked as moderated for all members." = "Повідомлення будуть позначені як модеровані для всіх учасників."; + /* No comment provided by engineer. */ "The next generation of private messaging" = "Наступне покоління приватних повідомлень"; @@ -3699,7 +4412,7 @@ "The text you pasted is not a SimpleX link." = "Текст, який ви вставили, не є посиланням SimpleX."; /* No comment provided by engineer. */ -"Theme" = "Тема"; +"Themes" = "Теми"; /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "Ці налаштування стосуються вашого поточного профілю **%@**."; @@ -3743,9 +4456,15 @@ /* No comment provided by engineer. */ "This is your own SimpleX address!" = "Це ваша власна SimpleX-адреса!"; +/* No comment provided by engineer. */ +"This link was used with another mobile device, please create a new link on the desktop." = "Це посилання було використано з іншого мобільного пристрою, будь ласка, створіть нове посилання на робочому столі."; + /* No comment provided by engineer. */ "This setting applies to messages in your current chat profile **%@**." = "Це налаштування застосовується до повідомлень у вашому поточному профілі чату **%@**."; +/* No comment provided by engineer. */ +"Title" = "Заголовок"; + /* No comment provided by engineer. */ "To ask any questions and to receive updates:" = "Задати будь-які питання та отримувати новини:"; @@ -3767,6 +4486,9 @@ /* No comment provided by engineer. */ "To protect your information, turn on SimpleX Lock.\nYou will be prompted to complete authentication before this feature is enabled." = "Щоб захистити вашу інформацію, увімкніть SimpleX Lock.\nПеред увімкненням цієї функції вам буде запропоновано пройти автентифікацію."; +/* No comment provided by engineer. */ +"To protect your IP address, private routing uses your SMP servers to deliver messages." = "Щоб захистити вашу IP-адресу, приватна маршрутизація використовує ваші SMP-сервери для доставки повідомлень."; + /* No comment provided by engineer. */ "To record voice message please grant permission to use Microphone." = "Щоб записати голосове повідомлення, будь ласка, надайте дозвіл на використання мікрофону."; @@ -3779,12 +4501,24 @@ /* No comment provided by engineer. */ "To verify end-to-end encryption with your contact compare (or scan) the code on your devices." = "Щоб перевірити наскрізне шифрування з вашим контактом, порівняйте (або відскануйте) код на ваших пристроях."; +/* No comment provided by engineer. */ +"Toggle chat list:" = "Перемикання списку чату:"; + /* No comment provided by engineer. */ "Toggle incognito when connecting." = "Увімкніть інкогніто при підключенні."; +/* No comment provided by engineer. */ +"Toolbar opacity" = "Непрозорість панелі інструментів"; + +/* No comment provided by engineer. */ +"Total" = "Всього"; + /* No comment provided by engineer. */ "Transport isolation" = "Транспортна ізоляція"; +/* No comment provided by engineer. */ +"Transport sessions" = "Транспортні сесії"; + /* No comment provided by engineer. */ "Trying to connect to the server used to receive messages from this contact (error: %@)." = "Спроба з'єднатися з сервером, який використовується для отримання повідомлень від цього контакту (помилка: %@)."; @@ -3821,13 +4555,10 @@ /* rcv group event chat item */ "unblocked %@" = "розблоковано %@"; -/* item status description */ -"Unexpected error: %@" = "Неочікувана помилка: %@"; - /* No comment provided by engineer. */ "Unexpected migration state" = "Неочікуваний стан міграції"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "Нелюб."; /* No comment provided by engineer. */ @@ -3854,6 +4585,12 @@ /* No comment provided by engineer. */ "Unknown error" = "Невідома помилка"; +/* No comment provided by engineer. */ +"unknown servers" = "невідомі реле"; + +/* No comment provided by engineer. */ +"Unknown servers!" = "Невідомі сервери!"; + /* No comment provided by engineer. */ "unknown status" = "невідомий статус"; @@ -3876,9 +4613,15 @@ "Unlock app" = "Розблокувати додаток"; /* No comment provided by engineer. */ +"unmute" = "увімкнути звук"; + +/* swipe action */ "Unmute" = "Увімкнути звук"; /* No comment provided by engineer. */ +"unprotected" = "незахищені"; + +/* swipe action */ "Unread" = "Непрочитане"; /* No comment provided by engineer. */ @@ -3887,9 +4630,6 @@ /* No comment provided by engineer. */ "Update" = "Оновлення"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = "Оновити налаштування хостів .onion?"; - /* No comment provided by engineer. */ "Update database passphrase" = "Оновити парольну фразу бази даних"; @@ -3897,7 +4637,7 @@ "Update network settings?" = "Оновити налаштування мережі?"; /* No comment provided by engineer. */ -"Update transport isolation mode?" = "Оновити режим транспортної ізоляції?"; +"Update settings?" = "Оновити налаштування?"; /* rcv group event chat item */ "updated group profile" = "оновлений профіль групи"; @@ -3909,10 +4649,10 @@ "Updating settings will re-connect the client to all servers." = "Оновлення налаштувань призведе до перепідключення клієнта до всіх серверів."; /* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "Оновлення цього параметра призведе до перепідключення клієнта до всіх серверів."; +"Upgrade and open chat" = "Оновлення та відкритий чат"; /* No comment provided by engineer. */ -"Upgrade and open chat" = "Оновлення та відкритий чат"; +"Upload errors" = "Помилки завантаження"; /* No comment provided by engineer. */ "Upload failed" = "Не вдалося завантфжити"; @@ -3920,6 +4660,12 @@ /* server test step */ "Upload file" = "Завантажити файл"; +/* No comment provided by engineer. */ +"Uploaded" = "Завантажено"; + +/* No comment provided by engineer. */ +"Uploaded files" = "Завантажені файли"; + /* No comment provided by engineer. */ "Uploading archive" = "Завантаження архіву"; @@ -3947,6 +4693,12 @@ /* No comment provided by engineer. */ "Use only local notifications?" = "Використовувати лише локальні сповіщення?"; +/* No comment provided by engineer. */ +"Use private routing with unknown servers when IP address is not protected." = "Використовуйте приватну маршрутизацію з невідомими серверами, якщо IP-адреса не захищена."; + +/* No comment provided by engineer. */ +"Use private routing with unknown servers." = "Використовуйте приватну маршрутизацію з невідомими серверами."; + /* No comment provided by engineer. */ "Use server" = "Використовувати сервер"; @@ -3956,11 +4708,14 @@ /* No comment provided by engineer. */ "Use the app while in the call." = "Використовуйте додаток під час розмови."; +/* No comment provided by engineer. */ +"Use the app with one hand." = "Використовуйте додаток однією рукою."; + /* No comment provided by engineer. */ "User profile" = "Профіль користувача"; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "Для використання хостів .onion потрібен сумісний VPN-провайдер."; +"User selection" = "Вибір користувача"; /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "Використання серверів SimpleX Chat."; @@ -4010,6 +4765,9 @@ /* No comment provided by engineer. */ "Via secure quantum resistant protocol." = "Через безпечний квантово-стійкий протокол."; +/* No comment provided by engineer. */ +"video" = "відео"; + /* No comment provided by engineer. */ "Video call" = "Відеодзвінок"; @@ -4043,6 +4801,9 @@ /* No comment provided by engineer. */ "Voice messages are prohibited in this group." = "Голосові повідомлення в цій групі заборонені."; +/* No comment provided by engineer. */ +"Voice messages not allowed" = "Голосові повідомлення заборонені"; + /* No comment provided by engineer. */ "Voice messages prohibited!" = "Голосові повідомлення заборонені!"; @@ -4064,6 +4825,12 @@ /* No comment provided by engineer. */ "Waiting for video" = "Чекаємо на відео"; +/* No comment provided by engineer. */ +"Wallpaper accent" = "Акцент на шпалерах"; + +/* No comment provided by engineer. */ +"Wallpaper background" = "Фон шпалер"; + /* No comment provided by engineer. */ "wants to connect to you!" = "хоче зв'язатися з вами!"; @@ -4094,12 +4861,27 @@ /* No comment provided by engineer. */ "When available" = "За наявності"; +/* No comment provided by engineer. */ +"When connecting audio and video calls." = "При підключенні аудіо та відеодзвінків."; + +/* No comment provided by engineer. */ +"when IP hidden" = "коли IP приховано"; + /* No comment provided by engineer. */ "When people request to connect, you can accept or reject it." = "Коли люди звертаються із запитом на підключення, ви можете прийняти або відхилити його."; /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "Коли ви ділитеся з кимось своїм профілем інкогніто, цей профіль буде використовуватися для груп, до яких вас запрошують."; +/* No comment provided by engineer. */ +"WiFi" = "WiFi"; + +/* No comment provided by engineer. */ +"Will be enabled in direct chats!" = "Буде ввімкнено в прямих чатах!"; + +/* No comment provided by engineer. */ +"Wired ethernet" = "Дротова мережа Ethernet"; + /* No comment provided by engineer. */ "With encrypted files and media." = "З зашифрованими файлами та медіа."; @@ -4109,18 +4891,33 @@ /* No comment provided by engineer. */ "With reduced battery usage." = "З меншим споживанням заряду акумулятора."; +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to file servers." = "Без Tor або VPN ваша IP-адреса буде видимою для файлових серверів."; + +/* No comment provided by engineer. */ +"Without Tor or VPN, your IP address will be visible to these XFTP relays: %@." = "Без Tor або VPN ваша IP-адреса буде видимою для цих XFTP-ретрансляторів: %@."; + /* No comment provided by engineer. */ "Wrong database passphrase" = "Неправильний пароль до бази даних"; +/* snd error text */ +"Wrong key or unknown connection - most likely this connection is deleted." = "Неправильний ключ або невідоме з'єднання - швидше за все, це з'єднання видалено."; + +/* file error text */ +"Wrong key or unknown file chunk address - most likely file is deleted." = "Неправильний ключ або невідома адреса фрагмента файлу - найімовірніше, файл видалено."; + /* No comment provided by engineer. */ "Wrong passphrase!" = "Неправильний пароль!"; /* No comment provided by engineer. */ -"XFTP servers" = "Сервери XFTP"; +"XFTP server" = "XFTP-сервер"; /* pref value */ "yes" = "так"; +/* No comment provided by engineer. */ +"you" = "ти"; + /* No comment provided by engineer. */ "You" = "Ти"; @@ -4169,6 +4966,9 @@ /* No comment provided by engineer. */ "You are invited to group" = "Запрошуємо вас до групи"; +/* No comment provided by engineer. */ +"You are not connected to these servers. Private routing is used to deliver messages to them." = "Не підключені до цих серверів. Для доставлення повідомлень до них використовується приватна маршрутизація."; + /* No comment provided by engineer. */ "you are observer" = "ви спостерігач"; @@ -4178,6 +4978,9 @@ /* No comment provided by engineer. */ "You can accept calls from lock screen, without device and app authentication." = "Ви можете приймати дзвінки з екрана блокування без автентифікації пристрою та програми."; +/* No comment provided by engineer. */ +"You can change it in Appearance settings." = "Ви можете змінити його в налаштуваннях зовнішнього вигляду."; + /* No comment provided by engineer. */ "You can create it later" = "Ви можете створити його пізніше"; @@ -4197,7 +5000,10 @@ "You can make it visible to your SimpleX contacts via Settings." = "Ви можете зробити його видимим для ваших контактів у SimpleX за допомогою налаштувань."; /* notification body */ -"You can now send messages to %@" = "Тепер ви можете надсилати повідомлення на адресу %@"; +"You can now chat with %@" = "Тепер ви можете надсилати повідомлення на адресу %@"; + +/* No comment provided by engineer. */ +"You can send messages to %@ from Archived contacts." = "Ви можете надсилати повідомлення на %@ з архівних контактів."; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "Ви можете налаштувати попередній перегляд сповіщень на екрані блокування за допомогою налаштувань."; @@ -4214,6 +5020,9 @@ /* No comment provided by engineer. */ "You can start chat via app Settings / Database or by restarting the app" = "Запустити чат можна через Налаштування програми / База даних або перезапустивши програму"; +/* No comment provided by engineer. */ +"You can still view conversation with %@ in the list of chats." = "Ви все ще можете переглянути розмову з %@ у списку чатів."; + /* No comment provided by engineer. */ "You can turn on SimpleX Lock via Settings." = "Увімкнути SimpleX Lock можна в Налаштуваннях."; @@ -4250,9 +5059,6 @@ /* No comment provided by engineer. */ "You have already requested connection!\nRepeat connection request?" = "Ви вже надіслали запит на підключення!\nПовторити запит на підключення?"; -/* No comment provided by engineer. */ -"You have no chats" = "У вас немає чатів"; - /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "Вам доведеться вводити парольну фразу щоразу під час запуску програми - вона не зберігається на пристрої."; @@ -4268,9 +5074,18 @@ /* snd group event chat item */ "you left" = "ти пішов"; +/* No comment provided by engineer. */ +"You may migrate the exported database." = "Ви можете мігрувати експортовану базу даних."; + +/* No comment provided by engineer. */ +"You may save the exported archive." = "Ви можете зберегти експортований архів."; + /* No comment provided by engineer. */ "You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts." = "Ви повинні використовувати найновішу версію бази даних чату ТІЛЬКИ на одному пристрої, інакше ви можете перестати отримувати повідомлення від деяких контактів."; +/* No comment provided by engineer. */ +"You need to allow your contact to call to be able to call them." = "Щоб мати змогу зателефонувати контакту, вам потрібно дозволити йому зателефонувати."; + /* No comment provided by engineer. */ "You need to allow your contact to send voice messages to be able to send them." = "Щоб мати змогу надсилати голосові повідомлення, вам потрібно дозволити контакту надсилати їх."; @@ -4343,9 +5158,6 @@ /* No comment provided by engineer. */ "Your chat profiles" = "Ваші профілі чату"; -/* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "Для завершення з'єднання ваш контакт має бути онлайн.\nВи можете скасувати це з'єднання і видалити контакт (і спробувати пізніше з новим посиланням)."; - /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "Ваш контакт надіслав файл, розмір якого перевищує підтримуваний на цей момент максимальний розмір (%@)."; diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index 5af77e4e51..1d37572498 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -133,9 +133,6 @@ /* No comment provided by engineer. */ "%@ is verified" = "%@ 已认证"; -/* No comment provided by engineer. */ -"%@ servers" = "%@ 服务器"; - /* notification title */ "%@ wants to connect!" = "%@ 要连接!"; @@ -301,11 +298,9 @@ /* No comment provided by engineer. */ "above, then choose:" = "上面,然后选择:"; -/* No comment provided by engineer. */ -"Accent color" = "色调"; - /* accept contact request via notification - accept incoming call via notification */ + accept incoming call via notification + swipe action */ "Accept" = "接受"; /* No comment provided by engineer. */ @@ -314,7 +309,8 @@ /* notification body */ "Accept contact request from %@?" = "接受来自 %@ 的联系人请求?"; -/* accept contact request via notification */ +/* accept contact request via notification + swipe action */ "Accept incognito" = "接受隐身聊天"; /* call status */ @@ -333,7 +329,7 @@ "Add profile" = "添加个人资料"; /* No comment provided by engineer. */ -"Add server…" = "添加服务器…"; +"Add server" = "添加服务器"; /* No comment provided by engineer. */ "Add servers by scanning QR codes." = "扫描二维码来添加服务器。"; @@ -353,6 +349,12 @@ /* member role */ "admin" = "管理员"; +/* feature role */ +"admins" = "管理员"; + +/* No comment provided by engineer. */ +"Admins can block a member for all." = "管理员可以为所有人封禁一名成员。"; + /* No comment provided by engineer. */ "Admins can create the links to join groups." = "管理员可以创建链接以加入群组。"; @@ -377,6 +379,9 @@ /* No comment provided by engineer. */ "All group members will remain connected." = "所有群组成员将保持连接。"; +/* feature role */ +"all members" = "所有成员"; + /* No comment provided by engineer. */ "All messages will be deleted - this cannot be undone!" = "所有消息都将被删除 - 这无法被撤销!"; @@ -389,6 +394,9 @@ /* No comment provided by engineer. */ "All your contacts will remain connected. Profile update will be sent to your contacts." = "您的所有联系人将保持连接。个人资料更新将发送给您的联系人。"; +/* No comment provided by engineer. */ +"All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays." = "你的所有联系人、对话和文件将被安全加密并分块上传到配置的 XFTP 中继。"; + /* No comment provided by engineer. */ "Allow" = "允许"; @@ -419,6 +427,9 @@ /* No comment provided by engineer. */ "Allow to send files and media." = "允许发送文件和媒体。"; +/* No comment provided by engineer. */ +"Allow to send SimpleX links." = "允许发送 SimpleX 链接。"; + /* No comment provided by engineer. */ "Allow to send voice messages." = "允许发送语音消息。"; @@ -467,6 +478,9 @@ /* No comment provided by engineer. */ "App build: %@" = "应用程序构建:%@"; +/* No comment provided by engineer. */ +"App data migration" = "应用数据迁移"; + /* No comment provided by engineer. */ "App encrypts new local files (except videos)." = "应用程序为新的本地文件(视频除外)加密。"; @@ -488,6 +502,15 @@ /* No comment provided by engineer. */ "Appearance" = "外观"; +/* No comment provided by engineer. */ +"Apply" = "应用"; + +/* No comment provided by engineer. */ +"Archive and upload" = "存档和上传"; + +/* No comment provided by engineer. */ +"Archiving database" = "正在存档数据库"; + /* No comment provided by engineer. */ "Attach" = "附件"; @@ -635,6 +658,9 @@ /* No comment provided by engineer. */ "Cancel" = "取消"; +/* No comment provided by engineer. */ +"Cancel migration" = "取消迁移"; + /* feature offered item */ "cancelled %@" = "已取消 %@"; @@ -644,6 +670,9 @@ /* No comment provided by engineer. */ "Cannot receive file" = "无法接收文件"; +/* No comment provided by engineer. */ +"Cellular" = "移动网络"; + /* No comment provided by engineer. */ "Change" = "更改"; @@ -714,6 +743,9 @@ /* No comment provided by engineer. */ "Chat is stopped. If you already used this database on another device, you should transfer it back before starting chat." = "聊天已停止。如果你已经在另一台设备商使用过此数据库,你应该在启动聊天前将数据库传输回来。"; +/* No comment provided by engineer. */ +"Chat migrated!" = "已迁移聊天!"; + /* No comment provided by engineer. */ "Chat preferences" = "聊天偏好设置"; @@ -732,7 +764,7 @@ /* No comment provided by engineer. */ "Choose from library" = "从库中选择"; -/* No comment provided by engineer. */ +/* swipe action */ "Clear" = "清除"; /* No comment provided by engineer. */ @@ -750,9 +782,6 @@ /* No comment provided by engineer. */ "colored" = "彩色"; -/* No comment provided by engineer. */ -"Colors" = "颜色"; - /* server test step */ "Compare file" = "对比文件"; @@ -771,6 +800,9 @@ /* No comment provided by engineer. */ "Confirm database upgrades" = "确认数据库升级"; +/* No comment provided by engineer. */ +"Confirm network settings" = "确认网络设置"; + /* No comment provided by engineer. */ "Confirm new passphrase…" = "确认新密码……"; @@ -780,6 +812,12 @@ /* No comment provided by engineer. */ "Confirm password" = "确认密码"; +/* No comment provided by engineer. */ +"Confirm that you remember database passphrase to migrate it." = "请在迁移前确认你记得数据库的密码短语。"; + +/* No comment provided by engineer. */ +"Confirm upload" = "确认上传"; + /* server test step */ "Connect" = "连接"; @@ -888,9 +926,6 @@ /* notification */ "Contact is connected" = "联系已连接"; -/* No comment provided by engineer. */ -"Contact is not connected yet!" = "联系人尚未连接!"; - /* No comment provided by engineer. */ "Contact name" = "联系人姓名"; @@ -906,7 +941,7 @@ /* No comment provided by engineer. */ "Continue" = "继续"; -/* chat item action */ +/* No comment provided by engineer. */ "Copy" = "复制"; /* No comment provided by engineer. */ @@ -957,6 +992,9 @@ /* No comment provided by engineer. */ "Created on %@" = "创建于 %@"; +/* No comment provided by engineer. */ +"Creating archive link" = "正在创建存档链接"; + /* No comment provided by engineer. */ "Creating link…" = "创建链接中…"; @@ -1056,7 +1094,8 @@ /* No comment provided by engineer. */ "default (yes)" = "默认 (是)"; -/* chat item action */ +/* chat item action + swipe action */ "Delete" = "删除"; /* No comment provided by engineer. */ @@ -1093,10 +1132,10 @@ "Delete contact" = "删除联系人"; /* No comment provided by engineer. */ -"Delete Contact" = "删除联系人"; +"Delete database" = "删除数据库"; /* No comment provided by engineer. */ -"Delete database" = "删除数据库"; +"Delete database from this device" = "从这部设备上删除数据库"; /* server test step */ "Delete file" = "删除文件"; @@ -1146,9 +1185,6 @@ /* No comment provided by engineer. */ "Delete old database?" = "删除旧数据库吗?"; -/* No comment provided by engineer. */ -"Delete pending connection" = "删除挂起连接"; - /* No comment provided by engineer. */ "Delete pending connection?" = "删除待定连接?"; @@ -1287,9 +1323,21 @@ /* No comment provided by engineer. */ "Downgrade and open chat" = "降级并打开聊天"; +/* chat item action */ +"Download" = "下载"; + +/* No comment provided by engineer. */ +"Download failed" = "下载失败了"; + /* server test step */ "Download file" = "下载文件"; +/* No comment provided by engineer. */ +"Downloading archive" = "正在下载存档"; + +/* No comment provided by engineer. */ +"Downloading link details" = "正在下载链接详情"; + /* No comment provided by engineer. */ "Duplicate display name!" = "重复的显示名!"; @@ -1323,6 +1371,9 @@ /* No comment provided by engineer. */ "Enable for all" = "全部启用"; +/* No comment provided by engineer. */ +"Enable in direct chats (BETA)!" = "在私聊中开启(公测)!"; + /* No comment provided by engineer. */ "Enable instant notifications?" = "启用即时通知?"; @@ -1350,6 +1401,9 @@ /* enabled status */ "enabled" = "已启用"; +/* No comment provided by engineer. */ +"Enabled for" = "启用对象"; + /* enabled status */ "enabled for contact" = "已为联系人启用"; @@ -1431,6 +1485,9 @@ /* No comment provided by engineer. */ "Enter Passcode" = "输入密码"; +/* No comment provided by engineer. */ +"Enter passphrase" = "输入密码短语"; + /* No comment provided by engineer. */ "Enter passphrase…" = "输入密码……"; @@ -1506,9 +1563,6 @@ /* No comment provided by engineer. */ "Error deleting connection" = "删除连接错误"; -/* No comment provided by engineer. */ -"Error deleting contact" = "删除联系人错误"; - /* No comment provided by engineer. */ "Error deleting database" = "删除数据库错误"; @@ -1521,6 +1575,9 @@ /* No comment provided by engineer. */ "Error deleting user profile" = "删除用户资料错误"; +/* No comment provided by engineer. */ +"Error downloading the archive" = "下载存档出错"; + /* No comment provided by engineer. */ "Error enabling delivery receipts!" = "启用送达回执出错!"; @@ -1563,6 +1620,9 @@ /* No comment provided by engineer. */ "Error saving passphrase to keychain" = "保存密码到钥匙串错误"; +/* when migrating */ +"Error saving settings" = "保存设置出错"; + /* No comment provided by engineer. */ "Error saving user password" = "保存用户密码时出错"; @@ -1603,9 +1663,16 @@ "Error updating user privacy" = "更新用户隐私时出错"; /* No comment provided by engineer. */ -"Error: " = "错误: "; +"Error uploading the archive" = "上传存档出错"; /* No comment provided by engineer. */ +"Error verifying passphrase:" = "验证密码短语出错:"; + +/* No comment provided by engineer. */ +"Error: " = "错误: "; + +/* file error text + snd error text */ "Error: %@" = "错误: %@"; /* No comment provided by engineer. */ @@ -1635,6 +1702,9 @@ /* No comment provided by engineer. */ "Exported database archive." = "导出数据库归档。"; +/* No comment provided by engineer. */ +"Exported file doesn't exist" = "导出的文件不存在"; + /* No comment provided by engineer. */ "Exporting database archive…" = "导出数据库档案中…"; @@ -1647,7 +1717,7 @@ /* No comment provided by engineer. */ "Faster joining and more reliable messages." = "加入速度更快、信息更可靠。"; -/* No comment provided by engineer. */ +/* swipe action */ "Favorite" = "最喜欢"; /* No comment provided by engineer. */ @@ -1671,12 +1741,21 @@ /* No comment provided by engineer. */ "Files and media are prohibited in this group." = "此群组中禁止文件和媒体。"; +/* No comment provided by engineer. */ +"Files and media not allowed" = "不允许文件和媒体"; + /* No comment provided by engineer. */ "Files and media prohibited!" = "禁止文件和媒体!"; /* No comment provided by engineer. */ "Filter unread and favorite chats." = "过滤未读和收藏的聊天记录。"; +/* No comment provided by engineer. */ +"Finalize migration" = "完成迁移"; + +/* No comment provided by engineer. */ +"Finalize migration on another device." = "在另一部设备上完成迁移"; + /* No comment provided by engineer. */ "Finally, we have them! 🚀" = "终于我们有它们了! 🚀"; @@ -1704,6 +1783,21 @@ /* No comment provided by engineer. */ "For console" = "用于控制台"; +/* chat item action */ +"Forward" = "转发"; + +/* No comment provided by engineer. */ +"Forward and save messages" = "转发并保存消息"; + +/* No comment provided by engineer. */ +"forwarded" = "已转发"; + +/* No comment provided by engineer. */ +"Forwarded" = "已转发"; + +/* No comment provided by engineer. */ +"Forwarded from" = "转发自"; + /* No comment provided by engineer. */ "Found desktop" = "找到了桌面"; @@ -1779,6 +1873,9 @@ /* No comment provided by engineer. */ "Group members can send files and media." = "群组成员可以发送文件和媒体。"; +/* No comment provided by engineer. */ +"Group members can send SimpleX links." = "群成员可发送 SimpleX 链接。"; + /* No comment provided by engineer. */ "Group members can send voice messages." = "群组成员可以发送语音消息。"; @@ -1896,6 +1993,12 @@ /* No comment provided by engineer. */ "Import database" = "导入数据库"; +/* No comment provided by engineer. */ +"Import failed" = "导入失败了"; + +/* No comment provided by engineer. */ +"Importing archive" = "正在导入存档"; + /* No comment provided by engineer. */ "Improved message delivery" = "改进了消息传递"; @@ -1905,9 +2008,15 @@ /* No comment provided by engineer. */ "Improved server configuration" = "改进的服务器配置"; +/* No comment provided by engineer. */ +"In order to continue, chat should be stopped." = "必须停止聊天才能继续。"; + /* No comment provided by engineer. */ "In reply to" = "答复"; +/* No comment provided by engineer. */ +"In-call sounds" = "通话声音"; + /* No comment provided by engineer. */ "Incognito" = "隐身聊天"; @@ -1986,6 +2095,12 @@ /* No comment provided by engineer. */ "Invalid display name!" = "无效的显示名!"; +/* No comment provided by engineer. */ +"Invalid link" = "无效链接"; + +/* No comment provided by engineer. */ +"Invalid migration confirmation" = "迁移确认无效"; + /* No comment provided by engineer. */ "Invalid name!" = "无效名称!"; @@ -2061,7 +2176,7 @@ /* No comment provided by engineer. */ "Japanese interface" = "日语界面"; -/* No comment provided by engineer. */ +/* swipe action */ "Join" = "加入"; /* No comment provided by engineer. */ @@ -2103,7 +2218,7 @@ /* No comment provided by engineer. */ "Learn more" = "了解更多"; -/* No comment provided by engineer. */ +/* swipe action */ "Leave" = "离开"; /* No comment provided by engineer. */ @@ -2232,18 +2347,45 @@ /* notification */ "message received" = "消息已收到"; +/* No comment provided by engineer. */ +"Message source remains private." = "消息来源保持私密。"; + /* No comment provided by engineer. */ "Message text" = "消息正文"; +/* No comment provided by engineer. */ +"Message too large" = "消息太大了"; + /* No comment provided by engineer. */ "Messages" = "消息"; /* No comment provided by engineer. */ "Messages & files" = "消息"; +/* No comment provided by engineer. */ +"Migrate device" = "迁移设备"; + +/* No comment provided by engineer. */ +"Migrate from another device" = "从另一台设备迁移"; + +/* No comment provided by engineer. */ +"Migrate here" = "迁移到此处"; + +/* No comment provided by engineer. */ +"Migrate to another device" = "迁移到另一部设备"; + +/* No comment provided by engineer. */ +"Migrate to another device via QR code." = "通过二维码迁移到另一部设备。"; + +/* No comment provided by engineer. */ +"Migrating" = "迁移中"; + /* No comment provided by engineer. */ "Migrating database archive…" = "迁移数据库档案中…"; +/* No comment provided by engineer. */ +"Migration complete" = "迁移完毕"; + /* No comment provided by engineer. */ "Migration error:" = "迁移错误:"; @@ -2283,27 +2425,33 @@ /* No comment provided by engineer. */ "More improvements are coming soon!" = "更多改进即将推出!"; +/* No comment provided by engineer. */ +"More reliable network connection." = "更可靠的网络连接。"; + /* item status description */ "Most likely this connection is deleted." = "此连接很可能已被删除。"; -/* No comment provided by engineer. */ -"Most likely this contact has deleted the connection with you." = "很可能此联系人已经删除了与您的联系。"; - /* No comment provided by engineer. */ "Multiple chat profiles" = "多个聊天资料"; -/* No comment provided by engineer. */ +/* swipe action */ "Mute" = "静音"; /* No comment provided by engineer. */ "Muted when inactive!" = "不活动时静音!"; -/* No comment provided by engineer. */ +/* swipe action */ "Name" = "名称"; /* No comment provided by engineer. */ "Network & servers" = "网络和服务器"; +/* No comment provided by engineer. */ +"Network connection" = "网络连接"; + +/* No comment provided by engineer. */ +"Network management" = "网络管理"; + /* No comment provided by engineer. */ "Network settings" = "网络设置"; @@ -2382,6 +2530,9 @@ /* No comment provided by engineer. */ "No history" = "无历史记录"; +/* No comment provided by engineer. */ +"No network connection" = "无网络连接"; + /* No comment provided by engineer. */ "No permission to record voice message" = "没有录制语音消息的权限"; @@ -2411,7 +2562,7 @@ time to disappear */ "off" = "关闭"; -/* No comment provided by engineer. */ +/* blur media */ "Off" = "关闭"; /* feature offered item */ @@ -2439,10 +2590,10 @@ "One-time invitation link" = "一次性邀请链接"; /* No comment provided by engineer. */ -"Onion hosts will be required for connection. Requires enabling VPN." = "Onion 主机将用于连接。需要启用 VPN。"; +"Onion hosts will be **required** for connection.\nRequires compatible VPN." = "Onion 主机将用于连接。需要启用 VPN。"; /* No comment provided by engineer. */ -"Onion hosts will be used when available. Requires enabling VPN." = "当可用时,将使用 Onion 主机。需要启用 VPN。"; +"Onion hosts will be used when available.\nRequires compatible VPN." = "当可用时,将使用 Onion 主机。需要启用 VPN。"; /* No comment provided by engineer. */ "Onion hosts will not be used." = "将不会使用 Onion 主机。"; @@ -2510,15 +2661,27 @@ /* No comment provided by engineer. */ "Open-source protocol and code – anybody can run the servers." = "开源协议和代码——任何人都可以运行服务器。"; +/* No comment provided by engineer. */ +"Or paste archive link" = "或粘贴存档链接"; + /* No comment provided by engineer. */ "Or scan QR code" = "或者扫描二维码"; +/* No comment provided by engineer. */ +"Or securely share this file link" = "或安全地分享此文件链接"; + /* No comment provided by engineer. */ "Or show this code" = "或者显示此码"; +/* No comment provided by engineer. */ +"Other" = "其他"; + /* member role */ "owner" = "群主"; +/* feature role */ +"owners" = "所有者"; + /* No comment provided by engineer. */ "Passcode" = "密码"; @@ -2561,6 +2724,9 @@ /* message decrypt error item */ "Permanent decryption error" = "解密错误"; +/* No comment provided by engineer. */ +"Picture-in-picture calls" = "画中画通话"; + /* No comment provided by engineer. */ "PING count" = "PING 次数"; @@ -2579,6 +2745,9 @@ /* No comment provided by engineer. */ "Please check yours and your contact preferences." = "请检查您和您的联系人偏好设置。"; +/* No comment provided by engineer. */ +"Please confirm that network settings are correct for this device." = "请确认网络设置对此这台设备正确无误。"; + /* No comment provided by engineer. */ "Please contact group admin." = "请联系群组管理员。"; @@ -2639,6 +2808,9 @@ /* No comment provided by engineer. */ "Profile image" = "资料图片"; +/* No comment provided by engineer. */ +"Profile images" = "个人资料图"; + /* No comment provided by engineer. */ "Profile name:" = "显示名:"; @@ -2687,13 +2859,19 @@ /* No comment provided by engineer. */ "Push notifications" = "推送通知"; +/* chat item text */ +"quantum resistant e2e encryption" = "抗量子端到端加密"; + +/* No comment provided by engineer. */ +"Quantum resistant encryption" = "抗量子加密"; + /* No comment provided by engineer. */ "Rate the app" = "评价此应用程序"; /* chat item menu */ "React…" = "回应…"; -/* No comment provided by engineer. */ +/* swipe action */ "Read" = "已读"; /* No comment provided by engineer. */ @@ -2744,6 +2922,9 @@ /* No comment provided by engineer. */ "Receiving via" = "接收通过"; +/* No comment provided by engineer. */ +"Recipient(s) can't see who this message is from." = "收件人看不到这条消息来自何人。"; + /* No comment provided by engineer. */ "Recipients see updates as you type them." = "对方会在您键入时看到更新。"; @@ -2762,7 +2943,8 @@ /* No comment provided by engineer. */ "Reduced battery usage" = "减少电池使用量"; -/* reject incoming call via notification */ +/* reject incoming call via notification + swipe action */ "Reject" = "拒绝"; /* No comment provided by engineer. */ @@ -2819,9 +3001,18 @@ /* No comment provided by engineer. */ "Repeat connection request?" = "重复连接请求吗?"; +/* No comment provided by engineer. */ +"Repeat download" = "重复下载"; + +/* No comment provided by engineer. */ +"Repeat import" = "重复导入"; + /* No comment provided by engineer. */ "Repeat join request?" = "重复加入请求吗?"; +/* No comment provided by engineer. */ +"Repeat upload" = "重复上传"; + /* chat item action */ "Reply" = "回复"; @@ -2861,9 +3052,6 @@ /* chat item action */ "Reveal" = "揭示"; -/* No comment provided by engineer. */ -"Revert" = "恢复"; - /* No comment provided by engineer. */ "Revoke" = "撤销"; @@ -2879,6 +3067,9 @@ /* No comment provided by engineer. */ "Run chat" = "运行聊天程序"; +/* No comment provided by engineer. */ +"Safer groups" = "更安全的群组"; + /* chat item action */ "Save" = "保存"; @@ -2927,6 +3118,15 @@ /* No comment provided by engineer. */ "Save welcome message?" = "保存欢迎信息?"; +/* No comment provided by engineer. */ +"saved" = "已保存"; + +/* No comment provided by engineer. */ +"Saved" = "已保存"; + +/* No comment provided by engineer. */ +"Saved from" = "保存自"; + /* message info title */ "Saved message" = "已保存的消息"; @@ -2978,7 +3178,7 @@ /* chat item text */ "security code changed" = "安全密码已更改"; -/* No comment provided by engineer. */ +/* chat item action */ "Select" = "选择"; /* No comment provided by engineer. */ @@ -3005,9 +3205,6 @@ /* No comment provided by engineer. */ "send direct message" = "发送私信"; -/* No comment provided by engineer. */ -"Send direct message" = "发送私信"; - /* No comment provided by engineer. */ "Send direct message to connect" = "发送私信来连接"; @@ -3119,6 +3316,9 @@ /* No comment provided by engineer. */ "Set passcode" = "设置密码"; +/* No comment provided by engineer. */ +"Set passphrase" = "设置密码短语"; + /* No comment provided by engineer. */ "Set passphrase to export" = "设置密码来导出"; @@ -3131,6 +3331,9 @@ /* No comment provided by engineer. */ "Settings" = "设置"; +/* No comment provided by engineer. */ +"Shape profile images" = "改变个人资料图形状"; + /* chat item action */ "Share" = "分享"; @@ -3164,6 +3367,9 @@ /* No comment provided by engineer. */ "Show preview" = "显示预览"; +/* No comment provided by engineer. */ +"Show QR code" = "显示二维码"; + /* No comment provided by engineer. */ "Show:" = "显示:"; @@ -3188,6 +3394,12 @@ /* chat feature */ "SimpleX links" = "SimpleX 链接"; +/* No comment provided by engineer. */ +"SimpleX links are prohibited in this group." = "此群禁止 SimpleX 链接。"; + +/* No comment provided by engineer. */ +"SimpleX links not allowed" = "不允许SimpleX 链接"; + /* No comment provided by engineer. */ "SimpleX Lock" = "SimpleX 锁定"; @@ -3215,15 +3427,18 @@ /* No comment provided by engineer. */ "Small groups (max 20)" = "小群组(最多 20 人)"; -/* No comment provided by engineer. */ -"SMP servers" = "SMP 服务器"; - /* No comment provided by engineer. */ "Some non-fatal errors occurred during import - you may see Chat console for more details." = "导入过程中发生了一些非致命错误——您可以查看聊天控制台了解更多详细信息。"; /* notification title */ "Somebody" = "某人"; +/* No comment provided by engineer. */ +"Square, circle, or anything in between." = "方形、圆形、或两者之间的任意形状"; + +/* chat item text */ +"standard end-to-end encryption" = "标准端到端加密"; + /* No comment provided by engineer. */ "Start chat" = "开始聊天"; @@ -3239,6 +3454,9 @@ /* No comment provided by engineer. */ "Stop" = "停止"; +/* No comment provided by engineer. */ +"Stop chat" = "停止聊天程序"; + /* No comment provided by engineer. */ "Stop chat to enable database actions" = "停止聊天以启用数据库操作"; @@ -3266,6 +3484,9 @@ /* authentication reason */ "Stop SimpleX" = "停止 SimpleX"; +/* No comment provided by engineer. */ +"Stopping chat" = "正在停止聊天"; + /* No comment provided by engineer. */ "strike" = "删去"; @@ -3305,9 +3526,6 @@ /* No comment provided by engineer. */ "Tap to scan" = "轻按扫描"; -/* No comment provided by engineer. */ -"Tap to start a new chat" = "点击开始一个新聊天"; - /* No comment provided by engineer. */ "TCP connection timeout" = "TCP 连接超时"; @@ -3398,9 +3616,6 @@ /* No comment provided by engineer. */ "The text you pasted is not a SimpleX link." = "您粘贴的文本不是 SimpleX 链接。"; -/* No comment provided by engineer. */ -"Theme" = "主题"; - /* No comment provided by engineer. */ "These settings are for your current profile **%@**." = "这些设置适用于您当前的配置文件 **%@**。"; @@ -3416,6 +3631,12 @@ /* No comment provided by engineer. */ "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost." = "此操作无法撤消——您的个人资料、联系人、消息和文件将不可撤回地丢失。"; +/* E2EE info chat item */ +"This chat is protected by end-to-end encryption." = "此聊天受端到端加密保护。"; + +/* E2EE info chat item */ +"This chat is protected by quantum resistant end-to-end encryption." = "此聊天受抗量子的端到端加密保护。"; + /* notification title */ "this contact" = "这个联系人"; @@ -3509,13 +3730,10 @@ /* No comment provided by engineer. */ "Unblock member?" = "解封成员吗?"; -/* item status description */ -"Unexpected error: %@" = "意外错误: %@"; - /* No comment provided by engineer. */ "Unexpected migration state" = "未预料的迁移状态"; -/* No comment provided by engineer. */ +/* swipe action */ "Unfav." = "取消最喜欢"; /* No comment provided by engineer. */ @@ -3563,10 +3781,10 @@ /* authentication reason */ "Unlock app" = "解锁应用程序"; -/* No comment provided by engineer. */ +/* swipe action */ "Unmute" = "取消静音"; -/* No comment provided by engineer. */ +/* swipe action */ "Unread" = "未读"; /* No comment provided by engineer. */ @@ -3575,18 +3793,12 @@ /* No comment provided by engineer. */ "Update" = "更新"; -/* No comment provided by engineer. */ -"Update .onion hosts setting?" = "更新 .onion 主机设置?"; - /* No comment provided by engineer. */ "Update database passphrase" = "更新数据库密码"; /* No comment provided by engineer. */ "Update network settings?" = "更新网络设置?"; -/* No comment provided by engineer. */ -"Update transport isolation mode?" = "更新传输隔离模式?"; - /* rcv group event chat item */ "updated group profile" = "已更新的群组资料"; @@ -3597,14 +3809,17 @@ "Updating settings will re-connect the client to all servers." = "更新设置会将客户端重新连接到所有服务器。"; /* No comment provided by engineer. */ -"Updating this setting will re-connect the client to all servers." = "更新此设置将重新连接客户端到所有服务器。"; +"Upgrade and open chat" = "升级并打开聊天"; /* No comment provided by engineer. */ -"Upgrade and open chat" = "升级并打开聊天"; +"Upload failed" = "上传失败了"; /* server test step */ "Upload file" = "上传文件"; +/* No comment provided by engineer. */ +"Uploading archive" = "正在上传存档"; + /* No comment provided by engineer. */ "Use .onion hosts" = "使用 .onion 主机"; @@ -3633,10 +3848,10 @@ "Use SimpleX Chat servers?" = "使用 SimpleX Chat 服务器?"; /* No comment provided by engineer. */ -"User profile" = "用户资料"; +"Use the app while in the call." = "通话时使用本应用"; /* No comment provided by engineer. */ -"Using .onion hosts requires compatible VPN provider." = "使用 .onion 主机需要兼容的 VPN 提供商。"; +"User profile" = "用户资料"; /* No comment provided by engineer. */ "Using SimpleX Chat servers." = "使用 SimpleX Chat 服务器。"; @@ -3656,6 +3871,12 @@ /* No comment provided by engineer. */ "Verify connections" = "验证连接"; +/* No comment provided by engineer. */ +"Verify database passphrase" = "验证数据库密码短语"; + +/* No comment provided by engineer. */ +"Verify passphrase" = "验证密码短语"; + /* No comment provided by engineer. */ "Verify security code" = "验证安全码"; @@ -3710,6 +3931,9 @@ /* No comment provided by engineer. */ "Voice messages are prohibited in this group." = "语音信息在该群组中被禁用。"; +/* No comment provided by engineer. */ +"Voice messages not allowed" = "不允许语音消息"; + /* No comment provided by engineer. */ "Voice messages prohibited!" = "语音消息禁止发送!"; @@ -3731,6 +3955,9 @@ /* No comment provided by engineer. */ "wants to connect to you!" = "想要与您连接!"; +/* No comment provided by engineer. */ +"Warning: starting chat on multiple devices is not supported and will cause message delivery failures" = "警告:不支持在多部设备上启动聊天,这么做会导致消息传送失败。"; + /* No comment provided by engineer. */ "Warning: you may lose some data!" = "警告:您可能会丢失部分数据!"; @@ -3746,18 +3973,33 @@ /* No comment provided by engineer. */ "Welcome message" = "欢迎消息"; +/* No comment provided by engineer. */ +"Welcome message is too long" = "欢迎消息太大了"; + /* No comment provided by engineer. */ "What's new" = "更新内容"; /* No comment provided by engineer. */ "When available" = "当可用时"; +/* No comment provided by engineer. */ +"When connecting audio and video calls." = "连接音频和视频通话时。"; + /* No comment provided by engineer. */ "When people request to connect, you can accept or reject it." = "当人们请求连接时,您可以接受或拒绝它。"; /* No comment provided by engineer. */ "When you share an incognito profile with somebody, this profile will be used for the groups they invite you to." = "当您与某人共享隐身聊天资料时,该资料将用于他们邀请您加入的群组。"; +/* No comment provided by engineer. */ +"WiFi" = "WiFi"; + +/* No comment provided by engineer. */ +"Will be enabled in direct chats!" = "将在私聊中启用!"; + +/* No comment provided by engineer. */ +"Wired ethernet" = "有线以太网"; + /* No comment provided by engineer. */ "With encrypted files and media." = "加密的文件和媒体。"; @@ -3773,12 +4015,12 @@ /* No comment provided by engineer. */ "Wrong passphrase!" = "密码错误!"; -/* No comment provided by engineer. */ -"XFTP servers" = "XFTP 服务器"; - /* pref value */ "yes" = "是"; +/* No comment provided by engineer. */ +"you" = "您"; + /* No comment provided by engineer. */ "You" = "您"; @@ -3824,6 +4066,9 @@ /* No comment provided by engineer. */ "You can enable them later via app Privacy & Security settings." = "您可以稍后通过应用程序的 \"隐私与安全 \"设置启用它们。"; +/* No comment provided by engineer. */ +"You can give another try." = "你可以再试一次。"; + /* No comment provided by engineer. */ "You can hide or mute a user profile - swipe it to the right." = "您可以隐藏或静音用户个人资料——只需向右滑动。"; @@ -3831,7 +4076,7 @@ "You can make it visible to your SimpleX contacts via Settings." = "你可以通过设置让它对你的 SimpleX 联系人可见。"; /* notification body */ -"You can now send messages to %@" = "您现在可以给 %@ 发送消息"; +"You can now chat with %@" = "您现在可以给 %@ 发送消息"; /* No comment provided by engineer. */ "You can set lock screen notification preview via settings." = "您可以通过设置来设置锁屏通知预览。"; @@ -3881,9 +4126,6 @@ /* No comment provided by engineer. */ "You have already requested connection via this address!" = "你已经请求通过此地址进行连接!"; -/* No comment provided by engineer. */ -"You have no chats" = "您没有聊天记录"; - /* No comment provided by engineer. */ "You have to enter passphrase every time the app starts - it is not stored on the device." = "您必须在每次应用程序启动时输入密码——它不存储在设备上。"; @@ -3968,9 +4210,6 @@ /* No comment provided by engineer. */ "Your chat profiles" = "您的聊天资料"; -/* No comment provided by engineer. */ -"Your contact needs to be online for the connection to complete.\nYou can cancel this connection and remove the contact (and try later with a new link)." = "您的联系人需要在线才能完成连接。\n您可以取消此连接并删除联系人(然后尝试使用新链接)。"; - /* No comment provided by engineer. */ "Your contact sent a file that is larger than currently supported maximum size (%@)." = "您的联系人发送的文件大于当前支持的最大大小 (%@)。"; diff --git a/apps/multiplatform/android/build.gradle.kts b/apps/multiplatform/android/build.gradle.kts index 4e279d3ccb..5c2c786a21 100644 --- a/apps/multiplatform/android/build.gradle.kts +++ b/apps/multiplatform/android/build.gradle.kts @@ -88,6 +88,7 @@ android { "cs", "de", "es", + "fa", "fi", "fr", "hu", diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt index 6ce582cad4..5a69d282b4 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/MainActivity.kt @@ -13,22 +13,26 @@ import chat.simplex.app.model.NtfManager.getUserIdFromIntent import chat.simplex.common.* import chat.simplex.common.helpers.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.onboarding.* import chat.simplex.common.platform.* +import chat.simplex.res.MR import kotlinx.coroutines.* import java.lang.ref.WeakReference class MainActivity: FragmentActivity() { override fun onCreate(savedInstanceState: Bundle?) { + mainActivity = WeakReference(this) platform.androidSetNightModeIfSupported() + val c = CurrentColors.value.colors + platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get()) applyAppLocale(ChatModel.controller.appPrefs.appLanguage) super.onCreate(savedInstanceState) // testJson() - mainActivity = WeakReference(this) // When call ended and orientation changes, it re-process old intent, it's unneeded. // Only needed to be processed on first creation of activity if (savedInstanceState == null) { @@ -146,7 +150,12 @@ fun processIntent(intent: Intent?) { "android.intent.action.VIEW" -> { val uri = intent.data if (uri != null) { - chatModel.appOpenUrl.value = null to uri.toURI() + val transformedUri = uri.toURIOrNull() + if (transformedUri != null) { + chatModel.appOpenUrl.value = null to transformedUri + } else { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_parsing_uri_title), generalGetString(MR.strings.error_parsing_uri_desc)) + } } } } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt index 0152f5e8c2..64b6639a58 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/MessagesFetcherWorker.kt @@ -7,7 +7,7 @@ import chat.simplex.app.SimplexService.Companion.showPassphraseNotification import chat.simplex.common.model.ChatController import chat.simplex.common.views.helpers.DBMigrationResult import chat.simplex.common.platform.chatModel -import chat.simplex.common.platform.initChatControllerAndRunMigrations +import chat.simplex.common.platform.initChatControllerOnStart import chat.simplex.common.views.helpers.DatabaseUtils import kotlinx.coroutines.* import java.util.Date @@ -60,7 +60,7 @@ class MessagesFetcherWork( try { // In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) { - initChatControllerAndRunMigrations() + initChatControllerOnStart() } withTimeout(durationSeconds * 1000L) { val chatController = ChatController diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt index 83105c678a..9206b5be89 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt @@ -6,11 +6,14 @@ import android.content.Context import chat.simplex.common.platform.Log import android.content.Intent import android.content.pm.ActivityInfo -import android.media.AudioManager import android.os.* +import android.view.View import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext +import androidx.core.view.ViewCompat import androidx.lifecycle.* import androidx.work.* import chat.simplex.app.model.NtfManager @@ -19,16 +22,15 @@ import chat.simplex.app.views.call.CallActivity import chat.simplex.common.helpers.* import chat.simplex.common.model.* import chat.simplex.common.model.ChatController.appPrefs -import chat.simplex.common.model.ChatModel.updatingChatsMutex +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* -import chat.simplex.common.ui.theme.CurrentColors -import chat.simplex.common.ui.theme.DefaultTheme +import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.* +import chat.simplex.common.views.chatlist.statusBarColorAfterCall import chat.simplex.common.views.helpers.* import chat.simplex.common.views.onboarding.OnboardingStage import com.jakewharton.processphoenix.ProcessPhoenix import kotlinx.coroutines.* -import kotlinx.coroutines.sync.withLock import java.io.* import java.util.* import java.util.concurrent.TimeUnit @@ -66,6 +68,7 @@ class SimplexApp: Application(), LifecycleEventObserver { context = this initHaskell() initMultiplatform() + runMigrations() tmpDir.deleteRecursively() tmpDir.mkdir() @@ -74,7 +77,7 @@ class SimplexApp: Application(), LifecycleEventObserver { // It's important, otherwise, user may be locked in undefined state appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) } else if (DatabaseUtils.ksAppPassword.get() == null || DatabaseUtils.ksSelfDestructPassword.get() == null) { - initChatControllerAndRunMigrations() + initChatControllerOnStart() } ProcessLifecycleOwner.get().lifecycle.addObserver(this@SimplexApp) } @@ -86,7 +89,7 @@ class SimplexApp: Application(), LifecycleEventObserver { Lifecycle.Event.ON_START -> { isAppOnForeground = true if (chatModel.chatRunning.value == true) { - updatingChatsMutex.withLock { + withChats { kotlin.runCatching { val currentUserId = chatModel.currentUser.value?.userId val chats = ArrayList(chatController.apiGetChats(chatModel.remoteHostId())) @@ -99,7 +102,7 @@ class SimplexApp: Application(), LifecycleEventObserver { /** Pass old chatStats because unreadCounter can be changed already while [ChatController.apiGetChats] is executing */ if (indexOfCurrentChat >= 0) chats[indexOfCurrentChat] = chats[indexOfCurrentChat].copy(chatStats = oldStats) } - chatModel.updateChats(chats) + updateChats(chats) } }.onFailure { Log.e(TAG, it.stackTraceToString()) } } @@ -179,6 +182,7 @@ class SimplexApp: Application(), LifecycleEventObserver { override fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean = NtfManager.notifyCallInvitation(invitation) override fun hasNotificationsForChat(chatId: String): Boolean = NtfManager.hasNotificationsForChat(chatId) override fun cancelNotificationsForChat(chatId: String) = NtfManager.cancelNotificationsForChat(chatId) + override fun cancelNotificationsForUser(userId: Long) = NtfManager.cancelNotificationsForUser(userId) override fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String?, actions: List Unit>>) = NtfManager.displayNotification(user, chatId, displayName, msgText, image, actions.map { it.first }) override fun androidCreateNtfChannelsMaybeShowAlert() = NtfManager.createNtfChannelsMaybeShowAlert() override fun cancelCallNotification() = NtfManager.cancelCallNotification() @@ -254,7 +258,7 @@ class SimplexApp: Application(), LifecycleEventObserver { override fun androidSetNightModeIfSupported() { if (Build.VERSION.SDK_INT < 31) return - val light = if (CurrentColors.value.name == DefaultTheme.SYSTEM.name) { + val light = if (CurrentColors.value.name == DefaultTheme.SYSTEM_THEME_NAME) { null } else { CurrentColors.value.colors.isLight @@ -268,6 +272,45 @@ class SimplexApp: Application(), LifecycleEventObserver { uiModeManager.setApplicationNightMode(mode) } + override fun androidSetStatusAndNavBarColors(isLight: Boolean, backgroundColor: Color, hasTop: Boolean, hasBottom: Boolean) { + val window = mainActivity.get()?.window ?: return + @Suppress("DEPRECATION") + val windowInsetController = ViewCompat.getWindowInsetsController(window.decorView) + + var statusBar = (if (hasTop && appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) { + backgroundColor.mixWith(CurrentColors.value.colors.onBackground, 0.97f) + } else { + if (CurrentColors.value.base == DefaultTheme.SIMPLEX) { + backgroundColor.lighter(0.4f) + } else { + backgroundColor + } + }).toArgb() + + // SimplexGreen while in call + if (window.statusBarColor == SimplexGreen.toArgb()) { + statusBarColorAfterCall.intValue = statusBar + statusBar = SimplexGreen.toArgb() + } + val navBar = (if (hasBottom && appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete) { + backgroundColor.mixWith(CurrentColors.value.colors.onBackground, 0.97f) + } else { + backgroundColor + }).toArgb() + if (window.statusBarColor != statusBar) { + window.statusBarColor = statusBar + } + if (windowInsetController?.isAppearanceLightStatusBars != isLight) { + windowInsetController?.isAppearanceLightStatusBars = isLight + } + if (window.navigationBarColor != navBar) { + window.navigationBarColor = navBar + } + if (windowInsetController?.isAppearanceLightNavigationBars != isLight) { + windowInsetController?.isAppearanceLightNavigationBars = isLight + } + } + override fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long?, chatId: ChatId?) { val context = mainActivity.get() ?: return val intent = Intent(context, CallActivity::class.java) diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt index f56bf4fe00..a5f5d84ec2 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexService.kt @@ -77,7 +77,7 @@ class SimplexService: Service() { isServiceStarted = true // In case of self-destruct is enabled the initialization process will not start in SimplexApp, Let's start it here if (DatabaseUtils.ksSelfDestructPassword.get() != null && chatModel.chatDbStatus.value == null) { - initChatControllerAndRunMigrations() + initChatControllerOnStart() } } } diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt index f83933b2e0..417a81a953 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt @@ -48,7 +48,8 @@ object NtfManager { } private val manager: NotificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - private var prevNtfTime = mutableMapOf() + // (UserId, ChatId) -> Time + private var prevNtfTime = mutableMapOf, Long>() private val msgNtfTimeoutMs = 30000L init { @@ -72,12 +73,27 @@ object NtfManager { } fun cancelNotificationsForChat(chatId: String) { - prevNtfTime.remove(chatId) + val key = prevNtfTime.keys.firstOrNull { it.second == chatId } + prevNtfTime.remove(key) manager.cancel(chatId.hashCode()) val msgNtfs = manager.activeNotifications.filter { ntf -> ntf.notification.channelId == MessageChannel } - if (msgNtfs.count() == 1) { + if (msgNtfs.size <= 1) { + // Have a group notification with no children so cancel it + manager.cancel(0) + } + } + + fun cancelNotificationsForUser(userId: Long) { + prevNtfTime.keys.filter { it.first == userId }.forEach { + prevNtfTime.remove(it) + manager.cancel(it.second.hashCode()) + } + val msgNtfs = manager.activeNotifications.filter { ntf -> + ntf.notification.channelId == MessageChannel + } + if (msgNtfs.size <= 1) { // Have a group notification with no children so cancel it manager.cancel(0) } @@ -87,8 +103,8 @@ object NtfManager { if (!user.showNotifications) return Log.d(TAG, "notifyMessageReceived $chatId") val now = Clock.System.now().toEpochMilliseconds() - val recentNotification = (now - prevNtfTime.getOrDefault(chatId, 0) < msgNtfTimeoutMs) - prevNtfTime[chatId] = now + val recentNotification = (now - prevNtfTime.getOrDefault(user.userId to chatId, 0) < msgNtfTimeoutMs) + prevNtfTime[user.userId to chatId] = now val previewMode = appPreferences.notificationPreviewMode.get() val title = if (previewMode == NotificationPreviewMode.HIDDEN.name) generalGetString(MR.strings.notification_preview_somebody) else displayName val content = if (previewMode != NotificationPreviewMode.MESSAGE.name) generalGetString(MR.strings.notification_preview_new_message) else msgText diff --git a/apps/multiplatform/common/build.gradle.kts b/apps/multiplatform/common/build.gradle.kts index 1f55a7195c..7e97ea3414 100644 --- a/apps/multiplatform/common/build.gradle.kts +++ b/apps/multiplatform/common/build.gradle.kts @@ -37,7 +37,7 @@ kotlin { api("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3") api("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0") api("com.russhwolf:multiplatform-settings:1.1.1") - api("com.charleskorn.kaml:kaml:0.58.0") + api("com.charleskorn.kaml:kaml:0.59.0") api("org.jetbrains.compose.ui:ui-text:${rootProject.extra["compose.version"] as String}") implementation("org.jetbrains.compose.components:components-animatedimage:${rootProject.extra["compose.version"] as String}") //Barcode @@ -53,6 +53,9 @@ kotlin { val commonTest by getting { dependencies { implementation(kotlin("test")) + implementation(kotlin("test-junit")) + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) } } val androidMain by getting { @@ -102,6 +105,7 @@ kotlin { implementation("uk.co.caprica:vlcj:4.8.2") implementation("com.github.NanoHttpd.nanohttpd:nanohttpd:efb2ebf85a") implementation("com.github.NanoHttpd.nanohttpd:nanohttpd-websocket:efb2ebf85a") + implementation("com.squareup.okhttp3:okhttp:4.12.0") } } val desktopTest by getting diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Extensions.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Extensions.kt index e237272eb0..540533e5ad 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Extensions.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/helpers/Extensions.kt @@ -17,6 +17,7 @@ val NotificationsMode.requiresIgnoringBattery lateinit var APPLICATION_ID: String -fun Uri.toURI(): URI = URI(toString()) +fun Uri.toURI(): URI = URI(toString().replace("\n", "")) +fun Uri.toURIOrNull(): URI? = try { toURI() } catch (e: Exception) { null } fun URI.toUri(): Uri = Uri.parse(toString()) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/AppCommon.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/AppCommon.android.kt index 547db51bad..d739a033f9 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/AppCommon.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/AppCommon.android.kt @@ -6,8 +6,6 @@ import android.net.LocalServerSocket import android.util.Log import androidx.activity.ComponentActivity import androidx.fragment.app.FragmentActivity -import chat.simplex.common.* -import chat.simplex.common.platform.* import java.io.* import java.lang.ref.WeakReference import java.util.* @@ -24,6 +22,8 @@ var isAppOnForeground: Boolean = false @Suppress("ConstantLocale") val defaultLocale: Locale = Locale.getDefault() +actual fun isAppVisibleAndFocused(): Boolean = isAppOnForeground + @SuppressLint("StaticFieldLeak") lateinit var androidAppContext: Context var mainActivity: WeakReference = WeakReference(null) diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt index dfc8c1d4e7..bfe961a512 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Files.android.kt @@ -15,8 +15,10 @@ actual val dataDir: File = androidAppContext.dataDir actual val tmpDir: File = androidAppContext.getDir("temp", Application.MODE_PRIVATE) actual val filesDir: File = File(dataDir.absolutePath + File.separator + "files") actual val appFilesDir: File = File(filesDir.absolutePath + File.separator + "app_files") +actual val wallpapersDir: File = File(filesDir.absolutePath + File.separator + "assets" + File.separator + "wallpapers").also { it.mkdirs() } actual val coreTmpDir: File = File(filesDir.absolutePath + File.separator + "temp_files") actual val dbAbsolutePrefixPath: String = dataDir.absolutePath + File.separator + "files" +actual val preferencesDir = File(dataDir.absolutePath + File.separator + "shared_prefs") actual val chatDatabaseFileName: String = "files_chat.db" actual val agentDatabaseFileName: String = "files_agent.db" @@ -27,6 +29,8 @@ actual val remoteHostsDir: File = File(tmpDir.absolutePath + File.separator + "r actual fun desktopOpenDatabaseDir() {} +actual fun desktopOpenDir(dir: File) {} + @Composable actual fun rememberFileChooserLauncher(getContent: Boolean, rememberedValue: Any?, onResult: (URI?) -> Unit): FileChooserLauncher { val launcher = rememberLauncherForActivityResult( diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt index b103367fe8..2ff2a3e021 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Modifier.android.kt @@ -28,3 +28,5 @@ actual fun Modifier.desktopOnExternalDrag( actual fun Modifier.onRightClick(action: () -> Unit): Modifier = this actual fun Modifier.desktopPointerHoverIconHand(): Modifier = this + +actual fun Modifier.desktopOnHovered(action: (Boolean) -> Unit): Modifier = Modifier diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt index 9e28c4f2bc..0b17a3aadf 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/PlatformTextField.android.kt @@ -2,6 +2,7 @@ package chat.simplex.common.platform import android.annotation.SuppressLint import android.content.Context +import android.graphics.drawable.ColorDrawable import android.os.Build import android.text.InputType import android.util.Log @@ -10,16 +11,18 @@ import android.view.ViewGroup import android.view.inputmethod.* import android.widget.EditText import android.widget.TextView -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.material.MaterialTheme import androidx.compose.material.Text import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.core.graphics.drawable.DrawableCompat @@ -29,14 +32,16 @@ import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doOnTextChanged import chat.simplex.common.R import chat.simplex.common.helpers.toURI +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel import chat.simplex.common.ui.theme.CurrentColors import chat.simplex.common.views.chat.* -import chat.simplex.common.views.helpers.SharedContent -import chat.simplex.common.views.helpers.generalGetString +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.filter import java.lang.reflect.Field import java.net.URI @@ -48,6 +53,8 @@ actual fun PlatformTextField( textStyle: MutableState, showDeleteTextButton: MutableState, userIsObserver: Boolean, + placeholder: String, + showVoiceButton: Boolean, onMessageChange: (String) -> Unit, onUpArrow: () -> Unit, onFilesPasted: (List) -> Unit, @@ -55,11 +62,11 @@ actual fun PlatformTextField( ) { val cs = composeState.value val textColor = MaterialTheme.colors.onBackground - val tintColor = MaterialTheme.colors.secondaryVariant - val padding = PaddingValues(12.dp, 7.dp, 45.dp, 0.dp) - val paddingStart = with(LocalDensity.current) { 12.dp.roundToPx() } + val hintColor = MaterialTheme.colors.secondary + val padding = PaddingValues(0.dp, 7.dp, 50.dp, 0.dp) + val paddingStart = 0 val paddingTop = with(LocalDensity.current) { 7.dp.roundToPx() } - val paddingEnd = with(LocalDensity.current) { 45.dp.roundToPx() } + val paddingEnd = with(LocalDensity.current) { 50.dp.roundToPx() } val paddingBottom = with(LocalDensity.current) { 7.dp.roundToPx() } var showKeyboard by remember { mutableStateOf(false) } var freeFocus by remember { mutableStateOf(false) } @@ -78,7 +85,15 @@ actual fun PlatformTextField( freeFocus = true } } + LaunchedEffect(Unit) { + snapshotFlow { ModalManager.start.modalCount.value } + .filter { it > 0 } + .collect { + freeFocus = true + } + } + val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl AndroidView(modifier = Modifier, factory = { val editText = @SuppressLint("AppCompatCustomView") object: EditText(it) { override fun setOnReceiveContentListener( @@ -107,12 +122,13 @@ actual fun PlatformTextField( editText.maxLines = 16 editText.inputType = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES or editText.inputType editText.setTextColor(textColor.toArgb()) - editText.textSize = textStyle.value.fontSize.value - val drawable = androidAppContext.getDrawable(R.drawable.send_msg_view_background)!! - DrawableCompat.setTint(drawable, tintColor.toArgb()) - editText.background = drawable - editText.setPadding(paddingStart, paddingTop, paddingEnd, paddingBottom) + editText.textSize = textStyle.value.fontSize.value * appPrefs.fontScale.get() + editText.background = ColorDrawable(Color.Transparent.toArgb()) + editText.textDirection = if (isRtl) EditText.TEXT_DIRECTION_LOCALE else EditText.TEXT_DIRECTION_ANY_RTL + editText.setPaddingRelative(paddingStart, paddingTop, paddingEnd, paddingBottom) editText.setText(cs.message) + editText.hint = placeholder + editText.setHintTextColor(hintColor.toArgb()) if (Build.VERSION.SDK_INT >= 29) { editText.textCursorDrawable?.let { DrawableCompat.setTint(it, CurrentColors.value.colors.secondary.toArgb()) } } else { @@ -135,8 +151,9 @@ actual fun PlatformTextField( editText }) { it.setTextColor(textColor.toArgb()) - it.textSize = textStyle.value.fontSize.value - DrawableCompat.setTint(it.background, tintColor.toArgb()) + it.setHintTextColor(hintColor.toArgb()) + it.hint = placeholder + it.textSize = textStyle.value.fontSize.value * appPrefs.fontScale.get() it.isFocusable = composeState.value.preview !is ComposePreview.VoicePreview it.isFocusableInTouchMode = it.isFocusable if (cs.message != it.text.toString()) { diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt index 4dbc9bd9a9..e5dda23f0f 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/RecAndPlay.android.kt @@ -126,16 +126,11 @@ actual object AudioPlayer: AudioPlayerInterface { .build() ) } - // Filepath: String, onProgressUpdate - private val currentlyPlaying: MutableState Unit>?> = mutableStateOf(null) + override val currentlyPlaying: MutableState = mutableStateOf(null) private var progressJob: Job? = null - enum class TrackState { - PLAYING, PAUSED, REPLACED - } - // Returns real duration of the track - private fun start(fileSource: CryptoFile, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? { + private fun start(fileSource: CryptoFile, smallView: Boolean, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? { val absoluteFilePath = if (fileSource.isAbsolutePath) fileSource.filePath else getAppFilePath(fileSource.filePath) if (!File(absoluteFilePath).exists()) { Log.e(TAG, "No such file: ${fileSource.filePath}") @@ -145,7 +140,7 @@ actual object AudioPlayer: AudioPlayerInterface { VideoPlayerHolder.stopAll() RecorderInterface.stopRecording?.invoke() val current = currentlyPlaying.value - if (current == null || current.first != fileSource.filePath) { + if (current == null || current.fileSource.filePath != fileSource.filePath || smallView != current.smallView) { stopListener() player.reset() runCatching { @@ -168,7 +163,7 @@ actual object AudioPlayer: AudioPlayerInterface { } if (seek != null) player.seekTo(seek) player.start() - currentlyPlaying.value = fileSource.filePath to onProgressUpdate + currentlyPlaying.value = CurrentlyPlayingState(fileSource, onProgressUpdate, smallView) progressJob = CoroutineScope(Dispatchers.Default).launch { onProgressUpdate(player.currentPosition, TrackState.PLAYING) while(isActive && player.isPlaying) { @@ -192,6 +187,10 @@ actual object AudioPlayer: AudioPlayerInterface { } keepScreenOn(false) onProgressUpdate(null, TrackState.PAUSED) + + if (smallView && isActive) { + stopListener() + } } return player.duration } @@ -215,7 +214,7 @@ actual object AudioPlayer: AudioPlayerInterface { // FileName or filePath are ok override fun stop(fileName: String?) { - if (fileName != null && currentlyPlaying.value?.first?.endsWith(fileName) == true) { + if (fileName != null && currentlyPlaying.value?.fileSource?.filePath?.endsWith(fileName) == true) { stop() } } @@ -223,7 +222,7 @@ actual object AudioPlayer: AudioPlayerInterface { private fun stopListener() { val afterCoroutineCancel: CompletionHandler = { // Notify prev audio listener about stop - currentlyPlaying.value?.second?.invoke(null, TrackState.REPLACED) + currentlyPlaying.value?.onProgressUpdate?.invoke(null, TrackState.REPLACED) currentlyPlaying.value = null } /** Preventing race by calling a code AFTER coroutine ends, so [TrackState] will be: @@ -244,11 +243,12 @@ actual object AudioPlayer: AudioPlayerInterface { progress: MutableState, duration: MutableState, resetOnEnd: Boolean, + smallView: Boolean, ) { if (progress.value == duration.value) { progress.value = 0 } - val realDuration = start(fileSource, progress.value) { pro, state -> + val realDuration = start(fileSource, smallView, progress.value) { pro, state -> if (pro != null) { progress.value = pro } @@ -274,7 +274,7 @@ actual object AudioPlayer: AudioPlayerInterface { override fun seekTo(ms: Int, pro: MutableState, filePath: String?) { pro.value = ms - if (currentlyPlaying.value?.first == filePath) { + if (currentlyPlaying.value?.fileSource?.filePath == filePath) { player.seekTo(ms) } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt index e15d1f9268..73c920b940 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/Resources.android.kt @@ -7,6 +7,8 @@ import android.content.SharedPreferences import android.content.res.Configuration import android.text.BidiFormatter import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.Font @@ -14,9 +16,11 @@ import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.core.graphics.drawable.toBitmap import chat.simplex.common.model.AppPreferences import com.russhwolf.settings.Settings import com.russhwolf.settings.SharedPreferencesSettings +import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.desc.desc @@ -48,6 +52,12 @@ actual fun windowOrientation(): WindowOrientation = when (mainActivity.get()?.re @Composable actual fun windowWidth(): Dp = LocalConfiguration.current.screenWidthDp.dp +@Composable +actual fun windowHeight(): Dp = LocalConfiguration.current.screenHeightDp.dp + actual fun desktopExpandWindowToWidth(width: Dp) {} actual fun isRtl(text: CharSequence): Boolean = BidiFormatter.getInstance().isRtl(text) + +actual fun ImageResource.toComposeImageBitmap(): ImageBitmap? = + getDrawable(androidAppContext)?.toBitmap()?.asImageBitmap() diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt index 199e719703..6851970b81 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/platform/ScrollableColumn.android.kt @@ -4,14 +4,21 @@ import androidx.compose.foundation.* import androidx.compose.foundation.gestures.FlingBehavior import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import chat.simplex.common.views.helpers.* +import kotlinx.coroutines.flow.filter +import kotlin.math.absoluteValue @Composable actual fun LazyColumnWithScrollBar( modifier: Modifier, - state: LazyListState, + state: LazyListState?, contentPadding: PaddingValues, reverseLayout: Boolean, verticalArrangement: Arrangement.Vertical, @@ -20,7 +27,24 @@ actual fun LazyColumnWithScrollBar( userScrollEnabled: Boolean, content: LazyListScope.() -> Unit ) { - LazyColumn(modifier, state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) + val state = state ?: LocalAppBarHandler.current?.listState ?: rememberLazyListState() + val connection = LocalAppBarHandler.current?.connection + LaunchedEffect(Unit) { + snapshotFlow { state.firstVisibleItemScrollOffset } + .filter { state.firstVisibleItemIndex == 0 } + .collect { scrollPosition -> + val offset = connection?.appBarOffset + if (offset != null && (offset + scrollPosition).absoluteValue > 1) { + connection.appBarOffset = -scrollPosition.toFloat() +// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") + } + } + } + if (connection != null) { + LazyColumn(modifier.nestedScroll(connection), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) + } else { + LazyColumn(modifier, state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) + } } @Composable @@ -28,8 +52,34 @@ actual fun ColumnWithScrollBar( modifier: Modifier, verticalArrangement: Arrangement.Vertical, horizontalAlignment: Alignment.Horizontal, - state: ScrollState, - content: @Composable ColumnScope.() -> Unit + state: ScrollState?, + maxIntrinsicSize: Boolean, + content: @Composable() (ColumnScope.() -> Unit) ) { - Column(modifier.verticalScroll(rememberScrollState()), verticalArrangement, horizontalAlignment, content) + val state = state ?: LocalAppBarHandler.current?.scrollState ?: rememberScrollState() + val connection = LocalAppBarHandler.current?.connection + LaunchedEffect(Unit) { + snapshotFlow { state.value } + .collect { scrollPosition -> + val offset = connection?.appBarOffset + if (offset != null && (offset + scrollPosition).absoluteValue > 1) { + connection.appBarOffset = -scrollPosition.toFloat() +// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") + } + } + } + if (connection != null) { + Column( + if (maxIntrinsicSize) { + modifier.nestedScroll(connection).verticalScroll(state).height(IntrinsicSize.Max) + } else { + modifier.nestedScroll(connection).verticalScroll(state) + }, verticalArrangement, horizontalAlignment, content) + } else { + Column(if (maxIntrinsicSize) { + modifier.verticalScroll(state).height(IntrinsicSize.Max) + } else { + modifier.verticalScroll(state) + }, verticalArrangement, horizontalAlignment, content) + } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt index c702264d82..d05172a7a1 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/call/CallView.android.kt @@ -11,6 +11,7 @@ import android.media.* import android.os.Build import android.os.PowerManager import android.os.PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK +import android.view.View import android.view.ViewGroup import android.webkit.* import androidx.compose.desktop.ui.tooling.preview.Preview @@ -456,7 +457,7 @@ private fun DisabledBackgroundCallsButton() { ) { Text(stringResource(MR.strings.system_restricted_background_in_call_title), color = WarningOrange) Spacer(Modifier.width(8.dp)) - IconButton(onClick = { show = false }, Modifier.size(24.dp)) { + IconButton(onClick = { show = false }, Modifier.size(22.dp)) { Icon(painterResource(MR.images.ic_close), null, tint = WarningOrange) } } @@ -538,7 +539,7 @@ fun CallPermissionsView(pipActive: Boolean, hasVideo: Boolean, cancel: () -> Uni Icon( painterResource(MR.images.ic_call_500), stringResource(MR.strings.permissions_record_audio), - Modifier.size(24.dp), + Modifier.size(22.dp), tint = Color(0xFFFFFFD8) ) } @@ -546,14 +547,14 @@ fun CallPermissionsView(pipActive: Boolean, hasVideo: Boolean, cancel: () -> Uni Icon( painterResource(MR.images.ic_videocam), stringResource(MR.strings.permissions_camera), - Modifier.size(24.dp), + Modifier.size(22.dp), tint = Color(0xFFFFFFD8) ) } } } else { ColumnWithScrollBar(Modifier.fillMaxSize()) { - Spacer(Modifier.height(AppBarHeight)) + Spacer(Modifier.height(AppBarHeight * fontSizeSqrtMultiplier)) AppBarTitle(stringResource(MR.strings.permissions_required)) Spacer(Modifier.weight(1f)) @@ -670,37 +671,43 @@ fun WebRTCView(callCommand: SnapshotStateList, onResponse: (WVAPIM Box(Modifier.fillMaxSize()) { AndroidView( factory = { AndroidViewContext -> - (staticWebView ?: WebView(androidAppContext)).apply { - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, - ) - this.webChromeClient = object: WebChromeClient() { - override fun onPermissionRequest(request: PermissionRequest) { - if (request.origin.toString().startsWith("file:/")) { - request.grant(request.resources) - } else { - Log.d(TAG, "Permission request from webview denied.") - request.deny() + try { + (staticWebView ?: WebView(androidAppContext)).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + this.webChromeClient = object: WebChromeClient() { + override fun onPermissionRequest(request: PermissionRequest) { + if (request.origin.toString().startsWith("file:/")) { + request.grant(request.resources) + } else { + Log.d(TAG, "Permission request from webview denied.") + request.deny() + } } } + this.webViewClient = LocalContentWebViewClient(webView, assetLoader) + this.clearHistory() + this.clearCache(true) + this.addJavascriptInterface(WebRTCInterface(onResponse), "WebRTCInterface") + this.setBackgroundColor(android.graphics.Color.BLACK) + val webViewSettings = this.settings + webViewSettings.allowFileAccess = true + webViewSettings.allowContentAccess = true + webViewSettings.javaScriptEnabled = true + webViewSettings.mediaPlaybackRequiresUserGesture = false + webViewSettings.cacheMode = WebSettings.LOAD_NO_CACHE + if (staticWebView == null) { + this.loadUrl("file:android_asset/www/android/call.html") + } else { + webView.value = this + } } - this.webViewClient = LocalContentWebViewClient(webView, assetLoader) - this.clearHistory() - this.clearCache(true) - this.addJavascriptInterface(WebRTCInterface(onResponse), "WebRTCInterface") - this.setBackgroundColor(android.graphics.Color.BLACK) - val webViewSettings = this.settings - webViewSettings.allowFileAccess = true - webViewSettings.allowContentAccess = true - webViewSettings.javaScriptEnabled = true - webViewSettings.mediaPlaybackRequiresUserGesture = false - webViewSettings.cacheMode = WebSettings.LOAD_NO_CACHE - if (staticWebView == null) { - this.loadUrl("file:android_asset/www/android/call.html") - } else { - webView.value = this - } + } catch (e: Exception) { + Log.e(TAG, "Error initializing WebView: ${e.stackTraceToString()}") + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), generalGetString(MR.strings.error_initializing_web_view).format(e.stackTraceToString())) + return@AndroidView View(androidAppContext) } } ) { /* WebView */ } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt index c606e9acb0..05a9430ff1 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.android.kt @@ -2,12 +2,15 @@ package chat.simplex.common.views.chat.item import android.os.Build.VERSION.SDK_INT import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalContext import chat.simplex.common.model.CIFile +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.CurrentColors import chat.simplex.common.views.helpers.ModalManager import coil.ImageLoader import coil.compose.rememberAsyncImagePainter @@ -21,6 +24,7 @@ actual fun SimpleAndAnimatedImageView( imageBitmap: ImageBitmap, file: CIFile?, imageProvider: () -> ImageGalleryProvider, + smallView: Boolean, ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit ) { val context = LocalContext.current @@ -35,6 +39,14 @@ actual fun SimpleAndAnimatedImageView( if (getLoadedFilePath(file) != null) { ModalManager.fullscreen.showCustomModal(animated = false) { close -> ImageFullScreenView(imageProvider, close) + if (smallView) { + DisposableEffect(Unit) { + onDispose { + val c = CurrentColors.value.colors + platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get()) + } + } + } } } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.android.kt index f0f733111a..9a3d9e5e4f 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.android.kt @@ -21,6 +21,7 @@ actual fun ChatListNavLinkLayout( nextChatSelected: State, ) { var modifier = Modifier.fillMaxWidth() + if (!disabled) modifier = modifier .combinedClickable(onClick = click, onLongClick = { showMenu.value = true }) .onRightClick { showMenu.value = true } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt index 4a8b912cdd..3283593e09 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.android.kt @@ -36,7 +36,7 @@ private val CALL_BOTTOM_ICON_OFFSET = (-15).dp private val CALL_BOTTOM_ICON_HEIGHT = CALL_INTERACTIVE_AREA_HEIGHT + CALL_BOTTOM_ICON_OFFSET @Composable -actual fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableStateFlow) { +actual fun ActiveCallInteractiveArea(call: Call) { val onClick = { platform.androidStartCallActivity(false) } Box(Modifier.offset(y = CALL_TOP_OFFSET).height(CALL_INTERACTIVE_AREA_HEIGHT)) { val source = remember { MutableInteractionSource() } @@ -64,6 +64,9 @@ actual fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableState } } +// Temporary solution for storing a color that needs to be applied after call ends +var statusBarColorAfterCall = mutableIntStateOf(CurrentColors.value.colors.background.toArgb()) + @Composable private fun GreenLine(call: Call) { Row( @@ -81,9 +84,10 @@ private fun GreenLine(call: Call) { } val window = (LocalContext.current as Activity).window DisposableEffect(Unit) { + statusBarColorAfterCall.intValue = window.statusBarColor window.statusBarColor = SimplexGreen.toArgb() onDispose { - window.statusBarColor = Color.Black.toArgb() + window.statusBarColor = statusBarColorAfterCall.intValue } } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt index 9ee816bb76..9d1e0c4e97 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/helpers/Utils.android.kt @@ -241,10 +241,15 @@ private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, } actual fun getFileName(uri: URI): String? { - return androidAppContext.contentResolver.query(uri.toUri(), null, null, null, null)?.use { cursor -> - val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) - cursor.moveToFirst() - cursor.getString(nameIndex) + return try { + androidAppContext.contentResolver.query(uri.toUri(), null, null, null, null)?.use { cursor -> + val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + cursor.moveToFirst() + // Can make an exception + cursor.getString(nameIndex) + } + } catch (e: Exception) { + null } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.android.kt index bfe87b17d7..d9d3af7bb7 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.android.kt @@ -1,6 +1,8 @@ package chat.simplex.common.views.onboarding +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import chat.simplex.common.model.SharedPreference import chat.simplex.common.model.User import chat.simplex.res.MR @@ -8,8 +10,8 @@ import chat.simplex.res.MR @Composable actual fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference, onclick: (() -> Unit)?) { if (user == null) { - OnboardingActionButton(MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, true, onclick = onclick) + OnboardingActionButton(Modifier.fillMaxWidth(), labelId = MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, onclick = onclick) } else { - OnboardingActionButton(MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, true, onclick = onclick) + OnboardingActionButton(Modifier.fillMaxWidth(), labelId = MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, onclick = onclick) } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt index 5a60e1d1b0..b985601962 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/Appearance.android.kt @@ -13,20 +13,19 @@ import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme.colors import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat -import androidx.core.graphics.drawable.toBitmap -import chat.simplex.common.R import chat.simplex.common.model.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* @@ -34,7 +33,7 @@ import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.* import chat.simplex.common.helpers.APPLICATION_ID import chat.simplex.common.helpers.saveAppLocale -import chat.simplex.common.views.usersettings.AppearanceScope.ColorEditor +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.compose.painterResource @@ -46,9 +45,8 @@ enum class AppIcon(val image: ImageResource) { } @Composable -actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) { +actual fun AppearanceView(m: ChatModel) { val appIcon = remember { mutableStateOf(findEnabledIcon()) } - fun setAppIcon(newIcon: AppIcon) { if (appIcon.value == newIcon) return val newComponent = ComponentName(APPLICATION_ID, "chat.simplex.app.MainActivity_${newIcon.name.lowercase()}") @@ -65,18 +63,11 @@ actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatMod appIcon.value = newIcon } - AppearanceScope.AppearanceLayout( appIcon, m.controller.appPrefs.appLanguage, m.controller.appPrefs.systemDarkTheme, changeIcon = ::setAppIcon, - showSettingsModal = showSettingsModal, - editColor = { name, initialColor -> - ModalManager.start.showModalCloseable { close -> - ColorEditor(name, initialColor, close) - } - }, ) } @@ -86,14 +77,12 @@ fun AppearanceScope.AppearanceLayout( languagePref: SharedPreference, systemDarkTheme: SharedPreference, changeIcon: (AppIcon) -> Unit, - showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - editColor: (ThemeColor, Color) -> Unit, ) { ColumnWithScrollBar( Modifier.fillMaxWidth(), ) { AppBarTitle(stringResource(MR.strings.appearance_settings)) - SectionView(stringResource(MR.strings.settings_section_title_language), padding = PaddingValues()) { + SectionView(stringResource(MR.strings.settings_section_title_interface), padding = PaddingValues()) { val context = LocalContext.current // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // SectionItemWithValue( @@ -119,8 +108,20 @@ fun AppearanceScope.AppearanceLayout( } } // } + + SettingsPreferenceItem(icon = null, stringResource(MR.strings.one_hand_ui), ChatModel.controller.appPrefs.oneHandUI) { + val c = CurrentColors.value.colors + platform.androidSetStatusAndNavBarColors(c.isLight, c.background, false, false) + } } + SectionDividerSpaced() + ThemesSection(systemDarkTheme) + + SectionDividerSpaced() + ProfileImageSection() + + SectionDividerSpaced(maxTopPadding = true) SectionView(stringResource(MR.strings.settings_section_title_icon), padding = PaddingValues(horizontal = DEFAULT_PADDING_HALF)) { LazyRow { @@ -131,7 +132,8 @@ fun AppearanceScope.AppearanceLayout( contentDescription = "", contentScale = ContentScale.Fit, modifier = Modifier - .shadow(if (item == icon.value) 1.dp else 0.dp, ambientColor = colors.secondaryVariant) + .border(1.dp, color = if (item == icon.value) colors.secondaryVariant else Color.Transparent, RoundedCornerShape(percent = 22)) + .clip(RoundedCornerShape(percent = 22)) .size(70.dp) .clickable { changeIcon(item) } .padding(10.dp) @@ -146,10 +148,8 @@ fun AppearanceScope.AppearanceLayout( } SectionDividerSpaced(maxTopPadding = true) - ProfileImageSection() + FontScaleSection() - SectionDividerSpaced(maxTopPadding = true) - ThemesSection(systemDarkTheme, showSettingsModal, editColor) SectionBottomSpacer() } } @@ -169,8 +169,6 @@ fun PreviewAppearanceSettings() { languagePref = SharedPreference({ null }, {}), systemDarkTheme = SharedPreference({ null }, {}), changeIcon = {}, - showSettingsModal = { {} }, - editColor = { _, _ -> }, ) } } diff --git a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt index f2c3d86ab5..38818a123d 100644 --- a/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt +++ b/apps/multiplatform/common/src/androidMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.android.kt @@ -19,9 +19,9 @@ actual fun SettingsSectionApp( withAuth: (title: String, desc: String, block: () -> Unit) -> Unit ) { SectionView(stringResource(MR.strings.settings_section_title_app)) { - SettingsActionItem(painterResource(MR.images.ic_restart_alt), stringResource(MR.strings.settings_restart_app), ::restartApp, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_power_settings_new), stringResource(MR.strings.settings_shutdown), { shutdownAppAlert(::shutdownApp) }, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) }, extraPadding = true) + SettingsActionItem(painterResource(MR.images.ic_restart_alt), stringResource(MR.strings.settings_restart_app), ::restartApp) + SettingsActionItem(painterResource(MR.images.ic_power_settings_new), stringResource(MR.strings.settings_shutdown), { shutdownAppAlert(::shutdownApp) }) + SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) }) AppVersionItem(showVersion) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt index e7dda42ade..94ca307529 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/App.kt @@ -1,5 +1,7 @@ package chat.simplex.common +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.SizeTransform import androidx.compose.animation.core.Animatable import androidx.compose.foundation.* import androidx.compose.foundation.interaction.MutableInteractionSource @@ -15,9 +17,11 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import chat.simplex.common.views.usersettings.SetDeliveryReceiptsView import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.CreateFirstProfile @@ -28,6 +32,8 @@ import chat.simplex.common.views.chat.ChatView import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.database.DatabaseErrorView import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.helpers.ModalManager.Companion.fromEndToStartTransition +import chat.simplex.common.views.helpers.ModalManager.Companion.fromStartToEndTransition import chat.simplex.common.views.localauth.VerticalDivider import chat.simplex.common.views.onboarding.* import chat.simplex.common.views.usersettings.* @@ -36,6 +42,7 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* import kotlinx.coroutines.flow.* +import kotlin.math.sqrt data class SettingsViewState( val userPickerState: MutableStateFlow, @@ -44,6 +51,7 @@ data class SettingsViewState( @Composable fun AppScreen() { + AppBarHandler.appBarMaxHeightPx = with(LocalDensity.current) { AppBarHeight.roundToPx() } SimpleXTheme { ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { Surface(color = MaterialTheme.colors.background, contentColor = LocalContentColor.current) { @@ -68,7 +76,7 @@ fun MainScreen() { !chatModel.controller.appPrefs.laNoticeShown.get() && showAdvertiseLAAlert && chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete - && chatModel.chats.size > 2 + && chatModel.chats.size > 3 && chatModel.activeCallInvitation.value == null ) { AppLock.showLANotice(ChatModel.controller.appPrefs.laNoticeShown) } @@ -78,6 +86,7 @@ fun MainScreen() { laUnavailableInstructionAlert() } } + platform.desktopShowAppUpdateNotice() LaunchedEffect(chatModel.clearOverlays.value) { if (chatModel.clearOverlays.value) { ModalManager.closeAllModalsEverywhere() @@ -145,17 +154,30 @@ fun MainScreen() { } } } - onboarding == OnboardingStage.Step1_SimpleXInfo -> { - SimpleXInfo(chatModel, onboarding = true) - if (appPlatform.isDesktop) { - ModalManager.fullscreen.showInView() + else -> AnimatedContent(targetState = onboarding, + transitionSpec = { + if (targetState > initialState) { + fromEndToStartTransition() + } else { + fromStartToEndTransition() + }.using(SizeTransform(clip = false)) + } + ) { state -> + when (state) { + OnboardingStage.OnboardingComplete -> { /* handled out of AnimatedContent block */} + OnboardingStage.Step1_SimpleXInfo -> { + SimpleXInfo(chatModel, onboarding = true) + if (appPlatform.isDesktop) { + ModalManager.fullscreen.showInView() + } + } + OnboardingStage.Step2_CreateProfile -> CreateFirstProfile(chatModel) {} + OnboardingStage.LinkAMobile -> LinkAMobile() + OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel) + OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel, null) + OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel) } } - onboarding == OnboardingStage.Step2_CreateProfile -> CreateFirstProfile(chatModel) {} - onboarding == OnboardingStage.LinkAMobile -> LinkAMobile() - onboarding == OnboardingStage.Step2_5_SetupDatabasePassphrase -> SetupDatabasePassphrase(chatModel) - onboarding == OnboardingStage.Step3_CreateSimpleXAddress -> CreateSimpleXAddress(chatModel, null) - onboarding == OnboardingStage.Step4_SetNotificationsMode -> SetNotificationsMode(chatModel) } if (appPlatform.isAndroid) { ModalManager.fullscreen.showInView() @@ -232,7 +254,7 @@ fun AndroidScreen(settingsState: SettingsViewState) { BoxWithConstraints { val call = remember { chatModel.activeCall} .value val showCallArea = call != null && call.callState != CallState.WaitCapabilities && call.callState != CallState.InvitationAccepted - var currentChatId by rememberSaveable { mutableStateOf(chatModel.chatId.value) } + val currentChatId = remember { mutableStateOf(chatModel.chatId.value) } val offset = remember { Animatable(if (chatModel.chatId.value == null) 0f else maxWidth.value) } Box( Modifier @@ -261,21 +283,37 @@ fun AndroidScreen(settingsState: SettingsViewState) { snapshotFlow { chatModel.chatId.value } .distinctUntilChanged() .collect { - if (it == null) onComposed(null) - currentChatId = it + if (it == null) { + platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get()) + onComposed(null) + } + currentChatId.value = it } } } + LaunchedEffect(Unit) { + snapshotFlow { ModalManager.center.modalCount.value > 0 } + .filter { chatModel.chatId.value == null } + .collect { modalBackground -> + if (chatModel.newChatSheetVisible.value) { + platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, appPrefs.oneHandUI.get()) + } else if (modalBackground) { + platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, false) + } else { + platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get()) + } + } + } Box(Modifier .graphicsLayer { translationX = maxWidth.toPx() - offset.value.dp.toPx() } .padding(top = if (showCallArea) ANDROID_CALL_TOP_PADDING else 0.dp) ) Box2@{ - currentChatId?.let { - ChatView(it, chatModel, onComposed) + currentChatId.value?.let { + ChatView(currentChatId, onComposed) } } if (call != null && showCallArea) { - ActiveCallInteractiveArea(call, remember { MutableStateFlow(AnimatedViewState.GONE) }) + ActiveCallInteractiveArea(call) } } } @@ -295,9 +333,9 @@ fun StartPartOfScreen(settingsState: SettingsViewState) { @Composable fun CenterPartOfScreen() { - val currentChatId by remember { ChatModel.chatId } + val currentChatId = remember { ChatModel.chatId } LaunchedEffect(Unit) { - snapshotFlow { currentChatId } + snapshotFlow { currentChatId.value } .distinctUntilChanged() .collect { if (it != null) { @@ -305,7 +343,7 @@ fun CenterPartOfScreen() { } } } - when (val id = currentChatId) { + when (currentChatId.value) { null -> { if (!rememberUpdatedState(ModalManager.center.hasModalsOpen()).value) { Box( @@ -320,7 +358,7 @@ fun CenterPartOfScreen() { ModalManager.center.showInView() } } - else -> ChatView(id, chatModel) {} + else -> ChatView(currentChatId) {} } } @@ -333,38 +371,38 @@ fun EndPartOfScreen() { fun DesktopScreen(settingsState: SettingsViewState) { Box { // 56.dp is a size of unused space of settings drawer - Box(Modifier.width(DEFAULT_START_MODAL_WIDTH + 56.dp)) { + Box(Modifier.width(DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier + 56.dp)) { StartPartOfScreen(settingsState) } - Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH)) { + Box(Modifier.widthIn(max = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) { ModalManager.start.showInView() SwitchingUsersView() } - Row(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH).clipToBounds()) { + Row(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier).clipToBounds()) { Box(Modifier.widthIn(min = DEFAULT_MIN_CENTER_MODAL_WIDTH).weight(1f)) { CenterPartOfScreen() } if (ModalManager.end.hasModalsOpen()) { VerticalDivider() } - Box(Modifier.widthIn(max = DEFAULT_END_MODAL_WIDTH).clipToBounds()) { + Box(Modifier.widthIn(max = DEFAULT_END_MODAL_WIDTH * fontSizeSqrtMultiplier).clipToBounds()) { EndPartOfScreen() } } val (userPickerState, scaffoldState ) = settingsState val scope = rememberCoroutineScope() - if (scaffoldState.drawerState.isOpen) { + if (scaffoldState.drawerState.isOpen || (ModalManager.start.hasModalsOpen && !ModalManager.center.hasModalsOpen)) { Box( Modifier .fillMaxSize() - .padding(start = DEFAULT_START_MODAL_WIDTH) + .padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier) .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = { ModalManager.start.closeModals() scope.launch { settingsState.scaffoldState.drawerState.close() } }) ) } - VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH)) + VerticalDivider(Modifier.padding(start = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier)) tryOrShowError("UserPicker", error = {}) { UserPicker(chatModel, userPickerState) { scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 441e8e0186..628481477d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -4,7 +4,7 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateMap -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.* import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.font.* import androidx.compose.ui.text.style.TextDecoration @@ -19,6 +19,8 @@ import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.internal.ChannelFlow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.datetime.* @@ -28,6 +30,7 @@ import kotlinx.serialization.descriptors.* import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.json.* +import java.io.Closeable import java.io.File import java.net.URI import java.time.format.DateTimeFormatter @@ -52,7 +55,10 @@ object ChatModel { val chatDbStatus = mutableStateOf(null) val ctrlInitInProgress = mutableStateOf(false) val dbMigrationInProgress = mutableStateOf(false) - val chats = mutableStateListOf() + val incompleteInitializedDbRemoved = mutableStateOf(false) + private val _chats = mutableStateOf(SnapshotStateList()) + val chats: State> = _chats + private val chatsContext = ChatsContext() // map of connections network statuses, key is agent connection id val networkStatuses = mutableStateMapOf() val switchingUsersAndHosts = mutableStateOf(false) @@ -64,6 +70,7 @@ object ChatModel { val deletedChats = mutableStateOf>>(emptyList()) val chatItemStatuses = mutableMapOf() val groupMembers = mutableStateListOf() + val groupMembersIndexes = mutableStateMapOf() val terminalItems = mutableStateOf>(listOf()) val userAddress = mutableStateOf(null) @@ -78,6 +85,9 @@ object ChatModel { // set when app is opened via contact or invitation URI (rhId, uri) val appOpenUrl = mutableStateOf?>(null) + // Needed to check for bottom nav bar and to apply or not navigation bar color on Android + val newChatSheetVisible = mutableStateOf(false) + // preferences val notificationPreviewMode by lazy { mutableStateOf( @@ -120,7 +130,10 @@ object ChatModel { val clipboardHasText = mutableStateOf(false) val networkInfo = mutableStateOf(UserNetworkInfo(networkType = UserNetworkType.OTHER, online = true)) - val updatingChatsMutex: Mutex = Mutex() + val updatingProgress = mutableStateOf(null as Float?) + var updatingRequest: Closeable? = null + + private val updatingChatsMutex: Mutex = Mutex() val changingActiveUserMutex: Mutex = Mutex() val desktopNoUserNoRemote: Boolean @Composable get() = appPlatform.isDesktop && currentUser.value == null && currentRemoteHost.value == null @@ -164,103 +177,132 @@ object ChatModel { } // toList() here is to prevent ConcurrentModificationException that is rarely happens but happens - fun hasChat(rhId: Long?, id: String): Boolean = chats.toList().firstOrNull { it.id == id && it.remoteHostId == rhId } != null + fun hasChat(rhId: Long?, id: String): Boolean = chats.value.firstOrNull { it.id == id && it.remoteHostId == rhId } != null // TODO pass rhId? - fun getChat(id: String): Chat? = chats.toList().firstOrNull { it.id == id } - fun getContactChat(contactId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId } - fun getGroupChat(groupId: Long): Chat? = chats.toList().firstOrNull { it.chatInfo is ChatInfo.Group && it.chatInfo.apiId == groupId } - fun getGroupMember(groupMemberId: Long): GroupMember? = groupMembers.firstOrNull { it.groupMemberId == groupMemberId } - private fun getChatIndex(rhId: Long?, id: String): Int = chats.toList().indexOfFirst { it.id == id && it.remoteHostId == rhId } - fun addChat(chat: Chat) = chats.add(index = 0, chat) + fun getChat(id: String): Chat? = chats.value.firstOrNull { it.id == id } + fun getContactChat(contactId: Long): Chat? = chats.value.firstOrNull { it.chatInfo is ChatInfo.Direct && it.chatInfo.apiId == contactId } + fun getGroupChat(groupId: Long): Chat? = chats.value.firstOrNull { it.chatInfo is ChatInfo.Group && it.chatInfo.apiId == groupId } - fun updateChatInfo(rhId: Long?, cInfo: ChatInfo) { - val i = getChatIndex(rhId, cInfo.id) - if (i >= 0) { - val currentCInfo = chats[i].chatInfo - var newCInfo = cInfo - if (currentCInfo is ChatInfo.Direct && newCInfo is ChatInfo.Direct) { - val currentStats = currentCInfo.contact.activeConn?.connectionStats - val newConn = newCInfo.contact.activeConn - val newStats = newConn?.connectionStats - if (currentStats != null && newConn != null && newStats == null) { - newCInfo = newCInfo.copy( - contact = newCInfo.contact.copy( - activeConn = newConn.copy( - connectionStats = currentStats + fun populateGroupMembersIndexes() { + groupMembersIndexes.clear() + groupMembers.forEachIndexed { i, member -> + groupMembersIndexes[member.groupMemberId] = i + } + } + + fun getGroupMember(groupMemberId: Long): GroupMember? { + val memberIndex = groupMembersIndexes[groupMemberId] + return if (memberIndex != null) { + groupMembers[memberIndex] + } else { + null + } + } + + suspend fun withChats(action: suspend ChatsContext.() -> T): T = updatingChatsMutex.withLock { + chatsContext.action() + } + + class ChatsContext { + val chats = _chats + + suspend fun addChat(chat: Chat) { + chats.add(index = 0, chat) + popChatCollector.throttlePopChat(chat.remoteHostId, chat.id, currentPosition = 0) + } + + fun updateChatInfo(rhId: Long?, cInfo: ChatInfo) { + val i = getChatIndex(rhId, cInfo.id) + if (i >= 0) { + val currentCInfo = chats[i].chatInfo + var newCInfo = cInfo + if (currentCInfo is ChatInfo.Direct && newCInfo is ChatInfo.Direct) { + val currentStats = currentCInfo.contact.activeConn?.connectionStats + val newConn = newCInfo.contact.activeConn + val newStats = newConn?.connectionStats + if (currentStats != null && newConn != null && newStats == null) { + newCInfo = newCInfo.copy( + contact = newCInfo.contact.copy( + activeConn = newConn.copy( + connectionStats = currentStats + ) ) ) - ) - } - } - chats[i] = chats[i].copy(chatInfo = newCInfo) - } - } - - fun updateContactConnection(rhId: Long?, contactConnection: PendingContactConnection) = updateChat(rhId, ChatInfo.ContactConnection(contactConnection)) - - fun updateContact(rhId: Long?, contact: Contact) = updateChat(rhId, ChatInfo.Direct(contact), addMissing = contact.directOrUsed) - - fun updateContactConnectionStats(rhId: Long?, contact: Contact, connectionStats: ConnectionStats) { - val updatedConn = contact.activeConn?.copy(connectionStats = connectionStats) - val updatedContact = contact.copy(activeConn = updatedConn) - updateContact(rhId, updatedContact) - } - - fun updateGroup(rhId: Long?, groupInfo: GroupInfo) = updateChat(rhId, ChatInfo.Group(groupInfo)) - - private fun updateChat(rhId: Long?, cInfo: ChatInfo, addMissing: Boolean = true) { - if (hasChat(rhId, cInfo.id)) { - updateChatInfo(rhId, cInfo) - } else if (addMissing) { - addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf())) - } - } - - fun updateChats(newChats: List) { - chats.clear() - chats.addAll(newChats) - - val cId = chatId.value - // If chat is null, it was deleted in background after apiGetChats call - if (cId != null && getChat(cId) == null) { - chatId.value = null - } - } - - fun replaceChat(rhId: Long?, id: String, chat: Chat) { - val i = getChatIndex(rhId, id) - if (i >= 0) { - chats[i] = chat - } else { - // invalid state, correcting - chats.add(index = 0, chat) - } - } - - suspend fun addChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) = updatingChatsMutex.withLock { - // update previews - val i = getChatIndex(rhId, cInfo.id) - val chat: Chat - if (i >= 0) { - chat = chats[i] - val newPreviewItem = when (cInfo) { - is ChatInfo.Group -> { - val currentPreviewItem = chat.chatItems.firstOrNull() - if (currentPreviewItem != null) { - if (cItem.meta.itemTs >= currentPreviewItem.meta.itemTs) { - cItem - } else { - currentPreviewItem - } - } else { - cItem } } - else -> cItem + chats[i] = chats[i].copy(chatInfo = newCInfo) } - chats[i] = chat.copy( - chatItems = arrayListOf(newPreviewItem), - chatStats = + } + + suspend fun updateContactConnection(rhId: Long?, contactConnection: PendingContactConnection) = updateChat(rhId, ChatInfo.ContactConnection(contactConnection)) + + suspend fun updateContact(rhId: Long?, contact: Contact) = updateChat(rhId, ChatInfo.Direct(contact), addMissing = contact.directOrUsed) + + suspend fun updateContactConnectionStats(rhId: Long?, contact: Contact, connectionStats: ConnectionStats) { + val updatedConn = contact.activeConn?.copy(connectionStats = connectionStats) + val updatedContact = contact.copy(activeConn = updatedConn) + updateContact(rhId, updatedContact) + } + + suspend fun updateGroup(rhId: Long?, groupInfo: GroupInfo) = updateChat(rhId, ChatInfo.Group(groupInfo)) + + private suspend fun updateChat(rhId: Long?, cInfo: ChatInfo, addMissing: Boolean = true) { + if (hasChat(rhId, cInfo.id)) { + updateChatInfo(rhId, cInfo) + } else if (addMissing) { + addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf())) + } + } + + fun updateChats(newChats: List) { + chats.replaceAll(newChats) + popChatCollector.clear() + + val cId = chatId.value + // If chat is null, it was deleted in background after apiGetChats call + if (cId != null && getChat(cId) == null) { + chatId.value = null + } + } + + suspend fun replaceChat(rhId: Long?, id: String, chat: Chat) { + val i = getChatIndex(rhId, id) + if (i >= 0) { + chats[i] = chat + } else { + // invalid state, correcting + addChat(chat) + } + } + suspend fun addChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) { + // mark chat non deleted + if (cInfo is ChatInfo.Direct && cInfo.chatDeleted) { + val updatedContact = cInfo.contact.copy(chatDeleted = false) + updateContact(rhId, updatedContact) + } + // update previews + val i = getChatIndex(rhId, cInfo.id) + val chat: Chat + if (i >= 0) { + chat = chats[i] + val newPreviewItem = when (cInfo) { + is ChatInfo.Group -> { + val currentPreviewItem = chat.chatItems.firstOrNull() + if (currentPreviewItem != null) { + if (cItem.meta.itemTs >= currentPreviewItem.meta.itemTs) { + cItem + } else { + currentPreviewItem + } + } else { + cItem + } + } + else -> cItem + } + chats[i] = chat.copy( + chatItems = arrayListOf(newPreviewItem), + chatStats = if (cItem.meta.itemStatus is CIStatus.RcvNew) { val minUnreadId = if(chat.chatStats.minUnreadItemId == 0L) cItem.id else chat.chatStats.minUnreadItemId increaseUnreadCounter(rhId, currentUser.value!!) @@ -268,123 +310,267 @@ object ChatModel { } else chat.chatStats - ) - if (i > 0) { - popChat_(i) + ) + if (appPlatform.isDesktop && cItem.chatDir.sent) { + addChat(chats.removeAt(i)) + } else { + popChatCollector.throttlePopChat(chat.remoteHostId, chat.id, currentPosition = i) + } + } else { + addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) } - } else { - addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) - } - withContext(Dispatchers.Main) { - // add to current chat - if (chatId.value == cInfo.id) { - // Prevent situation when chat item already in the list received from backend - if (chatItems.value.none { it.id == cItem.id }) { - if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { - chatItems.add(kotlin.math.max(0, chatItems.value.lastIndex), cItem) - } else { - chatItems.add(cItem) + withContext(Dispatchers.Main) { + // add to current chat + if (chatId.value == cInfo.id) { + // Prevent situation when chat item already in the list received from backend + if (chatItems.value.none { it.id == cItem.id }) { + if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { + chatItems.add(kotlin.math.max(0, chatItems.value.lastIndex), cItem) + } else { + chatItems.add(cItem) + } } } } } - } - suspend fun upsertChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem): Boolean = updatingChatsMutex.withLock { - // update previews - val i = getChatIndex(rhId, cInfo.id) - val chat: Chat - val res: Boolean - if (i >= 0) { - chat = chats[i] - val pItem = chat.chatItems.lastOrNull() - if (pItem?.id == cItem.id) { - chats[i] = chat.copy(chatItems = arrayListOf(cItem)) - if (pItem.isRcvNew && !cItem.isRcvNew) { - // status changed from New to Read, update counter - decreaseCounterInChat(rhId, cInfo.id) + suspend fun upsertChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem): Boolean { + // update previews + val i = getChatIndex(rhId, cInfo.id) + val chat: Chat + val res: Boolean + if (i >= 0) { + chat = chats[i] + val pItem = chat.chatItems.lastOrNull() + if (pItem?.id == cItem.id) { + chats[i] = chat.copy(chatItems = arrayListOf(cItem)) + if (pItem.isRcvNew && !cItem.isRcvNew) { + // status changed from New to Read, update counter + decreaseCounterInChat(rhId, cInfo.id) + } + } + res = false + } else { + addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) + res = true + } + return withContext(Dispatchers.Main) { + // update current chat + if (chatId.value == cInfo.id) { + if (cItem.isDeletedContent || cItem.meta.itemDeleted != null) { + AudioPlayer.stop(cItem) + } + val items = chatItems.value + val itemIndex = items.indexOfFirst { it.id == cItem.id } + if (itemIndex >= 0) { + items[itemIndex] = cItem + false + } else { + val status = chatItemStatuses.remove(cItem.id) + val ci = if (status != null && cItem.meta.itemStatus is CIStatus.SndNew) { + cItem.copy(meta = cItem.meta.copy(itemStatus = status)) + } else { + cItem + } + chatItems.add(ci) + true + } + } else { + res } } - res = false - } else { - addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = arrayListOf(cItem))) - res = true } - return withContext(Dispatchers.Main) { - // update current chat + + suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null) { + withContext(Dispatchers.Main) { + if (chatId.value == cInfo.id) { + val items = chatItems.value + val itemIndex = items.indexOfFirst { it.id == cItem.id } + if (itemIndex >= 0) { + items[itemIndex] = cItem + } + } else if (status != null) { + chatItemStatuses[cItem.id] = status + } + } + } + + fun removeChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) { + if (cItem.isRcvNew) { + decreaseCounterInChat(rhId, cInfo.id) + } + // update previews + val i = getChatIndex(rhId, cInfo.id) + val chat: Chat + if (i >= 0) { + chat = chats[i] + val pItem = chat.chatItems.lastOrNull() + if (pItem?.id == cItem.id) { + chats[i] = chat.copy(chatItems = arrayListOf(ChatItem.deletedItemDummy)) + } + } + // remove from current chat if (chatId.value == cInfo.id) { - val items = chatItems.value - val itemIndex = items.indexOfFirst { it.id == cItem.id } - if (itemIndex >= 0) { - items[itemIndex] = cItem + chatItems.removeAll { + val remove = it.id == cItem.id + if (remove) { AudioPlayer.stop(it) } + remove + } + } + } + + fun clearChat(rhId: Long?, cInfo: ChatInfo) { + // clear preview + val i = getChatIndex(rhId, cInfo.id) + if (i >= 0) { + decreaseUnreadCounter(rhId, currentUser.value!!, chats[i].chatStats.unreadCount) + chats[i] = chats[i].copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats(), chatInfo = cInfo) + } + // clear current chat + if (chatId.value == cInfo.id) { + chatItemStatuses.clear() + chatItems.clear() + } + } + + val popChatCollector = PopChatCollector() + + class PopChatCollector { + private val subject = MutableSharedFlow() + private var remoteHostId: Long? = null + private val chatsToPop = mutableMapOf() + + init { + withLongRunningApi { + subject + .throttleLatest(2000) + .collect { + withChats { + chats.replaceAll(popCollectedChats()) + } + } + } + } + + suspend fun throttlePopChat(rhId: Long?, chatId: ChatId, currentPosition: Int) { + if (rhId != remoteHostId) { + chatsToPop.clear() + remoteHostId = rhId + } + if (currentPosition > 0 || chatsToPop.isNotEmpty()) { + chatsToPop[chatId] = Clock.System.now() + subject.emit(Unit) + } + } + + fun clear() = chatsToPop.clear() + + private fun popCollectedChats(): List { + val chs = mutableListOf() + // collect chats that received updates + for ((chatId, popTs) in chatsToPop.entries) { + val ch = getChat(chatId) + if (ch != null) { + ch.popTs = popTs + chs.add(ch) + } + } + // sort chats by pop timestamp in descending order + val newChats = ArrayList(chs.sortedByDescending { it.popTs }) + newChats.addAll(chats.value.filter { !chatsToPop.containsKey(it.chatInfo.id) } ) + chatsToPop.clear() + return newChats + } + } + + fun markChatItemsRead(remoteHostId: Long?, chatInfo: ChatInfo, range: CC.ItemRange? = null, unreadCountAfter: Int? = null) { + val cInfo = chatInfo + val markedRead = markItemsReadInCurrentChat(chatInfo, range) + // update preview + val chatIdx = getChatIndex(remoteHostId, cInfo.id) + if (chatIdx >= 0) { + val chat = chats[chatIdx] + val lastId = chat.chatItems.lastOrNull()?.id + if (lastId != null) { + val unreadCount = unreadCountAfter ?: if (range != null) chat.chatStats.unreadCount - markedRead else 0 + decreaseUnreadCounter(remoteHostId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount) + chats[chatIdx] = chat.copy( + chatStats = chat.chatStats.copy( + unreadCount = unreadCount, + // Can't use minUnreadItemId currently since chat items can have unread items between read items + //minUnreadItemId = if (range != null) kotlin.math.max(chat.chatStats.minUnreadItemId, range.to + 1) else lastId + 1 + ) + ) + } + } + } + + private fun decreaseCounterInChat(rhId: Long?, chatId: ChatId) { + val chatIndex = getChatIndex(rhId, chatId) + if (chatIndex == -1) return + + val chat = chats[chatIndex] + val unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0) + decreaseUnreadCounter(rhId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount) + chats[chatIndex] = chat.copy( + chatStats = chat.chatStats.copy( + unreadCount = unreadCount, + ) + ) + } + + fun removeChat(rhId: Long?, id: String) { + chats.removeAll { it.id == id && it.remoteHostId == rhId } + } + + suspend fun upsertGroupMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember): Boolean { + // user member was updated + if (groupInfo.membership.groupMemberId == member.groupMemberId) { + updateGroup(rhId, groupInfo) + return false + } + // update current chat + return if (chatId.value == groupInfo.id) { + val memberIndex = groupMembersIndexes[member.groupMemberId] + val updated = chatItems.value.map { + // Take into account only specific changes, not all. Other member updates are not important and can be skipped + if (it.chatDir is CIDirection.GroupRcv && it.chatDir.groupMember.groupMemberId == member.groupMemberId && + (it.chatDir.groupMember.image != member.image || + it.chatDir.groupMember.chatViewName != member.chatViewName || + it.chatDir.groupMember.blocked != member.blocked || + it.chatDir.groupMember.memberRole != member.memberRole) + ) + it.copy(chatDir = CIDirection.GroupRcv(member)) + else + it + } + if (updated != chatItems.value) { + chatItems.replaceAll(updated) + } + if (memberIndex != null) { + groupMembers[memberIndex] = member false } else { - val status = chatItemStatuses.remove(cItem.id) - val ci = if (status != null && cItem.meta.itemStatus is CIStatus.SndNew) { - cItem.copy(meta = cItem.meta.copy(itemStatus = status)) - } else { - cItem - } - chatItems.add(ci) + groupMembers.add(member) + groupMembersIndexes[member.groupMemberId] = groupMembers.size - 1 true } } else { - res + false + } + } + + suspend fun updateGroupMemberConnectionStats(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) { + val memberConn = member.activeConn + if (memberConn != null) { + val updatedConn = memberConn.copy(connectionStats = connectionStats) + val updatedMember = member.copy(activeConn = updatedConn) + upsertGroupMember(rhId, groupInfo, updatedMember) } } } - suspend fun updateChatItem(cInfo: ChatInfo, cItem: ChatItem, status: CIStatus? = null) { - withContext(Dispatchers.Main) { - if (chatId.value == cInfo.id) { - val items = chatItems.value - val itemIndex = items.indexOfFirst { it.id == cItem.id } - if (itemIndex >= 0) { - items[itemIndex] = cItem - } - } else if (status != null) { - chatItemStatuses[cItem.id] = status - } - } - } - - fun removeChatItem(rhId: Long?, cInfo: ChatInfo, cItem: ChatItem) { - if (cItem.isRcvNew) { - decreaseCounterInChat(rhId, cInfo.id) - } - // update previews - val i = getChatIndex(rhId, cInfo.id) - val chat: Chat - if (i >= 0) { - chat = chats[i] - val pItem = chat.chatItems.lastOrNull() - if (pItem?.id == cItem.id) { - chats[i] = chat.copy(chatItems = arrayListOf(ChatItem.deletedItemDummy)) - } - } - // remove from current chat - if (chatId.value == cInfo.id) { - chatItems.removeAll { - val remove = it.id == cItem.id - if (remove) { AudioPlayer.stop(it) } - remove - } - } - } - - fun clearChat(rhId: Long?, cInfo: ChatInfo) { - // clear preview - val i = getChatIndex(rhId, cInfo.id) - if (i >= 0) { - decreaseUnreadCounter(rhId, currentUser.value!!, chats[i].chatStats.unreadCount) - chats[i] = chats[i].copy(chatItems = arrayListOf(), chatStats = Chat.ChatStats(), chatInfo = cInfo) - } - // clear current chat - if (chatId.value == cInfo.id) { - chatItemStatuses.clear() - chatItems.clear() - } - } + private fun getChatIndex(rhId: Long?, id: String): Int = chats.value.indexOfFirst { it.id == id && it.remoteHostId == rhId } fun updateCurrentUser(rhId: Long?, newProfile: Profile, preferences: FullChatPreferences? = null) { val current = currentUser.value ?: return @@ -399,6 +585,18 @@ object ChatModel { currentUser.value = updated } + fun updateCurrentUserUiThemes(rhId: Long?, uiThemes: ThemeModeOverrides?) { + val current = currentUser.value ?: return + val updated = current.copy( + uiThemes = uiThemes + ) + val i = users.indexOfFirst { it.user.userId == current.userId && it.user.remoteHostId == rhId } + if (i != -1) { + users[i] = users[i].copy(user = updated) + } + currentUser.value = updated + } + suspend fun addLiveDummy(chatInfo: ChatInfo): ChatItem { val cItem = ChatItem.liveDummy(chatInfo is ChatInfo.Direct) withContext(Dispatchers.Main) { @@ -413,30 +611,8 @@ object ChatModel { } } - fun markChatItemsRead(chat: Chat, range: CC.ItemRange? = null, unreadCountAfter: Int? = null) { - val cInfo = chat.chatInfo - val markedRead = markItemsReadInCurrentChat(chat, range) - // update preview - val chatIdx = getChatIndex(chat.remoteHostId, cInfo.id) - if (chatIdx >= 0) { - val chat = chats[chatIdx] - val lastId = chat.chatItems.lastOrNull()?.id - if (lastId != null) { - val unreadCount = unreadCountAfter ?: if (range != null) chat.chatStats.unreadCount - markedRead else 0 - decreaseUnreadCounter(chat.remoteHostId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount) - chats[chatIdx] = chat.copy( - chatStats = chat.chatStats.copy( - unreadCount = unreadCount, - // Can't use minUnreadItemId currently since chat items can have unread items between read items - //minUnreadItemId = if (range != null) kotlin.math.max(chat.chatStats.minUnreadItemId, range.to + 1) else lastId + 1 - ) - ) - } - } - } - - private fun markItemsReadInCurrentChat(chat: Chat, range: CC.ItemRange? = null): Int { - val cInfo = chat.chatInfo + private fun markItemsReadInCurrentChat(chatInfo: ChatInfo, range: CC.ItemRange? = null): Int { + val cInfo = chatInfo var markedRead = 0 if (chatId.value == cInfo.id) { var i = 0 @@ -459,20 +635,6 @@ object ChatModel { return markedRead } - private fun decreaseCounterInChat(rhId: Long?, chatId: ChatId) { - val chatIndex = getChatIndex(rhId, chatId) - if (chatIndex == -1) return - - val chat = chats[chatIndex] - val unreadCount = kotlin.math.max(chat.chatStats.unreadCount - 1, 0) - decreaseUnreadCounter(rhId, currentUser.value!!, chat.chatStats.unreadCount - unreadCount) - chats[chatIndex] = chat.copy( - chatStats = chat.chatStats.copy( - unreadCount = unreadCount, - ) - ) - } - fun increaseUnreadCounter(rhId: Long?, user: UserLike) { changeUnreadCounter(rhId, user, 1) } @@ -566,16 +728,12 @@ object ChatModel { // } // } - private fun popChat_(i: Int) { - val chat = chats.removeAt(i) - chats.add(index = 0, chat) - } - fun replaceConnReqView(id: String, withId: String) { if (id == showingInvitation.value?.connId) { showingInvitation.value = null chatModel.chatItems.clear() chatModel.chatId.value = withId + ModalManager.start.closeModals() ModalManager.end.closeModals() } } @@ -586,6 +744,7 @@ object ChatModel { chatModel.chatItems.clear() chatModel.chatId.value = null // Close NewChatView + ModalManager.start.closeModals() ModalManager.center.closeModals() ModalManager.end.closeModals() } @@ -595,40 +754,6 @@ object ChatModel { showingInvitation.value = showingInvitation.value?.copy(connChatUsed = true) } - fun removeChat(rhId: Long?, id: String) { - chats.removeAll { it.id == id && it.remoteHostId == rhId } - } - - fun upsertGroupMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember): Boolean { - // user member was updated - if (groupInfo.membership.groupMemberId == member.groupMemberId) { - updateGroup(rhId, groupInfo) - return false - } - // update current chat - return if (chatId.value == groupInfo.id) { - val memberIndex = groupMembers.indexOfFirst { it.groupMemberId == member.groupMemberId } - if (memberIndex >= 0) { - groupMembers[memberIndex] = member - false - } else { - groupMembers.add(member) - true - } - } else { - false - } - } - - fun updateGroupMemberConnectionStats(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, connectionStats: ConnectionStats) { - val memberConn = member.activeConn - if (memberConn != null) { - val updatedConn = memberConn.copy(connectionStats = connectionStats) - val updatedMember = member.copy(activeConn = updatedConn) - upsertGroupMember(rhId, groupInfo, updatedMember) - } - } - fun setContactNetworkStatus(contact: Contact, status: NetworkStatus) { val conn = contact.activeConn if (conn != null) { @@ -682,7 +807,8 @@ data class User( override val showNtfs: Boolean, val sendRcptsContacts: Boolean, val sendRcptsSmallGroups: Boolean, - val viewPwdHash: UserPwdHash? + val viewPwdHash: UserPwdHash?, + val uiThemes: ThemeModeOverrides? = null, ): NamedChat, UserLike { override val displayName: String get() = profile.displayName override val fullName: String get() = profile.fullName @@ -709,6 +835,7 @@ data class User( sendRcptsContacts = true, sendRcptsSmallGroups = false, viewPwdHash = null, + uiThemes = null, ) } } @@ -757,6 +884,11 @@ interface NamedChat { val localAlias: String val chatViewName: String get() = localAlias.ifEmpty { displayName + (if (fullName == "" || fullName == displayName) "" else " / $fullName") } + + fun anyNameContains(searchAnyCase: String): Boolean { + val s = searchAnyCase.trim().lowercase() + return chatViewName.lowercase().contains(s) || displayName.lowercase().contains(s) || fullName.lowercase().contains(s) + } } interface SomeChat { @@ -765,6 +897,7 @@ interface SomeChat { val id: ChatId val apiId: Long val ready: Boolean + val chatDeleted: Boolean val sendMsgEnabled: Boolean val ntfsEnabled: Boolean val incognito: Boolean @@ -781,13 +914,8 @@ data class Chat( val chatItems: List, val chatStats: ChatStats = ChatStats() ) { - val userCanSend: Boolean - get() = when (chatInfo) { - is ChatInfo.Direct -> true - is ChatInfo.Group -> chatInfo.groupInfo.membership.memberRole >= GroupMemberRole.Member - is ChatInfo.Local -> true - else -> false - } + @Transient + var popTs: Instant? = null val nextSendGrpInv: Boolean get() = when (chatInfo) { @@ -845,6 +973,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = contact.id override val apiId get() = contact.apiId override val ready get() = contact.ready + override val chatDeleted get() = contact.chatDeleted override val sendMsgEnabled get() = contact.sendMsgEnabled override val ntfsEnabled get() = contact.ntfsEnabled override val incognito get() = contact.incognito @@ -869,6 +998,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = groupInfo.id override val apiId get() = groupInfo.apiId override val ready get() = groupInfo.ready + override val chatDeleted get() = groupInfo.chatDeleted override val sendMsgEnabled get() = groupInfo.sendMsgEnabled override val ntfsEnabled get() = groupInfo.ntfsEnabled override val incognito get() = groupInfo.incognito @@ -893,6 +1023,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = noteFolder.id override val apiId get() = noteFolder.apiId override val ready get() = noteFolder.ready + override val chatDeleted get() = noteFolder.chatDeleted override val sendMsgEnabled get() = noteFolder.sendMsgEnabled override val ntfsEnabled get() = noteFolder.ntfsEnabled override val incognito get() = noteFolder.incognito @@ -917,6 +1048,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = contactRequest.id override val apiId get() = contactRequest.apiId override val ready get() = contactRequest.ready + override val chatDeleted get() = contactRequest.chatDeleted override val sendMsgEnabled get() = contactRequest.sendMsgEnabled override val ntfsEnabled get() = contactRequest.ntfsEnabled override val incognito get() = contactRequest.incognito @@ -941,6 +1073,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = contactConnection.id override val apiId get() = contactConnection.apiId override val ready get() = contactConnection.ready + override val chatDeleted get() = contactConnection.chatDeleted override val sendMsgEnabled get() = contactConnection.sendMsgEnabled override val ntfsEnabled get() = false override val incognito get() = contactConnection.incognito @@ -966,6 +1099,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = "" override val apiId get() = 0L override val ready get() = false + override val chatDeleted get() = false override val sendMsgEnabled get() = false override val ntfsEnabled get() = false override val incognito get() = false @@ -999,6 +1133,15 @@ sealed class ChatInfo: SomeChat, NamedChat { is ContactConnection -> contactConnection.updatedAt is InvalidJSON -> updatedAt } + + val userCanSend: Boolean + get() = when (this) { + is ChatInfo.Direct -> true + is ChatInfo.Group -> groupInfo.membership.memberRole >= GroupMemberRole.Member + is ChatInfo.Local -> true + else -> false + } + } @Serializable @@ -1041,16 +1184,23 @@ data class Contact( override val updatedAt: Instant, val chatTs: Instant?, val contactGroupMemberId: Long? = null, - val contactGrpInvSent: Boolean + val contactGrpInvSent: Boolean, + override val chatDeleted: Boolean, + val uiThemes: ThemeModeOverrides? = null, ): SomeChat, NamedChat { override val chatType get() = ChatType.Direct override val id get() = "@$contactId" override val apiId get() = contactId override val ready get() = activeConn?.connStatus == ConnStatus.Ready + val sndReady get() = ready || activeConn?.connStatus == ConnStatus.SndReady val active get() = contactStatus == ContactStatus.Active - override val sendMsgEnabled get() = - (ready && active && !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?: false)) - || nextSendGrpInv + override val sendMsgEnabled get() = ( + sndReady + && active + && !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?: false) + && !(activeConn?.connDisabled ?: true) + ) + || nextSendGrpInv val nextSendGrpInv get() = contactGroupMemberId != null && !contactGrpInvSent override val ntfsEnabled get() = chatSettings.enableNtfs == MsgFilter.All override val incognito get() = contactConnIncognito @@ -1109,7 +1259,9 @@ data class Contact( createdAt = Clock.System.now(), updatedAt = Clock.System.now(), chatTs = Clock.System.now(), - contactGrpInvSent = false + contactGrpInvSent = false, + chatDeleted = false, + uiThemes = null, ) } } @@ -1117,7 +1269,8 @@ data class Contact( @Serializable enum class ContactStatus { @SerialName("active") Active, - @SerialName("deleted") Deleted; + @SerialName("deleted") Deleted, + @SerialName("deletedByUser") DeletedByUser; } @Serializable @@ -1150,15 +1303,23 @@ data class Connection( val pqEncryption: Boolean, val pqSndEnabled: Boolean? = null, val pqRcvEnabled: Boolean? = null, - val connectionStats: ConnectionStats? = null + val connectionStats: ConnectionStats? = null, + val authErrCounter: Int, + val quotaErrCounter: Int ) { val id: ChatId get() = ":$connId" + val connDisabled: Boolean + get() = authErrCounter >= 10 // authErrDisableCount in core + + val connInactive: Boolean + get() = quotaErrCounter >= 5 // quotaErrInactiveCount in core + val connPQEnabled: Boolean get() = pqSndEnabled == true && pqRcvEnabled == true companion object { - val sampleData = Connection(connId = 1, agentConnId = "abc", connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, peerChatVRange = VersionRange(1, 1), customUserProfileId = null, pqSupport = false, pqEncryption = false) + val sampleData = Connection(connId = 1, agentConnId = "abc", connStatus = ConnStatus.Ready, connLevel = 0, viaGroupLink = false, peerChatVRange = VersionRange(1, 1), customUserProfileId = null, pqSupport = false, pqEncryption = false, authErrCounter = 0, quotaErrCounter = 0) } } @@ -1245,12 +1406,14 @@ data class GroupInfo ( val chatSettings: ChatSettings, override val createdAt: Instant, override val updatedAt: Instant, - val chatTs: Instant? + val chatTs: Instant?, + val uiThemes: ThemeModeOverrides? = null, ): SomeChat, NamedChat { override val chatType get() = ChatType.Group override val id get() = "#$groupId" override val apiId get() = groupId override val ready get() = membership.memberActive + override val chatDeleted get() = false override val sendMsgEnabled get() = membership.memberActive override val ntfsEnabled get() = chatSettings.enableNtfs == MsgFilter.All override val incognito get() = membership.memberIncognito @@ -1287,7 +1450,8 @@ data class GroupInfo ( chatSettings = ChatSettings(enableNtfs = MsgFilter.All, sendRcpts = null, favorite = false), createdAt = Clock.System.now(), updatedAt = Clock.System.now(), - chatTs = Clock.System.now() + chatTs = Clock.System.now(), + uiThemes = null, ) } } @@ -1328,21 +1492,23 @@ data class GroupMember ( val memberContactId: Long? = null, val memberContactProfileId: Long, var activeConn: Connection? = null -) { +): NamedChat { val id: String get() = "#$groupId @$groupMemberId" - val displayName: String + override val displayName: String get() { val p = memberProfile val name = p.localAlias.ifEmpty { p.displayName } return pastMember(name) } - val fullName: String get() = memberProfile.fullName - val image: String? get() = memberProfile.image + override val fullName: String get() = memberProfile.fullName + override val image: String? get() = memberProfile.image val contactLink: String? = memberProfile.contactLink val verified get() = activeConn?.connectionCode != null val blocked get() = blockedByAdmin || !memberSettings.showMessages - val chatViewName: String + override val localAlias: String = memberProfile.localAlias + + override val chatViewName: String get() { val p = memberProfile val name = p.localAlias.ifEmpty { p.displayName + (if (p.fullName == "" || p.fullName == p.displayName) "" else " / ${p.fullName}") } @@ -1555,6 +1721,7 @@ class NoteFolder( override val chatType get() = ChatType.Local override val id get() = "*$noteFolderId" override val apiId get() = noteFolderId + override val chatDeleted get() = false override val ready get() = true override val sendMsgEnabled get() = true override val ntfsEnabled get() = false @@ -1591,6 +1758,7 @@ class UserContactRequest ( override val chatType get() = ChatType.ContactRequest override val id get() = "<@$contactRequestId" override val apiId get() = contactRequestId + override val chatDeleted get() = false override val ready get() = true override val sendMsgEnabled get() = false override val ntfsEnabled get() = false @@ -1630,6 +1798,7 @@ class PendingContactConnection( override val chatType get() = ChatType.ContactConnection override val id get () = ":$pccConnId" override val apiId get() = pccConnId + override val chatDeleted get() = false override val ready get() = false override val sendMsgEnabled get() = false override val ntfsEnabled get() = false @@ -1700,12 +1869,18 @@ enum class ConnStatus { Joined -> false Requested -> true Accepted -> true - SndReady -> false + SndReady -> null Ready -> null Deleted -> null } } +@Serializable +data class ChatItemDeletion ( + val deletedChatItem: AChatItem, + val toChatItem: AChatItem? = null +) + @Serializable class AChatItem ( val chatInfo: ChatInfo, @@ -1826,7 +2001,7 @@ data class ChatItem ( } } - fun memberToModerate(chatInfo: ChatInfo): Pair? { + fun memberToModerate(chatInfo: ChatInfo): Pair? { return if (chatInfo is ChatInfo.Group && chatDir is CIDirection.GroupRcv) { val m = chatInfo.groupInfo.membership if (m.memberRole >= GroupMemberRole.Admin && m.memberRole >= chatDir.groupMember.memberRole && meta.itemDeleted == null) { @@ -1834,11 +2009,30 @@ data class ChatItem ( } else { null } + } else if (chatInfo is ChatInfo.Group && chatDir is CIDirection.GroupSnd) { + val m = chatInfo.groupInfo.membership + if (m.memberRole >= GroupMemberRole.Admin) { + chatInfo.groupInfo to null + } else { + null + } } else { null } } + val showLocalDelete: Boolean + get() = when (content) { + is CIContent.SndDirectE2EEInfo -> false + is CIContent.RcvDirectE2EEInfo -> false + is CIContent.SndGroupE2EEInfo -> false + is CIContent.RcvGroupE2EEInfo -> false + else -> true + } + + val canBeDeletedForSelf: Boolean + get() = (content.msgContent != null && !meta.isLive) || meta.itemDeleted != null || isDeletedContent || mergeCategory != null || showLocalDelete + val showNotification: Boolean get() = when (content) { is CIContent.SndMsgContent -> false @@ -1900,18 +2094,20 @@ data class ChatItem ( ts: Instant = Clock.System.now(), text: String = "hello\nthere", status: CIStatus = CIStatus.SndNew(), + sentViaProxy: Boolean? = null, quotedItem: CIQuote? = null, file: CIFile? = null, itemForwarded: CIForwardedFrom? = null, itemDeleted: CIDeleted? = null, itemEdited: Boolean = false, itemTimed: CITimed? = null, + itemLive: Boolean = false, deletable: Boolean = true, editable: Boolean = true ) = ChatItem( chatDir = dir, - meta = CIMeta.getSample(id, ts, text, status, itemForwarded, itemDeleted, itemEdited, itemTimed, deletable, editable), + meta = CIMeta.getSample(id, ts, text, status, sentViaProxy, itemForwarded, itemDeleted, itemEdited, itemTimed, itemLive, deletable, editable), content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)), quotedItem = quotedItem, reactions = listOf(), @@ -1993,6 +2189,7 @@ data class ChatItem ( itemTs = Clock.System.now(), itemText = generalGetString(MR.strings.deleted_description), itemStatus = CIStatus.RcvRead(), + sentViaProxy = null, createdAt = Clock.System.now(), updatedAt = Clock.System.now(), itemForwarded = null, @@ -2016,6 +2213,7 @@ data class ChatItem ( itemTs = Clock.System.now(), itemText = "", itemStatus = CIStatus.RcvRead(), + sentViaProxy = null, createdAt = Clock.System.now(), updatedAt = Clock.System.now(), itemForwarded = null, @@ -2044,45 +2242,55 @@ data class ChatItem ( } } -fun MutableState>.add(index: Int, chatItem: ChatItem) { - value = SnapshotStateList().apply { addAll(value); add(index, chatItem) } +fun MutableState>.add(index: Int, elem: T) { + value = SnapshotStateList().apply { addAll(value); add(index, elem) } } -fun MutableState>.add(chatItem: ChatItem) { - value = SnapshotStateList().apply { addAll(value); add(chatItem) } +fun MutableState>.add(elem: T) { + value = SnapshotStateList().apply { addAll(value); add(elem) } } -fun MutableState>.addAll(index: Int, chatItems: List) { - value = SnapshotStateList().apply { addAll(value); addAll(index, chatItems) } +fun MutableState>.addAll(index: Int, elems: List) { + value = SnapshotStateList().apply { addAll(value); addAll(index, elems) } } -fun MutableState>.addAll(chatItems: List) { - value = SnapshotStateList().apply { addAll(value); addAll(chatItems) } +fun MutableState>.addAll(elems: List) { + value = SnapshotStateList().apply { addAll(value); addAll(elems) } } -fun MutableState>.removeAll(block: (ChatItem) -> Boolean) { - value = SnapshotStateList().apply { addAll(value); removeAll(block) } +fun MutableState>.removeAll(block: (T) -> Boolean) { + value = SnapshotStateList().apply { addAll(value); removeAll(block) } } -fun MutableState>.removeAt(index: Int) { - value = SnapshotStateList().apply { addAll(value); removeAt(index) } +fun MutableState>.removeAt(index: Int): T { + val new = SnapshotStateList() + new.addAll(value) + val res = new.removeAt(index) + value = new + return res } -fun MutableState>.removeLast() { - value = SnapshotStateList().apply { addAll(value); removeLast() } +fun MutableState>.removeLast() { + value = SnapshotStateList().apply { addAll(value); removeLast() } } -fun MutableState>.replaceAll(chatItems: List) { - value = SnapshotStateList().apply { addAll(chatItems) } +fun MutableState>.replaceAll(elems: List) { + value = SnapshotStateList().apply { addAll(elems) } } -fun MutableState>.clear() { - value = SnapshotStateList() +fun MutableState>.clear() { + value = SnapshotStateList() } -fun State>.asReversed(): MutableList = value.asReversed() +fun State>.asReversed(): MutableList = value.asReversed() -val State>.size: Int get() = value.size +fun State>.toList(): List = value.toList() + +operator fun State>.get(i: Int): T = value[i] + +operator fun State>.set(index: Int, elem: T) { value[index] = elem } + +val State>.size: Int get() = value.size enum class CIMergeCategory { MemberConnected, @@ -2118,6 +2326,7 @@ data class CIMeta ( val itemTs: Instant, val itemText: String, val itemStatus: CIStatus, + val sentViaProxy: Boolean?, val createdAt: Instant, val updatedAt: Instant, val itemForwarded: CIForwardedFrom?, @@ -2144,7 +2353,7 @@ data class CIMeta ( companion object { fun getSample( - id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew(), + id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew(), sentViaProxy: Boolean? = null, itemForwarded: CIForwardedFrom? = null, itemDeleted: CIDeleted? = null, itemEdited: Boolean = false, itemTimed: CITimed? = null, itemLive: Boolean = false, deletable: Boolean = true, editable: Boolean = true ): CIMeta = @@ -2153,6 +2362,7 @@ data class CIMeta ( itemTs = ts, itemText = text, itemStatus = status, + sentViaProxy = sentViaProxy, createdAt = ts, updatedAt = ts, itemForwarded = itemForwarded, @@ -2171,6 +2381,7 @@ data class CIMeta ( itemTs = Clock.System.now(), itemText = "invalid JSON", itemStatus = CIStatus.SndNew(), + sentViaProxy = null, createdAt = Clock.System.now(), updatedAt = Clock.System.now(), itemForwarded = null, @@ -2227,7 +2438,8 @@ sealed class CIStatus { @Serializable @SerialName("sndSent") class SndSent(val sndProgress: SndCIStatusProgress): CIStatus() @Serializable @SerialName("sndRcvd") class SndRcvd(val msgRcptStatus: MsgReceiptStatus, val sndProgress: SndCIStatusProgress): CIStatus() @Serializable @SerialName("sndErrorAuth") class SndErrorAuth: CIStatus() - @Serializable @SerialName("sndError") class SndError(val agentError: String): CIStatus() + @Serializable @SerialName("sndError") class CISSndError(val agentError: SndError): CIStatus() + @Serializable @SerialName("sndWarning") class SndWarning(val agentError: SndError): CIStatus() @Serializable @SerialName("rcvNew") class RcvNew: CIStatus() @Serializable @SerialName("rcvRead") class RcvRead: CIStatus() @Serializable @SerialName("invalid") class Invalid(val text: String): CIStatus() @@ -2251,7 +2463,8 @@ sealed class CIStatus { MsgReceiptStatus.BadMsgHash -> MR.images.ic_double_check to Color.Red } is SndErrorAuth -> MR.images.ic_close to Color.Red - is SndError -> MR.images.ic_warning_filled to WarningYellow + is CISSndError -> MR.images.ic_close to Color.Red + is SndWarning -> MR.images.ic_warning_filled to WarningOrange is RcvNew -> MR.images.ic_circle_filled to primaryColor is RcvRead -> null is CIStatus.Invalid -> MR.images.ic_question_mark to metaColor @@ -2262,13 +2475,48 @@ sealed class CIStatus { is SndSent -> null is SndRcvd -> null is SndErrorAuth -> generalGetString(MR.strings.message_delivery_error_title) to generalGetString(MR.strings.message_delivery_error_desc) - is SndError -> generalGetString(MR.strings.message_delivery_error_title) to (generalGetString(MR.strings.unknown_error) + ": $agentError") + is CISSndError -> generalGetString(MR.strings.message_delivery_error_title) to agentError.errorInfo + is SndWarning -> generalGetString(MR.strings.message_delivery_warning_title) to agentError.errorInfo is RcvNew -> null is RcvRead -> null is Invalid -> "Invalid status" to this.text } } +@Serializable +sealed class SndError { + @Serializable @SerialName("auth") class Auth: SndError() + @Serializable @SerialName("quota") class Quota: SndError() + @Serializable @SerialName("expired") class Expired: SndError() + @Serializable @SerialName("relay") class Relay(val srvError: SrvError): SndError() + @Serializable @SerialName("proxy") class Proxy(val proxyServer: String, val srvError: SrvError): SndError() + @Serializable @SerialName("proxyRelay") class ProxyRelay(val proxyServer: String, val srvError: SrvError): SndError() + @Serializable @SerialName("other") class Other(val sndError: String): SndError() + + val errorInfo: String get() = when (this) { + is SndError.Auth -> generalGetString(MR.strings.snd_error_auth) + is SndError.Quota -> generalGetString(MR.strings.snd_error_quota) + is SndError.Expired -> generalGetString(MR.strings.snd_error_expired) + is SndError.Relay -> generalGetString(MR.strings.snd_error_relay).format(srvError.errorInfo) + is SndError.Proxy -> generalGetString(MR.strings.snd_error_proxy).format(proxyServer, srvError.errorInfo) + is SndError.ProxyRelay -> generalGetString(MR.strings.snd_error_proxy_relay).format(proxyServer, srvError.errorInfo) + is SndError.Other -> generalGetString(MR.strings.ci_status_other_error).format(sndError) + } +} + +@Serializable +sealed class SrvError { + @Serializable @SerialName("host") class Host: SrvError() + @Serializable @SerialName("version") class Version: SrvError() + @Serializable @SerialName("other") class Other(val srvError: String): SrvError() + + val errorInfo: String get() = when (this) { + is SrvError.Host -> generalGetString(MR.strings.srv_error_host) + is SrvError.Version -> generalGetString(MR.strings.srv_error_version) + is SrvError.Other -> srvError + } +} + @Serializable enum class MsgReceiptStatus { @SerialName("ok") Ok, @@ -2281,6 +2529,48 @@ enum class SndCIStatusProgress { @SerialName("complete") Complete; } +@Serializable +sealed class GroupSndStatus { + @Serializable @SerialName("new") class New: GroupSndStatus() + @Serializable @SerialName("forwarded") class Forwarded: GroupSndStatus() + @Serializable @SerialName("inactive") class Inactive: GroupSndStatus() + @Serializable @SerialName("sent") class Sent: GroupSndStatus() + @Serializable @SerialName("rcvd") class Rcvd(val msgRcptStatus: MsgReceiptStatus): GroupSndStatus() + @Serializable @SerialName("error") class Error(val agentError: SndError): GroupSndStatus() + @Serializable @SerialName("warning") class Warning(val agentError: SndError): GroupSndStatus() + @Serializable @SerialName("invalid") class Invalid(val text: String): GroupSndStatus() + + fun statusIcon( + primaryColor: Color, + metaColor: Color = CurrentColors.value.colors.secondary, + paleMetaColor: Color = CurrentColors.value.colors.secondary + ): Pair = + when (this) { + is New -> MR.images.ic_more_horiz to metaColor + is Forwarded -> MR.images.ic_chevron_right_2 to metaColor + is Inactive -> MR.images.ic_person_off to metaColor + is Sent -> MR.images.ic_check_filled to metaColor + is Rcvd -> when(this.msgRcptStatus) { + MsgReceiptStatus.Ok -> MR.images.ic_double_check to metaColor + MsgReceiptStatus.BadMsgHash -> MR.images.ic_double_check to Color.Red + } + is Error -> MR.images.ic_close to Color.Red + is Warning -> MR.images.ic_warning_filled to WarningOrange + is Invalid -> MR.images.ic_question_mark to metaColor + } + + val statusInto: Pair? get() = when (this) { + is New -> null + is Forwarded -> generalGetString(MR.strings.message_forwarded_title) to generalGetString(MR.strings.message_forwarded_desc) + is Inactive -> generalGetString(MR.strings.member_inactive_title) to generalGetString(MR.strings.member_inactive_desc) + is Sent -> null + is Rcvd -> null + is Error -> generalGetString(MR.strings.message_delivery_error_title) to agentError.errorInfo + is Warning -> generalGetString(MR.strings.message_delivery_warning_title) to agentError.errorInfo + is Invalid -> "Invalid status" to this.text + } +} + @Serializable sealed class CIDeleted { @Serializable @SerialName("deleted") class Deleted(val deletedTs: Instant?): CIDeleted() @@ -2590,12 +2880,15 @@ data class CIFile( is CIFileStatus.SndComplete -> true is CIFileStatus.SndCancelled -> true is CIFileStatus.SndError -> true + is CIFileStatus.SndWarning -> true is CIFileStatus.RcvInvitation -> false is CIFileStatus.RcvAccepted -> false is CIFileStatus.RcvTransfer -> false + is CIFileStatus.RcvAborted -> false is CIFileStatus.RcvCancelled -> false is CIFileStatus.RcvComplete -> true is CIFileStatus.RcvError -> false + is CIFileStatus.RcvWarning -> false is CIFileStatus.Invalid -> false } @@ -2611,15 +2904,36 @@ data class CIFile( } is CIFileStatus.SndCancelled -> null is CIFileStatus.SndError -> null + is CIFileStatus.SndWarning -> sndCancelAction is CIFileStatus.RcvInvitation -> null is CIFileStatus.RcvAccepted -> rcvCancelAction is CIFileStatus.RcvTransfer -> rcvCancelAction + is CIFileStatus.RcvAborted -> null is CIFileStatus.RcvCancelled -> null is CIFileStatus.RcvComplete -> null is CIFileStatus.RcvError -> null + is CIFileStatus.RcvWarning -> rcvCancelAction is CIFileStatus.Invalid -> null } + val showStatusIconInSmallView: Boolean = when (fileStatus) { + is CIFileStatus.SndStored -> fileProtocol != FileProtocol.LOCAL + is CIFileStatus.SndTransfer -> true + is CIFileStatus.SndComplete -> false + is CIFileStatus.SndCancelled -> true + is CIFileStatus.SndError -> true + is CIFileStatus.SndWarning -> true + is CIFileStatus.RcvInvitation -> false + is CIFileStatus.RcvAccepted -> true + is CIFileStatus.RcvTransfer -> true + is CIFileStatus.RcvAborted -> true + is CIFileStatus.RcvCancelled -> true + is CIFileStatus.RcvComplete -> false + is CIFileStatus.RcvError -> true + is CIFileStatus.RcvWarning -> true + is CIFileStatus.Invalid -> true + } + /** * DO NOT CALL this function in compose scope, [LaunchedEffect], [DisposableEffect] and so on. Only with [withBGApi] or [runBlocking]. * Otherwise, it will be canceled when moving to another screen/item/view, etc @@ -2790,13 +3104,16 @@ sealed class CIFileStatus { @Serializable @SerialName("sndTransfer") class SndTransfer(val sndProgress: Long, val sndTotal: Long): CIFileStatus() @Serializable @SerialName("sndComplete") object SndComplete: CIFileStatus() @Serializable @SerialName("sndCancelled") object SndCancelled: CIFileStatus() - @Serializable @SerialName("sndError") object SndError: CIFileStatus() + @Serializable @SerialName("sndError") class SndError(val sndFileError: FileError): CIFileStatus() + @Serializable @SerialName("sndWarning") class SndWarning(val sndFileError: FileError): CIFileStatus() @Serializable @SerialName("rcvInvitation") object RcvInvitation: CIFileStatus() @Serializable @SerialName("rcvAccepted") object RcvAccepted: CIFileStatus() @Serializable @SerialName("rcvTransfer") class RcvTransfer(val rcvProgress: Long, val rcvTotal: Long): CIFileStatus() + @Serializable @SerialName("rcvAborted") object RcvAborted: CIFileStatus() @Serializable @SerialName("rcvComplete") object RcvComplete: CIFileStatus() @Serializable @SerialName("rcvCancelled") object RcvCancelled: CIFileStatus() - @Serializable @SerialName("rcvError") object RcvError: CIFileStatus() + @Serializable @SerialName("rcvError") class RcvError(val rcvFileError: FileError): CIFileStatus() + @Serializable @SerialName("rcvWarning") class RcvWarning(val rcvFileError: FileError): CIFileStatus() @Serializable @SerialName("invalid") class Invalid(val text: String): CIFileStatus() val sent: Boolean get() = when (this) { @@ -2805,16 +3122,34 @@ sealed class CIFileStatus { is SndComplete -> true is SndCancelled -> true is SndError -> true + is SndWarning -> true is RcvInvitation -> false is RcvAccepted -> false is RcvTransfer -> false + is RcvAborted -> false is RcvComplete -> false is RcvCancelled -> false is RcvError -> false + is RcvWarning -> false is Invalid -> false } } +@Serializable +sealed class FileError { + @Serializable @SerialName("auth") class Auth: FileError() + @Serializable @SerialName("noFile") class NoFile: FileError() + @Serializable @SerialName("relay") class Relay(val srvError: SrvError): FileError() + @Serializable @SerialName("other") class Other(val fileError: String): FileError() + + val errorInfo: String get() = when (this) { + is FileError.Auth -> generalGetString(MR.strings.file_error_auth) + is FileError.NoFile -> generalGetString(MR.strings.file_error_no_file) + is FileError.Relay -> generalGetString(MR.strings.file_error_relay).format(srvError.errorInfo) + is FileError.Other -> generalGetString(MR.strings.ci_status_other_error).format(fileError) + } +} + @Suppress("SERIALIZER_TYPE_INCOMPATIBLE") @Serializable(with = MsgContentSerializer::class) sealed class MsgContent { @@ -2828,6 +3163,20 @@ sealed class MsgContent { @Serializable(with = MsgContentSerializer::class) class MCFile(override val text: String): MsgContent() @Serializable(with = MsgContentSerializer::class) class MCUnknown(val type: String? = null, override val text: String, val json: JsonElement): MsgContent() + val isVoice: Boolean get() = + when (this) { + is MCVoice -> true + else -> false + } + + val isMediaOrFileAttachment: Boolean get() = + when (this) { + is MCImage -> true + is MCVideo -> true + is MCFile -> true + else -> false + } + val cmdString: String get() = if (this is MCUnknown) "json $json" else "json ${json.encodeToString(this)}" } @@ -3354,7 +3703,8 @@ data class ChatItemVersion( @Serializable data class MemberDeliveryStatus( val groupMemberId: Long, - val memberDeliveryStatus: CIStatus + val memberDeliveryStatus: GroupSndStatus, + val sentViaProxy: Boolean? ) enum class NotificationPreviewMode { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index b71610597e..fe568b5144 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -1,13 +1,22 @@ package chat.simplex.common.model +import SectionItemView +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text import chat.simplex.common.views.helpers.* import androidx.compose.runtime.* +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.style.TextAlign import chat.simplex.common.model.ChatController.getNetCfg import chat.simplex.common.model.ChatController.setNetCfg -import chat.simplex.common.model.ChatModel.updatingChatsMutex import chat.simplex.common.model.ChatModel.changingActiveUserMutex +import chat.simplex.common.model.ChatModel.withChats import dev.icerock.moko.resources.compose.painterResource import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* @@ -19,14 +28,14 @@ import com.charleskorn.kaml.Yaml import com.charleskorn.kaml.YamlConfiguration import chat.simplex.res.MR import com.russhwolf.settings.Settings +import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.sync.withLock import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlinx.serialization.* -import kotlinx.serialization.builtins.MapSerializer -import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.builtins.* import kotlinx.serialization.json.* import java.util.Date @@ -106,6 +115,8 @@ class AppPreferences { val privacySaveLastDraft = mkBoolPreference(SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT, true) val privacyDeliveryReceiptsSet = mkBoolPreference(SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET, false) val privacyEncryptLocalFiles = mkBoolPreference(SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES, true) + val privacyAskToApproveRelays = mkBoolPreference(SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS, true) + val privacyMediaBlurRadius = mkIntPreference(SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS, 0) val experimentalCalls = mkBoolPreference(SHARED_PREFS_EXPERIMENTAL_CALLS, false) val showUnreadAndFavorites = mkBoolPreference(SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES, false) val chatArchiveName = mkStrPreference(SHARED_PREFS_CHAT_ARCHIVE_NAME, null) @@ -117,6 +128,7 @@ class AppPreferences { val showSlowApiCalls = mkBoolPreference(SHARED_PREFS_SHOW_SLOW_API_CALLS, false) val terminalAlwaysVisible = mkBoolPreference(SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE, false) val networkUseSocksProxy = mkBoolPreference(SHARED_PREFS_NETWORK_USE_SOCKS_PROXY, false) + val networkShowSubscriptionPercentage = mkBoolPreference(SHARED_PREFS_NETWORK_SHOW_SUBSCRIPTION_PERCENTAGE, false) val networkProxyHostPort = mkStrPreference(SHARED_PREFS_NETWORK_PROXY_HOST_PORT, "localhost:9050") private val _networkSessionMode = mkStrPreference(SHARED_PREFS_NETWORK_SESSION_MODE, TransportSessionMode.default.name) val networkSessionMode: SharedPreference = SharedPreference( @@ -130,6 +142,8 @@ class AppPreferences { }, set = fun(mode: TransportSessionMode) { _networkSessionMode.set(mode.name) } ) + val networkSMPProxyMode = mkStrPreference(SHARED_PREFS_NETWORK_SMP_PROXY_MODE, NetCfg.defaults.smpProxyMode.name) + val networkSMPProxyFallback = mkStrPreference(SHARED_PREFS_NETWORK_SMP_PROXY_FALLBACK, NetCfg.defaults.smpProxyFallback.name) val networkHostMode = mkStrPreference(SHARED_PREFS_NETWORK_HOST_MODE, HostMode.OnionViaSocks.name) val networkRequiredHostMode = mkBoolPreference(SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE, false) val networkTCPConnectTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT, NetCfg.defaults.tcpConnectTimeout, NetCfg.proxyDefaults.tcpConnectTimeout) @@ -145,8 +159,12 @@ class AppPreferences { val incognito = mkBoolPreference(SHARED_PREFS_INCOGNITO, false) val liveMessageAlertShown = mkBoolPreference(SHARED_PREFS_LIVE_MESSAGE_ALERT_SHOWN, false) val showHiddenProfilesNotice = mkBoolPreference(SHARED_PREFS_SHOW_HIDDEN_PROFILES_NOTICE, true) + val oneHandUICardShown = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN, false) val showMuteProfileAlert = mkBoolPreference(SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT, true) val appLanguage = mkStrPreference(SHARED_PREFS_APP_LANGUAGE, null) + val appUpdateChannel = mkEnumPreference(SHARED_PREFS_APP_UPDATE_CHANNEL, AppUpdatesChannel.DISABLED) { AppUpdatesChannel.entries.firstOrNull { it.name == this } } + val appSkippedUpdate = mkStrPreference(SHARED_PREFS_APP_SKIPPED_UPDATE, "") + val appUpdateNoticeShown = mkBoolPreference(SHARED_PREFS_APP_UPDATE_NOTICE_SHOWN, false) val onboardingStage = mkEnumPreference(SHARED_PREFS_ONBOARDING_STAGE, OnboardingStage.OnboardingComplete) { OnboardingStage.values().firstOrNull { it.name == this } } val migrationToStage = mkStrPreference(SHARED_PREFS_MIGRATION_TO_STAGE, null) @@ -164,14 +182,29 @@ class AppPreferences { val selfDestruct = mkBoolPreference(SHARED_PREFS_SELF_DESTRUCT, false) val selfDestructDisplayName = mkStrPreference(SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME, null) - val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM.name) - val systemDarkTheme = mkStrPreference(SHARED_PREFS_SYSTEM_DARK_THEME, DefaultTheme.SIMPLEX.name) - val themeOverrides = mkMapPreference(SHARED_PREFS_THEMES, mapOf(), encode = { + // This flag is set when database is first initialized and resets only when the database is removed. + // This is needed for recover from incomplete initialization when only one database file is created. + // If false - the app will clear database folder on missing file and re-initialize. + // Note that this situation can only happen if passphrase for the first database is incorrect because, otherwise, backend will re-create second database automatically + val newDatabaseInitialized = mkBoolPreference(SHARED_PREFS_NEW_DATABASE_INITIALIZED, false) + + val currentTheme = mkStrPreference(SHARED_PREFS_CURRENT_THEME, DefaultTheme.SYSTEM_THEME_NAME) + val systemDarkTheme = mkStrPreference(SHARED_PREFS_SYSTEM_DARK_THEME, DefaultTheme.SIMPLEX.themeName) + val currentThemeIds = mkMapPreference(SHARED_PREFS_CURRENT_THEME_IDs, mapOf(), encode = { + json.encodeToString(MapSerializer(String.serializer(), String.serializer()), it) + }, decode = { + json.decodeFromString(MapSerializer(String.serializer(), String.serializer()), it) + }) + // Deprecated. Remove key from preferences in 2025 + val themeOverridesOld = mkMapPreference(SHARED_PREFS_THEMES_OLD, mapOf(), encode = { json.encodeToString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it) }, decode = { - json.decodeFromString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it) + jsonCoerceInputValues.decodeFromString(MapSerializer(String.serializer(), ThemeOverrides.serializer()), it) }, settingsThemes) + val themeOverrides = mkThemeOverridesPreference() val profileImageCornerRadius = mkFloatPreference(SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS, 22.5f) + val fontScale = mkFloatPreference(SHARED_PREFS_FONT_SCALE, 1f) + val densityScale = mkFloatPreference(SHARED_PREFS_DENSITY_SCALE, 1f) val whatsNewVersion = mkStrPreference(SHARED_PREFS_WHATS_NEW_VERSION, null) val lastMigratedVersionCode = mkIntPreference(SHARED_PREFS_LAST_MIGRATED_VERSION_CODE, 0) @@ -185,11 +218,26 @@ class AppPreferences { val desktopWindowState = mkStrPreference(SHARED_PREFS_DESKTOP_WINDOW_STATE, null) + val showDeleteConversationNotice = mkBoolPreference(SHARED_PREFS_SHOW_DELETE_CONVERSATION_NOTICE, true) + val showDeleteContactNotice = mkBoolPreference(SHARED_PREFS_SHOW_DELETE_CONTACT_NOTICE, true) + val showSentViaProxy = mkBoolPreference(SHARED_PREFS_SHOW_SENT_VIA_RPOXY, false) + val iosCallKitEnabled = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_ENABLED, true) val iosCallKitCallsInRecents = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS, false) - + val oneHandUI = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI, appPlatform.isAndroid) + + val hintPreferences: List, Boolean>> = listOf( + laNoticeShown to false, + oneHandUICardShown to false, + liveMessageAlertShown to false, + showHiddenProfilesNotice to true, + showMuteProfileAlert to true, + showDeleteConversationNotice to true, + showDeleteContactNotice to true, + ) + private fun mkIntPreference(prefName: String, default: Int) = SharedPreference( get = fun() = settings.getInt(prefName, default), @@ -263,6 +311,12 @@ class AppPreferences { set = fun(value) = prefs.putString(prefName, encode(value)) ) + private fun mkThemeOverridesPreference(): SharedPreference> = + SharedPreference( + get = fun() = themeOverridesStore ?: (readThemeOverrides()).also { themeOverridesStore = it }, + set = fun(value) { if (writeThemeOverrides(value)) { themeOverridesStore = value } } + ) + companion object { const val SHARED_PREFS_ID = "chat.simplex.app.SIMPLEX_APP_PREFS" internal const val SHARED_PREFS_THEMES_ID = "chat.simplex.app.THEMES" @@ -288,12 +342,17 @@ class AppPreferences { private const val SHARED_PREFS_PRIVACY_SAVE_LAST_DRAFT = "PrivacySaveLastDraft" private const val SHARED_PREFS_PRIVACY_DELIVERY_RECEIPTS_SET = "PrivacyDeliveryReceiptsSet" private const val SHARED_PREFS_PRIVACY_ENCRYPT_LOCAL_FILES = "PrivacyEncryptLocalFiles" + private const val SHARED_PREFS_PRIVACY_ASK_TO_APPROVE_RELAYS = "PrivacyAskToApproveRelays" + private const val SHARED_PREFS_PRIVACY_MEDIA_BLUR_RADIUS = "PrivacyMediaBlurRadius" const val SHARED_PREFS_PRIVACY_FULL_BACKUP = "FullBackup" private const val SHARED_PREFS_EXPERIMENTAL_CALLS = "ExperimentalCalls" private const val SHARED_PREFS_SHOW_UNREAD_AND_FAVORITES = "ShowUnreadAndFavorites" private const val SHARED_PREFS_CHAT_ARCHIVE_NAME = "ChatArchiveName" private const val SHARED_PREFS_CHAT_ARCHIVE_TIME = "ChatArchiveTime" private const val SHARED_PREFS_APP_LANGUAGE = "AppLanguage" + private const val SHARED_PREFS_APP_UPDATE_CHANNEL = "AppUpdateChannel" + private const val SHARED_PREFS_APP_SKIPPED_UPDATE = "AppSkippedUpdate" + private const val SHARED_PREFS_APP_UPDATE_NOTICE_SHOWN = "AppUpdateNoticeShown" private const val SHARED_PREFS_ONBOARDING_STAGE = "OnboardingStage" const val SHARED_PREFS_MIGRATION_TO_STAGE = "MigrationToStage" const val SHARED_PREFS_MIGRATION_FROM_STAGE = "MigrationFromStage" @@ -304,8 +363,11 @@ class AppPreferences { private const val SHARED_PREFS_SHOW_SLOW_API_CALLS = "ShowSlowApiCalls" private const val SHARED_PREFS_TERMINAL_ALWAYS_VISIBLE = "TerminalAlwaysVisible" private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy" + private const val SHARED_PREFS_NETWORK_SHOW_SUBSCRIPTION_PERCENTAGE = "ShowSubscriptionPercentage" private const val SHARED_PREFS_NETWORK_PROXY_HOST_PORT = "NetworkProxyHostPort" private const val SHARED_PREFS_NETWORK_SESSION_MODE = "NetworkSessionMode" + private const val SHARED_PREFS_NETWORK_SMP_PROXY_MODE = "NetworkSMPProxyMode" + private const val SHARED_PREFS_NETWORK_SMP_PROXY_FALLBACK = "NetworkSMPProxyFallback" private const val SHARED_PREFS_NETWORK_HOST_MODE = "NetworkHostMode" private const val SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE = "NetworkRequiredHostMode" private const val SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT = "NetworkTCPConnectTimeout" @@ -321,6 +383,7 @@ class AppPreferences { private const val SHARED_PREFS_INCOGNITO = "Incognito" private const val SHARED_PREFS_LIVE_MESSAGE_ALERT_SHOWN = "LiveMessageAlertShown" private const val SHARED_PREFS_SHOW_HIDDEN_PROFILES_NOTICE = "ShowHiddenProfilesNotice" + private const val SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN = "OneHandUICardShown" private const val SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT = "ShowMuteProfileAlert" private const val SHARED_PREFS_STORE_DB_PASSPHRASE = "StoreDBPassphrase" private const val SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE = "InitialRandomDBPassphrase" @@ -331,14 +394,20 @@ class AppPreferences { private const val SHARED_PREFS_ENCRYPTED_SELF_DESTRUCT_PASSPHRASE = "EncryptedSelfDestructPassphrase" private const val SHARED_PREFS_INITIALIZATION_VECTOR_SELF_DESTRUCT_PASSPHRASE = "InitializationVectorSelfDestructPassphrase" private const val SHARED_PREFS_ENCRYPTION_STARTED_AT = "EncryptionStartedAt" + private const val SHARED_PREFS_NEW_DATABASE_INITIALIZED = "NewDatabaseInitialized" private const val SHARED_PREFS_CONFIRM_DB_UPGRADES = "ConfirmDBUpgrades" + private const val SHARED_PREFS_ONE_HAND_UI = "OneHandUI" private const val SHARED_PREFS_SELF_DESTRUCT = "LocalAuthenticationSelfDestruct" private const val SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME = "LocalAuthenticationSelfDestructDisplayName" private const val SHARED_PREFS_PQ_EXPERIMENTAL_ENABLED = "PQExperimentalEnabled" // no longer used private const val SHARED_PREFS_CURRENT_THEME = "CurrentTheme" + private const val SHARED_PREFS_CURRENT_THEME_IDs = "CurrentThemeIds" private const val SHARED_PREFS_SYSTEM_DARK_THEME = "SystemDarkTheme" - private const val SHARED_PREFS_THEMES = "Themes" + private const val SHARED_PREFS_THEMES_OLD = "Themes" + private const val SHARED_PREFS_THEME_OVERRIDES = "ThemeOverrides" private const val SHARED_PREFS_PROFILE_IMAGE_CORNER_RADIUS = "ProfileImageCornerRadius" + private const val SHARED_PREFS_FONT_SCALE = "FontScale" + private const val SHARED_PREFS_DENSITY_SCALE = "DensityScale" private const val SHARED_PREFS_WHATS_NEW_VERSION = "WhatsNewVersion" private const val SHARED_PREFS_LAST_MIGRATED_VERSION_CODE = "LastMigratedVersionCode" private const val SHARED_PREFS_CUSTOM_DISAPPEARING_MESSAGE_TIME = "CustomDisappearingMessageTime" @@ -348,9 +417,14 @@ class AppPreferences { private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "ConnectRemoteViaMulticastAuto" private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast" private const val SHARED_PREFS_DESKTOP_WINDOW_STATE = "DesktopWindowState" + private const val SHARED_PREFS_SHOW_DELETE_CONVERSATION_NOTICE = "showDeleteConversationNotice" + private const val SHARED_PREFS_SHOW_DELETE_CONTACT_NOTICE = "showDeleteContactNotice" + private const val SHARED_PREFS_SHOW_SENT_VIA_RPOXY = "showSentViaProxy" private const val SHARED_PREFS_IOS_CALL_KIT_ENABLED = "iOSCallKitEnabled" private const val SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS = "iOSCallKitCallsInRecents" + + private var themeOverridesStore: List? = null } } @@ -369,6 +443,28 @@ object ChatController { fun hasChatCtrl() = ctrl != -1L && ctrl != null + suspend fun getAgentSubsTotal(rh: Long?): Pair? { + val userId = currentUserId("getAgentSubsTotal") + + val r = sendCmd(rh, CC.GetAgentSubsTotal(userId), log = false) + + if (r is CR.AgentSubsTotal) return r.subsTotal to r.hasSession + Log.e(TAG, "getAgentSubsTotal bad response: ${r.responseType} ${r.details}") + return null + } + + suspend fun getAgentServersSummary(rh: Long?): PresentedServersSummary? { + val userId = currentUserId("getAgentServersSummary") + + val r = sendCmd(rh, CC.GetAgentServersSummary(userId), log = false) + + if (r is CR.AgentServersSummary) return r.serversSummary + Log.e(TAG, "getAgentServersSummary bad response: ${r.responseType} ${r.details}") + return null + } + + suspend fun resetAgentServersStats(rh: Long?): Boolean = sendCommandOkResp(rh, CC.ResetAgentServersStats()) + private suspend fun currentUserId(funcName: String): Long = changingActiveUserMutex.withLock { val userId = chatModel.currentUser.value?.userId if (userId == null) { @@ -383,12 +479,11 @@ object ChatController { Log.d(TAG, "user: $user") try { apiSetNetworkConfig(getNetCfg()) - val justStarted = apiStartChat() - appPrefs.chatStopped.set(false) + val chatRunning = apiCheckChatRunning() val users = listUsers(null) chatModel.users.clear() chatModel.users.addAll(users) - if (justStarted) { + if (!chatRunning) { chatModel.currentUser.value = user chatModel.localUserCreated.value = true getUserChatData(null) @@ -401,12 +496,14 @@ object ChatController { } Log.d(TAG, "startChat: started") } else { - updatingChatsMutex.withLock { + withChats { val chats = apiGetChats(null) - chatModel.updateChats(chats) + updateChats(chats) } Log.d(TAG, "startChat: running") } + apiStartChat() + appPrefs.chatStopped.set(false) } catch (e: Throwable) { Log.e(TAG, "failed starting chat $e") throw e @@ -434,9 +531,17 @@ object ChatController { suspend fun startChatWithTemporaryDatabase(ctrl: ChatCtrl, netCfg: NetCfg): User? { Log.d(TAG, "startChatWithTemporaryDatabase") val migrationActiveUser = apiGetActiveUser(null, ctrl) ?: apiCreateActiveUser(null, Profile(displayName = "Temp", fullName = ""), ctrl = ctrl) - apiSetNetworkConfig(netCfg, ctrl) - apiSetTempFolder(getMigrationTempFilesDirectory().absolutePath, ctrl) - apiSetFilesFolder(getMigrationTempFilesDirectory().absolutePath, ctrl) + if (!apiSetNetworkConfig(netCfg, ctrl)) { + Log.e(TAG, "Error setting network config, stopping migration") + return null + } + apiSetAppFilePaths( + getMigrationTempFilesDirectory().absolutePath, + getMigrationTempFilesDirectory().absolutePath, + wallpapersDir.parentFile.absolutePath, + remoteHostsDir.absolutePath, + ctrl + ) apiStartChat(ctrl) return migrationActiveUser } @@ -451,11 +556,15 @@ object ChatController { } suspend fun changeActiveUser_(rhId: Long?, toUserId: Long?, viewPwd: String?) { + val prevActiveUser = chatModel.currentUser.value val currentUser = changingActiveUserMutex.withLock { (if (toUserId != null) apiSetActiveUser(rhId, toUserId, viewPwd) else apiGetActiveUser(rhId)).also { chatModel.currentUser.value = it } } + if (prevActiveUser?.hidden == true) { + ntfManager.cancelNotificationsForUser(prevActiveUser.userId) + } val users = listUsers(rhId) chatModel.users.clear() chatModel.users.addAll(users) @@ -470,9 +579,9 @@ object ChatController { val hasUser = chatModel.currentUser.value != null chatModel.userAddress.value = if (hasUser) apiGetUserAddress(rhId) else null chatModel.chatItemTTL.value = if (hasUser) getChatItemTTL(rhId) else ChatItemTTL.None - updatingChatsMutex.withLock { + withChats { val chats = apiGetChats(rhId) - chatModel.updateChats(chats) + updateChats(chats) } } @@ -518,20 +627,24 @@ object ChatController { } } - suspend fun sendCmd(rhId: Long?, cmd: CC, otherCtrl: ChatCtrl? = null): CR { + suspend fun sendCmd(rhId: Long?, cmd: CC, otherCtrl: ChatCtrl? = null, log: Boolean = true): CR { val ctrl = otherCtrl ?: ctrl ?: throw Exception("Controller is not initialized") return withContext(Dispatchers.IO) { val c = cmd.cmdString - chatModel.addTerminalItem(TerminalItem.cmd(rhId, cmd.obfuscated)) - Log.d(TAG, "sendCmd: ${cmd.cmdType}") + if (log) { + chatModel.addTerminalItem(TerminalItem.cmd(rhId, cmd.obfuscated)) + Log.d(TAG, "sendCmd: ${cmd.cmdType}") + } val json = if (rhId == null) chatSendCmd(ctrl, c) else chatSendRemoteCmd(ctrl, rhId.toInt(), c) val r = APIResponse.decodeStr(json) - Log.d(TAG, "sendCmd response type ${r.resp.responseType}") - if (r.resp is CR.Response || r.resp is CR.Invalid) { - Log.d(TAG, "sendCmd response json $json") + if (log) { + Log.d(TAG, "sendCmd response type ${r.resp.responseType}") + if (r.resp is CR.Response || r.resp is CR.Invalid) { + Log.d(TAG, "sendCmd response json $json") + } + chatModel.addTerminalItem(TerminalItem.resp(rhId, r.resp)) } - chatModel.addTerminalItem(TerminalItem.resp(rhId, r.resp)) r.resp } } @@ -559,8 +672,8 @@ object ChatController { return null } - suspend fun apiCreateActiveUser(rh: Long?, p: Profile?, sameServers: Boolean = false, pastTimestamp: Boolean = false, ctrl: ChatCtrl? = null): User? { - val r = sendCmd(rh, CC.CreateActiveUser(p, sameServers = sameServers, pastTimestamp = pastTimestamp), ctrl) + suspend fun apiCreateActiveUser(rh: Long?, p: Profile?, pastTimestamp: Boolean = false, ctrl: ChatCtrl? = null): User? { + val r = sendCmd(rh, CC.CreateActiveUser(p, pastTimestamp = pastTimestamp), ctrl) if (r is CR.ActiveUser) return r.user.updateRemoteHostId(rh) else if ( r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.DuplicateName || @@ -647,6 +760,15 @@ object ChatController { } } + private suspend fun apiCheckChatRunning(): Boolean { + val r = sendCmd(null, CC.CheckChatRunning()) + when (r) { + is CR.ChatRunning -> return true + is CR.ChatStopped -> return false + else -> throw Exception("failed check chat running: ${r.responseType} ${r.details}") + } + } + suspend fun apiStopChat(): Boolean { val r = sendCmd(null, CC.ApiStopChat()) when (r) { @@ -655,22 +777,10 @@ object ChatController { } } - suspend fun apiSetTempFolder(tempFolder: String, ctrl: ChatCtrl? = null) { - val r = sendCmd(null, CC.SetTempFolder(tempFolder), ctrl) + suspend fun apiSetAppFilePaths(filesFolder: String, tempFolder: String, assetsFolder: String, remoteHostsFolder: String, ctrl: ChatCtrl? = null) { + val r = sendCmd(null, CC.ApiSetAppFilePaths(filesFolder, tempFolder, assetsFolder, remoteHostsFolder), ctrl) if (r is CR.CmdOk) return - throw Exception("failed to set temp folder: ${r.responseType} ${r.details}") - } - - suspend fun apiSetFilesFolder(filesFolder: String, ctrl: ChatCtrl? = null) { - val r = sendCmd(null, CC.SetFilesFolder(filesFolder), ctrl) - if (r is CR.CmdOk) return - throw Exception("failed to set files folder: ${r.responseType} ${r.details}") - } - - suspend fun apiSetRemoteHostsFolder(remoteHostsFolder: String) { - val r = sendCmd(null, CC.SetRemoteHostsFolder(remoteHostsFolder)) - if (r is CR.CmdOk) return - throw Exception("failed to set remote hosts folder: ${r.responseType} ${r.details}") + throw Exception("failed to set app file paths: ${r.responseType} ${r.details}") } suspend fun apiSetEncryptLocalFiles(enable: Boolean) = sendCommandOkResp(null, CC.ApiSetEncryptLocalFiles(enable)) @@ -687,9 +797,9 @@ object ChatController { throw Exception("failed to get app settings: ${r.responseType} ${r.details}") } - suspend fun apiExportArchive(config: ArchiveConfig) { + suspend fun apiExportArchive(config: ArchiveConfig): List { val r = sendCmd(null, CC.ApiExportArchive(config)) - if (r is CR.CmdOk) return + if (r is CR.ArchiveExported) return r.archiveErrors throw Exception("failed to export archive: ${r.responseType} ${r.details}") } @@ -775,8 +885,8 @@ object ChatController { } } - suspend fun apiForwardChatItem(rh: Long?, toChatType: ChatType, toChatId: Long, fromChatType: ChatType, fromChatId: Long, itemId: Long): ChatItem? { - val cmd = CC.ApiForwardChatItem(toChatType, toChatId, fromChatType, fromChatId, itemId) + suspend fun apiForwardChatItem(rh: Long?, toChatType: ChatType, toChatId: Long, fromChatType: ChatType, fromChatId: Long, itemId: Long, ttl: Int?): ChatItem? { + val cmd = CC.ApiForwardChatItem(toChatType, toChatId, fromChatType, fromChatId, itemId, ttl) return processSendMessageCmd(rh, cmd)?.chatItem } @@ -796,16 +906,16 @@ object ChatController { return null } - suspend fun apiDeleteChatItem(rh: Long?, type: ChatType, id: Long, itemId: Long, mode: CIDeleteMode): CR.ChatItemDeleted? { - val r = sendCmd(rh, CC.ApiDeleteChatItem(type, id, itemId, mode)) - if (r is CR.ChatItemDeleted) return r + suspend fun apiDeleteChatItems(rh: Long?, type: ChatType, id: Long, itemIds: List, mode: CIDeleteMode): List? { + val r = sendCmd(rh, CC.ApiDeleteChatItem(type, id, itemIds, mode)) + if (r is CR.ChatItemsDeleted) return r.chatItemDeletions Log.e(TAG, "apiDeleteChatItem bad response: ${r.responseType} ${r.details}") return null } - suspend fun apiDeleteMemberChatItem(rh: Long?, groupId: Long, groupMemberId: Long, itemId: Long): Pair? { - val r = sendCmd(rh, CC.ApiDeleteMemberChatItem(groupId, groupMemberId, itemId)) - if (r is CR.ChatItemDeleted) return r.deletedChatItem.chatItem to r.toChatItem?.chatItem + suspend fun apiDeleteMemberChatItems(rh: Long?, groupId: Long, itemIds: List): List? { + val r = sendCmd(rh, CC.ApiDeleteMemberChatItem(groupId, itemIds)) + if (r is CR.ChatItemsDeleted) return r.chatItemDeletions Log.e(TAG, "apiDeleteMemberChatItem bad response: ${r.responseType} ${r.details}") return null } @@ -881,6 +991,14 @@ object ChatController { } } + suspend fun reconnectServer(rh: Long?, server: String): Boolean { + val userId = currentUserId("reconnectServer") + + return sendCommandOkResp(rh, CC.ReconnectServer(userId, server)) + } + + suspend fun reconnectAllServers(rh: Long?): Boolean = sendCommandOkResp(rh, CC.ReconnectAllServers()) + suspend fun apiSetSettings(rh: Long?, type: ChatType, id: Long, settings: ChatSettings): Boolean { val r = sendCmd(rh, CC.APISetChatSettings(type, id, settings)) return when (r) { @@ -912,6 +1030,20 @@ object ChatController { return null } + suspend fun apiContactQueueInfo(rh: Long?, contactId: Long): Pair? { + val r = sendCmd(rh, CC.APIContactQueueInfo(contactId)) + if (r is CR.QueueInfoR) return Pair(r.rcvMsgInfo, r.queueInfo) + apiErrorAlert("apiContactQueueInfo", generalGetString(MR.strings.error), r) + return null + } + + suspend fun apiGroupMemberQueueInfo(rh: Long?, groupId: Long, groupMemberId: Long): Pair? { + val r = sendCmd(rh, CC.APIGroupMemberQueueInfo(groupId, groupMemberId)) + if (r is CR.QueueInfoR) return Pair(r.rcvMsgInfo, r.queueInfo) + apiErrorAlert("apiGroupMemberQueueInfo", generalGetString(MR.strings.error), r) + return null + } + suspend fun apiSwitchContact(rh: Long?, contactId: Long): ConnectionStats? { val r = sendCmd(rh, CC.APISwitchContact(contactId)) if (r is CR.ContactSwitchStarted) return r.connectionStats @@ -1066,16 +1198,18 @@ object ChatController { } } - suspend fun deleteChat(chat: Chat, notify: Boolean? = null) { + suspend fun deleteChat(chat: Chat, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)) { val cInfo = chat.chatInfo - if (apiDeleteChat(rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, notify = notify)) { - chatModel.removeChat(chat.remoteHostId, cInfo.id) + if (apiDeleteChat(rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, chatDeleteMode = chatDeleteMode)) { + withChats { + removeChat(chat.remoteHostId, cInfo.id) + } } } - suspend fun apiDeleteChat(rh: Long?, type: ChatType, id: Long, notify: Boolean? = null): Boolean { + suspend fun apiDeleteChat(rh: Long?, type: ChatType, id: Long, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)): Boolean { chatModel.deletedChats.value += rh to type.type + id - val r = sendCmd(rh, CC.ApiDeleteChat(type, id, notify)) + val r = sendCmd(rh, CC.ApiDeleteChat(type, id, chatDeleteMode)) val success = when { r is CR.ContactDeleted && type == ChatType.Direct -> true r is CR.ContactConnectionDeleted && type == ChatType.ContactConnection -> true @@ -1096,11 +1230,29 @@ object ChatController { return success } + suspend fun apiDeleteContact(rh: Long?, id: Long, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)): Contact? { + val type = ChatType.Direct + chatModel.deletedChats.value += rh to type.type + id + val r = sendCmd(rh, CC.ApiDeleteChat(type, id, chatDeleteMode)) + val contact = when { + r is CR.ContactDeleted -> r.contact + else -> { + val titleId = MR.strings.error_deleting_contact + apiErrorAlert("apiDeleteChat", generalGetString(titleId), r) + null + } + } + chatModel.deletedChats.value -= rh to type.type + id + return contact + } + fun clearChat(chat: Chat, close: (() -> Unit)? = null) { withBGApi { val updatedChatInfo = apiClearChat(chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId) if (updatedChatInfo != null) { - chatModel.clearChat(chat.remoteHostId, updatedChatInfo) + withChats { + clearChat(chat.remoteHostId, updatedChatInfo) + } ntfManager.cancelNotificationsForChat(chat.chatInfo.id) close?.invoke() } @@ -1156,6 +1308,20 @@ object ChatController { return null } + suspend fun apiSetUserUIThemes(rh: Long?, userId: Long, themes: ThemeModeOverrides?): Boolean { + val r = sendCmd(rh, CC.ApiSetUserUIThemes(userId, themes)) + if (r is CR.CmdOk) return true + Log.e(TAG, "apiSetUserUIThemes bad response: ${r.responseType} ${r.details}") + return false + } + + suspend fun apiSetChatUIThemes(rh: Long?, chatId: ChatId, themes: ThemeModeOverrides?): Boolean { + val r = sendCmd(rh, CC.ApiSetChatUIThemes(chatId, themes)) + if (r is CR.CmdOk) return true + Log.e(TAG, "apiSetChatUIThemes bad response: ${r.responseType} ${r.details}") + return false + } + suspend fun apiCreateUserAddress(rh: Long?): String? { val userId = kotlin.runCatching { currentUserId("apiCreateUserAddress") }.getOrElse { return null } val r = sendCmd(rh, CC.ApiCreateMyAddress(userId)) @@ -1330,9 +1496,9 @@ object ChatController { } } - suspend fun apiReceiveFile(rh: Long?, fileId: Long, encrypted: Boolean, inline: Boolean? = null, auto: Boolean = false): AChatItem? { + suspend fun apiReceiveFile(rh: Long?, fileId: Long, userApprovedRelays: Boolean, encrypted: Boolean, inline: Boolean? = null, auto: Boolean = false): AChatItem? { // -1 here is to override default behavior of providing current remote host id because file can be asked by local device while remote is connected - val r = sendCmd(rh, CC.ReceiveFile(fileId, encrypted, inline)) + val r = sendCmd(rh, CC.ReceiveFile(fileId, userApprovedRelays = userApprovedRelays, encrypt = encrypted, inline = inline)) return when (r) { is CR.RcvFileAccepted -> r.chatItem is CR.RcvFileAcceptedSndCancelled -> { @@ -1351,7 +1517,23 @@ object ChatController { val maybeChatError = chatError(r) if (maybeChatError is ChatErrorType.FileCancelled || maybeChatError is ChatErrorType.FileAlreadyReceiving) { Log.d(TAG, "apiReceiveFile ignoring FileCancelled or FileAlreadyReceiving error") - } else { + } else if (maybeChatError is ChatErrorType.FileNotApproved) { + Log.d(TAG, "apiReceiveFile FileNotApproved error") + if (!auto) { + val srvs = maybeChatError.unknownServers.map{ serverHostname(it) } + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.file_not_approved_title), + text = generalGetString(MR.strings.file_not_approved_descr).format(srvs.sorted().joinToString(separator = ", ")), + confirmText = generalGetString(MR.strings.download_file), + onConfirm = { + val user = chatModel.currentUser.value + if (user != null) { + withBGApi { chatModel.controller.receiveFile(rh, user, fileId, userApprovedRelays = true) } + } + }, + ) + } + } else if (!auto) { apiErrorAlert("apiReceiveFile", generalGetString(MR.strings.error_receiving_file), r) } } @@ -1405,10 +1587,12 @@ object ChatController { val r = sendCmd(rh, CC.ApiJoinGroup(groupId)) when (r) { is CR.UserAcceptedGroupSent -> - chatModel.updateGroup(rh, r.groupInfo) + withChats { + updateGroup(rh, r.groupInfo) + } is CR.ChatCmdError -> { val e = r.chatError - suspend fun deleteGroup() { if (apiDeleteChat(rh, ChatType.Group, groupId)) { chatModel.removeChat(rh, "#$groupId") } } + suspend fun deleteGroup() { if (apiDeleteChat(rh, ChatType.Group, groupId)) { withChats { removeChat(rh, "#$groupId") } } } if (e is ChatError.ChatErrorAgent && e.agentError is AgentErrorType.SMP && e.agentError.smpErr is SMPErrorType.AUTH) { deleteGroup() AlertManager.shared.showAlertMsg(generalGetString(MR.strings.alert_title_group_invitation_expired), generalGetString(MR.strings.alert_message_group_invitation_expired)) @@ -1562,7 +1746,9 @@ object ChatController { val prefs = contact.mergedPreferences.toPreferences().setAllowed(feature, param = param) val toContact = apiSetContactPrefs(rh, contact.contactId, prefs) if (toContact != null) { - chatModel.updateContact(rh, toContact) + withChats { + updateContact(rh, toContact) + } } } @@ -1696,6 +1882,123 @@ object ChatController { ) true } + r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent + && r.chatError.agentError is AgentErrorType.BROKER + && r.chatError.agentError.brokerErr is BrokerErrorType.HOST -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.connection_error), + String.format(generalGetString(MR.strings.network_error_broker_host_desc), serverHostname(r.chatError.agentError.brokerAddress)) + ) + true + } + r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent + && r.chatError.agentError is AgentErrorType.BROKER + && r.chatError.agentError.brokerErr is BrokerErrorType.TRANSPORT + && r.chatError.agentError.brokerErr.transportErr is SMPTransportError.Version -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.connection_error), + String.format(generalGetString(MR.strings.network_error_broker_version_desc), serverHostname(r.chatError.agentError.brokerAddress)) + ) + true + } + r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent + && r.chatError.agentError is AgentErrorType.SMP + && r.chatError.agentError.smpErr is SMPErrorType.PROXY -> + smpProxyErrorAlert(r.chatError.agentError.smpErr.proxyErr, r.chatError.agentError.serverAddress) + r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorAgent + && r.chatError.agentError is AgentErrorType.PROXY + && r.chatError.agentError.proxyErr is ProxyClientError.ProxyProtocolError + && r.chatError.agentError.proxyErr.protocolErr is SMPErrorType.PROXY -> + proxyDestinationErrorAlert( + r.chatError.agentError.proxyErr.protocolErr.proxyErr, + r.chatError.agentError.proxyServer, + r.chatError.agentError.relayServer + ) + else -> false + } + } + + private fun smpProxyErrorAlert(pe: ProxyError, srvAddr: String): Boolean { + return when { + pe is ProxyError.BROKER + && pe.brokerErr is BrokerErrorType.TIMEOUT -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.private_routing_error), + String.format(generalGetString(MR.strings.smp_proxy_error_connecting), serverHostname(srvAddr)) + ) + true + } + pe is ProxyError.BROKER + && pe.brokerErr is BrokerErrorType.NETWORK -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.private_routing_error), + String.format(generalGetString(MR.strings.smp_proxy_error_connecting), serverHostname(srvAddr)) + ) + true + } + pe is ProxyError.BROKER + && pe.brokerErr is BrokerErrorType.HOST -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.private_routing_error), + String.format(generalGetString(MR.strings.smp_proxy_error_broker_host), serverHostname(srvAddr)) + ) + true + } + pe is ProxyError.BROKER + && pe.brokerErr is BrokerErrorType.TRANSPORT + && pe.brokerErr.transportErr is SMPTransportError.Version -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.private_routing_error), + String.format(generalGetString(MR.strings.smp_proxy_error_broker_version), serverHostname(srvAddr)) + ) + true + } + else -> false + } + } + + private fun proxyDestinationErrorAlert(pe: ProxyError, proxyServer: String, relayServer: String): Boolean { + return when { + pe is ProxyError.BROKER + && pe.brokerErr is BrokerErrorType.TIMEOUT -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.private_routing_error), + String.format(generalGetString(MR.strings.proxy_destination_error_failed_to_connect), serverHostname(proxyServer), serverHostname(relayServer)) + ) + true + } + pe is ProxyError.BROKER + && pe.brokerErr is BrokerErrorType.NETWORK -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.private_routing_error), + String.format(generalGetString(MR.strings.proxy_destination_error_failed_to_connect), serverHostname(proxyServer), serverHostname(relayServer)) + ) + true + } + pe is ProxyError.NO_SESSION -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.private_routing_error), + String.format(generalGetString(MR.strings.proxy_destination_error_failed_to_connect), serverHostname(proxyServer), serverHostname(relayServer)) + ) + true + } + pe is ProxyError.BROKER + && pe.brokerErr is BrokerErrorType.HOST -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.private_routing_error), + String.format(generalGetString(MR.strings.proxy_destination_error_broker_host), serverHostname(relayServer), serverHostname(proxyServer)) + ) + true + } + pe is ProxyError.BROKER + && pe.brokerErr is BrokerErrorType.TRANSPORT + && pe.brokerErr.transportErr is SMPTransportError.Version -> { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.private_routing_error), + String.format(generalGetString(MR.strings.proxy_destination_error_broker_version), serverHostname(relayServer), serverHostname(proxyServer)) + ) + true + } else -> false } } @@ -1715,16 +2018,20 @@ object ChatController { when (r) { is CR.ContactDeletedByContact -> { if (active(r.user) && r.contact.directOrUsed) { - chatModel.updateContact(rhId, r.contact) + withChats { + updateContact(rhId, r.contact) + } } } is CR.ContactConnected -> { if (active(r.user) && r.contact.directOrUsed) { - chatModel.updateContact(rhId, r.contact) - val conn = r.contact.activeConn - if (conn != null) { - chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") - chatModel.removeChat(rhId, conn.id) + withChats { + updateContact(rhId, r.contact) + val conn = r.contact.activeConn + if (conn != null) { + chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") + removeChat(rhId, conn.id) + } } } if (r.contact.directOrUsed) { @@ -1734,22 +2041,39 @@ object ChatController { } is CR.ContactConnecting -> { if (active(r.user) && r.contact.directOrUsed) { - chatModel.updateContact(rhId, r.contact) - val conn = r.contact.activeConn - if (conn != null) { - chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") - chatModel.removeChat(rhId, conn.id) + withChats { + updateContact(rhId, r.contact) + val conn = r.contact.activeConn + if (conn != null) { + chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") + removeChat(rhId, conn.id) + } } } } + is CR.ContactSndReady -> { + if (active(r.user) && r.contact.directOrUsed) { + withChats { + updateContact(rhId, r.contact) + val conn = r.contact.activeConn + if (conn != null) { + chatModel.replaceConnReqView(conn.id, "@${r.contact.contactId}") + removeChat(rhId, conn.id) + } + } + } + chatModel.setContactNetworkStatus(r.contact, NetworkStatus.Connected()) + } is CR.ReceivedContactRequest -> { val contactRequest = r.contactRequest val cInfo = ChatInfo.ContactRequest(contactRequest) if (active(r.user)) { - if (chatModel.hasChat(rhId, contactRequest.id)) { - chatModel.updateChatInfo(rhId, cInfo) - } else { - chatModel.addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = listOf())) + withChats { + if (chatModel.hasChat(rhId, contactRequest.id)) { + updateChatInfo(rhId, cInfo) + } else { + addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = listOf())) + } } } ntfManager.notifyContactRequestReceived(r.user, cInfo) @@ -1757,12 +2081,16 @@ object ChatController { is CR.ContactUpdated -> { if (active(r.user) && chatModel.hasChat(rhId, r.toContact.id)) { val cInfo = ChatInfo.Direct(r.toContact) - chatModel.updateChatInfo(rhId, cInfo) + withChats { + updateChatInfo(rhId, cInfo) + } } } is CR.GroupMemberUpdated -> { if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.toMember) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.toMember) + } } } is CR.ContactsMerged -> { @@ -1770,7 +2098,9 @@ object ChatController { if (chatModel.chatId.value == r.mergedContact.id) { chatModel.chatId.value = r.intoContact.id } - chatModel.removeChat(rhId, r.mergedContact.id) + withChats { + removeChat(rhId, r.mergedContact.id) + } } } // ContactsSubscribed, ContactsDisconnected and ContactSubSummary are only used in CLI, @@ -1780,7 +2110,9 @@ object ChatController { is CR.ContactSubSummary -> { for (sub in r.contactSubscriptions) { if (active(r.user)) { - chatModel.updateContact(rhId, sub.contact) + withChats { + updateContact(rhId, sub.contact) + } } val err = sub.contactError if (err == null) { @@ -1804,7 +2136,9 @@ object ChatController { val cInfo = r.chatItem.chatInfo val cItem = r.chatItem.chatItem if (active(r.user)) { - chatModel.addChatItem(rhId, cInfo, cItem) + withChats { + addChatItem(rhId, cInfo, cItem) + } } else if (cItem.isRcvNew && cInfo.ntfsEnabled) { chatModel.increaseUnreadCounter(rhId, r.user) } @@ -1825,112 +2159,153 @@ object ChatController { val cInfo = r.chatItem.chatInfo val cItem = r.chatItem.chatItem if (!cItem.isDeletedContent && active(r.user)) { - chatModel.updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus) + withChats { + updateChatItem(cInfo, cItem, status = cItem.meta.itemStatus) + } } } is CR.ChatItemUpdated -> chatItemSimpleUpdate(rhId, r.user, r.chatItem) is CR.ChatItemReaction -> { if (active(r.user)) { - chatModel.updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem) + withChats { + updateChatItem(r.reaction.chatInfo, r.reaction.chatReaction.chatItem) + } } } - is CR.ChatItemDeleted -> { + is CR.ChatItemsDeleted -> { if (!active(r.user)) { - if (r.toChatItem == null && r.deletedChatItem.chatItem.isRcvNew && r.deletedChatItem.chatInfo.ntfsEnabled) { - chatModel.decreaseUnreadCounter(rhId, r.user) + r.chatItemDeletions.forEach { (deletedChatItem, toChatItem) -> + if (toChatItem == null && deletedChatItem.chatItem.isRcvNew && deletedChatItem.chatInfo.ntfsEnabled) { + chatModel.decreaseUnreadCounter(rhId, r.user) + } } return } - - val cInfo = r.deletedChatItem.chatInfo - val cItem = r.deletedChatItem.chatItem - AudioPlayer.stop(cItem) - val isLastChatItem = chatModel.getChat(cInfo.id)?.chatItems?.lastOrNull()?.id == cItem.id - if (isLastChatItem && ntfManager.hasNotificationsForChat(cInfo.id)) { - ntfManager.cancelNotificationsForChat(cInfo.id) - ntfManager.displayNotification( - r.user, - cInfo.id, - cInfo.displayName, - generalGetString(if (r.toChatItem != null) MR.strings.marked_deleted_description else MR.strings.deleted_description) - ) - } - if (r.toChatItem == null) { - chatModel.removeChatItem(rhId, cInfo, cItem) - } else { - chatModel.upsertChatItem(rhId, cInfo, r.toChatItem.chatItem) + r.chatItemDeletions.forEach { (deletedChatItem, toChatItem) -> + val cInfo = deletedChatItem.chatInfo + val cItem = deletedChatItem.chatItem + if (chatModel.chatId.value != null) { + // Stop voice playback only inside a chat, allow to play in a chat list + AudioPlayer.stop(cItem) + } + val isLastChatItem = chatModel.getChat(cInfo.id)?.chatItems?.lastOrNull()?.id == cItem.id + if (isLastChatItem && ntfManager.hasNotificationsForChat(cInfo.id)) { + ntfManager.cancelNotificationsForChat(cInfo.id) + ntfManager.displayNotification( + r.user, + cInfo.id, + cInfo.displayName, + generalGetString(if (toChatItem != null) MR.strings.marked_deleted_description else MR.strings.deleted_description) + ) + } + withChats { + if (toChatItem == null) { + removeChatItem(rhId, cInfo, cItem) + } else { + upsertChatItem(rhId, cInfo, toChatItem.chatItem) + } + } } } is CR.ReceivedGroupInvitation -> { if (active(r.user)) { - chatModel.updateGroup(rhId, r.groupInfo) // update so that repeat group invitations are not duplicated + withChats { + // update so that repeat group invitations are not duplicated + updateGroup(rhId, r.groupInfo) + } // TODO NtfManager.shared.notifyGroupInvitation } } is CR.UserAcceptedGroupSent -> { if (!active(r.user)) return - chatModel.updateGroup(rhId, r.groupInfo) - val conn = r.hostContact?.activeConn - if (conn != null) { - chatModel.replaceConnReqView(conn.id, "#${r.groupInfo.groupId}") - chatModel.removeChat(rhId, conn.id) + withChats { + updateGroup(rhId, r.groupInfo) + val conn = r.hostContact?.activeConn + if (conn != null) { + chatModel.replaceConnReqView(conn.id, "#${r.groupInfo.groupId}") + removeChat(rhId, conn.id) + } } } is CR.GroupLinkConnecting -> { if (!active(r.user)) return - chatModel.updateGroup(rhId, r.groupInfo) - val hostConn = r.hostMember.activeConn - if (hostConn != null) { - chatModel.replaceConnReqView(hostConn.id, "#${r.groupInfo.groupId}") - chatModel.removeChat(rhId, hostConn.id) + withChats { + updateGroup(rhId, r.groupInfo) + val hostConn = r.hostMember.activeConn + if (hostConn != null) { + chatModel.replaceConnReqView(hostConn.id, "#${r.groupInfo.groupId}") + removeChat(rhId, hostConn.id) + } } } is CR.JoinedGroupMemberConnecting -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.DeletedMemberUser -> // TODO update user member if (active(r.user)) { - chatModel.updateGroup(rhId, r.groupInfo) + withChats { + updateGroup(rhId, r.groupInfo) + } } is CR.DeletedMember -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.deletedMember) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.deletedMember) + } } is CR.LeftMember -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.MemberRole -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.MemberRoleUser -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.MemberBlockedForAll -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.GroupDeleted -> // TODO update user member if (active(r.user)) { - chatModel.updateGroup(rhId, r.groupInfo) + withChats { + updateGroup(rhId, r.groupInfo) + } } is CR.UserJoinedGroup -> if (active(r.user)) { - chatModel.updateGroup(rhId, r.groupInfo) + withChats { + updateGroup(rhId, r.groupInfo) + } } is CR.JoinedGroupMember -> if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.member) + } } is CR.ConnectedToGroupMember -> { if (active(r.user)) { - chatModel.upsertGroupMember(rhId, r.groupInfo, r.member) + withChats { + upsertGroupMember(rhId, r.groupInfo, r.member) + } } if (r.memberContact != null) { chatModel.setContactNetworkStatus(r.memberContact, NetworkStatus.Connected()) @@ -1938,11 +2313,15 @@ object ChatController { } is CR.GroupUpdated -> if (active(r.user)) { - chatModel.updateGroup(rhId, r.toGroup) + withChats { + updateGroup(rhId, r.toGroup) + } } is CR.NewMemberContactReceivedInv -> if (active(r.user)) { - chatModel.updateContact(rhId, r.contact) + withChats { + updateContact(rhId, r.contact) + } } is CR.RcvFileStart -> chatItemSimpleUpdate(rhId, r.user, r.chatItem) @@ -1963,6 +2342,11 @@ object ChatController { cleanupFile(r.chatItem_) } } + is CR.RcvFileWarning -> { + if (r.chatItem_ != null) { + chatItemSimpleUpdate(rhId, r.user, r.chatItem_) + } + } is CR.SndFileStart -> chatItemSimpleUpdate(rhId, r.user, r.chatItem) is CR.SndFileComplete -> { @@ -1989,6 +2373,11 @@ object ChatController { cleanupFile(r.chatItem_) } } + is CR.SndFileWarning -> { + if (r.chatItem_ != null) { + chatItemSimpleUpdate(rhId, r.user, r.chatItem_) + } + } is CR.CallInvitation -> { chatModel.callManager.reportNewIncomingCall(r.callInvitation.copy(remoteHostId = rhId)) } @@ -2031,13 +2420,29 @@ object ChatController { } } is CR.ContactSwitch -> - chatModel.updateContactConnectionStats(rhId, r.contact, r.switchProgress.connectionStats) + if (active(r.user)) { + withChats { + updateContactConnectionStats(rhId, r.contact, r.switchProgress.connectionStats) + } + } is CR.GroupMemberSwitch -> - chatModel.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.switchProgress.connectionStats) + if (active(r.user)) { + withChats { + updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.switchProgress.connectionStats) + } + } is CR.ContactRatchetSync -> - chatModel.updateContactConnectionStats(rhId, r.contact, r.ratchetSyncProgress.connectionStats) + if (active(r.user)) { + withChats { + updateContactConnectionStats(rhId, r.contact, r.ratchetSyncProgress.connectionStats) + } + } is CR.GroupMemberRatchetSync -> - chatModel.updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.ratchetSyncProgress.connectionStats) + if (active(r.user)) { + withChats { + updateGroupMemberConnectionStats(rhId, r.groupInfo, r.member, r.ratchetSyncProgress.connectionStats) + } + } is CR.RemoteHostSessionCode -> { chatModel.remoteHostPairing.value = r.remoteHost_ to RemoteHostSessionState.PendingConfirmation(r.sessionCode) } @@ -2046,6 +2451,13 @@ object ChatController { chatModel.currentRemoteHost.value = r.remoteHost switchUIRemoteHost(r.remoteHost.remoteHostId) } + is CR.ContactDisabled -> { + if (active(r.user)) { + withChats { + updateContact(rhId, r.contact) + } + } + } is CR.RemoteHostStopped -> { val disconnectedHost = chatModel.remoteHosts.firstOrNull { it.remoteHostId == r.remoteHostId_ } chatModel.remoteHostPairing.value = null @@ -2108,15 +2520,45 @@ object ChatController { val sess = chatModel.remoteCtrlSession.value if (sess != null) { chatModel.remoteCtrlSession.value = null + ModalManager.fullscreen.closeModals() fun showAlert(chatError: ChatError) { - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.remote_ctrl_was_disconnected_title), - if (chatError is ChatError.ChatErrorRemoteCtrl) { - chatError.remoteCtrlError.localizedString - } else { - generalGetString(MR.strings.remote_ctrl_disconnected_with_reason).format(chatError.string) - } - ) + when { + r.rcStopReason is RemoteCtrlStopReason.Disconnected -> + {} + r.rcStopReason is RemoteCtrlStopReason.ConnectionFailed + && r.rcStopReason.chatError is ChatError.ChatErrorAgent + && r.rcStopReason.chatError.agentError is AgentErrorType.RCP + && r.rcStopReason.chatError.agentError.rcpErr is RCErrorType.IDENTITY -> + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.remote_ctrl_was_disconnected_title), + text = generalGetString(MR.strings.remote_ctrl_connection_stopped_identity_desc) + ) + else -> + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.remote_ctrl_was_disconnected_title), + text = if (chatError is ChatError.ChatErrorRemoteCtrl) { + chatError.remoteCtrlError.localizedString + } else { + generalGetString(MR.strings.remote_ctrl_connection_stopped_desc) + }, + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(stringResource(MR.strings.ok), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + val clipboard = LocalClipboardManager.current + SectionItemView({ + clipboard.setText(AnnotatedString(json.encodeToString(r.rcStopReason))) + AlertManager.shared.hideAlert() + }) { + Text(stringResource(MR.strings.copy_error), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + ) + } } when (r.rcStopReason) { is RemoteCtrlStopReason.DiscoveryFailed -> showAlert(r.rcStopReason.chatError) @@ -2136,7 +2578,9 @@ object ChatController { } is CR.ContactPQEnabled -> if (active(r.user)) { - chatModel.updateContact(rhId, r.contact) + withChats { + updateContact(rhId, r.contact) + } } is CR.ChatRespError -> when { r.chatError is ChatError.ChatErrorAgent && r.chatError.agentError is AgentErrorType.CRITICAL -> { @@ -2196,9 +2640,14 @@ object ChatController { } } - suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, auto: Boolean = false) { - val encrypted = appPrefs.privacyEncryptLocalFiles.get() - val chatItem = apiReceiveFile(rhId, fileId, encrypted = encrypted, auto = auto) + suspend fun receiveFile(rhId: Long?, user: UserLike, fileId: Long, userApprovedRelays: Boolean = false, auto: Boolean = false) { + val chatItem = apiReceiveFile( + rhId, + fileId, + userApprovedRelays = userApprovedRelays || !appPrefs.privacyAskToApproveRelays.get(), + encrypted = appPrefs.privacyEncryptLocalFiles.get(), + auto = auto + ) if (chatItem != null) { chatItemSimpleUpdate(rhId, user, chatItem) } @@ -2207,7 +2656,9 @@ object ChatController { suspend fun leaveGroup(rh: Long?, groupId: Long) { val groupInfo = apiLeaveGroup(rh, groupId) if (groupInfo != null) { - chatModel.updateGroup(rh, groupInfo) + withChats { + updateGroup(rh, groupInfo) + } } } @@ -2217,7 +2668,9 @@ object ChatController { val notify = { ntfManager.notifyMessageReceived(user, cInfo, cItem) } if (!activeUser(rh, user)) { notify() - } else if (chatModel.upsertChatItem(rh, cInfo, cItem)) { + } else if (withChats { upsertChatItem(rh, cInfo, cItem) }) { + notify() + } else if (cItem.content is CIContent.RcvCall && cItem.content.status == CICallStatus.Missed) { notify() } } @@ -2259,7 +2712,10 @@ object ChatController { chatModel.currentUser.value = user if (user == null) { chatModel.chatItems.clear() - chatModel.chats.clear() + withChats { + chats.clear() + popChatCollector.clear() + } } val statuses = apiGetNetworkStatuses(rhId) if (statuses != null) { @@ -2302,6 +2758,8 @@ object ChatController { val hostMode = HostMode.valueOf(appPrefs.networkHostMode.get()!!) val requiredHostMode = appPrefs.networkRequiredHostMode.get() val sessionMode = appPrefs.networkSessionMode.get() + val smpProxyMode = SMPProxyMode.valueOf(appPrefs.networkSMPProxyMode.get()!!) + val smpProxyFallback = SMPProxyFallback.valueOf(appPrefs.networkSMPProxyFallback.get()!!) val tcpConnectTimeout = appPrefs.networkTCPConnectTimeout.get() val tcpTimeout = appPrefs.networkTCPTimeout.get() val tcpTimeoutPerKb = appPrefs.networkTCPTimeoutPerKb.get() @@ -2322,6 +2780,8 @@ object ChatController { hostMode = hostMode, requiredHostMode = requiredHostMode, sessionMode = sessionMode, + smpProxyMode = smpProxyMode, + smpProxyFallback = smpProxyFallback, tcpConnectTimeout = tcpConnectTimeout, tcpTimeout = tcpTimeout, tcpTimeoutPerKb = tcpTimeoutPerKb, @@ -2340,6 +2800,8 @@ object ChatController { appPrefs.networkHostMode.set(cfg.hostMode.name) appPrefs.networkRequiredHostMode.set(cfg.requiredHostMode) appPrefs.networkSessionMode.set(cfg.sessionMode) + appPrefs.networkSMPProxyMode.set(cfg.smpProxyMode.name) + appPrefs.networkSMPProxyFallback.set(cfg.smpProxyFallback.name) appPrefs.networkTCPConnectTimeout.set(cfg.tcpConnectTimeout) appPrefs.networkTCPTimeout.set(cfg.tcpTimeout) appPrefs.networkTCPTimeoutPerKb.set(cfg.tcpTimeoutPerKb) @@ -2374,7 +2836,7 @@ class SharedPreference(val get: () -> T, set: (T) -> Unit) { sealed class CC { class Console(val cmd: String): CC() class ShowActiveUser: CC() - class CreateActiveUser(val profile: Profile?, val sameServers: Boolean, val pastTimestamp: Boolean): CC() + class CreateActiveUser(val profile: Profile?, val pastTimestamp: Boolean): CC() class ListUsers: CC() class ApiSetActiveUser(val userId: Long, val viewPwd: String?): CC() class SetAllContactReceipts(val enable: Boolean): CC() @@ -2386,10 +2848,10 @@ sealed class CC { class ApiUnmuteUser(val userId: Long): CC() class ApiDeleteUser(val userId: Long, val delSMPQueues: Boolean, val viewPwd: String?): CC() class StartChat(val mainApp: Boolean): CC() + class CheckChatRunning: CC() class ApiStopChat: CC() - class SetTempFolder(val tempFolder: String): CC() - class SetFilesFolder(val filesFolder: String): CC() - class SetRemoteHostsFolder(val remoteHostsFolder: String): CC() + @Serializable + class ApiSetAppFilePaths(val appFilesFolder: String, val appTempFolder: String, val appAssetsFolder: String, val appRemoteHostsFolder: String): CC() class ApiSetEncryptLocalFiles(val enable: Boolean): CC() class ApiExportArchive(val config: ArchiveConfig): CC() class ApiImportArchive(val config: ArchiveConfig): CC() @@ -2404,10 +2866,10 @@ sealed class CC { class ApiSendMessage(val type: ChatType, val id: Long, val file: CryptoFile?, val quotedItemId: Long?, val mc: MsgContent, val live: Boolean, val ttl: Int?): CC() class ApiCreateChatItem(val noteFolderId: Long, val file: CryptoFile?, val mc: MsgContent): CC() class ApiUpdateChatItem(val type: ChatType, val id: Long, val itemId: Long, val mc: MsgContent, val live: Boolean): CC() - class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemId: Long, val mode: CIDeleteMode): CC() - class ApiDeleteMemberChatItem(val groupId: Long, val groupMemberId: Long, val itemId: Long): CC() + class ApiDeleteChatItem(val type: ChatType, val id: Long, val itemIds: List, val mode: CIDeleteMode): CC() + class ApiDeleteMemberChatItem(val groupId: Long, val itemIds: List): CC() class ApiChatItemReaction(val type: ChatType, val id: Long, val itemId: Long, val add: Boolean, val reaction: MsgReaction): CC() - class ApiForwardChatItem(val toChatType: ChatType, val toChatId: Long, val fromChatType: ChatType, val fromChatId: Long, val itemId: Long): CC() + class ApiForwardChatItem(val toChatType: ChatType, val toChatId: Long, val fromChatType: ChatType, val fromChatId: Long, val itemId: Long, val ttl: Int?): CC() class ApiNewGroup(val userId: Long, val incognito: Boolean, val groupProfile: GroupProfile): CC() class ApiAddMember(val groupId: Long, val contactId: Long, val memberRole: GroupMemberRole): CC() class ApiJoinGroup(val groupId: Long): CC() @@ -2431,10 +2893,14 @@ sealed class CC { class APISetNetworkConfig(val networkConfig: NetCfg): CC() class APIGetNetworkConfig: CC() class APISetNetworkInfo(val networkInfo: UserNetworkInfo): CC() + class ReconnectServer(val userId: Long, val server: String): CC() + class ReconnectAllServers: CC() class APISetChatSettings(val type: ChatType, val id: Long, val chatSettings: ChatSettings): CC() class ApiSetMemberSettings(val groupId: Long, val groupMemberId: Long, val memberSettings: GroupMemberSettings): CC() class APIContactInfo(val contactId: Long): CC() class APIGroupMemberInfo(val groupId: Long, val groupMemberId: Long): CC() + class APIContactQueueInfo(val contactId: Long): CC() + class APIGroupMemberQueueInfo(val groupId: Long, val groupMemberId: Long): CC() class APISwitchContact(val contactId: Long): CC() class APISwitchGroupMember(val groupId: Long, val groupMemberId: Long): CC() class APIAbortSwitchContact(val contactId: Long): CC() @@ -2450,13 +2916,15 @@ sealed class CC { class APIConnectPlan(val userId: Long, val connReq: String): CC() class APIConnect(val userId: Long, val incognito: Boolean, val connReq: String): CC() class ApiConnectContactViaAddress(val userId: Long, val incognito: Boolean, val contactId: Long): CC() - class ApiDeleteChat(val type: ChatType, val id: Long, val notify: Boolean?): CC() + class ApiDeleteChat(val type: ChatType, val id: Long, val chatDeleteMode: ChatDeleteMode): CC() class ApiClearChat(val type: ChatType, val id: Long): CC() class ApiListContacts(val userId: Long): CC() class ApiUpdateProfile(val userId: Long, val profile: Profile): CC() class ApiSetContactPrefs(val contactId: Long, val prefs: ChatPreferences): CC() class ApiSetContactAlias(val contactId: Long, val localAlias: String): CC() class ApiSetConnectionAlias(val connId: Long, val localAlias: String): CC() + class ApiSetUserUIThemes(val userId: Long, val themes: ThemeModeOverrides?): CC() + class ApiSetChatUIThemes(val chatId: String, val themes: ThemeModeOverrides?): CC() class ApiCreateMyAddress(val userId: Long): CC() class ApiDeleteMyAddress(val userId: Long): CC() class ApiShowMyAddress(val userId: Long): CC() @@ -2475,7 +2943,7 @@ sealed class CC { class ApiRejectContact(val contactReqId: Long): CC() class ApiChatRead(val type: ChatType, val id: Long, val range: ItemRange): CC() class ApiChatUnread(val type: ChatType, val id: Long, val unreadChat: Boolean): CC() - class ReceiveFile(val fileId: Long, val encrypt: Boolean, val inline: Boolean?): CC() + class ReceiveFile(val fileId: Long, val userApprovedRelays: Boolean, val encrypt: Boolean, val inline: Boolean?): CC() class CancelFile(val fileId: Long): CC() // Remote control class SetLocalDeviceName(val displayName: String): CC() @@ -2498,12 +2966,15 @@ sealed class CC { class ApiStandaloneFileInfo(val url: String): CC() // misc class ShowVersion(): CC() + class ResetAgentServersStats(): CC() + class GetAgentSubsTotal(val userId: Long): CC() + class GetAgentServersSummary(val userId: Long): CC() val cmdString: String get() = when (this) { is Console -> cmd is ShowActiveUser -> "/u" is CreateActiveUser -> { - val user = NewUser(profile, sameServers = sameServers, pastTimestamp = pastTimestamp) + val user = NewUser(profile, pastTimestamp = pastTimestamp) "/_create user ${json.encodeToString(user)}" } is ListUsers -> "/users" @@ -2523,10 +2994,9 @@ sealed class CC { is ApiUnmuteUser -> "/_unmute user $userId" is ApiDeleteUser -> "/_delete user $userId del_smp=${onOff(delSMPQueues)}${maybePwd(viewPwd)}" is StartChat -> "/_start main=${onOff(mainApp)}" + is CheckChatRunning -> "/_check running" is ApiStopChat -> "/_stop" - is SetTempFolder -> "/_temp_folder $tempFolder" - is SetFilesFolder -> "/_files_folder $filesFolder" - is SetRemoteHostsFolder -> "/remote_hosts_folder $remoteHostsFolder" + is ApiSetAppFilePaths -> "/set file paths ${json.encodeToString(this)}" is ApiSetEncryptLocalFiles -> "/_files_encrypt ${onOff(enable)}" is ApiExportArchive -> "/_db export ${json.encodeToString(config)}" is ApiImportArchive -> "/_db import ${json.encodeToString(config)}" @@ -2546,10 +3016,13 @@ sealed class CC { "/_create *$noteFolderId json ${json.encodeToString(ComposedMessage(file, null, mc))}" } is ApiUpdateChatItem -> "/_update item ${chatRef(type, id)} $itemId live=${onOff(live)} ${mc.cmdString}" - is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} $itemId ${mode.deleteMode}" - is ApiDeleteMemberChatItem -> "/_delete member item #$groupId $groupMemberId $itemId" + is ApiDeleteChatItem -> "/_delete item ${chatRef(type, id)} ${itemIds.joinToString(",")} ${mode.deleteMode}" + is ApiDeleteMemberChatItem -> "/_delete member item #$groupId ${itemIds.joinToString(",")}" is ApiChatItemReaction -> "/_reaction ${chatRef(type, id)} $itemId ${onOff(add)} ${json.encodeToString(reaction)}" - is ApiForwardChatItem -> "/_forward ${chatRef(toChatType, toChatId)} ${chatRef(fromChatType, fromChatId)} $itemId" + is ApiForwardChatItem -> { + val ttlStr = if (ttl != null) "$ttl" else "default" + "/_forward ${chatRef(toChatType, toChatId)} ${chatRef(fromChatType, fromChatId)} $itemId ttl=${ttlStr}" + } is ApiNewGroup -> "/_group $userId incognito=${onOff(incognito)} ${json.encodeToString(groupProfile)}" is ApiAddMember -> "/_add #$groupId $contactId ${memberRole.memberRole}" is ApiJoinGroup -> "/_join #$groupId" @@ -2573,10 +3046,14 @@ sealed class CC { is APISetNetworkConfig -> "/_network ${json.encodeToString(networkConfig)}" is APIGetNetworkConfig -> "/network" is APISetNetworkInfo -> "/_network info ${json.encodeToString(networkInfo)}" + is ReconnectServer -> "/reconnect $userId $server" + is ReconnectAllServers -> "/reconnect" is APISetChatSettings -> "/_settings ${chatRef(type, id)} ${json.encodeToString(chatSettings)}" is ApiSetMemberSettings -> "/_member settings #$groupId $groupMemberId ${json.encodeToString(memberSettings)}" is APIContactInfo -> "/_info @$contactId" is APIGroupMemberInfo -> "/_info #$groupId $groupMemberId" + is APIContactQueueInfo -> "/_queue info @$contactId" + is APIGroupMemberQueueInfo -> "/_queue info #$groupId $groupMemberId" is APISwitchContact -> "/_switch @$contactId" is APISwitchGroupMember -> "/_switch #$groupId $groupMemberId" is APIAbortSwitchContact -> "/_abort switch @$contactId" @@ -2592,17 +3069,15 @@ sealed class CC { is APIConnectPlan -> "/_connect plan $userId $connReq" is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq" is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId" - is ApiDeleteChat -> if (notify != null) { - "/_delete ${chatRef(type, id)} notify=${onOff(notify)}" - } else { - "/_delete ${chatRef(type, id)}" - } + is ApiDeleteChat -> "/_delete ${chatRef(type, id)} ${chatDeleteMode.cmdString}" is ApiClearChat -> "/_clear chat ${chatRef(type, id)}" is ApiListContacts -> "/_contacts $userId" is ApiUpdateProfile -> "/_profile $userId ${json.encodeToString(profile)}" is ApiSetContactPrefs -> "/_set prefs @$contactId ${json.encodeToString(prefs)}" is ApiSetContactAlias -> "/_set alias @$contactId ${localAlias.trim()}" is ApiSetConnectionAlias -> "/_set alias :$connId ${localAlias.trim()}" + is ApiSetUserUIThemes -> "/_set theme user $userId ${if (themes != null) json.encodeToString(themes) else ""}" + is ApiSetChatUIThemes -> "/_set theme $chatId ${if (themes != null) json.encodeToString(themes) else ""}" is ApiCreateMyAddress -> "/_address $userId" is ApiDeleteMyAddress -> "/_delete_address $userId" is ApiShowMyAddress -> "/_show_address $userId" @@ -2623,6 +3098,7 @@ sealed class CC { is ApiChatUnread -> "/_unread chat ${chatRef(type, id)} ${onOff(unreadChat)}" is ReceiveFile -> "/freceive $fileId" + + (" approved_relays=${onOff(userApprovedRelays)}") + (if (encrypt == null) "" else " encrypt=${onOff(encrypt)}") + (if (inline == null) "" else " inline=${onOff(inline)}") is CancelFile -> "/fcancel $fileId" @@ -2648,6 +3124,9 @@ sealed class CC { is ApiDownloadStandaloneFile -> "/_download $userId $url ${file.filePath}" is ApiStandaloneFileInfo -> "/_download info $url" is ShowVersion -> "/version" + is ResetAgentServersStats -> "/reset servers stats" + is GetAgentSubsTotal -> "/get subs total $userId" + is GetAgentServersSummary -> "/get servers summary $userId" } val cmdType: String get() = when (this) { @@ -2665,10 +3144,9 @@ sealed class CC { is ApiUnmuteUser -> "apiUnmuteUser" is ApiDeleteUser -> "apiDeleteUser" is StartChat -> "startChat" + is CheckChatRunning -> "checkChatRunning" is ApiStopChat -> "apiStopChat" - is SetTempFolder -> "setTempFolder" - is SetFilesFolder -> "setFilesFolder" - is SetRemoteHostsFolder -> "setRemoteHostsFolder" + is ApiSetAppFilePaths -> "apiSetAppFilePaths" is ApiSetEncryptLocalFiles -> "apiSetEncryptLocalFiles" is ApiExportArchive -> "apiExportArchive" is ApiImportArchive -> "apiImportArchive" @@ -2710,10 +3188,14 @@ sealed class CC { is APISetNetworkConfig -> "apiSetNetworkConfig" is APIGetNetworkConfig -> "apiGetNetworkConfig" is APISetNetworkInfo -> "apiSetNetworkInfo" + is ReconnectServer -> "reconnectServer" + is ReconnectAllServers -> "reconnectAllServers" is APISetChatSettings -> "apiSetChatSettings" is ApiSetMemberSettings -> "apiSetMemberSettings" is APIContactInfo -> "apiContactInfo" is APIGroupMemberInfo -> "apiGroupMemberInfo" + is APIContactQueueInfo -> "apiContactQueueInfo" + is APIGroupMemberQueueInfo -> "apiGroupMemberQueueInfo" is APISwitchContact -> "apiSwitchContact" is APISwitchGroupMember -> "apiSwitchGroupMember" is APIAbortSwitchContact -> "apiAbortSwitchContact" @@ -2736,6 +3218,8 @@ sealed class CC { is ApiSetContactPrefs -> "apiSetContactPrefs" is ApiSetContactAlias -> "apiSetContactAlias" is ApiSetConnectionAlias -> "apiSetConnectionAlias" + is ApiSetUserUIThemes -> "apiSetUserUIThemes" + is ApiSetChatUIThemes -> "apiSetChatUIThemes" is ApiCreateMyAddress -> "apiCreateMyAddress" is ApiDeleteMyAddress -> "apiDeleteMyAddress" is ApiShowMyAddress -> "apiShowMyAddress" @@ -2775,6 +3259,9 @@ sealed class CC { is ApiDownloadStandaloneFile -> "apiDownloadStandaloneFile" is ApiStandaloneFileInfo -> "apiStandaloneFileInfo" is ShowVersion -> "showVersion" + is ResetAgentServersStats -> "resetAgentServersStats" + is GetAgentSubsTotal -> "getAgentSubsTotal" + is GetAgentServersSummary -> "getAgentServersSummary" } class ItemRange(val from: Long, val to: Long) @@ -2804,8 +3291,6 @@ sealed class CC { null } - private fun onOff(b: Boolean): String = if (b) "on" else "off" - private fun maybePwd(pwd: String?): String = if (pwd == "" || pwd == null) "" else " " + json.encodeToString(pwd) companion object { @@ -2815,10 +3300,11 @@ sealed class CC { } } +fun onOff(b: Boolean): String = if (b) "on" else "off" + @Serializable data class NewUser( val profile: Profile?, - val sameServers: Boolean, val pastTimestamp: Boolean ) @@ -2864,7 +3350,7 @@ data class ProtoServersConfig( data class UserProtocolServers( val serverProtocol: ServerProtocol, val protoServers: List, - val presetServers: List, + val presetServers: List, ) @Serializable @@ -2887,7 +3373,7 @@ data class ServerCfg( get() = server.isBlank() companion object { - val empty = ServerCfg(remoteHostId = null, server = "", preset = false, tested = null, enabled = true) + val empty = ServerCfg(remoteHostId = null, server = "", preset = false, tested = null, enabled = false) class SampleData( val preset: ServerCfg, @@ -3025,17 +3511,20 @@ data class ParsedServerAddress ( @Serializable data class NetCfg( val socksProxy: String?, - val hostMode: HostMode, - val requiredHostMode: Boolean, - val sessionMode: TransportSessionMode, + val socksMode: SocksMode = SocksMode.Always, + val hostMode: HostMode = HostMode.OnionViaSocks, + val requiredHostMode: Boolean = false, + val sessionMode: TransportSessionMode = TransportSessionMode.User, + val smpProxyMode: SMPProxyMode = SMPProxyMode.Unknown, + val smpProxyFallback: SMPProxyFallback = SMPProxyFallback.AllowProtected, val tcpConnectTimeout: Long, // microseconds val tcpTimeout: Long, // microseconds val tcpTimeoutPerKb: Long, // microseconds val rcvConcurrency: Int, // pool size - val tcpKeepAlive: KeepAliveOpts?, + val tcpKeepAlive: KeepAliveOpts? = KeepAliveOpts.defaults, val smpPingInterval: Long, // microseconds - val smpPingCount: Int, - val logTLSErrors: Boolean = false + val smpPingCount: Int = 3, + val logTLSErrors: Boolean = false, ) { val useSocksProxy: Boolean get() = socksProxy != null val enableKeepAlive: Boolean get() = tcpKeepAlive != null @@ -3053,31 +3542,21 @@ data class NetCfg( val defaults: NetCfg = NetCfg( socksProxy = null, - hostMode = HostMode.OnionViaSocks, - requiredHostMode = false, - sessionMode = TransportSessionMode.User, tcpConnectTimeout = 25_000_000, tcpTimeout = 15_000_000, tcpTimeoutPerKb = 10_000, rcvConcurrency = 12, - tcpKeepAlive = KeepAliveOpts.defaults, - smpPingInterval = 1200_000_000, - smpPingCount = 3 + smpPingInterval = 1200_000_000 ) val proxyDefaults: NetCfg = NetCfg( socksProxy = ":9050", - hostMode = HostMode.OnionViaSocks, - requiredHostMode = false, - sessionMode = TransportSessionMode.User, tcpConnectTimeout = 35_000_000, tcpTimeout = 20_000_000, tcpTimeoutPerKb = 15_000, rcvConcurrency = 8, - tcpKeepAlive = KeepAliveOpts.defaults, - smpPingInterval = 1200_000_000, - smpPingCount = 3 + smpPingInterval = 1200_000_000 ) } @@ -3109,6 +3588,27 @@ enum class HostMode { @SerialName("public") Public; } +@Serializable +enum class SocksMode { + @SerialName("always") Always, + @SerialName("onion") Onion; +} + +@Serializable +enum class SMPProxyMode { + @SerialName("always") Always, + @SerialName("unknown") Unknown, + @SerialName("unprotected") Unprotected, + @SerialName("never") Never; +} + +@Serializable +enum class SMPProxyFallback { + @SerialName("allow") Allow, + @SerialName("allowProtected") AllowProtected, + @SerialName("prohibit") Prohibit; +} + @Serializable enum class TransportSessionMode { @SerialName("user") User, @@ -3227,6 +3727,157 @@ data class TimedMessagesPreference( } } +@Serializable +data class PresentedServersSummary( + val statsStartedAt: Instant, + val allUsersSMP: SMPServersSummary, + val allUsersXFTP: XFTPServersSummary, + val currentUserSMP: SMPServersSummary, + val currentUserXFTP: XFTPServersSummary +) + +@Serializable +data class SMPServersSummary( + val smpTotals: SMPTotals, + val currentlyUsedSMPServers: List, + val previouslyUsedSMPServers: List, + val onlyProxiedSMPServers: List +) + +@Serializable +data class SMPTotals( + val sessions: ServerSessions, + val subs: SMPServerSubs, + val stats: AgentSMPServerStatsData +) + +@Serializable +data class SMPServerSummary( + val smpServer: String, + val known: Boolean? = null, + val sessions: ServerSessions? = null, + val subs: SMPServerSubs? = null, + val stats: AgentSMPServerStatsData? = null +) { + val hasSubs: Boolean + get() = subs != null + + val sessionsOrNew: ServerSessions + get() = sessions ?: ServerSessions.newServerSessions + + val subsOrNew: SMPServerSubs + get() = subs ?: SMPServerSubs.newSMPServerSubs +} + +@Serializable +data class ServerSessions( + val ssConnected: Int, + val ssErrors: Int, + val ssConnecting: Int +) { + companion object { + val newServerSessions = ServerSessions( + ssConnected = 0, + ssErrors = 0, + ssConnecting = 0 + ) + } + + val hasSess: Boolean + get() = ssConnected > 0 +} + +@Serializable +data class SMPServerSubs( + val ssActive: Int, + val ssPending: Int +) { + companion object { + val newSMPServerSubs = SMPServerSubs( + ssActive = 0, + ssPending = 0 + ) + } + + val total: Int + get() = ssActive + ssPending + + val shareOfActive: Float + get() = if (total != 0) ssActive.toFloat() / total else 0f +} + +@Serializable +data class AgentSMPServerStatsData( + val _sentDirect: Int, + val _sentViaProxy: Int, + val _sentProxied: Int, + val _sentDirectAttempts: Int, + val _sentViaProxyAttempts: Int, + val _sentProxiedAttempts: Int, + val _sentAuthErrs: Int, + val _sentQuotaErrs: Int, + val _sentExpiredErrs: Int, + val _sentOtherErrs: Int, + val _recvMsgs: Int, + val _recvDuplicates: Int, + val _recvCryptoErrs: Int, + val _recvErrs: Int, + val _ackMsgs: Int, + val _ackAttempts: Int, + val _ackNoMsgErrs: Int, + val _ackOtherErrs: Int, + val _connCreated: Int, + val _connSecured: Int, + val _connCompleted: Int, + val _connDeleted: Int, + val _connDelAttempts: Int, + val _connDelErrs: Int, + val _connSubscribed: Int, + val _connSubAttempts: Int, + val _connSubIgnored: Int, + val _connSubErrs: Int +) + +@Serializable +data class XFTPServersSummary( + val xftpTotals: XFTPTotals, + val currentlyUsedXFTPServers: List, + val previouslyUsedXFTPServers: List +) + +@Serializable +data class XFTPTotals( + val sessions: ServerSessions, + val stats: AgentXFTPServerStatsData +) + +@Serializable +data class XFTPServerSummary( + val xftpServer: String, + val known: Boolean? = null, + val sessions: ServerSessions? = null, + val stats: AgentXFTPServerStatsData? = null, + val rcvInProgress: Boolean, + val sndInProgress: Boolean, + val delInProgress: Boolean +) {} + +@Serializable +data class AgentXFTPServerStatsData( + val _uploads: Int, + val _uploadsSize: Long, + val _uploadAttempts: Int, + val _uploadErrs: Int, + val _downloads: Int, + val _downloadsSize: Long, + val _downloadAttempts: Int, + val _downloadAuthErrs: Int, + val _downloadErrs: Int, + val _deletions: Int, + val _deleteAttempts: Int, + val _deleteErrs: Int +) + sealed class CustomTimeUnit { object Second: CustomTimeUnit() object Minute: CustomTimeUnit() @@ -3971,6 +4622,15 @@ val json = Json { explicitNulls = false } +// Can decode unknown enum to default value specified for this field +val jsonCoerceInputValues = Json { + prettyPrint = true + ignoreUnknownKeys = true + encodeDefaults = true + explicitNulls = false + coerceInputValues = true +} + val jsonShort = Json { prettyPrint = false ignoreUnknownKeys = true @@ -3981,6 +4641,8 @@ val jsonShort = Json { val yaml = Yaml(configuration = YamlConfiguration( strictMode = false, encodeDefaults = false, + /** ~5.5 MB limitation since wallpaper is limited by 5 MB, see [saveWallpaperFile] */ + codePointLimit = 5500000, )) @Serializable @@ -4072,6 +4734,7 @@ sealed class CR { @Serializable @SerialName("networkConfig") class NetworkConfig(val networkConfig: NetCfg): CR() @Serializable @SerialName("contactInfo") class ContactInfo(val user: UserRef, val contact: Contact, val connectionStats_: ConnectionStats? = null, val customUserProfile: Profile? = null): CR() @Serializable @SerialName("groupMemberInfo") class GroupMemberInfo(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats_: ConnectionStats? = null): CR() + @Serializable @SerialName("queueInfo") class QueueInfoR(val user: UserRef, val rcvMsgInfo: RcvMsgInfo?, val queueInfo: QueueInfo): CR() @Serializable @SerialName("contactSwitchStarted") class ContactSwitchStarted(val user: UserRef, val contact: Contact, val connectionStats: ConnectionStats): CR() @Serializable @SerialName("groupMemberSwitchStarted") class GroupMemberSwitchStarted(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val connectionStats: ConnectionStats): CR() @Serializable @SerialName("contactSwitchAborted") class ContactSwitchAborted(val user: UserRef, val contact: Contact, val connectionStats: ConnectionStats): CR() @@ -4110,6 +4773,7 @@ sealed class CR { @Serializable @SerialName("userContactLinkDeleted") class UserContactLinkDeleted(val user: User): CR() @Serializable @SerialName("contactConnected") class ContactConnected(val user: UserRef, val contact: Contact, val userCustomProfile: Profile? = null): CR() @Serializable @SerialName("contactConnecting") class ContactConnecting(val user: UserRef, val contact: Contact): CR() + @Serializable @SerialName("contactSndReady") class ContactSndReady(val user: UserRef, val contact: Contact): CR() @Serializable @SerialName("receivedContactRequest") class ReceivedContactRequest(val user: UserRef, val contactRequest: UserContactRequest): CR() @Serializable @SerialName("acceptingContactRequest") class AcceptingContactRequest(val user: UserRef, val contact: Contact): CR() @Serializable @SerialName("contactRequestRejected") class ContactRequestRejected(val user: UserRef): CR() @@ -4131,7 +4795,7 @@ sealed class CR { @Serializable @SerialName("chatItemUpdated") class ChatItemUpdated(val user: UserRef, val chatItem: AChatItem): CR() @Serializable @SerialName("chatItemNotChanged") class ChatItemNotChanged(val user: UserRef, val chatItem: AChatItem): CR() @Serializable @SerialName("chatItemReaction") class ChatItemReaction(val user: UserRef, val added: Boolean, val reaction: ACIReaction): CR() - @Serializable @SerialName("chatItemDeleted") class ChatItemDeleted(val user: UserRef, val deletedChatItem: AChatItem, val toChatItem: AChatItem? = null, val byUser: Boolean): CR() + @Serializable @SerialName("chatItemsDeleted") class ChatItemsDeleted(val user: UserRef, val chatItemDeletions: List, val byUser: Boolean): CR() // group events @Serializable @SerialName("groupCreated") class GroupCreated(val user: UserRef, val groupInfo: GroupInfo): CR() @Serializable @SerialName("sentGroupInvitation") class SentGroupInvitation(val user: UserRef, val groupInfo: GroupInfo, val contact: Contact, val member: GroupMember): CR() @@ -4176,6 +4840,7 @@ sealed class CR { @Serializable @SerialName("rcvFileCancelled") class RcvFileCancelled(val user: UserRef, val chatItem_: AChatItem?, val rcvFileTransfer: RcvFileTransfer): CR() @Serializable @SerialName("rcvFileSndCancelled") class RcvFileSndCancelled(val user: UserRef, val chatItem: AChatItem, val rcvFileTransfer: RcvFileTransfer): CR() @Serializable @SerialName("rcvFileError") class RcvFileError(val user: UserRef, val chatItem_: AChatItem?, val agentError: AgentErrorType, val rcvFileTransfer: RcvFileTransfer): CR() + @Serializable @SerialName("rcvFileWarning") class RcvFileWarning(val user: UserRef, val chatItem_: AChatItem?, val agentError: AgentErrorType, val rcvFileTransfer: RcvFileTransfer): CR() // sending file events @Serializable @SerialName("sndFileStart") class SndFileStart(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() @Serializable @SerialName("sndFileComplete") class SndFileComplete(val user: UserRef, val chatItem: AChatItem, val sndFileTransfer: SndFileTransfer): CR() @@ -4188,7 +4853,8 @@ sealed class CR { @Serializable @SerialName("sndFileCompleteXFTP") class SndFileCompleteXFTP(val user: UserRef, val chatItem: AChatItem, val fileTransferMeta: FileTransferMeta): CR() @Serializable @SerialName("sndStandaloneFileComplete") class SndStandaloneFileComplete(val user: UserRef, val fileTransferMeta: FileTransferMeta, val rcvURIs: List): CR() @Serializable @SerialName("sndFileCancelledXFTP") class SndFileCancelledXFTP(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta): CR() - @Serializable @SerialName("sndFileError") class SndFileError(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta): CR() + @Serializable @SerialName("sndFileError") class SndFileError(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val errorMessage: String): CR() + @Serializable @SerialName("sndFileWarning") class SndFileWarning(val user: UserRef, val chatItem_: AChatItem?, val fileTransferMeta: FileTransferMeta, val errorMessage: String): CR() // call events @Serializable @SerialName("callInvitation") class CallInvitation(val callInvitation: RcvCallInvitation): CR() @Serializable @SerialName("callInvitations") class CallInvitations(val callInvitations: List): CR() @@ -4197,6 +4863,7 @@ sealed class CR { @Serializable @SerialName("callExtraInfo") class CallExtraInfo(val user: UserRef, val contact: Contact, val extraInfo: WebRTCExtraInfo): CR() @Serializable @SerialName("callEnded") class CallEnded(val user: UserRef, val contact: Contact): CR() @Serializable @SerialName("contactConnectionDeleted") class ContactConnectionDeleted(val user: UserRef, val connection: PendingContactConnection): CR() + @Serializable @SerialName("contactDisabled") class ContactDisabled(val user: UserRef, val contact: Contact): CR() // remote events (desktop) @Serializable @SerialName("remoteHostList") class RemoteHostList(val remoteHosts: List): CR() @Serializable @SerialName("currentRemoteHost") class CurrentRemoteHost(val remoteHost_: RemoteHostInfo?): CR() @@ -4221,8 +4888,11 @@ sealed class CR { @Serializable @SerialName("cmdOk") class CmdOk(val user: UserRef?): CR() @Serializable @SerialName("chatCmdError") class ChatCmdError(val user_: UserRef?, val chatError: ChatError): CR() @Serializable @SerialName("chatError") class ChatRespError(val user_: UserRef?, val chatError: ChatError): CR() + @Serializable @SerialName("archiveExported") class ArchiveExported(val archiveErrors: List): CR() @Serializable @SerialName("archiveImported") class ArchiveImported(val archiveErrors: List): CR() @Serializable @SerialName("appSettings") class AppSettingsR(val appSettings: AppSettings): CR() + @Serializable @SerialName("agentSubsTotal") class AgentSubsTotal(val user: UserRef, val subsTotal: SMPServerSubs, val hasSession: Boolean): CR() + @Serializable @SerialName("agentServersSummary") class AgentServersSummary(val user: UserRef, val serversSummary: PresentedServersSummary): CR() // general @Serializable class Response(val type: String, val json: String): CR() @Serializable class Invalid(val str: String): CR() @@ -4242,6 +4912,7 @@ sealed class CR { is NetworkConfig -> "networkConfig" is ContactInfo -> "contactInfo" is GroupMemberInfo -> "groupMemberInfo" + is QueueInfoR -> "queueInfo" is ContactSwitchStarted -> "contactSwitchStarted" is GroupMemberSwitchStarted -> "groupMemberSwitchStarted" is ContactSwitchAborted -> "contactSwitchAborted" @@ -4280,6 +4951,7 @@ sealed class CR { is UserContactLinkDeleted -> "userContactLinkDeleted" is ContactConnected -> "contactConnected" is ContactConnecting -> "contactConnecting" + is ContactSndReady -> "contactSndReady" is ReceivedContactRequest -> "receivedContactRequest" is AcceptingContactRequest -> "acceptingContactRequest" is ContactRequestRejected -> "contactRequestRejected" @@ -4299,7 +4971,7 @@ sealed class CR { is ChatItemUpdated -> "chatItemUpdated" is ChatItemNotChanged -> "chatItemNotChanged" is ChatItemReaction -> "chatItemReaction" - is ChatItemDeleted -> "chatItemDeleted" + is ChatItemsDeleted -> "chatItemsDeleted" is GroupCreated -> "groupCreated" is SentGroupInvitation -> "sentGroupInvitation" is UserAcceptedGroupSent -> "userAcceptedGroupSent" @@ -4345,6 +5017,7 @@ sealed class CR { is RcvFileProgressXFTP -> "rcvFileProgressXFTP" is SndFileRedirectStartXFTP -> "sndFileRedirectStartXFTP" is RcvFileError -> "rcvFileError" + is RcvFileWarning -> "rcvFileWarning" is SndFileStart -> "sndFileStart" is SndFileComplete -> "sndFileComplete" is SndFileRcvCancelled -> "sndFileRcvCancelled" @@ -4354,6 +5027,7 @@ sealed class CR { is SndStandaloneFileComplete -> "sndStandaloneFileComplete" is SndFileCancelledXFTP -> "sndFileCancelledXFTP" is SndFileError -> "sndFileError" + is SndFileWarning -> "sndFileWarning" is CallInvitations -> "callInvitations" is CallInvitation -> "callInvitation" is CallOffer -> "callOffer" @@ -4361,6 +5035,7 @@ sealed class CR { is CallExtraInfo -> "callExtraInfo" is CallEnded -> "callEnded" is ContactConnectionDeleted -> "contactConnectionDeleted" + is ContactDisabled -> "contactDisabled" is RemoteHostList -> "remoteHostList" is CurrentRemoteHost -> "currentRemoteHost" is RemoteHostStarted -> "remoteHostStarted" @@ -4378,9 +5053,12 @@ sealed class CR { is ContactPQAllowed -> "contactPQAllowed" is ContactPQEnabled -> "contactPQEnabled" is VersionInfo -> "versionInfo" + is AgentSubsTotal -> "agentSubsTotal" + is AgentServersSummary -> "agentServersSummary" is CmdOk -> "cmdOk" is ChatCmdError -> "chatCmdError" is ChatRespError -> "chatError" + is ArchiveExported -> "archiveExported" is ArchiveImported -> "archiveImported" is AppSettingsR -> "appSettings" is Response -> "* $type" @@ -4402,6 +5080,7 @@ sealed class CR { is NetworkConfig -> json.encodeToString(networkConfig) is ContactInfo -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionStats: ${json.encodeToString(connectionStats_)}") is GroupMemberInfo -> withUser(user, "group: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionStats: ${json.encodeToString(connectionStats_)}") + is QueueInfoR -> withUser(user, "rcvMsgInfo: ${json.encodeToString(rcvMsgInfo)}\nqueueInfo: ${json.encodeToString(queueInfo)}\n") is ContactSwitchStarted -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionStats: ${json.encodeToString(connectionStats)}") is GroupMemberSwitchStarted -> withUser(user, "group: ${json.encodeToString(groupInfo)}\nmember: ${json.encodeToString(member)}\nconnectionStats: ${json.encodeToString(connectionStats)}") is ContactSwitchAborted -> withUser(user, "contact: ${json.encodeToString(contact)}\nconnectionStats: ${json.encodeToString(connectionStats)}") @@ -4440,6 +5119,7 @@ sealed class CR { is UserContactLinkDeleted -> withUser(user, noDetails()) is ContactConnected -> withUser(user, json.encodeToString(contact)) is ContactConnecting -> withUser(user, json.encodeToString(contact)) + is ContactSndReady -> withUser(user, json.encodeToString(contact)) is ReceivedContactRequest -> withUser(user, json.encodeToString(contactRequest)) is AcceptingContactRequest -> withUser(user, json.encodeToString(contact)) is ContactRequestRejected -> withUser(user, noDetails()) @@ -4459,7 +5139,7 @@ sealed class CR { is ChatItemUpdated -> withUser(user, json.encodeToString(chatItem)) is ChatItemNotChanged -> withUser(user, json.encodeToString(chatItem)) is ChatItemReaction -> withUser(user, "added: $added\n${json.encodeToString(reaction)}") - is ChatItemDeleted -> withUser(user, "deletedChatItem:\n${json.encodeToString(deletedChatItem)}\ntoChatItem:\n${json.encodeToString(toChatItem)}\nbyUser: $byUser") + is ChatItemsDeleted -> withUser(user, "${chatItemDeletions.map { (deletedChatItem, toChatItem) -> "deletedChatItem: ${json.encodeToString(deletedChatItem)}\ntoChatItem: ${json.encodeToString(toChatItem)}" }} \nbyUser: $byUser") is GroupCreated -> withUser(user, json.encodeToString(groupInfo)) is SentGroupInvitation -> withUser(user, "groupInfo: $groupInfo\ncontact: $contact\nmember: $member") is UserAcceptedGroupSent -> json.encodeToString(groupInfo) @@ -4502,6 +5182,7 @@ sealed class CR { is RcvFileProgressXFTP -> withUser(user, "chatItem: ${json.encodeToString(chatItem_)}\nreceivedSize: $receivedSize\ntotalSize: $totalSize") is RcvStandaloneFileComplete -> withUser(user, targetPath) is RcvFileError -> withUser(user, "chatItem_: ${json.encodeToString(chatItem_)}\nagentError: ${agentError.string}\nrcvFileTransfer: $rcvFileTransfer") + is RcvFileWarning -> withUser(user, "chatItem_: ${json.encodeToString(chatItem_)}\nagentError: ${agentError.string}\nrcvFileTransfer: $rcvFileTransfer") is SndFileCancelled -> json.encodeToString(chatItem_) is SndStandaloneFileCreated -> noDetails() is SndFileStartXFTP -> withUser(user, json.encodeToString(chatItem)) @@ -4513,7 +5194,8 @@ sealed class CR { is SndFileCompleteXFTP -> withUser(user, json.encodeToString(chatItem)) is SndStandaloneFileComplete -> withUser(user, rcvURIs.size.toString()) is SndFileCancelledXFTP -> withUser(user, json.encodeToString(chatItem_)) - is SndFileError -> withUser(user, json.encodeToString(chatItem_)) + is SndFileError -> withUser(user, "errorMessage: ${json.encodeToString(errorMessage)}\nchatItem: ${json.encodeToString(chatItem_)}") + is SndFileWarning -> withUser(user, "errorMessage: ${json.encodeToString(errorMessage)}\nchatItem: ${json.encodeToString(chatItem_)}") is CallInvitations -> "callInvitations: ${json.encodeToString(callInvitations)}" is CallInvitation -> "contact: ${callInvitation.contact.id}\ncallType: $callInvitation.callType\nsharedKey: ${callInvitation.sharedKey ?: ""}" is CallOffer -> withUser(user, "contact: ${contact.id}\ncallType: $callType\nsharedKey: ${sharedKey ?: ""}\naskConfirmation: $askConfirmation\noffer: ${json.encodeToString(offer)}") @@ -4521,6 +5203,7 @@ sealed class CR { is CallExtraInfo -> withUser(user, "contact: ${contact.id}\nextraInfo: ${json.encodeToString(extraInfo)}") is CallEnded -> withUser(user, "contact: ${contact.id}") is ContactConnectionDeleted -> withUser(user, json.encodeToString(connection)) + is ContactDisabled -> withUser(user, json.encodeToString(contact)) // remote events (mobile) is RemoteHostList -> json.encodeToString(remoteHosts) is CurrentRemoteHost -> if (remoteHost_ == null) "local" else json.encodeToString(remoteHost_) @@ -4549,15 +5232,18 @@ sealed class CR { (if (remoteCtrl_ == null) "null" else json.encodeToString(remoteCtrl_)) + "\nsessionCode: $sessionCode" is RemoteCtrlConnected -> json.encodeToString(remoteCtrl) - is RemoteCtrlStopped -> noDetails() + is RemoteCtrlStopped -> "rcsState: $rcsState\nrcsStopReason: $rcStopReason" is ContactPQAllowed -> withUser(user, "contact: ${contact.id}\npqEncryption: $pqEncryption") is ContactPQEnabled -> withUser(user, "contact: ${contact.id}\npqEnabled: $pqEnabled") + is AgentSubsTotal -> withUser(user, "subsTotal: ${subsTotal}\nhasSession: $hasSession") + is AgentServersSummary -> withUser(user, json.encodeToString(serversSummary)) is VersionInfo -> "version ${json.encodeToString(versionInfo)}\n\n" + "chat migrations: ${json.encodeToString(chatMigrations.map { it.upName })}\n\n" + "agent migrations: ${json.encodeToString(agentMigrations.map { it.upName })}" is CmdOk -> withUser(user, noDetails()) is ChatCmdError -> withUser(user_, chatError.string) is ChatRespError -> withUser(user_, chatError.string) + is ArchiveExported -> "${archiveErrors.map { it.string } }" is ArchiveImported -> "${archiveErrors.map { it.string } }" is AppSettingsR -> json.encodeToString(appSettings) is Response -> json @@ -4577,6 +5263,19 @@ fun chatError(r: CR): ChatErrorType? { ) } +@Serializable +sealed class ChatDeleteMode { + @Serializable @SerialName("full") class Full(val notify: Boolean): ChatDeleteMode() + @Serializable @SerialName("entity") class Entity(val notify: Boolean): ChatDeleteMode() + @Serializable @SerialName("messages") class Messages: ChatDeleteMode() + + val cmdString: String get() = when (this) { + is ChatDeleteMode.Full -> "full notify=${onOff(notify)}" + is ChatDeleteMode.Entity -> "entity notify=${onOff(notify)}" + is ChatDeleteMode.Messages -> "messages" + } +} + @Serializable sealed class ConnectionPlan { @Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan() @@ -4814,7 +5513,6 @@ sealed class ChatErrorType { is GroupMemberNotActive -> "groupMemberNotActive" is GroupMemberUserRemoved -> "groupMemberUserRemoved" is GroupMemberNotFound -> "groupMemberNotFound" - is GroupMemberIntroNotFound -> "groupMemberIntroNotFound" is GroupCantResendInvitation -> "groupCantResendInvitation" is GroupInternal -> "groupInternal" is FileNotFound -> "fileNotFound" @@ -4824,15 +5522,14 @@ sealed class ChatErrorType { is FileCancel -> "fileCancel" is FileAlreadyExists -> "fileAlreadyExists" is FileRead -> "fileRead" - is FileWrite -> "fileWrite" + is FileWrite -> "fileWrite $message" is FileSend -> "fileSend" is FileRcvChunk -> "fileRcvChunk" is FileInternal -> "fileInternal" is FileImageType -> "fileImageType" is FileImageSize -> "fileImageSize" is FileNotReceived -> "fileNotReceived" - // is XFTPRcvFile -> "xftpRcvFile" - // is XFTPSndFile -> "xftpSndFile" + is FileNotApproved -> "fileNotApproved" is FallbackToSMPProhibited -> "fallbackToSMPProhibited" is InlineFileProhibited -> "inlineFileProhibited" is InvalidQuote -> "invalidQuote" @@ -4893,7 +5590,6 @@ sealed class ChatErrorType { @Serializable @SerialName("groupMemberNotActive") object GroupMemberNotActive: ChatErrorType() @Serializable @SerialName("groupMemberUserRemoved") object GroupMemberUserRemoved: ChatErrorType() @Serializable @SerialName("groupMemberNotFound") object GroupMemberNotFound: ChatErrorType() - @Serializable @SerialName("groupMemberIntroNotFound") class GroupMemberIntroNotFound(val contactName: String): ChatErrorType() @Serializable @SerialName("groupCantResendInvitation") class GroupCantResendInvitation(val groupInfo: GroupInfo, val contactName: String): ChatErrorType() @Serializable @SerialName("groupInternal") class GroupInternal(val message: String): ChatErrorType() @Serializable @SerialName("fileNotFound") class FileNotFound(val message: String): ChatErrorType() @@ -4910,8 +5606,7 @@ sealed class ChatErrorType { @Serializable @SerialName("fileImageType") class FileImageType(val filePath: String): ChatErrorType() @Serializable @SerialName("fileImageSize") class FileImageSize(val filePath: String): ChatErrorType() @Serializable @SerialName("fileNotReceived") class FileNotReceived(val fileId: Long): ChatErrorType() - // @Serializable @SerialName("xFTPRcvFile") object XFTPRcvFile: ChatErrorType() - // @Serializable @SerialName("xFTPSndFile") object XFTPSndFile: ChatErrorType() + @Serializable @SerialName("fileNotApproved") class FileNotApproved(val fileId: Long, val unknownServers: List): ChatErrorType() @Serializable @SerialName("fallbackToSMPProhibited") class FallbackToSMPProhibited(val fileId: Long): ChatErrorType() @Serializable @SerialName("inlineFileProhibited") class InlineFileProhibited(val fileId: Long): ChatErrorType() @Serializable @SerialName("invalidQuote") object InvalidQuote: ChatErrorType() @@ -5076,7 +5771,7 @@ sealed class DatabaseError { @Serializable sealed class SQLiteError { @Serializable @SerialName("errorNotADatabase") object ErrorNotADatabase: SQLiteError() - @Serializable @SerialName("error") class Error(val error: String): SQLiteError() + @Serializable @SerialName("error") class Error(val dbError: String): SQLiteError() } @Serializable @@ -5087,6 +5782,7 @@ sealed class AgentErrorType { is SMP -> "SMP ${smpErr.string}" // is NTF -> "NTF ${ntfErr.string}" is XFTP -> "XFTP ${xftpErr.string}" + is PROXY -> "PROXY $proxyServer $relayServer ${proxyErr.string}" is RCP -> "RCP ${rcpErr.string}" is BROKER -> "BROKER ${brokerErr.string}" is AGENT -> "AGENT ${agentErr.string}" @@ -5096,9 +5792,10 @@ sealed class AgentErrorType { } @Serializable @SerialName("CMD") class CMD(val cmdErr: CommandErrorType): AgentErrorType() @Serializable @SerialName("CONN") class CONN(val connErr: ConnectionErrorType): AgentErrorType() - @Serializable @SerialName("SMP") class SMP(val smpErr: SMPErrorType): AgentErrorType() + @Serializable @SerialName("SMP") class SMP(val serverAddress: String, val smpErr: SMPErrorType): AgentErrorType() // @Serializable @SerialName("NTF") class NTF(val ntfErr: SMPErrorType): AgentErrorType() @Serializable @SerialName("XFTP") class XFTP(val xftpErr: XFTPErrorType): AgentErrorType() + @Serializable @SerialName("PROXY") class PROXY(val proxyServer: String, val relayServer: String, val proxyErr: ProxyClientError): AgentErrorType() @Serializable @SerialName("RCP") class RCP(val rcpErr: RCErrorType): AgentErrorType() @Serializable @SerialName("BROKER") class BROKER(val brokerAddress: String, val brokerErr: BrokerErrorType): AgentErrorType() @Serializable @SerialName("AGENT") class AGENT(val agentErr: SMPAgentError): AgentErrorType() @@ -5163,22 +5860,42 @@ sealed class SMPErrorType { is BLOCK -> "BLOCK" is SESSION -> "SESSION" is CMD -> "CMD ${cmdErr.string}" + is PROXY -> "PROXY ${proxyErr.string}" is AUTH -> "AUTH" + is CRYPTO -> "CRYPTO" is QUOTA -> "QUOTA" is NO_MSG -> "NO_MSG" is LARGE_MSG -> "LARGE_MSG" + is EXPIRED -> "EXPIRED" is INTERNAL -> "INTERNAL" } @Serializable @SerialName("BLOCK") class BLOCK: SMPErrorType() @Serializable @SerialName("SESSION") class SESSION: SMPErrorType() @Serializable @SerialName("CMD") class CMD(val cmdErr: ProtocolCommandError): SMPErrorType() + @Serializable @SerialName("PROXY") class PROXY(val proxyErr: ProxyError): SMPErrorType() @Serializable @SerialName("AUTH") class AUTH: SMPErrorType() + @Serializable @SerialName("CRYPTO") class CRYPTO: SMPErrorType() @Serializable @SerialName("QUOTA") class QUOTA: SMPErrorType() @Serializable @SerialName("NO_MSG") class NO_MSG: SMPErrorType() @Serializable @SerialName("LARGE_MSG") class LARGE_MSG: SMPErrorType() + @Serializable @SerialName("EXPIRED") class EXPIRED: SMPErrorType() @Serializable @SerialName("INTERNAL") class INTERNAL: SMPErrorType() } +@Serializable +sealed class ProxyError { + val string: String get() = when (this) { + is PROTOCOL -> "PROTOCOL ${protocolErr.string}" + is BROKER -> "BROKER ${brokerErr.string}" + is BASIC_AUTH -> "BASIC_AUTH" + is NO_SESSION -> "NO_SESSION" + } + @Serializable @SerialName("PROTOCOL") class PROTOCOL(val protocolErr: SMPErrorType): ProxyError() + @Serializable @SerialName("BROKER") class BROKER(val brokerErr: BrokerErrorType): ProxyError() + @Serializable @SerialName("BASIC_AUTH") class BASIC_AUTH: ProxyError() + @Serializable @SerialName("NO_SESSION") class NO_SESSION: ProxyError() +} + @Serializable sealed class ProtocolCommandError { val string: String get() = when (this) { @@ -5201,12 +5918,14 @@ sealed class ProtocolCommandError { sealed class SMPTransportError { val string: String get() = when (this) { is BadBlock -> "badBlock" + is Version -> "version" is LargeMsg -> "largeMsg" is BadSession -> "badSession" is NoServerAuth -> "noServerAuth" is Handshake -> "handshake ${handshakeErr.string}" } @Serializable @SerialName("badBlock") class BadBlock: SMPTransportError() + @Serializable @SerialName("version") class Version: SMPTransportError() @Serializable @SerialName("largeMsg") class LargeMsg: SMPTransportError() @Serializable @SerialName("badSession") class BadSession: SMPTransportError() @Serializable @SerialName("noServerAuth") class NoServerAuth: SMPTransportError() @@ -5279,6 +5998,18 @@ sealed class XFTPErrorType { @Serializable @SerialName("INTERNAL") object INTERNAL: XFTPErrorType() } +@Serializable +sealed class ProxyClientError { + val string: String get() = when (this) { + is ProxyProtocolError -> "ProxyProtocolError $protocolErr" + is ProxyUnexpectedResponse -> "ProxyUnexpectedResponse $responseStr" + is ProxyResponseError -> "ProxyResponseError $responseErr" + } + @Serializable @SerialName("protocolError") class ProxyProtocolError(val protocolErr: SMPErrorType): ProxyClientError() + @Serializable @SerialName("unexpectedResponse") class ProxyUnexpectedResponse(val responseStr: String): ProxyClientError() + @Serializable @SerialName("responseError") class ProxyResponseError(val responseErr: SMPErrorType): ProxyClientError() +} + @Serializable sealed class RCErrorType { val string: String get() = when (this) { @@ -5318,11 +6049,11 @@ sealed class RCErrorType { @Serializable sealed class ArchiveError { val string: String get() = when (this) { - is ArchiveErrorImport -> "import ${chatError.string}" - is ArchiveErrorImportFile -> "importFile $file ${chatError.string}" + is ArchiveErrorImport -> "import ${importError}" + is ArchiveErrorFile -> "importFile $file ${fileError}" } - @Serializable @SerialName("import") class ArchiveErrorImport(val chatError: ChatError): ArchiveError() - @Serializable @SerialName("importFile") class ArchiveErrorImportFile(val file: String, val chatError: ChatError): ArchiveError() + @Serializable @SerialName("import") class ArchiveErrorImport(val importError: String): ArchiveError() + @Serializable @SerialName("fileError") class ArchiveErrorFile(val file: String, val fileError: String): ArchiveError() } @Serializable @@ -5408,11 +6139,13 @@ enum class NotificationsMode() { data class AppSettings( var networkConfig: NetCfg? = null, var privacyEncryptLocalFiles: Boolean? = null, + var privacyAskToApproveRelays: Boolean? = null, var privacyAcceptImages: Boolean? = null, var privacyLinkPreviews: Boolean? = null, var privacyShowChatPreviews: Boolean? = null, var privacySaveLastDraft: Boolean? = null, var privacyProtectScreen: Boolean? = null, + var privacyMediaBlurRadius: Int? = null, var notificationMode: AppSettingsNotificationMode? = null, var notificationPreviewMode: AppSettingsNotificationPreviewMode? = null, var webrtcPolicyRelay: Boolean? = null, @@ -5425,17 +6158,25 @@ data class AppSettings( var androidCallOnLockScreen: AppSettingsLockScreenCalls? = null, var iosCallKitEnabled: Boolean? = null, var iosCallKitCallsInRecents: Boolean? = null, + var uiProfileImageCornerRadius: Float? = null, + var uiColorScheme: String? = null, + var uiDarkColorScheme: String? = null, + var uiCurrentThemeIds: Map? = null, + var uiThemes: List? = null, + var oneHandUI: Boolean? = null ) { fun prepareForExport(): AppSettings { val empty = AppSettings() val def = defaults if (networkConfig != def.networkConfig) { empty.networkConfig = networkConfig } if (privacyEncryptLocalFiles != def.privacyEncryptLocalFiles) { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles } + if (privacyAskToApproveRelays != def.privacyAskToApproveRelays) { empty.privacyAskToApproveRelays = privacyAskToApproveRelays } if (privacyAcceptImages != def.privacyAcceptImages) { empty.privacyAcceptImages = privacyAcceptImages } if (privacyLinkPreviews != def.privacyLinkPreviews) { empty.privacyLinkPreviews = privacyLinkPreviews } if (privacyShowChatPreviews != def.privacyShowChatPreviews) { empty.privacyShowChatPreviews = privacyShowChatPreviews } if (privacySaveLastDraft != def.privacySaveLastDraft) { empty.privacySaveLastDraft = privacySaveLastDraft } if (privacyProtectScreen != def.privacyProtectScreen) { empty.privacyProtectScreen = privacyProtectScreen } + if (privacyMediaBlurRadius != def.privacyMediaBlurRadius) { empty.privacyMediaBlurRadius = privacyMediaBlurRadius } if (notificationMode != def.notificationMode) { empty.notificationMode = notificationMode } if (notificationPreviewMode != def.notificationPreviewMode) { empty.notificationPreviewMode = notificationPreviewMode } if (webrtcPolicyRelay != def.webrtcPolicyRelay) { empty.webrtcPolicyRelay = webrtcPolicyRelay } @@ -5448,6 +6189,12 @@ data class AppSettings( if (androidCallOnLockScreen != def.androidCallOnLockScreen) { empty.androidCallOnLockScreen = androidCallOnLockScreen } if (iosCallKitEnabled != def.iosCallKitEnabled) { empty.iosCallKitEnabled = iosCallKitEnabled } if (iosCallKitCallsInRecents != def.iosCallKitCallsInRecents) { empty.iosCallKitCallsInRecents = iosCallKitCallsInRecents } + if (uiProfileImageCornerRadius != def.uiProfileImageCornerRadius) { empty.uiProfileImageCornerRadius = uiProfileImageCornerRadius } + if (uiColorScheme != def.uiColorScheme) { empty.uiColorScheme = uiColorScheme } + if (uiDarkColorScheme != def.uiDarkColorScheme) { empty.uiDarkColorScheme = uiDarkColorScheme } + if (uiCurrentThemeIds != def.uiCurrentThemeIds) { empty.uiCurrentThemeIds = uiCurrentThemeIds } + if (uiThemes != def.uiThemes) { empty.uiThemes = uiThemes } + if (oneHandUI != def.oneHandUI) { empty.oneHandUI = oneHandUI } return empty } @@ -5462,11 +6209,13 @@ data class AppSettings( setNetCfg(net) } privacyEncryptLocalFiles?.let { def.privacyEncryptLocalFiles.set(it) } + privacyAskToApproveRelays?.let { def.privacyAskToApproveRelays.set(it) } privacyAcceptImages?.let { def.privacyAcceptImages.set(it) } privacyLinkPreviews?.let { def.privacyLinkPreviews.set(it) } privacyShowChatPreviews?.let { def.privacyShowChatPreviews.set(it) } privacySaveLastDraft?.let { def.privacySaveLastDraft.set(it) } privacyProtectScreen?.let { def.privacyProtectScreen.set(it) } + privacyMediaBlurRadius?.let { def.privacyMediaBlurRadius.set(it) } notificationMode?.let { def.notificationsMode.set(it.toNotificationsMode()) } notificationPreviewMode?.let { def.notificationPreviewMode.set(it.toNotificationPreviewMode().name) } webrtcPolicyRelay?.let { def.webrtcPolicyRelay.set(it) } @@ -5479,6 +6228,12 @@ data class AppSettings( androidCallOnLockScreen?.let { def.callOnLockScreen.set(it.toCallOnLockScreen()) } iosCallKitEnabled?.let { def.iosCallKitEnabled.set(it) } iosCallKitCallsInRecents?.let { def.iosCallKitCallsInRecents.set(it) } + uiProfileImageCornerRadius?.let { def.profileImageCornerRadius.set(it) } + uiColorScheme?.let { def.currentTheme.set(it) } + uiDarkColorScheme?.let { def.systemDarkTheme.set(it) } + uiCurrentThemeIds?.let { def.currentThemeIds.set(it) } + uiThemes?.let { def.themeOverrides.set(it.skipDuplicates()) } + oneHandUI?.let { def.oneHandUI.set(if (appPlatform.isAndroid) it else false) } } companion object { @@ -5486,11 +6241,13 @@ data class AppSettings( get() = AppSettings( networkConfig = NetCfg.defaults, privacyEncryptLocalFiles = true, + privacyAskToApproveRelays = true, privacyAcceptImages = true, privacyLinkPreviews = true, privacyShowChatPreviews = true, privacySaveLastDraft = true, privacyProtectScreen = false, + privacyMediaBlurRadius = 0, notificationMode = AppSettingsNotificationMode.INSTANT, notificationPreviewMode = AppSettingsNotificationPreviewMode.MESSAGE, webrtcPolicyRelay = true, @@ -5502,7 +6259,13 @@ data class AppSettings( confirmDBUpgrades = false, androidCallOnLockScreen = AppSettingsLockScreenCalls.SHOW, iosCallKitEnabled = true, - iosCallKitCallsInRecents = false + iosCallKitCallsInRecents = false, + uiProfileImageCornerRadius = 22.5f, + uiColorScheme = DefaultTheme.SYSTEM_THEME_NAME, + uiDarkColorScheme = DefaultTheme.SIMPLEX.themeName, + uiCurrentThemeIds = null, + uiThemes = null, + oneHandUI = true ) val current: AppSettings @@ -5511,11 +6274,13 @@ data class AppSettings( return defaults.copy( networkConfig = getNetCfg(), privacyEncryptLocalFiles = def.privacyEncryptLocalFiles.get(), + privacyAskToApproveRelays = def.privacyAskToApproveRelays.get(), privacyAcceptImages = def.privacyAcceptImages.get(), privacyLinkPreviews = def.privacyLinkPreviews.get(), privacyShowChatPreviews = def.privacyShowChatPreviews.get(), privacySaveLastDraft = def.privacySaveLastDraft.get(), privacyProtectScreen = def.privacyProtectScreen.get(), + privacyMediaBlurRadius = def.privacyMediaBlurRadius.get(), notificationMode = AppSettingsNotificationMode.from(def.notificationsMode.get()), notificationPreviewMode = AppSettingsNotificationPreviewMode.from(NotificationPreviewMode.valueOf(def.notificationPreviewMode.get()!!)), webrtcPolicyRelay = def.webrtcPolicyRelay.get(), @@ -5528,6 +6293,12 @@ data class AppSettings( androidCallOnLockScreen = AppSettingsLockScreenCalls.from(def.callOnLockScreen.get()), iosCallKitEnabled = def.iosCallKitEnabled.get(), iosCallKitCallsInRecents = def.iosCallKitCallsInRecents.get(), + uiProfileImageCornerRadius = def.profileImageCornerRadius.get(), + uiColorScheme = def.currentTheme.get() ?: DefaultTheme.SYSTEM_THEME_NAME, + uiDarkColorScheme = def.systemDarkTheme.get() ?: DefaultTheme.SIMPLEX.themeName, + uiCurrentThemeIds = def.currentThemeIds.get(), + uiThemes = def.themeOverrides.get(), + oneHandUI = def.oneHandUI.get() ) } } @@ -5628,3 +6399,52 @@ enum class UserNetworkType { OTHER -> generalGetString(MR.strings.network_type_other) } } + +@Serializable +data class RcvMsgInfo ( + val msgId: Long, + val msgDeliveryId: Long, + val msgDeliveryStatus: String, + val agentMsgId: Long, + val agentMsgMeta: String +) + +@Serializable +data class QueueInfo ( + val qiSnd: Boolean, + val qiNtf: Boolean, + val qiSub: QSub? = null, + val qiSize: Int, + val qiMsg: MsgInfo? = null +) + +@Serializable +data class QSub ( + val qSubThread: QSubThread, + val qDelivered: String? = null +) + +enum class QSubThread { + @SerialName("noSub") + NO_SUB, + @SerialName("subPending") + SUB_PENDING, + @SerialName("subThread") + SUB_THREAD, + @SerialName("prohibitSub") + PROHIBIT_SUB +} + +@Serializable +data class MsgInfo ( + val msgId: String, + val msgTs: Instant, + val msgType: MsgType, +) + +enum class MsgType { + @SerialName("message") + MESSAGE, + @SerialName("quota") + QUOTA +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/other/wheel-picker/FWheelPickerDefault.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/other/wheel-picker/FWheelPickerDefault.kt index 9760e9c9f2..052e388f97 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/other/wheel-picker/FWheelPickerDefault.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/other/wheel-picker/FWheelPickerDefault.kt @@ -2,7 +2,6 @@ package com.sd.lib.compose.wheel_picker import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background -import chat.simplex.common.ui.theme.isSystemInDarkTheme import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -12,6 +11,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import chat.simplex.common.ui.theme.isInDarkTheme /** * The default implementation of focus view in vertical. @@ -76,7 +76,7 @@ fun FWheelPickerFocusHorizontal( */ private val DefaultDividerColor: Color @Composable - get() = (if (isSystemInDarkTheme()) { + get() = (if (isInDarkTheme()) { Color.White } else { Color.Black diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt index 7d5b1b0196..60a65eaac6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/AppCommon.kt @@ -3,7 +3,8 @@ package chat.simplex.common.platform import chat.simplex.common.BuildConfigCommon import chat.simplex.common.model.* import chat.simplex.common.ui.theme.DefaultTheme -import java.io.File +import chat.simplex.common.views.helpers.generalGetString +import chat.simplex.res.MR import java.util.* enum class AppPlatform { @@ -20,6 +21,8 @@ expect val appPlatform: AppPlatform expect val deviceName: String +expect fun isAppVisibleAndFocused(): Boolean + val appVersionInfo: Pair = if (appPlatform == AppPlatform.ANDROID) BuildConfigCommon.ANDROID_VERSION_NAME to BuildConfigCommon.ANDROID_VERSION_CODE else @@ -42,6 +45,12 @@ fun runMigrations() { ChatController.appPrefs.currentTheme.set(DefaultTheme.SIMPLEX.name) } lastMigration.set(117) + } else if (lastMigration.get() < 203) { + // Moving to a different key for storing themes as a List + val oldOverrides = ChatController.appPrefs.themeOverridesOld.get().values.toList() + ChatController.appPrefs.themeOverrides.set(oldOverrides) + ChatController.appPrefs.currentThemeIds.set(oldOverrides.associate { it.base.themeName to it.themeId }) + lastMigration.set(203) } else { lastMigration.set(BuildConfigCommon.ANDROID_VERSION_CODE) break @@ -49,3 +58,16 @@ fun runMigrations() { } } } + +enum class AppUpdatesChannel { + DISABLED, + STABLE, + BETA; + + val text: String + get() = when (this) { + DISABLED -> generalGetString(MR.strings.app_check_for_updates_disabled) + STABLE -> generalGetString(MR.strings.app_check_for_updates_stable) + BETA -> generalGetString(MR.strings.app_check_for_updates_beta) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt index 0d447a4a5a..57b93d4d6e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Core.kt @@ -1,6 +1,7 @@ package chat.simplex.common.platform import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.currentUser import chat.simplex.common.views.helpers.* @@ -43,14 +44,13 @@ val appPreferences: AppPreferences val chatController: ChatController = ChatController -fun initChatControllerAndRunMigrations() { +fun initChatControllerOnStart() { withLongRunningApi { if (appPreferences.chatStopped.get() && appPreferences.storeDBPassphrase.get() && ksDatabasePassword.get() != null) { initChatController(startChat = ::showStartChatAfterRestartAlert) } else { initChatController() } - runMigrations() } } @@ -58,6 +58,9 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat try { if (chatModel.ctrlInitInProgress.value) return chatModel.ctrlInitInProgress.value = true + if (!appPrefs.storeDBPassphrase.get() && !appPrefs.initialRandomDBPassphrase.get()) { + ksDatabasePassword.remove() + } val dbKey = useKey ?: DatabaseUtils.useDatabaseKey() val confirm = confirmMigrations ?: if (appPreferences.developerTools.get() && appPreferences.confirmDBUpgrades.get()) MigrationConfirmation.Error else MigrationConfirmation.YesUp var migrated: Array = chatMigrateInit(dbAbsolutePrefixPath, dbKey, MigrationConfirmation.Error.value) @@ -85,14 +88,31 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat chatModel.chatDbStatus.value = res if (res != DBMigrationResult.OK) { Log.d(TAG, "Unable to migrate successfully: $res") + if (!appPrefs.newDatabaseInitialized.get() && DatabaseUtils.hasOnlyOneDatabase(dataDir.absolutePath)) { + if (chatModel.incompleteInitializedDbRemoved.value) { + Log.d(TAG, "Incomplete initialized databases were removed but after repeated migration only one database exists again, not trying to remove again") + } else { + val dbPath = dbAbsolutePrefixPath + File(dbPath + "_chat.db").delete() + File(dbPath + "_agent.db").delete() + chatModel.incompleteInitializedDbRemoved.value = true + Log.d(TAG, "Incomplete initialized databases were removed for the first time, repeating migration") + chatModel.ctrlInitInProgress.value = false + initChatController(useKey, confirmMigrations, startChat) + } + } return } + appPrefs.newDatabaseInitialized.set(true) + chatModel.incompleteInitializedDbRemoved.value = false platform.androidRestartNetworkObserver() - controller.apiSetTempFolder(coreTmpDir.absolutePath) - controller.apiSetFilesFolder(appFilesDir.absolutePath) - if (appPlatform.isDesktop) { - controller.apiSetRemoteHostsFolder(remoteHostsDir.absolutePath) - } + controller.apiSetAppFilePaths( + appFilesDir.absolutePath, + coreTmpDir.absolutePath, + wallpapersDir.parentFile.absolutePath, + remoteHostsDir.absolutePath, + ctrl + ) controller.apiSetEncryptLocalFiles(controller.appPrefs.privacyEncryptLocalFiles.get()) // If we migrated successfully means previous re-encryption process on database level finished successfully too if (appPreferences.encryptionStartedAt.get() != null) appPreferences.encryptionStartedAt.set(null) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt index c788a6902e..9110987190 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Files.kt @@ -1,10 +1,12 @@ package chat.simplex.common.platform import androidx.compose.runtime.Composable -import chat.simplex.common.model.CIFile -import chat.simplex.common.model.CryptoFile +import chat.simplex.common.model.* +import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.generalGetString import chat.simplex.res.MR +import com.charleskorn.kaml.* +import kotlinx.serialization.encodeToString import java.io.* import java.net.URI import java.net.URLDecoder @@ -14,8 +16,10 @@ expect val dataDir: File expect val tmpDir: File expect val filesDir: File expect val appFilesDir: File +expect val wallpapersDir: File expect val coreTmpDir: File expect val dbAbsolutePrefixPath: String +expect val preferencesDir: File expect val chatDatabaseFileName: String expect val agentDatabaseFileName: String @@ -30,6 +34,8 @@ expect val remoteHostsDir: File expect fun desktopOpenDatabaseDir() +expect fun desktopOpenDir(dir: File) + fun createURIFromPath(absolutePath: String): URI = URI.create(URLEncoder.encode(absolutePath, "UTF-8")) fun URI.toFile(): File = File(URLDecoder.decode(rawPath, "UTF-8").removePrefix("file:")) @@ -78,6 +84,20 @@ fun getAppFilePath(fileName: String): String { } } +fun getWallpaperFilePath(fileName: String): String { + val rh = chatModel.currentRemoteHost.value + val s = File.separator + val path = if (rh == null) { + wallpapersDir.absolutePath + s + fileName + } else { + remoteHostsDir.absolutePath + s + rh.storePath + s + "simplex_v1_assets" + s + "wallpapers" + s + fileName + } + File(path).parentFile.mkdirs() + return path +} + +fun getPreferenceFilePath(fileName: String = "themes.yaml"): String = preferencesDir.absolutePath + File.separator + fileName + fun getLoadedFilePath(file: CIFile?): String? { val f = file?.fileSource?.filePath return if (f != null && file.loaded) { @@ -98,6 +118,42 @@ fun getLoadedFileSource(file: CIFile?): CryptoFile? { } } +fun readThemeOverrides(): List { + return try { + val file = File(getPreferenceFilePath("themes.yaml")) + if (!file.exists()) return emptyList() + + file.inputStream().use { + val map = yaml.parseToYamlNode(it).yamlMap + val list = map.get("themes") + val res = ArrayList() + list?.items?.forEach { + try { + res.add(yaml.decodeFromYamlNode(ThemeOverrides.serializer(), it)) + } catch (e: Throwable) { + Log.e(TAG, "Error while reading specific theme: ${e.stackTraceToString()}") + } + } + res.skipDuplicates() + } + } catch (e: Throwable) { + Log.e(TAG, "Error while reading themes file: ${e.stackTraceToString()}") + emptyList() + } +} + +fun writeThemeOverrides(overrides: List): Boolean = + try { + File(getPreferenceFilePath("themes.yaml")).outputStream().use { + val string = yaml.encodeToString(ThemesFile(themes = overrides)) + it.bufferedWriter().use { it.write(string) } + } + true + } catch (e: Throwable) { + Log.e(TAG, "Error while writing themes file: ${e.stackTraceToString()}") + false + } + private fun fileReady(file: CIFile, filePath: String) = File(filePath).exists() && CIFile.cachedRemoteFileRequests[file.fileSource] != false diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt index 4a10027746..6683ea7d33 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Modifier.kt @@ -1,8 +1,17 @@ package chat.simplex.common.platform -import androidx.compose.runtime.Composable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.BlurredEdgeTreatment +import androidx.compose.ui.draw.blur import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.views.helpers.KeyChangeEffect +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.filter import java.io.File expect fun Modifier.navigationBarsWithImePadding(): Modifier @@ -25,3 +34,68 @@ expect fun Modifier.desktopOnExternalDrag( expect fun Modifier.onRightClick(action: () -> Unit): Modifier expect fun Modifier.desktopPointerHoverIconHand(): Modifier + +expect fun Modifier.desktopOnHovered(action: (Boolean) -> Unit): Modifier + +@Composable +fun Modifier.desktopModifyBlurredState(enabled: Boolean, blurred: MutableState, showMenu: State,): Modifier { + val blurRadius = remember { appPrefs.privacyMediaBlurRadius.state } + if (appPlatform.isDesktop) { + KeyChangeEffect(blurRadius.value) { + blurred.value = enabled && blurRadius.value > 0 + } + } + return if (appPlatform.isDesktop && enabled && blurRadius.value > 0 && !showMenu.value) { + var job: Job = remember { Job() } + LaunchedEffect(Unit) { + // The approach here is to allow menu to show up and to not blur the view. When menu is shown and mouse is hovering, + // unhovered action is still received, but we don't need to handle it until menu closes. When it closes, it takes one frame to catch a + // hover action again and if: + // 1. mouse is still on the view, the hover action will cancel this coroutine and the view will stay unblurred + // 2. mouse is not on the view, the view will become blurred after 100 ms + job = launch { + delay(100) + blurred.value = true + } + } + this then Modifier.desktopOnHovered { hovered -> + job.cancel() + blurred.value = !hovered && !showMenu.value + } + } else { + this + } +} + +@Composable +fun Modifier.privacyBlur( + enabled: Boolean, + blurred: MutableState = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) }, + scrollState: State, + onLongClick: () -> Unit = {} +): Modifier { + val blurRadius = remember { appPrefs.privacyMediaBlurRadius.state } + return if (enabled && blurred.value) { + this then Modifier.blur( + radiusX = remember { appPrefs.privacyMediaBlurRadius.state }.value.dp, + radiusY = remember { appPrefs.privacyMediaBlurRadius.state }.value.dp, + edgeTreatment = BlurredEdgeTreatment(RoundedCornerShape(0.dp)) + ) + .combinedClickable( + onLongClick = onLongClick, + onClick = { + blurred.value = false + } + ) + } else if (enabled && blurRadius.value > 0 && appPlatform.isAndroid) { + LaunchedEffect(Unit) { + snapshotFlow { scrollState.value } + .filter { it } + .filter { !blurred.value } + .collect { blurred.value = true } + } + this + } else { + this + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt index 57c1e578ae..e7c653e1b9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt @@ -96,6 +96,7 @@ abstract class NtfManager { abstract fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean abstract fun hasNotificationsForChat(chatId: String): Boolean abstract fun cancelNotificationsForChat(chatId: String) + abstract fun cancelNotificationsForUser(userId: Long) abstract fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List Unit>> = emptyList()) abstract fun cancelCallNotification() abstract fun cancelAllNotifications() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt index f61c5bc83e..44fcddb54c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Platform.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.ScrollState import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import chat.simplex.common.model.ChatId import chat.simplex.common.model.NotificationsMode import kotlinx.coroutines.Job @@ -20,15 +21,14 @@ interface PlatformInterface { fun androidChatInitializedAndStarted() {} fun androidIsBackgroundCallAllowed(): Boolean = true fun androidSetNightModeIfSupported() {} + fun androidSetStatusAndNavBarColors(isLight: Boolean, backgroundColor: Color, hasTop: Boolean, hasBottom: Boolean) {} fun androidStartCallActivity(acceptCall: Boolean, remoteHostId: Long? = null, chatId: ChatId? = null) {} fun androidPictureInPictureAllowed(): Boolean = true fun androidCallEnded() {} fun androidRestartNetworkObserver() {} @Composable fun androidLockPortraitOrientation() {} suspend fun androidAskToAllowBackgroundCalls(): Boolean = true - @Composable fun desktopScrollBarComponents(): Triple, Modifier, MutableState> = remember { Triple(Animatable(0f), Modifier, mutableStateOf(Job())) } - @Composable fun desktopScrollBar(state: LazyListState, modifier: Modifier, scrollBarAlpha: Animatable, scrollJob: MutableState, reversed: Boolean) {} - @Composable fun desktopScrollBar(state: ScrollState, modifier: Modifier, scrollBarAlpha: Animatable, scrollJob: MutableState, reversed: Boolean) {} + @Composable fun desktopShowAppUpdateNotice() {} } /** * Multiplatform project has separate directories per platform + common directory that contains directories per platform + common for all of them. diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt index af47f9c3e0..1dff386684 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/PlatformTextField.kt @@ -14,6 +14,8 @@ expect fun PlatformTextField( textStyle: MutableState, showDeleteTextButton: MutableState, userIsObserver: Boolean, + placeholder: String, + showVoiceButton: Boolean, onMessageChange: (String) -> Unit, onUpArrow: () -> Unit, onFilesPasted: (List) -> Unit, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt index 1e902b5d88..fd1824d5b6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/RecAndPlay.kt @@ -16,13 +16,25 @@ interface RecorderInterface { expect class RecorderNative(): RecorderInterface +enum class TrackState { + PLAYING, PAUSED, REPLACED +} + +data class CurrentlyPlayingState( + val fileSource: CryptoFile, + val onProgressUpdate: (position: Int?, state: TrackState) -> Unit, + val smallView: Boolean, +) + interface AudioPlayerInterface { + val currentlyPlaying: MutableState fun play( fileSource: CryptoFile, audioPlaying: MutableState, progress: MutableState, duration: MutableState, resetOnEnd: Boolean, + smallView: Boolean, ) fun stop() fun stop(item: ChatItem) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Resources.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Resources.kt index 2ee668fb23..fd712624bd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Resources.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/Resources.kt @@ -1,11 +1,13 @@ package chat.simplex.common.platform import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import com.russhwolf.settings.Settings +import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.StringResource @Composable @@ -28,6 +30,11 @@ expect fun windowOrientation(): WindowOrientation @Composable expect fun windowWidth(): Dp +@Composable +expect fun windowHeight(): Dp + expect fun desktopExpandWindowToWidth(width: Dp) expect fun isRtl(text: CharSequence): Boolean + +expect fun ImageResource.toComposeImageBitmap(): ImageBitmap? diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/ScrollableColumn.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/ScrollableColumn.kt index d4fa2fe125..532bfddfcf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/ScrollableColumn.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/ScrollableColumn.kt @@ -13,7 +13,7 @@ import androidx.compose.ui.unit.dp @Composable expect fun LazyColumnWithScrollBar( modifier: Modifier = Modifier, - state: LazyListState = rememberLazyListState(), + state: LazyListState? = null, contentPadding: PaddingValues = PaddingValues(0.dp), reverseLayout: Boolean = false, verticalArrangement: Arrangement.Vertical = @@ -29,6 +29,8 @@ expect fun ColumnWithScrollBar( modifier: Modifier = Modifier, verticalArrangement: Arrangement.Vertical = Arrangement.Top, horizontalAlignment: Alignment.Horizontal = Alignment.Start, - state: ScrollState = rememberScrollState(), + state: ScrollState? = null, + // set true when you want to show something in the center with respected .fillMaxSize() + maxIntrinsicSize: Boolean = false, content: @Composable ColumnScope.() -> Unit ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Color.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Color.kt index 5eeedbb2a0..c50ea5c349 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Color.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Color.kt @@ -31,17 +31,6 @@ val WarningOrange = Color(255, 127, 0, 255) val WarningYellow = Color(255, 192, 0, 255) val FileLight = Color(183, 190, 199, 255) val FileDark = Color(101, 101, 106, 255) -val SentMessageColor = Color(0x1E45B8FF) val MenuTextColor: Color @Composable get () = if (isInDarkTheme()) LocalContentColor.current.copy(alpha = 0.8f) else Color.Black -val NoteFolderIconColor: Color @Composable get() = with(CurrentColors.collectAsState().value.appColors.sentMessage) { - // Default color looks too light and better to have it here a little bit brighter - if (alpha == SentMessageColor.alpha) { - copy(min(SentMessageColor.alpha + 0.1f, 1f)) - } else { - // Color is non-standard and theme maker can choose color without alpha at all since the theme bound to dark/light variant, - // and it shouldn't be universal - this - } -} - +val NoteFolderIconColor: Color @Composable get() = MaterialTheme.appColors.primaryVariant2 diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt index 62acc13bfe..595c22e3e2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt @@ -6,53 +6,137 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.* -import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp import chat.simplex.common.model.ChatController -import chat.simplex.common.platform.isInNightMode +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.ThemeManager.colorFromReadableHex +import chat.simplex.common.ui.theme.ThemeManager.toReadableHex import chat.simplex.common.views.helpers.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import chat.simplex.res.MR +import kotlinx.serialization.Transient +import java.util.UUID enum class DefaultTheme { - SYSTEM, LIGHT, DARK, SIMPLEX; + LIGHT, DARK, SIMPLEX, BLACK; - // Call it only with base theme, not SYSTEM - fun hasChangedAnyColor(colors: Colors, appColors: AppColors): Boolean { - val palette = when (this) { - SYSTEM -> return false - LIGHT -> LightColorPalette - DARK -> DarkColorPalette - SIMPLEX -> SimplexColorPalette - } - val appPalette = when (this) { - SYSTEM -> return false - LIGHT -> LightColorPaletteApp - DARK -> DarkColorPaletteApp - SIMPLEX -> SimplexColorPaletteApp - } - return colors.primary != palette.primary || - colors.primaryVariant != palette.primaryVariant || - colors.secondary != palette.secondary || - colors.secondaryVariant != palette.secondaryVariant || - colors.background != palette.background || - colors.surface != palette.surface || - appColors != appPalette + companion object { + const val SYSTEM_THEME_NAME: String = "SYSTEM" + } + + val themeName: String + get() = name + + val mode: DefaultThemeMode get() = if (this == LIGHT) DefaultThemeMode.LIGHT else DefaultThemeMode.DARK + + fun hasChangedAnyColor(overrides: ThemeOverrides?): Boolean { + if (overrides == null) return false + return overrides.colors != ThemeColors() || + overrides.wallpaper != null && (overrides.wallpaper.background != null || overrides.wallpaper.tint != null) } } -data class AppColors( - val title: Color, - val sentMessage: Color, - val receivedMessage: Color -) +@Serializable +enum class DefaultThemeMode { + @SerialName("light") LIGHT, + @SerialName("dark") DARK +} + +@Stable +class AppColors( + title: Color, + primaryVariant2: Color, + sentMessage: Color, + sentQuote: Color, + receivedMessage: Color, + receivedQuote: Color, +) { + var title by mutableStateOf(title, structuralEqualityPolicy()) + internal set + var primaryVariant2 by mutableStateOf(primaryVariant2, structuralEqualityPolicy()) + internal set + var sentMessage by mutableStateOf(sentMessage, structuralEqualityPolicy()) + internal set + var sentQuote by mutableStateOf(sentQuote, structuralEqualityPolicy()) + internal set + var receivedMessage by mutableStateOf(receivedMessage, structuralEqualityPolicy()) + internal set + var receivedQuote by mutableStateOf(receivedQuote, structuralEqualityPolicy()) + internal set + + fun copy( + title: Color = this.title, + primaryVariant2: Color = this.primaryVariant2, + sentMessage: Color = this.sentMessage, + sentQuote: Color = this.sentQuote, + receivedMessage: Color = this.receivedMessage, + receivedQuote: Color = this.receivedQuote, + ): AppColors = AppColors( + title, + primaryVariant2, + sentMessage, + sentQuote, + receivedMessage, + receivedQuote, + ) + + override fun toString(): String { + return buildString { + append("AppColors(") + append("title=$title, ") + append("primaryVariant2=$primaryVariant2, ") + append("sentMessage=$sentMessage, ") + append("sentQuote=$sentQuote, ") + append("receivedMessage=$receivedMessage, ") + append("receivedQuote=$receivedQuote") + append(")") + } + } +} + +@Stable +class AppWallpaper( + background: Color? = null, + tint: Color? = null, + type: WallpaperType = WallpaperType.Empty, +) { + var background by mutableStateOf(background, structuralEqualityPolicy()) + internal set + var tint by mutableStateOf(tint, structuralEqualityPolicy()) + internal set + var type by mutableStateOf(type, structuralEqualityPolicy()) + internal set + + fun copy( + background: Color? = this.background, + tint: Color? = this.tint, + type: WallpaperType = this.type, + ): AppWallpaper = AppWallpaper( + background, + tint, + type, + ) + + override fun toString(): String { + return buildString { + append("AppWallpaper(") + append("background=$background, ") + append("tint=$tint, ") + append("type=$type") + append(")") + } + } +} enum class ThemeColor { - PRIMARY, PRIMARY_VARIANT, SECONDARY, SECONDARY_VARIANT, BACKGROUND, SURFACE, TITLE, SENT_MESSAGE, RECEIVED_MESSAGE; + PRIMARY, PRIMARY_VARIANT, SECONDARY, SECONDARY_VARIANT, BACKGROUND, SURFACE, TITLE, SENT_MESSAGE, SENT_QUOTE, RECEIVED_MESSAGE, RECEIVED_QUOTE, PRIMARY_VARIANT2, WALLPAPER_BACKGROUND, WALLPAPER_TINT; - fun fromColors(colors: Colors, appColors: AppColors): Color { + fun fromColors(colors: Colors, appColors: AppColors, appWallpaper: AppWallpaper): Color? { return when (this) { PRIMARY -> colors.primary PRIMARY_VARIANT -> colors.primaryVariant @@ -61,8 +145,13 @@ enum class ThemeColor { BACKGROUND -> colors.background SURFACE -> colors.surface TITLE -> appColors.title + PRIMARY_VARIANT2 -> appColors.primaryVariant2 SENT_MESSAGE -> appColors.sentMessage + SENT_QUOTE -> appColors.sentQuote RECEIVED_MESSAGE -> appColors.receivedMessage + RECEIVED_QUOTE -> appColors.receivedQuote + WALLPAPER_BACKGROUND -> appWallpaper.background + WALLPAPER_TINT -> appWallpaper.tint } } @@ -75,8 +164,13 @@ enum class ThemeColor { BACKGROUND -> generalGetString(MR.strings.color_background) SURFACE -> generalGetString(MR.strings.color_surface) TITLE -> generalGetString(MR.strings.color_title) + PRIMARY_VARIANT2 -> generalGetString(MR.strings.color_primary_variant2) SENT_MESSAGE -> generalGetString(MR.strings.color_sent_message) + SENT_QUOTE -> generalGetString(MR.strings.color_sent_quote) RECEIVED_MESSAGE -> generalGetString(MR.strings.color_received_message) + RECEIVED_QUOTE -> generalGetString(MR.strings.color_received_quote) + WALLPAPER_BACKGROUND -> generalGetString(MR.strings.color_wallpaper_background) + WALLPAPER_TINT -> generalGetString(MR.strings.color_wallpaper_tint) } } @@ -92,45 +186,232 @@ data class ThemeColors( @SerialName("menus") val surface: String? = null, val title: String? = null, + @SerialName("accentVariant2") + val primaryVariant2: String? = null, val sentMessage: String? = null, + @SerialName("sentReply") + val sentQuote: String? = null, val receivedMessage: String? = null, + @SerialName("receivedReply") + val receivedQuote: String? = null, ) { - fun toColors(base: DefaultTheme): Colors { + companion object { + fun from(colors: Colors, appColors: AppColors): ThemeColors = + ThemeColors( + primary = colors.primary.toReadableHex(), + primaryVariant = colors.primaryVariant.toReadableHex(), + secondary = colors.secondary.toReadableHex(), + secondaryVariant = colors.secondaryVariant.toReadableHex(), + background = colors.background.toReadableHex(), + surface = colors.surface.toReadableHex(), + title = appColors.title.toReadableHex(), + primaryVariant2 = appColors.primaryVariant2.toReadableHex(), + sentMessage = appColors.sentMessage.toReadableHex(), + sentQuote = appColors.sentQuote.toReadableHex(), + receivedMessage = appColors.receivedMessage.toReadableHex(), + receivedQuote = appColors.receivedQuote.toReadableHex(), + ) + } +} + +@Serializable +data class ThemeWallpaper ( + val preset: String? = null, + val scale: Float? = null, + val scaleType: WallpaperScaleType? = null, + val background: String? = null, + val tint: String? = null, + val image: String? = null, + val imageFile: String? = null, +) { + fun toAppWallpaper(): AppWallpaper { + return AppWallpaper( + background = background?.colorFromReadableHex(), + tint = tint?.colorFromReadableHex(), + type = WallpaperType.from(this) ?: WallpaperType.Empty + ) + } + + fun withFilledWallpaperBase64(): ThemeWallpaper { + val aw = toAppWallpaper() + val type = aw.type + return ThemeWallpaper( + image = if (type is WallpaperType.Image && type.image != null) resizeImageToStrSize(type.image!!, 5_000_000) else null, + imageFile = null, + preset = if (type is WallpaperType.Preset) type.filename else null, + scale = if (type is WallpaperType.Preset) type.scale else if (type is WallpaperType.Image) type.scale else 1f, + scaleType = if (type is WallpaperType.Image) type.scaleType else null, + background = aw.background?.toReadableHex(), + tint = aw.tint?.toReadableHex(), + ) + } + + fun withFilledWallpaperPath(): ThemeWallpaper { + val aw = toAppWallpaper() + val type = aw.type + return ThemeWallpaper( + image = null, + imageFile = if (type is WallpaperType.Image) type.filename else null, + preset = if (type is WallpaperType.Preset) type.filename else null, + scale = if (scale == null) null else if (type is WallpaperType.Preset) type.scale else if (type is WallpaperType.Image) scale else null, + scaleType = if (scaleType == null) null else if (type is WallpaperType.Image) type.scaleType else null, + background = aw.background?.toReadableHex(), + tint = aw.tint?.toReadableHex(), + ) + } + + fun importFromString(): ThemeWallpaper = + if (preset == null && image != null) { + // Need to save image from string and to save its path + try { + val parsed = base64ToBitmap(image) + val filename = saveWallpaperFile(parsed) + copy(image = null, imageFile = filename) + } catch (e: Exception) { + Log.e(TAG, "Error while parsing/copying the image: ${e.stackTraceToString()}") + ThemeWallpaper() + } + } else this + + companion object { + fun from(type: WallpaperType, background: String?, tint: String?): ThemeWallpaper { + return ThemeWallpaper( + image = null, + imageFile = if (type is WallpaperType.Image) type.filename else null, + preset = if (type is WallpaperType.Preset) type.filename else null, + scale = if (type is WallpaperType.Preset) type.scale else if (type is WallpaperType.Image) type.scale else null, + scaleType = if (type is WallpaperType.Image) type.scaleType else null, + background = background, + tint = tint, + ) + } + } +} + +@Serializable +data class ThemesFile( + val themes: List = emptyList() +) + +@Serializable +data class ThemeOverrides ( + val themeId: String = UUID.randomUUID().toString(), + val base: DefaultTheme, + val colors: ThemeColors = ThemeColors(), + val wallpaper: ThemeWallpaper? = null, +) { + + fun isSame(type: WallpaperType?, themeName: String): Boolean = + ( + (wallpaper?.preset != null && type is WallpaperType.Preset && wallpaper.preset == type.filename) || + (wallpaper?.imageFile != null && type is WallpaperType.Image) || + (wallpaper?.preset == null && wallpaper?.imageFile == null && (type == WallpaperType.Empty || type == null)) + ) && base.themeName == themeName + + fun withUpdatedColor(name: ThemeColor, color: String?): ThemeOverrides { + return copy( + colors = when (name) { + ThemeColor.PRIMARY -> colors.copy(primary = color) + ThemeColor.PRIMARY_VARIANT -> colors.copy(primaryVariant = color) + ThemeColor.SECONDARY -> colors.copy(secondary = color) + ThemeColor.SECONDARY_VARIANT -> colors.copy(secondaryVariant = color) + ThemeColor.BACKGROUND -> colors.copy(background = color) + ThemeColor.SURFACE -> colors.copy(surface = color) + ThemeColor.TITLE -> colors.copy(title = color) + ThemeColor.PRIMARY_VARIANT2 -> colors.copy(primaryVariant2 = color) + ThemeColor.SENT_MESSAGE -> colors.copy(sentMessage = color) + ThemeColor.SENT_QUOTE -> colors.copy(sentQuote = color) + ThemeColor.RECEIVED_MESSAGE -> colors.copy(receivedMessage = color) + ThemeColor.RECEIVED_QUOTE -> colors.copy(receivedQuote = color) + ThemeColor.WALLPAPER_BACKGROUND -> colors.copy() + ThemeColor.WALLPAPER_TINT -> colors.copy() + }, wallpaper = when (name) { + ThemeColor.WALLPAPER_BACKGROUND -> wallpaper?.copy(background = color) + ThemeColor.WALLPAPER_TINT -> wallpaper?.copy(tint = color) + else -> wallpaper?.copy() + } + ) + } + + fun toColors(base: DefaultTheme, perChatTheme: ThemeColors?, perUserTheme: ThemeColors?, presetWallpaperTheme: ThemeColors?): Colors { val baseColors = when (base) { DefaultTheme.LIGHT -> LightColorPalette DefaultTheme.DARK -> DarkColorPalette DefaultTheme.SIMPLEX -> SimplexColorPalette - // shouldn't be here - DefaultTheme.SYSTEM -> LightColorPalette + DefaultTheme.BLACK -> BlackColorPalette } return baseColors.copy( - primary = primary?.colorFromReadableHex() ?: baseColors.primary, - primaryVariant = primaryVariant?.colorFromReadableHex() ?: baseColors.primaryVariant, - secondary = secondary?.colorFromReadableHex() ?: baseColors.secondary, - secondaryVariant = secondaryVariant?.colorFromReadableHex() ?: baseColors.secondaryVariant, - background = background?.colorFromReadableHex() ?: baseColors.background, - surface = surface?.colorFromReadableHex() ?: baseColors.surface, + primary = perChatTheme?.primary?.colorFromReadableHex() ?: perUserTheme?.primary?.colorFromReadableHex() ?: colors.primary?.colorFromReadableHex() ?: presetWallpaperTheme?.primary?.colorFromReadableHex() ?: baseColors.primary, + primaryVariant = perChatTheme?.primaryVariant?.colorFromReadableHex() ?: perUserTheme?.primaryVariant?.colorFromReadableHex() ?: colors.primaryVariant?.colorFromReadableHex() ?: presetWallpaperTheme?.primaryVariant?.colorFromReadableHex() ?: baseColors.primaryVariant, + secondary = perChatTheme?.secondary?.colorFromReadableHex() ?: perUserTheme?.secondary?.colorFromReadableHex() ?: colors.secondary?.colorFromReadableHex() ?: presetWallpaperTheme?.secondary?.colorFromReadableHex() ?: baseColors.secondary, + secondaryVariant = perChatTheme?.secondaryVariant?.colorFromReadableHex() ?: perUserTheme?.secondaryVariant?.colorFromReadableHex() ?: colors.secondaryVariant?.colorFromReadableHex() ?: presetWallpaperTheme?.secondaryVariant?.colorFromReadableHex() ?: baseColors.secondaryVariant, + background = perChatTheme?.background?.colorFromReadableHex() ?: perUserTheme?.background?.colorFromReadableHex() ?: colors.background?.colorFromReadableHex() ?: presetWallpaperTheme?.background?.colorFromReadableHex() ?: baseColors.background, + surface = perChatTheme?.surface?.colorFromReadableHex() ?: perUserTheme?.surface?.colorFromReadableHex() ?: colors.surface?.colorFromReadableHex() ?: presetWallpaperTheme?.surface?.colorFromReadableHex() ?: baseColors.surface, ) } - fun toAppColors(base: DefaultTheme): AppColors { + fun toAppColors(base: DefaultTheme, perChatTheme: ThemeColors?, perChatWallpaperType: WallpaperType?, perUserTheme: ThemeColors?, perUserWallpaperType: WallpaperType?, presetWallpaperTheme: ThemeColors?): AppColors { val baseColors = when (base) { DefaultTheme.LIGHT -> LightColorPaletteApp DefaultTheme.DARK -> DarkColorPaletteApp DefaultTheme.SIMPLEX -> SimplexColorPaletteApp - // shouldn't be here - DefaultTheme.SYSTEM -> LightColorPaletteApp + DefaultTheme.BLACK -> BlackColorPaletteApp } + + val sentMessageFallback = colors.sentMessage?.colorFromReadableHex() ?: presetWallpaperTheme?.sentMessage?.colorFromReadableHex() ?: baseColors.sentMessage + val sentQuoteFallback = colors.sentQuote?.colorFromReadableHex() ?: presetWallpaperTheme?.sentQuote?.colorFromReadableHex() ?: baseColors.sentQuote + val receivedMessageFallback = colors.receivedMessage?.colorFromReadableHex() ?: presetWallpaperTheme?.receivedMessage?.colorFromReadableHex() ?: baseColors.receivedMessage + val receivedQuoteFallback = colors.receivedQuote?.colorFromReadableHex() ?: presetWallpaperTheme?.receivedQuote?.colorFromReadableHex() ?: baseColors.receivedQuote return baseColors.copy( - title = title?.colorFromReadableHex() ?: baseColors.title, - sentMessage = sentMessage?.colorFromReadableHex() ?: baseColors.sentMessage, - receivedMessage = receivedMessage?.colorFromReadableHex() ?: baseColors.receivedMessage, + title = perChatTheme?.title?.colorFromReadableHex() ?: perUserTheme?.title?.colorFromReadableHex() ?: colors.title?.colorFromReadableHex() ?: presetWallpaperTheme?.title?.colorFromReadableHex() ?: baseColors.title, + primaryVariant2 = perChatTheme?.primaryVariant2?.colorFromReadableHex() ?: perUserTheme?.primaryVariant2?.colorFromReadableHex() ?: colors.primaryVariant2?.colorFromReadableHex() ?: presetWallpaperTheme?.primaryVariant2?.colorFromReadableHex() ?: baseColors.primaryVariant2, + sentMessage = if (perChatTheme?.sentMessage != null) perChatTheme.sentMessage.colorFromReadableHex() + else if (perUserTheme != null && (perChatWallpaperType == null || perUserWallpaperType == null || perChatWallpaperType.sameType(perUserWallpaperType))) perUserTheme.sentMessage?.colorFromReadableHex() ?: sentMessageFallback + else sentMessageFallback, + sentQuote = if (perChatTheme?.sentQuote != null) perChatTheme.sentQuote.colorFromReadableHex() + else if (perUserTheme != null && (perChatWallpaperType == null || perUserWallpaperType == null || perChatWallpaperType.sameType(perUserWallpaperType))) perUserTheme.sentQuote?.colorFromReadableHex() ?: sentQuoteFallback + else sentQuoteFallback, + receivedMessage = if (perChatTheme?.receivedMessage != null) perChatTheme.receivedMessage.colorFromReadableHex() + else if (perUserTheme != null && (perChatWallpaperType == null || perUserWallpaperType == null || perChatWallpaperType.sameType(perUserWallpaperType))) perUserTheme.receivedMessage?.colorFromReadableHex() ?: receivedMessageFallback + else receivedMessageFallback, + receivedQuote = if (perChatTheme?.receivedQuote != null) perChatTheme.receivedQuote.colorFromReadableHex() + else if (perUserTheme != null && (perChatWallpaperType == null || perUserWallpaperType == null || perChatWallpaperType.sameType(perUserWallpaperType))) perUserTheme.receivedQuote?.colorFromReadableHex() ?: receivedQuoteFallback + else receivedQuoteFallback, ) } - fun withFilledColors(base: DefaultTheme): ThemeColors { - val c = toColors(base) - val ac = toAppColors(base) + fun toAppWallpaper(themeOverridesForType: WallpaperType?, perChatTheme: ThemeModeOverride?, perUserTheme: ThemeModeOverride?, materialBackgroundColor: Color): AppWallpaper { + val mainType = when { + themeOverridesForType != null -> themeOverridesForType + // type can be null if override is empty `"wallpaper": "{}"`, in this case no wallpaper is needed, empty. + // It's not null to override upper level wallpaper + perChatTheme?.wallpaper != null -> perChatTheme.wallpaper.toAppWallpaper().type + perUserTheme?.wallpaper != null -> perUserTheme.wallpaper.toAppWallpaper().type + else -> wallpaper?.toAppWallpaper()?.type ?: return AppWallpaper() + } + val first: ThemeWallpaper? = if (mainType.sameType(perChatTheme?.wallpaper?.toAppWallpaper()?.type)) perChatTheme?.wallpaper else null + val second: ThemeWallpaper? = if (mainType.sameType(perUserTheme?.wallpaper?.toAppWallpaper()?.type)) perUserTheme?.wallpaper else null + val third: ThemeWallpaper? = if (mainType.sameType(this.wallpaper?.toAppWallpaper()?.type)) this.wallpaper else null + + return AppWallpaper(type = when (mainType) { + is WallpaperType.Preset -> mainType.copy( + scale = mainType.scale ?: first?.scale ?: second?.scale ?: third?.scale + ) + is WallpaperType.Image -> mainType.copy( + scale = if (themeOverridesForType == null) mainType.scale ?: first?.scale ?: second?.scale ?: third?.scale else second?.scale ?: third?.scale ?: mainType.scale, + scaleType = if (themeOverridesForType == null) mainType.scaleType ?: first?.scaleType ?: second?.scaleType ?: third?.scaleType else second?.scaleType ?: third?.scaleType ?: mainType.scaleType, + filename = if (themeOverridesForType == null) mainType.filename else first?.imageFile ?: second?.imageFile ?: third?.imageFile ?: mainType.filename, + ) + is WallpaperType.Empty -> mainType + }, + background = (first?.background ?: second?.background ?: third?.background)?.colorFromReadableHex() ?: mainType.defaultBackgroundColor(base, materialBackgroundColor), + tint = (first?.tint ?: second?.tint ?: third?.tint)?.colorFromReadableHex() ?: mainType.defaultTintColor(base) + ) + } + + fun withFilledColors(base: DefaultTheme, perChatTheme: ThemeColors?, perChatWallpaperType: WallpaperType?, perUserTheme: ThemeColors?, perUserWallpaperType: WallpaperType?, presetWallpaperTheme: ThemeColors?): ThemeColors { + val c = toColors(base, perChatTheme, perUserTheme, presetWallpaperTheme) + val ac = toAppColors(base, perChatTheme, perChatWallpaperType, perUserTheme, perUserWallpaperType, presetWallpaperTheme) return ThemeColors( primary = c.primary.toReadableHex(), primaryVariant = c.primaryVariant.toReadableHex(), @@ -139,23 +420,71 @@ data class ThemeColors( background = c.background.toReadableHex(), surface = c.surface.toReadableHex(), title = ac.title.toReadableHex(), + primaryVariant2 = ac.primaryVariant2.toReadableHex(), sentMessage = ac.sentMessage.toReadableHex(), - receivedMessage = ac.receivedMessage.toReadableHex() + sentQuote = ac.sentQuote.toReadableHex(), + receivedMessage = ac.receivedMessage.toReadableHex(), + receivedQuote = ac.receivedQuote.toReadableHex(), ) } } -private fun String.colorFromReadableHex(): Color = - Color(this.replace("#", "").toLongOrNull(16) ?: Color.White.toArgb().toLong()) +fun List.getTheme(themeId: String?): ThemeOverrides? = + firstOrNull { it.themeId == themeId } -private fun Color.toReadableHex(): String = "#" + Integer.toHexString(toArgb()) +fun List.getTheme(themeId: String?, type: WallpaperType?, base: DefaultTheme): ThemeOverrides? = + firstOrNull { it.themeId == themeId || it.isSame(type, base.themeName)} + +fun List.replace(theme: ThemeOverrides): List { + val index = indexOfFirst { it.themeId == theme.themeId || + // prevent situation when two themes has the same type but different theme id (maybe something was changed in prefs by hand) + it.isSame(WallpaperType.from(theme.wallpaper), theme.base.themeName) + } + return if (index != -1) { + val a = ArrayList(this) + a[index] = theme + a + } else { + this + theme + } +} + +fun List.sameTheme(type: WallpaperType?, themeName: String): ThemeOverrides? = firstOrNull { it.isSame(type, themeName) } + +/** See [ThemesTest.testSkipDuplicates] */ +fun List.skipDuplicates(): List { + val res = ArrayList() + forEach { theme -> + val themeType = WallpaperType.from(theme.wallpaper) + if (res.none { it.themeId == theme.themeId || it.isSame(themeType, theme.base.themeName) }) { + res.add(theme) + } + } + return res +} @Serializable -data class ThemeOverrides ( - val base: DefaultTheme, - val colors: ThemeColors +data class ThemeModeOverrides ( + val light: ThemeModeOverride? = null, + val dark: ThemeModeOverride? = null ) { - fun withUpdatedColor(name: ThemeColor, color: String): ThemeOverrides { + fun preferredMode(darkTheme: Boolean): ThemeModeOverride? = when (darkTheme) { + false -> light + else -> dark + } +} + +@Serializable +data class ThemeModeOverride ( + val mode: DefaultThemeMode = CurrentColors.value.base.mode, + val colors: ThemeColors = ThemeColors(), + val wallpaper: ThemeWallpaper? = null, +) { + + @Transient + val type = WallpaperType.from(wallpaper) + + fun withUpdatedColor(name: ThemeColor, color: String?): ThemeModeOverride { return copy(colors = when (name) { ThemeColor.PRIMARY -> colors.copy(primary = color) ThemeColor.PRIMARY_VARIANT -> colors.copy(primaryVariant = color) @@ -164,9 +493,97 @@ data class ThemeOverrides ( ThemeColor.BACKGROUND -> colors.copy(background = color) ThemeColor.SURFACE -> colors.copy(surface = color) ThemeColor.TITLE -> colors.copy(title = color) + ThemeColor.PRIMARY_VARIANT2 -> colors.copy(primaryVariant2 = color) ThemeColor.SENT_MESSAGE -> colors.copy(sentMessage = color) + ThemeColor.SENT_QUOTE -> colors.copy(sentQuote = color) ThemeColor.RECEIVED_MESSAGE -> colors.copy(receivedMessage = color) - }) + ThemeColor.RECEIVED_QUOTE -> colors.copy(receivedQuote = color) + ThemeColor.WALLPAPER_BACKGROUND -> colors.copy() + ThemeColor.WALLPAPER_TINT -> colors.copy() + }, wallpaper = when (name) { + ThemeColor.WALLPAPER_BACKGROUND -> wallpaper?.copy(background = color) + ThemeColor.WALLPAPER_TINT -> wallpaper?.copy(tint = color) + else -> wallpaper?.copy() + } + ) + } + + fun removeSameColors(base: DefaultTheme): ThemeModeOverride { + val c = when (base) { + DefaultTheme.LIGHT -> LightColorPalette + DefaultTheme.DARK -> DarkColorPalette + DefaultTheme.SIMPLEX -> SimplexColorPalette + DefaultTheme.BLACK -> BlackColorPalette + } + val ac = when (base) { + DefaultTheme.LIGHT -> LightColorPaletteApp + DefaultTheme.DARK -> DarkColorPaletteApp + DefaultTheme.SIMPLEX -> SimplexColorPaletteApp + DefaultTheme.BLACK -> BlackColorPaletteApp + } + val w = when (val wallpaperType = WallpaperType.from(wallpaper)) { + is WallpaperType.Preset -> { + val p = PresetWallpaper.from(wallpaperType.filename) + ThemeWallpaper( + preset = wallpaperType.filename, + scale = p?.scale ?: wallpaper?.scale, + scaleType = null, + background = p?.background?.get(base)?.toReadableHex(), + tint = p?.tint?.get(base)?.toReadableHex(), + image = null, + imageFile = null, + ) + } + is WallpaperType.Image -> { + ThemeWallpaper( + preset = null, + scale = null, + scaleType = WallpaperScaleType.FILL, + background = Color.Transparent.toReadableHex(), + tint = Color.Transparent.toReadableHex(), + image = null, + imageFile = null, + ) + } + else -> { + ThemeWallpaper() + } + } + + return copy( + colors = ThemeColors( + primary = if (colors.primary?.colorFromReadableHex() != c.primary) colors.primary else null, + primaryVariant = if (colors.primaryVariant?.colorFromReadableHex() != c.primaryVariant) colors.primaryVariant else null, + secondary = if (colors.secondary?.colorFromReadableHex() != c.secondary) colors.secondary else null, + secondaryVariant = if (colors.secondaryVariant?.colorFromReadableHex() != c.secondaryVariant) colors.secondaryVariant else null, + background = if (colors.background?.colorFromReadableHex() != c.background) colors.background else null, + surface = if (colors.surface?.colorFromReadableHex() != c.surface) colors.surface else null, + title = if (colors.title?.colorFromReadableHex() != ac.title) colors.title else null, + primaryVariant2 = if (colors.primaryVariant2?.colorFromReadableHex() != ac.primaryVariant2) colors.primary else null, + sentMessage = if (colors.sentMessage?.colorFromReadableHex() != ac.sentMessage) colors.sentMessage else null, + sentQuote = if (colors.sentQuote?.colorFromReadableHex() != ac.sentQuote) colors.sentQuote else null, + receivedMessage = if (colors.receivedMessage?.colorFromReadableHex() != ac.receivedMessage) colors.receivedMessage else null, + receivedQuote = if (colors.receivedQuote?.colorFromReadableHex() != ac.receivedQuote) colors.receivedQuote else null, + ), + wallpaper = wallpaper?.copy( + preset = wallpaper.preset, + scale = if (wallpaper.scale != w.scale) wallpaper.scale else null, + scaleType = if (wallpaper.scaleType != w.scaleType) wallpaper.scaleType else null, + background = if (wallpaper.background != w.background) wallpaper.background else null, + tint = if (wallpaper.tint != w.tint) wallpaper.tint else null, + image = wallpaper.image, + imageFile = wallpaper.imageFile, + ) + ) + } + + companion object { + fun withFilledAppDefaults(mode: DefaultThemeMode, base: DefaultTheme): ThemeModeOverride = + ThemeModeOverride( + mode = mode, + colors = ThemeOverrides(base = base).withFilledColors(base, null, null, null, null, null), + wallpaper = ThemeWallpaper(preset = PresetWallpaper.SCHOOL.filename) + ) } } @@ -190,6 +607,8 @@ val DEFAULT_SPACE_AFTER_ICON = 4.dp val DEFAULT_PADDING_HALF = DEFAULT_PADDING / 2 val DEFAULT_BOTTOM_PADDING = 48.dp val DEFAULT_BOTTOM_BUTTON_PADDING = 20.dp +val DEFAULT_MIN_SECTION_ITEM_HEIGHT = 50.dp +val DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL = 15.dp val DEFAULT_START_MODAL_WIDTH = 388.dp val DEFAULT_MIN_CENTER_MODAL_WIDTH = 590.dp @@ -204,7 +623,6 @@ val DarkColorPalette = darkColors( // background = Color.Black, surface = Color(0xFF222222), // background = Color(0xFF121212), -// surface = Color(0xFF121212), error = Color.Red, onBackground = Color(0xFFFFFBFA), onSurface = Color(0xFFFFFBFA), @@ -212,8 +630,11 @@ val DarkColorPalette = darkColors( ) val DarkColorPaletteApp = AppColors( title = SimplexBlue, - sentMessage = SentMessageColor, - receivedMessage = Color(0x20B1B0B5) + primaryVariant2 = Color(0xFF18262E), + sentMessage = Color(0xFF18262E), + sentQuote = Color(0xFF1D3847), + receivedMessage = Color(0xff262627), + receivedQuote = Color(0xff373739), ) val LightColorPalette = lightColors( @@ -231,8 +652,11 @@ val LightColorPalette = lightColors( ) val LightColorPaletteApp = AppColors( title = SimplexBlue, - sentMessage = SentMessageColor, - receivedMessage = Color(0x20B1B0B5) + primaryVariant2 = Color(0xFFE9F7FF), + sentMessage = Color(0xFFE9F7FF), + sentQuote = Color(0xFFD6F0FF), + receivedMessage = Color(0xfff5f5f6), + receivedQuote = Color(0xffececee), ) val SimplexColorPalette = darkColors( @@ -251,11 +675,39 @@ val SimplexColorPalette = darkColors( ) val SimplexColorPaletteApp = AppColors( title = Color(0xFF267BE5), - sentMessage = SentMessageColor, - receivedMessage = Color(0x20B1B0B5) + primaryVariant2 = Color(0xFF172941), + sentMessage = Color(0xFF172941), + sentQuote = Color(0xFF1C3A57), + receivedMessage = Color(0xff25283a), + receivedQuote = Color(0xff36394a), ) -val CurrentColors: MutableStateFlow = MutableStateFlow(ThemeManager.currentColors(isInNightMode())) +val BlackColorPalette = darkColors( + primary = Color(0xff0077e0), // If this value changes also need to update #0088ff in string resource files + primaryVariant = Color(0xff0077e0), + secondary = HighOrLowlight, + secondaryVariant = DarkGray, + background = Color(0xff070707), + surface = Color(0xff161617), + // background = Color(0xFF121212), + // surface = Color(0xFF121212), + error = Color.Red, + onBackground = Color(0xFFFFFBFA), + onSurface = Color(0xFFFFFBFA), + // onError: Color = Color.Black, +) +val BlackColorPaletteApp = AppColors( + title = Color(0xff0077e0), + primaryVariant2 = Color(0xff243747), + sentMessage = Color(0xFF18262E), + sentQuote = Color(0xFF1D3847), + receivedMessage = Color(0xff1b1b1b), + receivedQuote = Color(0xff29292b), +) + +var systemInDarkThemeCurrently: Boolean = isInNightMode() + +val CurrentColors: MutableStateFlow = MutableStateFlow(ThemeManager.currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get())) @Composable fun isInDarkTheme(): Boolean = !CurrentColors.collectAsState().value.colors.isLight @@ -263,31 +715,115 @@ fun isInDarkTheme(): Boolean = !CurrentColors.collectAsState().value.colors.isLi @Composable expect fun isSystemInDarkTheme(): Boolean +internal val LocalAppColors = staticCompositionLocalOf { LightColorPaletteApp } +internal val LocalAppWallpaper = staticCompositionLocalOf { AppWallpaper() } + +val MaterialTheme.appColors: AppColors + @Composable + @ReadOnlyComposable + get() = LocalAppColors.current + +fun AppColors.updateColorsFrom(other: AppColors) { + title = other.title + primaryVariant2 = other.primaryVariant2 + sentMessage = other.sentMessage + sentQuote = other.sentQuote + receivedMessage = other.receivedMessage + receivedQuote = other.receivedQuote +} + +fun AppWallpaper.updateWallpaperFrom(other: AppWallpaper) { + background = other.background + tint = other.tint + type = other.type +} + +val MaterialTheme.wallpaper: AppWallpaper + @Composable + @ReadOnlyComposable + get() = LocalAppWallpaper.current + fun reactOnDarkThemeChanges(isDark: Boolean) { - if (ChatController.appPrefs.currentTheme.get() == DefaultTheme.SYSTEM.name && CurrentColors.value.colors.isLight == isDark) { + systemInDarkThemeCurrently = isDark + if (ChatController.appPrefs.currentTheme.get() == DefaultTheme.SYSTEM_THEME_NAME && CurrentColors.value.colors.isLight == isDark) { // Change active colors from light to dark and back based on system theme - ThemeManager.applyTheme(DefaultTheme.SYSTEM.name, isDark) + ThemeManager.applyTheme(DefaultTheme.SYSTEM_THEME_NAME) } } @Composable fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) { - LaunchedEffect(darkTheme) { - // For preview - if (darkTheme != null) - CurrentColors.value = ThemeManager.currentColors(darkTheme) - } - val systemDark = isSystemInDarkTheme() - LaunchedEffect(systemDark) { - reactOnDarkThemeChanges(systemDark) +// TODO: Fix preview working with dark/light theme + +// LaunchedEffect(darkTheme) { +// // For preview +// if (darkTheme != null) +// CurrentColors.value = ThemeManager.currentColors(darkTheme, null, null, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get()) +// } + val systemDark = rememberUpdatedState(isSystemInDarkTheme()) + LaunchedEffect(Unit) { + // snapshotFlow vs LaunchedEffect reduce number of recomposes + snapshotFlow { systemDark.value } + .collect { + reactOnDarkThemeChanges(systemDark.value) + } } val theme by CurrentColors.collectAsState() + LaunchedEffect(Unit) { + // snapshotFlow vs LaunchedEffect reduce number of recomposes when user is changed or it's themes + snapshotFlow { chatModel.currentUser.value?.uiThemes } + .collect { + ThemeManager.applyTheme(appPrefs.currentTheme.get()!!) + } + } MaterialTheme( colors = theme.colors, typography = Typography, shapes = Shapes, content = { - CompositionLocalProvider(LocalContentColor provides MaterialTheme.colors.onBackground, content = content) + val density = Density(LocalDensity.current.density * desktopDensityScaleMultiplier, LocalDensity.current.fontScale * fontSizeMultiplier) + val rememberedAppColors = remember { + // Explicitly creating a new object here so we don't mutate the initial [appColors] + // provided, and overwrite the values set in it. + theme.appColors.copy() + }.apply { updateColorsFrom(theme.appColors) } + val rememberedWallpaper = remember { + // Explicitly creating a new object here so we don't mutate the initial [wallpaper] + // provided, and overwrite the values set in it. + theme.wallpaper.copy() + }.apply { updateWallpaperFrom(theme.wallpaper) } + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colors.onBackground, + LocalAppColors provides rememberedAppColors, + LocalAppWallpaper provides rememberedWallpaper, + LocalDensity provides density, + content = content) + } + ) +} + +@Composable +fun SimpleXThemeOverride(theme: ThemeManager.ActiveTheme, content: @Composable () -> Unit) { + MaterialTheme( + colors = theme.colors, + typography = Typography, + shapes = Shapes, + content = { + val rememberedAppColors = remember { + // Explicitly creating a new object here so we don't mutate the initial [appColors] + // provided, and overwrite the values set in it. + theme.appColors.copy() + }.apply { updateColorsFrom(theme.appColors) } + val rememberedWallpaper = remember { + // Explicitly creating a new object here so we don't mutate the initial [wallpaper] + // provided, and overwrite the values set in it. + theme.wallpaper.copy() + }.apply { updateWallpaperFrom(theme.wallpaper) } + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colors.onBackground, + LocalAppColors provides rememberedAppColors, + LocalAppWallpaper provides rememberedWallpaper, + content = content) } ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt index 49d3203455..7f19f58949 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/ThemeManager.kt @@ -1,14 +1,15 @@ package chat.simplex.common.ui.theme import androidx.compose.material.Colors +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.MutableState import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.text.font.FontFamily -import chat.simplex.res.MR -import chat.simplex.common.model.AppPreferences -import chat.simplex.common.model.ChatController -import chat.simplex.common.platform.platform -import chat.simplex.common.views.helpers.generalGetString +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.views.helpers.* +import java.io.File // https://github.com/rsms/inter // I place it here because IDEA shows an error (but still works anyway) when this declaration inside Type.kt @@ -18,140 +19,225 @@ expect val EmojiFont: FontFamily object ThemeManager { private val appPrefs: AppPreferences = ChatController.appPrefs - data class ActiveTheme(val name: String, val base: DefaultTheme, val colors: Colors, val appColors: AppColors) + data class ActiveTheme(val name: String, val base: DefaultTheme, val colors: Colors, val appColors: AppColors, val wallpaper: AppWallpaper = AppWallpaper()) private fun systemDarkThemeColors(): Pair = when (appPrefs.systemDarkTheme.get()) { - DefaultTheme.DARK.name -> DarkColorPalette to DefaultTheme.DARK - DefaultTheme.SIMPLEX.name -> SimplexColorPalette to DefaultTheme.SIMPLEX + DefaultTheme.DARK.themeName -> DarkColorPalette to DefaultTheme.DARK + DefaultTheme.SIMPLEX.themeName -> SimplexColorPalette to DefaultTheme.SIMPLEX + DefaultTheme.BLACK.themeName -> BlackColorPalette to DefaultTheme.BLACK else -> SimplexColorPalette to DefaultTheme.SIMPLEX } - fun currentColors(darkForSystemTheme: Boolean): ActiveTheme { + private fun nonSystemThemeName(): String { val themeName = appPrefs.currentTheme.get()!! - val themeOverrides = appPrefs.themeOverrides.get() - - val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) { + return if (themeName != DefaultTheme.SYSTEM_THEME_NAME) { themeName } else { - if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name + if (systemInDarkThemeCurrently) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.themeName } - val theme = themeOverrides[nonSystemThemeName] + } + + fun defaultActiveTheme(appSettingsTheme: List): ThemeOverrides? { + val nonSystemThemeName = nonSystemThemeName() + val defaultThemeId = appPrefs.currentThemeIds.get()[nonSystemThemeName] + return appSettingsTheme.getTheme(defaultThemeId) + } + + fun defaultActiveTheme(perUserTheme: ThemeModeOverrides?, appSettingsTheme: List): ThemeModeOverride { + val perUserTheme = if (!CurrentColors.value.colors.isLight) perUserTheme?.dark else perUserTheme?.light + if (perUserTheme != null) { + return perUserTheme + } + val defaultTheme = defaultActiveTheme(appSettingsTheme) + return ThemeModeOverride(colors = defaultTheme?.colors ?: ThemeColors(), wallpaper = defaultTheme?.wallpaper + // Situation when user didn't change global theme at all (it is not saved yet). Using defaults + ?: ThemeWallpaper.from(PresetWallpaper.SCHOOL.toType(CurrentColors.value.base), null, null)) + } + + fun currentColors(themeOverridesForType: WallpaperType?, perChatTheme: ThemeModeOverride?, perUserTheme: ThemeModeOverrides?, appSettingsTheme: List): ActiveTheme { + val themeName = appPrefs.currentTheme.get()!! + val nonSystemThemeName = nonSystemThemeName() + val defaultTheme = defaultActiveTheme(appSettingsTheme) + val baseTheme = when (nonSystemThemeName) { - DefaultTheme.LIGHT.name -> Triple(DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp) - DefaultTheme.DARK.name -> Triple(DefaultTheme.DARK, DarkColorPalette, DarkColorPaletteApp) - DefaultTheme.SIMPLEX.name -> Triple(DefaultTheme.SIMPLEX, SimplexColorPalette, SimplexColorPaletteApp) - else -> Triple(DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp) + DefaultTheme.LIGHT.themeName -> ActiveTheme(DefaultTheme.LIGHT.themeName, DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp, AppWallpaper(type = PresetWallpaper.SCHOOL.toType(DefaultTheme.LIGHT))) + DefaultTheme.DARK.themeName -> ActiveTheme(DefaultTheme.DARK.themeName, DefaultTheme.DARK, DarkColorPalette, DarkColorPaletteApp, AppWallpaper(type = PresetWallpaper.SCHOOL.toType(DefaultTheme.DARK))) + DefaultTheme.SIMPLEX.themeName -> ActiveTheme(DefaultTheme.SIMPLEX.themeName, DefaultTheme.SIMPLEX, SimplexColorPalette, SimplexColorPaletteApp, AppWallpaper(type = PresetWallpaper.SCHOOL.toType(DefaultTheme.SIMPLEX))) + DefaultTheme.BLACK.themeName -> ActiveTheme(DefaultTheme.BLACK.themeName, DefaultTheme.BLACK, BlackColorPalette, BlackColorPaletteApp, AppWallpaper(type = PresetWallpaper.SCHOOL.toType(DefaultTheme.BLACK))) + else -> ActiveTheme(DefaultTheme.LIGHT.themeName, DefaultTheme.LIGHT, LightColorPalette, LightColorPaletteApp, AppWallpaper(type = PresetWallpaper.SCHOOL.toType(DefaultTheme.LIGHT))) } - if (theme == null) { - return ActiveTheme(themeName, baseTheme.first, baseTheme.second, baseTheme.third) + + val perUserTheme = if (baseTheme.colors.isLight) perUserTheme?.light else perUserTheme?.dark + val theme = (appSettingsTheme.sameTheme(themeOverridesForType ?: perChatTheme?.type ?: perUserTheme?.type ?: defaultTheme?.wallpaper?.toAppWallpaper()?.type, nonSystemThemeName) ?: defaultTheme) + + if (theme == null && perUserTheme == null && perChatTheme == null && themeOverridesForType == null) { + return ActiveTheme(themeName, baseTheme.base, baseTheme.colors, baseTheme.appColors, baseTheme.wallpaper) } - return ActiveTheme(themeName, baseTheme.first, theme.colors.toColors(theme.base), theme.colors.toAppColors(theme.base)) + val presetWallpaperTheme = when { + perChatTheme?.wallpaper != null -> if (perChatTheme.wallpaper.preset != null) PresetWallpaper.from(perChatTheme.wallpaper.preset)?.colors?.get(baseTheme.base) else null + perUserTheme?.wallpaper != null -> if (perUserTheme.wallpaper.preset != null) PresetWallpaper.from(perUserTheme.wallpaper.preset)?.colors?.get(baseTheme.base) else null + else -> if (theme?.wallpaper?.preset != null) PresetWallpaper.from(theme.wallpaper.preset)?.colors?.get(baseTheme.base) else null + } + val themeOrEmpty = theme ?: ThemeOverrides(base = baseTheme.base) + val colors = themeOrEmpty.toColors(themeOrEmpty.base, perChatTheme?.colors, perUserTheme?.colors, presetWallpaperTheme) + return ActiveTheme( + themeName, + baseTheme.base, + colors, + themeOrEmpty.toAppColors(themeOrEmpty.base, perChatTheme?.colors, perChatTheme?.type, perUserTheme?.colors, perUserTheme?.type, presetWallpaperTheme), + themeOrEmpty.toAppWallpaper(themeOverridesForType, perChatTheme, perUserTheme, colors.background) + ) } - fun currentThemeOverridesForExport(darkForSystemTheme: Boolean): ThemeOverrides { - val themeName = appPrefs.currentTheme.get()!! - val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) { - themeName - } else { - if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name - } - val overrides = appPrefs.themeOverrides.get().toMutableMap() - val nonFilledTheme = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors()) - return nonFilledTheme.copy(colors = nonFilledTheme.colors.withFilledColors(CurrentColors.value.base)) + fun currentThemeOverridesForExport(perChatTheme: ThemeModeOverride?, perUserTheme: ThemeModeOverrides?): ThemeOverrides { + val current = currentColors(null, perChatTheme, perUserTheme, appPrefs.themeOverrides.get()) + val wType = current.wallpaper.type + val wBackground = current.wallpaper.background + val wTint = current.wallpaper.tint + return ThemeOverrides( + themeId = "", + base = current.base, + colors = ThemeColors.from(current.colors, current.appColors), + wallpaper = if (wType !is WallpaperType.Empty) ThemeWallpaper.from(wType, wBackground?.toReadableHex(), wTint?.toReadableHex()).withFilledWallpaperBase64() else null + ) } - // colors, default theme enum, localized name of theme - fun allThemes(darkForSystemTheme: Boolean): List> { - val allThemes = ArrayList>() - allThemes.add( - Triple( - if (darkForSystemTheme) systemDarkThemeColors().first else LightColorPalette, - DefaultTheme.SYSTEM, - generalGetString(MR.strings.theme_system) - ) - ) - allThemes.add( - Triple( - LightColorPalette, - DefaultTheme.LIGHT, - generalGetString(MR.strings.theme_light) - ) - ) - allThemes.add( - Triple( - DarkColorPalette, - DefaultTheme.DARK, - generalGetString(MR.strings.theme_dark) - ) - ) - allThemes.add( - Triple( - SimplexColorPalette, - DefaultTheme.SIMPLEX, - generalGetString(MR.strings.theme_simplex) - ) - ) - return allThemes - } - - fun applyTheme(theme: String, darkForSystemTheme: Boolean) { + fun applyTheme(theme: String) { appPrefs.currentTheme.set(theme) - CurrentColors.value = currentColors(darkForSystemTheme) + CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) platform.androidSetNightModeIfSupported() + val c = CurrentColors.value.colors + platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !ChatController.appPrefs.oneHandUI.get(), ChatController.appPrefs.oneHandUI.get()) } - fun changeDarkTheme(theme: String, darkForSystemTheme: Boolean) { + fun changeDarkTheme(theme: String) { appPrefs.systemDarkTheme.set(theme) - CurrentColors.value = currentColors(darkForSystemTheme) + CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) } - fun saveAndApplyThemeColor(name: ThemeColor, color: Color? = null, darkForSystemTheme: Boolean) { - val themeName = appPrefs.currentTheme.get()!! - val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) { - themeName - } else { - if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name + fun saveAndApplyThemeColor(baseTheme: DefaultTheme, name: ThemeColor, color: Color? = null, pref: SharedPreference> = appPrefs.themeOverrides) { + val nonSystemThemeName = baseTheme.themeName + val overrides = pref.get() + val themeId = appPrefs.currentThemeIds.get()[nonSystemThemeName] + val prevValue = overrides.getTheme(themeId) ?: ThemeOverrides(base = baseTheme) + pref.set(overrides.replace(prevValue.withUpdatedColor(name, color?.toReadableHex()))) + val themeIds = appPrefs.currentThemeIds.get().toMutableMap() + themeIds[nonSystemThemeName] = prevValue.themeId + appPrefs.currentThemeIds.set(themeIds) + CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + if (name == ThemeColor.BACKGROUND) { + val c = CurrentColors.value.colors + platform.androidSetStatusAndNavBarColors(c.isLight, c.background, false, false) } - var colorToSet = color - if (colorToSet == null) { - // Setting default color from a base theme - colorToSet = when(nonSystemThemeName) { - DefaultTheme.LIGHT.name -> name.fromColors(LightColorPalette, LightColorPaletteApp) - DefaultTheme.DARK.name -> name.fromColors(DarkColorPalette, DarkColorPaletteApp) - DefaultTheme.SIMPLEX.name -> name.fromColors(SimplexColorPalette, SimplexColorPaletteApp) - // Will not be here - else -> return + } + + fun applyThemeColor(name: ThemeColor, color: Color? = null, pref: MutableState) { + pref.value = pref.value.withUpdatedColor(name, color?.toReadableHex()) + } + + fun saveAndApplyWallpaper(baseTheme: DefaultTheme, type: WallpaperType?, pref: SharedPreference> = appPrefs.themeOverrides) { + val nonSystemThemeName = baseTheme.themeName + val overrides = pref.get() + val theme = overrides.sameTheme(type, baseTheme.themeName) + val prevValue = theme ?: ThemeOverrides(base = baseTheme) + pref.set(overrides.replace(prevValue.copy(wallpaper = if (type != null && type !is WallpaperType.Empty) ThemeWallpaper.from(type, prevValue.wallpaper?.background, prevValue.wallpaper?.tint) else null))) + val themeIds = appPrefs.currentThemeIds.get().toMutableMap() + themeIds[nonSystemThemeName] = prevValue.themeId + appPrefs.currentThemeIds.set(themeIds) + CurrentColors.value = currentColors( null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + } + + fun copyFromSameThemeOverrides(type: WallpaperType?, lowerLevelOverride: ThemeModeOverride?, pref: MutableState): Boolean { + val overrides = appPrefs.themeOverrides.get() + val sameWallpaper = if (lowerLevelOverride?.type?.sameType(type) == true) lowerLevelOverride.wallpaper else overrides.sameTheme(type, CurrentColors.value.base.themeName)?.wallpaper + if (sameWallpaper == null) { + if (type != null) { + pref.value = ThemeModeOverride(wallpaper = ThemeWallpaper.from(type, null, null).copy(scale = null, scaleType = null)) + } else { + // Make an empty wallpaper to override any top level ones + pref.value = ThemeModeOverride(wallpaper = ThemeWallpaper()) + } + return true + } + var type = sameWallpaper.toAppWallpaper().type + if (type is WallpaperType.Image && sameWallpaper.imageFile == type.filename) { + // same image file. Needs to be copied first in order to be able to remove the file once it's not needed anymore without affecting main theme override + val filename = saveWallpaperFile(File(getWallpaperFilePath(type.filename)).toURI()) + if (filename != null) { + type = WallpaperType.Image(filename, type.scale, type.scaleType) + } else { + Log.e(TAG, "Error while copying wallpaper from global overrides to chat overrides") + return false } } - val overrides = appPrefs.themeOverrides.get().toMutableMap() - val prevValue = overrides[nonSystemThemeName] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors()) - overrides[nonSystemThemeName] = prevValue.withUpdatedColor(name, colorToSet.toReadableHex()) - appPrefs.themeOverrides.set(overrides) - CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight) + val prevValue = pref.value + pref.value = prevValue.copy( + colors = ThemeColors(), + wallpaper = ThemeWallpaper.from(type, null, null).copy(scale = null, scaleType = null) + ) + return true } - fun saveAndApplyThemeOverrides(theme: ThemeOverrides, darkForSystemTheme: Boolean) { - val overrides = appPrefs.themeOverrides.get().toMutableMap() - val prevValue = overrides[theme.base.name] ?: ThemeOverrides(base = CurrentColors.value.base, colors = ThemeColors()) - overrides[theme.base.name] = prevValue.copy(colors = theme.colors) - appPrefs.themeOverrides.set(overrides) - appPrefs.currentTheme.set(theme.base.name) - CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight) + fun applyWallpaper(type: WallpaperType?, pref: MutableState) { + val prevValue = pref.value + pref.value = prevValue.copy( + wallpaper = if (type != null) + ThemeWallpaper.from(type, prevValue.wallpaper?.background, prevValue.wallpaper?.tint) + else null + ) } - fun resetAllThemeColors(darkForSystemTheme: Boolean) { - val themeName = appPrefs.currentTheme.get()!! - val nonSystemThemeName = if (themeName != DefaultTheme.SYSTEM.name) { - themeName - } else { - if (darkForSystemTheme) appPrefs.systemDarkTheme.get()!! else DefaultTheme.LIGHT.name + fun saveAndApplyThemeOverrides(theme: ThemeOverrides, pref: SharedPreference> = appPrefs.themeOverrides) { + val wallpaper = theme.wallpaper?.importFromString() + val nonSystemThemeName = theme.base.themeName + val overrides = pref.get() + val prevValue = overrides.getTheme(null, wallpaper?.toAppWallpaper()?.type, theme.base) ?: ThemeOverrides(base = theme.base) + if (prevValue.wallpaper?.imageFile != null) { + File(getWallpaperFilePath(prevValue.wallpaper.imageFile)).delete() + } + pref.set(overrides.replace(prevValue.copy(base = theme.base, colors = theme.colors, wallpaper = wallpaper))) + appPrefs.currentTheme.set(nonSystemThemeName) + val currentThemeIds = appPrefs.currentThemeIds.get().toMutableMap() + currentThemeIds[nonSystemThemeName] = prevValue.themeId + appPrefs.currentThemeIds.set(currentThemeIds) + CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + } + + fun resetAllThemeColors(pref: SharedPreference> = appPrefs.themeOverrides) { + val nonSystemThemeName = nonSystemThemeName() + val themeId = appPrefs.currentThemeIds.get()[nonSystemThemeName] ?: return + val overrides = pref.get() + val prevValue = overrides.getTheme(themeId) ?: return + pref.set(overrides.replace(prevValue.copy(colors = ThemeColors(), wallpaper = prevValue.wallpaper?.copy(background = null, tint = null)))) + CurrentColors.value = currentColors(null, null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + } + + fun resetAllThemeColors(pref: MutableState) { + val prevValue = pref.value + pref.value = prevValue.copy(colors = ThemeColors(), wallpaper = prevValue.wallpaper?.copy(background = null, tint = null)) + } + + fun removeTheme(themeId: String?) { + val themes = ArrayList(appPrefs.themeOverrides.get()) + themes.removeAll { it.themeId == themeId } + appPrefs.themeOverrides.set(themes) + } + + fun String.colorFromReadableHex(): Color = + Color(this.replace("#", "").toLongOrNull(16) ?: Color.White.toArgb().toLong()) + + fun Color.toReadableHex(): String { + val s = Integer.toHexString(toArgb()) + return when { + this == Color.Transparent -> "#00ffffff" + s.length == 1 -> "#ff$s$s$s$s$s$s" + s.length == 2 -> "#ff$s$s$s" + s.length == 3 -> "#ff$s$s" + s.length == 4 && this.alpha == 0f -> "#0000$s" // 000088ff treated as 88ff + s.length == 4 -> "#ff00$s" + s.length == 6 && this.alpha == 0f -> "#00$s" + s.length == 6 -> "#ff$s" + else -> "#$s" } - val overrides = appPrefs.themeOverrides.get().toMutableMap() - val prevValue = overrides[nonSystemThemeName] ?: return - overrides[nonSystemThemeName] = prevValue.copy(colors = ThemeColors()) - appPrefs.themeOverrides.set(overrides) - CurrentColors.value = currentColors(!CurrentColors.value.colors.isLight) } } - -private fun Color.toReadableHex(): String = "#" + Integer.toHexString(toArgb()) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt index 1f49a98728..1afbb0bdcb 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/TerminalView.kt @@ -20,6 +20,8 @@ import chat.simplex.common.views.chat.* import chat.simplex.common.views.helpers.* import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.stringResource @Composable fun TerminalView(chatModel: ChatModel, close: () -> Unit) { @@ -77,29 +79,33 @@ fun TerminalLayout( Scaffold( topBar = { CloseSheetBar(close) }, bottomBar = { - Box(Modifier.padding(horizontal = 8.dp)) { - SendMsgView( - composeState = composeState, - showVoiceRecordIcon = false, - recState = remember { mutableStateOf(RecordingState.NotStarted) }, - isDirectChat = false, - liveMessageAlertShown = SharedPreference(get = { false }, set = {}), - sendMsgEnabled = true, - sendButtonEnabled = true, - nextSendGrpInv = false, - needToAllowVoiceToContact = false, - allowedVoiceByPrefs = false, - userIsObserver = false, - userCanSend = true, - allowVoiceToContact = {}, - sendMessage = { sendCommand() }, - sendLiveMessage = null, - updateLiveMessage = null, - editPrevMessage = {}, - onMessageChange = ::onMessageChange, - onFilesPasted = {}, - textStyle = textStyle - ) + Column { + Divider() + Box(Modifier.padding(horizontal = 8.dp)) { + SendMsgView( + composeState = composeState, + showVoiceRecordIcon = false, + recState = remember { mutableStateOf(RecordingState.NotStarted) }, + isDirectChat = false, + liveMessageAlertShown = SharedPreference(get = { false }, set = {}), + sendMsgEnabled = true, + sendButtonEnabled = true, + nextSendGrpInv = false, + needToAllowVoiceToContact = false, + allowedVoiceByPrefs = false, + userIsObserver = false, + userCanSend = true, + allowVoiceToContact = {}, + placeholder = "", + sendMessage = { sendCommand() }, + sendLiveMessage = null, + updateLiveMessage = null, + editPrevMessage = {}, + onMessageChange = ::onMessageChange, + onFilesPasted = {}, + textStyle = textStyle + ) + } } }, contentColor = LocalContentColor.current, @@ -119,19 +125,13 @@ fun TerminalLayout( } } -private var lazyListState = 0 to 0 - @Composable fun TerminalLog() { - val listState = rememberLazyListState(lazyListState.first, lazyListState.second) - DisposableEffect(Unit) { - onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset } - } val reversedTerminalItems by remember { derivedStateOf { chatModel.terminalItems.value.asReversed() } } val clipboard = LocalClipboardManager.current - LazyColumnWithScrollBar(state = listState, reverseLayout = true) { + LazyColumnWithScrollBar(reverseLayout = true) { items(reversedTerminalItems) { item -> val rhId = item.remoteHostId val rhIdStr = if (rhId == null) "" else "$rhId " diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt index 41b30be640..f5dcc6b54a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/WelcomeView.kt @@ -2,8 +2,8 @@ package chat.simplex.common.views import SectionTextFooter import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.material.* import androidx.compose.material.MaterialTheme.colors @@ -17,11 +17,12 @@ import androidx.compose.ui.graphics.SolidColor import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.* import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* @@ -110,53 +111,59 @@ fun CreateFirstProfile(chatModel: ChatModel, close: () -> Unit) { val scrollState = rememberScrollState() val keyboardState by getKeyboardState() var savedKeyboardState by remember { mutableStateOf(keyboardState) } - - ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { - Box( - modifier = Modifier - .fillMaxSize() - .padding(top = 20.dp) - ) { - val displayName = rememberSaveable { mutableStateOf("") } - val focusRequester = remember { FocusRequester() } - - ColumnWithScrollBar( - modifier = Modifier.fillMaxSize() + val handler = remember { AppBarHandler() } + CompositionLocalProvider( + LocalAppBarHandler provides handler + ) { + ProvideWindowInsets(windowInsetsAnimationsEnabled = true) { + Column( + modifier = Modifier + .fillMaxSize() + .themedBackground(), + horizontalAlignment = Alignment.CenterHorizontally ) { - /*CloseSheetBar(close = { - if (chatModel.users.isEmpty()) { - chatModel.onboardingStage.value = OnboardingStage.Step1_SimpleXInfo + CloseSheetBar(close = { + if (chatModel.users.none { !it.user.hidden }) { + appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) } else { close() } - })*/ - Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { - AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING) - ReadableText(MR.strings.your_profile_is_stored_on_your_device, TextAlign.Center, padding = PaddingValues(), style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary)) - ReadableText(MR.strings.profile_is_only_shared_with_your_contacts, TextAlign.Center, style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary)) - Spacer(Modifier.height(DEFAULT_PADDING)) - Row(Modifier.padding(bottom = DEFAULT_PADDING_HALF).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { - Text( - stringResource(MR.strings.display_name), - fontSize = 16.sp - ) - val name = displayName.value.trim() - val validName = mkValidName(name) - Spacer(Modifier.height(20.dp)) - if (name != validName) { - IconButton({ showInvalidNameAlert(mkValidName(displayName.value), displayName) }, Modifier.size(20.dp)) { - Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error) - } - } - } - ProfileNameField(displayName, "", { it.trim() == mkValidName(it) }, focusRequester) - } - Spacer(Modifier.fillMaxHeight().weight(1f)) - OnboardingButtons(displayName, close) + }) + BackHandler(onBack = { + appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) + }) - LaunchedEffect(Unit) { - delay(300) - focusRequester.requestFocus() + ColumnWithScrollBar( + modifier = Modifier.fillMaxSize() + ) { + val displayName = rememberSaveable { mutableStateOf("") } + val focusRequester = remember { FocusRequester() } + Column(if (appPlatform.isAndroid) Modifier.fillMaxSize().padding(horizontal = DEFAULT_PADDING) else Modifier.widthIn(max = 600.dp).fillMaxHeight().padding(horizontal = DEFAULT_PADDING).align(Alignment.CenterHorizontally)) { + Box(Modifier.align(Alignment.CenterHorizontally)) { + AppBarTitle(stringResource(MR.strings.create_profile), bottomPadding = DEFAULT_PADDING, withPadding = false) + } + ProfileNameField(displayName, stringResource(MR.strings.display_name), { it.trim() == mkValidName(it) }, focusRequester) + Spacer(Modifier.height(DEFAULT_PADDING)) + ReadableText(MR.strings.your_profile_is_stored_on_your_device, TextAlign.Start, padding = PaddingValues(), style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary)) + ReadableText(MR.strings.profile_is_only_shared_with_your_contacts, TextAlign.Start, style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary)) + } + Spacer(Modifier.fillMaxHeight().weight(1f)) + Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { + OnboardingActionButton( + if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp), + labelId = MR.strings.create_profile_button, + onboarding = null, + enabled = canCreateProfile(displayName.value), + onclick = { createProfileOnboarding(chat.simplex.common.platform.chatModel, displayName.value, close) } + ) + // Reserve space + TextButtonBelowOnboardingButton("", null) + } + + LaunchedEffect(Unit) { + delay(300) + focusRequester.requestFocus() + } } } LaunchedEffect(Unit) { @@ -230,28 +237,6 @@ fun createProfileOnboarding(chatModel: ChatModel, displayName: String, close: () } } -@Composable -fun OnboardingButtons(displayName: MutableState, close: () -> Unit) { - Row { - SimpleButtonDecorated( - text = stringResource(MR.strings.about_simplex), - icon = painterResource(MR.images.ic_arrow_back_ios_new), - textDecoration = TextDecoration.None, - fontWeight = FontWeight.Medium - ) { chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) } - Spacer(Modifier.fillMaxWidth().weight(1f)) - val enabled = canCreateProfile(displayName.value) - val createModifier: Modifier = Modifier.clickable(enabled) { createProfileOnboarding(chatModel, displayName.value, close) }.padding(8.dp) - val createColor: Color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary - Surface(shape = RoundedCornerShape(20.dp), color = Color.Transparent, contentColor = LocalContentColor.current) { - Row(verticalAlignment = Alignment.CenterVertically, modifier = createModifier) { - Text(stringResource(MR.strings.create_profile_button), style = MaterialTheme.typography.caption, color = createColor, fontWeight = FontWeight.Medium) - Icon(painterResource(MR.images.ic_arrow_forward_ios), stringResource(MR.strings.create_profile_button), tint = createColor) - } - } - } -} - @Composable fun ProfileNameField(name: MutableState, placeholder: String = "", isValid: (String) -> Boolean = { true }, focusRequester: FocusRequester? = null) { var valid by rememberSaveable { mutableStateOf(true) } @@ -269,15 +254,13 @@ fun ProfileNameField(name: MutableState, placeholder: String = "", isVal } val modifier = Modifier .fillMaxWidth() - .padding(horizontal = DEFAULT_PADDING) + .heightIn(min = 50.dp) .navigationBarsWithImePadding() .onFocusChanged { focused = it.isFocused } - Box( + Column( Modifier - .fillMaxWidth() - .height(52.dp) - .border(border = BorderStroke(1.dp, strokeColor), shape = RoundedCornerShape(50)), - contentAlignment = Alignment.Center + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally ) { BasicTextField( value = name.value, @@ -285,8 +268,31 @@ fun ProfileNameField(name: MutableState, placeholder: String = "", isVal modifier = if (focusRequester == null) modifier else modifier.focusRequester(focusRequester), textStyle = TextStyle(fontSize = 18.sp, color = colors.onBackground), singleLine = true, - cursorBrush = SolidColor(MaterialTheme.colors.secondary) + cursorBrush = SolidColor(MaterialTheme.colors.secondary), + decorationBox = @Composable { innerTextField -> + TextFieldDefaults.TextFieldDecorationBox( + value = name.value, + innerTextField = innerTextField, + placeholder = if (placeholder != "") {{ Text(placeholder, style = MaterialTheme.typography.body1.copy(color = MaterialTheme.colors.secondary, lineHeight = 22.sp)) }} else null, + contentPadding = PaddingValues(), + label = null, + visualTransformation = VisualTransformation.None, + leadingIcon = null, + trailingIcon = if (!valid && placeholder != "") { + { + IconButton({ showInvalidNameAlert(mkValidName(name.value), name) }, Modifier.size(20.dp)) { + Icon(painterResource(MR.images.ic_info), null, tint = MaterialTheme.colors.error) + } + } + } else null, + singleLine = true, + enabled = true, + isError = false, + interactionSource = remember { MutableInteractionSource() }, + ) + } ) + Divider(color = strokeColor) } LaunchedEffect(Unit) { snapshotFlow { name.value } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index dcd36e026b..24416ff49e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -12,24 +12,30 @@ import SectionView import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.* import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.* +import chat.simplex.common.views.call.CallMediaType +import chat.simplex.common.views.chatlist.* import androidx.compose.ui.text.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* @@ -41,6 +47,8 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch import kotlinx.datetime.Clock +import kotlinx.serialization.encodeToString +import java.io.File @Composable fun ChatInfoView( @@ -51,10 +59,11 @@ fun ChatInfoView( localAlias: String, connectionCode: String?, close: () -> Unit, + onSearchClicked: () -> Unit ) { BackHandler(onBack = close) val contact = rememberUpdatedState(contact).value - val chat = remember(contact.id) { chatModel.chats.firstOrNull { it.id == contact.id } } + val chat = remember(contact.id) { chatModel.chats.value.firstOrNull { it.id == contact.id } } val currentUser = remember { chatModel.currentUser }.value val connStats = remember(contact.id, connectionStats) { mutableStateOf(connectionStats) } val developerTools = chatModel.controller.appPrefs.developerTools.get() @@ -71,7 +80,7 @@ fun ChatInfoView( sendReceipts = sendReceipts, setSendReceipts = { sendRcpts -> val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(sendRcpts = sendRcpts.bool) - updateChatSettings(chat, chatSettings, chatModel) + updateChatSettings(chat.remoteHostId, chat.chatInfo, chatSettings, chatModel) sendReceipts.value = sendRcpts }, connStats = connStats, @@ -99,7 +108,9 @@ fun ChatInfoView( val cStats = chatModel.controller.apiSwitchContact(chatRh, contact.contactId) connStats.value = cStats if (cStats != null) { - chatModel.updateContactConnectionStats(chatRh, contact, cStats) + withChats { + updateContactConnectionStats(chatRh, contact, cStats) + } } close.invoke() } @@ -111,7 +122,9 @@ fun ChatInfoView( val cStats = chatModel.controller.apiAbortSwitchContact(chatRh, contact.contactId) connStats.value = cStats if (cStats != null) { - chatModel.updateContactConnectionStats(chatRh, contact, cStats) + withChats { + updateContactConnectionStats(chatRh, contact, cStats) + } } } }) @@ -121,7 +134,9 @@ fun ChatInfoView( val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false) connStats.value = cStats if (cStats != null) { - chatModel.updateContactConnectionStats(chatRh, contact, cStats) + withChats { + updateContactConnectionStats(chatRh, contact, cStats) + } } close.invoke() } @@ -132,7 +147,9 @@ fun ChatInfoView( val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = true) connStats.value = cStats if (cStats != null) { - chatModel.updateContactConnectionStats(chatRh, contact, cStats) + withChats { + updateContactConnectionStats(chatRh, contact, cStats) + } } close.invoke() } @@ -148,14 +165,16 @@ fun ChatInfoView( verify = { code -> chatModel.controller.apiVerifyContact(chatRh, ct.contactId, code)?.let { r -> val (verified, existingCode) = r - chatModel.updateContact( - chatRh, - ct.copy( - activeConn = ct.activeConn?.copy( - connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null + withChats { + updateContact( + chatRh, + ct.copy( + activeConn = ct.activeConn?.copy( + connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null + ) ) ) - ) + } r } }, @@ -163,7 +182,9 @@ fun ChatInfoView( ) } } - } + }, + close = close, + onSearchClicked = onSearchClicked ) } } @@ -198,34 +219,42 @@ sealed class SendReceipts { fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = null) { val chatInfo = chat.chatInfo + if (chatInfo is ChatInfo.Direct) { + val contact = chatInfo.contact + when { + contact.sndReady && contact.active && !chatInfo.chatDeleted -> + deleteContactOrConversationDialog(chat, contact, chatModel, close) + + contact.sndReady && contact.active && chatInfo.chatDeleted -> + deleteContactWithoutConversation(chat, chatModel, close) + + else -> // !(contact.sndReady && contact.active) + deleteNotReadyContact(chat, chatModel, close) + } + } +} + +private fun deleteContactOrConversationDialog(chat: Chat, contact: Contact, chatModel: ChatModel, close: (() -> Unit)?) { AlertManager.shared.showAlertDialogButtonsColumn( title = generalGetString(MR.strings.delete_contact_question), - text = AnnotatedString(generalGetString(MR.strings.delete_contact_all_messages_deleted_cannot_undo_warning)), buttons = { Column { - if (chatInfo is ChatInfo.Direct && chatInfo.contact.ready && chatInfo.contact.active) { - // Delete and notify contact - SectionItemView({ - AlertManager.shared.hideAlert() - deleteContact(chat, chatModel, close, notify = true) - }) { - Text(generalGetString(MR.strings.delete_and_notify_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) - } - // Delete - SectionItemView({ - AlertManager.shared.hideAlert() - deleteContact(chat, chatModel, close, notify = false) - }) { - Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) - } - } else { - // Delete - SectionItemView({ - AlertManager.shared.hideAlert() - deleteContact(chat, chatModel, close) - }) { - Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + // Only delete conversation + SectionItemView({ + AlertManager.shared.hideAlert() + deleteContact(chat, chatModel, close, chatDeleteMode = ChatDeleteMode.Messages()) + if (chatModel.controller.appPrefs.showDeleteConversationNotice.get()) { + showDeleteConversationNotice(contact) } + }) { + Text(generalGetString(MR.strings.only_delete_conversation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + // Delete contact + SectionItemView({ + AlertManager.shared.hideAlert() + deleteActiveContactDialog(chat, contact, chatModel, close) + }) { + Text(generalGetString(MR.strings.button_delete_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) } // Cancel SectionItemView({ @@ -238,13 +267,207 @@ fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = ) } -fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, notify: Boolean? = null) { +private fun showDeleteConversationNotice(contact: Contact) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.conversation_deleted), + text = String.format(generalGetString(MR.strings.you_can_still_send_messages_to_contact), contact.displayName), + confirmText = generalGetString(MR.strings.ok), + dismissText = generalGetString(MR.strings.dont_show_again), + onDismiss = { + chatModel.controller.appPrefs.showDeleteConversationNotice.set(false) + }, + ) +} + +sealed class ContactDeleteMode { + class Full: ContactDeleteMode() + class Entity: ContactDeleteMode() + + fun toChatDeleteMode(notify: Boolean): ChatDeleteMode = + when (this) { + is Full -> ChatDeleteMode.Full(notify) + is Entity -> ChatDeleteMode.Entity(notify) + } +} + +private fun deleteActiveContactDialog(chat: Chat, contact: Contact, chatModel: ChatModel, close: (() -> Unit)? = null) { + val contactDeleteMode = mutableStateOf(ContactDeleteMode.Full()) + + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.delete_contact_question), + text = generalGetString(MR.strings.delete_contact_cannot_undo_warning), + buttons = { + Column { + // Keep conversation toggle + SectionItemView { + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text(stringResource(MR.strings.keep_conversation)) + Spacer(Modifier.width(DEFAULT_PADDING)) + DefaultSwitch( + checked = contactDeleteMode.value is ContactDeleteMode.Entity, + onCheckedChange = { + contactDeleteMode.value = + if (it) ContactDeleteMode.Entity() else ContactDeleteMode.Full() + }, + ) + } + } + // Delete without notification + SectionItemView({ + AlertManager.shared.hideAlert() + deleteContact(chat, chatModel, close, chatDeleteMode = contactDeleteMode.value.toChatDeleteMode(notify = false)) + if (contactDeleteMode.value is ContactDeleteMode.Entity && chatModel.controller.appPrefs.showDeleteContactNotice.get()) { + showDeleteContactNotice(contact) + } + }) { + Text(generalGetString(MR.strings.delete_without_notification), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + // Delete contact and notify + SectionItemView({ + AlertManager.shared.hideAlert() + deleteContact(chat, chatModel, close, chatDeleteMode = contactDeleteMode.value.toChatDeleteMode(notify = true)) + if (contactDeleteMode.value is ContactDeleteMode.Entity && chatModel.controller.appPrefs.showDeleteContactNotice.get()) { + showDeleteContactNotice(contact) + } + }) { + Text(generalGetString(MR.strings.delete_and_notify_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + // Cancel + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + ) +} + +private fun deleteContactWithoutConversation(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.confirm_delete_contact_question), + text = generalGetString(MR.strings.delete_contact_cannot_undo_warning), + buttons = { + Column { + // Delete and notify contact + SectionItemView({ + AlertManager.shared.hideAlert() + deleteContact( + chat, + chatModel, + close, + chatDeleteMode = ContactDeleteMode.Full().toChatDeleteMode(notify = true) + ) + }) { + Text( + generalGetString(MR.strings.delete_and_notify_contact), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.error + ) + } + // Delete without notification + SectionItemView({ + AlertManager.shared.hideAlert() + deleteContact( + chat, + chatModel, + close, + chatDeleteMode = ContactDeleteMode.Full().toChatDeleteMode(notify = false) + ) + }) { + Text( + generalGetString(MR.strings.delete_without_notification), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.error + ) + } + } + // Cancel + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text( + stringResource(MR.strings.cancel_verb), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.primary + ) + } + } + ) +} + +private fun deleteNotReadyContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.confirm_delete_contact_question), + text = generalGetString(MR.strings.delete_contact_cannot_undo_warning), + buttons = { + // Confirm + SectionItemView({ + AlertManager.shared.hideAlert() + deleteContact( + chat, + chatModel, + close, + chatDeleteMode = ContactDeleteMode.Full().toChatDeleteMode(notify = false) + ) + }) { + Text( + generalGetString(MR.strings.confirm_verb), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.error + ) + } + // Cancel + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text( + stringResource(MR.strings.cancel_verb), + Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + color = MaterialTheme.colors.primary + ) + } + } + ) +} + +private fun showDeleteContactNotice(contact: Contact) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.contact_deleted), + text = String.format(generalGetString(MR.strings.you_can_still_view_conversation_with_contact), contact.displayName), + confirmText = generalGetString(MR.strings.ok), + dismissText = generalGetString(MR.strings.dont_show_again), + onDismiss = { + chatModel.controller.appPrefs.showDeleteContactNotice.set(false) + }, + ) +} + +fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)) { val chatInfo = chat.chatInfo withBGApi { val chatRh = chat.remoteHostId - val r = chatModel.controller.apiDeleteChat(chatRh, chatInfo.chatType, chatInfo.apiId, notify) - if (r) { - chatModel.removeChat(chatRh, chatInfo.id) + val ct = chatModel.controller.apiDeleteContact(chatRh, chatInfo.apiId, chatDeleteMode) + if (ct != null) { + withChats { + when (chatDeleteMode) { + is ChatDeleteMode.Full -> + removeChat(chatRh, chatInfo.id) + is ChatDeleteMode.Entity -> + updateContact(chatRh, ct) + is ChatDeleteMode.Messages -> + clearChat(chatRh, ChatInfo.Direct(ct)) + } + } if (chatModel.chatId.value == chatInfo.id) { chatModel.chatId.value = null ModalManager.end.closeModals() @@ -297,6 +520,8 @@ fun ChatInfoLayout( syncContactConnection: () -> Unit, syncContactConnectionForce: () -> Unit, verifyClicked: () -> Unit, + close: () -> Unit, + onSearchClicked: () -> Unit ) { val cStats = connStats.value val scrollState = rememberScrollState() @@ -316,7 +541,29 @@ fun ChatInfoLayout( } LocalAliasEditor(chat.id, localAlias, updateValue = onLocalAliasChanged) + SectionSpacer() + + Box( + Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Row( + Modifier + .widthIn(max = 460.dp) + .padding(horizontal = DEFAULT_PADDING), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + SearchButton(modifier = Modifier.fillMaxWidth(0.25f), chat, contact, close, onSearchClicked) + AudioCallButton(modifier = Modifier.fillMaxWidth(0.33f), chat, contact) + VideoButton(modifier = Modifier.fillMaxWidth(0.5f), chat, contact) + MuteButton(modifier = Modifier.fillMaxWidth(1f), chat, contact) + } + } + + SectionSpacer() + if (customUserProfile != null) { SectionView(generalGetString(MR.strings.incognito).uppercase()) { SectionItemViewSpaceBetween { @@ -327,8 +574,8 @@ fun ChatInfoLayout( SectionDividerSpaced() } - if (contact.ready && contact.active) { - SectionView { + SectionView { + if (contact.ready && contact.active) { if (connectionCode != null) { VerifyCodeButton(contact.verified, verifyClicked) } @@ -337,12 +584,22 @@ fun ChatInfoLayout( if (cStats != null && cStats.ratchetSyncAllowed) { SynchronizeConnectionButton(syncContactConnection) } - // } else if (developerTools) { - // SynchronizeConnectionButtonForce(syncContactConnectionForce) - // } + // } else if (developerTools) { + // SynchronizeConnectionButtonForce(syncContactConnectionForce) + // } + } + + WallpaperButton { + ModalManager.end.showModal { + val chat = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chat.id } } } + val c = chat.value + if (c != null) { + ChatWallpaperEditorModal(c) + } + } } - SectionDividerSpaced() } + SectionDividerSpaced(maxBottomPadding = false) val conn = contact.activeConn if (conn != null) { @@ -359,7 +616,7 @@ fun ChatInfoLayout( ShareAddressButton { clipboard.shareText(simplexChatLink(contact.contactLink)) } SectionTextFooter(stringResource(MR.strings.you_can_share_this_address_with_your_contacts).format(contact.displayName)) } - SectionDividerSpaced() + SectionDividerSpaced(maxTopPadding = true) } if (contact.ready && contact.active) { @@ -393,7 +650,7 @@ fun ChatInfoLayout( } } } - SectionDividerSpaced() + SectionDividerSpaced(maxBottomPadding = false) } SectionView { @@ -406,6 +663,19 @@ fun ChatInfoLayout( SectionView(title = stringResource(MR.strings.section_title_for_console)) { InfoRow(stringResource(MR.strings.info_row_local_name), chat.chatInfo.localDisplayName) InfoRow(stringResource(MR.strings.info_row_database_id), chat.chatInfo.apiId.toString()) + SectionItemView({ + withBGApi { + val info = controller.apiContactQueueInfo(chat.remoteHostId, chat.chatInfo.apiId) + if (info != null) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.message_queue_info), + text = queueInfoText(info) + ) + } + } + }) { + Text(stringResource(MR.strings.info_row_debug_delivery)) + } } } SectionBottomSpacer() @@ -509,6 +779,214 @@ fun LocalAliasEditor( } } +@Composable +fun SearchButton( + modifier: Modifier, + chat: Chat, + contact: Contact, + close: () -> Unit, + onSearchClicked: () -> Unit +) { + val disabled = !contact.ready || chat.chatItems.isEmpty() + InfoViewActionButton( + modifier = modifier, + icon = painterResource(MR.images.ic_search), + title = generalGetString(MR.strings.info_view_search_button), + disabled = disabled, + disabledLook = disabled, + onClick = { + if (appPlatform.isAndroid) { + close.invoke() + } + onSearchClicked() + } + ) +} + +@Composable +fun MuteButton( + modifier: Modifier, + chat: Chat, + contact: Contact +) { + val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) } + val disabled = !contact.ready || !contact.active + + InfoViewActionButton( + modifier = modifier, + icon = if (ntfsEnabled.value) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications), + title = if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat), + disabled = disabled, + disabledLook = disabled, + onClick = { + toggleNotifications(chat.remoteHostId, chat.chatInfo, !ntfsEnabled.value, chatModel, ntfsEnabled) + } + ) +} + +@Composable +fun AudioCallButton( + modifier: Modifier, + chat: Chat, + contact: Contact +) { + CallButton( + modifier = modifier, + chat, + contact, + icon = painterResource(MR.images.ic_call), + title = generalGetString(MR.strings.info_view_call_button), + mediaType = CallMediaType.Audio + ) +} + +@Composable +fun VideoButton( + modifier: Modifier, + chat: Chat, + contact: Contact +) { + CallButton( + modifier = modifier, + chat, + contact, + icon = painterResource(MR.images.ic_videocam), + title = generalGetString(MR.strings.info_view_video_button), + mediaType = CallMediaType.Video + ) +} + +@Composable +fun CallButton( + modifier: Modifier, + chat: Chat, + contact: Contact, + icon: Painter, + title: String, + mediaType: CallMediaType +) { + val canCall = contact.ready && contact.active && contact.mergedPreferences.calls.enabled.forUser && chatModel.activeCall.value == null + val needToAllowCallsToContact = remember(chat.chatInfo) { + chat.chatInfo is ChatInfo.Direct && with(chat.chatInfo.contact.mergedPreferences.calls) { + ((userPreference as? ContactUserPref.User)?.preference?.allow == FeatureAllowed.NO || (userPreference as? ContactUserPref.Contact)?.preference?.allow == FeatureAllowed.NO) && + contactPreference.allow == FeatureAllowed.YES + } + } + val allowedCallsByPrefs = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.Calls) } + + InfoViewActionButton( + modifier = modifier, + icon = icon, + title = title, + disabled = chatModel.activeCall.value != null, + disabledLook = !canCall, + onClick = + when { + canCall -> { { startChatCall(chat.remoteHostId, chat.chatInfo, mediaType) } } + contact.nextSendGrpInv -> { { showCantCallContactSendMessageAlert() } } + !contact.active -> { { showCantCallContactDeletedAlert() } } + !contact.ready -> { { showCantCallContactConnectingAlert() } } + needToAllowCallsToContact -> { { showNeedToAllowCallsAlert(onConfirm = { allowCallsToContact(chat) }) } } + !allowedCallsByPrefs -> { { showCallsProhibitedAlert() }} + else -> { { AlertManager.shared.showAlertMsg(title = generalGetString(MR.strings.cant_call_contact_alert_title)) } } + } + ) +} + +private fun showCantCallContactSendMessageAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.cant_call_contact_alert_title), + text = generalGetString(MR.strings.cant_call_member_send_message_alert_text) + ) +} + +private fun showCantCallContactConnectingAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.cant_call_contact_alert_title), + text = generalGetString(MR.strings.cant_call_contact_connecting_wait_alert_text) + ) +} + +private fun showCantCallContactDeletedAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.cant_call_contact_alert_title), + text = generalGetString(MR.strings.cant_call_contact_deleted_alert_text) + ) +} + +private fun showNeedToAllowCallsAlert(onConfirm: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.allow_calls_question), + text = generalGetString(MR.strings.you_need_to_allow_calls), + confirmText = generalGetString(MR.strings.allow_verb), + dismissText = generalGetString(MR.strings.cancel_verb), + onConfirm = onConfirm, + ) +} + +private fun allowCallsToContact(chat: Chat) { + val contact = (chat.chatInfo as ChatInfo.Direct?)?.contact ?: return + withBGApi { + chatModel.controller.allowFeatureToContact(chat.remoteHostId, contact, ChatFeature.Calls) + } +} + +private fun showCallsProhibitedAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.calls_prohibited_alert_title), + text = generalGetString(MR.strings.calls_prohibited_ask_to_enable_calls_alert_text) + ) +} + +@Composable +fun InfoViewActionButton( + modifier: Modifier, + icon: Painter, + title: String, + disabled: Boolean, + disabledLook: Boolean, + onClick: () -> Unit +) { + Box(modifier) { + Column( + Modifier + .fillMaxWidth() + .padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + IconButton( + onClick = onClick, + enabled = !disabled + ) { + Box( + Modifier + .size(56.dp) + .background( + if (disabledLook) MaterialTheme.colors.secondaryVariant else MaterialTheme.colors.primary, + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Icon( + icon, + contentDescription = null, + Modifier.size(22.dp * fontSizeSqrtMultiplier), + tint = if (disabledLook) MaterialTheme.colors.secondary else MaterialTheme.colors.onPrimary + ) + } + } + Text( + title.capitalize(Locale.current), + Modifier.padding(top = DEFAULT_SPACE_AFTER_ICON), + style = MaterialTheme.typography.subtitle2.copy(fontWeight = FontWeight.Normal, fontSize = 12.sp), + color = MaterialTheme.colors.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } +} + @Composable private fun NetworkStatusRow(networkStatus: NetworkStatus) { Row( @@ -642,6 +1120,15 @@ private fun SendReceiptsOption(currentUser: User, state: State, on ) } +@Composable +fun WallpaperButton(onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_image), + stringResource(MR.strings.settings_section_title_chat_theme), + click = onClick + ) +} + @Composable fun ClearChatButton(onClick: () -> Unit) { SettingsActionItem( @@ -675,10 +1162,80 @@ fun ShareAddressButton(onClick: () -> Unit) { ) } +@Composable +fun ModalData.ChatWallpaperEditorModal(chat: Chat) { + val themes = remember(CurrentColors.collectAsState().value.base) { + (chat.chatInfo as? ChatInfo.Direct)?.contact?.uiThemes + ?: (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.uiThemes + ?: ThemeModeOverrides() + } + val globalThemeUsed = remember { stateGetOrPut("globalThemeUsed") { false } } + val initialTheme = remember(CurrentColors.collectAsState().value.base) { + val preferred = themes.preferredMode(!CurrentColors.value.colors.isLight) + globalThemeUsed.value = preferred == null + preferred ?: ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + } + ChatWallpaperEditor( + initialTheme, + applyToMode = if (themes.light == themes.dark) null else initialTheme.mode, + globalThemeUsed = globalThemeUsed, + save = { applyToMode, newTheme -> + save(applyToMode, newTheme, chatModel.getChat(chat.id) ?: chat) + }) +} + +suspend fun save(applyToMode: DefaultThemeMode?, newTheme: ThemeModeOverride?, chat: Chat) { + val unchangedThemes: ThemeModeOverrides = ((chat.chatInfo as? ChatInfo.Direct)?.contact?.uiThemes ?: (chat.chatInfo as? ChatInfo.Group)?.groupInfo?.uiThemes) ?: ThemeModeOverrides() + val wallpaperFiles = setOf(unchangedThemes.light?.wallpaper?.imageFile, unchangedThemes.dark?.wallpaper?.imageFile) + var changedThemes: ThemeModeOverrides? = unchangedThemes + val changed = newTheme?.copy(wallpaper = newTheme.wallpaper?.withFilledWallpaperPath()) + changedThemes = when (applyToMode) { + null -> changedThemes?.copy(light = changed?.copy(mode = DefaultThemeMode.LIGHT), dark = changed?.copy(mode = DefaultThemeMode.DARK)) + DefaultThemeMode.LIGHT -> changedThemes?.copy(light = changed?.copy(mode = applyToMode)) + DefaultThemeMode.DARK -> changedThemes?.copy(dark = changed?.copy(mode = applyToMode)) + } + changedThemes = if (changedThemes?.light != null || changedThemes?.dark != null) { + val light = changedThemes.light + val dark = changedThemes.dark + val currentMode = CurrentColors.value.base.mode + // same image file for both modes, copy image to make them as different files + if (light?.wallpaper?.imageFile != null && dark?.wallpaper?.imageFile != null && light.wallpaper.imageFile == dark.wallpaper.imageFile) { + val imageFile = if (currentMode == DefaultThemeMode.LIGHT) { + dark.wallpaper.imageFile + } else { + light.wallpaper.imageFile + } + val filePath = saveWallpaperFile(File(getWallpaperFilePath(imageFile)).toURI()) + changedThemes = if (currentMode == DefaultThemeMode.LIGHT) { + changedThemes.copy(dark = dark.copy(wallpaper = dark.wallpaper.copy(imageFile = filePath))) + } else { + changedThemes.copy(light = light.copy(wallpaper = light.wallpaper.copy(imageFile = filePath))) + } + } + changedThemes + } else { + null + } + val wallpaperFilesToDelete = wallpaperFiles - changedThemes?.light?.wallpaper?.imageFile - changedThemes?.dark?.wallpaper?.imageFile + wallpaperFilesToDelete.forEach(::removeWallpaperFile) + + if (controller.apiSetChatUIThemes(chat.remoteHostId, chat.id, changedThemes)) { + withChats { + if (chat.chatInfo is ChatInfo.Direct) { + updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(contact = chat.chatInfo.contact.copy(uiThemes = changedThemes))) + } else if (chat.chatInfo is ChatInfo.Group) { + updateChatInfo(chat.remoteHostId, chat.chatInfo.copy(groupInfo = chat.chatInfo.groupInfo.copy(uiThemes = changedThemes))) + } + } + } +} + private fun setContactAlias(chat: Chat, localAlias: String, chatModel: ChatModel) = withBGApi { val chatRh = chat.remoteHostId chatModel.controller.apiSetContactAlias(chatRh, chat.chatInfo.apiId, localAlias)?.let { - chatModel.updateContact(chatRh, it) + withChats { + updateContact(chatRh, it) + } } } @@ -711,6 +1268,12 @@ fun showSyncConnectionForceAlert(syncConnectionForce: () -> Unit) { ) } +fun queueInfoText(info: Pair): String { + val (rcvMsgInfo, qInfo) = info + val msgInfo: String = if (rcvMsgInfo != null) json.encodeToString(rcvMsgInfo) else generalGetString(MR.strings.message_queue_info_none) + return generalGetString(MR.strings.message_queue_info_server_info).format(json.encodeToString(qInfo), msgInfo) +} + @Preview @Composable fun PreviewChatInfoLayout() { @@ -740,6 +1303,8 @@ fun PreviewChatInfoLayout() { syncContactConnection = {}, syncContactConnectionForce = {}, verifyClicked = {}, + close = {}, + onSearchClicked = {} ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt index df8e535f82..d0e972965a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt @@ -28,9 +28,11 @@ import chat.simplex.common.views.chat.item.ItemAction import chat.simplex.common.views.chat.item.MarkdownText import chat.simplex.common.views.helpers.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.group.MemberProfileImage import chat.simplex.common.views.chatlist.* import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource +import kotlinx.serialization.encodeToString sealed class CIInfoTab { class Delivery(val memberDeliveryStatuses: List): CIInfoTab() @@ -42,7 +44,7 @@ sealed class CIInfoTab { @Composable fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools: Boolean) { val sent = ci.chatDir.sent - val appColors = CurrentColors.collectAsState().value.appColors + val appColors = MaterialTheme.appColors val uriHandler = LocalUriHandler.current val selection = remember { mutableStateOf(CIInfoTab.History) } @@ -216,6 +218,27 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools } } + @Composable + fun ExpandableInfoRow(title: String, value: String) { + val expanded = remember { mutableStateOf(false) } + Row( + Modifier + .fillMaxWidth() + .sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT) + .padding(PaddingValues(horizontal = DEFAULT_PADDING)) + .clickable { expanded.value = !expanded.value }, + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text(title, color = MaterialTheme.colors.onBackground) + if (expanded.value) { + Text(value, color = MaterialTheme.colors.secondary) + } else { + Text(value, color = MaterialTheme.colors.secondary, maxLines = 1) + } + } + } + @Composable fun Details() { AppBarTitle(stringResource(if (ci.localNote) MR.strings.saved_message_title else if (sent) MR.strings.sent_message else MR.strings.received_message)) @@ -244,13 +267,16 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools if (devTools) { InfoRow(stringResource(MR.strings.info_row_database_id), ci.meta.itemId.toString()) InfoRow(stringResource(MR.strings.info_row_updated_at), localTimestamp(ci.meta.updatedAt)) + ExpandableInfoRow(stringResource(MR.strings.info_row_message_status), jsonShort.encodeToString(ci.meta.itemStatus)) + if (ci.file != null) { + ExpandableInfoRow(stringResource(MR.strings.info_row_file_status), jsonShort.encodeToString(ci.file.fileStatus)) + } } } } @Composable fun HistoryTab() { - // LALAL SCROLLBAR DOESN'T WORK ColumnWithScrollBar(Modifier.fillMaxWidth()) { Details() SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) @@ -275,7 +301,6 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools @Composable fun QuoteTab(qi: CIQuote) { - // LALAL SCROLLBAR DOESN'T WORK ColumnWithScrollBar(Modifier.fillMaxWidth()) { Details() SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) @@ -289,7 +314,6 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools @Composable fun ForwardedFromTab(forwardedFromItem: AChatItem) { - // LALAL SCROLLBAR DOESN'T WORK ColumnWithScrollBar(Modifier.fillMaxWidth()) { Details() SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) @@ -304,11 +328,11 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools } @Composable - fun MemberDeliveryStatusView(member: GroupMember, status: CIStatus) { + fun MemberDeliveryStatusView(member: GroupMember, status: GroupSndStatus, sentViaProxy: Boolean?) { SectionItemView( padding = PaddingValues(horizontal = 0.dp) ) { - ProfileImage(size = 36.dp, member.image) + MemberProfileImage(size = 36.dp, member) Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON)) Text( member.chatViewName, @@ -317,7 +341,19 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools overflow = TextOverflow.Ellipsis ) Spacer(Modifier.fillMaxWidth().weight(1f)) - val statusIcon = status.statusIcon(MaterialTheme.colors.primary, CurrentColors.value.colors.secondary) + if (sentViaProxy == true) { + Box( + Modifier.size(36.dp), + contentAlignment = Alignment.Center + ) { + Icon( + painterResource(MR.images.ic_arrow_forward), + contentDescription = null, + tint = CurrentColors.value.colors.secondary + ) + } + } + val (icon, statusColor) = status.statusIcon(MaterialTheme.colors.primary, CurrentColors.value.colors.secondary) var modifier = Modifier.size(36.dp).clip(RoundedCornerShape(20.dp)) val info = status.statusInto if (info != null) { @@ -329,27 +365,17 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools } } Box(modifier, contentAlignment = Alignment.Center) { - if (statusIcon != null) { - val (icon, statusColor) = statusIcon - Icon( - painterResource(icon), - contentDescription = null, - tint = statusColor - ) - } else { - Icon( - painterResource(MR.images.ic_more_horiz), - contentDescription = null, - tint = CurrentColors.value.colors.secondary - ) - } + Icon( + painterResource(icon), + contentDescription = null, + tint = statusColor + ) } } } @Composable fun DeliveryTab(memberDeliveryStatuses: List) { - // LALAL SCROLLBAR DOESN'T WORK ColumnWithScrollBar(Modifier.fillMaxWidth()) { Details() SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = true) @@ -357,8 +383,8 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools if (mss.isNotEmpty()) { SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { Text(stringResource(MR.strings.delivery), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING)) - mss.forEach { (member, status) -> - MemberDeliveryStatusView(member, status) + mss.forEach { (member, status, sentViaProxy) -> + MemberDeliveryStatusView(member, status, sentViaProxy) } } } else { @@ -482,10 +508,10 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools } } -private fun membersStatuses(chatModel: ChatModel, memberDeliveryStatuses: List): List> { +private fun membersStatuses(chatModel: ChatModel, memberDeliveryStatuses: List): List> { return memberDeliveryStatuses.mapNotNull { mds -> chatModel.getGroupMember(mds.groupMemberId)?.let { mem -> - mem to mds.memberDeliveryStatus + Triple(mem, mds.memberDeliveryStatus, mds.sentViaProxy) } } } @@ -519,6 +545,10 @@ fun itemInfoShareText(chatModel: ChatModel, ci: ChatItem, chatItemInfo: ChatItem if (devTools) { shareText.add(String.format(generalGetString(MR.strings.share_text_database_id), meta.itemId)) shareText.add(String.format(generalGetString(MR.strings.share_text_updated_at), meta.updatedAt)) + shareText.add(String.format(generalGetString(MR.strings.share_text_message_status), jsonShort.encodeToString(ci.meta.itemStatus))) + if (ci.file != null) { + shareText.add(String.format(generalGetString(MR.strings.share_text_file_status), jsonShort.encodeToString(ci.file.fileStatus))) + } } val qi = ci.quotedItem if (qi != null) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index e6f90c1599..e90eed547d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -1,30 +1,31 @@ package chat.simplex.common.views.chat +import androidx.compose.animation.* +import androidx.compose.animation.core.* import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* import androidx.compose.foundation.gestures.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.mapSaver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.* -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.graphics.* import androidx.compose.ui.platform.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.text.* import androidx.compose.ui.unit.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.ui.theme.* import chat.simplex.common.views.call.* import chat.simplex.common.views.chat.group.* @@ -44,463 +45,493 @@ import java.net.URI import kotlin.math.sign @Composable -fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: String) -> Unit) { - val activeChat = remember { mutableStateOf(chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatId }) } - val searchText = rememberSaveable { mutableStateOf("") } +// staleChatId means the id that was before chatModel.chatId becomes null. It's needed for Android only to make transition from chat +// to chat list smooth. Otherwise, chat view will become blank right before the transition starts +fun ChatView(staleChatId: State, onComposed: suspend (chatId: String) -> Unit) { + val remoteHostId = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.remoteHostId } } + val showSearch = rememberSaveable { mutableStateOf(false) } + val activeChatInfo = remember { derivedStateOf { chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == staleChatId.value }?.chatInfo } } val user = chatModel.currentUser.value - val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() - val composeState = rememberSaveable(saver = ComposeState.saver()) { - val draft = chatModel.draft.value - val sharedContent = chatModel.sharedContent.value - mutableStateOf( - if (chatModel.draftChatId.value == chatId && draft != null && (sharedContent !is SharedContent.Forward || sharedContent.fromChatInfo.id == chatId)) { - draft - } else { - ComposeState(useLinkPreviews = useLinkPreviews) - } - ) - } - val attachmentOption = rememberSaveable { mutableStateOf(null) } - val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) - val scope = rememberCoroutineScope() - LaunchedEffect(Unit) { - // snapshotFlow here is because it reacts much faster on changes in chatModel.chatId.value. - // With LaunchedEffect(chatModel.chatId.value) there is a noticeable delay before reconstruction of the view - launch { - snapshotFlow { chatModel.chatId.value } - .distinctUntilChanged() - .filterNotNull() - .collect { chatId -> - if (activeChat.value?.id != chatId) { - // Redisplay the whole hierarchy if the chat is different to make going from groups to direct chat working correctly - // Also for situation when chatId changes after clicking in notification, etc - activeChat.value = chatModel.getChat(chatId) - } - markUnreadChatAsRead(activeChat, chatModel) - } + val chatInfo = activeChatInfo.value + if (chatInfo == null || user == null) { + LaunchedEffect(Unit) { + chatModel.chatId.value = null + ModalManager.end.closeModals() } - launch { - snapshotFlow { - /** - * It's possible that in some cases concurrent modification can happen on [ChatModel.chats] list. - * In this case only error log will be printed here (no crash). - * TODO: Re-write [ChatModel.chats] logic to a new list assignment instead of changing content of mutableList to prevent that - * */ - try { - chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value } - } catch (e: ConcurrentModificationException) { - Log.e(TAG, e.stackTraceToString()) - null - } - } - .distinctUntilChanged() - // Only changed chatInfo is important thing. Other properties can be skipped for reducing recompositions - .filter { it != null && it.chatInfo != activeChat.value?.chatInfo } - .collect { - activeChat.value = it - } - } - } - val view = LocalMultiplatformView() - val chat = activeChat.value - if (chat == null || user == null) { - chatModel.chatId.value = null - ModalManager.end.closeModals() } else { - val chatRh = chat.remoteHostId + val searchText = rememberSaveable { mutableStateOf("") } + val useLinkPreviews = chatModel.controller.appPrefs.privacyLinkPreviews.get() + val composeState = rememberSaveable(saver = ComposeState.saver()) { + val draft = chatModel.draft.value + val sharedContent = chatModel.sharedContent.value + mutableStateOf( + if (chatModel.draftChatId.value == staleChatId.value && draft != null && (sharedContent !is SharedContent.Forward || sharedContent.fromChatInfo.id == staleChatId.value)) { + draft + } else { + ComposeState(useLinkPreviews = useLinkPreviews) + } + ) + } + val attachmentOption = rememberSaveable { mutableStateOf(null) } + val attachmentBottomSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden) + val scope = rememberCoroutineScope() + val selectedChatItems = rememberSaveable { mutableStateOf(null as Set?) } + LaunchedEffect(Unit) { + // snapshotFlow here is because it reacts much faster on changes in chatModel.chatId.value. + // With LaunchedEffect(chatModel.chatId.value) there is a noticeable delay before reconstruction of the view + launch { + snapshotFlow { chatModel.chatId.value } + .distinctUntilChanged() + .filterNotNull() + .collect { chatId -> + markUnreadChatAsRead(chatId) + showSearch.value = false + selectedChatItems.value = null + } + } + } + val view = LocalMultiplatformView() + val chatRh = remoteHostId.value // We need to have real unreadCount value for displaying it inside top right button // Having activeChat reloaded on every change in it is inefficient (UI lags) val unreadCount = remember { derivedStateOf { - chatModel.chats.firstOrNull { chat -> chat.chatInfo.id == chatModel.chatId.value }?.chatStats?.unreadCount ?: 0 + chatModel.chats.value.firstOrNull { chat -> chat.chatInfo.id == activeChatInfo.value?.id }?.chatStats?.unreadCount ?: 0 } } val clipboard = LocalClipboardManager.current - when (chat.chatInfo) { + when (chatInfo) { is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> { - ChatLayout( - chat, - unreadCount, - composeState, - composeView = { - Column( - Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - if ( - chat.chatInfo is ChatInfo.Direct - && !chat.chatInfo.contact.ready - && chat.chatInfo.contact.active - && !chat.chatInfo.contact.nextSendGrpInv - ) { - Text( - generalGetString(MR.strings.contact_connection_pending), - Modifier.padding(top = 4.dp), - fontSize = 14.sp, - color = MaterialTheme.colors.secondary - ) - } - ComposeView( - chatModel, chat, composeState, attachmentOption, - showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } } - ) - } - }, - attachmentOption, - attachmentBottomSheetState, - searchText, - useLinkPreviews = useLinkPreviews, - linkMode = chatModel.simplexLinkMode.value, - back = { - hideKeyboard(view) - AudioPlayer.stop() - chatModel.chatId.value = null - chatModel.groupMembers.clear() - }, - info = { - if (ModalManager.end.hasModalsOpen()) { - ModalManager.end.closeModals() - return@ChatLayout - } - hideKeyboard(view) - withBGApi { - // The idea is to preload information before showing a modal because large groups can take time to load all members - var preloadedContactInfo: Pair? = null - var preloadedCode: String? = null - var preloadedLink: Pair? = null - if (chat.chatInfo is ChatInfo.Direct) { - preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId) - preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second - } else if (chat.chatInfo is ChatInfo.Group) { - setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel) - preloadedLink = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId) - } - ModalManager.end.showModalCloseable(true) { close -> - val chat = remember { activeChat }.value - if (chat?.chatInfo is ChatInfo.Direct) { - var contactInfo: Pair? by remember { mutableStateOf(preloadedContactInfo) } - var code: String? by remember { mutableStateOf(preloadedCode) } - KeyChangeEffect(chat.id, ChatModel.networkStatuses.toMap()) { - contactInfo = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId) - preloadedContactInfo = contactInfo - code = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second - preloadedCode = code + val perChatTheme = remember(chatInfo, CurrentColors.value.base) { if (chatInfo is ChatInfo.Direct) chatInfo.contact.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else null } + val overrides = if (perChatTheme != null) ThemeManager.currentColors(null, perChatTheme, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) else null + SimpleXThemeOverride(overrides ?: CurrentColors.collectAsState().value) { + ChatLayout( + remoteHostId = remoteHostId, + chatInfo = activeChatInfo, + unreadCount, + composeState, + composeView = { + if (selectedChatItems.value == null) { + Column( + Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + if ( + chatInfo is ChatInfo.Direct + && !chatInfo.contact.sndReady + && chatInfo.contact.active + && !chatInfo.contact.nextSendGrpInv + ) { + Text( + generalGetString(MR.strings.contact_connection_pending), + Modifier.padding(top = 4.dp), + fontSize = 14.sp, + color = MaterialTheme.colors.secondary + ) } - ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close) - } else if (chat?.chatInfo is ChatInfo.Group) { - var link: Pair? by remember(chat.id) { mutableStateOf(preloadedLink) } - KeyChangeEffect(chat.id) { - setGroupMembers(chatRh, (chat.chatInfo as ChatInfo.Group).groupInfo, chatModel) - link = chatModel.controller.apiGetGroupLink(chatRh, chat.chatInfo.groupInfo.groupId) - preloadedLink = link - } - GroupChatInfoView(chatModel, chatRh, chat.id, link?.first, link?.second, { - link = it - preloadedLink = it - }, close) + ComposeView( + chatModel, Chat(remoteHostId = chatRh, chatInfo = chatInfo, chatItems = emptyList()), composeState, attachmentOption, + showChooseAttachment = { scope.launch { attachmentBottomSheetState.show() } } + ) } - } - } - }, - showMemberInfo = { groupInfo: GroupInfo, member: GroupMember -> - hideKeyboard(view) - withBGApi { - val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId) - val stats = r?.second - val (_, code) = if (member.memberActive) { - val memCode = chatModel.controller.apiGetGroupMemberCode(chatRh, groupInfo.apiId, member.groupMemberId) - member to memCode?.second } else { - member to null - } - setGroupMembers(chatRh, groupInfo, chatModel) - ModalManager.end.closeModals() - ModalManager.end.showModalCloseable(true) { close -> - remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem -> - GroupMemberInfoView(chatRh, groupInfo, mem, stats, code, chatModel, close, close) - } - } - } - }, - loadPrevMessages = { - if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout - val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) - val firstId = chatModel.chatItems.value.firstOrNull()?.id - if (c != null && firstId != null) { - withBGApi { - apiLoadPrevMessages(c, chatModel, firstId, searchText.value) - } - } - }, - deleteMessage = { itemId, mode -> - withBGApi { - val cInfo = chat.chatInfo - val toDeleteItem = chatModel.chatItems.value.firstOrNull { it.id == itemId } - val toModerate = toDeleteItem?.memberToModerate(chat.chatInfo) - val groupInfo = toModerate?.first - val groupMember = toModerate?.second - val deletedChatItem: ChatItem? - val toChatItem: ChatItem? - if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) { - val r = chatModel.controller.apiDeleteMemberChatItem( - chatRh, - groupId = groupInfo.groupId, - groupMemberId = groupMember.groupMemberId, - itemId = itemId - ) - deletedChatItem = r?.first - toChatItem = r?.second - } else { - val r = chatModel.controller.apiDeleteChatItem( - chatRh, - type = cInfo.chatType, - id = cInfo.apiId, - itemId = itemId, - mode = mode - ) - deletedChatItem = r?.deletedChatItem?.chatItem - toChatItem = r?.toChatItem?.chatItem - } - if (toChatItem == null && deletedChatItem != null) { - chatModel.removeChatItem(chatRh, cInfo, deletedChatItem) - } else if (toChatItem != null) { - chatModel.upsertChatItem(chatRh, cInfo, toChatItem) - } - } - }, - deleteMessages = { itemIds -> - if (itemIds.isNotEmpty()) { - val chatInfo = chat.chatInfo - withBGApi { - val deletedItems: ArrayList = arrayListOf() - for (itemId in itemIds) { - val di = chatModel.controller.apiDeleteChatItem( - chatRh, chatInfo.chatType, chatInfo.apiId, itemId, CIDeleteMode.cidmInternal - )?.deletedChatItem?.chatItem - if (di != null) { - deletedItems.add(di) - } - } - for (di in deletedItems) { - chatModel.removeChatItem(chatRh, chatInfo, di) - } - } - } - }, - receiveFile = { fileId -> - withBGApi { chatModel.controller.receiveFile(chatRh, user, fileId) } - }, - cancelFile = { fileId -> - withBGApi { chatModel.controller.cancelFile(chatRh, user, fileId) } - }, - joinGroup = { groupId, onComplete -> - withBGApi { - chatModel.controller.apiJoinGroup(chatRh, groupId) - onComplete.invoke() - } - }, - startCall = out@{ media -> - withBGApi { - val cInfo = chat.chatInfo - if (cInfo is ChatInfo.Direct) { - val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId) - val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi - chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile) - chatModel.showCallView.value = true - chatModel.callCommand.add(WCallCommand.Capabilities(media)) - } - } - }, - endCall = { - val call = chatModel.activeCall.value - if (call != null) withBGApi { chatModel.callManager.endCall(call) } - }, - acceptCall = { contact -> - hideKeyboard(view) - withBGApi { - val invitation = chatModel.callInvitations.remove(contact.id) - ?: controller.apiGetCallInvitations(chatModel.remoteHostId()).firstOrNull { it.contact.id == contact.id } - if (invitation == null) { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended)) - } else { - chatModel.callManager.acceptIncomingCall(invitation = invitation) - } - } - }, - acceptFeature = { contact, feature, param -> - withBGApi { - chatModel.controller.allowFeatureToContact(chatRh, contact, feature, param) - } - }, - openDirectChat = { contactId -> - withBGApi { - openDirectChat(chatRh, contactId, chatModel) - } - }, - updateContactStats = { contact -> - withBGApi { - val r = chatModel.controller.apiContactInfo(chatRh, chat.chatInfo.apiId) - if (r != null) { - val contactStats = r.first - if (contactStats != null) - chatModel.updateContactConnectionStats(chatRh, contact, contactStats) - } - } - }, - updateMemberStats = { groupInfo, member -> - withBGApi { - val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId) - if (r != null) { - val memStats = r.second - if (memStats != null) { - chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats) - } - } - } - }, - syncContactConnection = { contact -> - withBGApi { - val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false) - if (cStats != null) { - chatModel.updateContactConnectionStats(chatRh, contact, cStats) - } - } - }, - syncMemberConnection = { groupInfo, member -> - withBGApi { - val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false) - if (r != null) { - chatModel.updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second) - } - } - }, - findModelChat = { chatId -> - chatModel.getChat(chatId) - }, - findModelMember = { memberId -> - chatModel.groupMembers.find { it.id == memberId } - }, - setReaction = { cInfo, cItem, add, reaction -> - withBGApi { - val updatedCI = chatModel.controller.apiChatItemReaction( - rh = chatRh, - type = cInfo.chatType, - id = cInfo.apiId, - itemId = cItem.id, - add = add, - reaction = reaction - ) - if (updatedCI != null) { - chatModel.updateChatItem(cInfo, updatedCI) - } - } - }, - showItemDetails = { cInfo, cItem -> - suspend fun loadChatItemInfo(): ChatItemInfo? { - val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cItem.id) - if (ciInfo != null) { - if (chat.chatInfo is ChatInfo.Group) { - setGroupMembers(chatRh, chat.chatInfo.groupInfo, chatModel) - } - } - return ciInfo - } - withBGApi { - var initialCiInfo = loadChatItemInfo() ?: return@withBGApi - ModalManager.end.closeModals() - ModalManager.end.showModalCloseable(endButtons = { - ShareButton { - clipboard.shareText(itemInfoShareText(chatModel, cItem, initialCiInfo, chatModel.controller.appPrefs.developerTools.get())) - } - }) { close -> - var ciInfo by remember(cItem.id) { mutableStateOf(initialCiInfo) } - ChatItemInfoView(chatRh, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get()) - LaunchedEffect(cItem.id) { - withContext(Dispatchers.Default) { - for (apiResp in controller.messagesChannel) { - val msg = apiResp.resp - if (apiResp.remoteHostId == chatRh && - msg is CR.ChatItemStatusUpdated && - msg.chatItem.chatItem.id == cItem.id - ) { - ciInfo = loadChatItemInfo() ?: return@withContext - initialCiInfo = ciInfo + SelectedItemsBottomToolbar( + chatItems = remember { chatModel.chatItems }.value, + selectedChatItems = selectedChatItems, + chatInfo = chatInfo, + deleteItems = { canDeleteForAll -> + val itemIds = selectedChatItems.value + if (itemIds != null) { + deleteMessagesAlertDialog( + itemIds.sorted(), + generalGetString(if (itemIds.size == 1) MR.strings.delete_message_mark_deleted_warning else MR.strings.delete_messages_mark_deleted_warning), + forAll = canDeleteForAll, + deleteMessages = { ids, forAll -> + deleteMessages(chatRh, chatInfo, ids, forAll, moderate = false) { + selectedChatItems.value = null + } + } + ) + } + }, + moderateItems = { + if (chatInfo is ChatInfo.Group) { + val itemIds = selectedChatItems.value + if (itemIds != null) { + moderateMessagesAlertDialog(itemIds.sorted(), moderateMessageQuestionText(chatInfo.featureEnabled(ChatFeature.FullDelete), itemIds.size), deleteMessages = { ids -> + deleteMessages(chatRh, chatInfo, ids, true, moderate = true) { + selectedChatItems.value = null + } + }) } } } + ) + } + }, + attachmentOption, + attachmentBottomSheetState, + searchText, + useLinkPreviews = useLinkPreviews, + linkMode = chatModel.simplexLinkMode.value, + selectedChatItems = selectedChatItems, + back = { + hideKeyboard(view) + AudioPlayer.stop() + chatModel.chatId.value = null + chatModel.groupMembers.clear() + chatModel.groupMembersIndexes.clear() + }, + info = { + if (ModalManager.end.hasModalsOpen()) { + ModalManager.end.closeModals() + return@ChatLayout + } + hideKeyboard(view) + withBGApi { + // The idea is to preload information before showing a modal because large groups can take time to load all members + var preloadedContactInfo: Pair? = null + var preloadedCode: String? = null + var preloadedLink: Pair? = null + if (chatInfo is ChatInfo.Direct) { + preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId) + preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chatInfo.apiId)?.second + } else if (chatInfo is ChatInfo.Group) { + setGroupMembers(chatRh, chatInfo.groupInfo, chatModel) + preloadedLink = chatModel.controller.apiGetGroupLink(chatRh, chatInfo.groupInfo.groupId) } - KeyChangeEffect(chatModel.chatId.value) { - close() + ModalManager.end.showModalCloseable(true) { close -> + val chatInfo = remember { activeChatInfo }.value + if (chatInfo is ChatInfo.Direct) { + var contactInfo: Pair? by remember { mutableStateOf(preloadedContactInfo) } + var code: String? by remember { mutableStateOf(preloadedCode) } + KeyChangeEffect(chatInfo.id, ChatModel.networkStatuses.toMap()) { + contactInfo = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId) + preloadedContactInfo = contactInfo + code = chatModel.controller.apiGetContactCode(chatRh, chatInfo.apiId)?.second + preloadedCode = code + } + ChatInfoView(chatModel, chatInfo.contact, contactInfo?.first, contactInfo?.second, chatInfo.localAlias, code, close) { + showSearch.value = true + } + } else if (chatInfo is ChatInfo.Group) { + var link: Pair? by remember(chatInfo.id) { mutableStateOf(preloadedLink) } + KeyChangeEffect(chatInfo.id) { + setGroupMembers(chatRh, chatInfo.groupInfo, chatModel) + link = chatModel.controller.apiGetGroupLink(chatRh, chatInfo.groupInfo.groupId) + preloadedLink = link + } + GroupChatInfoView(chatModel, chatRh, chatInfo.id, link?.first, link?.second, { + link = it + preloadedLink = it + }, close, { showSearch.value = true }) + } else { + LaunchedEffect(Unit) { + close() + } + } } } - } - }, - addMembers = { groupInfo -> - hideKeyboard(view) - withBGApi { - setGroupMembers(chatRh, groupInfo, chatModel) - ModalManager.end.closeModals() - ModalManager.end.showModalCloseable(true) { close -> - AddGroupMembersView(chatRh, groupInfo, false, chatModel, close) + }, + showMemberInfo = { groupInfo: GroupInfo, member: GroupMember -> + hideKeyboard(view) + withBGApi { + val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId) + val stats = r?.second + val (_, code) = if (member.memberActive) { + val memCode = chatModel.controller.apiGetGroupMemberCode(chatRh, groupInfo.apiId, member.groupMemberId) + member to memCode?.second + } else { + member to null + } + setGroupMembers(chatRh, groupInfo, chatModel) + ModalManager.end.closeModals() + ModalManager.end.showModalCloseable(true) { close -> + remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem -> + GroupMemberInfoView(chatRh, groupInfo, mem, stats, code, chatModel, close, close) + } + } } - } - }, - openGroupLink = { groupInfo -> - hideKeyboard(view) - withBGApi { - val link = chatModel.controller.apiGetGroupLink(chatRh, groupInfo.groupId) - ModalManager.end.closeModals() - ModalManager.end.showModalCloseable(true) { - GroupLinkView(chatModel, chatRh, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null) + }, + loadPrevMessages = { chatId -> + val c = chatModel.getChat(chatId) + if (chatModel.chatId.value != chatId) return@ChatLayout + val firstId = chatModel.chatItems.value.firstOrNull()?.id + if (c != null && firstId != null) { + withBGApi { + apiLoadPrevMessages(c, chatModel, firstId, searchText.value) + } } + }, + deleteMessage = { itemId, mode -> + withBGApi { + val toDeleteItem = chatModel.chatItems.value.firstOrNull { it.id == itemId } + val toModerate = toDeleteItem?.memberToModerate(chatInfo) + val groupInfo = toModerate?.first + val groupMember = toModerate?.second + val deletedChatItem: ChatItem? + val toChatItem: ChatItem? + val r = if (mode == CIDeleteMode.cidmBroadcast && groupInfo != null && groupMember != null) { + chatModel.controller.apiDeleteMemberChatItems( + chatRh, + groupId = groupInfo.groupId, + itemIds = listOf(itemId) + ) + } else { + chatModel.controller.apiDeleteChatItems( + chatRh, + type = chatInfo.chatType, + id = chatInfo.apiId, + itemIds = listOf(itemId), + mode = mode + ) + } + val deleted = r?.firstOrNull() + if (deleted != null) { + deletedChatItem = deleted.deletedChatItem.chatItem + toChatItem = deleted.toChatItem?.chatItem + withChats { + if (toChatItem != null) { + upsertChatItem(chatRh, chatInfo, toChatItem) + } else { + removeChatItem(chatRh, chatInfo, deletedChatItem) + } + } + } + } + }, + deleteMessages = { itemIds -> deleteMessages(chatRh, chatInfo, itemIds, false, moderate = false) }, + receiveFile = { fileId -> + withBGApi { chatModel.controller.receiveFile(chatRh, user, fileId) } + }, + cancelFile = { fileId -> + withBGApi { chatModel.controller.cancelFile(chatRh, user, fileId) } + }, + joinGroup = { groupId, onComplete -> + withBGApi { + chatModel.controller.apiJoinGroup(chatRh, groupId) + onComplete.invoke() + } + }, + startCall = out@{ media -> startChatCall(chatRh, chatInfo, media) }, + endCall = { + val call = chatModel.activeCall.value + if (call != null) withBGApi { chatModel.callManager.endCall(call) } + }, + acceptCall = { contact -> + hideKeyboard(view) + withBGApi { + val invitation = chatModel.callInvitations.remove(contact.id) + ?: controller.apiGetCallInvitations(chatModel.remoteHostId()).firstOrNull { it.contact.id == contact.id } + if (invitation == null) { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.call_already_ended)) + } else { + chatModel.callManager.acceptIncomingCall(invitation = invitation) + } + } + }, + acceptFeature = { contact, feature, param -> + withBGApi { + chatModel.controller.allowFeatureToContact(chatRh, contact, feature, param) + } + }, + openDirectChat = { contactId -> + scope.launch { + openDirectChat(chatRh, contactId, chatModel) + } + }, + forwardItem = { cItem, cInfo -> + chatModel.chatId.value = null + chatModel.sharedContent.value = SharedContent.Forward(cInfo, cItem) + }, + updateContactStats = { contact -> + withBGApi { + val r = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId) + if (r != null) { + val contactStats = r.first + if (contactStats != null) + withChats { + updateContactConnectionStats(chatRh, contact, contactStats) + } + } + } + }, + updateMemberStats = { groupInfo, member -> + withBGApi { + val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId) + if (r != null) { + val memStats = r.second + if (memStats != null) { + withChats { + updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, memStats) + } + } + } + } + }, + syncContactConnection = { contact -> + withBGApi { + val cStats = chatModel.controller.apiSyncContactRatchet(chatRh, contact.contactId, force = false) + if (cStats != null) { + withChats { + updateContactConnectionStats(chatRh, contact, cStats) + } + } + } + }, + syncMemberConnection = { groupInfo, member -> + withBGApi { + val r = chatModel.controller.apiSyncGroupMemberRatchet(chatRh, groupInfo.apiId, member.groupMemberId, force = false) + if (r != null) { + withChats { + updateGroupMemberConnectionStats(chatRh, groupInfo, r.first, r.second) + } + } + } + }, + findModelChat = { chatId -> + chatModel.getChat(chatId) + }, + findModelMember = { memberId -> + chatModel.groupMembers.find { it.id == memberId } + }, + setReaction = { cInfo, cItem, add, reaction -> + withBGApi { + val updatedCI = chatModel.controller.apiChatItemReaction( + rh = chatRh, + type = cInfo.chatType, + id = cInfo.apiId, + itemId = cItem.id, + add = add, + reaction = reaction + ) + if (updatedCI != null) { + withChats { + updateChatItem(cInfo, updatedCI) + } + } + } + }, + showItemDetails = { cInfo, cItem -> + suspend fun loadChatItemInfo(): ChatItemInfo? { + val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cItem.id) + if (ciInfo != null) { + if (chatInfo is ChatInfo.Group) { + setGroupMembers(chatRh, chatInfo.groupInfo, chatModel) + } + } + return ciInfo + } + withBGApi { + var initialCiInfo = loadChatItemInfo() ?: return@withBGApi + ModalManager.end.closeModals() + ModalManager.end.showModalCloseable(endButtons = { + ShareButton { + clipboard.shareText(itemInfoShareText(chatModel, cItem, initialCiInfo, chatModel.controller.appPrefs.developerTools.get())) + } + }) { close -> + var ciInfo by remember(cItem.id) { mutableStateOf(initialCiInfo) } + ChatItemInfoView(chatRh, cItem, ciInfo, devTools = chatModel.controller.appPrefs.developerTools.get()) + LaunchedEffect(cItem.id) { + withContext(Dispatchers.Default) { + for (apiResp in controller.messagesChannel) { + val msg = apiResp.resp + if (apiResp.remoteHostId == chatRh && + msg is CR.ChatItemStatusUpdated && + msg.chatItem.chatItem.id == cItem.id + ) { + ciInfo = loadChatItemInfo() ?: return@withContext + initialCiInfo = ciInfo + } + } + } + } + KeyChangeEffect(chatModel.chatId.value) { + close() + } + } + } + }, + addMembers = { groupInfo -> addGroupMembers(view = view, groupInfo = groupInfo, rhId = chatRh, close = { ModalManager.end.closeModals() }) }, + openGroupLink = { groupInfo -> openGroupLink(view = view, groupInfo = groupInfo, rhId = chatRh, close = { ModalManager.end.closeModals() }) }, + markRead = { range, unreadCountAfter -> + withBGApi { + withChats { + // It's important to call it on Main thread. Otherwise, composable crash occurs from time-to-time without useful stacktrace + withContext(Dispatchers.Main) { + markChatItemsRead(chatRh, chatInfo, range, unreadCountAfter) + } + ntfManager.cancelNotificationsForChat(chatInfo.id) + chatModel.controller.apiChatRead( + chatRh, + chatInfo.chatType, + chatInfo.apiId, + range + ) + } + } + }, + changeNtfsState = { enabled, currentValue -> toggleNotifications(chatRh, chatInfo, enabled, chatModel, currentValue) }, + onSearchValueChanged = { value -> + if (searchText.value == value) return@ChatLayout + val c = chatModel.getChat(chatInfo.id) ?: return@ChatLayout + if (chatModel.chatId.value != chatInfo.id) return@ChatLayout + withBGApi { + apiFindMessages(c, chatModel, value) + searchText.value = value + } + }, + onComposed, + developerTools = chatModel.controller.appPrefs.developerTools.get(), + showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(), + showSearch = showSearch + ) + if (appPlatform.isAndroid) { + val backgroundColor = MaterialTheme.colors.background + val backgroundColorState = rememberUpdatedState(backgroundColor) + LaunchedEffect(Unit) { + snapshotFlow { ModalManager.center.modalCount.value > 0 } + .collect { modalBackground -> + if (modalBackground) { + platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, CurrentColors.value.colors.background, false, false) + } else { + platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, backgroundColorState.value, true, false) + } + } } - }, - markRead = { range, unreadCountAfter -> - chatModel.markChatItemsRead(chat, range, unreadCountAfter) - ntfManager.cancelNotificationsForChat(chat.id) - withBGApi { - chatModel.controller.apiChatRead( - chatRh, - chat.chatInfo.chatType, - chat.chatInfo.apiId, - range - ) - } - }, - changeNtfsState = { enabled, currentValue -> toggleNotifications(chat, enabled, chatModel, currentValue) }, - onSearchValueChanged = { value -> - if (searchText.value == value) return@ChatLayout - if (chatModel.chatId.value != activeChat.value?.id) return@ChatLayout - val c = chatModel.getChat(chatModel.chatId.value ?: return@ChatLayout) ?: return@ChatLayout - withBGApi { - apiFindMessages(c, chatModel, value) - searchText.value = value - } - }, - onComposed, - developerTools = chatModel.controller.appPrefs.developerTools.get(), - ) + } + } } is ChatInfo.ContactConnection -> { val close = { chatModel.chatId.value = null } - ModalView(close, showClose = appPlatform.isAndroid, content = { - ContactConnectionInfoView(chatModel, chat.remoteHostId, chat.chatInfo.contactConnection.connReqInv, chat.chatInfo.contactConnection, false, close) - }) - LaunchedEffect(chat.id) { - onComposed(chat.id) - ModalManager.end.closeModals() - chatModel.chatItems.clear() + val handler = remember { AppBarHandler() } + CompositionLocalProvider( + LocalAppBarHandler provides handler + ) { + ModalView(close, showClose = appPlatform.isAndroid, content = { + ContactConnectionInfoView(chatModel, chatRh, chatInfo.contactConnection.connReqInv, chatInfo.contactConnection, false, close) + }) + LaunchedEffect(chatInfo.id) { + onComposed(chatInfo.id) + ModalManager.end.closeModals() + chatModel.chatItems.clear() + } } } is ChatInfo.InvalidJSON -> { val close = { chatModel.chatId.value = null } - ModalView(close, showClose = appPlatform.isAndroid, endButtons = { ShareButton { clipboard.shareText(chat.chatInfo.json) } }, content = { - InvalidJSONView(chat.chatInfo.json) - }) - LaunchedEffect(chat.id) { - onComposed(chat.id) - ModalManager.end.closeModals() - chatModel.chatItems.clear() + val handler = remember { AppBarHandler() } + CompositionLocalProvider( + LocalAppBarHandler provides handler + ) { + ModalView(close, showClose = appPlatform.isAndroid, endButtons = { ShareButton { clipboard.shareText(chatInfo.json) } }, content = { + InvalidJSONView(chatInfo.json) + }) + LaunchedEffect(chatInfo.id) { + onComposed(chatInfo.id) + ModalManager.end.closeModals() + chatModel.chatItems.clear() + } } } else -> {} @@ -508,9 +539,22 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: } } +fun startChatCall(remoteHostId: Long?, chatInfo: ChatInfo, media: CallMediaType) { + withBGApi { + if (chatInfo is ChatInfo.Direct) { + val contactInfo = chatModel.controller.apiContactInfo(remoteHostId, chatInfo.contact.contactId) + val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi + chatModel.activeCall.value = Call(remoteHostId = remoteHostId, contact = chatInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile) + chatModel.showCallView.value = true + chatModel.callCommand.add(WCallCommand.Capabilities(media)) + } + } +} + @Composable fun ChatLayout( - chat: Chat, + remoteHostId: State, + chatInfo: State, unreadCount: State, composeState: MutableState, composeView: (@Composable () -> Unit), @@ -519,10 +563,11 @@ fun ChatLayout( searchValue: State, useLinkPreviews: Boolean, linkMode: SimplexLinkMode, + selectedChatItems: MutableState?>, back: () -> Unit, info: () -> Unit, showMemberInfo: (GroupInfo, GroupMember) -> Unit, - loadPrevMessages: () -> Unit, + loadPrevMessages: (ChatId) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessages: (List) -> Unit, receiveFile: (Long) -> Unit, @@ -533,6 +578,7 @@ fun ChatLayout( acceptCall: (Contact) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit, openDirectChat: (Long) -> Unit, + forwardItem: (ChatInfo, ChatItem) -> Unit, updateContactStats: (Contact) -> Unit, updateMemberStats: (GroupInfo, GroupMember) -> Unit, syncContactConnection: (Contact) -> Unit, @@ -548,6 +594,8 @@ fun ChatLayout( onSearchValueChanged: (String) -> Unit, onComposed: suspend (chatId: String) -> Unit, developerTools: Boolean, + showViaProxy: Boolean, + showSearch: MutableState ) { val scope = rememberCoroutineScope() val attachmentDisabled = remember { derivedStateOf { composeState.value.attachmentDisabled } } @@ -555,7 +603,7 @@ fun ChatLayout( Modifier .fillMaxWidth() .desktopOnExternalDrag( - enabled = !attachmentDisabled.value && rememberUpdatedState(chat.userCanSend).value, + enabled = remember(attachmentDisabled.value, chatInfo.value?.userCanSend) { mutableStateOf(!attachmentDisabled.value && chatInfo.value?.userCanSend == true) }.value, onFiles = { paths -> composeState.onFilesAttached(paths.map { it.toURI() }) }, onImage = { // TODO: file is not saved anywhere?! @@ -575,6 +623,7 @@ fun ChatLayout( ModalBottomSheetLayout( scrimColor = Color.Black.copy(alpha = 0.12F), modifier = Modifier.navigationBarsWithImePadding(), + sheetElevation = 0.dp, sheetContent = { ChooseAttachmentView( attachmentOption, @@ -590,24 +639,47 @@ fun ChatLayout( } Scaffold( - topBar = { ChatInfoToolbar(chat, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged) }, + topBar = { + if (selectedChatItems.value == null) { + val chatInfo = chatInfo.value + if (chatInfo != null) { + ChatInfoToolbar(chatInfo, back, info, startCall, endCall, addMembers, openGroupLink, changeNtfsState, onSearchValueChanged, showSearch) + } + } else { + SelectedItemsTopToolbar(selectedChatItems) + } + }, bottomBar = composeView, modifier = Modifier.navigationBarsWithImePadding(), floatingActionButton = { floatingButton.value() }, contentColor = LocalContentColor.current, drawerContentColor = LocalContentColor.current, + backgroundColor = Color.Unspecified ) { contentPadding -> + val wallpaperImage = MaterialTheme.wallpaper.type.image + val wallpaperType = MaterialTheme.wallpaper.type + val backgroundColor = MaterialTheme.wallpaper.background ?: wallpaperType.defaultBackgroundColor(CurrentColors.value.base, MaterialTheme.colors.background) + val tintColor = MaterialTheme.wallpaper.tint ?: wallpaperType.defaultTintColor(CurrentColors.value.base) BoxWithConstraints(Modifier - .fillMaxHeight() + .fillMaxSize() + .background(MaterialTheme.colors.background) + .then(if (wallpaperImage != null) + Modifier.drawWithCache { chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor) } + else + Modifier) .padding(contentPadding) ) { - ChatItemsList( - chat, unreadCount, composeState, searchValue, - useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages, - receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, - updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, - setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, - ) + val remoteHostId = remember { remoteHostId }.value + val chatInfo = remember { chatInfo }.value + if (chatInfo != null) { + ChatItemsList( + remoteHostId, chatInfo, unreadCount, composeState, searchValue, + useLinkPreviews, linkMode, selectedChatItems, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages, + receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, forwardItem, + updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, + setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, showViaProxy, + ) + } } } } @@ -617,7 +689,7 @@ fun ChatLayout( @Composable fun ChatInfoToolbar( - chat: Chat, + chatInfo: ChatInfo, back: () -> Unit, info: () -> Unit, startCall: (CallMediaType) -> Unit, @@ -626,16 +698,17 @@ fun ChatInfoToolbar( openGroupLink: (GroupInfo) -> Unit, changeNtfsState: (Boolean, currentValue: MutableState) -> Unit, onSearchValueChanged: (String) -> Unit, + showSearch: MutableState ) { val scope = rememberCoroutineScope() val showMenu = rememberSaveable { mutableStateOf(false) } - var showSearch by rememberSaveable { mutableStateOf(false) } + val onBackClicked = { - if (!showSearch) { + if (!showSearch.value) { back() } else { onSearchValueChanged("") - showSearch = false + showSearch.value = false } } if (appPlatform.isAndroid) { @@ -644,17 +717,18 @@ fun ChatInfoToolbar( val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() val menuItems = arrayListOf<@Composable () -> Unit>() val activeCall by remember { chatModel.activeCall } - if (chat.chatInfo is ChatInfo.Local) { + if (chatInfo is ChatInfo.Local) { barButtons.add { - IconButton({ - showMenu.value = false - showSearch = true - }, enabled = chat.chatInfo.noteFolder.ready + IconButton( + { + showMenu.value = false + showSearch.value = true + }, enabled = chatInfo.noteFolder.ready ) { Icon( painterResource(MR.images.ic_search), stringResource(MR.strings.search_verb).capitalize(Locale.current), - tint = if (chat.chatInfo.noteFolder.ready) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + tint = if (chatInfo.noteFolder.ready) MaterialTheme.colors.primary else MaterialTheme.colors.secondary ) } } @@ -662,41 +736,41 @@ fun ChatInfoToolbar( menuItems.add { ItemAction(stringResource(MR.strings.search_verb), painterResource(MR.images.ic_search), onClick = { showMenu.value = false - showSearch = true + showSearch.value = true }) } } - if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.mergedPreferences.calls.enabled.forUser) { + if (chatInfo is ChatInfo.Direct && chatInfo.contact.mergedPreferences.calls.enabled.forUser) { if (activeCall == null) { barButtons.add { if (appPlatform.isAndroid) { IconButton({ showMenu.value = false startCall(CallMediaType.Audio) - }, enabled = chat.chatInfo.contact.ready && chat.chatInfo.contact.active + }, enabled = chatInfo.contact.ready && chatInfo.contact.active ) { Icon( painterResource(MR.images.ic_call_500), stringResource(MR.strings.icon_descr_audio_call).capitalize(Locale.current), - tint = if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + tint = if (chatInfo.contact.ready && chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary ) } } else { IconButton({ showMenu.value = false startCall(CallMediaType.Video) - }, enabled = chat.chatInfo.contact.ready && chat.chatInfo.contact.active + }, enabled = chatInfo.contact.ready && chatInfo.contact.active ) { Icon( painterResource(MR.images.ic_videocam), stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), - tint = if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + tint = if (chatInfo.contact.ready && chatInfo.contact.active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary ) } } } - } else if (activeCall?.contact?.id == chat.id && appPlatform.isDesktop) { + } else if (activeCall?.contact?.id == chatInfo.id && appPlatform.isDesktop) { barButtons.add { val call = remember { chatModel.activeCall }.value val connectedAt = call?.connectedAt @@ -725,7 +799,7 @@ fun ChatInfoToolbar( } } } - if (chat.chatInfo.contact.ready && chat.chatInfo.contact.active && activeCall == null) { + if (chatInfo.contact.ready && chatInfo.contact.active && activeCall == null) { menuItems.add { if (appPlatform.isAndroid) { ItemAction(stringResource(MR.strings.icon_descr_video_call).capitalize(Locale.current), painterResource(MR.images.ic_videocam), onClick = { @@ -740,12 +814,12 @@ fun ChatInfoToolbar( } } } - } else if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.canAddMembers) { - if (!chat.chatInfo.incognito) { + } else if (chatInfo is ChatInfo.Group && chatInfo.groupInfo.canAddMembers) { + if (!chatInfo.incognito) { barButtons.add { IconButton({ showMenu.value = false - addMembers(chat.chatInfo.groupInfo) + addMembers(chatInfo.groupInfo) }) { Icon(painterResource(MR.images.ic_person_add_500), stringResource(MR.strings.icon_descr_add_members), tint = MaterialTheme.colors.primary) } @@ -754,15 +828,16 @@ fun ChatInfoToolbar( barButtons.add { IconButton({ showMenu.value = false - openGroupLink(chat.chatInfo.groupInfo) + openGroupLink(chatInfo.groupInfo) }) { Icon(painterResource(MR.images.ic_add_link), stringResource(MR.strings.group_link), tint = MaterialTheme.colors.primary) } } } } - if ((chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.ready && chat.chatInfo.contact.active) || chat.chatInfo is ChatInfo.Group) { - val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) } + + if ((chatInfo is ChatInfo.Direct && chatInfo.contact.ready && chatInfo.contact.active) || chatInfo is ChatInfo.Group) { + val ntfsEnabled = remember { mutableStateOf(chatInfo.ntfsEnabled) } menuItems.add { ItemAction( if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat), @@ -788,17 +863,17 @@ fun ChatInfoToolbar( } DefaultTopAppBar( - navigationButton = { if (appPlatform.isAndroid || showSearch) { NavigationButtonBack(onBackClicked) } }, - title = { ChatInfoToolbarTitle(chat.chatInfo) }, - onTitleClick = if (chat.chatInfo is ChatInfo.Local) null else info, - showSearch = showSearch, + navigationButton = { if (appPlatform.isAndroid || showSearch.value) { NavigationButtonBack(onBackClicked) } }, + title = { ChatInfoToolbarTitle(chatInfo) }, + onTitleClick = if (chatInfo is ChatInfo.Local) null else info, + showSearch = showSearch.value, onSearchValueChanged = onSearchValueChanged, buttons = barButtons ) - Divider(Modifier.padding(top = AppBarHeight)) + Divider(Modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier)) - Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd).offset(y = AppBarHeight)) { + Box(Modifier.fillMaxWidth().wrapContentSize(Alignment.TopEnd).offset(y = AppBarHeight * fontSizeSqrtMultiplier)) { DefaultDropdownMenu(showMenu) { menuItems.forEach { it() } } @@ -812,9 +887,9 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo verticalAlignment = Alignment.CenterVertically ) { if (cInfo.incognito) { - IncognitoImage(size = 36.dp, Indigo) + IncognitoImage(size = 36.dp * fontSizeSqrtMultiplier, Indigo) } - ChatInfoImage(cInfo, size = imageSize, iconColor) + ChatInfoImage(cInfo, size = imageSize * fontSizeSqrtMultiplier, iconColor) Column( Modifier.padding(start = 8.dp), horizontalAlignment = Alignment.CenterHorizontally @@ -840,31 +915,21 @@ fun ChatInfoToolbarTitle(cInfo: ChatInfo, imageSize: Dp = 40.dp, iconColor: Colo @Composable private fun ContactVerifiedShield() { - Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(18.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary) -} - -data class CIListState(val scrolled: Boolean, val itemCount: Int, val keyboardState: KeyboardState) - -val CIListStateSaver = run { - val scrolledKey = "scrolled" - val countKey = "itemCount" - val keyboardKey = "keyboardState" - mapSaver( - save = { mapOf(scrolledKey to it.scrolled, countKey to it.itemCount, keyboardKey to it.keyboardState) }, - restore = { CIListState(it[scrolledKey] as Boolean, it[countKey] as Int, it[keyboardKey] as KeyboardState) } - ) + Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(18.dp * fontSizeSqrtMultiplier).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary) } @Composable fun BoxWithConstraintsScope.ChatItemsList( - chat: Chat, + remoteHostId: Long?, + chatInfo: ChatInfo, unreadCount: State, composeState: MutableState, searchValue: State, useLinkPreviews: Boolean, linkMode: SimplexLinkMode, + selectedChatItems: MutableState?>, showMemberInfo: (GroupInfo, GroupMember) -> Unit, - loadPrevMessages: () -> Unit, + loadPrevMessages: (ChatId) -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessages: (List) -> Unit, receiveFile: (Long) -> Unit, @@ -873,6 +938,7 @@ fun BoxWithConstraintsScope.ChatItemsList( acceptCall: (Contact) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit, openDirectChat: (Long) -> Unit, + forwardItem: (ChatInfo, ChatItem) -> Unit, updateContactStats: (Contact) -> Unit, updateMemberStats: (GroupInfo, GroupMember) -> Unit, syncContactConnection: (Contact) -> Unit, @@ -885,10 +951,11 @@ fun BoxWithConstraintsScope.ChatItemsList( setFloatingButton: (@Composable () -> Unit) -> Unit, onComposed: suspend (chatId: String) -> Unit, developerTools: Boolean, + showViaProxy: Boolean ) { val listState = rememberLazyListState() val scope = rememberCoroutineScope() - ScrollToBottom(chat.id, listState, chatModel.chatItems) + ScrollToBottom(chatInfo.id, listState, chatModel.chatItems) var prevSearchEmptiness by rememberSaveable { mutableStateOf(searchValue.value.isEmpty()) } // Scroll to bottom when search value changes from something to nothing and back LaunchedEffect(searchValue.value.isEmpty()) { @@ -902,7 +969,7 @@ fun BoxWithConstraintsScope.ChatItemsList( } } - PreloadItems(listState, ChatPagination.UNTIL_PRELOAD_COUNT, loadPrevMessages) + PreloadItems(chatInfo.id, listState, ChatPagination.UNTIL_PRELOAD_COUNT, loadPrevMessages) Spacer(Modifier.size(8.dp)) val reversedChatItems by remember { derivedStateOf { chatModel.chatItems.asReversed() } } @@ -913,13 +980,14 @@ fun BoxWithConstraintsScope.ChatItemsList( scope.launch { listState.animateScrollToItem(kotlin.math.min(reversedChatItems.lastIndex, index + 1), -maxHeightRounded) } } } - LaunchedEffect(chat.id) { + // TODO: Having this block on desktop makes ChatItemsList() to recompose twice on chatModel.chatId update instead of once + LaunchedEffect(chatInfo.id) { var stopListening = false snapshotFlow { listState.layoutInfo.visibleItemsInfo.lastIndex } .distinctUntilChanged() .filter { !stopListening } .collect { - onComposed(chat.id) + onComposed(chatInfo.id) stopListening = true } } @@ -935,19 +1003,10 @@ fun BoxWithConstraintsScope.ChatItemsList( // With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view LocalViewConfiguration provides LocalViewConfiguration.current.bigTouchSlop() ) { - val dismissState = rememberDismissState(initialValue = DismissValue.Default) { false } - val directions = setOf(DismissDirection.EndToStart) - val swipeableModifier = SwipeToDismissModifier( - state = dismissState, - directions = directions, - swipeDistance = with(LocalDensity.current) { 30.dp.toPx() }, - ) - val swipedToEnd = (dismissState.overflow.value > 0f && directions.contains(DismissDirection.StartToEnd)) - val swipedToStart = (dismissState.overflow.value < 0f && directions.contains(DismissDirection.EndToStart)) - if (dismissState.isAnimationRunning && (swipedToStart || swipedToEnd)) { - LaunchedEffect(Unit) { + val dismissState = rememberDismissState(initialValue = DismissValue.Default) { + if (it == DismissValue.DismissedToStart) { scope.launch { - if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chat.chatInfo !is ChatInfo.Local) { + if ((cItem.content is CIContent.SndMsgContent || cItem.content is CIContent.RcvMsgContent) && chatInfo !is ChatInfo.Local) { if (composeState.value.editing) { composeState.value = ComposeState(contextItem = ComposeContextItem.QuotedItem(cItem), useLinkPreviews = useLinkPreviews) } else if (cItem.id != ChatItem.TEMP_LIVE_CHAT_ITEM_ID) { @@ -956,7 +1015,13 @@ fun BoxWithConstraintsScope.ChatItemsList( } } } + false } + val swipeableModifier = SwipeToDismissModifier( + state = dismissState, + directions = setOf(DismissDirection.EndToStart), + swipeDistance = with(LocalDensity.current) { 30.dp.toPx() }, + ) val provider = { providerForGallery(i, chatModel.chatItems.value, cItem.id) { indexInReversed -> scope.launch { @@ -975,75 +1040,117 @@ fun BoxWithConstraintsScope.ChatItemsList( tryOrShowError("${cItem.id}ChatItem", error = { CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart) }) { - ChatItemView(chat.remoteHostId, chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools) + ChatItemView(remoteHostId, chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, selectedChatItems = selectedChatItems, selectChatItem = { selectUnselectChatItem(true, cItem, revealed, selectedChatItems) }, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, forwardItem = forwardItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy) } } @Composable fun ChatItemView(cItem: ChatItem, range: IntRange?, prevItem: ChatItem?) { - val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null && cItem.meta.itemForwarded == null - if (chat.chatInfo is ChatInfo.Group) { - if (cItem.chatDir is CIDirection.GroupRcv) { - val member = cItem.chatDir.groupMember - val (prevMember, memCount) = - if (range != null) { - chatModel.getPrevHiddenMember(member, range) - } else { - null to 1 - } - if (prevItem == null || showMemberImage(member, prevItem) || prevMember != null) { - Column( - Modifier - .padding(top = 8.dp) - .padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp), - verticalArrangement = Arrangement.spacedBy(4.dp), - horizontalAlignment = Alignment.Start - ) { - if (cItem.content.showMemberName) { - Text( - memberNames(member, prevMember, memCount), - Modifier.padding(start = MEMBER_IMAGE_SIZE + 10.dp), - style = TextStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary), - maxLines = 2 - ) + val sent = cItem.chatDir.sent + Box(Modifier.padding(bottom = 4.dp)) { + val voiceWithTransparentBack = cItem.content.msgContent is MsgContent.MCVoice && cItem.content.text.isEmpty() && cItem.quotedItem == null && cItem.meta.itemForwarded == null + val selectionVisible = selectedChatItems.value != null && cItem.canBeDeletedForSelf + val selectionOffset by animateDpAsState(if (selectionVisible && !sent) 4.dp + 22.dp * fontSizeMultiplier else 0.dp) + val swipeableOrSelectionModifier = (if (selectionVisible) Modifier else swipeableModifier).graphicsLayer { translationX = selectionOffset.toPx() } + if (chatInfo is ChatInfo.Group) { + if (cItem.chatDir is CIDirection.GroupRcv) { + val member = cItem.chatDir.groupMember + val (prevMember, memCount) = + if (range != null) { + chatModel.getPrevHiddenMember(member, range) + } else { + null to 1 } - Row( - swipeableModifier, - horizontalArrangement = Arrangement.spacedBy(4.dp) + if (prevItem == null || showMemberImage(member, prevItem) || prevMember != null) { + Column( + Modifier + .padding(top = 8.dp) + .padding(start = 8.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.Start ) { - Box(Modifier.clickable { showMemberInfo(chat.chatInfo.groupInfo, member) }) { - MemberImage(member) + if (cItem.content.showMemberName) { + val memberNameStyle = SpanStyle(fontSize = 13.5.sp, color = CurrentColors.value.colors.secondary) + val memberNameString = if (memCount == 1 && member.memberRole > GroupMemberRole.Member) { + buildAnnotatedString { + withStyle(memberNameStyle.copy(fontWeight = FontWeight.Medium)) { append(member.memberRole.text) } + append(" ") + withStyle(memberNameStyle) { append(memberNames(member, prevMember, memCount)) } + } + } else { + buildAnnotatedString { + withStyle(memberNameStyle) { append(memberNames(member, prevMember, memCount)) } + } + } + Text( + memberNameString, + Modifier.padding(start = MEMBER_IMAGE_SIZE + 10.dp), + maxLines = 2 + ) + } + Box(contentAlignment = Alignment.CenterStart) { + androidx.compose.animation.AnimatedVisibility(selectionVisible, enter = fadeIn(), exit = fadeOut()) { + SelectedChatItem(Modifier, cItem.id, selectedChatItems) + } + Row( + swipeableOrSelectionModifier, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Box(Modifier.clickable { showMemberInfo(chatInfo.groupInfo, member) }) { + MemberImage(member) + } + ChatItemViewShortHand(cItem, range) + } + } + } + } else { + Box(contentAlignment = Alignment.CenterStart) { + AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) { + SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems) + } + Row( + Modifier + .padding(start = 8.dp + MEMBER_IMAGE_SIZE + 4.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp) + .then(swipeableOrSelectionModifier) + ) { + ChatItemViewShortHand(cItem, range) } - ChatItemViewShortHand(cItem, range) } } } else { - Row( - Modifier - .padding(start = 8.dp + MEMBER_IMAGE_SIZE + 4.dp, end = if (voiceWithTransparentBack) 12.dp else 66.dp) - .then(swipeableModifier) + Box(contentAlignment = Alignment.CenterStart) { + AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) { + SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems) + } + Box( + Modifier + .padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp) + .then(if (selectionVisible) Modifier else swipeableModifier) + ) { + ChatItemViewShortHand(cItem, range) + } + } + } + } else { // direct message + Box(contentAlignment = Alignment.CenterStart) { + AnimatedVisibility (selectionVisible, enter = fadeIn(), exit = fadeOut()) { + SelectedChatItem(Modifier.padding(start = 8.dp), cItem.id, selectedChatItems) + } + Box( + Modifier.padding( + start = if (sent && !voiceWithTransparentBack) 76.dp else 12.dp, + end = if (sent || voiceWithTransparentBack) 12.dp else 76.dp, + ).then(if (!selectionVisible || !sent) swipeableOrSelectionModifier else Modifier) ) { ChatItemViewShortHand(cItem, range) } } - } else { - Box( - Modifier - .padding(start = if (voiceWithTransparentBack) 12.dp else 104.dp, end = 12.dp) - .then(swipeableModifier) - ) { - ChatItemViewShortHand(cItem, range) - } } - } else { // direct message - val sent = cItem.chatDir.sent - Box( - Modifier.padding( - start = if (sent && !voiceWithTransparentBack) 76.dp else 12.dp, - end = if (sent || voiceWithTransparentBack) 12.dp else 76.dp, - ).then(swipeableModifier) - ) { - ChatItemViewShortHand(cItem, range) + if (selectionVisible) { + Box(Modifier.matchParentSize().clickable { + val checked = selectedChatItems.value?.contains(cItem.id) == true + selectUnselectChatItem(select = !checked, cItem, revealed, selectedChatItems) + }) } } } @@ -1065,7 +1172,7 @@ fun BoxWithConstraintsScope.ChatItemsList( } } - if (cItem.isRcvNew && chat.id == ChatModel.chatId.value) { + if (cItem.isRcvNew && chatInfo.id == ChatModel.chatId.value) { LaunchedEffect(cItem.id) { scope.launch { delay(600) @@ -1076,7 +1183,13 @@ fun BoxWithConstraintsScope.ChatItemsList( } } } - FloatingButtons(chatModel.chatItems, unreadCount, chat.chatStats.minUnreadItemId, searchValue, markRead, setFloatingButton, listState) + FloatingButtons(chatModel.chatItems, unreadCount, remoteHostId, chatInfo, searchValue, markRead, setFloatingButton, listState) + LaunchedEffect(Unit) { + snapshotFlow { listState.isScrollInProgress } + .collect { + chatViewScrollState.value = it + } + } } @Composable @@ -1103,7 +1216,7 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: .filter { listState.layoutInfo.visibleItemsInfo.firstOrNull()?.key != it } .collect { try { - if (listState.firstVisibleItemIndex == 0 || (listState.firstVisibleItemIndex == 1 && listState.layoutInfo.totalItemsCount == chatItems.size)) { + if (listState.firstVisibleItemIndex == 0 || (listState.firstVisibleItemIndex == 1 && listState.layoutInfo.totalItemsCount == chatItems.value.size)) { if (appPlatform.isAndroid) listState.animateScrollToItem(0) else listState.scrollToItem(0) } else { if (appPlatform.isAndroid) listState.animateScrollBy(scrollDistance) else listState.scrollBy(scrollDistance) @@ -1114,6 +1227,8 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: * this coroutine will be canceled with the message "Current mutation had a higher priority" because of animatedScroll. * Which breaks auto-scrolling to bottom. So just ignoring the exception * */ + } catch (e: IllegalArgumentException) { + Log.e(TAG, "Failed to scroll: ${e.stackTraceToString()}") } } } @@ -1123,7 +1238,8 @@ private fun ScrollToBottom(chatId: ChatId, listState: LazyListState, chatItems: fun BoxWithConstraintsScope.FloatingButtons( chatItems: State>, unreadCount: State, - minUnreadItemId: Long, + remoteHostId: Long?, + chatInfo: ChatInfo, searchValue: State, markRead: (CC.ItemRange, unreadCountAfter: Int?) -> Unit, setFloatingButton: (@Composable () -> Unit) -> Unit, @@ -1204,8 +1320,9 @@ fun BoxWithConstraintsScope.FloatingButtons( generalGetString(MR.strings.mark_read), painterResource(MR.images.ic_check), onClick = { + val minUnreadItemId = chatModel.chats.value.firstOrNull { it.remoteHostId == remoteHostId && it.id == chatInfo.id }?.chatStats?.minUnreadItemId ?: return@ItemAction markRead( - CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1), + CC.ItemRange(minUnreadItemId, chatItems.value[chatItems.value.size - listState.layoutInfo.visibleItemsInfo.lastIndex - 1].id - 1), bottomUnreadCount ) showDropDown.value = false @@ -1216,14 +1333,17 @@ fun BoxWithConstraintsScope.FloatingButtons( @Composable fun PreloadItems( + chatId: String, listState: LazyListState, remaining: Int = 10, - onLoadMore: () -> Unit, + onLoadMore: (ChatId) -> Unit, ) { // Prevent situation when initial load and load more happens one after another after selecting a chat with long scroll position from previous selection val allowLoad = remember { mutableStateOf(false) } + val chatId = rememberUpdatedState(chatId) + val onLoadMore = rememberUpdatedState(onLoadMore) LaunchedEffect(Unit) { - snapshotFlow { chatModel.chatId.value } + snapshotFlow { chatId.value } .filterNotNull() .collect { allowLoad.value = listState.layoutInfo.totalItemsCount == listState.layoutInfo.visibleItemsInfo.size @@ -1243,7 +1363,7 @@ fun PreloadItems( } .filter { it > 0 } .collect { - onLoadMore() + onLoadMore.value(chatId.value) } } } @@ -1259,7 +1379,7 @@ val MEMBER_IMAGE_SIZE: Dp = 38.dp @Composable fun MemberImage(member: GroupMember) { - ProfileImage(MEMBER_IMAGE_SIZE, member.memberProfile.image) + MemberProfileImage(MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier, member, backgroundColor = MaterialTheme.colors.background) } @Composable @@ -1274,7 +1394,7 @@ private fun TopEndFloatingButton( val interactionSource = interactionSourceWithDetection(onClick, onLongClick) FloatingActionButton( {}, // no action here - modifier.size(48.dp), + modifier.size(48.dp).onRightClick(onLongClick), backgroundColor = MaterialTheme.colors.secondaryVariant, elevation = FloatingActionButtonDefaults.elevation(0.dp, 0.dp), interactionSource = interactionSource, @@ -1290,6 +1410,30 @@ private fun TopEndFloatingButton( } } +val chatViewScrollState = MutableStateFlow(false) + +fun addGroupMembers(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (() -> Unit)? = null) { + hideKeyboard(view) + withBGApi { + setGroupMembers(rhId, groupInfo, chatModel) + close?.invoke() + ModalManager.end.showModalCloseable(true) { close -> + AddGroupMembersView(rhId, groupInfo, false, chatModel, close) + } + } +} + +fun openGroupLink(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: (() -> Unit)? = null) { + hideKeyboard(view) + withBGApi { + val link = chatModel.controller.apiGetGroupLink(rhId, groupInfo.groupId) + close?.invoke() + ModalManager.end.showModalCloseable(true) { + GroupLinkView(chatModel, rhId, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null) + } + } +} + private fun bottomEndFloatingButton( unreadCount: Int, showButtonWithCounter: Boolean, @@ -1334,8 +1478,98 @@ private fun bottomEndFloatingButton( } } -private fun markUnreadChatAsRead(activeChat: MutableState, chatModel: ChatModel) { - val chat = activeChat.value +@Composable +private fun SelectedChatItem( + modifier: Modifier, + ciId: Long, + selectedChatItems: State?>, +) { + val checked = remember { derivedStateOf { selectedChatItems.value?.contains(ciId) == true } } + Icon( + painterResource(if (checked.value) MR.images.ic_check_circle_filled else MR.images.ic_radio_button_unchecked), + null, + modifier.size(22.dp * fontSizeMultiplier), + tint = if (checked.value) { + MaterialTheme.colors.primary + } else if (isInDarkTheme()) { + // .tertiaryLabel instead of .secondary + Color(red = 235f / 255f, 235f / 255f, 245f / 255f, 76f / 255f) + } else { + // .tertiaryLabel instead of .secondary + Color(red = 60f / 255f, 60f / 255f, 67f / 255f, 76f / 255f) + } + ) +} + +private fun selectUnselectChatItem(select: Boolean, ci: ChatItem, revealed: State, selectedChatItems: MutableState?>) { + val itemIds = mutableSetOf() + if (!revealed.value) { + val currIndex = chatModel.getChatItemIndexOrNull(ci) + val ciCategory = ci.mergeCategory + if (currIndex != null && ciCategory != null) { + val (prevHidden, _) = chatModel.getPrevShownChatItem(currIndex, ciCategory) + val range = chatViewItemsRange(currIndex, prevHidden) + if (range != null) { + val reversedChatItems = chatModel.chatItems.asReversed() + for (i in range) { + itemIds.add(reversedChatItems[i].id) + } + } else { + itemIds.add(ci.id) + } + } else { + itemIds.add(ci.id) + } + } else { + itemIds.add(ci.id) + } + if (select) { + val sel = selectedChatItems.value ?: setOf() + selectedChatItems.value = sel.union(itemIds) + } else { + val sel = (selectedChatItems.value ?: setOf()).toMutableSet() + sel.removeAll(itemIds) + selectedChatItems.value = sel + } +} + +private fun deleteMessages(chatRh: Long?, chatInfo: ChatInfo, itemIds: List, forAll: Boolean, moderate: Boolean, onSuccess: () -> Unit = {}) { + if (itemIds.isNotEmpty()) { + withBGApi { + val deleted = if (chatInfo is ChatInfo.Group && forAll && moderate) { + chatModel.controller.apiDeleteMemberChatItems( + chatRh, + groupId = chatInfo.groupInfo.groupId, + itemIds = itemIds + ) + } else { + chatModel.controller.apiDeleteChatItems( + chatRh, + type = chatInfo.chatType, + id = chatInfo.apiId, + itemIds = itemIds, + mode = if (forAll) CIDeleteMode.cidmBroadcast else CIDeleteMode.cidmInternal + ) + } + if (deleted != null) { + withChats { + for (di in deleted) { + val toChatItem = di.toChatItem?.chatItem + if (toChatItem != null) { + upsertChatItem(chatRh, chatInfo, toChatItem) + } else { + removeChatItem(chatRh, chatInfo, di.deletedChatItem.chatItem) + } + } + } + onSuccess() + } + } + } +} + +private fun markUnreadChatAsRead(chatId: String) { + val chat = chatModel.chats.value.firstOrNull { it.id == chatId } if (chat?.chatStats?.unreadChat != true) return withApi { val chatRh = chat.remoteHostId @@ -1345,9 +1579,10 @@ private fun markUnreadChatAsRead(activeChat: MutableState, chatModel: Cha chat.chatInfo.apiId, false ) - if (success && chat.id == activeChat.value?.id) { - activeChat.value = chat.copy(chatStats = chat.chatStats.copy(unreadChat = false)) - chatModel.replaceChat(chatRh, chat.id, activeChat.value!!) + if (success) { + withChats { + replaceChat(chatRh, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false))) + } } } } @@ -1380,7 +1615,7 @@ sealed class ProviderMedia { data class Video(val uri: URI, val fileSource: CryptoFile?, val preview: String): ProviderMedia() } -private fun providerForGallery( +fun providerForGallery( listStateIndex: Int, chatItems: List, cItemId: Long, @@ -1500,12 +1735,8 @@ fun PreviewChatLayout() { val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) } val searchValue = remember { mutableStateOf("") } ChatLayout( - chat = Chat( - remoteHostId = null, - chatInfo = ChatInfo.Direct.sampleData, - chatItems = chatItems, - chatStats = Chat.ChatStats() - ), + remoteHostId = remember { mutableStateOf(null) }, + chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) }, unreadCount = unreadCount, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, composeView = {}, @@ -1514,6 +1745,7 @@ fun PreviewChatLayout() { searchValue, useLinkPreviews = true, linkMode = SimplexLinkMode.DESCRIPTION, + selectedChatItems = remember { mutableStateOf(setOf()) }, back = {}, info = {}, showMemberInfo = { _, _ -> }, @@ -1528,6 +1760,7 @@ fun PreviewChatLayout() { acceptCall = { _ -> }, acceptFeature = { _, _, _ -> }, openDirectChat = { _ -> }, + forwardItem = { _, _ -> }, updateContactStats = { }, updateMemberStats = { _, _ -> }, syncContactConnection = { }, @@ -1543,6 +1776,8 @@ fun PreviewChatLayout() { onSearchValueChanged = {}, onComposed = {}, developerTools = false, + showViaProxy = false, + showSearch = remember { mutableStateOf(false) } ) } } @@ -1572,12 +1807,8 @@ fun PreviewGroupChatLayout() { val unreadCount = remember { mutableStateOf(chatItems.count { it.isRcvNew }) } val searchValue = remember { mutableStateOf("") } ChatLayout( - chat = Chat( - remoteHostId = null, - chatInfo = ChatInfo.Group.sampleData, - chatItems = chatItems, - chatStats = Chat.ChatStats() - ), + remoteHostId = remember { mutableStateOf(null) }, + chatInfo = remember { mutableStateOf(ChatInfo.Direct.sampleData) }, unreadCount = unreadCount, composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, composeView = {}, @@ -1586,6 +1817,7 @@ fun PreviewGroupChatLayout() { searchValue, useLinkPreviews = true, linkMode = SimplexLinkMode.DESCRIPTION, + selectedChatItems = remember { mutableStateOf(setOf()) }, back = {}, info = {}, showMemberInfo = { _, _ -> }, @@ -1600,6 +1832,7 @@ fun PreviewGroupChatLayout() { acceptCall = { _ -> }, acceptFeature = { _, _, _ -> }, openDirectChat = { _ -> }, + forwardItem = { _, _ -> }, updateContactStats = { }, updateMemberStats = { _, _ -> }, syncContactConnection = { }, @@ -1615,6 +1848,8 @@ fun PreviewGroupChatLayout() { onSearchValueChanged = {}, onComposed = {}, developerTools = false, + showViaProxy = false, + showSearch = remember { mutableStateOf(false) } ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextInvitingContactMemberView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextInvitingContactMemberView.kt index 20316dd524..bc82bc593f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextInvitingContactMemberView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextInvitingContactMemberView.kt @@ -16,7 +16,7 @@ import dev.icerock.moko.resources.compose.stringResource @Composable fun ComposeContextInvitingContactMemberView() { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage + val sentColor = MaterialTheme.appColors.sentMessage Row( Modifier .height(60.dp) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeFileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeFileView.kt index 83076f885b..7ab7963547 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeFileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeFileView.kt @@ -16,7 +16,7 @@ import chat.simplex.res.MR @Composable fun ComposeFileView(fileName: String, cancelFile: () -> Unit, cancelEnabled: Boolean) { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage + val sentColor = MaterialTheme.appColors.sentMessage Row( Modifier .height(60.dp) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeImageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeImageView.kt index 906065f741..97b6f9afda 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeImageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeImageView.kt @@ -20,7 +20,7 @@ import chat.simplex.common.views.helpers.UploadContent @Composable fun ComposeImageView(media: ComposePreview.MediaPreview, cancelImages: () -> Unit, cancelEnabled: Boolean) { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage + val sentColor = MaterialTheme.appColors.sentMessage Row( Modifier .padding(top = 8.dp) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index 26ff8796d4..372de02b41 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -13,13 +13,16 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontStyle import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.ChatModel.filesToDelete +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.item.* @@ -391,7 +394,9 @@ fun ComposeView( ttl = ttl ) if (aChatItem != null) { - chatModel.addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem) + withChats { + addChatItem(chat.remoteHostId, cInfo, aChatItem.chatItem) + } return aChatItem.chatItem } if (file != null) removeFile(file.filePath) @@ -408,17 +413,20 @@ fun ComposeView( composeState.value = composeState.value.copy(inProgress = true) } - suspend fun forwardItem(rhId: Long?, forwardedItem: ChatItem, fromChatInfo: ChatInfo): ChatItem? { + suspend fun forwardItem(rhId: Long?, forwardedItem: ChatItem, fromChatInfo: ChatInfo, ttl: Int?): ChatItem? { val chatItem = controller.apiForwardChatItem( rh = rhId, toChatType = chat.chatInfo.chatType, toChatId = chat.chatInfo.apiId, fromChatType = fromChatInfo.chatType, fromChatId = fromChatInfo.apiId, - itemId = forwardedItem.id + itemId = forwardedItem.id, + ttl = ttl ) if (chatItem != null) { - chatModel.addChatItem(rhId, chat.chatInfo, chatItem) + withChats { + addChatItem(rhId, chat.chatInfo, chatItem) + } } return chatItem } @@ -455,7 +463,9 @@ fun ComposeView( val mc = checkLinkPreview() val contact = chatModel.controller.apiSendMemberContactInvitation(chat.remoteHostId, chat.chatInfo.apiId, mc) if (contact != null) { - chatModel.updateContact(chat.remoteHostId, contact) + withChats { + updateContact(chat.remoteHostId, contact) + } } } @@ -471,7 +481,9 @@ fun ComposeView( mc = updateMsgContent(oldMsgContent), live = live ) - if (updatedItem != null) chatModel.upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem) + if (updatedItem != null) withChats { + upsertChatItem(chat.remoteHostId, cInfo, updatedItem.chatItem) + } return updatedItem?.chatItem } return null @@ -490,9 +502,9 @@ fun ComposeView( sendMemberContactInvitation() sent = null } else if (cs.contextItem is ComposeContextItem.ForwardingItem) { - sent = forwardItem(chat.remoteHostId, cs.contextItem.chatItem, cs.contextItem.fromChatInfo) + sent = forwardItem(chat.remoteHostId, cs.contextItem.chatItem, cs.contextItem.fromChatInfo, ttl = ttl) if (cs.message.isNotEmpty()) { - sent = send(chat, checkLinkPreview(), quoted = sent?.id, live = false, ttl = null) + sent = send(chat, checkLinkPreview(), quoted = sent?.id, live = false, ttl = ttl) } } else if (cs.contextItem is ComposeContextItem.EditingItem) { val ei = cs.contextItem.chatItem @@ -775,7 +787,7 @@ fun ComposeView( @Composable fun MsgNotAllowedView(reason: String, icon: Painter) { - val color = CurrentColors.collectAsState().value.appColors.receivedMessage + val color = MaterialTheme.appColors.receivedMessage Row(Modifier.padding(top = 5.dp).fillMaxWidth().background(color).padding(horizontal = DEFAULT_PADDING_HALF, vertical = DEFAULT_PADDING_HALF * 1.5f), verticalAlignment = Alignment.CenterVertically) { Icon(icon, null, tint = MaterialTheme.colors.secondary) Spacer(Modifier.width(DEFAULT_PADDING_HALF)) @@ -824,7 +836,7 @@ fun ComposeView( chatModel.sharedContent.value = null } - val userCanSend = rememberUpdatedState(chat.userCanSend) + val userCanSend = rememberUpdatedState(chat.chatInfo.userCanSend) val sendMsgEnabled = rememberUpdatedState(chat.chatInfo.sendMsgEnabled) val userIsObserver = rememberUpdatedState(chat.userIsObserver) val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv) @@ -860,10 +872,9 @@ fun ComposeView( } } } - Row( - modifier = Modifier.padding(end = 8.dp), - verticalAlignment = Alignment.Bottom, - ) { + Column(Modifier.background(MaterialTheme.colors.background)) { + Divider() + Row(Modifier.padding(end = 8.dp), verticalAlignment = Alignment.Bottom) { val isGroupAndProhibitedFiles = chat.chatInfo is ChatInfo.Group && !chat.chatInfo.groupInfo.fullGroupPreferences.files.on(chat.chatInfo.groupInfo.membership) val attachmentClicked = if (isGroupAndProhibitedFiles) { { @@ -883,7 +894,7 @@ fun ComposeView( && !nextSendGrpInv.value IconButton( attachmentClicked, - Modifier.padding(bottom = if (appPlatform.isAndroid) 0.dp else 7.dp), + Modifier.padding(bottom = if (appPlatform.isAndroid) 2.sp.toDp() else 5.sp.toDp() * fontSizeSqrtMultiplier), enabled = attachmentEnabled ) { Icon( @@ -912,7 +923,7 @@ fun ComposeView( snapshotFlow { recState.value } .distinctUntilChanged() .collect { - when(it) { + when (it) { is RecordingState.Started -> onAudioAdded(it.filePath, it.progressMs, false) is RecordingState.Finished -> if (it.durationMs > 300) { onAudioAdded(it.filePath, it.durationMs, true) @@ -924,8 +935,8 @@ fun ComposeView( } } - LaunchedEffect(rememberUpdatedState(chat.userCanSend).value) { - if (!chat.userCanSend) { + LaunchedEffect(rememberUpdatedState(chat.chatInfo.userCanSend).value) { + if (!chat.chatInfo.userCanSend) { clearCurrentDraft() clearState() } @@ -973,7 +984,7 @@ fun ComposeView( val timedMessageAllowed = remember(chat.chatInfo) { chat.chatInfo.featureEnabled(ChatFeature.TimedMessages) } val sendButtonColor = if (chat.chatInfo.incognito) - if (isSystemInDarkTheme()) Indigo else Indigo.copy(alpha = 0.7F) + if (isInDarkTheme()) Indigo else Indigo.copy(alpha = 0.7F) else MaterialTheme.colors.primary SendMsgView( composeState, @@ -992,6 +1003,7 @@ fun ComposeView( sendButtonColor = sendButtonColor, timedMessageAllowed = timedMessageAllowed, customDisappearingMessageTimePref = chatModel.controller.appPrefs.customDisappearingMessageTime, + placeholder = stringResource(MR.strings.compose_message_placeholder), sendMessage = { ttl -> sendMessage(ttl) resetLinkPreview() @@ -1009,4 +1021,5 @@ fun ComposeView( ) } } + } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeVoiceView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeVoiceView.kt index a4c90d30dd..b070dce1d1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeVoiceView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeVoiceView.kt @@ -35,7 +35,7 @@ fun ComposeVoiceView( ) { val progress = rememberSaveable { mutableStateOf(0) } val duration = rememberSaveable(recordedDurationMs) { mutableStateOf(recordedDurationMs) } - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage + val sentColor = MaterialTheme.appColors.sentMessage Box { Box( Modifier @@ -53,7 +53,7 @@ fun ComposeVoiceView( IconButton( onClick = { if (!audioPlaying.value) { - AudioPlayer.play(CryptoFile.plain(filePath), audioPlaying, progress, duration, false) + AudioPlayer.play(CryptoFile.plain(filePath), audioPlaying, progress, duration, resetOnEnd = false, smallView = false) } else { AudioPlayer.pause(audioPlaying, progress) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt index 502074d629..725367e150 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContactPreferences.kt @@ -19,6 +19,7 @@ import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.PreferenceToggle import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.res.MR @@ -40,8 +41,10 @@ fun ContactPreferencesView( val prefs = contactFeaturesAllowedToPrefs(featuresAllowed) val toContact = m.controller.apiSetContactPrefs(rhId, ct.contactId, prefs) if (toContact != null) { - m.updateContact(rhId, toContact) - currentFeaturesAllowed = featuresAllowed + withChats { + updateContact(rhId, toContact) + currentFeaturesAllowed = featuresAllowed + } } afterSave() } @@ -90,22 +93,22 @@ private fun ContactPreferencesLayout( TimedMessagesFeatureSection(featuresAllowed, contact.mergedPreferences.timedMessages, timedMessages, onTTLUpdated) { allowed, ttl -> applyPrefs(featuresAllowed.copy(timedMessagesAllowed = allowed, timedMessagesTTL = ttl ?: currentFeaturesAllowed.timedMessagesTTL)) } - SectionDividerSpaced(true, maxBottomPadding = false) + SectionDividerSpaced(true) val allowFullDeletion: MutableState = remember(featuresAllowed) { mutableStateOf(featuresAllowed.fullDelete) } FeatureSection(ChatFeature.FullDelete, user.fullPreferences.fullDelete.allow, contact.mergedPreferences.fullDelete, allowFullDeletion) { applyPrefs(featuresAllowed.copy(fullDelete = it)) } - SectionDividerSpaced(true, maxBottomPadding = false) + SectionDividerSpaced(true) val allowReactions: MutableState = remember(featuresAllowed) { mutableStateOf(featuresAllowed.reactions) } FeatureSection(ChatFeature.Reactions, user.fullPreferences.reactions.allow, contact.mergedPreferences.reactions, allowReactions) { applyPrefs(featuresAllowed.copy(reactions = it)) } - SectionDividerSpaced(true, maxBottomPadding = false) + SectionDividerSpaced(true) val allowVoice: MutableState = remember(featuresAllowed) { mutableStateOf(featuresAllowed.voice) } FeatureSection(ChatFeature.Voice, user.fullPreferences.voice.allow, contact.mergedPreferences.voice, allowVoice) { applyPrefs(featuresAllowed.copy(voice = it)) } - SectionDividerSpaced(true, maxBottomPadding = false) + SectionDividerSpaced(true) val allowCalls: MutableState = remember(featuresAllowed) { mutableStateOf(featuresAllowed.calls) } FeatureSection(ChatFeature.Calls, user.fullPreferences.calls.allow, contact.mergedPreferences.calls, allowCalls) { applyPrefs(featuresAllowed.copy(calls = it)) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt index ce34ecf0c3..0c4efa7d0d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ContextItemView.kt @@ -30,8 +30,8 @@ fun ContextItemView( cancelContextItem: () -> Unit ) { val sent = contextItem.chatDir.sent - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage @Composable fun MessageText(attachment: ImageResource?, lines: Int) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt new file mode 100644 index 0000000000..2aebe07306 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SelectableChatItemToolbars.kt @@ -0,0 +1,133 @@ +package chat.simplex.common.views.chat + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import chat.simplex.common.model.* +import chat.simplex.common.platform.BackHandler +import chat.simplex.common.platform.chatModel +import chat.simplex.common.views.helpers.* +import dev.icerock.moko.resources.compose.stringResource +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource + +@Composable +fun SelectedItemsTopToolbar(selectedChatItems: MutableState?>) { + val onBackClicked = { selectedChatItems.value = null } + BackHandler(onBack = onBackClicked) + val count = selectedChatItems.value?.size ?: 0 + DefaultTopAppBar( + navigationButton = { NavigationButtonClose(onButtonClicked = onBackClicked) }, + title = { + Text( + if (count == 0) { + stringResource(MR.strings.selected_chat_items_nothing_selected) + } else { + stringResource(MR.strings.selected_chat_items_selected_n).format(count) + }, + fontWeight = FontWeight.SemiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + onTitleClick = null, + showSearch = false, + onSearchValueChanged = {}, + ) + Divider(Modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier)) +} + +@Composable +fun SelectedItemsBottomToolbar( + chatInfo: ChatInfo, + chatItems: List, + selectedChatItems: MutableState?>, + deleteItems: (Boolean) -> Unit, // Boolean - delete for everyone is possible + moderateItems: () -> Unit, +// shareItems: () -> Unit, +) { + val deleteEnabled = remember { mutableStateOf(false) } + val deleteForEveryoneEnabled = remember { mutableStateOf(false) } + val canModerate = remember { mutableStateOf(false) } + val moderateEnabled = remember { mutableStateOf(false) } + val allButtonsDisabled = remember { mutableStateOf(false) } + Box { + // It's hard to measure exact height of ComposeView with different fontSizes. Better to depend on actual ComposeView, even empty + ComposeView(chatModel = chatModel, Chat.sampleData, remember { mutableStateOf(ComposeState(useLinkPreviews = false)) }, remember { mutableStateOf(null) }, {}) + Row(Modifier.matchParentSize().background(MaterialTheme.colors.background), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) { + IconButton({ deleteItems(deleteForEveryoneEnabled.value) }, enabled = deleteEnabled.value && !allButtonsDisabled.value) { + Icon( + painterResource(MR.images.ic_delete), + null, + Modifier.size(22.dp), + tint = if (!deleteEnabled.value || allButtonsDisabled.value) MaterialTheme.colors.secondary else MaterialTheme.colors.error + ) + } + + IconButton({ moderateItems() }, Modifier.alpha(if (canModerate.value) 1f else 0f), enabled = moderateEnabled.value && !allButtonsDisabled.value) { + Icon( + painterResource(MR.images.ic_flag), + null, + Modifier.size(22.dp), + tint = if (!moderateEnabled.value || allButtonsDisabled.value) MaterialTheme.colors.secondary else MaterialTheme.colors.error + ) + } + + IconButton({ /*shareItems()*/ }, Modifier.alpha(0f), enabled = false/*!allButtonsDisabled.value*/) { + Icon( + painterResource(MR.images.ic_share), + null, + Modifier.size(22.dp), + tint = if (allButtonsDisabled.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + ) + } + } + } + LaunchedEffect(chatInfo, chatItems, selectedChatItems.value) { + recheckItems(chatInfo, chatItems, selectedChatItems, deleteEnabled, deleteForEveryoneEnabled, canModerate, moderateEnabled, allButtonsDisabled) + } +} + +private fun recheckItems(chatInfo: ChatInfo, + chatItems: List, + selectedChatItems: MutableState?>, + deleteEnabled: MutableState, + deleteForEveryoneEnabled: MutableState, + canModerate: MutableState, + moderateEnabled: MutableState, + allButtonsDisabled: MutableState +) { + val count = selectedChatItems.value?.size ?: 0 + allButtonsDisabled.value = count == 0 || count > 20 + canModerate.value = possibleToModerate(chatInfo) + val selected = selectedChatItems.value ?: return + var rDeleteEnabled = true + var rDeleteForEveryoneEnabled = true + var rModerateEnabled = true + var rOnlyOwnGroupItems = true + val rSelectedChatItems = mutableSetOf() + for (ci in chatItems) { + if (selected.contains(ci.id)) { + rDeleteEnabled = rDeleteEnabled && ci.canBeDeletedForSelf + rDeleteForEveryoneEnabled = rDeleteForEveryoneEnabled && ci.meta.deletable && !ci.localNote + rOnlyOwnGroupItems = rOnlyOwnGroupItems && ci.chatDir is CIDirection.GroupSnd + rModerateEnabled = rModerateEnabled && ci.content.msgContent != null && ci.memberToModerate(chatInfo) != null + rSelectedChatItems.add(ci.id) // we are collecting new selected items here to account for any changes in chat items list + } + } + rModerateEnabled = rModerateEnabled && !rOnlyOwnGroupItems + deleteEnabled.value = rDeleteEnabled + deleteForEveryoneEnabled.value = rDeleteForEveryoneEnabled + moderateEnabled.value = rModerateEnabled + selectedChatItems.value = rSelectedChatItems +} + +private fun possibleToModerate(chatInfo: ChatInfo): Boolean = + chatInfo is ChatInfo.Group && chatInfo.groupInfo.membership.memberRole >= GroupMemberRole.Admin diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt index 58705bd00a..162e753b18 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/SendMsgView.kt @@ -49,6 +49,7 @@ fun SendMsgView( allowVoiceToContact: () -> Unit, timedMessageAllowed: Boolean = false, customDisappearingMessageTimePref: SharedPreference? = null, + placeholder: String, sendMessage: (Int?) -> Unit, sendLiveMessage: (suspend () -> Unit)? = null, updateLiveMessage: (suspend () -> Unit)? = null, @@ -60,7 +61,7 @@ fun SendMsgView( ) { val showCustomDisappearingMessageDialog = remember { mutableStateOf(false) } - Box(Modifier.padding(vertical = 8.dp)) { + Box(Modifier.padding(vertical = if (appPlatform.isAndroid) 8.dp else 6.dp)) { val cs = composeState.value var progressByTimeout by rememberSaveable { mutableStateOf(false) } LaunchedEffect(composeState.value.inProgress) { @@ -78,13 +79,25 @@ fun SendMsgView( (!allowedVoiceByPrefs && cs.preview is ComposePreview.VoicePreview) || cs.endLiveDisabled || !sendButtonEnabled - PlatformTextField(composeState, sendMsgEnabled, sendMsgButtonDisabled, textStyle, showDeleteTextButton, userIsObserver, onMessageChange, editPrevMessage, onFilesPasted) { + val clicksOnTextFieldDisabled = !sendMsgEnabled || cs.preview is ComposePreview.VoicePreview || !userCanSend || cs.inProgress + PlatformTextField( + composeState, + sendMsgEnabled, + sendMsgButtonDisabled, + textStyle, + showDeleteTextButton, + userIsObserver, + if (clicksOnTextFieldDisabled) "" else placeholder, + showVoiceButton, + onMessageChange, + editPrevMessage, + onFilesPasted + ) { if (!cs.inProgress) { sendMessage(null) } } - // Disable clicks on text field - if (!sendMsgEnabled || cs.preview is ComposePreview.VoicePreview || !userCanSend || cs.inProgress) { + if (clicksOnTextFieldDisabled) { Box( Modifier .matchParentSize() @@ -99,7 +112,7 @@ fun SendMsgView( if (showDeleteTextButton.value) { DeleteTextButton(composeState) } - Box(Modifier.align(Alignment.BottomEnd).padding(bottom = if (appPlatform.isAndroid) 0.dp else 5.dp)) { + Box(Modifier.align(Alignment.BottomEnd).padding(bottom = if (appPlatform.isAndroid) 0.dp else 5.sp.toDp() * fontSizeSqrtMultiplier)) { val sendButtonSize = remember { Animatable(36f) } val sendButtonAlpha = remember { Animatable(1f) } val scope = rememberCoroutineScope() @@ -157,7 +170,7 @@ fun SendMsgView( fun MenuItems(): List<@Composable () -> Unit> { val menuItems = mutableListOf<@Composable () -> Unit>() - if (cs.liveMessage == null && !cs.editing && !cs.forwarding && !nextSendGrpInv || sendMsgEnabled) { + if (cs.liveMessage == null && !cs.editing && !nextSendGrpInv || sendMsgEnabled) { if ( cs.preview !is ComposePreview.VoicePreview && cs.contextItem is ComposeContextItem.NoContextItem && @@ -562,6 +575,7 @@ fun PreviewSendMsgView() { userCanSend = true, allowVoiceToContact = {}, timedMessageAllowed = false, + placeholder = "", sendMessage = {}, editPrevMessage = {}, onMessageChange = { _ -> }, @@ -597,6 +611,7 @@ fun PreviewSendMsgViewEditing() { userCanSend = true, allowVoiceToContact = {}, timedMessageAllowed = false, + placeholder = "", sendMessage = {}, editPrevMessage = {}, onMessageChange = { _ -> }, @@ -632,6 +647,7 @@ fun PreviewSendMsgViewInProgress() { userCanSend = true, allowVoiceToContact = {}, timedMessageAllowed = false, + placeholder = "", sendMessage = {}, editPrevMessage = {}, onMessageChange = { _ -> }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt index d546b51a93..18a3a0d14d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt @@ -4,6 +4,7 @@ import SectionBottomSpacer import SectionCustomFooter import SectionDividerSpaced import SectionItemView +import SectionItemViewWithoutMinPadding import SectionSpacer import SectionView import androidx.compose.foundation.* @@ -24,6 +25,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.ChatInfoToolbarTitle import chat.simplex.common.views.helpers.* @@ -58,7 +60,9 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea for (contactId in selectedContacts) { val member = chatModel.controller.apiAddMember(rhId, groupInfo.groupId, contactId, selectedRole.value) if (member != null) { - chatModel.upsertGroupMember(rhId, groupInfo, member) + withChats { + upsertGroupMember(rhId, groupInfo, member) + } } else { break } @@ -81,12 +85,13 @@ fun getContactsToAdd(chatModel: ChatModel, search: String): List { val memberContactIds = chatModel.groupMembers .filter { it.memberCurrent } .mapNotNull { it.memberContactId } - return chatModel.chats + return chatModel.chats.value .asSequence() .map { it.chatInfo } .filterIsInstance() .map { it.contact } - .filter { c -> c.ready && c.active && c.contactId !in memberContactIds && c.chatViewName.lowercase().contains(s) } + .filter { c -> c.sendMsgEnabled && !c.nextSendGrpInv && c.contactId !in memberContactIds && c.anyNameContains(s) + } .sortedBy { it.displayName.lowercase() } .toList() } @@ -173,7 +178,7 @@ fun AddGroupMembersLayout( InviteSectionFooter(selectedContactsCount = selectedContacts.size, allowModifyMembers, clearSelection) } SectionDividerSpaced(maxTopPadding = true) - SectionView(stringResource(MR.strings.select_contacts)) { + SectionView(stringResource(MR.strings.select_contacts).uppercase()) { SectionItemView(padding = PaddingValues(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF)) { SearchRowView(searchText) } @@ -251,7 +256,8 @@ fun InviteSectionFooter(selectedContactsCount: Int, enabled: Boolean, clearSelec Text( String.format(generalGetString(MR.strings.num_contacts_selected), selectedContactsCount), color = MaterialTheme.colors.secondary, - fontSize = 12.sp + lineHeight = 18.sp, + fontSize = 14.sp ) Box( Modifier.clickable { if (enabled) clearSelection() } @@ -259,14 +265,16 @@ fun InviteSectionFooter(selectedContactsCount: Int, enabled: Boolean, clearSelec Text( stringResource(MR.strings.clear_contacts_selection_button), color = if (enabled) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, - fontSize = 12.sp + lineHeight = 18.sp, + fontSize = 14.sp, ) } } else { Text( stringResource(MR.strings.no_contacts_selected), color = MaterialTheme.colors.secondary, - fontSize = 12.sp + lineHeight = 18.sp, + fontSize = 14.sp, ) } } @@ -314,7 +322,7 @@ fun ContactCheckRow( icon = painterResource(MR.images.ic_circle) iconColor = MaterialTheme.colors.secondary } - SectionItemView( + SectionItemViewWithoutMinPadding( click = if (enabled) { { if (prohibitedToInviteIncognito) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index c52dd941fd..9b1bb45d8f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -9,7 +9,6 @@ import SectionSpacer import SectionTextFooter import SectionView import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* import androidx.compose.material.* @@ -24,9 +23,9 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* @@ -41,10 +40,10 @@ import kotlinx.coroutines.launch const val SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20 @Composable -fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair?) -> Unit, close: () -> Unit) { +fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair?) -> Unit, close: () -> Unit, onSearchClicked: () -> Unit) { BackHandler(onBack = close) // TODO derivedStateOf? - val chat = chatModel.chats.firstOrNull { ch -> ch.id == chatId && ch.remoteHostId == rhId } + val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatId && ch.remoteHostId == rhId } val currentUser = chatModel.currentUser.value val developerTools = chatModel.controller.appPrefs.developerTools.get() if (chat != null && chat.chatInfo is ChatInfo.Group && currentUser != null) { @@ -57,7 +56,7 @@ fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLi sendReceipts = sendReceipts, setSendReceipts = { sendRcpts -> val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(sendRcpts = sendRcpts.bool) - updateChatSettings(chat, chatSettings, chatModel) + updateChatSettings(chat.remoteHostId, chat.chatInfo, chatSettings, chatModel) sendReceipts.value = sendRcpts }, members = chatModel.groupMembers @@ -114,7 +113,8 @@ fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLi leaveGroup = { leaveGroupDialog(rhId, groupInfo, chatModel, close) }, manageGroupLink = { ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) } - } + }, + onSearchClicked = onSearchClicked ) } } @@ -132,13 +132,15 @@ fun deleteGroupDialog(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, cl withBGApi { val r = chatModel.controller.apiDeleteChat(chat.remoteHostId, chatInfo.chatType, chatInfo.apiId) if (r) { - chatModel.removeChat(chat.remoteHostId, chatInfo.id) - if (chatModel.chatId.value == chatInfo.id) { - chatModel.chatId.value = null - ModalManager.end.closeModals() + withChats { + removeChat(chat.remoteHostId, chatInfo.id) + if (chatModel.chatId.value == chatInfo.id) { + chatModel.chatId.value = null + ModalManager.end.closeModals() + } + ntfManager.cancelNotificationsForChat(chatInfo.id) + close?.invoke() } - ntfManager.cancelNotificationsForChat(chatInfo.id) - close?.invoke() } } }, @@ -170,7 +172,9 @@ private fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMembe withBGApi { val updatedMember = chatModel.controller.apiRemoveMember(rhId, groupInfo.groupId, mem.groupMemberId) if (updatedMember != null) { - chatModel.upsertGroupMember(rhId, groupInfo, updatedMember) + withChats { + upsertGroupMember(rhId, groupInfo, updatedMember) + } } } }, @@ -178,6 +182,74 @@ private fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMembe ) } +@Composable +fun SearchButton( + modifier: Modifier, + chat: Chat, + group: GroupInfo, + close: () -> Unit, + onSearchClicked: () -> Unit +) { + val disabled = !group.ready || chat.chatItems.isEmpty() + + InfoViewActionButton( + modifier = modifier, + icon = painterResource(MR.images.ic_search), + title = generalGetString(MR.strings.info_view_search_button), + disabled = disabled, + disabledLook = disabled, + onClick = { + if (appPlatform.isAndroid) { + close.invoke() + } + onSearchClicked() + } + ) +} + +@Composable +fun MuteButton( + modifier: Modifier, + chat: Chat, + groupInfo: GroupInfo +) { + val ntfsEnabled = remember { mutableStateOf(chat.chatInfo.ntfsEnabled) } + + InfoViewActionButton( + modifier = modifier, + icon = if (ntfsEnabled.value) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications), + title = if (ntfsEnabled.value) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat), + disabled = !groupInfo.ready, + disabledLook = !groupInfo.ready, + onClick = { + toggleNotifications(chat.remoteHostId, chat.chatInfo, !ntfsEnabled.value, chatModel, ntfsEnabled) + } + ) +} + +@Composable +fun AddGroupMembersButton( + modifier: Modifier, + chat: Chat, + groupInfo: GroupInfo +) { + InfoViewActionButton( + modifier = modifier, + icon = if (groupInfo.incognito) painterResource(MR.images.ic_add_link) else painterResource(MR.images.ic_person_add_500), + title = stringResource(MR.strings.action_button_add_members), + disabled = !groupInfo.ready, + disabledLook = !groupInfo.ready, + onClick = { + if (groupInfo.incognito) { + openGroupLink(groupInfo = groupInfo, rhId = chat.remoteHostId) + } else { + addGroupMembers(groupInfo = groupInfo, rhId = chat.remoteHostId) + } + } + ) +} + + @Composable fun GroupChatInfoLayout( chat: Chat, @@ -197,6 +269,8 @@ fun GroupChatInfoLayout( clearChat: () -> Unit, leaveGroup: () -> Unit, manageGroupLink: () -> Unit, + close: () -> Unit = { ModalManager.closeAllModalsEverywhere()}, + onSearchClicked: () -> Unit ) { val listState = rememberLazyListState() val scope = rememberCoroutineScope() @@ -204,8 +278,12 @@ fun GroupChatInfoLayout( scope.launch { listState.scrollToItem(0) } } val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) } - val filteredMembers = remember(members) { derivedStateOf { members.filter { it.chatViewName.lowercase().contains(searchText.value.text.trim()) } } } - // LALAL strange scrolling + val filteredMembers = remember(members) { + derivedStateOf { + val s = searchText.value.text.trim().lowercase() + if (s.isEmpty()) members else members.filter { m -> m.anyNameContains(s) } + } + } LazyColumnWithScrollBar( Modifier .fillMaxWidth(), @@ -220,6 +298,30 @@ fun GroupChatInfoLayout( } SectionSpacer() + Box( + Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Row( + Modifier + .widthIn(max = if (groupInfo.canAddMembers) 320.dp else 230.dp) + .padding(horizontal = DEFAULT_PADDING), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + if (groupInfo.canAddMembers) { + SearchButton(modifier = Modifier.fillMaxWidth(0.33f), chat, groupInfo, close, onSearchClicked) + AddGroupMembersButton(modifier = Modifier.fillMaxWidth(0.5f), chat, groupInfo) + MuteButton(modifier = Modifier.fillMaxWidth(1f), chat, groupInfo) + } else { + SearchButton(modifier = Modifier.fillMaxWidth(0.5f), chat, groupInfo, close, onSearchClicked) + MuteButton(modifier = Modifier.fillMaxWidth(1f), chat, groupInfo) + } + } + } + + SectionSpacer() + SectionView { if (groupInfo.canEdit) { EditGroupProfileButton(editGroupProfile) @@ -233,6 +335,16 @@ fun GroupChatInfoLayout( } else { SendReceiptsOptionDisabled() } + + WallpaperButton { + ModalManager.end.showModal { + val chat = remember { derivedStateOf { chatModel.chats.value.firstOrNull { it.id == chat.id } } } + val c = chat.value + if (c != null) { + ChatWallpaperEditorModal(c) + } + } + } } SectionTextFooter(stringResource(MR.strings.only_group_owners_can_change_prefs)) SectionDividerSpaced(maxTopPadding = true) @@ -253,7 +365,7 @@ fun GroupChatInfoLayout( SearchRowView(searchText) } } - SectionItemView(minHeight = 54.dp) { + SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) { MemberRow(groupInfo.membership, user = true) } } @@ -261,7 +373,7 @@ fun GroupChatInfoLayout( items(filteredMembers.value) { member -> Divider() val showMenu = remember { mutableStateOf(false) } - SectionItemViewLongClickable({ showMemberInfo(member) }, { showMenu.value = true }, minHeight = 54.dp) { + SectionItemViewLongClickable({ showMemberInfo(member) }, { showMenu.value = true }, minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) { DropDownMenuForMember(chat.remoteHostId, member, groupInfo, showMenu) MemberRow(member, onClick = { showMemberInfo(member) }) } @@ -381,6 +493,16 @@ private fun MemberRow(member: GroupMember, user: Boolean = false, onClick: (() - } } + fun memberConnStatus(): String { + return if (member.activeConn?.connDisabled == true) { + generalGetString(MR.strings.member_info_member_disabled) + } else if (member.activeConn?.connDisabled == true) { + generalGetString(MR.strings.member_info_member_inactive) + } else { + member.memberStatus.shortText + } + } + Row( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, @@ -391,7 +513,7 @@ private fun MemberRow(member: GroupMember, user: Boolean = false, onClick: (() - verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp) ) { - ProfileImage(size = 46.dp, member.image) + MemberProfileImage(size = DEFAULT_MIN_SECTION_ITEM_HEIGHT, member) Spacer(Modifier.width(DEFAULT_PADDING_HALF)) Column { Row(verticalAlignment = Alignment.CenterVertically) { @@ -403,8 +525,8 @@ private fun MemberRow(member: GroupMember, user: Boolean = false, onClick: (() - color = if (member.memberIncognito) Indigo else Color.Unspecified ) } - val s = member.memberStatus.shortText - val statusDescr = if (user) String.format(generalGetString(MR.strings.group_info_member_you), s) else s + val statusDescr = + if (user) String.format(generalGetString(MR.strings.group_info_member_you), member.memberStatus.shortText) else memberConnStatus() Text( statusDescr, color = MaterialTheme.colors.secondary, @@ -565,7 +687,7 @@ fun PreviewGroupChatInfoLayout() { members = listOf(GroupMember.sampleData, GroupMember.sampleData, GroupMember.sampleData), developerTools = false, groupLink = null, - addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, addOrEditWelcomeMessage = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {}, + addMembers = {}, showMemberInfo = {}, editGroupProfile = {}, addOrEditWelcomeMessage = {}, openPreferences = {}, deleteGroup = {}, clearChat = {}, leaveGroup = {}, manageGroupLink = {}, onSearchClicked = {}, ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index e90efa7d1b..f814fb2eb8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt @@ -3,6 +3,7 @@ package chat.simplex.common.views.chat.group import InfoRow import SectionBottomSpacer import SectionDividerSpaced +import SectionItemView import SectionSpacer import SectionTextFooter import SectionView @@ -24,9 +25,10 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import androidx.compose.ui.unit.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.* import chat.simplex.common.views.helpers.* @@ -50,7 +52,7 @@ fun GroupMemberInfoView( closeAll: () -> Unit, // Close all open windows up to ChatView ) { BackHandler(onBack = close) - val chat = chatModel.chats.firstOrNull { ch -> ch.id == chatModel.chatId.value && ch.remoteHostId == rhId } + val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatModel.chatId.value && ch.remoteHostId == rhId } val connStats = remember { mutableStateOf(connectionStats) } val developerTools = chatModel.controller.appPrefs.developerTools.get() var progressIndicator by remember { mutableStateOf(false) } @@ -58,6 +60,7 @@ fun GroupMemberInfoView( if (chat != null) { val newRole = remember { mutableStateOf(member.memberRole) } GroupMemberInfoLayout( + rhId = rhId, groupInfo, member, connStats, @@ -69,13 +72,15 @@ fun GroupMemberInfoView( withBGApi { val c = chatModel.controller.apiGetChat(rhId, ChatType.Direct, it) if (c != null) { - if (chatModel.getContactChat(it) == null) { - chatModel.addChat(c) + withChats { + if (chatModel.getContactChat(it) == null) { + addChat(c) + } + chatModel.chatItemStatuses.clear() + chatModel.chatItems.replaceAll(c.chatItems) + chatModel.chatId.value = c.id + closeAll() } - chatModel.chatItemStatuses.clear() - chatModel.chatItems.replaceAll(c.chatItems) - chatModel.chatId.value = c.id - closeAll() } } }, @@ -85,8 +90,10 @@ fun GroupMemberInfoView( val memberContact = chatModel.controller.apiCreateMemberContact(rhId, groupInfo.apiId, member.groupMemberId) if (memberContact != null) { val memberChat = Chat(remoteHostId = rhId, ChatInfo.Direct(memberContact), chatItems = arrayListOf()) - chatModel.addChat(memberChat) - openLoadedChat(memberChat, chatModel) + withChats { + addChat(memberChat) + openLoadedChat(memberChat, chatModel) + } closeAll() chatModel.setContactNetworkStatus(memberContact, NetworkStatus.Connected()) } @@ -111,7 +118,9 @@ fun GroupMemberInfoView( withBGApi { kotlin.runCatching { val mem = chatModel.controller.apiMemberRole(rhId, groupInfo.groupId, member.groupMemberId, it) - chatModel.upsertGroupMember(rhId, groupInfo, mem) + withChats { + upsertGroupMember(rhId, groupInfo, mem) + } }.onFailure { newRole.value = prevValue } @@ -124,7 +133,9 @@ fun GroupMemberInfoView( val r = chatModel.controller.apiSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId) if (r != null) { connStats.value = r.second - chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + withChats { + updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + } close.invoke() } } @@ -136,7 +147,9 @@ fun GroupMemberInfoView( val r = chatModel.controller.apiAbortSwitchGroupMember(rhId, groupInfo.apiId, member.groupMemberId) if (r != null) { connStats.value = r.second - chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + withChats { + updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + } close.invoke() } } @@ -147,7 +160,9 @@ fun GroupMemberInfoView( val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = false) if (r != null) { connStats.value = r.second - chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + withChats { + updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + } close.invoke() } } @@ -158,7 +173,9 @@ fun GroupMemberInfoView( val r = chatModel.controller.apiSyncGroupMemberRatchet(rhId, groupInfo.apiId, member.groupMemberId, force = true) if (r != null) { connStats.value = r.second - chatModel.updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + withChats { + updateGroupMemberConnectionStats(rhId, groupInfo, r.first, r.second) + } close.invoke() } } @@ -174,15 +191,17 @@ fun GroupMemberInfoView( verify = { code -> chatModel.controller.apiVerifyGroupMember(rhId, mem.groupId, mem.groupMemberId, code)?.let { r -> val (verified, existingCode) = r - chatModel.upsertGroupMember( - rhId, - groupInfo, - mem.copy( - activeConn = mem.activeConn?.copy( - connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null + withChats { + upsertGroupMember( + rhId, + groupInfo, + mem.copy( + activeConn = mem.activeConn?.copy( + connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null + ) ) ) - ) + } r } }, @@ -208,7 +227,9 @@ fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, c withBGApi { val removedMember = chatModel.controller.apiRemoveMember(rhId, member.groupId, member.groupMemberId) if (removedMember != null) { - chatModel.upsertGroupMember(rhId, groupInfo, removedMember) + withChats { + upsertGroupMember(rhId, groupInfo, removedMember) + } } close?.invoke() } @@ -219,6 +240,7 @@ fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, c @Composable fun GroupMemberInfoLayout( + rhId: Long?, groupInfo: GroupInfo, member: GroupMember, connStats: MutableState, @@ -242,10 +264,10 @@ fun GroupMemberInfoLayout( verifyClicked: () -> Unit, ) { val cStats = connStats.value - fun knownDirectChat(contactId: Long): Chat? { + fun knownDirectChat(contactId: Long): Pair? { val chat = getContactChat(contactId) return if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) { - chat + chat to chat.chatInfo.contact } else { null } @@ -305,17 +327,53 @@ fun GroupMemberInfoLayout( val contactId = member.memberContactId - if (member.memberActive) { - SectionView { - if (contactId != null && knownDirectChat(contactId) != null) { - OpenChatButton(onClick = { openDirectChat(contactId) }) + Box( + Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Row( + Modifier + .widthIn(max = 320.dp) + .padding(horizontal = DEFAULT_PADDING), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + val knownChat = if (contactId != null) knownDirectChat(contactId) else null + if (knownChat != null) { + val (chat, contact) = knownChat + OpenChatButton(modifier = Modifier.fillMaxWidth(0.33f), onClick = { openDirectChat(contact.contactId) }) + AudioCallButton(modifier = Modifier.fillMaxWidth(0.5f), chat, contact) + VideoButton(modifier = Modifier.fillMaxWidth(1f), chat, contact) } else if (groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) { if (contactId != null) { - OpenChatButton(onClick = { openDirectChat(contactId) }) - } else if (member.activeConn?.peerChatVRange?.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) == true) { - OpenChatButton(onClick = { createMemberContact() }) + OpenChatButton(modifier = Modifier.fillMaxWidth(0.33f), onClick = { openDirectChat(contactId) }) // legacy - only relevant for direct contacts created when joining group + } else { + OpenChatButton(modifier = Modifier.fillMaxWidth(0.33f), onClick = { createMemberContact() }) } + InfoViewActionButton(modifier = Modifier.fillMaxWidth(0.5f), painterResource(MR.images.ic_call), generalGetString(MR.strings.info_view_call_button), disabled = false, disabledLook = true, onClick = { + showSendMessageToEnableCallsAlert() + }) + InfoViewActionButton(modifier = Modifier.fillMaxWidth(1f), painterResource(MR.images.ic_videocam), generalGetString(MR.strings.info_view_video_button), disabled = false, disabledLook = true, onClick = { + showSendMessageToEnableCallsAlert() + }) + } else { // no known contact chat && directMessages are off + InfoViewActionButton(modifier = Modifier.fillMaxWidth(0.33f), painterResource(MR.images.ic_chat_bubble), generalGetString(MR.strings.info_view_message_button), disabled = false, disabledLook = true, onClick = { + showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_send_message_to_member_alert_title)) + }) + InfoViewActionButton(modifier = Modifier.fillMaxWidth(0.5f), painterResource(MR.images.ic_call), generalGetString(MR.strings.info_view_call_button), disabled = false, disabledLook = true, onClick = { + showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_call_member_alert_title)) + }) + InfoViewActionButton(modifier = Modifier.fillMaxWidth(1f), painterResource(MR.images.ic_videocam), generalGetString(MR.strings.info_view_video_button), disabled = false, disabledLook = true, onClick = { + showDirectMessagesProhibitedAlert(generalGetString(MR.strings.cant_call_member_alert_title)) + }) } + } + } + + SectionSpacer() + + if (member.memberActive) { + SectionView { if (connectionCode != null) { VerifyCodeButton(member.verified, verifyClicked) } @@ -354,13 +412,6 @@ fun GroupMemberInfoLayout( } else { InfoRow(stringResource(MR.strings.role_in_group), member.memberRole.text) } - val conn = member.activeConn - if (conn != null) { - val connLevelDesc = - if (conn.connLevel == 0) stringResource(MR.strings.conn_level_desc_direct) - else String.format(generalGetString(MR.strings.conn_level_desc_indirect), conn.connLevel) - InfoRow(stringResource(MR.strings.info_row_connection), connLevelDesc) - } } if (cStats != null) { SectionDividerSpaced() @@ -397,19 +448,53 @@ fun GroupMemberInfoLayout( SectionView(title = stringResource(MR.strings.section_title_for_console)) { InfoRow(stringResource(MR.strings.info_row_local_name), member.localDisplayName) InfoRow(stringResource(MR.strings.info_row_database_id), member.groupMemberId.toString()) + val conn = member.activeConn + if (conn != null) { + val connLevelDesc = + if (conn.connLevel == 0) stringResource(MR.strings.conn_level_desc_direct) + else String.format(generalGetString(MR.strings.conn_level_desc_indirect), conn.connLevel) + InfoRow(stringResource(MR.strings.info_row_connection), connLevelDesc) + } + SectionItemView({ + withBGApi { + val info = controller.apiGroupMemberQueueInfo(rhId, groupInfo.apiId, member.groupMemberId) + if (info != null) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.message_queue_info), + text = queueInfoText(info) + ) + } + } + }) { + Text(stringResource(MR.strings.info_row_debug_delivery)) + } } } SectionBottomSpacer() } } +private fun showSendMessageToEnableCallsAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.cant_call_member_alert_title), + text = generalGetString(MR.strings.cant_call_member_send_message_alert_text) + ) +} + +private fun showDirectMessagesProhibitedAlert(title: String) { + AlertManager.shared.showAlertMsg( + title = title, + text = generalGetString(MR.strings.direct_messages_are_prohibited_in_chat) + ) +} + @Composable fun GroupMemberInfoHeader(member: GroupMember) { Column( Modifier.padding(horizontal = 16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - ProfileImage(size = 192.dp, member.image, color = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight) + MemberProfileImage(size = 192.dp, member, color = if (isInDarkTheme()) GroupDark else SettingsSecondaryLight) val text = buildAnnotatedString { if (member.verified) { appendInlineContent(id = "shieldIcon") @@ -495,13 +580,17 @@ fun RemoveMemberButton(onClick: () -> Unit) { } @Composable -fun OpenChatButton(onClick: () -> Unit) { - SettingsActionItem( - painterResource(MR.images.ic_chat), - stringResource(MR.strings.button_send_direct_message), - click = onClick, - textColor = MaterialTheme.colors.primary, - iconColor = MaterialTheme.colors.primary, +fun OpenChatButton( + modifier: Modifier, + onClick: () -> Unit +) { + InfoViewActionButton( + modifier = modifier, + icon = painterResource(MR.images.ic_chat_bubble), + title = generalGetString(MR.strings.info_view_message_button), + disabled = false, + disabledLook = false, + onClick = onClick ) } @@ -539,6 +628,22 @@ private fun RoleSelectionRow( } } +@Composable +fun MemberProfileImage( + size: Dp, + mem: GroupMember, + color: Color = MaterialTheme.colors.secondaryVariant, + backgroundColor: Color? = null +) { + ProfileImage( + size = size, + image = mem.image, + color = color, + backgroundColor = backgroundColor, + blurred = mem.blocked + ) +} + private fun updateMemberRoleDialog( newRole: GroupMemberRole, member: GroupMember, @@ -604,7 +709,9 @@ fun updateMemberSettings(rhId: Long?, gInfo: GroupInfo, member: GroupMember, mem withBGApi { val success = ChatController.apiSetMemberSettings(rhId, gInfo.groupId, member.groupMemberId, memberSettings) if (success) { - ChatModel.upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings)) + withChats { + upsertGroupMember(rhId, gInfo, member.copy(memberSettings = memberSettings)) + } } } } @@ -635,7 +742,9 @@ fun unblockForAllAlert(rhId: Long?, gInfo: GroupInfo, mem: GroupMember) { fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, member: GroupMember, blocked: Boolean) { withBGApi { val updatedMember = ChatController.apiBlockMemberForAll(rhId, gInfo.groupId, member.groupMemberId, blocked) - chatModel.upsertGroupMember(rhId, gInfo, updatedMember) + withChats { + upsertGroupMember(rhId, gInfo, updatedMember) + } } } @@ -644,6 +753,7 @@ fun blockMemberForAll(rhId: Long?, gInfo: GroupInfo, member: GroupMember, blocke fun PreviewGroupMemberInfoLayout() { SimpleXTheme { GroupMemberInfoLayout( + rhId = null, groupInfo = GroupInfo.sampleData, member = GroupMember.sampleData, connStats = remember { mutableStateOf(null) }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt index 265d0cdeae..b7d66dd4f6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupPreferences.kt @@ -17,6 +17,7 @@ import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.PreferenceToggleWithIcon import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource @@ -43,8 +44,10 @@ fun GroupPreferencesView(m: ChatModel, rhId: Long?, chatId: String, close: () -> val gp = gInfo.groupProfile.copy(groupPreferences = preferences.toGroupPreferences()) val g = m.controller.apiUpdateGroup(rhId, gInfo.groupId, gp) if (g != null) { - m.updateGroup(rhId, g) - currentPreferences = preferences + withChats { + updateGroup(rhId, g) + currentPreferences = preferences + } } afterSave() } @@ -123,13 +126,12 @@ private fun GroupPreferencesLayout( applyPrefs(preferences.copy(files = RoleGroupPreference(enable = enable, role))) } - // TODO enable simplexLinks preference in 5.8 -// SectionDividerSpaced(true, maxBottomPadding = false) -// val allowSimplexLinks = remember(preferences) { mutableStateOf(preferences.simplexLinks.enable) } -// val simplexLinksRole = remember(preferences) { mutableStateOf(preferences.simplexLinks.role) } -// FeatureSection(GroupFeature.SimplexLinks, allowSimplexLinks, simplexLinksRole, groupInfo, preferences, onTTLUpdated) { enable, role -> -// applyPrefs(preferences.copy(simplexLinks = RoleGroupPreference(enable = enable, role))) -// } + SectionDividerSpaced(true, maxBottomPadding = false) + val allowSimplexLinks = remember(preferences) { mutableStateOf(preferences.simplexLinks.enable) } + val simplexLinksRole = remember(preferences) { mutableStateOf(preferences.simplexLinks.role) } + FeatureSection(GroupFeature.SimplexLinks, allowSimplexLinks, simplexLinksRole, groupInfo, preferences, onTTLUpdated) { enable, role -> + applyPrefs(preferences.copy(simplexLinks = RoleGroupPreference(enable = enable, role))) + } SectionDividerSpaced(true, maxBottomPadding = false) val enableHistory = remember(preferences) { mutableStateOf(preferences.history.enable) } @@ -189,8 +191,6 @@ private fun FeatureSection( generalGetString(MR.strings.feature_enabled_for), featureRoles, enableForRole, - // remove in v5.8 - enabled = remember { mutableStateOf(false) }, onSelected = { value -> onSelected(enableFeature.value, value) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt index 7975a298d1..6375ef1a20 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupProfileView.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.* @@ -38,7 +39,9 @@ fun GroupProfileView(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, cl withBGApi { val gInfo = chatModel.controller.apiUpdateGroup(rhId, groupInfo.groupId, p) if (gInfo != null) { - chatModel.updateGroup(rhId, gInfo) + withChats { + updateGroup(rhId, gInfo) + } close.invoke() } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt index 3bbeceb03c..b6312e4d82 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/WelcomeMessageView.kt @@ -28,6 +28,7 @@ import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.chat.item.MarkdownText import chat.simplex.common.views.helpers.* import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.model.GroupInfo import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.common.platform.chatJsonLength @@ -52,7 +53,9 @@ fun GroupWelcomeView(m: ChatModel, rhId: Long?, groupInfo: GroupInfo, close: () val res = m.controller.apiUpdateGroup(rhId, gInfo.groupId, groupProfileUpdated) if (res != null) { gInfo = res - m.updateGroup(rhId, res) + withChats { + updateGroup(rhId, res) + } welcomeText.value = welcome ?: "" } afterSave() @@ -130,13 +133,7 @@ private fun GroupWelcomeLayout( val clipboard = LocalClipboardManager.current CopyTextButton { clipboard.setText(AnnotatedString(wt.value)) } - Divider( - Modifier.padding( - start = DEFAULT_PADDING_HALF, - top = 8.dp, - end = DEFAULT_PADDING_HALF, - bottom = 8.dp) - ) + SectionDividerSpaced(maxBottomPadding = false) SaveButton( save = save, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CICallItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CICallItemView.kt index 5d3d5aa94a..74c6e38566 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CICallItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CICallItemView.kt @@ -47,7 +47,7 @@ fun CICallItemView( CICallStatus.Error -> {} } - CIMetaView(cItem, timedMessagesTTL, showStatus = false, showEdited = false) + CIMetaView(cItem, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt index f079a152ab..59643afdf4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIFileView.kt @@ -1,6 +1,6 @@ package chat.simplex.common.views.chat.item -import androidx.compose.foundation.clickable +import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CornerSize @@ -13,10 +13,9 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* @@ -30,14 +29,17 @@ fun CIFileView( file: CIFile?, edited: Boolean, showMenu: MutableState, + smallView: Boolean = false, receiveFile: (Long) -> Unit ) { val saveFileLauncher = rememberSaveFileLauncher(ciFile = file) - + val sizeMultiplier = 1f + val progressSizeMultiplier = if (smallView) 0.7f else 1f @Composable fun fileIcon( innerIcon: Painter? = null, - color: Color = if (isInDarkTheme()) FileDark else FileLight + color: Color = if (isInDarkTheme()) FileDark else FileLight, + topPadding: Dp = 12.sp.toDp() ) { Box( contentAlignment = Alignment.Center @@ -53,8 +55,9 @@ fun CIFileView( innerIcon, stringResource(MR.strings.icon_descr_file), Modifier - .size(32.dp) - .padding(top = 12.dp), + .padding(top = topPadding * sizeMultiplier) + .height(20.sp.toDp() * sizeMultiplier) + .width(32.sp.toDp() * sizeMultiplier), tint = Color.White ) } @@ -64,7 +67,7 @@ fun CIFileView( fun fileAction() { if (file != null) { when { - file.fileStatus is CIFileStatus.RcvInvitation -> { + file.fileStatus is CIFileStatus.RcvInvitation || file.fileStatus is CIFileStatus.RcvAborted -> { if (fileSizeValid(file)) { receiveFile(file.fileId) } else { @@ -88,6 +91,26 @@ fun CIFileView( ) FileProtocol.LOCAL -> {} } + file.fileStatus is CIFileStatus.RcvError -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.file_error), + file.fileStatus.rcvFileError.errorInfo + ) + file.fileStatus is CIFileStatus.RcvWarning -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.temporary_file_error), + file.fileStatus.rcvFileError.errorInfo + ) + file.fileStatus is CIFileStatus.SndError -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.file_error), + file.fileStatus.sndFileError.errorInfo + ) + file.fileStatus is CIFileStatus.SndWarning -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.temporary_file_error), + file.fileStatus.sndFileError.errorInfo + ) file.forwardingAllowed() -> { withLongRunningApi(slow = 600_000) { var filePath = getLoadedFilePath(file) @@ -109,76 +132,50 @@ fun CIFileView( } } - @Composable - fun progressIndicator() { - CircularProgressIndicator( - Modifier.size(32.dp), - color = if (isInDarkTheme()) FileDark else FileLight, - strokeWidth = 3.dp - ) - } - - @Composable - fun progressCircle(progress: Long, total: Long) { - val angle = 360f * (progress.toDouble() / total.toDouble()).toFloat() - val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() } - val strokeColor = if (isInDarkTheme()) FileDark else FileLight - Surface( - Modifier.drawRingModifier(angle, strokeColor, strokeWidth), - color = Color.Transparent, - shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)), - contentColor = LocalContentColor.current - ) { - Box(Modifier.size(32.dp)) - } - } - @Composable fun fileIndicator() { Box( Modifier - .size(42.dp) - .clip(RoundedCornerShape(4.dp)), + .size(42.sp.toDp() * sizeMultiplier) + .clip(RoundedCornerShape(4.sp.toDp() * sizeMultiplier)), contentAlignment = Alignment.Center ) { if (file != null) { when (file.fileStatus) { is CIFileStatus.SndStored -> when (file.fileProtocol) { - FileProtocol.XFTP -> progressIndicator() + FileProtocol.XFTP -> CIFileViewScope.progressIndicator(progressSizeMultiplier) FileProtocol.SMP -> fileIcon() FileProtocol.LOCAL -> fileIcon() } is CIFileStatus.SndTransfer -> when (file.fileProtocol) { - FileProtocol.XFTP -> progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal) - FileProtocol.SMP -> progressIndicator() + FileProtocol.XFTP -> CIFileViewScope.progressCircle(file.fileStatus.sndProgress, file.fileStatus.sndTotal, progressSizeMultiplier) + FileProtocol.SMP -> CIFileViewScope.progressIndicator(progressSizeMultiplier) FileProtocol.LOCAL -> {} } - is CIFileStatus.SndComplete -> { - if ((file.forwardingAllowed() || (chatModel.connectedToRemote() && CIFile.cachedRemoteFileRequests[file.fileSource] == true))) { - fileIcon() - } else { - fileIcon(innerIcon = painterResource(MR.images.ic_check_filled)) - } - } + is CIFileStatus.SndComplete -> fileIcon(innerIcon = if (!smallView) painterResource(MR.images.ic_check_filled) else null) is CIFileStatus.SndCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.SndError -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) + is CIFileStatus.SndWarning -> fileIcon(innerIcon = painterResource(MR.images.ic_warning_filled)) is CIFileStatus.RcvInvitation -> if (fileSizeValid(file)) - fileIcon(innerIcon = painterResource(MR.images.ic_arrow_downward), color = MaterialTheme.colors.primary) + fileIcon(innerIcon = painterResource(MR.images.ic_arrow_downward), color = MaterialTheme.colors.primary, topPadding = 10.sp.toDp()) else fileIcon(innerIcon = painterResource(MR.images.ic_priority_high), color = WarningOrange) is CIFileStatus.RcvAccepted -> fileIcon(innerIcon = painterResource(MR.images.ic_more_horiz)) is CIFileStatus.RcvTransfer -> if (file.fileProtocol == FileProtocol.XFTP && file.fileStatus.rcvProgress < file.fileStatus.rcvTotal) { - progressCircle(file.fileStatus.rcvProgress, file.fileStatus.rcvTotal) + CIFileViewScope.progressCircle(file.fileStatus.rcvProgress, file.fileStatus.rcvTotal, progressSizeMultiplier) } else { - progressIndicator() + CIFileViewScope.progressIndicator(progressSizeMultiplier) } + is CIFileStatus.RcvAborted -> + fileIcon(innerIcon = painterResource(MR.images.ic_sync_problem), color = MaterialTheme.colors.primary) is CIFileStatus.RcvComplete -> fileIcon() is CIFileStatus.RcvCancelled -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) is CIFileStatus.RcvError -> fileIcon(innerIcon = painterResource(MR.images.ic_close)) + is CIFileStatus.RcvWarning -> fileIcon(innerIcon = painterResource(MR.images.ic_warning_filled)) is CIFileStatus.Invalid -> fileIcon(innerIcon = painterResource(MR.images.ic_question_mark)) } } else { @@ -193,31 +190,33 @@ fun CIFileView( onClick = { fileAction() }, onLongClick = { showMenu.value = true } ) - .padding(top = 4.dp, bottom = 6.dp, start = 6.dp, end = 12.dp), + .padding(if (smallView) PaddingValues() else PaddingValues(top = 4.sp.toDp(), bottom = 6.sp.toDp(), start = 6.sp.toDp(), end = 12.sp.toDp())), //Modifier.clickable(enabled = file?.fileSource != null) { if (file?.fileSource != null && getLoadedFilePath(file) != null) openFile(file.fileSource) }.padding(top = 4.dp, bottom = 6.dp, start = 6.dp, end = 12.dp), verticalAlignment = Alignment.Bottom, - horizontalArrangement = Arrangement.spacedBy(2.dp) + horizontalArrangement = Arrangement.spacedBy(2.sp.toDp()) ) { fileIndicator() - val metaReserve = if (edited) - " " - else - " " - if (file != null) { - Column { - Text( - file.fileName, - maxLines = 1 - ) - Text( - formatBytes(file.fileSize) + metaReserve, - color = MaterialTheme.colors.secondary, - fontSize = 14.sp, - maxLines = 1 - ) + if (!smallView) { + val metaReserve = if (edited) + " " + else + " " + if (file != null) { + Column { + Text( + file.fileName, + maxLines = 1 + ) + Text( + formatBytes(file.fileSize) + metaReserve, + color = MaterialTheme.colors.secondary, + fontSize = 14.sp, + maxLines = 1 + ) + } + } else { + Text(metaReserve) } - } else { - Text(metaReserve) } } } @@ -248,6 +247,32 @@ fun rememberSaveFileLauncher(ciFile: CIFile?): FileChooserLauncher = } } +object CIFileViewScope { + @Composable + fun progressIndicator(sizeMultiplier: Float = 1f) { + CircularProgressIndicator( + Modifier.size(32.sp.toDp() * sizeMultiplier), + color = if (isInDarkTheme()) FileDark else FileLight, + strokeWidth = 3.sp.toDp() * sizeMultiplier + ) + } + + @Composable + fun progressCircle(progress: Long, total: Long, sizeMultiplier: Float = 1f) { + val angle = 360f * (progress.toDouble() / total.toDouble()).toFloat() + val strokeWidth = with(LocalDensity.current) { 3.sp.toPx() } + val strokeColor = if (isInDarkTheme()) FileDark else FileLight + Surface( + Modifier.drawRingModifier(angle, strokeColor, strokeWidth), + color = Color.Transparent, + shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)), + contentColor = LocalContentColor.current + ) { + Box(Modifier.size(32.sp.toDp() * sizeMultiplier)) + } + } +} + /* class ChatItemProvider: PreviewParameterProvider { private val sentFile = ChatItem( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt index e3008f36b3..577327c159 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt @@ -83,8 +83,8 @@ fun CIGroupInvitationView( } } - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Surface( modifier = if (action && !inProgress.value) Modifier.clickable(onClick = { inProgress.value = true @@ -110,6 +110,7 @@ fun CIGroupInvitationView( .padding(bottom = 4.dp), ) { groupInfoView() + val secondaryColor = MaterialTheme.colors.secondary Column(Modifier.padding(top = 2.dp, start = 5.dp)) { Divider(Modifier.fillMaxWidth().padding(bottom = 4.dp)) if (action) { @@ -117,7 +118,7 @@ fun CIGroupInvitationView( Text( buildAnnotatedString { append(generalGetString(if (chatIncognito) MR.strings.group_invitation_tap_to_join_incognito else MR.strings.group_invitation_tap_to_join)) - withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false)) } + withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor)) } }, color = if (inProgress.value) MaterialTheme.colors.secondary @@ -128,7 +129,7 @@ fun CIGroupInvitationView( Text( buildAnnotatedString { append(groupInvitationStr()) - withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false)) } + withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, timedMessagesTTL, encrypted = null, showStatus = false, showEdited = false, secondaryColor = secondaryColor)) } } ) } @@ -144,7 +145,7 @@ fun CIGroupInvitationView( } } - CIMetaView(ci, timedMessagesTTL, showStatus = false, showEdited = false) + CIMetaView(ci, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt index 65fb38575d..b7fe9ea4cf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.kt @@ -1,6 +1,8 @@ package chat.simplex.common.views.chat.item import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.HoverInteraction +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* @@ -17,25 +19,24 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.DEFAULT_MAX_IMAGE_WIDTH +import chat.simplex.common.views.chat.chatViewScrollState import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.runBlocking -import java.io.File -import java.net.URI @Composable fun CIImageView( image: String, file: CIFile?, - metaColor: Color, imageProvider: () -> ImageGalleryProvider, showMenu: MutableState, + smallView: Boolean, receiveFile: (Long) -> Unit ) { + val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) } @Composable fun progressIndicator() { CircularProgressIndicator( @@ -51,7 +52,7 @@ fun CIImageView( icon, stringResource(stringId), Modifier.fillMaxSize(), - tint = metaColor + tint = Color.White ) } @@ -60,7 +61,7 @@ fun CIImageView( if (file != null) { Box( Modifier - .padding(8.dp) + .padding(if (smallView) 0.dp else 8.dp) .size(20.dp), contentAlignment = Alignment.Center ) { @@ -75,13 +76,16 @@ fun CIImageView( is CIFileStatus.SndComplete -> fileIcon(painterResource(MR.images.ic_check_filled), MR.strings.icon_descr_image_snd_complete) is CIFileStatus.SndCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.SndError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) + is CIFileStatus.SndWarning -> fileIcon(painterResource(MR.images.ic_warning_filled), MR.strings.icon_descr_file) is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_asked_to_receive) is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_image) is CIFileStatus.RcvTransfer -> progressIndicator() + is CIFileStatus.RcvComplete -> {} + is CIFileStatus.RcvAborted -> fileIcon(painterResource(MR.images.ic_sync_problem), MR.strings.icon_descr_file) is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) + is CIFileStatus.RcvWarning -> fileIcon(painterResource(MR.images.ic_warning_filled), MR.strings.icon_descr_file) is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file) - else -> {} } } } @@ -106,8 +110,9 @@ fun CIImageView( onLongClick = { showMenu.value = true }, onClick = onClick ) - .onRightClick { showMenu.value = true }, - contentScale = ContentScale.FillWidth, + .onRightClick { showMenu.value = true } + .privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }), + contentScale = if (smallView) ContentScale.Crop else ContentScale.FillWidth, ) } @@ -129,8 +134,9 @@ fun CIImageView( onLongClick = { showMenu.value = true }, onClick = onClick ) - .onRightClick { showMenu.value = true }, - contentScale = ContentScale.FillWidth, + .onRightClick { showMenu.value = true } + .privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }), + contentScale = if (smallView) ContentScale.Crop else ContentScale.FillWidth, ) } else { Box(Modifier @@ -139,7 +145,8 @@ fun CIImageView( onLongClick = { showMenu.value = true }, onClick = {} ) - .onRightClick { showMenu.value = true }, + .onRightClick { showMenu.value = true } + .privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = { showMenu.value = true }), contentAlignment = Alignment.Center ) { imageView(base64ToBitmap(image), onClick = { @@ -175,7 +182,8 @@ fun CIImageView( } Box( - Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID), + Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID) + .desktopModifyBlurredState(!smallView, blurred, showMenu), contentAlignment = Alignment.TopEnd ) { val res: MutableState?> = remember { @@ -193,7 +201,7 @@ fun CIImageView( } } else { KeyChangeEffect(file) { - if (res.value == null) { + if (res.value == null || res.value!!.third != getLoadedFilePath(file)) { res.value = imageAndFilePath(file) } } @@ -201,12 +209,12 @@ fun CIImageView( val loaded = res.value if (loaded != null && file != null) { val (imageBitmap, data, _) = loaded - SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, @Composable { painter, onClick -> ImageView(painter, image, file.fileSource, onClick) }) + SimpleAndAnimatedImageView(data, imageBitmap, file, imageProvider, smallView, @Composable { painter, onClick -> ImageView(painter, image, file.fileSource, onClick) }) } else { imageView(base64ToBitmap(image), onClick = { if (file != null) { - when (file.fileStatus) { - CIFileStatus.RcvInvitation -> + when { + file.fileStatus is CIFileStatus.RcvInvitation || file.fileStatus is CIFileStatus.RcvAborted -> if (fileSizeValid()) { receiveFile(file.fileId) } else { @@ -215,7 +223,7 @@ fun CIImageView( String.format(generalGetString(MR.strings.contact_sent_large_file), formatBytes(getMaxFileSize(file.fileProtocol))) ) } - CIFileStatus.RcvAccepted -> + file.fileStatus is CIFileStatus.RcvAccepted -> when (file.fileProtocol) { FileProtocol.XFTP -> AlertManager.shared.showAlertMsg( @@ -229,23 +237,54 @@ fun CIImageView( ) FileProtocol.LOCAL -> {} } - CIFileStatus.RcvTransfer(rcvProgress = 7, rcvTotal = 10) -> {} // ? - CIFileStatus.RcvComplete -> {} // ? - CIFileStatus.RcvCancelled -> {} // TODO + file.fileStatus is CIFileStatus.RcvError -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.file_error), + file.fileStatus.rcvFileError.errorInfo + ) + file.fileStatus is CIFileStatus.RcvWarning -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.temporary_file_error), + file.fileStatus.rcvFileError.errorInfo + ) + file.fileStatus is CIFileStatus.SndError -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.file_error), + file.fileStatus.sndFileError.errorInfo + ) + file.fileStatus is CIFileStatus.SndWarning -> + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.temporary_file_error), + file.fileStatus.sndFileError.errorInfo + ) + file.fileStatus is CIFileStatus.RcvTransfer -> {} // ? + file.fileStatus is CIFileStatus.RcvComplete -> {} // ? + file.fileStatus is CIFileStatus.RcvCancelled -> {} // TODO else -> {} } } }) } - loadingIndicator() + // Do not show download icon when the view is blurred + if (!smallView && (!showDownloadButton(file?.fileStatus) || !blurred.value)) { + loadingIndicator() + } else if (smallView && file?.showStatusIconInSmallView == true) { + Box(Modifier.matchParentSize(), contentAlignment = Alignment.Center) { + loadingIndicator() + } + } } } +private fun showDownloadButton(status: CIFileStatus?): Boolean = + status is CIFileStatus.RcvInvitation || status is CIFileStatus.RcvAborted + @Composable expect fun SimpleAndAnimatedImageView( data: ByteArray, imageBitmap: ImageBitmap, file: CIFile?, imageProvider: () -> ImageGalleryProvider, + smallView: Boolean, ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt index b40d8989e1..def3b14ebc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt @@ -12,7 +12,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import chat.simplex.common.ui.theme.CurrentColors import chat.simplex.common.model.* import chat.simplex.common.ui.theme.isInDarkTheme import chat.simplex.res.MR @@ -35,7 +34,8 @@ fun CIMetaView( blue = minOf(metaColor.red * 1.33F, 1F)) }, showStatus: Boolean = true, - showEdited: Boolean = true + showEdited: Boolean = true, + showViaProxy: Boolean ) { Row(Modifier.padding(start = 3.dp), verticalAlignment = Alignment.CenterVertically) { if (chatItem.isDeletedContent) { @@ -53,7 +53,8 @@ fun CIMetaView( metaColor, paleMetaColor, showStatus = showStatus, - showEdited = showEdited + showEdited = showEdited, + showViaProxy = showViaProxy ) } } @@ -68,7 +69,8 @@ private fun CIMetaText( color: Color, paleColor: Color, showStatus: Boolean = true, - showEdited: Boolean = true + showEdited: Boolean = true, + showViaProxy: Boolean ) { if (showEdited && meta.itemEdited) { StatusIconText(painterResource(MR.images.ic_edit), color) @@ -82,6 +84,9 @@ private fun CIMetaText( } Spacer(Modifier.width(4.dp)) } + if (showViaProxy && meta.sentViaProxy == true) { + Icon(painterResource(MR.images.ic_arrow_forward), null, Modifier.height(17.dp), tint = MaterialTheme.colors.secondary) + } if (showStatus) { val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color, paleColor) if (statusIcon != null) { @@ -105,7 +110,15 @@ private fun CIMetaText( } // the conditions in this function should match CIMetaText -fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?, encrypted: Boolean?, showStatus: Boolean = true, showEdited: Boolean = true): String { +fun reserveSpaceForMeta( + meta: CIMeta, + chatTTL: Int?, + encrypted: Boolean?, + secondaryColor: Color, + showStatus: Boolean = true, + showEdited: Boolean = true, + showViaProxy: Boolean = false +): String { val iconSpace = " " var res = "" if (showEdited && meta.itemEdited) res += iconSpace @@ -116,7 +129,10 @@ fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?, encrypted: Boolean?, showSt res += shortTimeText(ttl) } } - if (showStatus && (meta.statusIcon(CurrentColors.value.colors.secondary) != null || !meta.disappearing)) { + if (showViaProxy && meta.sentViaProxy == true) { + res += iconSpace + } + if (showStatus && (meta.statusIcon(secondaryColor) != null || !meta.disappearing)) { res += iconSpace } if (encrypted != null) { @@ -137,7 +153,8 @@ fun PreviewCIMetaView() { chatItem = ChatItem.getSampleData( 1, CIDirection.DirectSnd(), Clock.System.now(), "hello" ), - null + null, + showViaProxy = false ) } @@ -149,7 +166,8 @@ fun PreviewCIMetaViewUnread() { 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.RcvNew() ), - null + null, + showViaProxy = false ) } @@ -159,9 +177,10 @@ fun PreviewCIMetaViewSendFailed() { CIMetaView( chatItem = ChatItem.getSampleData( 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", - status = CIStatus.SndError("CMD SYNTAX") + status = CIStatus.CISSndError(SndError.Other("CMD SYNTAX")) ), - null + null, + showViaProxy = false ) } @@ -172,7 +191,8 @@ fun PreviewCIMetaViewSendNoAuth() { chatItem = ChatItem.getSampleData( 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndErrorAuth() ), - null + null, + showViaProxy = false ) } @@ -183,7 +203,8 @@ fun PreviewCIMetaViewSendSent() { chatItem = ChatItem.getSampleData( 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndSent(SndCIStatusProgress.Complete) ), - null + null, + showViaProxy = false ) } @@ -195,7 +216,8 @@ fun PreviewCIMetaViewEdited() { 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", itemEdited = true ), - null + null, + showViaProxy = false ) } @@ -208,7 +230,8 @@ fun PreviewCIMetaViewEditedUnread() { itemEdited = true, status= CIStatus.RcvNew() ), - null + null, + showViaProxy = false ) } @@ -221,7 +244,8 @@ fun PreviewCIMetaViewEditedSent() { itemEdited = true, status= CIStatus.SndSent(SndCIStatusProgress.Complete) ), - null + null, + showViaProxy = false ) } @@ -230,6 +254,7 @@ fun PreviewCIMetaViewEditedSent() { fun PreviewCIMetaViewDeletedContent() { CIMetaView( chatItem = ChatItem.getDeletedContentSampleData(), - null + null, + showViaProxy = false ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt index 318a8a6a05..dd0e9cf1a2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.ui.theme.CurrentColors +import chat.simplex.common.ui.theme.appColors import chat.simplex.common.views.helpers.AlertManager import chat.simplex.common.views.helpers.generalGetString import chat.simplex.res.MR @@ -137,7 +138,7 @@ fun DecryptionErrorItemFixButton( onClick: () -> Unit, syncSupported: Boolean ) { - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Surface( Modifier.clickable(onClick = onClick), shape = RoundedCornerShape(18.dp), @@ -164,17 +165,18 @@ fun DecryptionErrorItemFixButton( tint = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary ) Spacer(Modifier.padding(2.dp)) + val secondaryColor = MaterialTheme.colors.secondary Text( buildAnnotatedString { append(generalGetString(MR.strings.fix_connection)) - withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null)) } + withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor)) } withStyle(reserveTimestampStyle) { append(" ") } // for icon }, color = if (syncSupported) MaterialTheme.colors.primary else MaterialTheme.colors.secondary ) } } - CIMetaView(ci, timedMessagesTTL = null) + CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false) } } } @@ -184,7 +186,7 @@ fun DecryptionErrorItem( ci: ChatItem, onClick: () -> Unit ) { - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Surface( Modifier.clickable(onClick = onClick), shape = RoundedCornerShape(18.dp), @@ -195,14 +197,15 @@ fun DecryptionErrorItem( Modifier.padding(vertical = 6.dp, horizontal = 12.dp), contentAlignment = Alignment.BottomEnd, ) { + val secondaryColor = MaterialTheme.colors.secondary Text( buildAnnotatedString { withStyle(SpanStyle(fontStyle = FontStyle.Italic, color = Color.Red)) { append(ci.content.text) } - withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null)) } + withStyle(reserveTimestampStyle) { append(reserveSpaceForMeta(ci.meta, null, encrypted = null, secondaryColor = secondaryColor)) } }, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp) ) - CIMetaView(ci, timedMessagesTTL = null) + CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.kt similarity index 61% rename from apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt rename to apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.kt index a79e509d02..ca93349092 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVIdeoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVideoView.kt @@ -19,7 +19,9 @@ import chat.simplex.res.MR import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* +import chat.simplex.common.views.chat.chatViewScrollState import dev.icerock.moko.resources.StringResource import java.io.File import java.net.URI @@ -31,14 +33,18 @@ fun CIVideoView( file: CIFile?, imageProvider: () -> ImageGalleryProvider, showMenu: MutableState, + smallView: Boolean = false, receiveFile: (Long) -> Unit ) { + val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) } Box( - Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID), + Modifier.layoutId(CHAT_IMAGE_LAYOUT_ID) + .desktopModifyBlurredState(!smallView, blurred, showMenu), contentAlignment = Alignment.TopEnd ) { val preview = remember(image) { base64ToBitmap(image) } val filePath = remember(file, CIFile.cachedRemoteFileRequests.toList()) { mutableStateOf(getLoadedFilePath(file)) } + val sizeMultiplier = if (smallView) 0.38f else 1f if (chatModel.connectedToRemote()) { LaunchedEffect(file) { withLongRunningApi(slow = 600_000) { @@ -63,17 +69,21 @@ fun CIVideoView( val autoPlay = remember { mutableStateOf(false) } val uriDecrypted = remember(filePath) { mutableStateOf(if (file.fileSource?.cryptoArgs == null) uri else file.fileSource.decryptedGet()) } val decrypted = uriDecrypted.value - if (decrypted != null) { - VideoView(decrypted, file, preview, duration * 1000L, autoPlay, showMenu, openFullscreen = openFullscreen) + if (decrypted != null && smallView) { + SmallVideoView(decrypted, file, preview, duration * 1000L, autoPlay, sizeMultiplier, openFullscreen = openFullscreen) + } else if (decrypted != null) { + VideoView(decrypted, file, preview, duration * 1000L, autoPlay, showMenu, blurred, openFullscreen = openFullscreen) + } else if (smallView) { + SmallVideoViewEncrypted(uriDecrypted, file, preview, autoPlay, showMenu, sizeMultiplier, openFullscreen = openFullscreen) } else { - VideoViewEncrypted(uriDecrypted, file, preview, duration * 1000L, autoPlay, showMenu, openFullscreen = openFullscreen) + VideoViewEncrypted(uriDecrypted, file, preview, duration * 1000L, autoPlay, showMenu, blurred, openFullscreen = openFullscreen) } } else { Box { - VideoPreviewImageView(preview, onClick = { + VideoPreviewImageView(preview, blurred = blurred, onClick = { if (file != null) { when (file.fileStatus) { - CIFileStatus.RcvInvitation -> + CIFileStatus.RcvInvitation, CIFileStatus.RcvAborted -> receiveFileIfValidSize(file, receiveFile) CIFileStatus.RcvAccepted -> when (file.fileProtocol) { @@ -96,18 +106,26 @@ fun CIVideoView( } } }, + smallView = smallView, onLongClick = { showMenu.value = true }) - if (file != null) { + if (file != null && !smallView) { DurationProgress(file, remember { mutableStateOf(false) }, remember { mutableStateOf(duration * 1000L) }, remember { mutableStateOf(0L) }/*, soundEnabled*/) } - if (file?.fileStatus is CIFileStatus.RcvInvitation) { - PlayButton(error = false, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) } + if (showDownloadButton(file?.fileStatus) && !blurred.value && file != null) { + PlayButton(error = false, sizeMultiplier, { showMenu.value = true }) { receiveFileIfValidSize(file, receiveFile) } } } } - loadingIndicator(file) + // Do not show download icon when the view is blurred + if (!smallView && (!showDownloadButton(file?.fileStatus) || !blurred.value)) { + fileStatusIcon(file, false) + } else if (smallView && file?.showStatusIconInSmallView == true) { + Box(Modifier.align(Alignment.Center)) { + fileStatusIcon(file, true) + } + } } } @@ -119,16 +137,17 @@ private fun VideoViewEncrypted( defaultDuration: Long, autoPlay: MutableState, showMenu: MutableState, + blurred: MutableState, openFullscreen: () -> Unit, ) { var decryptionInProgress by rememberSaveable(file.fileName) { mutableStateOf(false) } val onLongClick = { showMenu.value = true } Box { - VideoPreviewImageView(defaultPreview, if (decryptionInProgress) {{}} else openFullscreen, onLongClick) + VideoPreviewImageView(defaultPreview, smallView = false, blurred = blurred, if (decryptionInProgress) {{}} else openFullscreen, onLongClick) if (decryptionInProgress) { - VideoDecryptionProgress(onLongClick = onLongClick) - } else { - PlayButton(false, onLongClick = onLongClick) { + VideoDecryptionProgress(1f, onLongClick = onLongClick) + } else if (!blurred.value) { + PlayButton(false, 1f, onLongClick = onLongClick) { decryptionInProgress = true withBGApi { try { @@ -145,7 +164,82 @@ private fun VideoViewEncrypted( } @Composable -private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defaultDuration: Long, autoPlay: MutableState, showMenu: MutableState, openFullscreen: () -> Unit) { +private fun SmallVideoViewEncrypted( + uriUnencrypted: MutableState, + file: CIFile, + defaultPreview: ImageBitmap, + autoPlay: MutableState, + showMenu: MutableState, + sizeMultiplier: Float, + openFullscreen: () -> Unit, +) { + var decryptionInProgress by rememberSaveable(file.fileName) { mutableStateOf(false) } + val onLongClick = { showMenu.value = true } + Box { + VideoPreviewImageView(defaultPreview, smallView = true, blurred = remember { mutableStateOf(false) }, onClick = if (decryptionInProgress) {{}} else openFullscreen, onLongClick = onLongClick) + if (decryptionInProgress) { + VideoDecryptionProgress(sizeMultiplier, onLongClick = onLongClick) + } else if (!file.showStatusIconInSmallView) { + PlayButton(false, sizeMultiplier, onLongClick = onLongClick) { + decryptionInProgress = true + withBGApi { + try { + uriUnencrypted.value = file.fileSource?.decryptedGetOrCreate() + autoPlay.value = uriUnencrypted.value != null + } finally { + decryptionInProgress = false + } + } + } + } + } +} + +@Composable +private fun SmallVideoView( + uri: URI, + file: CIFile, + defaultPreview: ImageBitmap, + defaultDuration: Long, + autoPlay: MutableState, + sizeMultiplier: Float, + openFullscreen: () -> Unit +) { + val player = remember(uri) { VideoPlayerHolder.getOrCreate(uri, true, defaultPreview, defaultDuration, true) } + val preview by remember { player.preview } + // val soundEnabled by rememberSaveable(uri.path) { player.soundEnabled } + val brokenVideo by rememberSaveable(uri.path) { player.brokenVideo } + Box { + val windowWidth = LocalWindowWidth() + val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH } + PlayerView( + player, + width, + onClick = openFullscreen, + onLongClick = {}, + {} + ) + VideoPreviewImageView(preview, smallView = true, blurred = remember { mutableStateOf(false) }, onClick = openFullscreen, onLongClick = {}) + if (!file.showStatusIconInSmallView) { + PlayButton(brokenVideo, sizeMultiplier, onLongClick = {}, onClick = openFullscreen) + } + } + LaunchedEffect(uri) { + if (autoPlay.value) openFullscreen() + } +} + +@Composable +private fun VideoView( + uri: URI, + file: CIFile, + defaultPreview: ImageBitmap, + defaultDuration: Long, + autoPlay: MutableState, + showMenu: MutableState, + blurred: MutableState, + openFullscreen: () -> Unit +) { val player = remember(uri) { VideoPlayerHolder.getOrCreate(uri, false, defaultPreview, defaultDuration, true) } val videoPlaying = remember(uri.path) { player.videoPlaying } val progress = remember(uri.path) { player.progress } @@ -186,9 +280,9 @@ private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defau stop ) if (showPreview.value) { - VideoPreviewImageView(preview, openFullscreen, onLongClick) - if (!autoPlay.value) { - PlayButton(brokenVideo, onLongClick = onLongClick, play) + VideoPreviewImageView(preview, smallView = false, blurred = blurred, openFullscreen, onLongClick) + if (!autoPlay.value && !blurred.value) { + PlayButton(brokenVideo, onLongClick = onLongClick, onClick = play) } } DurationProgress(file, videoPlaying, duration, progress/*, soundEnabled*/) @@ -199,16 +293,16 @@ private fun VideoView(uri: URI, file: CIFile, defaultPreview: ImageBitmap, defau expect fun PlayerView(player: VideoPlayer, width: Dp, onClick: () -> Unit, onLongClick: () -> Unit, stop: () -> Unit) @Composable -private fun BoxScope.PlayButton(error: Boolean = false, onLongClick: () -> Unit, onClick: () -> Unit) { +private fun BoxScope.PlayButton(error: Boolean = false, sizeMultiplier: Float = 1f, onLongClick: () -> Unit, onClick: () -> Unit) { Surface( - Modifier.align(Alignment.Center), + Modifier.align(if (sizeMultiplier != 1f) Alignment.TopStart else Alignment.Center), color = Color.Black.copy(alpha = 0.25f), shape = RoundedCornerShape(percent = 50), contentColor = LocalContentColor.current ) { Box( Modifier - .defaultMinSize(minWidth = 40.dp, minHeight = 40.dp) + .defaultMinSize(minWidth = if (sizeMultiplier != 1f) 40.sp.toDp() * sizeMultiplier else 40.sp.toDp(), minHeight = if (sizeMultiplier != 1f) 40.sp.toDp() * sizeMultiplier else 40.sp.toDp()) .combinedClickable(onClick = onClick, onLongClick = onLongClick) .onRightClick { onLongClick.invoke() }, contentAlignment = Alignment.Center @@ -216,6 +310,7 @@ private fun BoxScope.PlayButton(error: Boolean = false, onLongClick: () -> Unit, Icon( painterResource(MR.images.ic_play_arrow_filled), contentDescription = null, + Modifier.size(if (sizeMultiplier != 1f) 24.sp.toDp() * sizeMultiplier * 1.6f else 24.sp.toDp()), tint = if (error) WarningOrange else Color.White ) } @@ -223,25 +318,25 @@ private fun BoxScope.PlayButton(error: Boolean = false, onLongClick: () -> Unit, } @Composable -fun BoxScope.VideoDecryptionProgress(onLongClick: () -> Unit) { +fun BoxScope.VideoDecryptionProgress(sizeMultiplier: Float = 1f, onLongClick: () -> Unit) { Surface( - Modifier.align(Alignment.Center), + Modifier.align(if (sizeMultiplier != 1f) Alignment.TopStart else Alignment.Center), color = Color.Black.copy(alpha = 0.25f), shape = RoundedCornerShape(percent = 50), contentColor = LocalContentColor.current ) { Box( Modifier - .defaultMinSize(minWidth = 40.dp, minHeight = 40.dp) + .defaultMinSize(minWidth = if (sizeMultiplier != 1f) 40.sp.toDp() * sizeMultiplier else 40.sp.toDp(), minHeight = if (sizeMultiplier != 1f) 40.sp.toDp() * sizeMultiplier else 40.sp.toDp()) .combinedClickable(onClick = {}, onLongClick = onLongClick) .onRightClick { onLongClick.invoke() }, contentAlignment = Alignment.Center ) { CircularProgressIndicator( Modifier - .size(30.dp), + .size(if (sizeMultiplier != 1f) 30.sp.toDp() * sizeMultiplier else 30.sp.toDp()), color = Color.White, - strokeWidth = 2.5.dp + strokeWidth = 2.5.sp.toDp() * sizeMultiplier ) } } @@ -293,7 +388,13 @@ private fun DurationProgress(file: CIFile, playing: MutableState, durat } @Composable -fun VideoPreviewImageView(preview: ImageBitmap, onClick: () -> Unit, onLongClick: () -> Unit) { +fun VideoPreviewImageView( + preview: ImageBitmap, + smallView: Boolean, + blurred: MutableState, + onClick: () -> Unit, + onLongClick: () -> Unit +) { val windowWidth = LocalWindowWidth() val width = remember(preview) { if (preview.width * 0.97 <= preview.height) videoViewFullWidth(windowWidth) * 0.75f else DEFAULT_MAX_IMAGE_WIDTH } Image( @@ -305,8 +406,9 @@ fun VideoPreviewImageView(preview: ImageBitmap, onClick: () -> Unit, onLongClick onLongClick = onLongClick, onClick = onClick ) - .onRightClick(onLongClick), - contentScale = ContentScale.FillWidth, + .onRightClick(onLongClick) + .privacyBlur(!smallView, blurred, scrollState = chatViewScrollState.collectAsState(), onLongClick = onLongClick), + contentScale = if (smallView) ContentScale.Crop else ContentScale.FillWidth, ) } @@ -339,11 +441,13 @@ private fun progressIndicator() { } @Composable -private fun fileIcon(icon: Painter, stringId: StringResource) { +private fun fileIcon(icon: Painter, stringId: StringResource, onClick: (() -> Unit)? = null) { + var modifier = Modifier.fillMaxSize() + modifier = if (onClick != null) { modifier.clickable { onClick() } } else { modifier } Icon( icon, stringResource(stringId), - Modifier.fillMaxSize(), + modifier, tint = Color.White ) } @@ -364,11 +468,11 @@ private fun progressCircle(progress: Long, total: Long) { } @Composable -private fun loadingIndicator(file: CIFile?) { +private fun fileStatusIcon(file: CIFile?, smallView: Boolean) { if (file != null) { Box( Modifier - .padding(8.dp) + .padding(if (smallView) 0.dp else 8.dp) .size(20.dp), contentAlignment = Alignment.Center ) { @@ -387,7 +491,28 @@ private fun loadingIndicator(file: CIFile?) { } is CIFileStatus.SndComplete -> fileIcon(painterResource(MR.images.ic_check_filled), MR.strings.icon_descr_video_snd_complete) is CIFileStatus.SndCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) - is CIFileStatus.SndError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) + is CIFileStatus.SndError -> + fileIcon( + painterResource(MR.images.ic_close), + MR.strings.icon_descr_file, + onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.file_error), + file.fileStatus.sndFileError.errorInfo + ) + } + ) + is CIFileStatus.SndWarning -> + fileIcon( + painterResource(MR.images.ic_warning_filled), + MR.strings.icon_descr_file, + onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.temporary_file_error), + file.fileStatus.sndFileError.errorInfo + ) + } + ) is CIFileStatus.RcvInvitation -> fileIcon(painterResource(MR.images.ic_arrow_downward), MR.strings.icon_descr_video_asked_to_receive) is CIFileStatus.RcvAccepted -> fileIcon(painterResource(MR.images.ic_more_horiz), MR.strings.icon_descr_waiting_for_video) is CIFileStatus.RcvTransfer -> @@ -396,15 +521,40 @@ private fun loadingIndicator(file: CIFile?) { } else { progressIndicator() } + is CIFileStatus.RcvAborted -> fileIcon(painterResource(MR.images.ic_sync_problem), MR.strings.icon_descr_file) + is CIFileStatus.RcvComplete -> {} is CIFileStatus.RcvCancelled -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) - is CIFileStatus.RcvError -> fileIcon(painterResource(MR.images.ic_close), MR.strings.icon_descr_file) + is CIFileStatus.RcvError -> + fileIcon( + painterResource(MR.images.ic_close), + MR.strings.icon_descr_file, + onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.file_error), + file.fileStatus.rcvFileError.errorInfo + ) + } + ) + is CIFileStatus.RcvWarning -> + fileIcon( + painterResource(MR.images.ic_warning_filled), + MR.strings.icon_descr_file, + onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.temporary_file_error), + file.fileStatus.rcvFileError.errorInfo + ) + } + ) is CIFileStatus.Invalid -> fileIcon(painterResource(MR.images.ic_question_mark), MR.strings.icon_descr_file) - else -> {} } } } } +private fun showDownloadButton(status: CIFileStatus?): Boolean = + status is CIFileStatus.RcvInvitation || status is CIFileStatus.RcvAborted + private fun fileSizeValid(file: CIFile?): Boolean { if (file != null) { return file.fileSize <= getMaxFileSize(file.fileProtocol) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt index e59c5f1370..5ae46ef4e7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt @@ -22,7 +22,9 @@ import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* import chat.simplex.common.platform.* import chat.simplex.res.MR +import dev.icerock.moko.resources.ImageResource import kotlinx.coroutines.flow.* +import kotlin.math.* // TODO refactor https://github.com/simplex-chat/simplex-chat/pull/1451#discussion_r1033429901 @@ -35,11 +37,19 @@ fun CIVoiceView( hasText: Boolean, ci: ChatItem, timedMessagesTTL: Int?, + showViaProxy: Boolean, + smallView: Boolean = false, longClick: () -> Unit, receiveFile: (Long) -> Unit, ) { + val sizeMultiplier = if (smallView) voiceMessageSizeBasedOnSquareSize(36f) / 56f else 1f + val padding = when { + smallView -> PaddingValues() + hasText -> PaddingValues(top = 14.sp.toDp() * sizeMultiplier, bottom = 14.sp.toDp() * sizeMultiplier, start = 6.sp.toDp() * sizeMultiplier, end = 6.sp.toDp() * sizeMultiplier) + else -> PaddingValues(top = 4.sp.toDp() * sizeMultiplier, bottom = 6.sp.toDp() * sizeMultiplier, start = 0.dp, end = 0.dp) + } Row( - Modifier.padding(top = if (hasText) 14.dp else 4.dp, bottom = if (hasText) 14.dp else 6.dp, start = if (hasText) 6.dp else 0.dp, end = if (hasText) 6.dp else 0.dp), + Modifier.padding(padding), verticalAlignment = Alignment.CenterVertically ) { if (file != null) { @@ -52,7 +62,7 @@ fun CIVoiceView( val play: () -> Unit = { val playIfExists = { if (fileSource.value != null) { - AudioPlayer.play(fileSource.value!!, audioPlaying, progress, duration, true) + AudioPlayer.play(fileSource.value!!, audioPlaying, progress, duration, resetOnEnd = true, smallView = smallView) brokenAudio = !audioPlaying.value } } @@ -67,7 +77,7 @@ fun CIVoiceView( val pause = { AudioPlayer.pause(audioPlaying, progress) } - val text = remember { + val text = remember(ci.file?.fileId, ci.file?.fileStatus) { derivedStateOf { val time = when { audioPlaying.value || progress.value != 0 -> progress.value @@ -76,11 +86,18 @@ fun CIVoiceView( durationText(time / 1000) } } - VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, play, pause, longClick, receiveFile) { + VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, showViaProxy, sizeMultiplier, play, pause, longClick, receiveFile) { AudioPlayer.seekTo(it, progress, fileSource.value?.filePath) } + if (smallView) { + KeyChangeEffect(chatModel.chatId.value, chatModel.currentUser.value?.userId, chatModel.currentRemoteHost.value) { + AudioPlayer.stop() + } + } + } else if (smallView) { + VoiceMsgIndicator(null, false, sent, hasText, null, null, false, sizeMultiplier, {}, {}, longClick, receiveFile) } else { - VoiceMsgIndicator(null, false, sent, hasText, null, null, false, {}, {}, longClick, receiveFile) + VoiceMsgIndicator(null, false, sent, hasText, null, null, false, 1f, {}, {}, longClick, receiveFile) val metaReserve = if (edited) " " else @@ -102,6 +119,8 @@ private fun VoiceLayout( sent: Boolean, hasText: Boolean, timedMessagesTTL: Int?, + showViaProxy: Boolean, + sizeMultiplier: Float, play: () -> Unit, pause: () -> Unit, longClick: () -> Unit, @@ -113,15 +132,16 @@ private fun VoiceLayout( var movedManuallyTo by rememberSaveable(file.fileId) { mutableStateOf(-1) } if (audioPlaying.value || progress.value > 0 || movedManuallyTo == progress.value) { val dp4 = with(LocalDensity.current) { 4.dp.toPx() } - val dp10 = with(LocalDensity.current) { 10.dp.toPx() } val primary = MaterialTheme.colors.primary val inactiveTrackColor = MaterialTheme.colors.primary.mixWith( backgroundColor.copy(1f).mixWith(MaterialTheme.colors.background, backgroundColor.alpha), 0.24f) val width = LocalWindowWidth() + // Built-in slider has rounded corners but we need square corners, so drawing a track manually val colors = SliderDefaults.colors( - inactiveTrackColor = inactiveTrackColor + inactiveTrackColor = Color.Transparent, + activeTrackColor = Color.Transparent ) Slider( progress.value.toFloat(), @@ -130,12 +150,12 @@ private fun VoiceLayout( movedManuallyTo = it.toInt() }, Modifier - .size(width, 48.dp) + .size(width, 48.sp.toDp()) .weight(1f) .padding(padding) .drawBehind { - drawRect(primary, Offset(0f, (size.height - dp4) / 2), size = androidx.compose.ui.geometry.Size(dp10, dp4)) - drawRect(inactiveTrackColor, Offset(size.width - dp10, (size.height - dp4) / 2), size = androidx.compose.ui.geometry.Size(dp10, dp4)) + drawRect(inactiveTrackColor, Offset(0f, (size.height - dp4) / 2), size = Size(size.width, dp4)) + drawRect(primary, Offset(0f, (size.height - dp4) / 2), size = Size(progress.value.toFloat() / max(0.00001f, duration.value.toFloat()) * size.width, dp4)) }, valueRange = 0f..duration.value.toFloat(), colors = colors @@ -150,13 +170,22 @@ private fun VoiceLayout( } } when { - hasText -> { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage - Spacer(Modifier.width(6.dp)) - VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile) + sizeMultiplier != 1f -> { Row(verticalAlignment = Alignment.CenterVertically) { - DurationText(text, PaddingValues(start = 12.dp)) + VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, sizeMultiplier, play, pause, longClick, receiveFile) + Row(Modifier.weight(1f, false), verticalAlignment = Alignment.CenterVertically) { + DurationText(text, PaddingValues(start = 8.sp.toDp()), true) + Slider(MaterialTheme.colors.background, PaddingValues(start = 7.sp.toDp())) + } + } + } + hasText -> { + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage + Spacer(Modifier.width(6.sp.toDp() * sizeMultiplier)) + VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, 1f, play, pause, longClick, receiveFile) + Row(verticalAlignment = Alignment.CenterVertically) { + DurationText(text, PaddingValues(start = 12.sp.toDp() * sizeMultiplier)) Slider(if (ci.chatDir.sent) sentColor else receivedColor) } } @@ -164,29 +193,29 @@ private fun VoiceLayout( Column(horizontalAlignment = Alignment.End) { Row { Row(Modifier.weight(1f, false), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.End) { - Spacer(Modifier.height(56.dp)) + Spacer(Modifier.height(56.sp.toDp() * sizeMultiplier)) Slider(MaterialTheme.colors.background, PaddingValues(end = DEFAULT_PADDING_HALF + 3.dp)) - DurationText(text, PaddingValues(end = 12.dp)) + DurationText(text, PaddingValues(end = 12.sp.toDp() * sizeMultiplier)) } - VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile) + VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, 1f, play, pause, longClick, receiveFile) } - Box(Modifier.padding(top = 6.dp, end = 6.dp)) { - CIMetaView(ci, timedMessagesTTL) + Box(Modifier.padding(top = 6.sp.toDp() * sizeMultiplier, end = 6.sp.toDp() * sizeMultiplier)) { + CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy) } } } else -> { Column(horizontalAlignment = Alignment.Start) { Row { - VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile) + VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, 1f, play, pause, longClick, receiveFile) Row(Modifier.weight(1f, false), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) { - DurationText(text, PaddingValues(start = 12.dp)) + DurationText(text, PaddingValues(start = 12.sp.toDp() * sizeMultiplier)) Slider(MaterialTheme.colors.background, PaddingValues(start = DEFAULT_PADDING_HALF + 3.dp)) - Spacer(Modifier.height(56.dp)) + Spacer(Modifier.height(56.sp.toDp() * sizeMultiplier)) } } - Box(Modifier.padding(top = 6.dp)) { - CIMetaView(ci, timedMessagesTTL) + Box(Modifier.padding(top = 6.sp.toDp() * sizeMultiplier)) { + CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy) } } } @@ -194,7 +223,7 @@ private fun VoiceLayout( } @Composable -private fun DurationText(text: State, padding: PaddingValues) { +private fun DurationText(text: State, padding: PaddingValues, smallView: Boolean = false) { val minWidth = with(LocalDensity.current) { 45.sp.toDp() } Text( text.value, @@ -202,7 +231,7 @@ private fun DurationText(text: State, padding: PaddingValues) { .padding(padding) .widthIn(min = minWidth), color = MaterialTheme.colors.secondary, - fontSize = 16.sp, + fontSize = if (smallView) 15.sp else 16.sp, maxLines = 1 ) } @@ -216,12 +245,14 @@ private fun PlayPauseButton( strokeColor: Color, enabled: Boolean, error: Boolean, + sizeMultiplier: Float = 1f, play: () -> Unit, pause: () -> Unit, - longClick: () -> Unit + longClick: () -> Unit, + icon: ImageResource = MR.images.ic_play_arrow_filled, ) { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Surface( Modifier.drawRingModifier(angle, strokeColor, strokeWidth), color = if (sent) sentColor else receivedColor, @@ -230,19 +261,103 @@ private fun PlayPauseButton( ) { Box( Modifier - .defaultMinSize(minWidth = 56.dp, minHeight = 56.dp) + .defaultMinSize(minWidth = 56.sp.toDp() * sizeMultiplier, minHeight = 56.sp.toDp() * sizeMultiplier) .combinedClickable( onClick = { if (!audioPlaying) play() else pause() }, onLongClick = longClick ) .onRightClick { longClick() }, contentAlignment = Alignment.Center + ) { + Icon( + if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(icon), + contentDescription = null, + Modifier.size(36.sp.toDp() * sizeMultiplier), + tint = if (error) WarningOrange else if (!enabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + ) + } + } +} + +@Composable +private fun PlayablePlayPauseButton( + audioPlaying: Boolean, + sent: Boolean, + hasText: Boolean, + progress: State, + duration: State, + strokeWidth: Float, + strokeColor: Color, + error: Boolean, + sizeMultiplier: Float = 1f, + play: () -> Unit, + pause: () -> Unit, + longClick: () -> Unit, +) { + val angle = 360f * (progress.value.toDouble() / duration.value).toFloat() + if (hasText) { + Box( + Modifier + .defaultMinSize(minWidth = 56.sp.toDp() * sizeMultiplier, minHeight = 56.sp.toDp() * sizeMultiplier) + .clip(MaterialTheme.shapes.small.copy(CornerSize(percent = 50))) + .combinedClickable(onClick = { if (!audioPlaying) play() else pause() } ) + .drawRingModifier(angle, strokeColor, strokeWidth), + contentAlignment = Alignment.Center ) { Icon( if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(MR.images.ic_play_arrow_filled), contentDescription = null, - Modifier.size(36.dp), - tint = if (error) WarningOrange else if (!enabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + Modifier.size(36.sp.toDp() * sizeMultiplier), + tint = MaterialTheme.colors.primary + ) + } + } else { + PlayPauseButton(audioPlaying, sent, angle, strokeWidth, strokeColor, true, error, sizeMultiplier, play, pause, longClick = longClick) + } +} + +@Composable +private fun VoiceMsgLoadingProgressIndicator(sizeMultiplier: Float) { + Box( + Modifier + .size(56.sp.toDp() * sizeMultiplier) + .clip(RoundedCornerShape(4.sp.toDp() * sizeMultiplier)), + contentAlignment = Alignment.Center + ) { + ProgressIndicator(sizeMultiplier) + } +} + +@Composable +private fun FileStatusIcon( + sent: Boolean, + icon: ImageResource, + sizeMultiplier: Float, + longClick: () -> Unit, + onClick: () -> Unit, +) { + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage + Surface( + color = if (sent) sentColor else receivedColor, + shape = MaterialTheme.shapes.small.copy(CornerSize(percent = 50)), + contentColor = LocalContentColor.current + ) { + Box( + Modifier + .defaultMinSize(minWidth = 56.sp.toDp() * sizeMultiplier, minHeight = 56.sp.toDp() * sizeMultiplier) + .combinedClickable( + onClick = onClick, + onLongClick = longClick + ) + .onRightClick { longClick() }, + contentAlignment = Alignment.Center + ) { + Icon( + painterResource(icon), + contentDescription = null, + Modifier.size(36.sp.toDp() * sizeMultiplier), + tint = MaterialTheme.colors.secondary ) } } @@ -257,44 +372,85 @@ private fun VoiceMsgIndicator( progress: State?, duration: State?, error: Boolean, + sizeMultiplier: Float, play: () -> Unit, pause: () -> Unit, longClick: () -> Unit, receiveFile: (Long) -> Unit, ) { - val strokeWidth = with(LocalDensity.current) { 3.dp.toPx() } + val strokeWidth = with(LocalDensity.current) { 3.sp.toPx() } * sizeMultiplier val strokeColor = MaterialTheme.colors.primary - if (file != null && file.loaded && progress != null && duration != null) { - val angle = 360f * (progress.value.toDouble() / duration.value).toFloat() - if (hasText) { - IconButton({ if (!audioPlaying) play() else pause() }, Modifier.size(56.dp).drawRingModifier(angle, strokeColor, strokeWidth)) { - Icon( - if (audioPlaying) painterResource(MR.images.ic_pause_filled) else painterResource(MR.images.ic_play_arrow_filled), - contentDescription = null, - Modifier.size(36.dp), - tint = MaterialTheme.colors.primary - ) + when { + file?.fileStatus is CIFileStatus.SndStored -> + if (file.fileProtocol == FileProtocol.LOCAL && progress != null && duration != null) { + PlayablePlayPauseButton(audioPlaying, sent, hasText, progress, duration, strokeWidth, strokeColor, error, sizeMultiplier, play, pause, longClick = longClick) + } else { + VoiceMsgLoadingProgressIndicator(sizeMultiplier) } - } else { - PlayPauseButton(audioPlaying, sent, angle, strokeWidth, strokeColor, true, error, play, pause, longClick = longClick) - } - } else { - if (file?.fileStatus is CIFileStatus.RcvInvitation) { - PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, { receiveFile(file.fileId) }, {}, longClick = longClick) - } else if (file?.fileStatus is CIFileStatus.RcvTransfer - || file?.fileStatus is CIFileStatus.RcvAccepted - ) { - Box( - Modifier - .size(56.dp) - .clip(RoundedCornerShape(4.dp)), - contentAlignment = Alignment.Center - ) { - ProgressIndicator() - } - } else { - PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, false, false, {}, {}, longClick) - } + file?.fileStatus is CIFileStatus.SndTransfer -> + VoiceMsgLoadingProgressIndicator(sizeMultiplier) + file != null && file.fileStatus is CIFileStatus.SndError -> + FileStatusIcon( + sent, + MR.images.ic_close, + sizeMultiplier, + longClick, + onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.file_error), + file.fileStatus.sndFileError.errorInfo + ) + } + ) + file != null && file.fileStatus is CIFileStatus.SndWarning -> + FileStatusIcon( + sent, + MR.images.ic_warning_filled, + sizeMultiplier, + longClick, + onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.temporary_file_error), + file.fileStatus.sndFileError.errorInfo + ) + } + ) + file?.fileStatus is CIFileStatus.RcvInvitation -> + PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, sizeMultiplier, { receiveFile(file.fileId) }, {}, longClick = longClick) + file?.fileStatus is CIFileStatus.RcvTransfer || file?.fileStatus is CIFileStatus.RcvAccepted -> + VoiceMsgLoadingProgressIndicator(sizeMultiplier) + file?.fileStatus is CIFileStatus.RcvAborted -> + PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, true, error, sizeMultiplier, { receiveFile(file.fileId) }, {}, longClick = longClick, icon = MR.images.ic_sync_problem) + file != null && file.fileStatus is CIFileStatus.RcvError -> + FileStatusIcon( + sent, + MR.images.ic_close, + sizeMultiplier, + longClick, + onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.file_error), + file.fileStatus.rcvFileError.errorInfo + ) + } + ) + file != null && file.fileStatus is CIFileStatus.RcvWarning -> + FileStatusIcon( + sent, + MR.images.ic_warning_filled, + sizeMultiplier, + longClick, + onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.temporary_file_error), + file.fileStatus.rcvFileError.errorInfo + ) + } + ) + file != null && file.loaded && progress != null && duration != null -> + PlayablePlayPauseButton(audioPlaying, sent, hasText, progress, duration, strokeWidth, strokeColor, error, sizeMultiplier, play, pause, longClick = longClick) + else -> + PlayPauseButton(audioPlaying, sent, 0f, strokeWidth, strokeColor, false, false, sizeMultiplier, {}, {}, longClick) } } @@ -320,11 +476,16 @@ fun Modifier.drawRingModifier(angle: Float, color: Color, strokeWidth: Float) = } } +fun voiceMessageSizeBasedOnSquareSize(squareSize: Float): Float { + val squareToCircleRatio = 0.935f + return squareSize + squareSize * (1 - squareToCircleRatio) +} + @Composable -private fun ProgressIndicator() { +private fun ProgressIndicator(sizeMultiplier: Float) { CircularProgressIndicator( - Modifier.size(32.dp), + Modifier.size(32.sp.toDp() * sizeMultiplier), color = if (isInDarkTheme()) FileDark else FileLight, - strokeWidth = 4.dp + strokeWidth = 4.sp.toDp() * sizeMultiplier ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 8dc7f8bcea..29717e3ecf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -50,6 +50,8 @@ fun ChatItemView( linkMode: SimplexLinkMode, revealed: MutableState, range: IntRange?, + selectedChatItems: MutableState?>, + selectChatItem: () -> Unit, deleteMessage: (Long, CIDeleteMode) -> Unit, deleteMessages: (List) -> Unit, receiveFile: (Long) -> Unit, @@ -59,6 +61,7 @@ fun ChatItemView( scrollToItem: (Long) -> Unit, acceptFeature: (Contact, ChatFeature, Int?) -> Unit, openDirectChat: (Long) -> Unit, + forwardItem: (ChatInfo, ChatItem) -> Unit, updateContactStats: (Contact) -> Unit, updateMemberStats: (GroupInfo, GroupMember) -> Unit, syncContactConnection: (Contact) -> Unit, @@ -68,6 +71,8 @@ fun ChatItemView( setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit, showItemDetails: (ChatInfo, ChatItem) -> Unit, developerTools: Boolean, + showViaProxy: Boolean, + preview: Boolean = false, ) { val uriHandler = LocalUriHandler.current val sent = cItem.chatDir.sent @@ -78,22 +83,18 @@ fun ChatItemView( val live = composeState.value.liveMessage != null Box( - modifier = Modifier - .padding(bottom = 4.dp) - .fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), contentAlignment = alignment, ) { - val onClick = { - when (cItem.meta.itemStatus) { - is CIStatus.SndErrorAuth -> { - showMsgDeliveryErrorAlert(generalGetString(MR.strings.message_delivery_error_desc)) - } - is CIStatus.SndError -> { - showMsgDeliveryErrorAlert(generalGetString(MR.strings.unknown_error) + ": ${cItem.meta.itemStatus.agentError}") - } - else -> {} + val info = cItem.meta.itemStatus.statusInto + val onClick = if (info != null) { + { + AlertManager.shared.showAlertMsg( + title = info.first, + text = info.second, + ) } - } + } else { {} } @Composable fun ChatItemReactions() { @@ -130,7 +131,7 @@ fun ChatItemView( ) { @Composable fun framedItemView() { - FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showMenu, receiveFile, onLinkLongClick, scrollToItem) + FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, receiveFile, onLinkLongClick, scrollToItem) } fun deleteMessageQuestionText(): String { @@ -141,14 +142,6 @@ fun ChatItemView( } } - fun moderateMessageQuestionText(): String { - return if (fullDeleteAllowed) { - generalGetString(MR.strings.moderate_message_will_be_deleted_warning) - } else { - generalGetString(MR.strings.moderate_message_will_be_marked_warning) - } - } - @Composable fun MsgReactionsMenu() { val rs = MsgReaction.values.mapNotNull { r -> @@ -179,6 +172,10 @@ fun ChatItemView( fun DeleteItemMenu() { DefaultDropdownMenu(showMenu) { DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + if (cItem.canBeDeletedForSelf) { + Divider() + SelectItemAction(showMenu, selectChatItem) + } } } @@ -261,8 +258,7 @@ fun ChatItemView( !cItem.isLiveDummy && !live ) { ItemAction(stringResource(MR.strings.forward_chat_item), painterResource(MR.images.ic_forward), onClick = { - chatModel.chatId.value = null - chatModel.sharedContent.value = SharedContent.Forward(cItem, cInfo) + forwardItem(cInfo, cItem) showMenu.value = false }) } @@ -273,12 +269,16 @@ fun ChatItemView( if (cItem.meta.itemDeleted == null && cItem.file != null && cItem.file.cancelAction != null && !cItem.localNote) { CancelFileItemAction(cItem.file.fileId, showMenu, cancelFile = cancelFile, cancelAction = cItem.file.cancelAction) } - if (!(live && cItem.meta.isLive)) { + if (!(live && cItem.meta.isLive) && !preview) { DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) } val groupInfo = cItem.memberToModerate(cInfo)?.first - if (groupInfo != null) { - ModerateItemAction(cItem, questionText = moderateMessageQuestionText(), showMenu, deleteMessage) + if (groupInfo != null && cItem.chatDir !is CIDirection.GroupSnd) { + ModerateItemAction(cItem, questionText = moderateMessageQuestionText(cInfo.featureEnabled(ChatFeature.FullDelete), 1), showMenu, deleteMessage) + } + if (cItem.canBeDeletedForSelf) { + Divider() + SelectItemAction(showMenu, selectChatItem) } } } @@ -293,12 +293,20 @@ fun ChatItemView( } ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + if (cItem.canBeDeletedForSelf) { + Divider() + SelectItemAction(showMenu, selectChatItem) + } } } cItem.isDeletedContent -> { DefaultDropdownMenu(showMenu) { ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + if (cItem.canBeDeletedForSelf) { + Divider() + SelectItemAction(showMenu, selectChatItem) + } } } cItem.mergeCategory != null && ((range?.count() ?: 0) > 1 || revealed.value) -> { @@ -309,11 +317,19 @@ fun ChatItemView( ExpandItemAction(revealed, showMenu) } DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + if (cItem.canBeDeletedForSelf) { + Divider() + SelectItemAction(showMenu, selectChatItem) + } } } else -> { DefaultDropdownMenu(showMenu) { DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + if (selectedChatItems.value == null) { + Divider() + SelectItemAction(showMenu, selectChatItem) + } } } } @@ -327,6 +343,10 @@ fun ChatItemView( } ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + if (cItem.canBeDeletedForSelf) { + Divider() + SelectItemAction(showMenu, selectChatItem) + } } } @@ -334,14 +354,14 @@ fun ChatItemView( fun ContentItem() { val mc = cItem.content.msgContent if (cItem.meta.itemDeleted != null && (!revealed.value || cItem.isDeletedContent)) { - MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed) + MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy) MarkedDeletedItemDropdownMenu() } else { if (cItem.quotedItem == null && cItem.meta.itemForwarded == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) { if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) { - EmojiItemView(cItem, cInfo.timedMessagesTTL) + EmojiItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy) } else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) { - CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, longClick = { onLinkLongClick("") }, receiveFile) + CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, longClick = { onLinkLongClick("") }, receiveFile = receiveFile) } else { framedItemView() } @@ -353,10 +373,14 @@ fun ChatItemView( } @Composable fun LegacyDeletedItem() { - DeletedItemView(cItem, cInfo.timedMessagesTTL) + DeletedItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy) DefaultDropdownMenu(showMenu) { ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) + if (cItem.canBeDeletedForSelf) { + Divider() + SelectItemAction(showMenu, selectChatItem) + } } } @@ -406,10 +430,14 @@ fun ChatItemView( @Composable fun DeletedItem() { - MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed) + MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy) DefaultDropdownMenu(showMenu) { ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) DeleteItemAction(cItem, revealed, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage, deleteMessages) + if (cItem.canBeDeletedForSelf) { + Divider() + SelectItemAction(showMenu, selectChatItem) + } } } @@ -607,7 +635,12 @@ fun DeleteItemAction( for (i in range) { itemIds.add(reversedChatItems[i].id) } - deleteMessagesAlertDialog(itemIds, generalGetString(MR.strings.delete_message_mark_deleted_warning), deleteMessages = deleteMessages) + deleteMessagesAlertDialog( + itemIds, + generalGetString(if (itemIds.size == 1) MR.strings.delete_message_mark_deleted_warning else MR.strings.delete_messages_mark_deleted_warning), + forAll = false, + deleteMessages = { ids, _ -> deleteMessages(ids) } + ) } else { deleteMessageAlertDialog(cItem, questionText, deleteMessage = deleteMessage) } @@ -640,6 +673,21 @@ fun ModerateItemAction( ) } +@Composable +fun SelectItemAction( + showMenu: MutableState, + selectChatItem: () -> Unit, +) { + ItemAction( + stringResource(MR.strings.select_verb), + painterResource(MR.images.ic_check_circle), + onClick = { + showMenu.value = false + selectChatItem() + } + ) +} + @Composable private fun RevealItemAction(revealed: MutableState, showMenu: MutableState) { ItemAction( @@ -784,7 +832,7 @@ fun deleteMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMes ) } -fun deleteMessagesAlertDialog(itemIds: List, questionText: String, deleteMessages: (List) -> Unit) { +fun deleteMessagesAlertDialog(itemIds: List, questionText: String, forAll: Boolean, deleteMessages: (List, Boolean) -> Unit) { AlertManager.shared.showAlertDialogButtons( title = generalGetString(MR.strings.delete_messages__question).format(itemIds.size), text = questionText, @@ -796,14 +844,29 @@ fun deleteMessagesAlertDialog(itemIds: List, questionText: String, deleteM horizontalArrangement = Arrangement.Center, ) { TextButton(onClick = { - deleteMessages(itemIds) + deleteMessages(itemIds, false) AlertManager.shared.hideAlert() }) { Text(stringResource(MR.strings.for_me_only), color = MaterialTheme.colors.error) } + + if (forAll) { + TextButton(onClick = { + deleteMessages(itemIds, true) + AlertManager.shared.hideAlert() + }) { Text(stringResource(MR.strings.for_everybody), color = MaterialTheme.colors.error) } + } } } ) } +fun moderateMessageQuestionText(fullDeleteAllowed: Boolean, count: Int): String { + return if (fullDeleteAllowed) { + generalGetString(if (count == 1) MR.strings.moderate_message_will_be_deleted_warning else MR.strings.moderate_messages_will_be_deleted_warning) + } else { + generalGetString(if (count == 1) MR.strings.moderate_message_will_be_marked_warning else MR.strings.moderate_messages_will_be_marked_warning) + } +} + fun moderateMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteMessage: (Long, CIDeleteMode) -> Unit) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.delete_member_message__question), @@ -816,10 +879,13 @@ fun moderateMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteM ) } -private fun showMsgDeliveryErrorAlert(description: String) { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.message_delivery_error_title), - text = description, +fun moderateMessagesAlertDialog(itemIds: List, questionText: String, deleteMessages: (List) -> Unit) { + AlertManager.shared.showAlertDialog( + title = if (itemIds.size == 1) generalGetString(MR.strings.delete_member_message__question) else generalGetString(MR.strings.delete_members_messages__question).format(itemIds.size), + text = questionText, + confirmText = generalGetString(MR.strings.delete_verb), + destructive = true, + onConfirm = { deleteMessages(itemIds) } ) } @@ -827,39 +893,42 @@ expect fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager) @Preview @Composable -fun PreviewChatItemView() { - SimpleXTheme { - ChatItemView( - rhId = null, - ChatInfo.Direct.sampleData, - ChatItem.getSampleData( - 1, CIDirection.DirectSnd(), Clock.System.now(), "hello" - ), - useLinkPreviews = true, - linkMode = SimplexLinkMode.DESCRIPTION, - composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, - revealed = remember { mutableStateOf(false) }, - range = 0..1, - deleteMessage = { _, _ -> }, - deleteMessages = { _ -> }, - receiveFile = { _ -> }, - cancelFile = {}, - joinGroup = { _, _ -> }, - acceptCall = { _ -> }, - scrollToItem = {}, - acceptFeature = { _, _, _ -> }, - openDirectChat = { _ -> }, - updateContactStats = { }, - updateMemberStats = { _, _ -> }, - syncContactConnection = { }, - syncMemberConnection = { _, _ -> }, - findModelChat = { null }, - findModelMember = { null }, - setReaction = { _, _, _, _ -> }, - showItemDetails = { _, _ -> }, - developerTools = false, - ) - } +fun PreviewChatItemView( + chatItem: ChatItem = ChatItem.getSampleData(1, CIDirection.DirectSnd(), Clock.System.now(), "hello") +) { + ChatItemView( + rhId = null, + ChatInfo.Direct.sampleData, + chatItem, + useLinkPreviews = true, + linkMode = SimplexLinkMode.DESCRIPTION, + composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, + revealed = remember { mutableStateOf(false) }, + range = 0..1, + selectedChatItems = remember { mutableStateOf(setOf()) }, + selectChatItem = {}, + deleteMessage = { _, _ -> }, + deleteMessages = { _ -> }, + receiveFile = { _ -> }, + cancelFile = {}, + joinGroup = { _, _ -> }, + acceptCall = { _ -> }, + scrollToItem = {}, + acceptFeature = { _, _, _ -> }, + openDirectChat = { _ -> }, + forwardItem = { _, _ -> }, + updateContactStats = { }, + updateMemberStats = { _, _ -> }, + syncContactConnection = { }, + syncMemberConnection = { _, _ -> }, + findModelChat = { null }, + findModelMember = { null }, + setReaction = { _, _, _, _ -> }, + showItemDetails = { _, _ -> }, + developerTools = false, + showViaProxy = false, + preview = true, + ) } @Preview @@ -875,6 +944,8 @@ fun PreviewChatItemViewDeletedContent() { composeState = remember { mutableStateOf(ComposeState(useLinkPreviews = true)) }, revealed = remember { mutableStateOf(false) }, range = 0..1, + selectedChatItems = remember { mutableStateOf(setOf()) }, + selectChatItem = {}, deleteMessage = { _, _ -> }, deleteMessages = { _ -> }, receiveFile = { _ -> }, @@ -884,6 +955,7 @@ fun PreviewChatItemViewDeletedContent() { scrollToItem = {}, acceptFeature = { _, _, _ -> }, openDirectChat = { _ -> }, + forwardItem = { _, _ -> }, updateContactStats = { }, updateMemberStats = { _, _ -> }, syncContactConnection = { }, @@ -893,6 +965,8 @@ fun PreviewChatItemViewDeletedContent() { setReaction = { _, _, _, _ -> }, showItemDetails = { _, _ -> }, developerTools = false, + showViaProxy = false, + preview = true, ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt index 7514b6e280..9b7db099b6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt @@ -16,10 +16,10 @@ import chat.simplex.common.model.ChatItem import chat.simplex.common.ui.theme.* @Composable -fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) { +fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean) { val sent = ci.chatDir.sent - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Surface( shape = RoundedCornerShape(18.dp), color = if (sent) sentColor else receivedColor, @@ -36,7 +36,7 @@ fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) { style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), modifier = Modifier.padding(end = 8.dp) ) - CIMetaView(ci, timedMessagesTTL) + CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy) } } } @@ -50,7 +50,8 @@ fun PreviewDeletedItemView() { SimpleXTheme { DeletedItemView( ChatItem.getDeletedContentSampleData(), - null + null, + showViaProxy = false ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.kt index 3ede737ffa..4969eccbb6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.kt @@ -17,13 +17,13 @@ val largeEmojiFont: TextStyle = TextStyle(fontSize = 48.sp, fontFamily = EmojiFo val mediumEmojiFont: TextStyle = TextStyle(fontSize = 36.sp, fontFamily = EmojiFont) @Composable -fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?) { +fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean) { Column( Modifier.padding(vertical = 8.dp, horizontal = 12.dp), horizontalAlignment = Alignment.CenterHorizontally ) { EmojiText(chatItem.content.text) - CIMetaView(chatItem, timedMessagesTTL) + CIMetaView(chatItem, timedMessagesTTL, showViaProxy = showViaProxy) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index 09cb1cfd23..8a579d5289 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -33,6 +33,7 @@ fun FramedItemView( uriHandler: UriHandler? = null, imageProvider: (() -> ImageGalleryProvider)? = null, linkMode: SimplexLinkMode, + showViaProxy: Boolean, showMenu: MutableState, receiveFile: (Long) -> Unit, onLinkLongClick: (link: String) -> Unit = {}, @@ -45,9 +46,6 @@ fun FramedItemView( return if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.membership else null } - @Composable - fun Color.toQuote(): Color = if (isInDarkTheme()) lighter(0.12f) else darker(0.12f) - @Composable fun ciQuotedMsgTextView(qi: CIQuote, lines: Int) { MarkdownText( @@ -88,11 +86,11 @@ fun FramedItemView( @Composable fun FramedItemHeader(caption: String, italic: Boolean, icon: Painter? = null, pad: Boolean = false) { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val sentColor = MaterialTheme.appColors.sentQuote + val receivedColor = MaterialTheme.appColors.receivedQuote Row( Modifier - .background(if (sent) sentColor.toQuote() else receivedColor.toQuote()) + .background(if (sent) sentColor else receivedColor) .fillMaxWidth() .padding(start = 8.dp, top = 6.dp, end = 12.dp, bottom = if (pad || (ci.quotedItem == null && ci.meta.itemForwarded == null)) 6.dp else 0.dp), horizontalArrangement = Arrangement.spacedBy(4.dp), @@ -121,11 +119,11 @@ fun FramedItemView( @Composable fun ciQuoteView(qi: CIQuote) { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val sentColor = MaterialTheme.appColors.sentQuote + val receivedColor = MaterialTheme.appColors.receivedQuote Row( Modifier - .background(if (sent) sentColor.toQuote() else receivedColor.toQuote()) + .background(if (sent) sentColor else receivedColor) .fillMaxWidth() .combinedClickable( onLongClick = { showMenu.value = true }, @@ -178,17 +176,17 @@ fun FramedItemView( @Composable fun ciFileView(ci: ChatItem, text: String) { - CIFileView(ci.file, ci.meta.itemEdited, showMenu, receiveFile) + CIFileView(ci.file, ci.meta.itemEdited, showMenu, false, receiveFile) if (text != "" || ci.meta.isLive) { - CIMarkdownText(ci, chatTTL, linkMode = linkMode, uriHandler) + CIMarkdownText(ci, chatTTL, linkMode = linkMode, uriHandler, showViaProxy = showViaProxy) } } val transparentBackground = (ci.content.msgContent is MsgContent.MCImage || ci.content.msgContent is MsgContent.MCVideo) && !ci.meta.isLive && ci.content.text.isEmpty() && ci.quotedItem == null && ci.meta.itemForwarded == null - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Box(Modifier .clip(RoundedCornerShape(18.dp)) .background( @@ -240,47 +238,47 @@ fun FramedItemView( } else { when (val mc = ci.content.msgContent) { is MsgContent.MCImage -> { - CIImageView(image = mc.image, file = ci.file, metaColor = metaColor, imageProvider ?: return@PriorityLayout, showMenu, receiveFile) + CIImageView(image = mc.image, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, false, receiveFile) if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { - CIMarkdownText(ci, chatTTL, linkMode, uriHandler) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy) } } is MsgContent.MCVideo -> { - CIVideoView(image = mc.image, mc.duration, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, receiveFile) + CIVideoView(image = mc.image, mc.duration, file = ci.file, imageProvider ?: return@PriorityLayout, showMenu, smallView = false, receiveFile = receiveFile) if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { - CIMarkdownText(ci, chatTTL, linkMode, uriHandler) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy) } } is MsgContent.MCVoice -> { - CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, longClick = { onLinkLongClick("") }, receiveFile) + CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, showViaProxy = showViaProxy, longClick = { onLinkLongClick("") }, receiveFile = receiveFile) if (mc.text != "") { - CIMarkdownText(ci, chatTTL, linkMode, uriHandler) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy) } } is MsgContent.MCFile -> ciFileView(ci, mc.text) is MsgContent.MCUnknown -> if (ci.file == null) { - CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy) } else { ciFileView(ci, mc.text) } is MsgContent.MCLink -> { - ChatItemLinkView(mc.preview) + ChatItemLinkView(mc.preview, showMenu, onLongClick = { showMenu.value = true }) Box(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) { - CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy) } } - else -> CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick) + else -> CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy) } } } } Box(Modifier.padding(bottom = 6.dp, end = 12.dp)) { - CIMetaView(ci, chatTTL, metaColor) + CIMetaView(ci, chatTTL, metaColor, showViaProxy = showViaProxy) } } } @@ -292,14 +290,15 @@ fun CIMarkdownText( chatTTL: Int?, linkMode: SimplexLinkMode, uriHandler: UriHandler?, - onLinkLongClick: (link: String) -> Unit = {} + onLinkLongClick: (link: String) -> Unit = {}, + showViaProxy: Boolean ) { Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) { val text = if (ci.meta.isLive) ci.content.msgContent?.text ?: ci.text else ci.text MarkdownText( text, if (text.isEmpty()) emptyList() else ci.formattedText, toggleSecrets = true, meta = ci.meta, chatTTL = chatTTL, linkMode = linkMode, - uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick + uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick, showViaProxy = showViaProxy ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt index 1dd5e4ee69..ab3918549d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ImageFullScreenView.kt @@ -12,11 +12,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.* import androidx.compose.ui.input.pointer.* import androidx.compose.ui.layout.onGloballyPositioned +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.CryptoFile import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.CurrentColors import chat.simplex.common.views.chat.ProviderMedia import chat.simplex.common.views.helpers.* -import chat.simplex.res.MR import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import java.net.URI @@ -40,24 +41,33 @@ fun ImageFullScreenView(imageProvider: () -> ImageGalleryProvider, close: () -> ) { provider.totalMediaSize.value } + val firstValidPageBeforeScrollingToStart = remember { mutableStateOf(0) } val goBack = { provider.onDismiss(pagerState.currentPage); close() } BackHandler(onBack = goBack) // Pager doesn't ask previous page at initialization step who knows why. By not doing this, prev page is not checked and can be blank, // which makes this blank page visible for a moment. Prevent it by doing the check ourselves LaunchedEffect(Unit) { if (provider.getMedia(provider.initialIndex - 1) == null) { + firstValidPageBeforeScrollingToStart.value = provider.initialIndex provider.scrollToStart() pagerState.scrollToPage(0) + firstValidPageBeforeScrollingToStart.value = 0 } } val scope = rememberCoroutineScope() val playersToRelease = rememberSaveable { mutableSetOf() } DisposableEffectOnGone( + always = { + platform.androidSetStatusAndNavBarColors(CurrentColors.value.colors.isLight, Color.Black, false, false) + }, whenGone = { playersToRelease.forEach { VideoPlayerHolder.release(it, true, true) } } ) @Composable fun Content(index: Int) { + // Index can be huge but in reality at that moment pager state scrolls to 0 and that page should have index 0 too if it's the first one. + // Or index 1 if it's the second page + val index = index - firstValidPageBeforeScrollingToStart.value Column( Modifier .fillMaxSize() @@ -174,7 +184,7 @@ private fun VideoViewEncrypted(uriUnencrypted: MutableState, fileSource: C } Box(contentAlignment = Alignment.Center) { VideoPreviewImageViewFullScreen(defaultPreview, {}, {}) - VideoDecryptionProgress {} + VideoDecryptionProgress() {} } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt index 7be0cc2f6c..dc585358c4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt @@ -17,8 +17,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.ChatItem import chat.simplex.common.model.MsgErrorType -import chat.simplex.common.ui.theme.CurrentColors -import chat.simplex.common.ui.theme.SimpleXTheme +import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.AlertManager import chat.simplex.common.views.helpers.generalGetString import chat.simplex.res.MR @@ -51,7 +50,7 @@ fun IntegrityErrorItemView(msgError: MsgErrorType, ci: ChatItem, timedMessagesTT @Composable fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) { - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Surface( Modifier.clickable(onClick = onClick), shape = RoundedCornerShape(18.dp), @@ -69,7 +68,7 @@ fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) { style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), modifier = Modifier.padding(end = 8.dp) ) - CIMetaView(ci, timedMessagesTTL) + CIMetaView(ci, timedMessagesTTL, showViaProxy = false) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt index 0e2e8867cb..5b5438d76f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt @@ -20,9 +20,9 @@ import dev.icerock.moko.resources.compose.stringResource import kotlinx.datetime.Clock @Composable -fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: MutableState) { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage - val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage +fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: MutableState, showViaProxy: Boolean) { + val sentColor = MaterialTheme.appColors.sentMessage + val receivedColor = MaterialTheme.appColors.receivedMessage Surface( shape = RoundedCornerShape(18.dp), color = if (ci.chatDir.sent) sentColor else receivedColor, @@ -35,7 +35,7 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: Mutabl Box(Modifier.weight(1f, false)) { MergedMarkedDeletedText(ci, revealed) } - CIMetaView(ci, timedMessagesTTL) + CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy) } } } @@ -112,7 +112,8 @@ fun PreviewMarkedDeletedItemView() { SimpleXTheme { DeletedItemView( ChatItem.getSampleData(itemDeleted = CIDeleted.Deleted(Clock.System.now())), - null + null, + showViaProxy = false ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt index 66061767e5..c0e222d7d1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt @@ -69,7 +69,8 @@ fun MarkdownText ( modifier: Modifier = Modifier, linkMode: SimplexLinkMode, inlineContent: Pair Unit, Map>? = null, - onLinkLongClick: (link: String) -> Unit = {} + onLinkLongClick: (link: String) -> Unit = {}, + showViaProxy: Boolean = false ) { val textLayoutDirection = remember (text) { if (isRtl(text.subSequence(0, kotlin.math.min(50, text.length)))) LayoutDirection.Rtl else LayoutDirection.Ltr @@ -77,7 +78,7 @@ fun MarkdownText ( val reserve = if (textLayoutDirection != LocalLayoutDirection.current && meta != null) { "\n" } else if (meta != null) { - reserveSpaceForMeta(meta, chatTTL, null) // LALAL + reserveSpaceForMeta(meta, chatTTL, null, secondaryColor = MaterialTheme.colors.secondary, showViaProxy = showViaProxy) } else { " " } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index 2324d62ea3..e39adcca3b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -1,6 +1,7 @@ package chat.simplex.common.views.chatlist import SectionItemView +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* @@ -19,16 +20,18 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.* import chat.simplex.common.views.chat.group.deleteGroupDialog import chat.simplex.common.views.chat.group.leaveGroupDialog import chat.simplex.common.views.chat.item.ItemAction +import chat.simplex.common.views.contacts.onRequestAccepted import chat.simplex.common.views.helpers.* import chat.simplex.common.views.newchat.* import chat.simplex.res.MR -import kotlinx.coroutines.delay +import kotlinx.coroutines.* import kotlinx.datetime.Clock @Composable @@ -47,6 +50,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { val showChatPreviews = chatModel.showChatPreviews.value val inProgress = remember { mutableStateOf(false) } var progressByTimeout by rememberSaveable { mutableStateOf(false) } + LaunchedEffect(inProgress.value) { progressByTimeout = if (inProgress.value) { delay(1000) @@ -56,6 +60,8 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { } } + val scope = rememberCoroutineScope() + when (chat.chatInfo) { is ChatInfo.Direct -> { val contactNetworkStatus = chatModel.contactNetworkStatus(chat.chatInfo.contact) @@ -65,7 +71,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, disabled, linkMode, inProgress = false, progressByTimeout = false) } }, - click = { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) }, + click = { scope.launch { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) } }, dropdownMenuItems = { tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { ContactMenuItems(chat, chat.chatInfo.contact, chatModel, showMenu, showMarkRead) @@ -84,7 +90,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, disabled, linkMode, inProgress.value, progressByTimeout) } }, - click = { if (!inProgress.value) groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel, inProgress) }, + click = { if (!inProgress.value) scope.launch { groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel, inProgress) } }, dropdownMenuItems = { tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { GroupMenuItems(chat, chat.chatInfo.groupInfo, chatModel, showMenu, inProgress, showMarkRead) @@ -102,7 +108,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, null, disabled, linkMode, inProgress = false, progressByTimeout = false) } }, - click = { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) }, + click = { scope.launch { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) } }, dropdownMenuItems = { tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { NoteFolderMenuItems(chat, showMenu, showMarkRead) @@ -121,7 +127,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { ContactRequestView(chat.chatInfo) } }, - click = { contactRequestAlertDialog(chat.remoteHostId, chat.chatInfo, chatModel) }, + click = { contactRequestAlertDialog(chat.remoteHostId, chat.chatInfo, chatModel) { onRequestAccepted(it) } }, dropdownMenuItems = { tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { ContactRequestMenuItems(chat.remoteHostId, chat.chatInfo, chatModel, showMenu) @@ -172,48 +178,48 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { } @Composable -private fun ErrorChatListItem() { +fun ErrorChatListItem() { Box(Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 6.dp)) { Text(stringResource(MR.strings.error_showing_content), color = MaterialTheme.colors.error, fontStyle = FontStyle.Italic) } } -fun directChatAction(rhId: Long?, contact: Contact, chatModel: ChatModel) { +suspend fun directChatAction(rhId: Long?, contact: Contact, chatModel: ChatModel) { when { contact.activeConn == null && contact.profile.contactLink != null -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close = null, openChat = true) - else -> withBGApi { openChat(rhId, ChatInfo.Direct(contact), chatModel) } + else -> openChat(rhId, ChatInfo.Direct(contact), chatModel) } } -fun groupChatAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState? = null) { +suspend fun groupChatAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState? = null) { when (groupInfo.membership.memberStatus) { GroupMemberStatus.MemInvited -> acceptGroupInvitationAlertDialog(rhId, groupInfo, chatModel, inProgress) GroupMemberStatus.MemAccepted -> groupInvitationAcceptedAlert(rhId) - else -> withBGApi { openChat(rhId, ChatInfo.Group(groupInfo), chatModel) } + else -> openChat(rhId, ChatInfo.Group(groupInfo), chatModel) } } -fun noteFolderChatAction(rhId: Long?, noteFolder: NoteFolder) { - withBGApi { openChat(rhId, ChatInfo.Local(noteFolder), chatModel) } +suspend fun noteFolderChatAction(rhId: Long?, noteFolder: NoteFolder) { + openChat(rhId, ChatInfo.Local(noteFolder), chatModel) } -suspend fun openDirectChat(rhId: Long?, contactId: Long, chatModel: ChatModel) { +suspend fun openDirectChat(rhId: Long?, contactId: Long, chatModel: ChatModel) = coroutineScope { val chat = chatModel.controller.apiGetChat(rhId, ChatType.Direct, contactId) - if (chat != null) { + if (chat != null && isActive) { openLoadedChat(chat, chatModel) } } -suspend fun openGroupChat(rhId: Long?, groupId: Long, chatModel: ChatModel) { +suspend fun openGroupChat(rhId: Long?, groupId: Long, chatModel: ChatModel) = coroutineScope { val chat = chatModel.controller.apiGetChat(rhId, ChatType.Group, groupId) - if (chat != null) { + if (chat != null && isActive) { openLoadedChat(chat, chatModel) } } -suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatModel) { +suspend fun openChat(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatModel) = coroutineScope { val chat = chatModel.controller.apiGetChat(rhId, chatInfo.chatType, chatInfo.apiId) - if (chat != null) { + if (chat != null && isActive) { openLoadedChat(chat, chatModel) } } @@ -253,7 +259,9 @@ suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatMo } } chatModel.groupMembers.clear() + chatModel.groupMembersIndexes.clear() chatModel.groupMembers.addAll(newMembers) + chatModel.populateGroupMembersIndexes() } @Composable @@ -357,7 +365,7 @@ fun ToggleFavoritesChatAction(chat: Chat, chatModel: ChatModel, favorite: Boolea if (favorite) stringResource(MR.strings.unfavorite_chat) else stringResource(MR.strings.favorite_chat), if (favorite) painterResource(MR.images.ic_star_off) else painterResource(MR.images.ic_star), onClick = { - toggleChatFavorite(chat, !favorite, chatModel) + toggleChatFavorite(chat.remoteHostId, chat.chatInfo, !favorite, chatModel) showMenu.value = false } ) @@ -369,7 +377,7 @@ fun ToggleNotificationsChatAction(chat: Chat, chatModel: ChatModel, ntfsEnabled: if (ntfsEnabled) stringResource(MR.strings.mute_chat) else stringResource(MR.strings.unmute_chat), if (ntfsEnabled) painterResource(MR.images.ic_notifications_off) else painterResource(MR.images.ic_notifications), onClick = { - toggleNotifications(chat, !ntfsEnabled, chatModel) + toggleNotifications(chat.remoteHostId, chat.chatInfo, !ntfsEnabled, chatModel) showMenu.value = false } ) @@ -467,13 +475,13 @@ fun LeaveGroupAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, sh } @Composable -fun ContactRequestMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactRequest, chatModel: ChatModel, showMenu: MutableState) { +fun ContactRequestMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactRequest, chatModel: ChatModel, showMenu: MutableState, onSuccess: ((chat: Chat) -> Unit)? = null) { ItemAction( stringResource(MR.strings.accept_contact_button), painterResource(MR.images.ic_check), color = MaterialTheme.colors.onBackground, onClick = { - acceptContactRequest(rhId, incognito = false, chatInfo.apiId, chatInfo, true, chatModel) + acceptContactRequest(rhId, incognito = false, chatInfo.apiId, chatInfo, true, chatModel, onSuccess) showMenu.value = false } ) @@ -482,7 +490,7 @@ fun ContactRequestMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactRequest, chat painterResource(MR.images.ic_theater_comedy), color = MaterialTheme.colors.onBackground, onClick = { - acceptContactRequest(rhId, incognito = true, chatInfo.apiId, chatInfo, true, chatModel) + acceptContactRequest(rhId, incognito = true, chatInfo.apiId, chatInfo, true, chatModel, onSuccess) showMenu.value = false } ) @@ -552,7 +560,9 @@ fun markChatRead(c: Chat, chatModel: ChatModel) { withApi { if (chat.chatStats.unreadCount > 0) { val minUnreadItemId = chat.chatStats.minUnreadItemId - chatModel.markChatItemsRead(chat) + withChats { + markChatItemsRead(chat.remoteHostId, chat.chatInfo) + } chatModel.controller.apiChatRead( chat.remoteHostId, chat.chatInfo.chatType, @@ -569,7 +579,9 @@ fun markChatRead(c: Chat, chatModel: ChatModel) { false ) if (success) { - chatModel.replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false))) + withChats { + replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = false))) + } } } } @@ -587,12 +599,14 @@ fun markChatUnread(chat: Chat, chatModel: ChatModel) { true ) if (success) { - chatModel.replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true))) + withChats { + replaceChat(chat.remoteHostId, chat.id, chat.copy(chatStats = chat.chatStats.copy(unreadChat = true))) + } } } } -fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) { +fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel, onSucess: ((chat: Chat) -> Unit)? = null) { AlertManager.shared.showAlertDialogButtonsColumn( title = generalGetString(MR.strings.accept_connection_request__question), text = AnnotatedString(generalGetString(MR.strings.if_you_choose_to_reject_the_sender_will_not_be_notified)), @@ -600,13 +614,13 @@ fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactReque Column { SectionItemView({ AlertManager.shared.hideAlert() - acceptContactRequest(rhId, incognito = false, contactRequest.apiId, contactRequest, true, chatModel) + acceptContactRequest(rhId, incognito = false, contactRequest.apiId, contactRequest, true, chatModel, onSucess) }) { Text(generalGetString(MR.strings.accept_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } SectionItemView({ AlertManager.shared.hideAlert() - acceptContactRequest(rhId, incognito = true, contactRequest.apiId, contactRequest, true, chatModel) + acceptContactRequest(rhId, incognito = true, contactRequest.apiId, contactRequest, true, chatModel, onSucess) }) { Text(generalGetString(MR.strings.accept_contact_incognito_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } @@ -622,12 +636,16 @@ fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactReque ) } -fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRequest: ChatInfo.ContactRequest?, isCurrentUser: Boolean, chatModel: ChatModel) { +fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRequest: ChatInfo.ContactRequest?, isCurrentUser: Boolean, chatModel: ChatModel, close: ((chat: Chat) -> Unit)? = null ) { withBGApi { val contact = chatModel.controller.apiAcceptContactRequest(rhId, incognito, apiId) if (contact != null && isCurrentUser && contactRequest != null) { val chat = Chat(remoteHostId = rhId, ChatInfo.Direct(contact), listOf()) - chatModel.replaceChat(rhId, contactRequest.id, chat) + withChats { + replaceChat(rhId, contactRequest.id, chat) + } + chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected()) + close?.invoke(chat) } } } @@ -635,7 +653,9 @@ fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRe fun rejectContactRequest(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) { withBGApi { chatModel.controller.apiRejectContactRequest(rhId, contactRequest.apiId) - chatModel.removeChat(rhId, contactRequest.id) + withChats { + removeChat(rhId, contactRequest.id) + } } } @@ -651,7 +671,9 @@ fun deleteContactConnectionAlert(rhId: Long?, connection: PendingContactConnecti withBGApi { AlertManager.shared.hideAlert() if (chatModel.controller.apiDeleteChat(rhId, ChatType.ContactConnection, connection.apiId)) { - chatModel.removeChat(rhId, connection.id) + withChats { + removeChat(rhId, connection.id) + } onSuccess() } } @@ -670,7 +692,9 @@ fun pendingContactAlertDialog(rhId: Long?, chatInfo: ChatInfo, chatModel: ChatMo withBGApi { val r = chatModel.controller.apiDeleteChat(rhId, chatInfo.chatType, chatInfo.apiId) if (r) { - chatModel.removeChat(rhId, chatInfo.id) + withChats { + removeChat(rhId, chatInfo.id) + } if (chatModel.chatId.value == chatInfo.id) { chatModel.chatId.value = null ModalManager.end.closeModals() @@ -732,7 +756,9 @@ fun askCurrentOrIncognitoProfileConnectContactViaAddress( suspend fun connectContactViaAddress(chatModel: ChatModel, rhId: Long?, contactId: Long, incognito: Boolean): Boolean { val contact = chatModel.controller.apiConnectContactViaAddress(rhId, incognito, contactId) if (contact != null) { - chatModel.updateContact(rhId, contact) + withChats { + updateContact(rhId, contact) + } AlertManager.privacySensitive.showAlertMsg( title = generalGetString(MR.strings.connection_request_sent), text = generalGetString(MR.strings.you_will_be_connected_when_your_connection_request_is_accepted), @@ -773,7 +799,9 @@ fun deleteGroup(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) { withBGApi { val r = chatModel.controller.apiDeleteChat(rhId, ChatType.Group, groupInfo.apiId) if (r) { - chatModel.removeChat(rhId, groupInfo.id) + withChats { + removeChat(rhId, groupInfo.id) + } if (chatModel.chatId.value == groupInfo.id) { chatModel.chatId.value = null ModalManager.end.closeModals() @@ -791,22 +819,22 @@ fun groupInvitationAcceptedAlert(rhId: Long?) { ) } -fun toggleNotifications(chat: Chat, enableAllNtfs: Boolean, chatModel: ChatModel, currentState: MutableState? = null) { - val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(enableNtfs = if (enableAllNtfs) MsgFilter.All else MsgFilter.None) - updateChatSettings(chat, chatSettings, chatModel, currentState) +fun toggleNotifications(remoteHostId: Long?, chatInfo: ChatInfo, enableAllNtfs: Boolean, chatModel: ChatModel, currentState: MutableState? = null) { + val chatSettings = (chatInfo.chatSettings ?: ChatSettings.defaults).copy(enableNtfs = if (enableAllNtfs) MsgFilter.All else MsgFilter.None) + updateChatSettings(remoteHostId, chatInfo, chatSettings, chatModel, currentState) } -fun toggleChatFavorite(chat: Chat, favorite: Boolean, chatModel: ChatModel) { - val chatSettings = (chat.chatInfo.chatSettings ?: ChatSettings.defaults).copy(favorite = favorite) - updateChatSettings(chat, chatSettings, chatModel) +fun toggleChatFavorite(remoteHostId: Long?, chatInfo: ChatInfo, favorite: Boolean, chatModel: ChatModel) { + val chatSettings = (chatInfo.chatSettings ?: ChatSettings.defaults).copy(favorite = favorite) + updateChatSettings(remoteHostId, chatInfo, chatSettings, chatModel) } -fun updateChatSettings(chat: Chat, chatSettings: ChatSettings, chatModel: ChatModel, currentState: MutableState? = null) { - val newChatInfo = when(chat.chatInfo) { - is ChatInfo.Direct -> with (chat.chatInfo) { +fun updateChatSettings(remoteHostId: Long?, chatInfo: ChatInfo, chatSettings: ChatSettings, chatModel: ChatModel, currentState: MutableState? = null) { + val newChatInfo = when(chatInfo) { + is ChatInfo.Direct -> with (chatInfo) { ChatInfo.Direct(contact.copy(chatSettings = chatSettings)) } - is ChatInfo.Group -> with(chat.chatInfo) { + is ChatInfo.Group -> with(chatInfo) { ChatInfo.Group(groupInfo.copy(chatSettings = chatSettings)) } else -> null @@ -814,17 +842,19 @@ fun updateChatSettings(chat: Chat, chatSettings: ChatSettings, chatModel: ChatMo withBGApi { val res = when (newChatInfo) { is ChatInfo.Direct -> with(newChatInfo) { - chatModel.controller.apiSetSettings(chat.remoteHostId, chatType, apiId, contact.chatSettings) + chatModel.controller.apiSetSettings(remoteHostId, chatType, apiId, contact.chatSettings) } is ChatInfo.Group -> with(newChatInfo) { - chatModel.controller.apiSetSettings(chat.remoteHostId, chatType, apiId, groupInfo.chatSettings) + chatModel.controller.apiSetSettings(remoteHostId, chatType, apiId, groupInfo.chatSettings) } else -> false } if (res && newChatInfo != null) { - chatModel.updateChatInfo(chat.remoteHostId, newChatInfo) + withChats { + updateChatInfo(remoteHostId, newChatInfo) + } if (chatSettings.enableNtfs != MsgFilter.All) { - ntfManager.cancelNotificationsForChat(chat.id) + ntfManager.cancelNotificationsForChat(chatInfo.id) } val current = currentState?.value if (current != null) { @@ -885,7 +915,7 @@ fun PreviewChatListNavLinkDirect() { showMenu = remember { mutableStateOf(false) }, disabled = false, selectedChat = remember { mutableStateOf(false) }, - nextChatSelected = remember { mutableStateOf(false) } + nextChatSelected = remember { mutableStateOf(false) }, ) } } @@ -930,7 +960,7 @@ fun PreviewChatListNavLinkGroup() { showMenu = remember { mutableStateOf(false) }, disabled = false, selectedChat = remember { mutableStateOf(false) }, - nextChatSelected = remember { mutableStateOf(false) } + nextChatSelected = remember { mutableStateOf(false) }, ) } } @@ -952,7 +982,7 @@ fun PreviewChatListNavLinkContactRequest() { showMenu = remember { mutableStateOf(false) }, disabled = false, selectedChat = remember { mutableStateOf(false) }, - nextChatSelected = remember { mutableStateOf(false) } + nextChatSelected = remember { mutableStateOf(false) }, ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index d03b8a708d..11b1006f41 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -1,6 +1,8 @@ package chat.simplex.common.views.chatlist import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.* import androidx.compose.foundation.shape.CircleShape @@ -10,6 +12,7 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.* import androidx.compose.ui.graphics.* import androidx.compose.ui.text.font.FontStyle @@ -22,40 +25,125 @@ import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.unit.* import chat.simplex.common.SettingsViewState import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.onboarding.WhatsNewView import chat.simplex.common.views.onboarding.shouldShowWhatsNew -import chat.simplex.common.views.usersettings.SettingsView import chat.simplex.common.platform.* import chat.simplex.common.views.call.Call +import chat.simplex.common.views.chat.item.CIFileViewScope import chat.simplex.common.views.newchat.* +import chat.simplex.common.views.usersettings.* import chat.simplex.res.MR import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.serialization.json.Json import java.net.URI +import kotlin.time.Duration.Companion.seconds + +private fun showNewChatSheet(oneHandUI: State) { + ModalManager.start.closeModals() + ModalManager.end.closeModals() + chatModel.newChatSheetVisible.value = true + ModalManager.start.showCustomModal { close -> + val close = { + // It will set it faster than in onDispose. It's important to catch the actual state before + // closing modal for reacting with status bar changes in [App] + chatModel.newChatSheetVisible.value = false + close() + } + ModalView(close, closeOnTop = !oneHandUI.value) { + if (appPlatform.isAndroid) { + BackHandler { + close() + } + } + NewChatSheet(rh = chatModel.currentRemoteHost.value, close) + DisposableEffect(Unit) { + onDispose { + chatModel.newChatSheetVisible.value = false + } + } + } + } +} + +@Composable +fun ToggleChatListCard() { + Column( + modifier = Modifier + .padding(16.dp) + .clip(RoundedCornerShape(18.dp)) + ) { + Box( + modifier = Modifier + .background(MaterialTheme.appColors.sentMessage) + ) { + Box( + modifier = Modifier.fillMaxWidth().matchParentSize().padding(5.dp), + contentAlignment = Alignment.TopEnd + ) { + IconButton( + onClick = { + appPrefs.oneHandUICardShown.set(true) + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.one_hand_ui), + text = generalGetString(MR.strings.one_hand_ui_change_instruction), + ) + } + ) { + Icon( + painterResource(MR.images.ic_close), stringResource(MR.strings.back), tint = MaterialTheme.colors.secondary + ) + } + } + Column( + modifier = Modifier + .padding(horizontal = DEFAULT_PADDING) + .padding(top = DEFAULT_PADDING) + ) { + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(MR.strings.one_hand_ui_card_title), style = MaterialTheme.typography.h3) + } + Row( + Modifier.fillMaxWidth().padding(top = 6.dp, bottom = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text(stringResource(MR.strings.one_hand_ui), Modifier.weight(10f), style = MaterialTheme.typography.body1) + + Spacer(Modifier.fillMaxWidth().weight(1f)) + + SharedPreferenceToggle( + appPrefs.oneHandUI, + enabled = true, + onChange = { + val c = CurrentColors.value.colors + platform.androidSetStatusAndNavBarColors(c.isLight, c.background, !appPrefs.oneHandUI.get(), appPrefs.oneHandUI.get()) + } + ) + } + } + } + } +} @Composable fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerformLA: (Boolean) -> Unit, stopped: Boolean) { - val newChatSheetState by rememberSaveable(stateSaver = AnimatedViewState.saver()) { mutableStateOf(MutableStateFlow(AnimatedViewState.GONE)) } - val showNewChatSheet = { - newChatSheetState.value = AnimatedViewState.VISIBLE - } - val hideNewChatSheet: (animated: Boolean) -> Unit = { animated -> - if (animated) newChatSheetState.value = AnimatedViewState.HIDING - else newChatSheetState.value = AnimatedViewState.GONE - } + val oneHandUI = remember { appPrefs.oneHandUI.state } LaunchedEffect(Unit) { if (shouldShowWhatsNew(chatModel)) { delay(1000L) ModalManager.center.showCustomModal { close -> WhatsNewView(close = close) } } } - LaunchedEffect(chatModel.clearOverlays.value) { - if (chatModel.clearOverlays.value && newChatSheetState.value.isVisible()) hideNewChatSheet(false) - } + if (appPlatform.isDesktop) { KeyChangeEffect(chatModel.chatId.value) { if (chatModel.chatId.value != null) { @@ -69,11 +157,42 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } val scope = rememberCoroutineScope() val (userPickerState, scaffoldState ) = settingsState - Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(searchText, scaffoldState.drawerState, userPickerState, stopped)} }, + Scaffold( + topBar = { + if (!oneHandUI.value) { + Column(Modifier.padding(end = endPadding)) { + ChatListToolbar( + scaffoldState.drawerState, + userPickerState, + stopped, + ) + Divider() + } + } + }, + bottomBar = { + if (oneHandUI.value) { + Column(Modifier.padding(end = endPadding)) { + Divider() + ChatListToolbar( + scaffoldState.drawerState, + userPickerState, + stopped, + ) + } + } + }, scaffoldState = scaffoldState, drawerContent = { tryOrShowError("Settings", error = { ErrorSettingsView() }) { - SettingsView(chatModel, setPerformLA, scaffoldState.drawerState) + val handler = remember { AppBarHandler() } + CompositionLocalProvider( + LocalAppBarHandler provides handler + ) { + ModalView(showClose = appPlatform.isDesktop, close = { scope.launch { scaffoldState.drawerState.close() } }) { + SettingsView(chatModel, setPerformLA, scaffoldState.drawerState) + } + } } }, contentColor = LocalContentColor.current, @@ -81,14 +200,16 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf drawerScrimColor = MaterialTheme.colors.onSurface.copy(alpha = if (isInDarkTheme()) 0.16f else 0.32f), drawerGesturesEnabled = appPlatform.isAndroid, floatingActionButton = { - if (searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) { + if (!oneHandUI.value && searchText.value.text.isEmpty() && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == true) { FloatingActionButton( onClick = { if (!stopped) { - if (newChatSheetState.value.isVisible()) hideNewChatSheet(true) else showNewChatSheet() + showNewChatSheet(oneHandUI) } }, - Modifier.padding(end = DEFAULT_PADDING - 16.dp + endPadding, bottom = DEFAULT_PADDING - 16.dp), + Modifier + .padding(end = DEFAULT_PADDING - 16.dp + endPadding, bottom = DEFAULT_PADDING - 16.dp) + .size(AppBarHeight * fontSizeSqrtMultiplier), elevation = FloatingActionButtonDefaults.elevation( defaultElevation = 0.dp, pressedElevation = 0.dp, @@ -98,7 +219,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, contentColor = Color.White ) { - Icon(if (!newChatSheetState.collectAsState().value.isVisible()) painterResource(MR.images.ic_edit_filled) else painterResource(MR.images.ic_close), stringResource(MR.strings.add_contact_or_create_group)) + Icon(painterResource(MR.images.ic_edit_filled), stringResource(MR.strings.add_contact_or_create_group), Modifier.size(22.dp * fontSizeSqrtMultiplier)) } } } @@ -111,12 +232,9 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf if (!chatModel.desktopNoUserNoRemote) { ChatList(chatModel, searchText = searchText) } - if (chatModel.chats.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) { + if (chatModel.chats.value.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) { Text(stringResource( if (chatModel.chatRunning.value == null) MR.strings.loading_chats else MR.strings.you_have_no_chats), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary) - if (!stopped && !newChatSheetState.collectAsState().value.isVisible() && chatModel.chatRunning.value == true && searchText.value.text.isEmpty()) { - OnboardingButtons(showNewChatSheet) - } } } } @@ -125,17 +243,17 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf if (appPlatform.isDesktop) { val call = remember { chatModel.activeCall }.value if (call != null) { - ActiveCallInteractiveArea(call, newChatSheetState) + ActiveCallInteractiveArea(call) } } - // TODO disable this button and sheet for the duration of the switch - tryOrShowError("NewChatSheet", error = {}) { - NewChatSheet(chatModel, newChatSheetState, stopped, hideNewChatSheet) - } } if (appPlatform.isAndroid) { tryOrShowError("UserPicker", error = {}) { - UserPicker(chatModel, userPickerState) { + UserPicker( + chatModel = chatModel, + userPickerState = userPickerState, + contentAlignment = if (oneHandUI.value) Alignment.BottomStart else Alignment.TopStart + ) { scope.launch { if (scaffoldState.drawerState.isOpen) scaffoldState.drawerState.close() else scaffoldState.drawerState.open() } userPickerState.value = AnimatedViewState.GONE } @@ -143,27 +261,6 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf } } -@Composable -private fun OnboardingButtons(openNewChatSheet: () -> Unit) { - Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING), horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.Bottom) { - ConnectButton(generalGetString(MR.strings.tap_to_start_new_chat), openNewChatSheet) - val color = MaterialTheme.colors.primaryVariant - Canvas(modifier = Modifier.width(40.dp).height(10.dp), onDraw = { - val trianglePath = Path().apply { - moveTo(0.dp.toPx(), 0f) - lineTo(16.dp.toPx(), 0.dp.toPx()) - lineTo(8.dp.toPx(), 10.dp.toPx()) - lineTo(0.dp.toPx(), 0.dp.toPx()) - } - drawPath( - color = color, - path = trianglePath - ) - }) - Spacer(Modifier.height(62.dp)) - } -} - @Composable private fun ConnectButton(text: String, onClick: () -> Unit) { Button( @@ -181,9 +278,57 @@ private fun ConnectButton(text: String, onClick: () -> Unit) { } @Composable -private fun ChatListToolbar(searchInList: State, drawerState: DrawerState, userPickerState: MutableStateFlow, stopped: Boolean) { +private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableStateFlow, stopped: Boolean) { + val serversSummary: MutableState = remember { mutableStateOf(null) } val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() - if (stopped) { + val updatingProgress = remember { chatModel.updatingProgress }.value + val oneHandUI = remember { appPrefs.oneHandUI.state } + + if (oneHandUI.value) { + val sp16 = with(LocalDensity.current) { 16.sp.toDp() } + + if (!stopped) { + barButtons.add { + IconButton( + onClick = { + showNewChatSheet(oneHandUI) + }, + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .background(MaterialTheme.colors.primary, shape = CircleShape) + .size(33.dp * fontSizeSqrtMultiplier) + ) { + Icon( + painterResource(MR.images.ic_edit_filled), + stringResource(MR.strings.add_contact_or_create_group), + Modifier.size(sp16), + tint = Color.White + ) + } + } + } + } + } + + if (updatingProgress != null) { + barButtons.add { + val interactionSource = remember { MutableInteractionSource() } + val hovered = interactionSource.collectIsHoveredAsState().value + IconButton(onClick = { + chatModel.updatingRequest?.close() + }, Modifier.hoverable(interactionSource)) { + if (hovered) { + Icon(painterResource(MR.images.ic_close), null, tint = WarningOrange) + } else if (updatingProgress == -1f) { + CIFileViewScope.progressIndicator() + } else { + CIFileViewScope.progressCircle((updatingProgress * 100).toLong(), 100) + } + } + } + } else if (stopped) { barButtons.add { IconButton(onClick = { AlertManager.shared.showAlertMsg( @@ -200,6 +345,7 @@ private fun ChatListToolbar(searchInList: State, drawerState: Dr } } val scope = rememberCoroutineScope() + val clipboard = LocalClipboardManager.current DefaultTopAppBar( navigationButton = { if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) { @@ -219,20 +365,32 @@ private fun ChatListToolbar(searchInList: State, drawerState: Dr } }, title = { - Row(verticalAlignment = Alignment.CenterVertically) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON)) { Text( stringResource(MR.strings.your_chats), color = MaterialTheme.colors.onBackground, fontWeight = FontWeight.SemiBold, ) - if (chatModel.chats.size > 0) { - val enabled = remember { derivedStateOf { searchInList.value.text.isEmpty() } } - if (enabled.value) { - ToggleFilterEnabledButton() - } else { - ToggleFilterDisabledButton() + SubscriptionStatusIndicator( + click = { + ModalManager.start.closeModals() + ModalManager.start.showModalCloseable( + endButtons = { + val summary = serversSummary.value + if (summary != null) { + ShareButton { + val json = Json { + prettyPrint = true + } + + val text = json.encodeToString(PresentedServersSummary.serializer(), summary) + clipboard.shareText(text) + } + } + } + ) { ServersSummaryView(chatModel.currentRemoteHost.value, serversSummary) } } - } + ) } }, onTitleClick = null, @@ -240,7 +398,42 @@ private fun ChatListToolbar(searchInList: State, drawerState: Dr onSearchValueChanged = {}, buttons = barButtons ) - Divider(Modifier.padding(top = AppBarHeight)) +} + +@Composable +fun SubscriptionStatusIndicator(click: (() -> Unit)) { + var subs by remember { mutableStateOf(SMPServerSubs.newSMPServerSubs) } + var hasSess by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + + suspend fun setSubsTotal() { + if (chatModel.currentUser.value != null && chatModel.controller.hasChatCtrl() && chatModel.chatRunning.value == true) { + val r = chatModel.controller.getAgentSubsTotal(chatModel.remoteHostId()) + if (r != null) { + subs = r.first + hasSess = r.second + } + } + } + + LaunchedEffect(Unit) { + setSubsTotal() + scope.launch { + while (isActive) { + delay(1.seconds) + if ((appPlatform.isDesktop || chatModel.chatId.value == null) && !ModalManager.start.hasModalsOpen() && !ModalManager.fullscreen.hasModalsOpen() && isAppVisibleAndFocused()) { + setSubsTotal() + } + } + } + } + + SimpleButtonFrame( + click = click, + disabled = chatModel.chatRunning.value != true + ) { + SubscriptionStatusIndicatorView(subs = subs, hasSess = hasSess) + } } @Composable @@ -250,7 +443,7 @@ fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> U Box { ProfileImage( image = image, - size = 37.dp, + size = 37.dp * fontSizeSqrtMultiplier, color = MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f) ) if (!allRead) { @@ -270,6 +463,7 @@ fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> U } } + @Composable private fun BoxScope.unreadBadge(text: String? = "") { Text( @@ -289,38 +483,23 @@ private fun BoxScope.unreadBadge(text: String? = "") { private fun ToggleFilterEnabledButton() { val pref = remember { ChatController.appPrefs.showUnreadAndFavorites } IconButton(onClick = { pref.set(!pref.get()) }) { + val sp16 = with(LocalDensity.current) { 16.sp.toDp() } Icon( painterResource(MR.images.ic_filter_list), null, - tint = if (pref.state.value) MaterialTheme.colors.background else MaterialTheme.colors.primary, + tint = if (pref.state.value) MaterialTheme.colors.background else MaterialTheme.colors.secondary, modifier = Modifier .padding(3.dp) .background(color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50)) - .border(width = 1.dp, color = MaterialTheme.colors.primary, shape = RoundedCornerShape(50)) + .border(width = 1.dp, color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50)) .padding(3.dp) - .size(16.dp) + .size(sp16) ) } } @Composable -private fun ToggleFilterDisabledButton() { - IconButton({}, enabled = false) { - Icon( - painterResource(MR.images.ic_filter_list), - null, - tint = MaterialTheme.colors.secondary, - modifier = Modifier - .padding(3.dp) - .border(width = 1.dp, color = MaterialTheme.colors.secondary, shape = RoundedCornerShape(50)) - .padding(3.dp) - .size(16.dp) - ) - } -} - -@Composable -expect fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableStateFlow) +expect fun ActiveCallInteractiveArea(call: Call) fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) { Log.d(TAG, "connectIfOpenedViaUri: opened via link") @@ -335,10 +514,15 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: URI, chatModel: ChatModel) { @Composable private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState, searchShowingSimplexLink: MutableState, searchChatFilteredBySimplexLink: MutableState) { - Row(verticalAlignment = Alignment.CenterVertically) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { val focusRequester = remember { FocusRequester() } var focused by remember { mutableStateOf(false) } - Icon(painterResource(MR.images.ic_search), null, Modifier.padding(horizontal = DEFAULT_PADDING_HALF), tint = MaterialTheme.colors.secondary) + Icon( + painterResource(MR.images.ic_search), + contentDescription = null, + Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF).size(22.dp * fontSizeSqrtMultiplier), + tint = MaterialTheme.colors.secondary + ) SearchTextField( Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester), placeholder = stringResource(MR.strings.search_or_paste_simplex_link), @@ -357,33 +541,11 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState hideSearchOnBack() } } else { - Row { - val padding = if (appPlatform.isDesktop) 0.dp else 7.dp - val clipboard = LocalClipboardManager.current - val clipboardHasText = remember(focused) { chatModel.clipboardHasText }.value - if (clipboardHasText) { - IconButton( - onClick = { searchText.value = searchText.value.copy(clipboard.getText()?.text ?: return@IconButton) }, - Modifier.size(30.dp).desktopPointerHoverIconHand() - ) { - Icon(painterResource(MR.images.ic_article), null, tint = MaterialTheme.colors.secondary) - } - } - Spacer(Modifier.width(padding)) - IconButton( - onClick = { - val fixedRhId = chatModel.currentRemoteHost.value - ModalManager.center.closeModals() - ModalManager.center.showModalCloseable { close -> - NewChatView(fixedRhId, selection = NewChatOption.CONNECT, showQRCodeScanner = true, close = close) - } - }, - Modifier.size(30.dp).desktopPointerHoverIconHand() - ) { - Icon(painterResource(MR.images.ic_qr_code), null, tint = MaterialTheme.colors.secondary) - } - Spacer(Modifier.width(padding)) + val padding = if (appPlatform.isDesktop) 0.dp else 7.dp + if (chatModel.chats.value.isNotEmpty()) { + ToggleFilterEnabledButton() } + Spacer(Modifier.width(padding)) } val focusManager = LocalFocusManager.current val keyboardState = getKeyboardState() @@ -446,9 +608,38 @@ private fun ErrorSettingsView() { private var lazyListState = 0 to 0 +enum class ScrollDirection { + Up, Down, Idle +} + @Composable private fun ChatList(chatModel: ChatModel, searchText: MutableState) { val listState = rememberLazyListState(lazyListState.first, lazyListState.second) + var scrollDirection by remember { mutableStateOf(ScrollDirection.Idle) } + var previousIndex by remember { mutableStateOf(0) } + var previousScrollOffset by remember { mutableStateOf(0) } + val keyboardState by getKeyboardState() + val oneHandUI = remember { appPrefs.oneHandUI.state } + val oneHandUICardShown = remember { appPrefs.oneHandUICardShown.state } + + LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) { + val currentIndex = listState.firstVisibleItemIndex + val currentScrollOffset = listState.firstVisibleItemScrollOffset + val threshold = 25 + + scrollDirection = when { + currentIndex > previousIndex -> ScrollDirection.Down + currentIndex < previousIndex -> ScrollDirection.Up + currentScrollOffset > previousScrollOffset + threshold -> ScrollDirection.Down + currentScrollOffset < previousScrollOffset - threshold -> ScrollDirection.Up + currentScrollOffset == previousScrollOffset -> ScrollDirection.Idle + else -> scrollDirection + } + + previousIndex = currentIndex + previousScrollOffset = currentScrollOffset + } + DisposableEffect(Unit) { onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset } } @@ -459,43 +650,65 @@ private fun ChatList(chatModel: ChatModel, searchText: MutableState(null) } - val chats = filteredChats(showUnreadAndFavorites, searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.toList()) + val chats = filteredChats(showUnreadAndFavorites, searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.value.toList()) LazyColumnWithScrollBar( - Modifier.fillMaxWidth(), - listState + Modifier.fillMaxSize(), + listState, + reverseLayout = oneHandUI.value ) { stickyHeader { Column( Modifier .offset { val y = if (searchText.value.text.isEmpty()) { - if (listState.firstVisibleItemIndex == 0) -listState.firstVisibleItemScrollOffset else -1000 + val offsetMultiplier = if (oneHandUI.value) 1 else -1 + if ( + (oneHandUI.value && scrollDirection == ScrollDirection.Up) || + (appPlatform.isAndroid && keyboardState == KeyboardState.Opened) + ) { + 0 + } else if (listState.firstVisibleItemIndex == 0) offsetMultiplier * listState.firstVisibleItemScrollOffset else offsetMultiplier * 1000 } else { 0 } IntOffset(0, y) } - .background(MaterialTheme.colors.background) - ) { + .background(MaterialTheme.colors.background), + ) { + if (oneHandUI.value) { + Divider() + } ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink) - Divider() + if (!oneHandUI.value) { + Divider() + } } } - itemsIndexed(chats) { index, chat -> + if (appPlatform.isAndroid && !oneHandUICardShown.value && chats.count() > 1) { + item { + ToggleChatListCard() + } + } + itemsIndexed(chats, key = { _, chat -> chat.remoteHostId to chat.id }) { index, chat -> val nextChatSelected = remember(chat.id, chats) { derivedStateOf { chatModel.chatId.value != null && chats.getOrNull(index + 1)?.id == chatModel.chatId.value } } ChatListNavLinkView(chat, nextChatSelected) } + if (appPlatform.isAndroid && !oneHandUICardShown.value && chats.count() <= 1) { + item { + ToggleChatListCard() + } + } } - if (chats.isEmpty() && chatModel.chats.isNotEmpty()) { + if (chats.isEmpty() && chatModel.chats.value.isNotEmpty()) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Text(generalGetString(MR.strings.no_filtered_chats), color = MaterialTheme.colors.secondary) } } } -private fun filteredChats( +fun filteredChats( showUnreadAndFavorites: Boolean, searchShowingSimplexLink: State, searchChatFilteredBySimplexLink: State, @@ -508,25 +721,24 @@ private fun filteredChats( } else { val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase() if (s.isEmpty() && !showUnreadAndFavorites) - chats + chats.filter { chat -> !chat.chatInfo.chatDeleted && chatContactType(chat) != ContactType.CARD } else { chats.filter { chat -> when (val cInfo = chat.chatInfo) { - is ChatInfo.Direct -> if (s.isEmpty()) { - chat.id == chatModel.chatId.value || filtered(chat) - } else { - (viewNameContains(cInfo, s) || - cInfo.contact.profile.displayName.lowercase().contains(s) || - cInfo.contact.fullName.lowercase().contains(s)) - } + is ChatInfo.Direct -> chatContactType(chat) != ContactType.CARD && !chat.chatInfo.chatDeleted && ( + if (s.isEmpty()) { + chat.id == chatModel.chatId.value || filtered(chat) + } else { + cInfo.anyNameContains(s) + }) is ChatInfo.Group -> if (s.isEmpty()) { chat.id == chatModel.chatId.value || filtered(chat) || cInfo.groupInfo.membership.memberStatus == GroupMemberStatus.MemInvited } else { - viewNameContains(cInfo, s) + cInfo.anyNameContains(s) } - is ChatInfo.Local -> s.isEmpty() || viewNameContains(cInfo, s) - is ChatInfo.ContactRequest -> s.isEmpty() || viewNameContains(cInfo, s) - is ChatInfo.ContactConnection -> (s.isNotEmpty() && cInfo.contactConnection.localAlias.lowercase().contains(s)) || (s.isEmpty() && chat.id == chatModel.chatId.value) + is ChatInfo.Local -> s.isEmpty() || cInfo.anyNameContains(s) + is ChatInfo.ContactRequest -> s.isEmpty() || cInfo.anyNameContains(s) + is ChatInfo.ContactConnection -> (s.isNotEmpty() && cInfo.anyNameContains(s)) || (s.isEmpty() && chat.id == chatModel.chatId.value) is ChatInfo.InvalidJSON -> chat.id == chatModel.chatId.value } } @@ -538,6 +750,3 @@ private fun filtered(chat: Chat): Boolean = (chat.chatInfo.chatSettings?.favorite ?: false) || chat.chatStats.unreadChat || (chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0) - -private fun viewNameContains(cInfo: ChatInfo, s: String): Boolean = - cInfo.chatViewName.lowercase().contains(s.lowercase()) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index 336d104d2d..0edaf89974 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -1,6 +1,5 @@ package chat.simplex.common.views.chatlist -import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.InlineTextContent @@ -15,18 +14,22 @@ import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.draw.clip +import androidx.compose.ui.input.pointer.pointerHoverIcon +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.style.* import androidx.compose.ui.unit.* import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.chat.ComposePreview -import chat.simplex.common.views.chat.ComposeState -import chat.simplex.common.views.chat.item.MarkdownText import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* import chat.simplex.common.model.GroupInfo -import chat.simplex.common.platform.chatModel -import chat.simplex.common.views.chat.item.markedDeletedText +import chat.simplex.common.platform.* +import chat.simplex.common.views.chat.* +import chat.simplex.common.views.chat.item.* import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource @@ -50,7 +53,7 @@ fun ChatPreviewView( Icon( painterResource(MR.images.ic_cancel_filled), stringResource(MR.strings.icon_descr_group_inactive), - Modifier.size(18.dp).background(MaterialTheme.colors.background, CircleShape), + Modifier.size(18.sp.toDp()).background(MaterialTheme.colors.background, CircleShape), tint = MaterialTheme.colors.secondary ) } @@ -87,10 +90,10 @@ fun ChatPreviewView( @Composable fun VerifiedIcon() { - Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(19.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary) + Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(19.sp.toDp()).padding(end = 3.sp.toDp(), top = 1.sp.toDp()), tint = MaterialTheme.colors.secondary) } - fun messageDraft(draft: ComposeState): Pair Unit, Map> { + fun messageDraft(draft: ComposeState, sp20: Dp): Pair Unit, Map> { fun attachment(): Pair? = when (draft.preview) { is ComposePreview.FilePreview -> MR.images.ic_draft_filled to draft.preview.fileName @@ -115,12 +118,12 @@ fun ChatPreviewView( "editIcon" to InlineTextContent( Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter) ) { - Icon(painterResource(MR.images.ic_edit_note), null, tint = MaterialTheme.colors.primary) + Icon(painterResource(MR.images.ic_edit_note), null, Modifier.size(sp20), tint = MaterialTheme.colors.primary) }, "attachmentIcon" to InlineTextContent( Placeholder(20.sp, 20.sp, PlaceholderVerticalAlign.TextCenter) ) { - Icon(if (attachment?.first != null) painterResource(attachment.first) else painterResource(MR.images.ic_edit_note), null, tint = MaterialTheme.colors.secondary) + Icon(if (attachment?.first != null) painterResource(attachment.first) else painterResource(MR.images.ic_edit_note), null, Modifier.size(sp20), tint = MaterialTheme.colors.secondary) } ) return inlineContentBuilder to inlineContent @@ -167,8 +170,9 @@ fun ChatPreviewView( val ci = chat.chatItems.lastOrNull() if (ci != null) { if (showChatPreviews || (chatModelDraftChatId == chat.id && chatModelDraft != null)) { + val sp20 = with(LocalDensity.current) { 20.sp.toDp() } val (text: CharSequence, inlineTextContent) = when { - chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { chatModelDraft.message to messageDraft(chatModelDraft) } + chatModelDraftChatId == chat.id && chatModelDraft != null -> remember(chatModelDraft) { chatModelDraft.message to messageDraft(chatModelDraft, sp20) } ci.meta.itemDeleted == null -> ci.text to null else -> markedDeletedText(ci.meta) to null } @@ -190,7 +194,12 @@ fun ChatPreviewView( senderBold = true, maxLines = 2, overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.body1.copy(color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, lineHeight = 22.sp), + style = TextStyle( + fontFamily = Inter, + fontSize = 15.sp, + color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, + lineHeight = 21.sp + ), inlineContent = inlineTextContent, modifier = Modifier.fillMaxWidth(), ) @@ -198,9 +207,9 @@ fun ChatPreviewView( } else { when (cInfo) { is ChatInfo.Direct -> - if (cInfo.contact.activeConn == null && cInfo.contact.profile.contactLink != null) { + if (cInfo.contact.activeConn == null && cInfo.contact.profile.contactLink != null && cInfo.contact.active) { Text(stringResource(MR.strings.contact_tap_to_connect), color = MaterialTheme.colors.primary) - } else if (!cInfo.ready && cInfo.contact.activeConn != null) { + } else if (!cInfo.contact.sndReady && cInfo.contact.activeConn != null) { if (cInfo.contact.nextSendGrpInv) { Text(stringResource(MR.strings.member_contact_send_direct_message), color = MaterialTheme.colors.secondary) } else if (cInfo.contact.active) { @@ -218,12 +227,56 @@ fun ChatPreviewView( } } + @Composable + fun chatItemContentPreview(chat: Chat, ci: ChatItem?) { + val mc = ci?.content?.msgContent + val provider by remember(chat.id, ci?.id, ci?.file?.fileStatus) { + mutableStateOf({ providerForGallery(0, chat.chatItems, ci?.id ?: 0) {} }) + } + val uriHandler = LocalUriHandler.current + when (mc) { + is MsgContent.MCLink -> SmallContentPreview { + IconButton({ uriHandler.openUriCatching(mc.preview.uri) }, Modifier.desktopPointerHoverIconHand()) { + Image(base64ToBitmap(mc.preview.image), null, contentScale = ContentScale.Crop) + } + Box(Modifier.align(Alignment.TopEnd).size(15.sp.toDp()).background(Color.Black.copy(0.25f), CircleShape), contentAlignment = Alignment.Center) { + Icon(painterResource(MR.images.ic_arrow_outward), null, Modifier.size(13.sp.toDp()), tint = Color.White) + } + } + is MsgContent.MCImage -> SmallContentPreview { + CIImageView(image = mc.image, file = ci.file, provider, remember { mutableStateOf(false) }, smallView = true) { + val user = chatModel.currentUser.value ?: return@CIImageView + withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) } + } + } + is MsgContent.MCVideo -> SmallContentPreview { + CIVideoView(image = mc.image, mc.duration, file = ci.file, provider, remember { mutableStateOf(false) }, smallView = true) { + val user = chatModel.currentUser.value ?: return@CIVideoView + withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) } + } + } + is MsgContent.MCVoice -> SmallContentPreviewVoice() { + CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = false, ci, cInfo.timedMessagesTTL, showViaProxy = false, smallView = true, longClick = {}) { + val user = chatModel.currentUser.value ?: return@CIVoiceView + withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) } + } + } + is MsgContent.MCFile -> SmallContentPreviewFile { + CIFileView(ci.file, false, remember { mutableStateOf(false) }, smallView = true) { + val user = chatModel.currentUser.value ?: return@CIFileView + withBGApi { chatModel.controller.receiveFile(chat.remoteHostId, user, it) } + } + } + else -> {} + } + } + @Composable fun progressView() { CircularProgressIndicator( Modifier - .padding(horizontal = 2.dp) - .size(15.dp), + .size(15.sp.toDp()) + .offset(y = 2.sp.toDp()), color = MaterialTheme.colors.secondary, strokeWidth = 1.5.dp ) @@ -244,7 +297,8 @@ fun ChatPreviewView( contentDescription = descr, tint = MaterialTheme.colors.secondary, modifier = Modifier - .size(19.dp) + .size(19.sp.toDp()) + .offset(x = 2.sp.toDp()) ) else -> @@ -266,92 +320,129 @@ fun ChatPreviewView( Row { Box(contentAlignment = Alignment.BottomEnd) { - ChatInfoImage(cInfo, size = 72.dp) - Box(Modifier.padding(end = 6.dp, bottom = 6.dp)) { + ChatInfoImage(cInfo, size = 72.dp * fontSizeSqrtMultiplier) + Box(Modifier.padding(end = 6.sp.toDp(), bottom = 6.sp.toDp())) { chatPreviewImageOverlayIcon() } } - Column( - modifier = Modifier - .padding(horizontal = 8.dp) - .weight(1F) - ) { - chatPreviewTitle() - val height = with(LocalDensity.current) { 46.sp.toDp() } - Row(Modifier.heightIn(min = height)) { - chatPreviewText() + Spacer(Modifier.width(8.dp)) + Column(Modifier.weight(1f)) { + Row { + Box(Modifier.weight(1f)) { + chatPreviewTitle() + } + Spacer(Modifier.width(8.sp.toDp())) + val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.chatTs) + ChatListTimestampView(ts) } - } + Row(Modifier.heightIn(min = 46.sp.toDp()).fillMaxWidth()) { + Row(Modifier.padding(top = 3.sp.toDp()).weight(1f)) { + val activeVoicePreview: MutableState<(ActiveVoicePreview)?> = remember(chat.id) { mutableStateOf(null) } + val chat = activeVoicePreview.value?.chat ?: chat + val ci = activeVoicePreview.value?.ci ?: chat.chatItems.lastOrNull() + val mc = ci?.content?.msgContent + val deleted = ci?.isDeletedContent == true || ci?.meta?.itemDeleted != null + val showContentPreview = (showChatPreviews && chatModelDraftChatId != chat.id && !deleted) || activeVoicePreview.value != null + if (ci != null && showContentPreview) { + chatItemContentPreview(chat, ci) + } + if (mc !is MsgContent.MCVoice || !showContentPreview || mc.text.isNotEmpty() || chatModelDraftChatId == chat.id) { + Box(Modifier.offset(x = if (mc is MsgContent.MCFile) -15.sp.toDp() else 0.dp)) { + chatPreviewText() + } + } + LaunchedEffect(AudioPlayer.currentlyPlaying.value, activeVoicePreview.value) { + val playing = AudioPlayer.currentlyPlaying.value + when { + playing == null -> activeVoicePreview.value = null + activeVoicePreview.value == null -> if (mc is MsgContent.MCVoice && playing.fileSource.filePath == ci.file?.fileSource?.filePath) { + activeVoicePreview.value = ActiveVoicePreview(chat, ci, mc) + } + else -> if (playing.fileSource.filePath != ci?.file?.fileSource?.filePath) { + activeVoicePreview.value = null + } + } + } + LaunchedEffect(chatModel.deletedChats.value) { + val voicePreview = activeVoicePreview.value + // Stop voice when deleting the chat + if (chatModel.deletedChats.value.contains(chatModel.remoteHostId() to chat.id) && voicePreview?.ci != null) { + AudioPlayer.stop(voicePreview.ci) + } + } + } - Box( - contentAlignment = Alignment.TopEnd - ) { - val ts = chat.chatItems.lastOrNull()?.timestampText ?: getTimestampText(chat.chatInfo.chatTs) - Text( - ts, - color = MaterialTheme.colors.secondary, - style = MaterialTheme.typography.body2, - modifier = Modifier.padding(bottom = 5.dp) - ) - val n = chat.chatStats.unreadCount - val showNtfsIcon = !chat.chatInfo.ntfsEnabled && (chat.chatInfo is ChatInfo.Direct || chat.chatInfo is ChatInfo.Group) - if (n > 0 || chat.chatStats.unreadChat) { - Box( - Modifier.padding(top = 24.dp), - contentAlignment = Alignment.Center - ) { - Text( - if (n > 0) unreadCountStr(n) else "", - color = Color.White, - fontSize = 11.sp, - modifier = Modifier - .background(if (disabled || showNtfsIcon) MaterialTheme.colors.secondary else MaterialTheme.colors.primaryVariant, shape = CircleShape) - .badgeLayout() - .padding(horizontal = 3.dp) - .padding(vertical = 1.dp) - ) + Spacer(Modifier.width(8.sp.toDp())) + + Box(Modifier.widthIn(min = 34.sp.toDp()), contentAlignment = Alignment.TopEnd) { + val n = chat.chatStats.unreadCount + val showNtfsIcon = !chat.chatInfo.ntfsEnabled && (chat.chatInfo is ChatInfo.Direct || chat.chatInfo is ChatInfo.Group) + if (n > 0 || chat.chatStats.unreadChat) { + Text( + if (n > 0) unreadCountStr(n) else "", + color = Color.White, + fontSize = 10.sp, + style = TextStyle(textAlign = TextAlign.Center), + modifier = Modifier + .offset(y = 3.sp.toDp()) + .background(if (disabled || showNtfsIcon) MaterialTheme.colors.secondary else MaterialTheme.colors.primaryVariant, shape = CircleShape) + .badgeLayout() + .padding(horizontal = 2.sp.toDp()) + .padding(vertical = 1.sp.toDp()) + ) + } else if (showNtfsIcon) { + Icon( + painterResource(MR.images.ic_notifications_off_filled), + contentDescription = generalGetString(MR.strings.notifications), + tint = MaterialTheme.colors.secondary, + modifier = Modifier + .padding(start = 2.sp.toDp()) + .size(18.sp.toDp()) + .offset(x = 2.5.sp.toDp(), y = 2.sp.toDp()) + ) + } else if (chat.chatInfo.chatSettings?.favorite == true) { + Icon( + painterResource(MR.images.ic_star_filled), + contentDescription = generalGetString(MR.strings.favorite_chat), + tint = MaterialTheme.colors.secondary, + modifier = Modifier + .size(20.sp.toDp()) + .offset(x = 2.5.sp.toDp()) + ) + } + Box( + Modifier.offset(y = 28.sp.toDp()), + contentAlignment = Alignment.Center + ) { + chatStatusImage() + } } - } else if (showNtfsIcon) { - Box( - Modifier.padding(top = 24.dp), - contentAlignment = Alignment.Center - ) { - Icon( - painterResource(MR.images.ic_notifications_off_filled), - contentDescription = generalGetString(MR.strings.notifications), - tint = MaterialTheme.colors.secondary, - modifier = Modifier - .padding(horizontal = 3.dp) - .padding(vertical = 1.dp) - .size(17.dp) - ) - } - } else if (chat.chatInfo.chatSettings?.favorite == true) { - Box( - Modifier.padding(top = 24.dp), - contentAlignment = Alignment.Center - ) { - Icon( - painterResource(MR.images.ic_star_filled), - contentDescription = generalGetString(MR.strings.favorite_chat), - tint = MaterialTheme.colors.secondary, - modifier = Modifier - .padding(horizontal = 3.dp) - .padding(vertical = 1.dp) - .size(17.dp) - ) - } - } - Box( - Modifier.padding(top = 50.dp), - contentAlignment = Alignment.Center - ) { - chatStatusImage() } } } } +@Composable +private fun SmallContentPreview(content: @Composable BoxScope.() -> Unit) { + Box(Modifier.padding(top = 2.sp.toDp(), end = 8.sp.toDp()).size(36.sp.toDp()).border(1.dp, MaterialTheme.colors.onSurface.copy(alpha = 0.12f), RoundedCornerShape(22)).clip(RoundedCornerShape(22))) { + content() + } +} + +@Composable +private fun SmallContentPreviewVoice(content: @Composable () -> Unit) { + Box(Modifier.padding(top = 2.sp.toDp(), end = 8.sp.toDp()).height(voiceMessageSizeBasedOnSquareSize(36f).sp.toDp())) { + content() + } +} + +@Composable +private fun SmallContentPreviewFile(content: @Composable () -> Unit) { + Box(Modifier.padding(top = 3.sp.toDp(), end = 8.sp.toDp()).offset(x = -8.sp.toDp(), y = -4.sp.toDp()).height(41.sp.toDp())) { + content() + } +} + @Composable fun IncognitoIcon(incognito: Boolean) { if (incognito) { @@ -360,7 +451,8 @@ fun IncognitoIcon(incognito: Boolean) { contentDescription = null, tint = MaterialTheme.colors.secondary, modifier = Modifier - .size(21.dp) + .size(21.sp.toDp()) + .offset(x = 1.sp.toDp()) ) } } @@ -378,6 +470,29 @@ fun unreadCountStr(n: Int): String { return if (n < 1000) "$n" else "${n / 1000}" + stringResource(MR.strings.thousand_abbreviation) } +@Composable fun ChatListTimestampView(ts: String) { + Box(contentAlignment = Alignment.BottomStart) { + // This should be the same font style as in title to make date located on the same line as title + Text( + " ", + style = MaterialTheme.typography.h3, + fontWeight = FontWeight.Bold, + ) + Text( + ts, + Modifier.padding(bottom = 5.sp.toDp()).offset(x = if (appPlatform.isDesktop) 1.5.sp.toDp() else 0.dp), + color = MaterialTheme.colors.secondary, + style = MaterialTheme.typography.body2.copy(fontSize = 13.sp), + ) + } +} + +private data class ActiveVoicePreview( + val chat: Chat, + val ci: ChatItem, + val mc: MsgContent.MCVoice +) + @Preview/*( uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactConnectionView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactConnectionView.kt index 99d6c5db15..349f1f12d4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactConnectionView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactConnectionView.kt @@ -1,5 +1,6 @@ package chat.simplex.common.views.chatlist +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.MaterialTheme import androidx.compose.material.Text @@ -7,25 +8,26 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.helpers.ProfileImage import chat.simplex.common.model.PendingContactConnection import chat.simplex.common.model.getTimestampText +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR @Composable fun ContactConnectionView(contactConnection: PendingContactConnection) { Row { - Box(Modifier.size(72.dp), contentAlignment = Alignment.Center) { - ProfileImage(size = 54.dp, null, if (contactConnection.initiated) MR.images.ic_add_link else MR.images.ic_link) + Box(Modifier.size(72.dp * fontSizeSqrtMultiplier), contentAlignment = Alignment.Center) { + ProfileImage(size = 54.dp * fontSizeSqrtMultiplier, null, if (contactConnection.initiated) MR.images.ic_add_link else MR.images.ic_link) } Column( modifier = Modifier - .padding(horizontal = 8.dp) + .padding(start = 8.dp / fontSizeSqrtMultiplier, end = 8.sp.toDp()) .weight(1F) ) { Text( @@ -36,21 +38,25 @@ fun ContactConnectionView(contactConnection: PendingContactConnection) { fontWeight = FontWeight.Bold, color = MaterialTheme.colors.secondary ) - val height = with(LocalDensity.current) { 46.sp.toDp() } - Text(contactConnection.description, Modifier.heightIn(min = height), maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight) + Text( + contactConnection.description, + Modifier.heightIn(min = 46.sp.toDp()).padding(top = 3.sp.toDp()), + maxLines = 2, + style = TextStyle( + fontFamily = Inter, + fontSize = 15.sp, + color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, + lineHeight = 21.sp + ) + ) } Box( contentAlignment = Alignment.TopEnd ) { val ts = getTimestampText(contactConnection.updatedAt) - Text( - ts, - color = MaterialTheme.colors.secondary, - style = MaterialTheme.typography.body2, - modifier = Modifier.padding(bottom = 5.dp) - ) + ChatListTimestampView(ts) Box( - Modifier.padding(top = 50.dp), + Modifier.padding(top = 50.sp.toDp()), contentAlignment = Alignment.Center ) { IncognitoIcon(contactConnection.incognito) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactRequestView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactRequestView.kt index 8debcce98c..901761f65c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactRequestView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ContactRequestView.kt @@ -6,24 +6,25 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextStyle import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.helpers.ChatInfoImage import chat.simplex.common.model.ChatInfo import chat.simplex.common.model.getTimestampText +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR @Composable fun ContactRequestView(contactRequest: ChatInfo.ContactRequest) { Row { - ChatInfoImage(contactRequest, size = 72.dp) + ChatInfoImage(contactRequest, size = 72.dp * fontSizeSqrtMultiplier) Column( modifier = Modifier - .padding(horizontal = 8.dp) + .padding(start = 8.dp, end = 8.sp.toDp()) .weight(1F) ) { Text( @@ -32,21 +33,21 @@ fun ContactRequestView(contactRequest: ChatInfo.ContactRequest) { overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.h3, fontWeight = FontWeight.Bold, - color = MaterialTheme.colors.primary + color = MaterialTheme.colors.primary, + ) + Text( + stringResource(MR.strings.contact_wants_to_connect_with_you), + Modifier.heightIn(min = 46.sp.toDp()).padding(top = 3.sp.toDp()), + maxLines = 2, + style = TextStyle( + fontFamily = Inter, + fontSize = 15.sp, + color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight, + lineHeight = 21.sp + ) ) - val height = with(LocalDensity.current) { 46.sp.toDp() } - Text(stringResource(MR.strings.contact_wants_to_connect_with_you), Modifier.heightIn(min = height), maxLines = 2, color = if (isInDarkTheme()) MessagePreviewDark else MessagePreviewLight) } val ts = getTimestampText(contactRequest.contactRequest.updatedAt) - Column( - Modifier.fillMaxHeight(), - ) { - Text( - ts, - color = MaterialTheme.colors.secondary, - style = MaterialTheme.typography.body2, - modifier = Modifier.padding(bottom = 5.dp) - ) - } + ChatListTimestampView(ts) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt new file mode 100644 index 0000000000..6219252b54 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt @@ -0,0 +1,1003 @@ +package chat.simplex.common.views.chatlist + +import InfoRow +import InfoRowTwoValues +import SectionBottomSpacer +import SectionDividerSpaced +import SectionItemView +import SectionTextFooter +import SectionView +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.common.model.AgentSMPServerStatsData +import chat.simplex.common.model.AgentXFTPServerStatsData +import chat.simplex.common.model.ChatController.chatModel +import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.model.PresentedServersSummary +import chat.simplex.common.model.RemoteHostInfo +import chat.simplex.common.model.SMPServerSubs +import chat.simplex.common.model.SMPServerSummary +import chat.simplex.common.model.SMPTotals +import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress +import chat.simplex.common.model.ServerProtocol +import chat.simplex.common.model.ServerSessions +import chat.simplex.common.model.XFTPServerSummary +import chat.simplex.common.model.localTimestamp +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.usersettings.ProtocolServersView +import chat.simplex.common.views.usersettings.SettingsPreferenceItem +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.* +import kotlinx.datetime.Instant +import numOrDash +import java.text.DecimalFormat +import kotlin.math.floor +import kotlin.math.roundToInt +import kotlin.time.Duration.Companion.seconds + +enum class SubscriptionColorType { + ACTIVE, ACTIVE_SOCKS_PROXY, DISCONNECTED, ACTIVE_DISCONNECTED +} + +data class SubscriptionStatus( + val color: SubscriptionColorType, + val variableValue: Float, + val statusPercent: Float +) + +fun subscriptionStatusColorAndPercentage( + online: Boolean, + socksProxy: String?, + subs: SMPServerSubs, + hasSess: Boolean +): SubscriptionStatus { + fun roundedToQuarter(n: Float): Float = when { + n >= 1 -> 1f + n <= 0 -> 0f + else -> (n * 4).roundToInt() / 4f + } + + val activeColor: SubscriptionColorType = if (socksProxy != null) SubscriptionColorType.ACTIVE_SOCKS_PROXY else SubscriptionColorType.ACTIVE + val noConnColorAndPercent = SubscriptionStatus(SubscriptionColorType.DISCONNECTED, 1f, 0f) + val activeSubsRounded = roundedToQuarter(subs.shareOfActive) + + return if (!online) + noConnColorAndPercent + else if (subs.total == 0 && !hasSess) + // On freshly installed app (without chats) and on app start + SubscriptionStatus(activeColor, 0f, 0f) + else if (subs.ssActive == 0) { + if (hasSess) + SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive) + else + noConnColorAndPercent + } else { // ssActive > 0 + if (hasSess) + SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive) + else + // This would mean implementation error + SubscriptionStatus(SubscriptionColorType.ACTIVE_DISCONNECTED, activeSubsRounded, subs.shareOfActive) + } +} + +@Composable +private fun SubscriptionStatusIndicatorPercentage(percentageText: String) { + Text( + percentageText, + color = MaterialTheme.colors.secondary, + fontSize = 12.sp, + style = MaterialTheme.typography.caption + ) +} + +@Composable +fun SubscriptionStatusIndicatorView(subs: SMPServerSubs, hasSess: Boolean, leadingPercentage: Boolean = false) { + val netCfg = rememberUpdatedState(chatModel.controller.getNetCfg()) + val statusColorAndPercentage = subscriptionStatusColorAndPercentage(chatModel.networkInfo.value.online, netCfg.value.socksProxy, subs, hasSess) + val pref = remember { chatModel.controller.appPrefs.networkShowSubscriptionPercentage } + val percentageText = "${(floor(statusColorAndPercentage.statusPercent * 100)).toInt()}%" + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON) + ) { + if (pref.state.value && leadingPercentage) SubscriptionStatusIndicatorPercentage(percentageText) + val sp16 = with(LocalDensity.current) { 16.sp.toDp() } + SubscriptionStatusIcon( + color = when(statusColorAndPercentage.color) { + SubscriptionColorType.ACTIVE -> MaterialTheme.colors.primary + SubscriptionColorType.ACTIVE_SOCKS_PROXY -> Indigo + SubscriptionColorType.ACTIVE_DISCONNECTED -> WarningOrange + SubscriptionColorType.DISCONNECTED -> MaterialTheme.colors.secondary + }, + modifier = Modifier.size(sp16), + variableValue = statusColorAndPercentage.variableValue) + if (pref.state.value && !leadingPercentage) SubscriptionStatusIndicatorPercentage(percentageText) + } +} + +enum class PresentedUserCategory { + CURRENT_USER, ALL_USERS +} + +enum class PresentedServerType { + SMP, XFTP +} + +@Composable +private fun ServerSessionsView(sess: ServerSessions) { + SectionView(generalGetString(MR.strings.servers_info_transport_sessions_section_header).uppercase()) { + InfoRow( + generalGetString(MR.strings.servers_info_sessions_connected), + numOrDash(sess.ssConnected) + ) + InfoRow( + generalGetString(MR.strings.servers_info_sessions_errors), + numOrDash(sess.ssErrors) + ) + InfoRow( + generalGetString(MR.strings.servers_info_sessions_connecting), + numOrDash(sess.ssConnecting) + ) + } +} + +private fun serverAddress(server: String): String { + val address = parseServerAddress(server) + + return address?.hostnames?.first() ?: server +} + +@Composable +private fun SMPServerView(srvSumm: SMPServerSummary, statsStartedAt: Instant, rh: RemoteHostInfo?) { + SectionItemView( + click = { + ModalManager.start.showCustomModal { close -> + SMPServerSummaryView( + rh = rh, + close = close, + summary = srvSumm, + statsStartedAt = statsStartedAt + ) + } + } + ) { + Text( + serverAddress(srvSumm.smpServer), + modifier = Modifier.weight(10f, fill = true) + ) + if (srvSumm.subs != null) { + Spacer(Modifier.fillMaxWidth().weight(1f)) + SubscriptionStatusIndicatorView(subs = srvSumm.subs, hasSess = srvSumm.sessionsOrNew.hasSess, leadingPercentage = true) + } else if (srvSumm.sessions != null) { + Spacer(Modifier.fillMaxWidth().weight(1f)) + Icon(painterResource(MR.images.ic_arrow_upward), contentDescription = null, tint = SessIconColor(srvSumm.sessions)) + } + } +} + +@Composable +private fun SessIconColor(sess: ServerSessions): Color { + val online = chatModel.networkInfo.value.online + return if (online && sess.ssConnected > 0) SessionActiveColor() else MaterialTheme.colors.secondary +} + +@Composable +private fun SessionActiveColor(): Color { + val netCfg = rememberUpdatedState(chatModel.controller.getNetCfg()) + return if (netCfg.value.socksProxy != null) Indigo else MaterialTheme.colors.primary +} + +@Composable +private fun SMPServersListView(servers: List, statsStartedAt: Instant, header: String? = null, footer: String? = null, rh: RemoteHostInfo?) { + val sortedServers = servers.sortedWith(compareBy { !it.hasSubs } + .thenBy { serverAddress(it.smpServer) }) + + SectionView(header) { + sortedServers.map { svr -> SMPServerView(srvSumm = svr, statsStartedAt = statsStartedAt, rh = rh) } + } + if (footer != null) { + SectionTextFooter( + footer + ) + } +} + +fun prettySize(sizeInKB: Long): String { + if (sizeInKB == 0L) { + return "-" + } + + val sizeInBytes = sizeInKB * 1024 + val units = arrayOf("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB") + var size = sizeInBytes.toDouble() + var unitIndex = 0 + + while (size >= 1024 && unitIndex < units.size - 1) { + size /= 1024 + unitIndex++ + } + + val formatter = DecimalFormat("#,##0.#") + return "${formatter.format(size)} ${units[unitIndex]}" +} + +@Composable +private fun XFTPServerView(srvSumm: XFTPServerSummary, statsStartedAt: Instant, rh: RemoteHostInfo?) { + SectionItemView( + click = { + ModalManager.start.showCustomModal { close -> + XFTPServerSummaryView( + rh = rh, + close = close, + summary = srvSumm, + statsStartedAt = statsStartedAt + ) + } + } + ) { + Text( + serverAddress(srvSumm.xftpServer), + modifier = Modifier.weight(10f, fill = true) + ) + if (srvSumm.rcvInProgress || srvSumm.sndInProgress || srvSumm.delInProgress) { + Spacer(Modifier.fillMaxWidth().weight(1f)) + XFTPServerInProgressIcon(srvSumm) + } + } +} + +@Composable +private fun XFTPServerInProgressIcon(srvSumm: XFTPServerSummary) { + return when { + srvSumm.rcvInProgress && !srvSumm.sndInProgress && !srvSumm.delInProgress -> Icon(painterResource(MR.images.ic_arrow_downward),"download", tint = SessionActiveColor()) + !srvSumm.rcvInProgress && srvSumm.sndInProgress && !srvSumm.delInProgress -> Icon(painterResource(MR.images.ic_arrow_upward), "upload", tint = SessionActiveColor()) + !srvSumm.rcvInProgress && !srvSumm.sndInProgress && srvSumm.delInProgress -> Icon(painterResource(MR.images.ic_delete), "deletion", tint = SessionActiveColor()) + else -> Icon(painterResource(MR.images.ic_expand_all), "upload and download", tint = SessionActiveColor()) + } +} + +@Composable +private fun XFTPServersListView(servers: List, statsStartedAt: Instant, header: String? = null, rh: RemoteHostInfo?) { + val sortedServers = servers.sortedBy { serverAddress(it.xftpServer) } + + SectionView(header) { + sortedServers.map { svr -> XFTPServerView(svr, statsStartedAt, rh) } + } +} + +@Composable +private fun SMPStatsView(stats: AgentSMPServerStatsData, statsStartedAt: Instant, remoteHostInfo: RemoteHostInfo?) { + SectionView(generalGetString(MR.strings.servers_info_statistics_section_header).uppercase()) { + InfoRow( + generalGetString(MR.strings.servers_info_messages_sent), + numOrDash(stats._sentDirect + stats._sentViaProxy) + ) + InfoRow( + generalGetString(MR.strings.servers_info_messages_received), + numOrDash(stats._recvMsgs) + ) + SectionItemView( + click = { + ModalManager.start.showCustomModal { close -> DetailedSMPStatsView( + rh = remoteHostInfo, + close = close, + stats = stats, + statsStartedAt = statsStartedAt) + } + } + ) { + Text(text = generalGetString(MR.strings.servers_info_details), color = MaterialTheme.colors.onBackground) + } + } + SectionTextFooter( + String.format(stringResource(MR.strings.servers_info_private_data_disclaimer), localTimestamp(statsStartedAt)) + ) +} + +@Composable +private fun SMPSubscriptionsSection(totals: SMPTotals) { + Column { + Row( + Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON * 2) + ) { + Text( + generalGetString(MR.strings.servers_info_subscriptions_section_header).uppercase(), + color = MaterialTheme.colors.secondary, + style = MaterialTheme.typography.body2, + fontSize = 12.sp + ) + SubscriptionStatusIndicatorView(totals.subs, totals.sessions.hasSess) + } + Column(Modifier.padding(PaddingValues()).fillMaxWidth()) { + InfoRow( + generalGetString(MR.strings.servers_info_subscriptions_connections_subscribed), + numOrDash(totals.subs.ssActive) + ) + InfoRow( + generalGetString(MR.strings.servers_info_subscriptions_total), + numOrDash(totals.subs.total) + ) + SettingsPreferenceItem(null, stringResource(MR.strings.subscription_percentage), chatModel.controller.appPrefs.networkShowSubscriptionPercentage) + } + } +} + +@Composable +private fun SMPSubscriptionsSection(subs: SMPServerSubs, summary: SMPServerSummary, rh: RemoteHostInfo?) { + Column { + Row( + Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON * 2) + ) { + Text( + generalGetString(MR.strings.servers_info_subscriptions_section_header).uppercase(), + color = MaterialTheme.colors.secondary, + style = MaterialTheme.typography.body2, + fontSize = 12.sp + ) + SubscriptionStatusIndicatorView(subs, summary.sessionsOrNew.hasSess) + } + Column(Modifier.padding(PaddingValues()).fillMaxWidth()) { + InfoRow( + generalGetString(MR.strings.servers_info_subscriptions_connections_subscribed), + numOrDash(subs.ssActive) + ) + InfoRow( + generalGetString(MR.strings.servers_info_subscriptions_connections_pending), + numOrDash(subs.ssPending) + ) + InfoRow( + generalGetString(MR.strings.servers_info_subscriptions_total), + numOrDash(subs.total) + ) + ReconnectServerButton(rh, summary.smpServer) + } + } +} + +@Composable +private fun ReconnectServerButton(rh: RemoteHostInfo?, server: String) { + SectionItemView(click = { reconnectServerAlert(rh, server) }) { + Text( + stringResource(MR.strings.reconnect), + color = MaterialTheme.colors.primary + ) + } +} + +private fun reconnectServerAlert(rh: RemoteHostInfo?, server: String) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.servers_info_reconnect_server_title), + text = generalGetString(MR.strings.servers_info_reconnect_server_message), + onConfirm = { + withBGApi { + val success = controller.reconnectServer(rh?.remoteHostId, server) + + if (!success) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.servers_info_modal_error_title), + text = generalGetString(MR.strings.servers_info_reconnect_server_error) + ) + } + } + } + ) +} + +@Composable +fun XFTPStatsView(stats: AgentXFTPServerStatsData, statsStartedAt: Instant, rh: RemoteHostInfo?) { + SectionView(generalGetString(MR.strings.servers_info_statistics_section_header).uppercase()) { + InfoRow( + generalGetString(MR.strings.servers_info_uploaded), + prettySize(stats._uploadsSize) + ) + InfoRow( + generalGetString(MR.strings.servers_info_downloaded), + prettySize(stats._downloadsSize) + ) + SectionItemView ( + click = { + ModalManager.start.showCustomModal { close -> DetailedXFTPStatsView( + rh = rh, + close = close, + stats = stats, + statsStartedAt = statsStartedAt) + } + } + ) { + Text(text = generalGetString(MR.strings.servers_info_details), color = MaterialTheme.colors.onBackground) + } + } + SectionTextFooter( + String.format(stringResource(MR.strings.servers_info_private_data_disclaimer), localTimestamp(statsStartedAt)) + ) +} + +@Composable +private fun IndentedInfoRow(title: String, desc: String) { + InfoRow(title, desc, padding = PaddingValues(start = 24.dp + DEFAULT_PADDING, end = DEFAULT_PADDING)) +} + +@Composable +fun DetailedSMPStatsLayout(stats: AgentSMPServerStatsData, statsStartedAt: Instant) { + SectionView(generalGetString(MR.strings.servers_info_detailed_statistics_sent_messages_header).uppercase()) { + InfoRow(generalGetString(MR.strings.servers_info_detailed_statistics_sent_messages_total), numOrDash(stats._sentDirect + stats._sentViaProxy)) + InfoRowTwoValues(generalGetString(MR.strings.sent_directly), generalGetString(MR.strings.attempts_label), stats._sentDirect, stats._sentDirectAttempts) + InfoRowTwoValues(generalGetString(MR.strings.sent_via_proxy), generalGetString(MR.strings.attempts_label), stats._sentViaProxy, stats._sentViaProxyAttempts) + InfoRowTwoValues(generalGetString(MR.strings.proxied), generalGetString(MR.strings.attempts_label), stats._sentProxied, stats._sentProxiedAttempts) + SectionItemView { + Text(generalGetString(MR.strings.send_errors), color = MaterialTheme.colors.onBackground) + } + IndentedInfoRow("AUTH", numOrDash(stats._sentAuthErrs)) + IndentedInfoRow("QUOTA", numOrDash(stats._sentQuotaErrs)) + IndentedInfoRow(generalGetString(MR.strings.expired_label), numOrDash(stats._sentExpiredErrs)) + IndentedInfoRow(generalGetString(MR.strings.other_label), numOrDash(stats._sentOtherErrs)) + } + + SectionDividerSpaced() + + SectionView(generalGetString(MR.strings.servers_info_detailed_statistics_received_messages_header).uppercase()) { + InfoRow(generalGetString(MR.strings.servers_info_detailed_statistics_received_total), numOrDash(stats._recvMsgs)) + SectionItemView { + Text(generalGetString(MR.strings.servers_info_detailed_statistics_receive_errors), color = MaterialTheme.colors.onBackground) + } + IndentedInfoRow(generalGetString(MR.strings.duplicates_label), numOrDash(stats._recvDuplicates)) + IndentedInfoRow(generalGetString(MR.strings.decryption_errors), numOrDash(stats._recvCryptoErrs)) + IndentedInfoRow(generalGetString(MR.strings.other_errors), numOrDash(stats._recvErrs)) + InfoRowTwoValues(generalGetString(MR.strings.acknowledged), generalGetString(MR.strings.attempts_label), stats._ackMsgs, stats._ackAttempts) + SectionItemView { + Text(generalGetString(MR.strings.acknowledgement_errors), color = MaterialTheme.colors.onBackground) + } + IndentedInfoRow("NO_MSG errors", numOrDash(stats._ackNoMsgErrs)) + IndentedInfoRow(generalGetString(MR.strings.other_errors), numOrDash(stats._ackOtherErrs)) + } + + SectionDividerSpaced() + + SectionView(generalGetString(MR.strings.connections).uppercase()) { + InfoRow(generalGetString(MR.strings.created), numOrDash(stats._connCreated)) + InfoRow(generalGetString(MR.strings.secured), numOrDash(stats._connSecured)) + InfoRow(generalGetString(MR.strings.completed), numOrDash(stats._connCompleted)) + InfoRowTwoValues(generalGetString(MR.strings.deleted), generalGetString(MR.strings.attempts_label), stats._connDeleted, stats._connDelAttempts) + InfoRow(generalGetString(MR.strings.deletion_errors), numOrDash(stats._connDelErrs)) + InfoRowTwoValues(generalGetString(MR.strings.subscribed), generalGetString(MR.strings.attempts_label), stats._connSubscribed, stats._connSubAttempts) + InfoRow(generalGetString(MR.strings.subscription_results_ignored), numOrDash(stats._connSubIgnored)) + InfoRow(generalGetString(MR.strings.subscription_errors), numOrDash(stats._connSubErrs)) + } + SectionTextFooter( + String.format(stringResource(MR.strings.servers_info_starting_from), localTimestamp(statsStartedAt)) + ) + + SectionBottomSpacer() +} + +@Composable +fun DetailedXFTPStatsLayout(stats: AgentXFTPServerStatsData, statsStartedAt: Instant) { + SectionView(generalGetString(MR.strings.uploaded_files).uppercase()) { + InfoRow(generalGetString(MR.strings.size), prettySize(stats._uploadsSize)) + InfoRowTwoValues(generalGetString(MR.strings.chunks_uploaded), generalGetString(MR.strings.attempts_label), stats._uploads, stats._uploadAttempts) + InfoRow(generalGetString(MR.strings.upload_errors), numOrDash(stats._uploadErrs)) + InfoRowTwoValues(generalGetString(MR.strings.chunks_deleted), generalGetString(MR.strings.attempts_label), stats._deletions, stats._deleteAttempts) + InfoRow(generalGetString(MR.strings.deletion_errors), numOrDash(stats._deleteErrs)) + } + SectionDividerSpaced() + SectionView(generalGetString(MR.strings.downloaded_files).uppercase()) { + InfoRow(generalGetString(MR.strings.size), prettySize(stats._downloadsSize)) + InfoRowTwoValues(generalGetString(MR.strings.chunks_downloaded), generalGetString(MR.strings.attempts_label), stats._downloads, stats._downloadAttempts) + SectionItemView { + Text(generalGetString(MR.strings.download_errors), color = MaterialTheme.colors.onBackground) + } + IndentedInfoRow("AUTH", numOrDash(stats._downloadAuthErrs)) + IndentedInfoRow(generalGetString(MR.strings.other_label), numOrDash(stats._downloadErrs)) + } + SectionTextFooter( + String.format(stringResource(MR.strings.servers_info_starting_from), localTimestamp(statsStartedAt)) + ) + + SectionBottomSpacer() +} + +@Composable +fun XFTPServerSummaryLayout(summary: XFTPServerSummary, statsStartedAt: Instant, rh: RemoteHostInfo?) { + SectionView(generalGetString(MR.strings.server_address).uppercase()) { + SelectionContainer { + Text( + summary.xftpServer, + Modifier.padding(start = DEFAULT_PADDING, top = 5.dp, end = DEFAULT_PADDING, bottom = 10.dp), + style = TextStyle( + fontFamily = FontFamily.Monospace, fontSize = 16.sp, + color = MaterialTheme.colors.secondary + ) + ) + } + if (summary.known == true) { + SectionItemView(click = { + ModalManager.start.showCustomModal { close -> ProtocolServersView(chatModel, rhId = rh?.remoteHostId, ServerProtocol.XFTP, close) } + }) { + Text(generalGetString(MR.strings.open_server_settings_button)) + } + if (summary.stats != null || summary.sessions != null) { + SectionDividerSpaced() + } + } + + if (summary.stats != null) { + XFTPStatsView(stats = summary.stats, rh = rh, statsStartedAt = statsStartedAt) + if (summary.sessions != null) { + SectionDividerSpaced(maxTopPadding = true) + } + } + + if (summary.sessions != null) { + ServerSessionsView(summary.sessions) + } + } + + SectionBottomSpacer() +} + +@Composable +fun SMPServerSummaryLayout(summary: SMPServerSummary, statsStartedAt: Instant, rh: RemoteHostInfo?) { + SectionView(generalGetString(MR.strings.server_address).uppercase()) { + SelectionContainer { + Text( + summary.smpServer, + Modifier.padding(start = DEFAULT_PADDING, top = 5.dp, end = DEFAULT_PADDING, bottom = 10.dp), + style = TextStyle( + fontFamily = FontFamily.Monospace, fontSize = 16.sp, + color = MaterialTheme.colors.secondary + ) + ) + } + if (summary.known == true) { + SectionItemView(click = { + ModalManager.start.showCustomModal { close -> ProtocolServersView(chatModel, rhId = rh?.remoteHostId, ServerProtocol.SMP, close) } + }) { + Text(generalGetString(MR.strings.open_server_settings_button)) + } + SectionDividerSpaced() + } + + if (summary.stats != null) { + SMPStatsView(stats = summary.stats, remoteHostInfo = rh, statsStartedAt = statsStartedAt) + if (summary.subs != null || summary.sessions != null) { + SectionDividerSpaced(maxTopPadding = true) + } + } + + if (summary.subs != null) { + SMPSubscriptionsSection(subs = summary.subs, summary = summary, rh = rh) + if (summary.sessions != null) { + SectionDividerSpaced() + } + } + + if (summary.sessions != null) { + ServerSessionsView(summary.sessions) + } + } + + SectionBottomSpacer() +} + +@Composable +fun ModalData.SMPServerSummaryView( + rh: RemoteHostInfo?, + close: () -> Unit, + summary: SMPServerSummary, + statsStartedAt: Instant +) { + ModalView( + close = close + ) { + ColumnWithScrollBar( + Modifier.fillMaxSize(), + ) { + val bottomPadding = DEFAULT_PADDING + AppBarTitle( + stringResource(MR.strings.smp_server), + hostDevice(rh?.remoteHostId), + bottomPadding = bottomPadding + ) + SMPServerSummaryLayout(summary, statsStartedAt, rh) + } + } +} + + +@Composable +fun ModalData.DetailedXFTPStatsView( + rh: RemoteHostInfo?, + close: () -> Unit, + stats: AgentXFTPServerStatsData, + statsStartedAt: Instant +) { + ModalView( + close = close + ) { + ColumnWithScrollBar( + Modifier.fillMaxSize(), + ) { + Box(contentAlignment = Alignment.Center) { + val bottomPadding = DEFAULT_PADDING + AppBarTitle( + stringResource(MR.strings.servers_info_detailed_statistics), + hostDevice(rh?.remoteHostId), + bottomPadding = bottomPadding + ) + } + DetailedXFTPStatsLayout(stats, statsStartedAt) + } + } +} + +@Composable +fun ModalData.DetailedSMPStatsView( + rh: RemoteHostInfo?, + close: () -> Unit, + stats: AgentSMPServerStatsData, + statsStartedAt: Instant +) { + ModalView( + close = close + ) { + ColumnWithScrollBar( + Modifier.fillMaxSize(), + ) { + Box(contentAlignment = Alignment.Center) { + val bottomPadding = DEFAULT_PADDING + AppBarTitle( + stringResource(MR.strings.servers_info_detailed_statistics), + hostDevice(rh?.remoteHostId), + bottomPadding = bottomPadding + ) + } + DetailedSMPStatsLayout(stats, statsStartedAt) + } + } +} + +@Composable +fun ModalData.XFTPServerSummaryView( + rh: RemoteHostInfo?, + close: () -> Unit, + summary: XFTPServerSummary, + statsStartedAt: Instant +) { + ModalView( + close = close + ) { + ColumnWithScrollBar( + Modifier.fillMaxSize(), + ) { + Box(contentAlignment = Alignment.Center) { + val bottomPadding = DEFAULT_PADDING + AppBarTitle( + stringResource(MR.strings.xftp_server), + hostDevice(rh?.remoteHostId), + bottomPadding = bottomPadding + ) + } + XFTPServerSummaryLayout(summary, statsStartedAt, rh) + } + } +} + +@Composable +fun ModalData.ServersSummaryView(rh: RemoteHostInfo?, serversSummary: MutableState) { + ColumnWithScrollBar( + Modifier.fillMaxSize(), + ) { + var showUserSelection by remember { mutableStateOf(false) } + val selectedUserCategory = + remember { stateGetOrPut("selectedUserCategory") { PresentedUserCategory.ALL_USERS } } + val selectedServerType = + remember { stateGetOrPut("serverTypeSelection") { PresentedServerType.SMP } } + val scope = rememberCoroutineScope() + + suspend fun setServersSummary() { + if (chatModel.currentUser.value != null) { + serversSummary.value = chatModel.controller.getAgentServersSummary(chatModel.remoteHostId()) + } + } + + LaunchedEffect(Unit) { + if (chatModel.users.count { u -> u.user.activeUser || !u.user.hidden } == 1 + ) { + selectedUserCategory.value = PresentedUserCategory.CURRENT_USER + } else { + showUserSelection = true + } + setServersSummary() + scope.launch { + while (isActive) { + delay(1.seconds) + if ((appPlatform.isDesktop || chat.simplex.common.platform.chatModel.chatId.value == null) && isAppVisibleAndFocused()) { + setServersSummary() + } + } + } + } + + fun resetStats() { + withBGApi { + val success = controller.resetAgentServersStats(rh?.remoteHostId) + if (success) { + setServersSummary() + } else { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.servers_info_modal_error_title), + text = generalGetString(MR.strings.servers_info_reset_stats_alert_error_title) + ) + } + } + } + + Column( + Modifier.fillMaxSize(), + ) { + val bottomPadding = DEFAULT_PADDING + AppBarTitle( + stringResource(MR.strings.servers_info), + hostDevice(rh?.remoteHostId), + bottomPadding = bottomPadding + ) + if (serversSummary.value == null) { + Box( + modifier = Modifier + .fillMaxSize() + ) { + Text(generalGetString(MR.strings.servers_info_missing), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary) + } + } else { + val userOptions by remember { + mutableStateOf( + listOf( + PresentedUserCategory.ALL_USERS to generalGetString(MR.strings.all_users), + PresentedUserCategory.CURRENT_USER to generalGetString(MR.strings.current_user), + ) + ) + } + val serverTypeTabTitles = PresentedServerType.entries.map { + when (it) { + PresentedServerType.SMP -> + stringResource(MR.strings.messages_section_title) + + PresentedServerType.XFTP -> + stringResource(MR.strings.servers_info_files_tab) + } + } + val serverTypePagerState = rememberPagerState( + initialPage = selectedServerType.value.ordinal, + initialPageOffsetFraction = 0f + ) { PresentedServerType.entries.size } + + KeyChangeEffect(serverTypePagerState.currentPage) { + selectedServerType.value = PresentedServerType.entries[serverTypePagerState.currentPage] + } + TabRow( + selectedTabIndex = serverTypePagerState.currentPage, + backgroundColor = Color.Transparent, + contentColor = MaterialTheme.colors.primary, + ) { + serverTypeTabTitles.forEachIndexed { index, it -> + Tab( + selected = serverTypePagerState.currentPage == index, + onClick = { + scope.launch { + serverTypePagerState.animateScrollToPage(index) + } + }, + text = { Text(it, fontSize = 13.sp) }, + selectedContentColor = MaterialTheme.colors.primary, + unselectedContentColor = MaterialTheme.colors.secondary, + ) + } + } + + HorizontalPager( + state = serverTypePagerState, + Modifier.fillMaxSize(), + verticalAlignment = Alignment.Top, + userScrollEnabled = appPlatform.isAndroid + ) { index -> + Column( + Modifier + .fillMaxSize(), + verticalArrangement = Arrangement.Top + ) { + Spacer(Modifier.height(DEFAULT_PADDING)) + if (showUserSelection) { + ExposedDropDownSettingRow( + generalGetString(MR.strings.servers_info_target), + userOptions, + selectedUserCategory, + icon = null, + enabled = remember { mutableStateOf(true) }, + onSelected = { + selectedUserCategory.value = it + } + ) + SectionDividerSpaced() + } + when (index) { + PresentedServerType.SMP.ordinal -> { + serversSummary.value?.let { + val smpSummary = + if (selectedUserCategory.value == PresentedUserCategory.CURRENT_USER) it.currentUserSMP else it.allUsersSMP; + val totals = smpSummary.smpTotals + val currentlyUsedSMPServers = smpSummary.currentlyUsedSMPServers + val previouslyUsedSMPServers = smpSummary.previouslyUsedSMPServers + val proxySMPServers = smpSummary.onlyProxiedSMPServers + val statsStartedAt = it.statsStartedAt + + SMPStatsView(totals.stats, statsStartedAt, rh) + SectionDividerSpaced(maxTopPadding = true) + SMPSubscriptionsSection(totals) + SectionDividerSpaced() + + if (currentlyUsedSMPServers.isNotEmpty()) { + SMPServersListView( + servers = currentlyUsedSMPServers, + statsStartedAt = statsStartedAt, + header = generalGetString(MR.strings.servers_info_connected_servers_section_header).uppercase(), + rh = rh + ) + SectionDividerSpaced() + } + + if (previouslyUsedSMPServers.isNotEmpty()) { + SMPServersListView( + servers = previouslyUsedSMPServers, + statsStartedAt = statsStartedAt, + header = generalGetString(MR.strings.servers_info_previously_connected_servers_section_header).uppercase(), + rh = rh + ) + SectionDividerSpaced() + } + + if (proxySMPServers.isNotEmpty()) { + SMPServersListView( + servers = proxySMPServers, + statsStartedAt = statsStartedAt, + header = generalGetString(MR.strings.servers_info_proxied_servers_section_header).uppercase(), + footer = generalGetString(MR.strings.servers_info_proxied_servers_section_footer), + rh = rh + ) + SectionDividerSpaced(maxTopPadding = true) + } + + ServerSessionsView(totals.sessions) + } + } + + PresentedServerType.XFTP.ordinal -> { + serversSummary.value?.let { + val xftpSummary = + if (selectedUserCategory.value == PresentedUserCategory.CURRENT_USER) it.currentUserXFTP else it.allUsersXFTP + val totals = xftpSummary.xftpTotals + val statsStartedAt = it.statsStartedAt + val currentlyUsedXFTPServers = xftpSummary.currentlyUsedXFTPServers + val previouslyUsedXFTPServers = xftpSummary.previouslyUsedXFTPServers + + XFTPStatsView(totals.stats, statsStartedAt, rh) + SectionDividerSpaced(maxTopPadding = true) + + if (currentlyUsedXFTPServers.isNotEmpty()) { + XFTPServersListView( + currentlyUsedXFTPServers, + statsStartedAt, + generalGetString(MR.strings.servers_info_connected_servers_section_header).uppercase(), + rh + ) + SectionDividerSpaced() + } + + if (previouslyUsedXFTPServers.isNotEmpty()) { + XFTPServersListView( + previouslyUsedXFTPServers, + statsStartedAt, + generalGetString(MR.strings.servers_info_previously_connected_servers_section_header).uppercase(), + rh + ) + SectionDividerSpaced() + } + + ServerSessionsView(totals.sessions) + } + } + } + + SectionDividerSpaced(maxBottomPadding = false) + + SectionView { + ReconnectAllServersButton(rh) + ResetStatisticsButton(rh, resetStats = { resetStats() }) + } + + SectionBottomSpacer() + } + } + } + } + } +} + +@Composable +private fun ReconnectAllServersButton(rh: RemoteHostInfo?) { + SectionItemView(click = { reconnectAllServersAlert(rh) }) { + Text( + stringResource(MR.strings.servers_info_reconnect_all_servers_button), + color = MaterialTheme.colors.primary + ) + } +} + +private fun reconnectAllServersAlert(rh: RemoteHostInfo?) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.servers_info_reconnect_servers_title), + text = generalGetString(MR.strings.servers_info_reconnect_servers_message), + onConfirm = { + withBGApi { + val success = controller.reconnectAllServers(rh?.remoteHostId) + + if (!success) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.servers_info_modal_error_title), + text = generalGetString(MR.strings.servers_info_reconnect_servers_error) + ) + } + } + } + ) +} + +@Composable +private fun ResetStatisticsButton(rh: RemoteHostInfo?, resetStats: () -> Unit) { + SectionItemView(click = { resetStatisticsAlert(rh, resetStats) }) { + Text( + stringResource(MR.strings.servers_info_reset_stats), + color = MaterialTheme.colors.primary + ) + } +} + +private fun resetStatisticsAlert(rh: RemoteHostInfo?, resetStats: () -> Unit) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.servers_info_reset_stats_alert_title), + text = generalGetString(MR.strings.servers_info_reset_stats_alert_message), + confirmText = generalGetString(MR.strings.servers_info_reset_stats_alert_confirm), + destructive = true, + onConfirm = resetStats + ) +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListNavLinkView.kt index 1de1e40afb..8b2e008ad3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListNavLinkView.kt @@ -9,42 +9,76 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import chat.simplex.common.views.helpers.ProfileImage import chat.simplex.common.model.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR +import kotlinx.coroutines.launch @Composable -fun ShareListNavLinkView(chat: Chat, chatModel: ChatModel) { +fun ShareListNavLinkView( + chat: Chat, + chatModel: ChatModel, + isMediaOrFileAttachment: Boolean, + isVoice: Boolean, + hasSimplexLink: Boolean +) { val stopped = chatModel.chatRunning.value == false + val scope = rememberCoroutineScope() when (chat.chatInfo) { - is ChatInfo.Direct -> + is ChatInfo.Direct -> { + val voiceProhibited = isVoice && !chat.chatInfo.featureEnabled(ChatFeature.Voice) ShareListNavLinkLayout( - chatLinkPreview = { SharePreviewView(chat) }, - click = { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) }, + chatLinkPreview = { SharePreviewView(chat, disabled = voiceProhibited) }, + click = { + if (voiceProhibited) { + showForwardProhibitedByPrefAlert() + } else { + scope.launch { directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) } + } + }, stopped ) - is ChatInfo.Group -> + } + is ChatInfo.Group -> { + val simplexLinkProhibited = hasSimplexLink && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks) + val fileProhibited = isMediaOrFileAttachment && !chat.groupFeatureEnabled(GroupFeature.Files) + val voiceProhibited = isVoice && !chat.chatInfo.featureEnabled(ChatFeature.Voice) + val prohibitedByPref = simplexLinkProhibited || fileProhibited || voiceProhibited ShareListNavLinkLayout( - chatLinkPreview = { SharePreviewView(chat) }, - click = { groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel) }, + chatLinkPreview = { SharePreviewView(chat, disabled = prohibitedByPref) }, + click = { + if (prohibitedByPref) { + showForwardProhibitedByPrefAlert() + } else { + scope.launch { groupChatAction(chat.remoteHostId, chat.chatInfo.groupInfo, chatModel) } + } + }, stopped ) + } is ChatInfo.Local -> ShareListNavLinkLayout( - chatLinkPreview = { SharePreviewView(chat) }, - click = { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) }, + chatLinkPreview = { SharePreviewView(chat, disabled = false) }, + click = { scope.launch { noteFolderChatAction(chat.remoteHostId, chat.chatInfo.noteFolder) } }, stopped ) is ChatInfo.ContactRequest, is ChatInfo.ContactConnection, is ChatInfo.InvalidJSON -> {} } } +private fun showForwardProhibitedByPrefAlert() { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.cannot_share_message_alert_title), + text = generalGetString(MR.strings.cannot_share_message_alert_text), + ) +} + @Composable private fun ShareListNavLinkLayout( chatLinkPreview: @Composable () -> Unit, click: () -> Unit, - stopped: Boolean + stopped: Boolean, ) { SectionItemView(minHeight = 50.dp, click = click, disabled = stopped) { chatLinkPreview() @@ -53,7 +87,7 @@ private fun ShareListNavLinkLayout( } @Composable -private fun SharePreviewView(chat: Chat) { +private fun SharePreviewView(chat: Chat, disabled: Boolean) { Row( Modifier.fillMaxSize(), horizontalArrangement = Arrangement.SpaceBetween, @@ -70,7 +104,7 @@ private fun SharePreviewView(chat: Chat) { } Text( chat.chatInfo.chatViewName, maxLines = 1, overflow = TextOverflow.Ellipsis, - color = if (chat.chatInfo.incognito) Indigo else Color.Unspecified + color = if (disabled) MaterialTheme.colors.secondary else if (chat.chatInfo.incognito) Indigo else Color.Unspecified ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt index a36930f5ce..cdf1766a25 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ShareListView.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import chat.simplex.common.SettingsViewState import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.views.helpers.* import chat.simplex.common.platform.* import chat.simplex.res.MR @@ -24,20 +25,67 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe var searchInList by rememberSaveable { mutableStateOf("") } val (userPickerState, scaffoldState) = settingsState val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp + val oneHandUI = remember { appPrefs.oneHandUI.state } + Scaffold( Modifier.padding(end = endPadding), contentColor = LocalContentColor.current, drawerContentColor = LocalContentColor.current, scaffoldState = scaffoldState, - topBar = { Column { ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } } }, + topBar = { + if (!oneHandUI.value) { + Column { + ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } + Divider() + } + } + }, + bottomBar = { + if (oneHandUI.value) { + Column { + Divider() + ShareListToolbar(chatModel, userPickerState, stopped) { searchInList = it.trim() } + } + } + } ) { + val sharedContent = chatModel.sharedContent.value + var isMediaOrFileAttachment = false + var isVoice = false + var hasSimplexLink = false + when (sharedContent) { + is SharedContent.Text -> + hasSimplexLink = hasSimplexLink(sharedContent.text) + is SharedContent.Media -> { + isMediaOrFileAttachment = true + hasSimplexLink = hasSimplexLink(sharedContent.text) + } + is SharedContent.File -> { + isMediaOrFileAttachment = true + hasSimplexLink = hasSimplexLink(sharedContent.text) + } + is SharedContent.Forward -> { + val mc = sharedContent.chatItem.content.msgContent + if (mc != null) { + isMediaOrFileAttachment = mc.isMediaOrFileAttachment + isVoice = mc.isVoice + hasSimplexLink = hasSimplexLink(mc.text) + } + } + null -> {} + } Box(Modifier.padding(it)) { Column( - modifier = Modifier - .fillMaxSize() + modifier = Modifier.fillMaxSize() ) { - if (chatModel.chats.isNotEmpty()) { - ShareList(chatModel, search = searchInList) + if (chatModel.chats.value.isNotEmpty()) { + ShareList( + chatModel, + search = searchInList, + isMediaOrFileAttachment = isMediaOrFileAttachment, + isVoice = isVoice, + hasSimplexLink = hasSimplexLink, + ) } else { EmptyList() } @@ -54,6 +102,11 @@ fun ShareListView(chatModel: ChatModel, settingsState: SettingsViewState, stoppe } } +private fun hasSimplexLink(msg: String): Boolean { + val parsedMsg = parseToMarkdown(msg) ?: return false + return parsedMsg.any { ft -> ft.format is Format.SimplexLink } +} + @Composable private fun EmptyList() { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { @@ -91,7 +144,7 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState }) } } - if (chatModel.chats.size >= 8) { + if (chatModel.chats.value.size >= 8) { barButtons.add { IconButton({ showSearch = true }) { Icon(painterResource(MR.images.ic_search_500), stringResource(MR.strings.search_verb), tint = MaterialTheme.colors.primary) @@ -137,26 +190,35 @@ private fun ShareListToolbar(chatModel: ChatModel, userPickerState: MutableState onSearchValueChanged = onSearchValueChanged, buttons = barButtons ) - Divider() } @Composable -private fun ShareList(chatModel: ChatModel, search: String) { +private fun ShareList( + chatModel: ChatModel, + search: String, + isMediaOrFileAttachment: Boolean, + isVoice: Boolean, + hasSimplexLink: Boolean, +) { + val oneHandUI = remember { appPrefs.oneHandUI.state } val chats by remember(search) { derivedStateOf { - val sorted = chatModel.chats.toList().sortedByDescending { it.chatInfo is ChatInfo.Local } - if (search.isEmpty()) { - sorted.filter { it.chatInfo.ready } - } else { - sorted.filter { it.chatInfo.ready && it.chatInfo.chatViewName.lowercase().contains(search.lowercase()) } - } + val sorted = chatModel.chats.value.toList().filter { it.chatInfo.ready }.sortedByDescending { it.chatInfo is ChatInfo.Local } + filteredChats(false, mutableStateOf(false), mutableStateOf(null), search, sorted) } } LazyColumnWithScrollBar( - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxSize(), + reverseLayout = oneHandUI.value ) { items(chats) { chat -> - ShareListNavLinkView(chat, chatModel) + ShareListNavLinkView( + chat, + chatModel, + isMediaOrFileAttachment = isMediaOrFileAttachment, + isVoice = isVoice, + hasSimplexLink = hasSimplexLink, + ) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt index 23272fcc0c..e15bc3863e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.unit.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatController.stopRemoteHostAndReloadHosts import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.ui.theme.* @@ -40,6 +41,7 @@ fun UserPicker( chatModel: ChatModel, userPickerState: MutableStateFlow, showSettings: Boolean = true, + contentAlignment: Alignment = Alignment.TopStart, showCancel: Boolean = false, cancelClicked: () -> Unit = {}, useFromDesktopClicked: () -> Unit = {}, @@ -148,8 +150,9 @@ fun UserPicker( .padding(bottom = 10.dp, top = 10.dp) .graphicsLayer { alpha = animatedFloat.value - translationY = (animatedFloat.value - 1) * xOffset - } + translationY = (if (appPrefs.oneHandUI.state.value) -1 else 1) * (animatedFloat.value - 1) * xOffset + }, + contentAlignment = contentAlignment ) { Column( Modifier @@ -259,7 +262,7 @@ fun UserProfilePickerItem( Row( Modifier .fillMaxWidth() - .sizeIn(minHeight = 46.dp) + .sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT) .combinedClickable( enabled = enabled, onClick = if (u.activeUser) openSettings else onClick, @@ -284,7 +287,7 @@ fun UserProfilePickerItem( Text( unreadCountStr(unreadCount), color = Color.White, - fontSize = 11.sp, + fontSize = 10.sp, modifier = Modifier .background(MaterialTheme.colors.primaryVariant, shape = CircleShape) .padding(2.dp) @@ -309,7 +312,7 @@ fun UserProfileRow(u: User, enabled: Boolean = chatModel.chatRunning.value == tr ) { ProfileImage( image = u.image, - size = 54.dp + size = 54.dp * fontSizeSqrtMultiplier ) Text( u.displayName, @@ -327,7 +330,7 @@ fun RemoteHostPickerItem(h: RemoteHostInfo, onLongClick: () -> Unit = {}, action Modifier .fillMaxWidth() .background(color = if (h.activeHost) MaterialTheme.colors.surface.mixWith(MaterialTheme.colors.onBackground, 0.95f) else Color.Unspecified) - .sizeIn(minHeight = 46.dp) + .sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT) .combinedClickable( onClick = onClick, onLongClick = onLongClick @@ -354,7 +357,7 @@ fun RemoteHostRow(h: RemoteHostInfo) { .padding(start = 17.dp), verticalAlignment = Alignment.CenterVertically ) { - Icon(painterResource(MR.images.ic_smartphone_300), h.hostDeviceName, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) + Icon(painterResource(MR.images.ic_smartphone_300), h.hostDeviceName, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground) Text( h.hostDeviceName, modifier = Modifier.padding(start = 26.dp, end = 8.dp), @@ -370,7 +373,7 @@ fun LocalDevicePickerItem(active: Boolean, onLongClick: () -> Unit = {}, onClick Modifier .fillMaxWidth() .background(color = if (active) MaterialTheme.colors.surface.mixWith(MaterialTheme.colors.onBackground, 0.95f) else Color.Unspecified) - .sizeIn(minHeight = 46.dp) + .sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT) .combinedClickable( onClick = if (active) {{}} else onClick, onLongClick = onLongClick, @@ -395,7 +398,7 @@ fun LocalDeviceRow(active: Boolean) { .padding(start = 17.dp, end = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically ) { - Icon(painterResource(MR.images.ic_desktop), stringResource(MR.strings.this_device), Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) + Icon(painterResource(MR.images.ic_desktop), stringResource(MR.strings.this_device), Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground) Text( stringResource(MR.strings.this_device), modifier = Modifier.padding(start = 26.dp, end = 8.dp), @@ -409,7 +412,7 @@ fun LocalDeviceRow(active: Boolean) { private fun UseFromDesktopPickerItem(onClick: () -> Unit) { SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) { val text = generalGetString(MR.strings.settings_section_title_use_from_desktop).lowercase().capitalize(Locale.current) - Icon(painterResource(MR.images.ic_desktop), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) + Icon(painterResource(MR.images.ic_desktop), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground) Spacer(Modifier.width(DEFAULT_PADDING + 6.dp)) Text(text, color = MenuTextColor) } @@ -419,7 +422,7 @@ private fun UseFromDesktopPickerItem(onClick: () -> Unit) { private fun LinkAMobilePickerItem(onClick: () -> Unit) { SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) { val text = generalGetString(MR.strings.link_a_mobile) - Icon(painterResource(MR.images.ic_smartphone_300), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) + Icon(painterResource(MR.images.ic_smartphone_300), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground) Spacer(Modifier.width(DEFAULT_PADDING + 6.dp)) Text(text, color = MenuTextColor) } @@ -429,7 +432,7 @@ private fun LinkAMobilePickerItem(onClick: () -> Unit) { private fun CreateInitialProfile(onClick: () -> Unit) { SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) { val text = generalGetString(MR.strings.create_chat_profile) - Icon(painterResource(MR.images.ic_manage_accounts), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) + Icon(painterResource(MR.images.ic_manage_accounts), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground) Spacer(Modifier.width(DEFAULT_PADDING + 6.dp)) Text(text, color = MenuTextColor) } @@ -439,7 +442,7 @@ private fun CreateInitialProfile(onClick: () -> Unit) { private fun SettingsPickerItem(onClick: () -> Unit) { SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) { val text = generalGetString(MR.strings.settings_section_title_settings).lowercase().capitalize(Locale.current) - Icon(painterResource(MR.images.ic_settings), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) + Icon(painterResource(MR.images.ic_settings), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground) Spacer(Modifier.width(DEFAULT_PADDING + 6.dp)) Text(text, color = MenuTextColor) } @@ -449,7 +452,7 @@ private fun SettingsPickerItem(onClick: () -> Unit) { private fun CancelPickerItem(onClick: () -> Unit) { SectionItemView(onClick, padding = PaddingValues(start = DEFAULT_PADDING + 7.dp, end = DEFAULT_PADDING), minHeight = 68.dp) { val text = generalGetString(MR.strings.cancel_verb) - Icon(painterResource(MR.images.ic_close), text, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground) + Icon(painterResource(MR.images.ic_close), text, Modifier.size(20.dp * fontSizeSqrtMultiplier), tint = MaterialTheme.colors.onBackground) Spacer(Modifier.width(DEFAULT_PADDING + 6.dp)) Text(text, color = MenuTextColor) } @@ -459,7 +462,7 @@ private fun CancelPickerItem(onClick: () -> Unit) { fun HostDisconnectButton(onClick: (() -> Unit)?) { val interactionSource = remember { MutableInteractionSource() } val hovered = interactionSource.collectIsHoveredAsState().value - IconButton(onClick ?: {}, Modifier.requiredSize(20.dp), enabled = onClick != null) { + IconButton(onClick ?: {}, Modifier.requiredSize(20.dp * fontSizeSqrtMultiplier), enabled = onClick != null) { Icon( painterResource(if (onClick == null) MR.images.ic_desktop else if (hovered) MR.images.ic_wifi_off else MR.images.ic_wifi), null, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt new file mode 100644 index 0000000000..711fb1377d --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt @@ -0,0 +1,137 @@ +package chat.simplex.common.views.contacts + +import androidx.compose.runtime.* +import androidx.compose.ui.graphics.Color +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats +import chat.simplex.common.platform.* +import chat.simplex.common.views.chat.* +import chat.simplex.common.views.chat.item.ItemAction +import chat.simplex.common.views.chatlist.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.ContactType +import chat.simplex.common.views.newchat.chatContactType +import chat.simplex.res.MR +import kotlinx.coroutines.delay + +fun onRequestAccepted(chat: Chat) { + val chatInfo = chat.chatInfo + if (chatInfo is ChatInfo.Direct) { + ModalManager.start.closeModals() + if (chatInfo.contact.sndReady) { + openLoadedChat(chat, chatModel) + } + } +} + +@Composable +fun ContactListNavLinkView(chat: Chat, nextChatSelected: State, showDeletedChatIcon: Boolean) { + val showMenu = remember { mutableStateOf(false) } + val rhId = chat.remoteHostId + val disabled = chatModel.chatRunning.value == false || chatModel.deletedChats.value.contains(rhId to chat.chatInfo.id) + val contactType = chatContactType(chat) + + LaunchedEffect(chat.id) { + showMenu.value = false + delay(500L) + } + + val selectedChat = remember(chat.id) { derivedStateOf { chat.id == chatModel.chatId.value } } + val view = LocalMultiplatformView() + + when (chat.chatInfo) { + is ChatInfo.Direct -> { + ChatListNavLinkLayout( + chatLinkPreview = { + tryOrShowError("${chat.id}ContactListNavLink", error = { ErrorChatListItem() }) { + ContactPreviewView(chat, disabled, showDeletedChatIcon) + } + }, + click = { + hideKeyboard(view) + when (contactType) { + ContactType.RECENT -> { + withApi { + openChat(rhId, chat.chatInfo, chatModel) + ModalManager.start.closeModals() + } + } + ContactType.CHAT_DELETED -> { + withApi { + openChat(rhId, chat.chatInfo, chatModel) + ModalManager.start.closeModals() + } + } + ContactType.CARD -> { + askCurrentOrIncognitoProfileConnectContactViaAddress( + chatModel, + rhId, + chat.chatInfo.contact, + close = { ModalManager.start.closeModals() }, + openChat = true + ) + } + else -> {} + } + }, + dropdownMenuItems = { + tryOrShowError("${chat.id}ContactListNavLinkDropdown", error = {}) { + DeleteContactAction(chat, chatModel, showMenu) + } + }, + showMenu, + disabled, + selectedChat, + nextChatSelected, + ) + } + is ChatInfo.ContactRequest -> { + ChatListNavLinkLayout( + chatLinkPreview = { + tryOrShowError("${chat.id}ContactListNavLink", error = { ErrorChatListItem() }) { + ContactPreviewView(chat, disabled, showDeletedChatIcon) + } + }, + click = { + hideKeyboard(view) + contactRequestAlertDialog( + rhId, + chat.chatInfo, + chatModel, + onSucess = { onRequestAccepted(it) } + ) + }, + dropdownMenuItems = { + tryOrShowError("${chat.id}ContactListNavLinkDropdown", error = {}) { + ContactRequestMenuItems( + rhId = chat.remoteHostId, + chatInfo = chat.chatInfo, + chatModel = chatModel, + showMenu = showMenu, + onSuccess = { onRequestAccepted(it) } + ) + } + }, + showMenu, + disabled, + selectedChat, + nextChatSelected) + } + else -> {} + } +} + +@Composable +fun DeleteContactAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState) { + ItemAction( + stringResource(MR.strings.delete_contact_menu_action), + painterResource(MR.images.ic_delete), + onClick = { + deleteContactDialog(chat, chatModel) + showMenu.value = false + }, + color = Color.Red + ) +} \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactPreviewView.kt new file mode 100644 index 0000000000..dd03bca921 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactPreviewView.kt @@ -0,0 +1,143 @@ +package chat.simplex.common.views.contacts + +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import dev.icerock.moko.resources.compose.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.platform.chatModel +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.newchat.ContactType +import chat.simplex.common.views.newchat.chatContactType +import chat.simplex.res.MR + +@Composable +fun ContactPreviewView( + chat: Chat, + disabled: Boolean, + showDeletedChatIcon: Boolean +) { + val cInfo = chat.chatInfo + val contactType = chatContactType(chat) + + @Composable + fun VerifiedIcon() { + Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(19.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary) + } + + @Composable + fun chatPreviewTitle() { + val deleting by remember(disabled, chat.id) { mutableStateOf(chatModel.deletedChats.value.contains(chat.remoteHostId to chat.chatInfo.id)) } + + val textColor = when { + deleting -> MaterialTheme.colors.secondary + contactType == ContactType.CARD -> MaterialTheme.colors.primary + contactType == ContactType.REQUEST -> MaterialTheme.colors.primary + contactType == ContactType.RECENT && chat.chatInfo.incognito -> Indigo + else -> Color.Unspecified + } + + when (cInfo) { + is ChatInfo.Direct -> + Row(verticalAlignment = Alignment.CenterVertically) { + if (cInfo.contact.verified) { + VerifiedIcon() + } + Text( + cInfo.chatViewName, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = textColor + ) + } + is ChatInfo.ContactRequest -> + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + cInfo.chatViewName, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = textColor + ) + } + else -> {} + } + } + + Row( + modifier = Modifier.padding(PaddingValues(horizontal = DEFAULT_PADDING_HALF)), + verticalAlignment = Alignment.CenterVertically, + ) { + Box(contentAlignment = Alignment.BottomEnd) { + ChatInfoImage(cInfo, size = 42.dp) + } + + Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON)) + + Box(modifier = Modifier.weight(10f, fill = true)) { + chatPreviewTitle() + } + + Spacer(Modifier.fillMaxWidth().weight(1f)) + + if (chat.chatInfo is ChatInfo.ContactRequest) { + Icon( + painterResource(MR.images.ic_check), + contentDescription = null, + tint = MaterialTheme.colors.primary, + modifier = Modifier + .size(23.dp) + ) + } + + if (contactType == ContactType.CARD) { + Icon( + painterResource(MR.images.ic_mail), + contentDescription = null, + tint = MaterialTheme.colors.primary, + modifier = Modifier + .size(21.dp) + ) + } + + if (showDeletedChatIcon && chat.chatInfo.chatDeleted) { + Icon( + painterResource(MR.images.ic_inventory_2), + contentDescription = null, + tint = MaterialTheme.colors.secondary, + modifier = Modifier + .size(17.dp) + ) + if (chat.chatInfo.incognito) { + Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON)) + } + } else if (chat.chatInfo.chatSettings?.favorite == true) { + Icon( + painterResource(MR.images.ic_star_filled), + contentDescription = null, + tint = MaterialTheme.colors.secondary, + modifier = Modifier + .size(17.dp) + ) + if (chat.chatInfo.incognito) { + Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON)) + } + } + + + if (chat.chatInfo.incognito) { + Icon( + painterResource(MR.images.ic_theater_comedy), + contentDescription = null, + tint = MaterialTheme.colors.secondary, + modifier = Modifier + .size(21.dp) + ) + } + } +} \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt index ca14b19adf..b73a0ca0bc 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseEncryptionView.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.unit.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* @@ -40,9 +41,8 @@ import kotlin.math.log2 @Composable fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) { val progressIndicator = remember { mutableStateOf(false) } - val prefs = m.controller.appPrefs - val useKeychain = remember { mutableStateOf(prefs.storeDBPassphrase.get()) } - val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) } + val useKeychain = remember { mutableStateOf(appPrefs.storeDBPassphrase.get()) } + val initialRandomDBPassphrase = remember { mutableStateOf(appPrefs.initialRandomDBPassphrase.get()) } val storedKey = remember { val key = DatabaseUtils.ksDatabasePassword.get(); mutableStateOf(key != null && key != "") } // Do not do rememberSaveable on current key to prevent saving it on disk in clear text val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.ksDatabasePassword.get() ?: "" else "") } @@ -54,7 +54,6 @@ fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) { ) { DatabaseEncryptionLayout( useKeychain, - prefs, m.chatDbEncrypted.value, currentKey, newKey, @@ -65,7 +64,16 @@ fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) { migration, onConfirmEncrypt = { withLongRunningApi { - encryptDatabase(currentKey, newKey, confirmNewKey, initialRandomDBPassphrase, useKeychain, storedKey, progressIndicator, migration) + encryptDatabase( + currentKey = currentKey, + newKey = newKey, + confirmNewKey = confirmNewKey, + initialRandomDBPassphrase = initialRandomDBPassphrase, + useKeychain = useKeychain, + storedKey = storedKey, + progressIndicator = progressIndicator, + migration = migration + ) } } ) @@ -89,7 +97,6 @@ fun DatabaseEncryptionView(m: ChatModel, migration: Boolean) { @Composable fun DatabaseEncryptionLayout( useKeychain: MutableState, - prefs: AppPreferences, chatDbEncrypted: Boolean?, currentKey: MutableState, newKey: MutableState, @@ -100,101 +107,104 @@ fun DatabaseEncryptionLayout( migration: Boolean, onConfirmEncrypt: () -> Unit, ) { - val (scrollBarAlpha, scrollModifier, scrollJob) = platform.desktopScrollBarComponents() - val scrollState = rememberScrollState() - Column( - if (!migration) Modifier.fillMaxWidth().verticalScroll(scrollState).then(if (appPlatform.isDesktop) scrollModifier else Modifier) else Modifier.fillMaxWidth(), - ) { - if (!migration) { - AppBarTitle(stringResource(MR.strings.database_passphrase)) - } else { - ChatStoppedView() - SectionSpacer() - } - SectionView(if (migration) generalGetString(MR.strings.database_passphrase).uppercase() else null) { - SavePassphraseSetting( - useKeychain.value, - initialRandomDBPassphrase.value, - storedKey.value, - enabled = (!initialRandomDBPassphrase.value && !progressIndicator.value) || migration - ) { checked -> - if (checked) { - setUseKeychain(true, useKeychain, prefs, migration) - } else if (storedKey.value && !migration) { - // Don't show in migration process since it will remove the key after successful encryption - removePassphraseAlert { - removePassphraseFromKeyChain(useKeychain, prefs, storedKey, false) - } - } else { - setUseKeychain(false, useKeychain, prefs, migration) - } + @Composable + fun Layout() { + Column { + if (!migration) { + AppBarTitle(stringResource(MR.strings.database_passphrase)) + } else { + ChatStoppedView() + SectionSpacer() } + SectionView(if (migration) generalGetString(MR.strings.database_passphrase).uppercase() else null) { + SavePassphraseSetting( + useKeychain.value, + initialRandomDBPassphrase.value, + storedKey.value, + enabled = (!initialRandomDBPassphrase.value && !progressIndicator.value) || migration + ) { checked -> + if (checked) { + setUseKeychain(true, useKeychain, migration) + } else if (storedKey.value && !migration) { + // Don't show in migration process since it will remove the key after successful encryption + removePassphraseAlert { + removePassphraseFromKeyChain(useKeychain, storedKey, false) + } + } else { + setUseKeychain(false, useKeychain, migration) + } + } + + if (!initialRandomDBPassphrase.value && chatDbEncrypted == true) { + PassphraseField( + currentKey, + generalGetString(MR.strings.current_passphrase), + modifier = Modifier.padding(horizontal = DEFAULT_PADDING), + isValid = ::validKey, + keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), + ) + } - if (!initialRandomDBPassphrase.value && chatDbEncrypted == true) { PassphraseField( - currentKey, - generalGetString(MR.strings.current_passphrase), + newKey, + generalGetString(MR.strings.new_passphrase), modifier = Modifier.padding(horizontal = DEFAULT_PADDING), + showStrength = true, isValid = ::validKey, keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), ) - } - - PassphraseField( - newKey, - generalGetString(MR.strings.new_passphrase), - modifier = Modifier.padding(horizontal = DEFAULT_PADDING), - showStrength = true, - isValid = ::validKey, - keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), - ) - val onClickUpdate = { - // Don't do things concurrently. Shouldn't be here concurrently, just in case - if (!progressIndicator.value) { - if (currentKey.value == "") { - if (useKeychain.value) - encryptDatabaseSavedAlert(onConfirmEncrypt) - else - encryptDatabaseAlert(onConfirmEncrypt) - } else { - if (useKeychain.value) - changeDatabaseKeySavedAlert(onConfirmEncrypt) - else - changeDatabaseKeyAlert(onConfirmEncrypt) + val onClickUpdate = { + // Don't do things concurrently. Shouldn't be here concurrently, just in case + if (!progressIndicator.value) { + if (currentKey.value == "") { + if (useKeychain.value) + encryptDatabaseSavedAlert(onConfirmEncrypt) + else + encryptDatabaseAlert(onConfirmEncrypt) + } else { + if (useKeychain.value) + changeDatabaseKeySavedAlert(onConfirmEncrypt) + else + changeDatabaseKeyAlert(onConfirmEncrypt) + } } } + val disabled = currentKey.value == newKey.value || + newKey.value != confirmNewKey.value || + newKey.value.isEmpty() || + !validKey(currentKey.value) || + !validKey(newKey.value) || + progressIndicator.value + + PassphraseField( + confirmNewKey, + generalGetString(MR.strings.confirm_new_passphrase), + modifier = Modifier.padding(horizontal = DEFAULT_PADDING), + isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value }, + keyboardActions = KeyboardActions(onDone = { + if (!disabled) onClickUpdate() + defaultKeyboardAction(ImeAction.Done) + }), + ) + + SectionItemViewSpaceBetween(onClickUpdate, disabled = disabled, minHeight = TextFieldDefaults.MinHeight) { + Text(generalGetString(if (migration) MR.strings.set_passphrase else MR.strings.update_database_passphrase), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) + } } - val disabled = currentKey.value == newKey.value || - newKey.value != confirmNewKey.value || - newKey.value.isEmpty() || - !validKey(currentKey.value) || - !validKey(newKey.value) || - progressIndicator.value - PassphraseField( - confirmNewKey, - generalGetString(MR.strings.confirm_new_passphrase), - modifier = Modifier.padding(horizontal = DEFAULT_PADDING), - isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value }, - keyboardActions = KeyboardActions(onDone = { - if (!disabled) onClickUpdate() - defaultKeyboardAction(ImeAction.Done) - }), - ) - - SectionItemViewSpaceBetween(onClickUpdate, disabled = disabled, minHeight = TextFieldDefaults.MinHeight) { - Text(generalGetString(if (migration) MR.strings.set_passphrase else MR.strings.update_database_passphrase), color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) + Column { + DatabaseEncryptionFooter(useKeychain, chatDbEncrypted, storedKey, initialRandomDBPassphrase, migration) } + SectionBottomSpacer() } - - Column { - DatabaseEncryptionFooter(useKeychain, chatDbEncrypted, storedKey, initialRandomDBPassphrase, migration) - } - SectionBottomSpacer() } - if (appPlatform.isDesktop && !migration) { - Box(Modifier.fillMaxSize()) { - platform.desktopScrollBar(scrollState, Modifier.align(Alignment.CenterEnd).fillMaxHeight(), scrollBarAlpha, scrollJob, false) + if (migration) { + Column(Modifier.fillMaxWidth()) { + Layout() + } + } else { + ColumnWithScrollBar(Modifier.fillMaxWidth(), maxIntrinsicSize = true) { + Layout() } } } @@ -272,17 +282,17 @@ fun resetFormAfterEncryption( m.controller.appPrefs.initialRandomDBPassphrase.set(false) } -fun setUseKeychain(value: Boolean, useKeychain: MutableState, prefs: AppPreferences, migration: Boolean) { +fun setUseKeychain(value: Boolean, useKeychain: MutableState, migration: Boolean) { useKeychain.value = value // Postpone it when migrating to the end of encryption process if (!migration) { - prefs.storeDBPassphrase.set(value) + appPrefs.storeDBPassphrase.set(value) } } -private fun removePassphraseFromKeyChain(useKeychain: MutableState, prefs: AppPreferences, storedKey: MutableState, migration: Boolean) { +private fun removePassphraseFromKeyChain(useKeychain: MutableState, storedKey: MutableState, migration: Boolean) { DatabaseUtils.ksDatabasePassword.remove() - setUseKeychain(false, useKeychain, prefs, migration) + setUseKeychain(false, useKeychain, migration) storedKey.value = false } @@ -415,15 +425,14 @@ suspend fun encryptDatabase( migration: Boolean, ): Boolean { val m = ChatModel - val prefs = ChatController.appPrefs progressIndicator.value = true return try { - prefs.encryptionStartedAt.set(Clock.System.now()) + appPrefs.encryptionStartedAt.set(Clock.System.now()) if (!m.chatDbChanged.value) { m.controller.apiSaveAppSettings(AppSettings.current.prepareForExport()) } val error = m.controller.apiStorageEncryption(currentKey.value, newKey.value) - prefs.encryptionStartedAt.set(null) + appPrefs.encryptionStartedAt.set(null) val sqliteError = ((error?.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorExport)?.sqliteError when { sqliteError is SQLiteError.ErrorNotADatabase -> { @@ -451,8 +460,8 @@ suspend fun encryptDatabase( resetFormAfterEncryption(m, initialRandomDBPassphrase, currentKey, newKey, confirmNewKey, storedKey, useKeychain.value) if (useKeychain.value) { DatabaseUtils.ksDatabasePassword.set(new) - } else if (migration) { - removePassphraseFromKeyChain(useKeychain, prefs, storedKey, true) + } else { + removePassphraseFromKeyChain(useKeychain, storedKey, migration) } operationEnded(m, progressIndicator) { AlertManager.shared.showAlertMsg(generalGetString(MR.strings.database_encrypted)) @@ -523,7 +532,6 @@ fun PreviewDatabaseEncryptionLayout() { SimpleXTheme { DatabaseEncryptionLayout( useKeychain = remember { mutableStateOf(true) }, - prefs = AppPreferences(), chatDbEncrypted = true, currentKey = remember { mutableStateOf("") }, newKey = remember { mutableStateOf("") }, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt index 5f0356bb2d..109e5bc737 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseErrorView.kt @@ -22,8 +22,11 @@ import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.AppVersionText +import chat.simplex.common.views.usersettings.SettingsActionItem import chat.simplex.res.MR import dev.icerock.moko.resources.StringResource +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.* import kotlinx.datetime.Clock import java.io.File @@ -106,7 +109,7 @@ fun DatabaseErrorView( } } is DBMigrationResult.ErrorMigration -> when (val err = status.migrationError) { - is MigrationError.Upgrade -> + is MigrationError.Upgrade -> { DatabaseErrorDetails(MR.strings.database_upgrade) { TextButton({ callRunChat(confirmMigrations = MigrationConfirmation.YesUp) }, Modifier.align(Alignment.CenterHorizontally), enabled = !progressIndicator.value) { Text(generalGetString(MR.strings.upgrade_and_open_chat)) @@ -116,7 +119,9 @@ fun DatabaseErrorView( MigrationsText(err.upMigrations.map { it.upName }) AppVersionText() } - is MigrationError.Downgrade -> + OpenDatabaseDirectoryButton() + } + is MigrationError.Downgrade -> { DatabaseErrorDetails(MR.strings.database_downgrade) { TextButton({ callRunChat(confirmMigrations = MigrationConfirmation.YesUpDown) }, Modifier.align(Alignment.CenterHorizontally), enabled = !progressIndicator.value) { Text(generalGetString(MR.strings.downgrade_and_open_chat)) @@ -127,29 +132,41 @@ fun DatabaseErrorView( MigrationsText(err.downMigrations) AppVersionText() } - is MigrationError.Error -> + OpenDatabaseDirectoryButton() + } + is MigrationError.Error -> { DatabaseErrorDetails(MR.strings.incompatible_database_version) { FileNameText(status.dbFile) Text(String.format(generalGetString(MR.strings.error_with_info), mtrErrorDescription(err.mtrError))) } + OpenDatabaseDirectoryButton() + } } - is DBMigrationResult.ErrorSQL -> + is DBMigrationResult.ErrorSQL -> { DatabaseErrorDetails(MR.strings.database_error) { FileNameText(status.dbFile) Text(String.format(generalGetString(MR.strings.error_with_info), status.migrationSQLError)) } - is DBMigrationResult.ErrorKeychain -> + OpenDatabaseDirectoryButton() + } + is DBMigrationResult.ErrorKeychain -> { DatabaseErrorDetails(MR.strings.keychain_error) { Text(generalGetString(MR.strings.cannot_access_keychain)) } - is DBMigrationResult.InvalidConfirmation -> + OpenDatabaseDirectoryButton() + } + is DBMigrationResult.InvalidConfirmation -> { DatabaseErrorDetails(MR.strings.invalid_migration_confirmation) { // this can only happen if incorrect parameter is passed } - is DBMigrationResult.Unknown -> + OpenDatabaseDirectoryButton() + } + is DBMigrationResult.Unknown -> { DatabaseErrorDetails(MR.strings.database_error) { Text(String.format(generalGetString(MR.strings.unknown_database_error_with_info), status.json)) } + OpenDatabaseDirectoryButton() + } is DBMigrationResult.OK -> {} null -> {} } @@ -294,6 +311,18 @@ private fun ColumnScope.SaveAndOpenButton(enabled: Boolean, onClick: () -> Unit) } } +@Composable +private fun OpenDatabaseDirectoryButton() { + if (appPlatform.isDesktop) { + Spacer(Modifier.padding(top = DEFAULT_PADDING)) + SettingsActionItem( + painterResource(MR.images.ic_folder_open), + stringResource(MR.strings.open_database_folder), + ::desktopOpenDatabaseDir + ) + } +} + @Composable private fun ColumnScope.OpenChatButton(enabled: Boolean, onClick: () -> Unit) { TextButton(onClick, Modifier.align(Alignment.CenterHorizontally), enabled = enabled) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt index 40dfbeac73..333fda307a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/database/DatabaseView.kt @@ -18,8 +18,9 @@ import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel.controller -import chat.simplex.common.model.ChatModel.updatingChatsMutex +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.* @@ -194,7 +195,7 @@ fun DatabaseLayout( stringResource(MR.strings.stop_chat_to_enable_database_actions) } ) - SectionDividerSpaced() + SectionDividerSpaced(maxTopPadding = true) } SectionView(stringResource(MR.strings.chat_database_section)) { @@ -263,7 +264,7 @@ fun DatabaseLayout( disabled = operationsDisabled ) } - SectionDividerSpaced(maxTopPadding = true) + SectionDividerSpaced() SectionView(stringResource(MR.strings.files_and_media_section).uppercase()) { val deleteFilesDisabled = operationsDisabled || appFilesCountAndSize.value.first == 0 @@ -448,6 +449,11 @@ private fun stopChat(m: ChatModel, progressIndicator: MutableState? = n progressIndicator?.value = true stopChatAsync(m) platform.androidChatStopped() + // close chat view for desktop + chatModel.chatId.value = null + if (appPlatform.isDesktop) { + ModalManager.end.closeModals() + } onStop?.invoke() } catch (e: Error) { m.chatRunning.value = true @@ -486,15 +492,24 @@ fun deleteChatDatabaseFilesAndState() { tmpDir.deleteRecursively() getMigrationTempFilesDirectory().deleteRecursively() tmpDir.mkdir() + wallpapersDir.deleteRecursively() + wallpapersDir.mkdirs() DatabaseUtils.ksDatabasePassword.remove() + appPrefs.newDatabaseInitialized.set(false) controller.appPrefs.storeDBPassphrase.set(true) controller.ctrl = null // Clear sensitive data on screen just in case ModalManager will fail to prevent hiding its modals while database encrypts itself chatModel.chatId.value = null chatModel.chatItems.clear() - chatModel.chats.clear() + withLongRunningApi { + withChats { + chats.clear() + popChatCollector.clear() + } + } chatModel.users.clear() + ntfManager.cancelAllNotifications() } private fun exportArchive( @@ -508,9 +523,17 @@ private fun exportArchive( progressIndicator.value = true withLongRunningApi { try { - val archiveFile = exportChatArchive(m, null, chatArchiveName, chatArchiveTime, chatArchiveFile) + val (archiveFile, archiveErrors) = exportChatArchive(m, null, chatArchiveName, chatArchiveTime, chatArchiveFile) chatArchiveFile.value = archiveFile - saveArchiveLauncher.launch(archiveFile.substringAfterLast(File.separator)) + if (archiveErrors.isEmpty()) { + saveArchiveLauncher.launch(archiveFile.substringAfterLast(File.separator)) + } else { + showArchiveExportedWithErrorsAlert(generalGetString(MR.strings.chat_database_exported_save), archiveErrors) { + withLongRunningApi { + saveArchiveLauncher.launch(archiveFile.substringAfterLast(File.separator)) + } + } + } progressIndicator.value = false } catch (e: Error) { AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_exporting_chat_database), e.toString()) @@ -525,7 +548,7 @@ suspend fun exportChatArchive( chatArchiveName: MutableState, chatArchiveTime: MutableState, chatArchiveFile: MutableState -): String { +): Pair> { val archiveTime = Clock.System.now() val ts = SimpleDateFormat("yyyy-MM-dd'T'HHmmss", Locale.US).format(Date.from(archiveTime.toJavaInstant())) val archiveName = "simplex-chat.$ts.zip" @@ -535,7 +558,8 @@ suspend fun exportChatArchive( if (!m.chatDbChanged.value) { controller.apiSaveAppSettings(AppSettings.current.prepareForExport()) } - m.controller.apiExportArchive(config) + wallpapersDir.mkdirs() + val archiveErrors = m.controller.apiExportArchive(config) if (storagePath == null) { deleteOldArchive(m) m.controller.appPrefs.chatArchiveName.set(archiveName) @@ -544,7 +568,7 @@ suspend fun exportChatArchive( chatArchiveName.value = archiveName chatArchiveTime.value = archiveTime chatArchiveFile.value = archivePath - return archivePath + return archivePath to archiveErrors } private fun deleteOldArchive(m: ChatModel) { @@ -577,6 +601,28 @@ private fun importArchiveAlert( ) } +fun showArchiveImportedWithErrorsAlert(archiveErrors: List) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.chat_database_imported), + text = generalGetString(MR.strings.restart_the_app_to_use_imported_chat_database) + "\n\n" + generalGetString(MR.strings.non_fatal_errors_occured_during_import) + archiveErrorsText(archiveErrors)) +} + +fun showArchiveExportedWithErrorsAlert(description: String, archiveErrors: List, onConfirm: () -> Unit) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.chat_database_exported_title), + text = description + "\n\n" + generalGetString(MR.strings.chat_database_exported_not_all_files) + archiveErrorsText(archiveErrors), + confirmText = generalGetString(MR.strings.chat_database_exported_continue), + onConfirm = onConfirm + ) +} + +private fun archiveErrorsText(errs: List): String = "\n" + errs.map { + when (it) { + is ArchiveError.ArchiveErrorImport -> it.importError + is ArchiveError.ArchiveErrorFile -> "${it.file}: ${it.fileError}" + } +}.joinToString(separator = "\n") + private fun importArchive( m: ChatModel, importedArchiveURI: URI, @@ -590,6 +636,7 @@ private fun importArchive( withLongRunningApi { try { m.controller.apiDeleteStorage() + wallpapersDir.mkdirs() try { val config = ArchiveConfig(archivePath, parentTempDirectory = databaseExportDir.toString()) val archiveErrors = m.controller.apiImportArchive(config) @@ -605,7 +652,7 @@ private fun importArchive( } } else { operationEnded(m, progressIndicator) { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.chat_database_imported), text = generalGetString(MR.strings.restart_the_app_to_use_imported_chat_database) + "\n" + generalGetString(MR.strings.non_fatal_errors_occured_during_import)) + showArchiveImportedWithErrorsAlert(archiveErrors) } } } catch (e: Error) { @@ -702,10 +749,10 @@ private fun afterSetCiTTL( appFilesCountAndSize.value = directoryFileCountAndSize(appFilesDir.absolutePath) withApi { try { - updatingChatsMutex.withLock { + withChats { // this is using current remote host on purpose - if it changes during update, it will load correct chats val chats = m.controller.apiGetChats(m.remoteHostId()) - m.updateChats(chats) + updateChats(chats) } } catch (e: Exception) { Log.e(TAG, "apiGetChats error: ${e.message}") diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt index 98f3921059..6bfcf2809f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt @@ -69,16 +69,19 @@ class AlertManager { fun showAlertDialogButtonsColumn( title: String, text: String? = null, + textAlign: TextAlign = TextAlign.Center, + dismissible: Boolean = true, onDismissRequest: (() -> Unit)? = null, hostDevice: Pair? = null, + belowTextContent: @Composable (() -> Unit) = {}, buttons: @Composable () -> Unit, ) { showAlert { AlertDialog( - onDismissRequest = { onDismissRequest?.invoke(); hideAlert() }, + onDismissRequest = { onDismissRequest?.invoke(); if (dismissible) hideAlert() }, title = alertTitle(title), buttons = { - AlertContent(text, hostDevice, extraPadding = true) { + AlertContent(text, hostDevice, extraPadding = true, textAlign = textAlign, belowTextContent = belowTextContent) { buttons() } }, @@ -286,7 +289,14 @@ private fun alertTitle(title: String): (@Composable () -> Unit)? { } @Composable -private fun AlertContent(text: String?, hostDevice: Pair?, extraPadding: Boolean = false, content: @Composable (() -> Unit)) { +private fun AlertContent( + text: String?, + hostDevice: Pair?, + extraPadding: Boolean = false, + textAlign: TextAlign = TextAlign.Center, + belowTextContent: @Composable (() -> Unit) = {}, + content: @Composable (() -> Unit) +) { BoxWithConstraints { Column( Modifier @@ -300,17 +310,20 @@ private fun AlertContent(text: String?, hostDevice: Pair?, extraP CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) { if (text != null) { Column(Modifier.heightIn(max = this@BoxWithConstraints.maxHeight * 0.7f) + .padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING) .verticalScroll(rememberScrollState()) ) { SelectionContainer { Text( escapedHtmlToAnnotatedString(text, LocalDensity.current), - Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING, bottom = DEFAULT_PADDING * 1.5f), + Modifier.fillMaxWidth(), fontSize = 16.sp, - textAlign = TextAlign.Center, + textAlign = textAlign, color = MaterialTheme.colors.secondary ) } + belowTextContent() + Spacer(Modifier.height(DEFAULT_PADDING * 1.5f)) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt index 34d916781b..1289687601 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatInfoImage.kt @@ -2,14 +2,16 @@ package chat.simplex.common.views.helpers import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.* import androidx.compose.material.Icon import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.* import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.* import androidx.compose.ui.layout.ContentScale @@ -25,7 +27,7 @@ import dev.icerock.moko.resources.ImageResource import kotlin.math.max @Composable -fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme.colors.secondaryVariant) { +fun ChatInfoImage(chatInfo: ChatInfo, size: Dp, iconColor: Color = MaterialTheme.colors.secondaryVariant, shadow: Boolean = false) { val icon = when (chatInfo) { is ChatInfo.Group -> MR.images.ic_supervised_user_circle_filled @@ -51,7 +53,9 @@ fun ProfileImage( size: Dp, image: String? = null, icon: ImageResource = MR.images.ic_account_circle_filled, - color: Color = MaterialTheme.colors.secondaryVariant + color: Color = MaterialTheme.colors.secondaryVariant, + backgroundColor: Color? = null, + blurred: Boolean = false ) { Box(Modifier.size(size)) { if (image == null) { @@ -61,6 +65,9 @@ fun ProfileImage( else -> null } if (iconToReplace != null) { + if (backgroundColor != null) { + Box(Modifier.size(size * 0.7f).align(Alignment.Center).background(backgroundColor, CircleShape)) + } Icon( iconToReplace, contentDescription = stringResource(MR.strings.icon_descr_profile_image_placeholder), @@ -81,7 +88,7 @@ fun ProfileImage( imageBitmap, stringResource(MR.strings.image_descr_profile_image), contentScale = ContentScale.Crop, - modifier = ProfileIconModifier(size) + modifier = ProfileIconModifier(size, blurred = blurred) ) } } @@ -102,12 +109,12 @@ private const val squareToCircleRatio = 0.935f private const val radiusFactor = (1 - squareToCircleRatio) / 50 @Composable -fun ProfileIconModifier(size: Dp, padding: Boolean = true): Modifier { +fun ProfileIconModifier(size: Dp, padding: Boolean = true, blurred: Boolean = false): Modifier { val percent = remember { appPreferences.profileImageCornerRadius.state } val r = max(0f, percent.value) val pad = if (padding) size / 12 else 0.dp val m = Modifier.size(size) - return when { + val m1 = when { r >= 50 -> m.padding(pad).clip(CircleShape) r <= 0 -> { @@ -119,6 +126,7 @@ fun ProfileIconModifier(size: Dp, padding: Boolean = true): Modifier { m.padding((size - sz) / 2).clip(RoundedCornerShape(size = sz * r / 100)) } } + return if (blurred) m1.blur(size / 4) else m1 } /** [AccountCircleFilled] has its inner padding which leads to visible border if there is background underneath. diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatWallpaper.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatWallpaper.kt new file mode 100644 index 0000000000..8921685cd6 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ChatWallpaper.kt @@ -0,0 +1,447 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.ui.draw.CacheDrawScope +import androidx.compose.ui.draw.DrawResult +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.drawscope.* +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.* +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.ui.theme.ThemeManager.colorFromReadableHex +import chat.simplex.res.MR +import dev.icerock.moko.resources.ImageResource +import dev.icerock.moko.resources.StringResource +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import java.io.File +import kotlin.math.* + +enum class PresetWallpaper( + val res: ImageResource, + val filename: String, + val scale: Float, + val background: Map, + val tint: Map, + val colors: Map, +) { + CATS(MR.images.wallpaper_cats, "cats", 0.63f, + wallpaperBackgrounds(light = "#ffF8F6EA"), + tint = mapOf( + DefaultTheme.LIGHT to "#ffefdca6".colorFromReadableHex(), + DefaultTheme.DARK to "#ff4b3b0e".colorFromReadableHex(), + DefaultTheme.SIMPLEX to "#ff51400f".colorFromReadableHex(), + DefaultTheme.BLACK to "#ff4b3b0e".colorFromReadableHex() + ), + mapOf( + DefaultTheme.LIGHT to ThemeColors( + sentMessage = "#fffffaed", + sentQuote = "#fffaf0d6", + receivedMessage = "#ffF8F7F4", + receivedQuote = "#ffefede9", + ), + DefaultTheme.DARK to ThemeColors( + sentMessage = "#ff2f2919", + sentQuote = "#ff473a1d", + receivedMessage = "#ff272624", + receivedQuote = "#ff373633", + ), + DefaultTheme.SIMPLEX to ThemeColors( + sentMessage = "#ff41371b", + sentQuote = "#ff654f1c", + receivedMessage = "#ff272624", + receivedQuote = "#ff373633", + ), + DefaultTheme.BLACK to ThemeColors( + sentMessage = "#ff41371b", + sentQuote = "#ff654f1c", + receivedMessage = "#ff1f1e1b", + receivedQuote = "#ff2f2d27", + ), + ) + ), + FLOWERS(MR.images.wallpaper_flowers, "flowers", 0.53f, + wallpaperBackgrounds(light = "#ffE2FFE4"), + tint = mapOf( + DefaultTheme.LIGHT to "#ff9CEA59".colorFromReadableHex(), + DefaultTheme.DARK to "#ff31560D".colorFromReadableHex(), + DefaultTheme.SIMPLEX to "#ff36600f".colorFromReadableHex(), + DefaultTheme.BLACK to "#ff31560D".colorFromReadableHex() + ), + mapOf( + DefaultTheme.LIGHT to ThemeColors( + sentMessage = "#fff1ffe5", + sentQuote = "#ffdcf9c4", + receivedMessage = "#ffF4F8F2", + receivedQuote = "#ffe7ece7", + ), + DefaultTheme.DARK to ThemeColors( + sentMessage = "#ff163521", + sentQuote = "#ff1B5330", + receivedMessage = "#ff242523", + receivedQuote = "#ff353733", + ), + DefaultTheme.SIMPLEX to ThemeColors( + sentMessage = "#ff184739", + sentQuote = "#ff1F6F4B", + receivedMessage = "#ff242523", + receivedQuote = "#ff353733", + ), + DefaultTheme.BLACK to ThemeColors( + sentMessage = "#ff184739", + sentQuote = "#ff1F6F4B", + receivedMessage = "#ff1c1f1a", + receivedQuote = "#ff282b25", + ), + ) + ), + HEARTS(MR.images.wallpaper_hearts, "hearts", 0.59f, + wallpaperBackgrounds(light = "#ffFDECEC"), + tint = mapOf( + DefaultTheme.LIGHT to "#fffde0e0".colorFromReadableHex(), + DefaultTheme.DARK to "#ff3c0f0f".colorFromReadableHex(), + DefaultTheme.SIMPLEX to "#ff411010".colorFromReadableHex(), + DefaultTheme.BLACK to "#ff3C0F0F".colorFromReadableHex() + ), + mapOf( + DefaultTheme.LIGHT to ThemeColors( + sentMessage = "#fffff4f4", + sentQuote = "#ffffdfdf", + receivedMessage = "#fff8f6f6", + receivedQuote = "#ffefebeb", + ), + DefaultTheme.DARK to ThemeColors( + sentMessage = "#ff301515", + sentQuote = "#ff4C1818", + receivedMessage = "#ff242121", + receivedQuote = "#ff3b3535", + ), + DefaultTheme.SIMPLEX to ThemeColors( + sentMessage = "#ff491A28", + sentQuote = "#ff761F29", + receivedMessage = "#ff242121", + receivedQuote = "#ff3b3535", + ), + DefaultTheme.BLACK to ThemeColors( + sentMessage = "#ff491A28", + sentQuote = "#ff761F29", + receivedMessage = "#ff1f1b1b", + receivedQuote = "#ff2e2626", + ), + ) + ), + KIDS(MR.images.wallpaper_kids, "kids", 0.53f, + wallpaperBackgrounds(light = "#ffdbfdfb"), + tint = mapOf( + DefaultTheme.LIGHT to "#ffadeffc".colorFromReadableHex(), + DefaultTheme.DARK to "#ff16404B".colorFromReadableHex(), + DefaultTheme.SIMPLEX to "#ff184753".colorFromReadableHex(), + DefaultTheme.BLACK to "#ff16404B".colorFromReadableHex() + ), + mapOf( + DefaultTheme.LIGHT to ThemeColors( + sentMessage = "#ffeafeff", + sentQuote = "#ffcbf4f7", + receivedMessage = "#fff3fafa", + receivedQuote = "#ffe4efef", + ), + DefaultTheme.DARK to ThemeColors( + sentMessage = "#ff16302F", + sentQuote = "#ff1a4a49", + receivedMessage = "#ff252626", + receivedQuote = "#ff373A39", + ), + DefaultTheme.SIMPLEX to ThemeColors( + sentMessage = "#ff1a4745", + sentQuote = "#ff1d6b69", + receivedMessage = "#ff252626", + receivedQuote = "#ff373a39", + ), + DefaultTheme.BLACK to ThemeColors( + sentMessage = "#ff1a4745", + sentQuote = "#ff1d6b69", + receivedMessage = "#ff1e1f1f", + receivedQuote = "#ff262b29", + ), + ) + ), + SCHOOL(MR.images.wallpaper_school, "school", 0.53f, + wallpaperBackgrounds(light = "#ffE7F5FF"), + tint = mapOf( + DefaultTheme.LIGHT to "#ffCEEBFF".colorFromReadableHex(), + DefaultTheme.DARK to "#ff0F293B".colorFromReadableHex(), + DefaultTheme.SIMPLEX to "#ff112f43".colorFromReadableHex(), + DefaultTheme.BLACK to "#ff0F293B".colorFromReadableHex() + ), + mapOf( + DefaultTheme.LIGHT to ThemeColors( + sentMessage = "#ffeef9ff", + sentQuote = "#ffD6EDFA", + receivedMessage = "#ffF3F5F9", + receivedQuote = "#ffe4e8ee", + ), + DefaultTheme.DARK to ThemeColors( + sentMessage = "#ff172833", + sentQuote = "#ff1C3E4F", + receivedMessage = "#ff26282c", + receivedQuote = "#ff393c40", + ), + DefaultTheme.SIMPLEX to ThemeColors( + sentMessage = "#ff1A3C5D", + sentQuote = "#ff235b80", + receivedMessage = "#ff26282c", + receivedQuote = "#ff393c40", + ), + DefaultTheme.BLACK to ThemeColors( + sentMessage = "#ff1A3C5D", + sentQuote = "#ff235b80", + receivedMessage = "#ff1d1e22", + receivedQuote = "#ff292b2f", + ), + ) + ), + TRAVEL(MR.images.wallpaper_travel, "travel", 0.68f, + wallpaperBackgrounds(light = "#fff9eeff"), + tint = mapOf( + DefaultTheme.LIGHT to "#ffeedbfe".colorFromReadableHex(), + DefaultTheme.DARK to "#ff311E48".colorFromReadableHex(), + DefaultTheme.SIMPLEX to "#ff35204e".colorFromReadableHex(), + DefaultTheme.BLACK to "#ff311E48".colorFromReadableHex() + ), + mapOf( + DefaultTheme.LIGHT to ThemeColors( + sentMessage = "#fffcf6ff", + sentQuote = "#fff2e0fc", + receivedMessage = "#ffF6F4F7", + receivedQuote = "#ffede9ee", + ), + DefaultTheme.DARK to ThemeColors( + sentMessage = "#ff33263B", + sentQuote = "#ff53385E", + receivedMessage = "#ff272528", + receivedQuote = "#ff3B373E", + ), + DefaultTheme.SIMPLEX to ThemeColors( + sentMessage = "#ff3C255D", + sentQuote = "#ff623485", + receivedMessage = "#ff26273B", + receivedQuote = "#ff3A394F", + ), + DefaultTheme.BLACK to ThemeColors( + sentMessage = "#ff3C255D", + sentQuote = "#ff623485", + receivedMessage = "#ff231f23", + receivedQuote = "#ff2c2931", + ), + ) + ); + + fun toType(base: DefaultTheme, scale: Float? = null): WallpaperType = + WallpaperType.Preset( + filename, + scale ?: appPrefs.themeOverrides.get().firstOrNull { it.wallpaper != null && it.wallpaper.preset == filename && it.base == base }?.wallpaper?.scale ?: 1f + ) + + companion object { + fun from(filename: String): PresetWallpaper? = + entries.firstOrNull { it.filename == filename } + } +} + +fun wallpaperBackgrounds(light: String): Map = + mapOf( + DefaultTheme.LIGHT to light.colorFromReadableHex(), + DefaultTheme.DARK to "#ff121212".colorFromReadableHex(), + DefaultTheme.SIMPLEX to "#ff111528".colorFromReadableHex(), + DefaultTheme.BLACK to "#ff070707".colorFromReadableHex() + ) + +@Serializable +enum class WallpaperScaleType(val contentScale: ContentScale, val text: StringResource) { + @SerialName("fill") FILL(ContentScale.Crop, MR.strings.wallpaper_scale_fill), + @SerialName("fit") FIT(ContentScale.Fit, MR.strings.wallpaper_scale_fit), + @SerialName("repeat") REPEAT(ContentScale.Fit, MR.strings.wallpaper_scale_repeat), +} + +sealed class WallpaperType { + abstract val scale: Float? + + val image by lazy { + val filename = when (this) { + is Preset -> filename + is Image -> filename + else -> return@lazy null + } + if (filename == "") return@lazy null + if (cachedImages[filename] != null) { + cachedImages[filename] + } else { + val res = if (this is Preset) { + (PresetWallpaper.from(filename) ?: PresetWallpaper.CATS).res.toComposeImageBitmap()!! + } else { + try { + // In case of unintentional image deletion don't crash the app + File(getWallpaperFilePath(filename)).inputStream().use { loadImageBitmap(it) } + } catch (e: Exception) { + Log.e(TAG, "Error while loading wallpaper file: ${e.stackTraceToString()}") + null + } + } + res?.prepareToDraw() + cachedImages[filename] = res ?: return@lazy null + res + } + } + + fun sameType(other: WallpaperType?): Boolean = + if (this is Preset && other is Preset) this.filename == other.filename + else this.javaClass == other?.javaClass + + fun samePreset(other: PresetWallpaper?): Boolean = this is Preset && filename == other?.filename + + data class Preset( + val filename: String, + override val scale: Float?, + ): WallpaperType() { + val predefinedImageScale = PresetWallpaper.from(filename)?.scale ?: 1f + } + + data class Image( + val filename: String, + override val scale: Float?, + val scaleType: WallpaperScaleType?, + ): WallpaperType() + + object Empty: WallpaperType() { + override val scale: Float? + get() = null + } + + fun defaultBackgroundColor(theme: DefaultTheme, materialBackground: Color): Color = + if (this is Preset) { + (PresetWallpaper.from(filename) ?: PresetWallpaper.CATS).background[theme]!! + } else { + materialBackground + } + + fun defaultTintColor(theme: DefaultTheme): Color = + if (this is Preset) { + (PresetWallpaper.from(filename) ?: PresetWallpaper.CATS).tint[theme]!! + } else if (this is Image && scaleType == WallpaperScaleType.REPEAT) { + Color.Transparent + } else { + Color.Transparent + } + + companion object { + var cachedImages: MutableMap = mutableMapOf() + + fun from(wallpaper: ThemeWallpaper?): WallpaperType? { + return if (wallpaper == null) { + null + } else if (wallpaper.preset != null) { + Preset(wallpaper.preset, wallpaper.scale) + } else if (wallpaper.imageFile != null) { + Image(wallpaper.imageFile, wallpaper.scale, wallpaper.scaleType) + } else { + Empty + } + } + } +} + +private fun drawToBitmap(image: ImageBitmap, imageScale: Float, tint: Color, size: Size, density: Float, layoutDirection: LayoutDirection): ImageBitmap { + val quality = if (appPlatform.isAndroid) FilterQuality.High else FilterQuality.Low + val drawScope = CanvasDrawScope() + val bitmap = ImageBitmap(size.width.toInt(), size.height.toInt()) + val canvas = Canvas(bitmap) + drawScope.draw( + density = Density(density), + layoutDirection = layoutDirection, + canvas = canvas, + size = size, + ) { + val scale = imageScale * density + for (h in 0..(size.height / image.height / scale).roundToInt()) { + for (w in 0..(size.width / image.width / scale).roundToInt()) { + drawImage( + image, + dstOffset = IntOffset(x = (w * image.width * scale).roundToInt(), y = (h * image.height * scale).roundToInt()), + dstSize = IntSize((image.width * scale).roundToInt(), (image.height * scale).roundToInt()), + colorFilter = ColorFilter.tint(tint, BlendMode.SrcIn), + filterQuality = quality + ) + } + } + } + return bitmap +} + +fun CacheDrawScope.chatViewBackground(image: ImageBitmap, imageType: WallpaperType, background: Color, tint: Color): DrawResult { + val imageScale = if (imageType is WallpaperType.Preset) { + (imageType.scale ?: 1f) * imageType.predefinedImageScale + } else if (imageType is WallpaperType.Image && imageType.scaleType == WallpaperScaleType.REPEAT) { + imageType.scale ?: 1f + } else { + 1f + } + val image = if (imageType is WallpaperType.Preset || (imageType is WallpaperType.Image && imageType.scaleType == WallpaperScaleType.REPEAT)) { + drawToBitmap(image, imageScale, tint, size, density, layoutDirection) + } else { + image + } + + return onDrawBehind { + val quality = if (appPlatform.isAndroid) FilterQuality.High else FilterQuality.Low + drawRect(background) + when (imageType) { + is WallpaperType.Preset -> drawImage(image) + is WallpaperType.Image -> when (val scaleType = imageType.scaleType ?: WallpaperScaleType.FILL) { + WallpaperScaleType.REPEAT -> drawImage(image) + WallpaperScaleType.FILL, WallpaperScaleType.FIT -> { + clipRect { + val scale = scaleType.contentScale.computeScaleFactor(Size(image.width.toFloat(), image.height.toFloat()), Size(size.width, size.height)) + val scaledWidth = (image.width * scale.scaleX).roundToInt() + val scaledHeight = (image.height * scale.scaleY).roundToInt() + // Large image will cause freeze + if (image.width > 4320 || image.height > 4320) return@clipRect + + drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) + if (scaleType == WallpaperScaleType.FIT) { + if (scaledWidth < size.width) { + // has black lines at left and right sides + var x = (size.width - scaledWidth) / 2 + while (x > 0) { + drawImage(image, dstOffset = IntOffset(x = (x - scaledWidth).roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) + x -= scaledWidth + } + x = size.width - (size.width - scaledWidth) / 2 + while (x < size.width) { + drawImage(image, dstOffset = IntOffset(x = x.roundToInt(), y = ((size.height - scaledHeight) / 2).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) + x += scaledWidth + } + } else { + // has black lines at top and bottom sides + var y = (size.height - scaledHeight) / 2 + while (y > 0) { + drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = (y - scaledHeight).roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) + y -= scaledHeight + } + y = size.height - (size.height - scaledHeight) / 2 + while (y < size.height) { + drawImage(image, dstOffset = IntOffset(x = ((size.width - scaledWidth) / 2).roundToInt(), y = y.roundToInt()), dstSize = IntSize(scaledWidth, scaledHeight), filterQuality = quality) + y += scaledHeight + } + } + } + } + drawRect(tint) + } + } + is WallpaperType.Empty -> {} + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt index 2fb27e29b1..080edd22b2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CloseSheetBar.kt @@ -6,52 +6,117 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp +import androidx.compose.foundation.background +import androidx.compose.ui.draw.* +import androidx.compose.ui.graphics.* +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.* +import chat.simplex.common.platform.appPlatform import chat.simplex.common.ui.theme.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource +import kotlin.math.absoluteValue @Composable -fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Color = if (close != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, endButtons: @Composable RowScope.() -> Unit = {}) { +fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Color = if (close != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, arrangement: Arrangement.Vertical = Arrangement.Top, closeBarTitle: String? = null, barPaddingValues: PaddingValues = PaddingValues(horizontal = AppBarHorizontalPadding), endButtons: @Composable RowScope.() -> Unit = {}) { + var rowModifier = Modifier + .fillMaxWidth() + .height(AppBarHeight * fontSizeSqrtMultiplier) + val themeBackgroundMix = MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f) + if (!closeBarTitle.isNullOrEmpty()) { + rowModifier = rowModifier.background(themeBackgroundMix) + } + val handler = LocalAppBarHandler.current + val connection = LocalAppBarHandler.current?.connection + val title = remember(handler?.title?.value) { handler?.title ?: mutableStateOf("") } + Column( - Modifier + verticalArrangement = arrangement, + modifier = Modifier .fillMaxWidth() - .heightIn(min = AppBarHeight) - .padding(horizontal = AppBarHorizontalPadding), + .heightIn(min = AppBarHeight * fontSizeSqrtMultiplier) + .drawWithCache { + val backgroundColor = if (appPlatform.isDesktop && connection != null) themeBackgroundMix.copy(alpha = topTitleAlpha(connection)) else Color.Transparent + onDrawBehind { + if (appPlatform.isDesktop) { + drawRect(backgroundColor) + } + } + } ) { Row( - Modifier - .padding(top = 4.dp), // Like in DefaultAppBar + modifier = Modifier.padding(barPaddingValues), content = { Row( - Modifier.fillMaxWidth().height(TextFieldDefaults.MinHeight), - horizontalArrangement = Arrangement.SpaceBetween, + rowModifier, verticalAlignment = Alignment.CenterVertically ) { - if (showClose) { + if (showClose) { NavigationButtonBack(tintColor = tintColor, onButtonClicked = close) } else { Spacer(Modifier) } + if (!closeBarTitle.isNullOrEmpty()) { + Row( + Modifier.weight(1f), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + closeBarTitle, + fontWeight = FontWeight.SemiBold, + maxLines = 1 + ) + } + } else if (title.value.isNotEmpty() && connection != null) { + Row( + Modifier + .padding(start = if (showClose) 0.dp else DEFAULT_PADDING_HALF) + .weight(1f) // hides the title if something wants full width (eg, search field in chat profiles screen) + .graphicsLayer { + alpha = topTitleAlpha((connection)) + } + .padding(start = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + title.value, + fontWeight = FontWeight.SemiBold, + maxLines = 1 + ) + } + } else { + Spacer(Modifier.weight(1f)) + } Row { endButtons() } } } ) + if (closeBarTitle.isNullOrEmpty() && title.value.isNotEmpty() && connection != null) { + Divider( + Modifier + .graphicsLayer { + alpha = topTitleAlpha(connection) + } + ) + } } } @Composable -fun AppBarTitle(title: String, hostDevice: Pair? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f) { +fun AppBarTitle(title: String, hostDevice: Pair? = null, withPadding: Boolean = true, bottomPadding: Dp = DEFAULT_PADDING * 1.5f + 8.dp) { + val handler = LocalAppBarHandler.current + val connection = handler?.connection + LaunchedEffect(title) { + handler?.title?.value = title + } val theme = CurrentColors.collectAsState() - val titleColor = CurrentColors.collectAsState().value.appColors.title + val titleColor = MaterialTheme.appColors.title val brush = if (theme.value.base == DefaultTheme.SIMPLEX) Brush.linearGradient(listOf(titleColor.darker(0.2f), titleColor.lighter(0.35f)), Offset(0f, Float.POSITIVE_INFINITY), Offset(Float.POSITIVE_INFINITY, 0f)) else // color is not updated when changing themes if I pass null here @@ -60,23 +125,37 @@ fun AppBarTitle(title: String, hostDevice: Pair? = null, withPad Text( title, Modifier - .fillMaxWidth() - .padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp,), + .padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, top = DEFAULT_PADDING_HALF, end = if (withPadding) DEFAULT_PADDING else 0.dp,) + .graphicsLayer { + alpha = bottomTitleAlpha(connection) + }, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.h1.copy(brush = brush), color = MaterialTheme.colors.primaryVariant, - textAlign = TextAlign.Center + textAlign = TextAlign.Start ) if (hostDevice != null) { - HostDeviceTitle(hostDevice) + Box(Modifier.padding(start = if (withPadding) DEFAULT_PADDING else 0.dp, end = if (withPadding) DEFAULT_PADDING else 0.dp).graphicsLayer { + alpha = bottomTitleAlpha(connection) + }) { + HostDeviceTitle(hostDevice) + } } Spacer(Modifier.height(bottomPadding)) } } +private fun topTitleAlpha(connection: CollapsingAppBarNestedScrollConnection) = + if (connection.appBarOffset.absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 0f + else ((-connection.appBarOffset * 1.5f) / (AppBarHandler.appBarMaxHeightPx)).coerceIn(0f, 1f) + +private fun bottomTitleAlpha(connection: CollapsingAppBarNestedScrollConnection?) = + if ((connection?.appBarOffset ?: 0f).absoluteValue < AppBarHandler.appBarMaxHeightPx / 3) 1f + else ((AppBarHandler.appBarMaxHeightPx) + (connection?.appBarOffset ?: 0f) / 1.5f).coerceAtLeast(0f) / AppBarHandler.appBarMaxHeightPx + @Composable private fun HostDeviceTitle(hostDevice: Pair, extraPadding: Boolean = false) { - Row(Modifier.fillMaxWidth().padding(top = 5.dp, bottom = if (extraPadding) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center) { + Row(Modifier.fillMaxWidth().padding(top = 5.dp, bottom = if (extraPadding) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start) { Icon(painterResource(if (hostDevice.first == null) MR.images.ic_desktop else MR.images.ic_smartphone_300), null, Modifier.size(15.dp), tint = MaterialTheme.colors.secondary) Spacer(Modifier.width(10.dp)) Text(hostDevice.second, color = MaterialTheme.colors.secondary) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CollapsingAppBar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CollapsingAppBar.kt new file mode 100644 index 0000000000..4410f7ada5 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/CollapsingAppBar.kt @@ -0,0 +1,44 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.* +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.unit.Velocity + +val LocalAppBarHandler: ProvidableCompositionLocal = staticCompositionLocalOf { null } + +@Stable +class AppBarHandler( + listState: LazyListState = LazyListState(0, 0), + scrollState: ScrollState = ScrollState(initial = 0) +) { + val title = mutableStateOf("") + var listState by mutableStateOf(listState, structuralEqualityPolicy()) + internal set + + var scrollState by mutableStateOf(scrollState, structuralEqualityPolicy()) + internal set + + val connection = CollapsingAppBarNestedScrollConnection() + + companion object { + var appBarMaxHeightPx: Int = 0 + } +} + +class CollapsingAppBarNestedScrollConnection(): NestedScrollConnection { + var appBarOffset: Float by mutableFloatStateOf(0f) + + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + appBarOffset += available.y + return Offset(0f, 0f) + } + + override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { + appBarOffset -= available.y + return Offset(x = 0f, 0f) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt index 0ad7af439f..c621f186cd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.kt @@ -39,22 +39,25 @@ object DatabaseUtils { } } - private fun hasDatabase(rootDir: String): Boolean = - File(rootDir + File.separator + chatDatabaseFileName).exists() && File(rootDir + File.separator + agentDatabaseFileName).exists() + private fun hasAtLeastOneDatabase(rootDir: String): Boolean = + File(rootDir + File.separator + chatDatabaseFileName).exists() || File(rootDir + File.separator + agentDatabaseFileName).exists() + + fun hasOnlyOneDatabase(rootDir: String): Boolean = + File(rootDir + File.separator + chatDatabaseFileName).exists() != File(rootDir + File.separator + agentDatabaseFileName).exists() fun useDatabaseKey(): String { Log.d(TAG, "useDatabaseKey ${appPreferences.storeDBPassphrase.get()}") var dbKey = "" val useKeychain = appPreferences.storeDBPassphrase.get() if (useKeychain) { - if (!hasDatabase(dataDir.absolutePath)) { + if (!hasAtLeastOneDatabase(dataDir.absolutePath)) { dbKey = randomDatabasePassword() ksDatabasePassword.set(dbKey) appPreferences.initialRandomDBPassphrase.set(true) } else { dbKey = ksDatabasePassword.get() ?: "" } - } else if (appPlatform.isDesktop && !hasDatabase(dataDir.absolutePath)) { + } else if (appPlatform.isDesktop && !hasAtLeastOneDatabase(dataDir.absolutePath)) { // In case of database was deleted by hand dbKey = randomDatabasePassword() ksDatabasePassword.set(dbKey) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultSwitch.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultSwitch.kt index 75abc67b46..79255fb0bf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultSwitch.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultSwitch.kt @@ -25,9 +25,9 @@ fun DefaultSwitch( ) ) { val color = if (checked) MaterialTheme.colors.primary.copy(alpha = 0.3f) else MaterialTheme.colors.secondary.copy(alpha = 0.3f) - val size = with(LocalDensity.current) { Size(46.dp.toPx(), 28.dp.toPx()) } - val offset = with(LocalDensity.current) { Offset(1.dp.toPx(), 10.dp.toPx()) } - val radius = with(LocalDensity.current) { 28.dp.toPx() } + val size = with(LocalDensity.current) { Size(40.dp.toPx(), 26.dp.toPx()) } + val offset = with(LocalDensity.current) { Offset(4.dp.toPx(), 11.dp.toPx()) } + val radius = with(LocalDensity.current) { 13.dp.toPx() } Switch( checked = checked, onCheckedChange = onCheckedChange, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt index 577411c7e3..28e9a997ae 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.* +import androidx.compose.ui.unit.Dp import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp @@ -15,7 +16,7 @@ import chat.simplex.res.MR @Composable fun DefaultTopAppBar( - navigationButton: @Composable RowScope.() -> Unit, + navigationButton: (@Composable RowScope.() -> Unit)? = null, title: (@Composable () -> Unit)?, onTitleClick: (() -> Unit)? = null, showSearch: Boolean, @@ -44,10 +45,19 @@ fun DefaultTopAppBar( } @Composable -fun NavigationButtonBack(onButtonClicked: (() -> Unit)?, tintColor: Color = if (onButtonClicked != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary) { +fun NavigationButtonBack(onButtonClicked: (() -> Unit)?, tintColor: Color = if (onButtonClicked != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, height: Dp = 24.dp) { IconButton(onButtonClicked ?: {}, enabled = onButtonClicked != null) { Icon( - painterResource(MR.images.ic_arrow_back_ios_new), stringResource(MR.strings.back), tint = tintColor + painterResource(MR.images.ic_arrow_back_ios_new), stringResource(MR.strings.back), Modifier.height(height), tint = tintColor + ) + } +} + +@Composable +fun NavigationButtonClose(onButtonClicked: (() -> Unit)?, tintColor: Color = if (onButtonClicked != null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, height: Dp = 24.dp) { + IconButton(onButtonClicked ?: {}, enabled = onButtonClicked != null) { + Icon( + painterResource(MR.images.ic_close), stringResource(MR.strings.back), Modifier.height(height), tint = tintColor ) } } @@ -84,7 +94,7 @@ private fun TopAppBar( Box( modifier .fillMaxWidth() - .height(AppBarHeight) + .height(AppBarHeight * fontSizeSqrtMultiplier) .background(backgroundColor) .padding(horizontal = 4.dp), contentAlignment = Alignment.CenterStart, @@ -125,5 +135,6 @@ private fun TopAppBar( val AppBarHeight = 56.dp val AppBarHorizontalPadding = 4.dp +val BottomAppBarHeight = 60.dp private val TitleInsetWithoutIcon = DEFAULT_PADDING - AppBarHorizontalPadding val TitleInsetWithIcon = 72.dp diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt index 7e57bda928..4141fd2ead 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ExposedDropDownSettingRow.kt @@ -50,7 +50,7 @@ fun ExposedDropDownSetting( ) Spacer(Modifier.size(12.dp)) Icon( - if (!expanded.value) painterResource(MR.images.ic_expand_more) else painterResource(MR.images.ic_expand_less), + if (!expanded.value) painterResource(MR.images.ic_arrow_drop_down) else painterResource(MR.images.ic_arrow_drop_up), generalGetString(MR.strings.icon_descr_more_button), tint = MaterialTheme.colors.secondary ) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt index 93d0a56766..cce7cf17a5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/LinkPreviews.kt @@ -1,12 +1,11 @@ package chat.simplex.common.views.helpers import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.Image -import androidx.compose.foundation.background +import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.material.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale @@ -15,9 +14,11 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.LinkPreview import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chat.chatViewScrollState import chat.simplex.res.MR import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -76,7 +77,7 @@ suspend fun getLinkPreview(url: String): LinkPreview? { @Composable fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit, cancelEnabled: Boolean) { - val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage + val sentColor = MaterialTheme.appColors.sentMessage Row( Modifier.fillMaxWidth().padding(top = 8.dp).background(sentColor), verticalAlignment = Alignment.CenterVertically @@ -121,12 +122,16 @@ fun ComposeLinkView(linkPreview: LinkPreview?, cancelPreview: () -> Unit, cancel } @Composable -fun ChatItemLinkView(linkPreview: LinkPreview) { +fun ChatItemLinkView(linkPreview: LinkPreview, showMenu: State, onLongClick: () -> Unit) { Column(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) { + val blurred = remember { mutableStateOf(appPrefs.privacyMediaBlurRadius.get() > 0) } Image( base64ToBitmap(linkPreview.image), stringResource(MR.strings.image_descr_link_preview), - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .desktopModifyBlurredState(true, blurred, showMenu) + .privacyBlur(true, blurred, chatViewScrollState.collectAsState(), onLongClick = onLongClick), contentScale = ContentScale.FillWidth, ) Column(Modifier.padding(top = 6.dp).padding(horizontal = 12.dp)) { @@ -179,7 +184,7 @@ private fun normalizeImageUri(u: URL, imageUri: String) = when { @Composable fun PreviewChatItemLinkView() { SimpleXTheme { - ChatItemLinkView(LinkPreview.sampleData) + ChatItemLinkView(LinkPreview.sampleData, remember { mutableStateOf(false) }) {} } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt index 887a5bfdd9..bfd61a2add 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ModalView.kt @@ -2,18 +2,22 @@ package chat.simplex.common.views.helpers import androidx.compose.animation.* import androidx.compose.animation.core.* +import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyListState import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import kotlinx.coroutines.flow.MutableStateFlow import java.util.concurrent.atomic.AtomicBoolean import kotlin.math.min +import kotlin.math.sqrt @Composable fun ModalView( @@ -22,6 +26,7 @@ fun ModalView( enableClose: Boolean = true, background: Color = MaterialTheme.colors.background, modifier: Modifier = Modifier, + closeOnTop: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable () -> Unit, ) { @@ -30,8 +35,12 @@ fun ModalView( } Surface(Modifier.fillMaxSize(), contentColor = LocalContentColor.current) { Column(if (background != MaterialTheme.colors.background) Modifier.background(background) else Modifier.themedBackground()) { - CloseSheetBar(if (enableClose) close else null, showClose, endButtons = endButtons) - Box(modifier) { content() } + if (closeOnTop) { + CloseSheetBar(if (enableClose) close else null, showClose, endButtons = endButtons) + } + Box(modifier = modifier) { + content() + } } } } @@ -40,32 +49,38 @@ enum class ModalPlacement { START, CENTER, END, FULLSCREEN } -class ModalData { - private val state = mutableMapOf>() +class ModalData() { + private val state = mutableMapOf>() fun stateGetOrPut (key: String, default: () -> T): MutableState = state.getOrPut(key) { mutableStateOf(default() as Any) } as MutableState + + fun stateGetOrPutNullable (key: String, default: () -> T?): MutableState = + state.getOrPut(key) { mutableStateOf(default() as Any?) } as MutableState + + val appBarHandler = AppBarHandler() } class ModalManager(private val placement: ModalPlacement? = null) { private val modalViews = arrayListOf Unit) -> Unit)>>() - private val modalCount = mutableStateOf(0) + private val _modalCount = mutableStateOf(0) + val modalCount: State = _modalCount private val toRemove = mutableSetOf() private var oldViewChanging = AtomicBoolean(false) // Don't use mutableStateOf() here, because it produces this if showing from SimpleXAPI.startChat(): // java.lang.IllegalStateException: Reading a state that was created after the snapshot was taken or in a snapshot that has not yet been applied private var passcodeView: MutableStateFlow<(@Composable (close: () -> Unit) -> Unit)?> = MutableStateFlow(null) - fun showModal(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) { + fun showModal(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.() -> Unit) { val data = ModalData() showCustomModal { close -> - ModalView(close, showClose = showClose, endButtons = endButtons, content = { data.content() }) + ModalView(close, showClose = showClose, closeOnTop = closeOnTop, endButtons = endButtons, content = { data.content() }) } } - fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) { + fun showModalCloseable(settings: Boolean = false, showClose: Boolean = true, closeOnTop: Boolean = true, endButtons: @Composable RowScope.() -> Unit = {}, content: @Composable ModalData.(close: () -> Unit) -> Unit) { val data = ModalData() showCustomModal { close -> - ModalView(close, showClose = showClose, endButtons = endButtons, content = { data.content(close) }) + ModalView(close, showClose = showClose, endButtons = endButtons, closeOnTop = closeOnTop, content = { data.content(close) }) } } @@ -81,12 +96,12 @@ class ModalManager(private val placement: ModalPlacement? = null) { // to prevent unneeded animation on different situations val anim = if (appPlatform.isAndroid) animated else animated && (modalCount.value > 0 || placement == ModalPlacement.START) modalViews.add(Triple(anim, data, modal)) - modalCount.value = modalViews.size - toRemove.size + _modalCount.value = modalViews.size - toRemove.size if (placement == ModalPlacement.CENTER) { ChatModel.chatId.value = null } else if (placement == ModalPlacement.END) { - desktopExpandWindowToWidth(DEFAULT_START_MODAL_WIDTH + DEFAULT_MIN_CENTER_MODAL_WIDTH + DEFAULT_END_MODAL_WIDTH) + desktopExpandWindowToWidth(DEFAULT_START_MODAL_WIDTH * sqrt(appPrefs.fontScale.get()) + DEFAULT_MIN_CENTER_MODAL_WIDTH + DEFAULT_END_MODAL_WIDTH * sqrt(appPrefs.fontScale.get())) } } @@ -97,18 +112,23 @@ class ModalManager(private val placement: ModalPlacement? = null) { fun hasModalsOpen() = modalCount.value > 0 + val hasModalsOpen: Boolean + @Composable get () = remember { modalCount }.value > 0 + + fun openModalCount() = modalCount.value + fun closeModal() { if (modalViews.isNotEmpty()) { if (modalViews.lastOrNull()?.first == false) modalViews.removeAt(modalViews.lastIndex) else runAtomically { toRemove.add(modalViews.lastIndex - min(toRemove.size, modalViews.lastIndex)) } } - modalCount.value = modalViews.size - toRemove.size + _modalCount.value = modalViews.size - toRemove.size } fun closeModals() { modalViews.clear() toRemove.clear() - modalCount.value = 0 + _modalCount.value = 0 } fun closeModalsExceptFirst() { @@ -122,7 +142,13 @@ class ModalManager(private val placement: ModalPlacement? = null) { fun showInView() { // Without animation if (modalCount.value > 0 && modalViews.lastOrNull()?.first == false) { - modalViews.lastOrNull()?.let { it.third(it.second, ::closeModal) } + modalViews.lastOrNull()?.let { + CompositionLocalProvider( + LocalAppBarHandler provides it.second.appBarHandler + ) { + it.third(it.second, ::closeModal) + } + } return } AnimatedContent(targetState = modalCount.value, @@ -134,7 +160,13 @@ class ModalManager(private val placement: ModalPlacement? = null) { }.using(SizeTransform(clip = false)) } ) { - modalViews.getOrNull(it - 1)?.let { it.third(it.second, ::closeModal) } + modalViews.getOrNull(it - 1)?.let { + CompositionLocalProvider( + LocalAppBarHandler provides it.second.appBarHandler + ) { + it.third(it.second, ::closeModal) + } + } // This is needed because if we delete from modalViews immediately on request, animation will be bad if (toRemove.isNotEmpty() && it == modalCount.value && transition.currentState == EnterExitState.Visible && !transition.isRunning) { runAtomically { toRemove.removeIf { elem -> modalViews.removeAt(elem); true } } @@ -157,28 +189,6 @@ class ModalManager(private val placement: ModalPlacement? = null) { block() atomicBoolean.set(false) } - - @OptIn(ExperimentalAnimationApi::class) - private fun fromStartToEndTransition() = - slideInHorizontally( - initialOffsetX = { fullWidth -> -fullWidth }, - animationSpec = animationSpec() - ) with slideOutHorizontally( - targetOffsetX = { fullWidth -> fullWidth }, - animationSpec = animationSpec() - ) - - @OptIn(ExperimentalAnimationApi::class) - private fun fromEndToStartTransition() = - slideInHorizontally( - initialOffsetX = { fullWidth -> fullWidth }, - animationSpec = animationSpec() - ) with slideOutHorizontally( - targetOffsetX = { fullWidth -> -fullWidth }, - animationSpec = animationSpec() - ) - -private fun animationSpec() = tween(durationMillis = 250, easing = FastOutSlowInEasing) // private fun animationSpecFromStart() = tween(durationMillis = 150, easing = FastOutLinearInEasing) // private fun animationSpecFromEnd() = tween(durationMillis = 100, easing = FastOutSlowInEasing) @@ -195,5 +205,27 @@ private fun animationSpec() = tween(durationMillis = 250, easing = FastOu end.closeModals() fullscreen.closeModals() } + + @OptIn(ExperimentalAnimationApi::class) + fun fromStartToEndTransition() = + slideInHorizontally( + initialOffsetX = { fullWidth -> -fullWidth }, + animationSpec = animationSpec() + ) with slideOutHorizontally( + targetOffsetX = { fullWidth -> fullWidth }, + animationSpec = animationSpec() + ) + + @OptIn(ExperimentalAnimationApi::class) + fun fromEndToStartTransition() = + slideInHorizontally( + initialOffsetX = { fullWidth -> fullWidth }, + animationSpec = animationSpec() + ) with slideOutHorizontally( + targetOffsetX = { fullWidth -> -fullWidth }, + animationSpec = animationSpec() + ) + + private fun animationSpec() = tween(durationMillis = 250, easing = FastOutSlowInEasing) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Modifiers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Modifiers.kt index 6990a69ebd..8ad877d879 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Modifiers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Modifiers.kt @@ -37,7 +37,7 @@ fun SwipeToDismissModifier( return Modifier.swipeable( state = state, anchors = anchors, - thresholds = { _, _ -> FractionalThreshold(0.5f) }, + thresholds = { _, _ -> FractionalThreshold(0.99f) }, orientation = Orientation.Horizontal, reverseDirection = isRtl, ).offset { IntOffset(state.offset.value.roundToInt(), 0) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt index ca451bd3a2..c6d5d6ad16 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SearchTextField.kt @@ -10,7 +10,6 @@ import androidx.compose.material.TextFieldDefaults.indicatorLine import androidx.compose.material.TextFieldDefaults.textFieldWithLabelPadding import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester @@ -25,10 +24,12 @@ import androidx.compose.ui.text.input.* import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import chat.simplex.common.platform.* import chat.simplex.res.MR import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.launch -@OptIn(ExperimentalComposeUiApi::class) @Composable fun SearchTextField( modifier: Modifier, @@ -50,6 +51,25 @@ fun SearchTextField( keyboard?.show() } } + if (appPlatform.isAndroid) { + LaunchedEffect(Unit) { + val modalCountOnOpen = ModalManager.start.modalCount.value + launch { + snapshotFlow { ModalManager.start.modalCount.value } + .filter { it > modalCountOnOpen } + .collect { + keyboard?.hide() + } + } + } + KeyChangeEffect(chatModel.chatId.value) { + if (chatModel.chatId.value != null) { + // Delay is needed here because when ChatView is being opened and keyboard is hiding, bottom sheet (to choose attachment) is visible on a screen + delay(300) + keyboard?.hide() + } + } + } DisposableEffect(Unit) { onDispose { @@ -63,6 +83,7 @@ fun SearchTextField( focusedIndicatorColor = Color.Unspecified, unfocusedIndicatorColor = Color.Unspecified, disabledIndicatorColor = Color.Unspecified, + placeholderColor = MaterialTheme.colors.secondary, ) val shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize) val interactionSource = remember { MutableInteractionSource() } @@ -88,7 +109,7 @@ fun SearchTextField( textStyle = TextStyle( color = MaterialTheme.colors.onBackground, fontWeight = FontWeight.Normal, - fontSize = 16.sp + fontSize = 15.sp ), interactionSource = interactionSource, decorationBox = @Composable { innerTextField -> diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt index c96a277fb8..facecd2398 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Section.kt @@ -1,6 +1,5 @@ import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -17,6 +16,7 @@ import chat.simplex.common.platform.onRightClick import chat.simplex.common.platform.windowWidth import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.SelectableCard import chat.simplex.common.views.usersettings.SettingsActionItemWithContent import chat.simplex.res.MR @@ -26,7 +26,7 @@ fun SectionView(title: String? = null, padding: PaddingValues = PaddingValues(), if (title != null) { Text( title, color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, - modifier = Modifier.padding(start = DEFAULT_PADDING, bottom = 5.dp), fontSize = 12.sp + modifier = Modifier.padding(start = DEFAULT_PADDING, bottom = DEFAULT_PADDING), fontSize = 12.sp ) } Column(Modifier.padding(padding).fillMaxWidth()) { content() } @@ -76,16 +76,36 @@ fun SectionViewSelectable( SectionTextFooter(values.first { it.value == currentValue.value }.description) } +@Composable +fun SectionViewSelectableCards( + title: String?, + currentValue: State, + values: List>, + onSelected: (T) -> Unit, +) { + SectionView(title) { + Column(Modifier.padding(horizontal = DEFAULT_PADDING)) { + if (title != null) { + Text(title, Modifier.fillMaxWidth(), textAlign = TextAlign.Center) + Spacer(Modifier.height(DEFAULT_PADDING * 2f)) + } + values.forEach { item -> + SelectableCard(currentValue, item.value, item.title, item.description, onSelected) + } + } + } +} + @Composable fun SectionItemView( click: (() -> Unit)? = null, - minHeight: Dp = 46.dp, + minHeight: Dp = DEFAULT_MIN_SECTION_ITEM_HEIGHT, disabled: Boolean = false, extraPadding: Boolean = false, padding: PaddingValues = if (extraPadding) - PaddingValues(start = DEFAULT_PADDING * 1.7f, end = DEFAULT_PADDING) + PaddingValues(start = DEFAULT_PADDING * 1.7f, end = DEFAULT_PADDING, top = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL, bottom = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL) else - PaddingValues(horizontal = DEFAULT_PADDING), + PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL), content: (@Composable RowScope.() -> Unit) ) { val modifier = Modifier @@ -100,10 +120,9 @@ fun SectionItemView( } @Composable -fun SectionItemViewLongClickable( - click: () -> Unit, - longClick: () -> Unit, - minHeight: Dp = 46.dp, +fun SectionItemViewWithoutMinPadding( + click: (() -> Unit)? = null, + minHeight: Dp = DEFAULT_MIN_SECTION_ITEM_HEIGHT, disabled: Boolean = false, extraPadding: Boolean = false, padding: PaddingValues = if (extraPadding) @@ -111,6 +130,22 @@ fun SectionItemViewLongClickable( else PaddingValues(horizontal = DEFAULT_PADDING), content: (@Composable RowScope.() -> Unit) +) { + SectionItemView(click, minHeight, disabled, extraPadding, padding, content) +} + +@Composable +fun SectionItemViewLongClickable( + click: () -> Unit, + longClick: () -> Unit, + minHeight: Dp = DEFAULT_MIN_SECTION_ITEM_HEIGHT, + disabled: Boolean = false, + extraPadding: Boolean = false, + padding: PaddingValues = if (extraPadding) + PaddingValues(start = DEFAULT_PADDING * 1.7f, end = DEFAULT_PADDING, top = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL, bottom = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL) + else + PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL), + content: (@Composable RowScope.() -> Unit) ) { val modifier = Modifier .fillMaxWidth() @@ -127,30 +162,11 @@ fun SectionItemViewLongClickable( } } -@Composable -fun SectionItemViewWithIcon( - click: (() -> Unit)? = null, - minHeight: Dp = 46.dp, - disabled: Boolean = false, - padding: PaddingValues = PaddingValues(start = DEFAULT_PADDING * 1.7f, end = DEFAULT_PADDING), - content: (@Composable RowScope.() -> Unit) -) { - val modifier = Modifier - .fillMaxWidth() - .sizeIn(minHeight = minHeight) - Row( - if (click == null || disabled) modifier.padding(padding) else modifier.clickable(onClick = click).padding(padding), - verticalAlignment = Alignment.CenterVertically - ) { - content() - } -} - @Composable fun SectionItemViewSpaceBetween( click: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null, - minHeight: Dp = 46.dp, + minHeight: Dp = DEFAULT_MIN_SECTION_ITEM_HEIGHT, padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING), disabled: Boolean = false, content: (@Composable RowScope.() -> Unit) @@ -159,7 +175,7 @@ fun SectionItemViewSpaceBetween( .fillMaxWidth() .sizeIn(minHeight = minHeight) Row( - if (click == null || disabled) modifier.padding(padding) else modifier + if (click == null || disabled) modifier.padding(padding).padding(vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL) else modifier .combinedClickable(onClick = click, onLongClick = onLongClick).padding(padding) .onRightClick { onLongClick?.invoke() }, horizontalArrangement = Arrangement.SpaceBetween, @@ -232,9 +248,9 @@ fun SectionDividerSpaced(maxTopPadding: Boolean = false, maxBottomPadding: Boole Divider( Modifier.padding( start = DEFAULT_PADDING_HALF, - top = if (maxTopPadding) 37.dp else 27.dp, + top = if (maxTopPadding) DEFAULT_PADDING + 18.dp else DEFAULT_PADDING + 2.dp, end = DEFAULT_PADDING_HALF, - bottom = if (maxBottomPadding) 37.dp else 27.dp) + bottom = if (maxBottomPadding) DEFAULT_PADDING + 18.dp else DEFAULT_PADDING + 2.dp) ) } @@ -254,8 +270,8 @@ fun TextIconSpaced(extraPadding: Boolean = false) { } @Composable -fun InfoRow(title: String, value: String, icon: Painter? = null, iconTint: Color? = null, textColor: Color = MaterialTheme.colors.onBackground) { - SectionItemViewSpaceBetween { +fun InfoRow(title: String, value: String, icon: Painter? = null, iconTint: Color? = null, textColor: Color = MaterialTheme.colors.onBackground, padding: PaddingValues = PaddingValues(horizontal = DEFAULT_PADDING)) { + SectionItemViewSpaceBetween(padding = padding) { Row { val iconSize = with(LocalDensity.current) { 21.sp.toDp() } if (icon != null) Icon(icon, title, Modifier.padding(end = 8.dp).size(iconSize), tint = iconTint ?: MaterialTheme.colors.secondary) @@ -265,6 +281,61 @@ fun InfoRow(title: String, value: String, icon: Painter? = null, iconTint: Color } } +fun numOrDash(n: Number): String = if (n.toLong() == 0L) "-" else n.toString() + +@Composable +fun InfoRowTwoValues( + title: String, + title2: String, + value: Int, + value2: Int, + textColor: Color = MaterialTheme.colors.onBackground +) { + SectionItemViewSpaceBetween { + Row( + verticalAlignment = Alignment.Bottom + ) { + Text( + text = title, + color = textColor, + ) + Text( + text = " / ", + fontSize = 12.sp, + ) + Text( + text = title2, + color = textColor, + fontSize = 12.sp, + ) + } + Row(verticalAlignment = Alignment.Bottom) { + if (value == 0 && value2 == 0) { + Text( + text = "-", + color = MaterialTheme.colors.secondary + ) + } else { + Text( + text = numOrDash(value), + color = MaterialTheme.colors.secondary, + ) + Text( + text = " / ", + color = MaterialTheme.colors.secondary, + fontSize = 12.sp, + ) + Text( + text = numOrDash(value2), + color = MaterialTheme.colors.secondary, + fontSize = 12.sp, + ) + } + } + } +} + + @Composable fun InfoRowEllipsis(title: String, value: String, onClick: () -> Unit) { SectionItemViewSpaceBetween(onClick) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SimpleButton.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SimpleButton.kt index 7db001a4bd..1c4879dedd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SimpleButton.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SimpleButton.kt @@ -1,8 +1,10 @@ package chat.simplex.common.views.helpers import androidx.compose.desktop.ui.tooling.preview.Preview +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import dev.icerock.moko.resources.compose.painterResource @@ -16,6 +18,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp +import chat.simplex.common.platform.appPlatform import chat.simplex.common.ui.theme.SimpleXTheme import chat.simplex.res.MR diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SubscriptionStatusIcon.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SubscriptionStatusIcon.kt new file mode 100644 index 0000000000..e0e61b598e --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/SubscriptionStatusIcon.kt @@ -0,0 +1,41 @@ +package chat.simplex.common.views.helpers + +import androidx.compose.foundation.layout.Box +import androidx.compose.material.Icon +import androidx.compose.ui.graphics.Color +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource + +@Composable +fun SubscriptionStatusIcon( + color: Color, + variableValue: Float, + modifier: Modifier = Modifier +) { + @Composable + fun ZeroIcon() { + Icon(painterResource(MR.images.ic_radiowaves_up_forward_4_bar), null, tint = color.copy(alpha = 0.33f), modifier = modifier) + } + + when { + variableValue <= 0f -> ZeroIcon() + variableValue > 0f && variableValue <= 0.25f -> Box { + ZeroIcon() + Icon(painterResource(MR.images.ic_radiowaves_up_forward_1_bar), null, tint = color, modifier = modifier) + } + + variableValue > 0.25f && variableValue <= 0.5f -> Box { + ZeroIcon() + Icon(painterResource(MR.images.ic_radiowaves_up_forward_2_bar), null, tint = color, modifier = modifier) + } + + variableValue > 0.5f && variableValue <= 0.75f -> Box { + ZeroIcon() + Icon(painterResource(MR.images.ic_radiowaves_up_forward_3_bar), null, tint = color, modifier = modifier) + } + + else -> Icon(painterResource(MR.images.ic_radiowaves_up_forward_4_bar), null, tint = color, modifier = modifier) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ThemeModeEditor.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ThemeModeEditor.kt new file mode 100644 index 0000000000..a77290d90f --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/ThemeModeEditor.kt @@ -0,0 +1,522 @@ +package chat.simplex.common.views.helpers + +import SectionBottomSpacer +import SectionDividerSpaced +import SectionItemView +import SectionSpacer +import SectionView +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.MaterialTheme +import androidx.compose.material.MaterialTheme.colors +import androidx.compose.material.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.yaml +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.usersettings.* +import chat.simplex.common.views.usersettings.AppearanceScope.WallpaperPresetSelector +import chat.simplex.common.views.usersettings.AppearanceScope.editColor +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.serialization.encodeToString +import java.net.URI + +@Composable +fun ModalData.UserWallpaperEditor( + theme: ThemeModeOverride, + applyToMode: DefaultThemeMode?, + globalThemeUsed: MutableState, + save: suspend (applyToMode: DefaultThemeMode?, ThemeModeOverride?) -> Unit +) { + ColumnWithScrollBar( + Modifier + .fillMaxSize() + ) { + val applyToMode = remember { stateGetOrPutNullable("applyToMode") { applyToMode } } + var showMore by remember { stateGetOrPut("showMore") { false } } + val themeModeOverride = remember { stateGetOrPut("themeModeOverride") { theme } } + val currentTheme by CurrentColors.collectAsState() + + AppBarTitle(stringResource(MR.strings.settings_section_title_user_theme)) + val wallpaperImage = MaterialTheme.wallpaper.type.image + val wallpaperType = MaterialTheme.wallpaper.type + + val onTypeCopyFromSameTheme = { type: WallpaperType? -> + if (type is WallpaperType.Image && chatModel.remoteHostId() != null) { + false + } else { + ThemeManager.copyFromSameThemeOverrides(type, null, themeModeOverride) + withBGApi { save(applyToMode.value, themeModeOverride.value) } + globalThemeUsed.value = false + true + } + } + val preApplyGlobalIfNeeded = { type: WallpaperType? -> + if (globalThemeUsed.value) { + onTypeCopyFromSameTheme(type) + } + } + val onTypeChange: (WallpaperType?) -> Unit = { type: WallpaperType? -> + if (globalThemeUsed.value) { + preApplyGlobalIfNeeded(type) + // Saves copied static image instead of original from global theme + ThemeManager.applyWallpaper(themeModeOverride.value.type, themeModeOverride) + } else { + ThemeManager.applyWallpaper(type, themeModeOverride) + } + withBGApi { save(applyToMode.value, themeModeOverride.value) } + } + + val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? -> + if (to != null) { + val filename = saveWallpaperFile(to) + if (filename != null) { + onTypeChange(WallpaperType.Image(filename, 1f, WallpaperScaleType.FILL)) + } + } + } + + val currentColors = { type: WallpaperType? -> + // If applying for : + // - all themes: no overrides needed + // - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected + val perUserOverride = if (wallpaperType.sameType(type)) chatModel.currentUser.value?.uiThemes else null + ThemeManager.currentColors(type, null, perUserOverride, appPrefs.themeOverrides.get()) + } + val onChooseType: (WallpaperType?) -> Unit = { type: WallpaperType? -> + when { + // don't have image in parent or already selected wallpaper with custom image + type is WallpaperType.Image && chatModel.remoteHostId() != null -> { /* do nothing */ } + type is WallpaperType.Image && (wallpaperType is WallpaperType.Image || currentColors(type).wallpaper.type.image == null) -> withLongRunningApi { importWallpaperLauncher.launch("image/*") } + type is WallpaperType.Image -> onTypeCopyFromSameTheme(currentColors(type).wallpaper.type) + themeModeOverride.value.type != type || currentTheme.wallpaper.type != type -> onTypeCopyFromSameTheme(type) + else -> onTypeChange(type) + } + } + + val editColor = { name: ThemeColor -> + editColor( + name, + wallpaperType, + wallpaperImage, + onColorChange = { color -> + preApplyGlobalIfNeeded(themeModeOverride.value.type) + ThemeManager.applyThemeColor(name, color, themeModeOverride) + withBGApi { save(applyToMode.value, themeModeOverride.value) } + } + ) + } + + WallpaperPresetSelector( + selectedWallpaper = wallpaperType, + baseTheme = currentTheme.base, + currentColors = { type -> + // If applying for : + // - all themes: no overrides needed + // - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected + val perUserOverride = if (wallpaperType.sameType(type)) chatModel.currentUser.value?.uiThemes else null + ThemeManager.currentColors(type, null, perUserOverride, appPrefs.themeOverrides.get()) + }, + onChooseType = onChooseType + ) + + WallpaperSetupView( + themeModeOverride.value.type, + CurrentColors.collectAsState().value.base, + currentTheme.wallpaper, + currentTheme.appColors.sentMessage, + currentTheme.appColors.sentQuote, + currentTheme.appColors.receivedMessage, + currentTheme.appColors.receivedQuote, + editColor = { name -> editColor(name) }, + onTypeChange = onTypeChange, + ) + + SectionSpacer() + + if (!globalThemeUsed.value) { + ResetToGlobalThemeButton(true) { + themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + globalThemeUsed.value = true + withBGApi { save(applyToMode.value, null) } + } + } + + SetDefaultThemeButton { + globalThemeUsed.value = false + val lightBase = DefaultTheme.LIGHT + val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX + val mode = themeModeOverride.value.mode + withBGApi { + // Saving for both modes in one place by changing mode once per save + if (applyToMode.value == null) { + val oppositeMode = if (mode == DefaultThemeMode.LIGHT) DefaultThemeMode.DARK else DefaultThemeMode.LIGHT + save(oppositeMode, ThemeModeOverride.withFilledAppDefaults(oppositeMode, if (oppositeMode == DefaultThemeMode.LIGHT) lightBase else darkBase)) + } + themeModeOverride.value = ThemeModeOverride.withFilledAppDefaults(mode, if (mode == DefaultThemeMode.LIGHT) lightBase else darkBase) + save(themeModeOverride.value.mode, themeModeOverride.value) + } + } + + KeyChangeEffect(theme.mode) { + themeModeOverride.value = theme + if (applyToMode.value != null) { + applyToMode.value = theme.mode + } + } + + // Applies updated global theme if current one tracks global theme + KeyChangeEffect(CurrentColors.collectAsState().value) { + if (globalThemeUsed.value) { + themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + globalThemeUsed.value = true + } + } + + SectionSpacer() + + if (showMore) { + val values by remember { mutableStateOf( + listOf( + null to generalGetString(MR.strings.chat_theme_apply_to_all_modes), + DefaultThemeMode.LIGHT to generalGetString(MR.strings.chat_theme_apply_to_light_mode), + DefaultThemeMode.DARK to generalGetString(MR.strings.chat_theme_apply_to_dark_mode), + ) + ) + } + ExposedDropDownSettingRow( + generalGetString(MR.strings.chat_theme_apply_to_mode), + values, + applyToMode, + icon = null, + enabled = remember { mutableStateOf(true) }, + onSelected = { + applyToMode.value = it + if (it != null && it != CurrentColors.value.base.mode) { + val lightBase = DefaultTheme.LIGHT + val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX + ThemeManager.applyTheme(if (it == DefaultThemeMode.LIGHT) lightBase.themeName else darkBase.themeName) + } + } + ) + + SectionDividerSpaced() + + AppearanceScope.CustomizeThemeColorsSection(currentTheme, editColor = editColor) + + SectionDividerSpaced(maxBottomPadding = false) + + ImportExportThemeSection(null, remember { chatModel.currentUser }.value?.uiThemes) { + withBGApi { + themeModeOverride.value = it + save(applyToMode.value, it) + } + } + } else { + AdvancedSettingsButton { showMore = true } + } + + SectionBottomSpacer() + } +} + +@Composable +fun ModalData.ChatWallpaperEditor( + theme: ThemeModeOverride, + applyToMode: DefaultThemeMode?, + globalThemeUsed: MutableState, + save: suspend (applyToMode: DefaultThemeMode?, ThemeModeOverride?) -> Unit +) { + ColumnWithScrollBar( + Modifier + .fillMaxSize() + ) { + val applyToMode = remember { stateGetOrPutNullable("applyToMode") { applyToMode } } + var showMore by remember { stateGetOrPut("showMore") { false } } + val themeModeOverride = remember { stateGetOrPut("themeModeOverride") { theme } } + val currentTheme by remember(themeModeOverride.value, CurrentColors.collectAsState().value) { + mutableStateOf( + ThemeManager.currentColors(null, if (globalThemeUsed.value) null else themeModeOverride.value, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get()) + ) + } + + AppBarTitle(stringResource(MR.strings.settings_section_title_chat_theme)) + + val onTypeCopyFromSameTheme: (WallpaperType?) -> Boolean = { type -> + if (type is WallpaperType.Image && chatModel.remoteHostId() != null) { + false + } else { + val success = ThemeManager.copyFromSameThemeOverrides(type, chatModel.currentUser.value?.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight), themeModeOverride) + if (success) { + withBGApi { save(applyToMode.value, themeModeOverride.value) } + globalThemeUsed.value = false + } + success + } + } + val preApplyGlobalIfNeeded = { type: WallpaperType? -> + if (globalThemeUsed.value) { + onTypeCopyFromSameTheme(type) + } + } + val onTypeChange: (WallpaperType?) -> Unit = { type -> + if (globalThemeUsed.value) { + preApplyGlobalIfNeeded(type) + // Saves copied static image instead of original from global theme + ThemeManager.applyWallpaper(themeModeOverride.value.type, themeModeOverride) + } else { + ThemeManager.applyWallpaper(type, themeModeOverride) + } + withBGApi { save(applyToMode.value, themeModeOverride.value) } + } + + val editColor: (ThemeColor) -> Unit = { name: ThemeColor -> + ModalManager.end.showModal { + val currentTheme by remember(themeModeOverride.value, CurrentColors.collectAsState().value) { + mutableStateOf( + ThemeManager.currentColors(null, themeModeOverride.value, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get()) + ) + } + val initialColor: Color = when (name) { + ThemeColor.WALLPAPER_BACKGROUND -> currentTheme.wallpaper.background ?: Color.Transparent + ThemeColor.WALLPAPER_TINT -> currentTheme.wallpaper.tint ?: Color.Transparent + ThemeColor.PRIMARY -> currentTheme.colors.primary + ThemeColor.PRIMARY_VARIANT -> currentTheme.colors.primaryVariant + ThemeColor.SECONDARY -> currentTheme.colors.secondary + ThemeColor.SECONDARY_VARIANT -> currentTheme.colors.secondaryVariant + ThemeColor.BACKGROUND -> currentTheme.colors.background + ThemeColor.SURFACE -> currentTheme.colors.surface + ThemeColor.TITLE -> currentTheme.appColors.title + ThemeColor.PRIMARY_VARIANT2 -> currentTheme.appColors.primaryVariant2 + ThemeColor.SENT_MESSAGE -> currentTheme.appColors.sentMessage + ThemeColor.SENT_QUOTE -> currentTheme.appColors.sentQuote + ThemeColor.RECEIVED_MESSAGE -> currentTheme.appColors.receivedMessage + ThemeColor.RECEIVED_QUOTE -> currentTheme.appColors.receivedQuote + } + AppearanceScope.ColorEditor( + name, + initialColor, + CurrentColors.collectAsState().value.base, + themeModeOverride.value.type, + themeModeOverride.value.type?.image, + currentTheme.wallpaper.background, + currentTheme.wallpaper.tint, + currentColors = { + ThemeManager.currentColors(null, themeModeOverride.value, chatModel.currentUser.value?.uiThemes, appPreferences.themeOverrides.get()) + }, + onColorChange = { color -> + preApplyGlobalIfNeeded(themeModeOverride.value.type) + ThemeManager.applyThemeColor(name, color, themeModeOverride) + withBGApi { save(applyToMode.value, themeModeOverride.value) } + } + ) + } + } + + val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? -> + if (to != null) { + val filename = saveWallpaperFile(to) + if (filename != null) { + // Delete only non-user image + if (!globalThemeUsed.value) { + removeWallpaperFile((themeModeOverride.value.type as? WallpaperType.Image)?.filename) + } + globalThemeUsed.value = false + onTypeChange(WallpaperType.Image(filename, 1f, WallpaperScaleType.FILL)) + } + } + } + + val currentColors = { type: WallpaperType? -> + ThemeManager.currentColors(type, if (type?.sameType(themeModeOverride.value.type) == true) themeModeOverride.value else null, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + } + + WallpaperPresetSelector( + selectedWallpaper = currentTheme.wallpaper.type, + activeBackgroundColor = currentTheme.wallpaper.background, + activeTintColor = currentTheme.wallpaper.tint, + baseTheme = CurrentColors.collectAsState().value.base, + currentColors = { type -> currentColors(type) }, + onChooseType = { type -> + when { + type is WallpaperType.Image && chatModel.remoteHostId() != null -> { /* do nothing */ } + type is WallpaperType.Image && ((themeModeOverride.value.type is WallpaperType.Image && !globalThemeUsed.value) || currentColors(type).wallpaper.type.image == null) -> { + withLongRunningApi { importWallpaperLauncher.launch("image/*") } + } + type is WallpaperType.Image -> { + if (!onTypeCopyFromSameTheme(currentColors(type).wallpaper.type)) { + withLongRunningApi { importWallpaperLauncher.launch("image/*") } + } + } + globalThemeUsed.value || themeModeOverride.value.type != type -> { + onTypeCopyFromSameTheme(type) + } + else -> { + onTypeChange(type) + } + } + }, + ) + + WallpaperSetupView( + themeModeOverride.value.type, + CurrentColors.collectAsState().value.base, + currentTheme.wallpaper, + currentTheme.appColors.sentMessage, + currentTheme.appColors.sentQuote, + currentTheme.appColors.receivedMessage, + currentTheme.appColors.receivedQuote, + editColor = editColor, + onTypeChange = onTypeChange, + ) + + SectionSpacer() + + if (!globalThemeUsed.value) { + ResetToGlobalThemeButton(remember { chatModel.currentUser }.value?.uiThemes?.preferredMode(isInDarkTheme()) == null) { + themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + globalThemeUsed.value = true + withBGApi { save(applyToMode.value, null) } + } + } + + SetDefaultThemeButton { + globalThemeUsed.value = false + val lightBase = DefaultTheme.LIGHT + val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX + val mode = themeModeOverride.value.mode + withBGApi { + // Saving for both modes in one place by changing mode once per save + if (applyToMode.value == null) { + val oppositeMode = if (mode == DefaultThemeMode.LIGHT) DefaultThemeMode.DARK else DefaultThemeMode.LIGHT + save(oppositeMode, ThemeModeOverride.withFilledAppDefaults(oppositeMode, if (oppositeMode == DefaultThemeMode.LIGHT) lightBase else darkBase)) + } + themeModeOverride.value = ThemeModeOverride.withFilledAppDefaults(mode, if (mode == DefaultThemeMode.LIGHT) lightBase else darkBase) + save(themeModeOverride.value.mode, themeModeOverride.value) + } + } + + KeyChangeEffect(theme.mode) { + themeModeOverride.value = theme + if (applyToMode.value != null) { + applyToMode.value = theme.mode + } + } + + // Applies updated global theme if current one tracks global theme + KeyChangeEffect(CurrentColors.collectAsState()) { + if (globalThemeUsed.value) { + themeModeOverride.value = ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + globalThemeUsed.value = true + } + } + + SectionSpacer() + + if (showMore) { + val values by remember { mutableStateOf( + listOf( + null to generalGetString(MR.strings.chat_theme_apply_to_all_modes), + DefaultThemeMode.LIGHT to generalGetString(MR.strings.chat_theme_apply_to_light_mode), + DefaultThemeMode.DARK to generalGetString(MR.strings.chat_theme_apply_to_dark_mode), + ) + ) + } + ExposedDropDownSettingRow( + generalGetString(MR.strings.chat_theme_apply_to_mode), + values, + applyToMode, + icon = null, + enabled = remember { mutableStateOf(true) }, + onSelected = { + applyToMode.value = it + if (it != null && it != CurrentColors.value.base.mode) { + val lightBase = DefaultTheme.LIGHT + val darkBase = if (CurrentColors.value.base != DefaultTheme.LIGHT) CurrentColors.value.base else if (appPrefs.systemDarkTheme.get() == DefaultTheme.DARK.themeName) DefaultTheme.DARK else if (appPrefs.systemDarkTheme.get() == DefaultTheme.BLACK.themeName) DefaultTheme.BLACK else DefaultTheme.SIMPLEX + ThemeManager.applyTheme(if (it == DefaultThemeMode.LIGHT) lightBase.themeName else darkBase.themeName) + } + } + ) + + SectionDividerSpaced() + + AppearanceScope.CustomizeThemeColorsSection(currentTheme, editColor = editColor) + + SectionDividerSpaced(maxBottomPadding = false) + ImportExportThemeSection(themeModeOverride.value, remember { chatModel.currentUser }.value?.uiThemes) { + withBGApi { + themeModeOverride.value = it + save(applyToMode.value, it) + } + } + } else { + AdvancedSettingsButton { showMore = true } + } + + SectionBottomSpacer() + } +} + +@Composable +private fun ImportExportThemeSection(perChat: ThemeModeOverride?, perUser: ThemeModeOverrides?, save: (ThemeModeOverride) -> Unit) { + SectionView { + val theme = remember { mutableStateOf(null as String?) } + val exportThemeLauncher = rememberFileChooserLauncher(false) { to: URI? -> + val themeValue = theme.value + if (themeValue != null && to != null) { + copyBytesToFile(themeValue.byteInputStream(), to) { + theme.value = null + } + } + } + SectionItemView({ + val overrides = ThemeManager.currentThemeOverridesForExport(perChat, perUser) + val lines = yaml.encodeToString(overrides).lines() + // Removing theme id without using custom serializer or data class + theme.value = lines.subList(1, lines.size).joinToString("\n") + withLongRunningApi { exportThemeLauncher.launch("simplex.theme") } + }) { + Text(generalGetString(MR.strings.export_theme), color = colors.primary) + } + val importThemeLauncher = rememberFileChooserLauncher(true) { to: URI? -> + if (to != null) { + val theme = getThemeFromUri(to) + if (theme != null) { + val res = ThemeModeOverride(mode = theme.base.mode, colors = theme.colors, wallpaper = theme.wallpaper?.importFromString()).removeSameColors(theme.base) + save(res) + } + } + } + // Can not limit to YAML mime type since it's unsupported by Android + SectionItemView({ withLongRunningApi { importThemeLauncher.launch("*/*") } }) { + Text(generalGetString(MR.strings.import_theme), color = colors.primary) + } + } +} + +@Composable +private fun ResetToGlobalThemeButton(app: Boolean, onClick: () -> Unit) { + SectionItemView(onClick) { + Text(stringResource(if (app) MR.strings.chat_theme_reset_to_app_theme else MR.strings.chat_theme_reset_to_user_theme), color = MaterialTheme.colors.primary) + } +} + +@Composable +private fun SetDefaultThemeButton(onClick: () -> Unit) { + SectionItemView(onClick) { + Text(stringResource(MR.strings.chat_theme_set_default_theme), color = MaterialTheme.colors.primary) + } +} + +@Composable +private fun AdvancedSettingsButton(onClick: () -> Unit) { + SettingsActionItem( + painterResource(MR.images.ic_arrow_downward), + stringResource(MR.strings.wallpaper_advanced_settings), + click = onClick + ) +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt index 9a6e3a0f9a..7512cf872e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt @@ -8,6 +8,7 @@ import androidx.compose.ui.platform.* import androidx.compose.ui.text.* import androidx.compose.ui.unit.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.ThemeOverrides import chat.simplex.common.views.chatlist.connectIfOpenedViaUri @@ -15,10 +16,12 @@ import chat.simplex.res.MR import com.charleskorn.kaml.decodeFromStream import dev.icerock.moko.resources.StringResource import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* import kotlinx.serialization.encodeToString import java.io.* import java.net.URI import java.nio.file.Files +import java.nio.file.StandardCopyOption import java.text.SimpleDateFormat import java.util.* import java.util.concurrent.Executors @@ -145,6 +148,7 @@ fun getThemeFromUri(uri: URI, withAlertOnException: Boolean = true): ThemeOverri runCatching { return yaml.decodeFromStream(it!!) }.onFailure { + Log.e(TAG, "Error while decoding theme: ${it.stackTraceToString()}") if (withAlertOnException) { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.import_theme_error), @@ -280,6 +284,38 @@ fun saveFileFromUri(uri: URI, withAlertOnException: Boolean = true): CryptoFile? } } +fun saveWallpaperFile(uri: URI): String? { + val destFileName = generateNewFileName("wallpaper", "jpg", File(getWallpaperFilePath(""))) + val destFile = File(getWallpaperFilePath(destFileName)) + try { + val inputStream = uri.inputStream() + Files.copy(inputStream!!, destFile.toPath(), StandardCopyOption.REPLACE_EXISTING) + } catch (e: Exception) { + Log.e(TAG, "Error saving wallpaper file: ${e.stackTraceToString()}") + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error), e.stackTraceToString()) + return null + } + return destFile.name +} + +fun saveWallpaperFile(image: ImageBitmap): String { + val destFileName = generateNewFileName("wallpaper", "jpg", File(getWallpaperFilePath(""))) + val destFile = File(getWallpaperFilePath(destFileName)) + val dataResized = resizeImageToDataSize(image, false, maxDataSize = 5_000_000) + val output = FileOutputStream(destFile) + dataResized.use { + it.writeTo(output) + } + return destFile.name +} + +fun removeWallpaperFile(fileName: String? = null) { + File(getWallpaperFilePath("_")).parentFile.listFiles()?.forEach { + if (it.name == fileName) it.delete() + } + WallpaperType.cachedImages.remove(fileName) +} + fun createTmpFileAndDelete(onCreated: (File) -> T): T { val tmpFile = File(tmpDir, UUID.randomUUID().toString()) tmpFile.deleteOnExit() @@ -485,6 +521,28 @@ fun includeMoreFailedComposables() { lastExecutedComposables.clear() } +val fontSizeMultiplier: Float + @Composable get() = remember { appPrefs.fontScale.state }.value + +val fontSizeSqrtMultiplier: Float + @Composable get() = sqrt(remember { appPrefs.fontScale.state }.value) + +val desktopDensityScaleMultiplier: Float + @Composable get() = if (appPlatform.isDesktop) remember { appPrefs.densityScale.state }.value else 1f + +@Composable +fun TextUnit.toDp(): Dp { + check(type == TextUnitType.Sp) { "Only Sp can convert to Px" } + return Dp(value * LocalDensity.current.fontScale) +} + +fun Flow.throttleLatest(delayMillis: Long): Flow = this + .conflate() + .transform { + emit(it) + delay(delayMillis) + } + @Composable fun DisposableEffectOnGone(always: () -> Unit = {}, whenDispose: () -> Unit = {}, whenGone: () -> Unit) { DisposableEffect(Unit) { @@ -550,10 +608,33 @@ fun KeyChangeEffect( val initialKey = remember { key1 } val initialKey2 = remember { key2 } var anyChange by remember { mutableStateOf(false) } - LaunchedEffect(key1) { + LaunchedEffect(key1, key2) { if (anyChange || key1 != initialKey || key2 != initialKey2) { block() anyChange = true } } } + +/** + * Runs the [block] only after initial value of the [key1], or [key2], or [key3] changes, not after initial launch + * */ +@Composable +@NonRestartableComposable +fun KeyChangeEffect( + key1: Any?, + key2: Any?, + key3: Any?, + block: suspend CoroutineScope.() -> Unit +) { + val initialKey = remember { key1 } + val initialKey2 = remember { key2 } + val initialKey3 = remember { key3 } + var anyChange by remember { mutableStateOf(false) } + LaunchedEffect(key1, key2, key3) { + if (anyChange || key1 != initialKey || key2 != initialKey2 || key3 != initialKey3) { + block() + anyChange = true + } + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt index 5a37c860a0..65e1864935 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/localauth/LocalAuthView.kt @@ -34,7 +34,7 @@ fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) { } else { val r: LAResult = if (passcode.value == authRequest.password) { if (authRequest.selfDestruct && sdPassword != null && controller.ctrl == -1L) { - initChatControllerAndRunMigrations() + initChatControllerOnStart() } LAResult.Success } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt index f8e3d48625..dbb805971e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateFromDevice.kt @@ -4,7 +4,7 @@ import SectionBottomSpacer import SectionSpacer import SectionTextFooter import SectionView -import androidx.compose.foundation.* +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.runtime.* @@ -38,6 +38,7 @@ import kotlinx.serialization.* import java.io.File import java.net.URLEncoder import kotlin.math.max +import kotlin.math.sqrt @Serializable data class MigrationFileLinkData( @@ -145,20 +146,13 @@ private fun MigrateFromDeviceLayout( ) { val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) } - val (scrollBarAlpha, scrollModifier, scrollJob) = platform.desktopScrollBarComponents() - val scrollState = rememberScrollState() - Column( - Modifier.fillMaxSize().verticalScroll(scrollState).then(if (appPlatform.isDesktop) scrollModifier else Modifier).height(IntrinsicSize.Max), + ColumnWithScrollBar( + Modifier.fillMaxSize(), maxIntrinsicSize = true ) { AppBarTitle(stringResource(MR.strings.migrate_from_device_title)) SectionByState(migrationState, tempDatabaseFile.value, chatReceiver) SectionBottomSpacer() } - if (appPlatform.isDesktop) { - Box(Modifier.fillMaxSize()) { - platform.desktopScrollBar(scrollState, Modifier.align(Alignment.CenterEnd).fillMaxHeight(), scrollBarAlpha, scrollJob, false) - } - } platform.androidLockPortraitOrientation() } @@ -426,7 +420,8 @@ fun LargeProgressView(value: Float, title: String, description: String) { Box(Modifier.padding(DEFAULT_PADDING).fillMaxSize(), contentAlignment = Alignment.Center) { CircularProgressIndicator( progress = value, - (if (appPlatform.isDesktop) Modifier.size(DEFAULT_START_MODAL_WIDTH) else Modifier.size(windowWidth() - DEFAULT_PADDING * 2)) + (if (appPlatform.isDesktop) Modifier.size(DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier) else Modifier.size(windowWidth() - DEFAULT_PADDING * + 2)) .rotate(-90f), color = MaterialTheme.colors.primary, strokeWidth = 25.dp @@ -477,12 +472,13 @@ private fun MutableState.exportArchive() { withLongRunningApi { try { getMigrationTempFilesDirectory().mkdir() - val archivePath = exportChatArchive(chatModel, getMigrationTempFilesDirectory(), mutableStateOf(""), mutableStateOf(Instant.DISTANT_PAST), mutableStateOf("")) - val totalBytes = File(archivePath).length() - if (totalBytes > 0L) { - state = MigrationFromState.DatabaseInit(totalBytes, archivePath) + val (archivePath, archiveErrors) = exportChatArchive(chatModel, getMigrationTempFilesDirectory(), mutableStateOf(""), mutableStateOf(Instant.DISTANT_PAST), mutableStateOf("")) + if (archiveErrors.isEmpty()) { + uploadArchive(archivePath) } else { - AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migrate_from_device_exported_file_doesnt_exist)) + showArchiveExportedWithErrorsAlert(generalGetString(MR.strings.chat_database_exported_migrate), archiveErrors) { + uploadArchive(archivePath) + } state = MigrationFromState.UploadConfirmation } } catch (e: Exception) { @@ -495,14 +491,28 @@ private fun MutableState.exportArchive() { } } +private fun MutableState.uploadArchive(archivePath: String) { + val totalBytes = File(archivePath).length() + if (totalBytes > 0L) { + state = MigrationFromState.DatabaseInit(totalBytes, archivePath) + } else { + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migrate_from_device_exported_file_doesnt_exist)) + state = MigrationFromState.UploadConfirmation + } + +} + suspend fun initTemporaryDatabase(tempDatabaseFile: File, netCfg: NetCfg): Pair? { val (status, ctrl) = chatInitTemporaryDatabase(tempDatabaseFile.absolutePath) showErrorOnMigrationIfNeeded(status) try { if (ctrl != null) { val user = startChatWithTemporaryDatabase(ctrl, netCfg) - return if (user != null) ctrl to user else null + if (user != null) return ctrl to user + chatCloseStore(ctrl) } + File(tempDatabaseFile.absolutePath + "_chat.db").delete() + File(tempDatabaseFile.absolutePath + "_agent.db").delete() } catch (e: Throwable) { Log.e(TAG, "Error while starting chat in temporary database: ${e.stackTraceToString()}") } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt index 9d204ad42c..4fa36b06f0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/migration/MigrateToDevice.kt @@ -155,20 +155,13 @@ private fun ModalData.MigrateToDeviceLayout( close: () -> Unit, ) { val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) } - val (scrollBarAlpha, scrollModifier, scrollJob) = platform.desktopScrollBarComponents() - val scrollState = rememberScrollState() - Column( - Modifier.fillMaxSize().verticalScroll(scrollState).then(if (appPlatform.isDesktop) scrollModifier else Modifier).height(IntrinsicSize.Max), + ColumnWithScrollBar( + Modifier.fillMaxSize(), maxIntrinsicSize = true ) { AppBarTitle(stringResource(MR.strings.migrate_to_device_title)) SectionByState(migrationState, tempDatabaseFile.value, chatReceiver, close) SectionBottomSpacer() } - if (appPlatform.isDesktop) { - Box(Modifier.fillMaxSize()) { - platform.desktopScrollBar(scrollState, Modifier.align(Alignment.CenterEnd).fillMaxHeight(), scrollBarAlpha, scrollJob, false) - } - } platform.androidLockPortraitOrientation() } @@ -237,7 +230,6 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos proxy } } - val proxyPort = remember { derivedStateOf { networkProxyHostPort.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } } val netCfg = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf(getNetCfg().withOnionHosts(onionHosts.value).copy(socksProxy = socksProxy, sessionMode = sessionMode.value)) @@ -275,7 +267,6 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos onionHosts, sessionMode, networkProxyHostPortPref, - proxyPort, toggleSocksProxy = { enable -> networkUseSocksProxy.value = enable }, @@ -517,7 +508,9 @@ private fun MutableState.prepareDatabase( withLongRunningApi { val ctrlAndUser = initTemporaryDatabase(tempDatabaseFile, netCfg) if (ctrlAndUser == null) { - state = MigrationToState.DownloadFailed(0, link, archivePath(), netCfg) + // Probably, something wrong with network config or database initialization, let's start from scratch + state = MigrationToState.PasteOrScanLink + MigrationToDeviceState.save(null) return@withLongRunningApi } @@ -594,14 +587,12 @@ private fun MutableState.importArchive(archivePath: String, n chatInitControllerRemovingDatabases() } controller.apiDeleteStorage() + wallpapersDir.mkdirs() try { val config = ArchiveConfig(archivePath, parentTempDirectory = databaseExportDir.toString()) val archiveErrors = controller.apiImportArchive(config) if (archiveErrors.isNotEmpty()) { - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.chat_database_imported), - generalGetString(MR.strings.non_fatal_errors_occured_during_import) - ) + showArchiveImportedWithErrorsAlert(archiveErrors) } state = MigrationToState.Passphrase("", netCfg) MigrationToDeviceState.save(MigrationToDeviceState.Passphrase(netCfg)) @@ -669,7 +660,7 @@ private suspend fun MutableState.cleanUpOnBack(chatReceiver: if (state is MigrationToState.ArchiveImportFailed) { // Original database is not exist, nothing is set up correctly for showing to a user yet. Return to clean state deleteChatDatabaseFilesAndState() - initChatControllerAndRunMigrations() + initChatControllerOnStart() } else if (state is MigrationToState.DownloadProgress && state.ctrl != null) { stopArchiveDownloading(state.fileId, state.ctrl) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt index 4c63d0a974..c430a62340 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt @@ -18,6 +18,7 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.group.AddGroupMembersView import chat.simplex.common.views.chatlist.setGroupMembers @@ -32,19 +33,24 @@ import kotlinx.coroutines.launch import java.net.URI @Composable -fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit) { +fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit, closeAll: () -> Unit) { val rhId = rh?.remoteHostId + val view = LocalMultiplatformView() AddGroupLayout( createGroup = { incognito, groupProfile -> + hideKeyboard(view) withBGApi { val groupInfo = chatModel.controller.apiNewGroup(rhId, incognito, groupProfile) if (groupInfo != null) { - chatModel.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Group(groupInfo), chatItems = listOf())) - chatModel.chatItems.clear() - chatModel.chatItemStatuses.clear() - chatModel.chatId.value = groupInfo.id + withChats { + updateGroup(rhId = rhId, groupInfo) + chatModel.chatItems.clear() + chatModel.chatItemStatuses.clear() + chatModel.chatId.value = groupInfo.id + } setGroupMembers(rhId, groupInfo, chatModel) - close.invoke() + closeAll.invoke() + if (!groupInfo.incognito) { ModalManager.end.showModalCloseable(true) { close -> AddGroupMembersView(rhId, groupInfo, creatingGroup = true, chatModel, close) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt index 5b56fe5e39..e49fbcf1e6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt @@ -7,6 +7,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* import chat.simplex.common.views.chatlist.* import chat.simplex.common.views.helpers.* @@ -331,7 +332,9 @@ suspend fun connectViaUri( val pcc = chatModel.controller.apiConnect(rhId, incognito, uri.toString()) val connLinkType = if (connectionPlan != null) planToConnectionLinkType(connectionPlan) else ConnectionLinkType.INVITATION if (pcc != null) { - chatModel.updateContactConnection(rhId, pcc) + withChats { + updateContactConnection(rhId, pcc) + } close?.invoke() AlertManager.privacySensitive.showAlertMsg( title = generalGetString(MR.strings.connection_request_sent), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt index 31623e4a61..88e483e92d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ContactConnectionInfoView.kt @@ -22,6 +22,7 @@ import chat.simplex.common.views.chat.LocalAliasEditor import chat.simplex.common.views.chatlist.deleteContactConnectionAlert import chat.simplex.common.views.helpers.* import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.model.PendingContactConnection import chat.simplex.common.platform.* import chat.simplex.common.views.usersettings.* @@ -141,7 +142,7 @@ private fun ContactConnectionInfoLayout( } SectionTextFooter(sharedProfileInfo(chatModel, contactConnection.incognito)) - SectionDividerSpaced(maxBottomPadding = false) + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) DeleteButton(deleteConnection) @@ -186,7 +187,9 @@ fun DeleteButton(onClick: () -> Unit) { private fun setContactAlias(rhId: Long?, contactConnection: PendingContactConnection, localAlias: String, chatModel: ChatModel) = withBGApi { chatModel.controller.apiSetConnectionAlias(rhId, contactConnection.pccConnId, localAlias)?.let { - chatModel.updateContactConnection(rhId, it) + withChats { + updateContactConnection(rhId, it) + } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt index 26c5422623..0621c5509f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt @@ -1,168 +1,632 @@ package chat.simplex.common.views.newchat -import androidx.compose.animation.* -import androidx.compose.animation.core.* +import SectionDividerSpaced +import SectionItemView +import SectionView +import TextIconSpaced import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.focus.* import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.platform.* +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.TextRange import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.dp -import chat.simplex.common.model.ChatModel +import androidx.compose.ui.unit.* +import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.chatlist.ScrollDirection +import chat.simplex.common.views.contacts.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.launch -import kotlin.math.roundToInt +import kotlinx.coroutines.flow.distinctUntilChanged +import java.net.URI @Composable -fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow, stopped: Boolean, closeNewChatSheet: (animated: Boolean) -> Unit) { - // TODO close new chat if remote host changes in model - if (newChatSheetState.collectAsState().value.isVisible()) BackHandler { closeNewChatSheet(true) } - NewChatSheetLayout( - newChatSheetState, - stopped, - addContact = { - closeNewChatSheet(false) - ModalManager.center.closeModals() - ModalManager.center.showModalCloseable { close -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = close) } - }, - createGroup = { - closeNewChatSheet(false) - ModalManager.center.closeModals() - ModalManager.center.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close) } - }, - closeNewChatSheet, - ) +fun NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) { + val oneHandUI = remember { appPrefs.oneHandUI.state } + val keyboardState by getKeyboardState() + val showToolbarInOneHandUI = remember { derivedStateOf { keyboardState == KeyboardState.Closed && oneHandUI.value } } + + Scaffold( + bottomBar = { + if (showToolbarInOneHandUI.value) { + Column { + Divider() + CloseSheetBar( + close = close, + showClose = true, + endButtons = { Spacer(Modifier.minimumInteractiveComponentSize()) }, + arrangement = Arrangement.Bottom, + closeBarTitle = generalGetString(MR.strings.new_message), + barPaddingValues = PaddingValues(horizontal = 0.dp) + ) + } + } + } + ) { + Column( + modifier = Modifier.fillMaxSize().padding(it) + ) { + val closeAll = { ModalManager.start.closeModals() } + + Column(modifier = Modifier.fillMaxSize()) { + NewChatSheetLayout( + addContact = { + ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = closeAll ) } + }, + scanPaste = { + ModalManager.start.showModalCloseable(endButtons = { AddContactLearnMoreButton() }) { _ -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = appPlatform.isAndroid, close = closeAll) } + }, + createGroup = { + ModalManager.start.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close, closeAll) } + }, + rh = rh, + close = close + ) + } + } + } } -private val titles = listOf( - MR.strings.add_contact_tab, - MR.strings.create_group_button -) -private val icons = listOf(MR.images.ic_add_link, MR.images.ic_group) +enum class ContactType { + CARD, REQUEST, RECENT, CHAT_DELETED, UNLISTED +} + +fun chatContactType(chat: Chat): ContactType { + return when (val cInfo = chat.chatInfo) { + is ChatInfo.ContactRequest -> ContactType.REQUEST + is ChatInfo.Direct -> { + val contact = cInfo.contact + + when { + contact.activeConn == null && contact.profile.contactLink != null -> ContactType.CARD + contact.chatDeleted -> ContactType.CHAT_DELETED + contact.contactStatus == ContactStatus.Active -> ContactType.RECENT + else -> ContactType.UNLISTED + } + } + else -> ContactType.UNLISTED + } +} + +private fun filterContactTypes(c: List, contactTypes: List): List { + return c.filter { chat -> contactTypes.contains(chatContactType(chat)) } +} + +private var lazyListState = 0 to 0 @Composable private fun NewChatSheetLayout( - newChatSheetState: StateFlow, - stopped: Boolean, + rh: RemoteHostInfo?, addContact: () -> Unit, + scanPaste: () -> Unit, createGroup: () -> Unit, - closeNewChatSheet: (animated: Boolean) -> Unit, + close: () -> Unit, ) { - var newChat by remember { mutableStateOf(newChatSheetState.value) } - val resultingColor = if (isInDarkTheme()) Color.Black.copy(0.64f) else DrawerDefaults.scrimColor - val animatedColor = remember { - Animatable( - if (newChat.isVisible()) Color.Transparent else resultingColor, - Color.VectorConverter(resultingColor.colorSpace) - ) + val oneHandUI = remember { appPrefs.oneHandUI.state } + val listState = rememberLazyListState(lazyListState.first, lazyListState.second) + val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } + val searchShowingSimplexLink = remember { mutableStateOf(false) } + val searchChatFilteredBySimplexLink = remember { mutableStateOf(null) } + val showUnreadAndFavorites = remember { ChatController.appPrefs.showUnreadAndFavorites.state }.value + val baseContactTypes = remember { listOf(ContactType.CARD, ContactType.RECENT, ContactType.REQUEST) } + val contactTypes by remember(searchText.value.text.isEmpty()) { + derivedStateOf { contactTypesSearchTargets(baseContactTypes, searchText.value.text.isEmpty()) } } - val animatedFloat = remember { Animatable(if (newChat.isVisible()) 0f else 1f) } - LaunchedEffect(Unit) { - launch { - newChatSheetState.collect { - newChat = it - launch { - animatedColor.animateTo(if (newChat.isVisible()) resultingColor else Color.Transparent, newChatSheetAnimSpec()) - } - launch { - animatedFloat.animateTo(if (newChat.isVisible()) 1f else 0f, newChatSheetAnimSpec()) - if (newChat.isHiding()) closeNewChatSheet(false) + val allChats by remember(chatModel.chats.value, contactTypes) { + derivedStateOf { filterContactTypes(chatModel.chats.value, contactTypes) } + } + var scrollDirection by remember { mutableStateOf(ScrollDirection.Idle) } + var previousIndex by remember { mutableStateOf(0) } + var previousScrollOffset by remember { mutableStateOf(0) } + val keyboardState by getKeyboardState() + + LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) { + val currentIndex = listState.firstVisibleItemIndex + val currentScrollOffset = listState.firstVisibleItemScrollOffset + val threshold = 25 + + scrollDirection = when { + currentIndex > previousIndex -> ScrollDirection.Down + currentIndex < previousIndex -> ScrollDirection.Up + currentScrollOffset > previousScrollOffset + threshold -> ScrollDirection.Down + currentScrollOffset < previousScrollOffset - threshold -> ScrollDirection.Up + currentScrollOffset == previousScrollOffset -> ScrollDirection.Idle + else -> scrollDirection + } + + previousIndex = currentIndex + previousScrollOffset = currentScrollOffset + } + + val filteredContactChats = filteredContactChats( + showUnreadAndFavorites = showUnreadAndFavorites, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + searchShowingSimplexLink = searchShowingSimplexLink, + searchText = searchText.value.text, + contactChats = allChats + ) + + val sectionModifier = Modifier.fillMaxWidth() + + LazyColumnWithScrollBar( + Modifier.fillMaxSize(), + listState, + reverseLayout = oneHandUI.value + ) { + if (!oneHandUI.value) { + item { + Box(contentAlignment = Alignment.Center) { + val bottomPadding = DEFAULT_PADDING + AppBarTitle( + stringResource(MR.strings.new_message), + hostDevice(rh?.remoteHostId), + bottomPadding = bottomPadding + ) } } } - } - val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp - val maxWidth = with(LocalDensity.current) { windowWidth() * density } - Column( - Modifier - .fillMaxSize() - .padding(end = endPadding) - .offset { IntOffset(if (newChat.isGone()) -maxWidth.value.roundToInt() else 0, 0) } - .clickable(interactionSource = remember { MutableInteractionSource() }, indication = null) { closeNewChatSheet(true) } - .drawBehind { drawRect(animatedColor.value) }, - verticalArrangement = Arrangement.Bottom, - horizontalAlignment = Alignment.End - ) { - val actions = remember { listOf(addContact, createGroup) } - val backgroundColor = if (isInDarkTheme()) - blendARGB(MaterialTheme.colors.primary, Color.Black, 0.7F) - else - MaterialTheme.colors.background - LazyColumn(Modifier - .graphicsLayer { - alpha = animatedFloat.value - translationY = (1 - animatedFloat.value) * 20.dp.toPx() - }) { - items(actions.size) { index -> + stickyHeader { + Column( + Modifier + .offset { + val y = if (searchText.value.text.isEmpty()) { + val offsetMultiplier = if (oneHandUI.value) 1 else -1 + + if ( + (oneHandUI.value && scrollDirection == ScrollDirection.Up) || + (appPlatform.isAndroid && keyboardState == KeyboardState.Opened) + ) { + 0 + } else if (oneHandUI.value && listState.firstVisibleItemIndex == 0) { + listState.firstVisibleItemScrollOffset + } else if (!oneHandUI.value && listState.firstVisibleItemIndex == 0) { + 0 + } else if (!oneHandUI.value && listState.firstVisibleItemIndex == 1) { + -listState.firstVisibleItemScrollOffset + } else { + offsetMultiplier * 1000 + } + } else { + 0 + } + IntOffset(0, y) + } + .background(MaterialTheme.colors.background) + ) { + Divider() + ContactsSearchBar( + listState = listState, + searchText = searchText, + searchShowingSimplexLink = searchShowingSimplexLink, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + close = close, + ) + if (!oneHandUI.value) { + Divider() + } + } + } + item { + Spacer(Modifier.padding(bottom = 27.dp)) + + val actionButtonsOriginal = listOf( + Triple( + painterResource(MR.images.ic_add_link), + stringResource(MR.strings.add_contact_tab), + addContact, + ), + Triple( + painterResource(MR.images.ic_qr_code), + if (appPlatform.isAndroid) stringResource(MR.strings.scan_paste_link) else stringResource(MR.strings.paste_link), + scanPaste, + ), + Triple( + painterResource(MR.images.ic_group), + stringResource(MR.strings.create_group_button), + createGroup, + ) + ) + + val actionButtons by remember(oneHandUI.value) { + derivedStateOf { + if (oneHandUI.value) actionButtonsOriginal.asReversed() else actionButtonsOriginal + } + } + + if (searchText.value.text.isEmpty()) { Row { - Spacer(Modifier.weight(1f)) - Box(contentAlignment = Alignment.CenterEnd) { - Button( - actions[index], - shape = RoundedCornerShape(21.dp), - colors = ButtonDefaults.textButtonColors(backgroundColor = backgroundColor), - elevation = null, - contentPadding = PaddingValues(horizontal = DEFAULT_PADDING_HALF, vertical = DEFAULT_PADDING_HALF), - modifier = Modifier.height(42.dp) - ) { - Text( - stringResource(titles[index]), - Modifier.padding(start = DEFAULT_PADDING_HALF), - color = if (isInDarkTheme()) MaterialTheme.colors.primary else MaterialTheme.colors.primary, - fontWeight = FontWeight.Medium, - ) - Icon( - painterResource(icons[index]), - stringResource(titles[index]), - Modifier.size(42.dp), - tint = if (isInDarkTheme()) MaterialTheme.colors.primary else MaterialTheme.colors.primary + SectionView { + actionButtons.map { + NewChatButton( + icon = it.first, + text = it.second, + click = it.third, ) } } - Spacer(Modifier.width(DEFAULT_PADDING)) } - Spacer(Modifier.height(DEFAULT_PADDING)) + + val deletedContactTypes = listOf(ContactType.CHAT_DELETED) + val deletedChats by remember(chatModel.chats.value, deletedContactTypes) { + derivedStateOf { filterContactTypes(chatModel.chats.value, deletedContactTypes) } + } + if (deletedChats.isNotEmpty()) { + SectionDividerSpaced(maxBottomPadding = false) + Row(modifier = sectionModifier) { + SectionView { + SectionItemView( + click = { + ModalManager.start.showCustomModal { closeDeletedChats -> + ModalView( + close = closeDeletedChats, + closeOnTop = !oneHandUI.value, + ) { + DeletedContactsView(rh = rh, closeDeletedChats = closeDeletedChats, close = { + ModalManager.start.closeModals() + }) + } + } + } + ) { + Icon( + painterResource(MR.images.ic_inventory_2), + contentDescription = stringResource(MR.strings.deleted_chats), + tint = MaterialTheme.colors.secondary, + ) + TextIconSpaced(false) + Text(text = stringResource(MR.strings.deleted_chats), color = MaterialTheme.colors.onBackground) + } + } + } + } } } - FloatingActionButton( - onClick = { if (!stopped) closeNewChatSheet(true) }, - Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING), - elevation = FloatingActionButtonDefaults.elevation( - defaultElevation = 0.dp, - pressedElevation = 0.dp, - hoveredElevation = 0.dp, - focusedElevation = 0.dp, - ), - backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, - contentColor = Color.White + + item { + if (filteredContactChats.isNotEmpty() && !oneHandUI.value) { + if (searchText.value.text.isNotEmpty()) { + Spacer(Modifier.height(DEFAULT_PADDING)) + } else { + SectionDividerSpaced() + } + Text( + stringResource(MR.strings.contact_list_header_title).uppercase(), color = MaterialTheme.colors.secondary, style = MaterialTheme.typography.body2, + modifier = sectionModifier.padding(start = DEFAULT_PADDING, bottom = DEFAULT_PADDING_HALF), fontSize = 12.sp + ) + } + } + + itemsIndexed(filteredContactChats) { index, chat -> + val nextChatSelected = remember(chat.id, filteredContactChats) { + derivedStateOf { + chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value + } + } + ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = true) + } + } + + if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) { + Column(sectionModifier.fillMaxSize().padding(DEFAULT_PADDING)) { + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + Text( + generalGetString(MR.strings.no_filtered_contacts), + color = MaterialTheme.colors.secondary + ) + } + } + } +} + +@Composable +private fun NewChatButton( + icon: Painter, + text: String, + click: () -> Unit, + textColor: Color = Color.Unspecified, + iconColor: Color = MaterialTheme.colors.primary, + disabled: Boolean = false +) { + SectionItemView(click, disabled = disabled) { + Row { + Icon(icon, text, tint = if (disabled) MaterialTheme.colors.secondary else iconColor) + TextIconSpaced(false) + Text(text, color = if (disabled) MaterialTheme.colors.secondary else textColor) + } + } +} + +@Composable +private fun ContactsSearchBar( + listState: LazyListState, + searchText: MutableState, + searchShowingSimplexLink: MutableState, + searchChatFilteredBySimplexLink: MutableState, + close: () -> Unit, +) { + var focused by remember { mutableStateOf(false) } + + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + val focusRequester = remember { FocusRequester() } + Icon( + painterResource(MR.images.ic_search), + contentDescription = null, + Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF).size(22.dp * fontSizeSqrtMultiplier), + tint = MaterialTheme.colors.secondary + ) + SearchTextField( + Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester), + placeholder = stringResource(MR.strings.search_or_paste_simplex_link), + alwaysVisible = true, + searchText = searchText, + trailingContent = null, ) { - Icon( - painterResource(MR.images.ic_edit_filled), stringResource(MR.strings.add_contact_or_create_group), - Modifier.graphicsLayer { alpha = 1 - animatedFloat.value } + searchText.value = searchText.value.copy(it) + } + val hasText = remember { derivedStateOf { searchText.value.text.isNotEmpty() } } + if (hasText.value) { + val hideSearchOnBack: () -> Unit = { searchText.value = TextFieldValue() } + BackHandler(onBack = hideSearchOnBack) + KeyChangeEffect(chatModel.currentRemoteHost.value) { + hideSearchOnBack() + } + } else { + Row { + val padding = if (appPlatform.isDesktop) 0.dp else 7.dp + if (chatModel.chats.size > 0) { + ToggleFilterButton() + } + Spacer(Modifier.width(padding)) + } + } + val focusManager = LocalFocusManager.current + val keyboardState = getKeyboardState() + LaunchedEffect(keyboardState.value) { + if (keyboardState.value == KeyboardState.Closed && focused) { + focusManager.clearFocus() + } + } + val view = LocalMultiplatformView() + LaunchedEffect(Unit) { + snapshotFlow { searchText.value.text } + .distinctUntilChanged() + .collect { + val link = strHasSingleSimplexLink(it.trim()) + if (link != null) { + // if SimpleX link is pasted, show connection dialogue + hideKeyboard(view) + if (link.format is Format.SimplexLink) { + val linkText = + link.simplexLinkText(link.format.linkType, link.format.smpHosts) + searchText.value = + searchText.value.copy(linkText, selection = TextRange.Zero) + } + searchShowingSimplexLink.value = true + searchChatFilteredBySimplexLink.value = null + connect( + link = link.text, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + close = close, + cleanup = { searchText.value = TextFieldValue() } + ) + } else if (!searchShowingSimplexLink.value || it.isEmpty()) { + if (it.isNotEmpty()) { + // if some other text is pasted, enter search mode + focusRequester.requestFocus() + } else if (listState.layoutInfo.totalItemsCount > 0) { + listState.scrollToItem(0) + } + searchShowingSimplexLink.value = false + searchChatFilteredBySimplexLink.value = null + } + } + } + } +} + +@Composable +private fun ToggleFilterButton() { + val pref = remember { appPrefs.showUnreadAndFavorites } + IconButton(onClick = { pref.set(!pref.get()) }) { + val sp16 = with(LocalDensity.current) { 16.sp.toDp() } + Icon( + painterResource(MR.images.ic_filter_list), + null, + tint = if (pref.state.value) MaterialTheme.colors.background else MaterialTheme.colors.secondary, + modifier = Modifier + .padding(3.dp) + .background(color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50)) + .border(width = 1.dp, color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50)) + .padding(3.dp) + .size(sp16) + ) + } +} + +private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState, close: () -> Unit, cleanup: (() -> Unit)?) { + withBGApi { + planAndConnect( + chatModel.remoteHostId(), + URI.create(link), + incognito = null, + filterKnownContact = { searchChatFilteredBySimplexLink.value = it.id }, + close = close, + cleanup = cleanup, + ) + } +} + +private fun filteredContactChats( + showUnreadAndFavorites: Boolean, + searchShowingSimplexLink: State, + searchChatFilteredBySimplexLink: State, + searchText: String, + contactChats: List +): List { + val linkChatId = searchChatFilteredBySimplexLink.value + val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase() + + return if (linkChatId != null) { + contactChats.filter { it.id == linkChatId } + } else { + contactChats.filter { chat -> + filterChat( + chat = chat, + searchText = s, + showUnreadAndFavorites = showUnreadAndFavorites ) - Icon( - painterResource(MR.images.ic_close), stringResource(MR.strings.add_contact_or_create_group), - Modifier.graphicsLayer { alpha = animatedFloat.value } + } + } + .sortedWith(chatsByTypeComparator) +} + +private fun filterChat(chat: Chat, searchText: String, showUnreadAndFavorites: Boolean): Boolean { + var meetsPredicate = true + val s = searchText.trim().lowercase() + val cInfo = chat.chatInfo + + if (searchText.isNotEmpty()) { + meetsPredicate = cInfo.anyNameContains(s) + } + + if (showUnreadAndFavorites) { + meetsPredicate = meetsPredicate && (cInfo.chatSettings?.favorite ?: false) + } + + return meetsPredicate +} + +private val chatsByTypeComparator = Comparator { chat1, chat2 -> + val chat1Type = chatContactType(chat1) + val chat2Type = chatContactType(chat2) + + when { + chat1Type.ordinal < chat2Type.ordinal -> -1 + chat1Type.ordinal > chat2Type.ordinal -> 1 + + else -> chat2.chatInfo.chatTs.compareTo(chat1.chatInfo.chatTs) + } +} + +private fun contactTypesSearchTargets(baseContactTypes: List, searchEmpty: Boolean): List { + return if (baseContactTypes.contains(ContactType.CHAT_DELETED) || searchEmpty) { + baseContactTypes + } else { + baseContactTypes + ContactType.CHAT_DELETED + } +} + +@Composable +private fun DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats: () -> Unit, close: () -> Unit) { + val oneHandUI = remember { appPrefs.oneHandUI.state } + val keyboardState by getKeyboardState() + val showToolbarInOneHandUI = remember { derivedStateOf { keyboardState == KeyboardState.Closed && oneHandUI.value } } + + Scaffold( + bottomBar = { + if (showToolbarInOneHandUI.value) { + Column { + Divider() + CloseSheetBar( + close = closeDeletedChats, + showClose = true, + endButtons = { Spacer(Modifier.minimumInteractiveComponentSize()) }, + arrangement = Arrangement.Bottom, + closeBarTitle = generalGetString(MR.strings.deleted_chats), + barPaddingValues = PaddingValues(horizontal = 0.dp) + ) + } + } + } + ) { + Column( + Modifier + .fillMaxSize() + .padding(it) + ) { + if (!oneHandUI.value) { + Box(contentAlignment = Alignment.Center) { + val bottomPadding = DEFAULT_PADDING + AppBarTitle( + stringResource(MR.strings.deleted_chats), + hostDevice(rh?.remoteHostId), + bottomPadding = bottomPadding + ) + } + } + + val listState = rememberLazyListState(lazyListState.first, lazyListState.second) + val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } + val searchShowingSimplexLink = remember { mutableStateOf(false) } + val searchChatFilteredBySimplexLink = remember { mutableStateOf(null) } + val showUnreadAndFavorites = remember { appPrefs.showUnreadAndFavorites.state }.value + val allChats by remember(chatModel.chats.value) { + derivedStateOf { filterContactTypes(chatModel.chats.value, listOf(ContactType.CHAT_DELETED)) } + } + val filteredContactChats = filteredContactChats( + showUnreadAndFavorites = showUnreadAndFavorites, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + searchShowingSimplexLink = searchShowingSimplexLink, + searchText = searchText.value.text, + contactChats = allChats ) + + LazyColumnWithScrollBar( + Modifier.fillMaxSize(), + reverseLayout = oneHandUI.value, + ) { + item { + if (!oneHandUI.value) { + Divider() + } + ContactsSearchBar( + listState = listState, + searchText = searchText, + searchShowingSimplexLink = searchShowingSimplexLink, + searchChatFilteredBySimplexLink = searchChatFilteredBySimplexLink, + close = close, + ) + Divider() + + Spacer(Modifier.padding(bottom = DEFAULT_PADDING)) + } + + itemsIndexed(filteredContactChats) { index, chat -> + val nextChatSelected = remember(chat.id, filteredContactChats) { + derivedStateOf { + chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value + } + } + ContactListNavLinkView(chat, nextChatSelected, showDeletedChatIcon = false) + } + } + if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) { + Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING)) { + Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + Text( + generalGetString(MR.strings.no_filtered_contacts), + color = MaterialTheme.colors.secondary, + ) + } + } + } } } } @@ -260,12 +724,6 @@ fun ActionButton( @Composable private fun PreviewNewChatSheet() { SimpleXTheme { - NewChatSheetLayout( - MutableStateFlow(AnimatedViewState.VISIBLE), - stopped = false, - addContact = {}, - createGroup = {}, - closeNewChatSheet = {}, - ) + NewChatSheet(rh = null, close = {}) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt index f341d59305..419d3b6ed7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt @@ -5,6 +5,7 @@ import SectionItemView import SectionTextFooter import SectionView import androidx.compose.foundation.* +import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.pager.HorizontalPager @@ -25,6 +26,7 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.sp import chat.simplex.common.model.* import chat.simplex.common.model.ChatModel.controller +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* @@ -60,9 +62,8 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC /** When [AddContactLearnMore] is open, we don't need to drop [ChatModel.showingInvitation]. * Otherwise, it will be called here AFTER [AddContactLearnMore] is launched and will clear the value too soon. * It will be dropped automatically when connection established or when user goes away from this screen. - * It applies only to Android because on Desktop center space will not be overlapped by [AddContactLearnMore] **/ - if (chatModel.showingInvitation.value != null && (!ModalManager.center.hasModalsOpen() || appPlatform.isDesktop)) { + if (chatModel.showingInvitation.value != null && ModalManager.start.openModalCount() == 1) { val conn = contactConnection.value if (chatModel.showingInvitation.value?.connChatUsed == false && conn != null) { AlertManager.shared.showAlertDialog( @@ -77,7 +78,7 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC controller.deleteChat(Chat(remoteHostId = rh?.remoteHostId, chatInfo = chatInfo, chatItems = listOf())) if (chatModel.chatId.value == chatInfo.id) { chatModel.chatId.value = null - ModalManager.end.closeModals() + ModalManager.start.closeModals() } } } @@ -96,66 +97,61 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC } } - Column( - Modifier.fillMaxSize(), - ) { - Box(contentAlignment = Alignment.Center) { - val bottomPadding = DEFAULT_PADDING - AppBarTitle(stringResource(MR.strings.new_chat), hostDevice(rh?.remoteHostId), bottomPadding = bottomPadding) - Column(Modifier.align(Alignment.CenterEnd).padding(bottom = bottomPadding, end = DEFAULT_PADDING)) { - AddContactLearnMoreButton() + BoxWithConstraints { + ColumnWithScrollBar { + AppBarTitle(stringResource(MR.strings.new_chat), hostDevice(rh?.remoteHostId), bottomPadding = DEFAULT_PADDING) + val scope = rememberCoroutineScope() + val pagerState = rememberPagerState( + initialPage = selection.value.ordinal, + initialPageOffsetFraction = 0f + ) { NewChatOption.values().size } + KeyChangeEffect(pagerState.currentPage) { + selection.value = NewChatOption.values()[pagerState.currentPage] } - } - val scope = rememberCoroutineScope() - val pagerState = rememberPagerState( - initialPage = selection.value.ordinal, - initialPageOffsetFraction = 0f - ) { NewChatOption.values().size } - KeyChangeEffect(pagerState.currentPage) { - selection.value = NewChatOption.values()[pagerState.currentPage] - } - TabRow( - selectedTabIndex = pagerState.currentPage, - backgroundColor = Color.Transparent, - contentColor = MaterialTheme.colors.primary, - ) { - tabTitles.forEachIndexed { index, it -> - LeadingIconTab( - selected = pagerState.currentPage == index, - onClick = { - scope.launch { - pagerState.animateScrollToPage(index) - } - }, - text = { Text(it, fontSize = 13.sp) }, - icon = { - Icon( - if (NewChatOption.INVITE.ordinal == index) painterResource(MR.images.ic_repeat_one) else painterResource(MR.images.ic_qr_code), - it - ) - }, - selectedContentColor = MaterialTheme.colors.primary, - unselectedContentColor = MaterialTheme.colors.secondary, - ) - } - } - - HorizontalPager(state = pagerState, Modifier.fillMaxSize(), verticalAlignment = Alignment.Top) { index -> - // LALAL SCROLLBAR DOESN'T WORK - ColumnWithScrollBar( - Modifier - .fillMaxSize(), - verticalArrangement = if (index == NewChatOption.INVITE.ordinal && connReqInvitation.isEmpty()) Arrangement.Center else Arrangement.Top) { - Spacer(Modifier.height(DEFAULT_PADDING)) - when (index) { - NewChatOption.INVITE.ordinal -> { - PrepareAndInviteView(rh?.remoteHostId, contactConnection, connReqInvitation, creatingConnReq) - } - NewChatOption.CONNECT.ordinal -> { - ConnectView(rh?.remoteHostId, showQRCodeScanner, pastedLink, close) - } + TabRow( + selectedTabIndex = pagerState.currentPage, + backgroundColor = Color.Transparent, + contentColor = MaterialTheme.colors.primary, + ) { + tabTitles.forEachIndexed { index, it -> + LeadingIconTab( + selected = pagerState.currentPage == index, + onClick = { + scope.launch { + pagerState.animateScrollToPage(index) + } + }, + text = { Text(it, fontSize = 13.sp) }, + icon = { + Icon( + if (NewChatOption.INVITE.ordinal == index) painterResource(MR.images.ic_repeat_one) else painterResource(MR.images.ic_qr_code), + it + ) + }, + selectedContentColor = MaterialTheme.colors.primary, + unselectedContentColor = MaterialTheme.colors.secondary, + ) + } + } + + HorizontalPager(state = pagerState, Modifier, pageNestedScrollConnection = LocalAppBarHandler.current!!.connection, verticalAlignment = Alignment.Top, userScrollEnabled = appPlatform.isAndroid) { index -> + Column( + Modifier + .fillMaxWidth() + .heightIn(min = this@BoxWithConstraints.maxHeight - 150.dp), + verticalArrangement = if (index == NewChatOption.INVITE.ordinal && connReqInvitation.isEmpty()) Arrangement.Center else Arrangement.Top + ) { + Spacer(Modifier.height(DEFAULT_PADDING)) + when (index) { + NewChatOption.INVITE.ordinal -> { + PrepareAndInviteView(rh?.remoteHostId, contactConnection, connReqInvitation, creatingConnReq) + } + NewChatOption.CONNECT.ordinal -> { + ConnectView(rh?.remoteHostId, showQRCodeScanner, pastedLink, close) + } + } + SectionBottomSpacer() } - SectionBottomSpacer() } } } @@ -211,15 +207,16 @@ private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection Spacer(Modifier.height(10.dp)) val incognito = remember { mutableStateOf(controller.appPrefs.incognito.get()) } IncognitoToggle(controller.appPrefs.incognito, incognito) { - if (appPlatform.isDesktop) ModalManager.end.closeModals() - ModalManager.end.showModal { IncognitoView() } + ModalManager.start.showModal { IncognitoView() } } KeyChangeEffect(incognito.value) { withBGApi { val contactConn = contactConnection.value ?: return@withBGApi val conn = controller.apiSetConnectionIncognito(rhId, contactConn.pccConnId, incognito.value) ?: return@withBGApi - contactConnection.value = conn - chatModel.updateContactConnection(rhId, conn) + withChats { + contactConnection.value = conn + updateContactConnection(rhId, conn) + } } chatModel.markShowingInvitationUsed() } @@ -227,11 +224,10 @@ private fun InviteView(rhId: Long?, connReqInvitation: String, contactConnection } @Composable -private fun AddContactLearnMoreButton() { +fun AddContactLearnMoreButton() { IconButton( { - if (appPlatform.isDesktop) ModalManager.end.closeModals() - ModalManager.end.showModalCloseable { close -> + ModalManager.start.showModalCloseable { close -> AddContactLearnMore(close) } } @@ -239,6 +235,7 @@ private fun AddContactLearnMoreButton() { Icon( painterResource(MR.images.ic_info), stringResource(MR.strings.learn_more), + tint = MaterialTheme.colors.primary ) } } @@ -296,7 +293,7 @@ private fun PasteLinkView(rhId: Long?, pastedLink: MutableState, showQRC @Composable fun LinkTextView(link: String, share: Boolean) { val clipboard = LocalClipboardManager.current - Row(Modifier.fillMaxWidth().heightIn(min = 46.dp).padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically) { + Row(Modifier.fillMaxWidth().heightIn(min = DEFAULT_MIN_SECTION_ITEM_HEIGHT).padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically) { Box(Modifier.weight(1f).clickable { chatModel.markShowingInvitationUsed() clipboard.shareText(link) @@ -367,9 +364,11 @@ private fun createInvitation( withBGApi { val (r, alert) = controller.apiAddContact(rhId, incognito = controller.appPrefs.incognito.get()) if (r != null) { - chatModel.updateContactConnection(rhId, r.second) - chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connReq = simplexChatLink(r.first), connChatUsed = false) - contactConnection.value = r.second + withChats { + updateContactConnection(rhId, r.second) + chatModel.showingInvitation.value = ShowingInvitation(connId = r.second.id, connReq = simplexChatLink(r.first), connChatUsed = false) + contactConnection.value = r.second + } } else { creatingConnReq.value = false if (alert != null) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt index 5e50475951..20a7ada3aa 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/CreateSimpleXAddress.kt @@ -1,15 +1,16 @@ package chat.simplex.common.views.onboarding -import SectionBottomSpacer +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.font.FontWeight import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.style.TextAlign @@ -75,79 +76,102 @@ private fun CreateSimpleXAddressLayout( createAddress: () -> Unit, nextStep: () -> Unit, ) { - ColumnWithScrollBar( - Modifier.fillMaxSize().padding(top = DEFAULT_PADDING), - horizontalAlignment = Alignment.CenterHorizontally, + val handler = remember { AppBarHandler() } + CompositionLocalProvider( + LocalAppBarHandler provides handler ) { - AppBarTitle(stringResource(MR.strings.simplex_address)) + ModalView({}, showClose = false) { + ColumnWithScrollBar( + Modifier + .fillMaxSize() + .themedBackground(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AppBarTitle(stringResource(MR.strings.simplex_address)) - Spacer(Modifier.weight(1f)) + Spacer(Modifier.weight(1f)) - if (userAddress != null) { - SimpleXLinkQRCode(userAddress.connReqContact) - ShareAddressButton { share(simplexChatLink(userAddress.connReqContact)) } - Spacer(Modifier.weight(1f)) - ShareViaEmailButton { sendEmail(userAddress) } - Spacer(Modifier.weight(1f)) - ContinueButton(nextStep) - } else { - CreateAddressButton(createAddress) - TextBelowButton(stringResource(MR.strings.you_can_make_address_visible_via_settings)) - Spacer(Modifier.weight(1f)) - SkipButton(nextStep) + if (userAddress != null) { + SimpleXLinkQRCode(userAddress.connReqContact) + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + Row { + ShareAddressButton { share(simplexChatLink(userAddress.connReqContact)) } + Spacer(Modifier.width(DEFAULT_PADDING * 2)) + ShareViaEmailButton { sendEmail(userAddress) } + } + Spacer(Modifier.height(DEFAULT_PADDING)) + Spacer(Modifier.weight(1f)) + Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { + OnboardingActionButton( + modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp), + labelId = MR.strings.continue_to_next_step, + onboarding = null, + onclick = nextStep + ) + // Reserve space + TextButtonBelowOnboardingButton("", null) + } + } else { + Button(createAddress, Modifier, shape = CircleShape, contentPadding = PaddingValues()) { + Icon(painterResource(MR.images.ic_mail_filled), null, Modifier.size(100.dp).background(MaterialTheme.colors.primary, CircleShape).padding(25.dp), tint = Color.White) + } + Spacer(Modifier.height(DEFAULT_PADDING)) + Spacer(Modifier.weight(1f)) + Text(stringResource(MR.strings.create_simplex_address), style = MaterialTheme.typography.h3, fontWeight = FontWeight.Bold) + TextBelowButton(stringResource(MR.strings.you_can_make_address_visible_via_settings)) + Spacer(Modifier.height(DEFAULT_PADDING)) + Spacer(Modifier.weight(1f)) + + Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { + OnboardingActionButton( + modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier.widthIn(min = 300.dp), + labelId = MR.strings.create_address_button, + onboarding = null, + onclick = createAddress + ) + TextButtonBelowOnboardingButton(stringResource(MR.strings.dont_create_address), nextStep) + } + } + } } - SectionBottomSpacer() - } -} - -@Composable -private fun CreateAddressButton(onClick: () -> Unit) { - TextButton(onClick) { - Text(stringResource(MR.strings.create_simplex_address), style = MaterialTheme.typography.h2, color = MaterialTheme.colors.primary) } } @Composable fun ShareAddressButton(onClick: () -> Unit) { - SimpleButtonFrame(onClick) { - Icon( - painterResource(MR.images.ic_share_filled), generalGetString(MR.strings.share_verb), tint = MaterialTheme.colors.primary, - modifier = Modifier.padding(end = 8.dp).size(18.dp) - ) - Text(stringResource(MR.strings.share_verb), style = MaterialTheme.typography.caption, color = MaterialTheme.colors.primary) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + IconButton(onClick, Modifier.padding(bottom = DEFAULT_PADDING_HALF).border(1.dp, MaterialTheme.colors.secondary.copy(0.1f), CircleShape)) { + Icon( + painterResource(MR.images.ic_share_filled), generalGetString(MR.strings.share_verb), tint = MaterialTheme.colors.primary, + modifier = Modifier.size(50.dp).padding(DEFAULT_PADDING_HALF) + ) + } + Text(stringResource(MR.strings.share_verb)) } } @Composable fun ShareViaEmailButton(onClick: () -> Unit) { - SimpleButtonFrame(onClick) { - Icon( - painterResource(MR.images.ic_mail), generalGetString(MR.strings.share_verb), tint = MaterialTheme.colors.primary, - modifier = Modifier.padding(end = 8.dp).size(30.dp) - ) - Text(stringResource(MR.strings.invite_friends), style = MaterialTheme.typography.h6, color = MaterialTheme.colors.primary) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + IconButton(onClick, Modifier.padding(bottom = DEFAULT_PADDING_HALF).border(1.dp, MaterialTheme.colors.secondary.copy(0.1f), CircleShape)) { + Icon( + painterResource(MR.images.ic_mail), generalGetString(MR.strings.share_verb), tint = MaterialTheme.colors.primary, + modifier = Modifier.size(50.dp).padding(DEFAULT_PADDING_HALF) + ) + } + Text(stringResource(MR.strings.invite_friends_short)) } } -@Composable -private fun ContinueButton(onClick: () -> Unit) { - SimpleButtonIconEnded(stringResource(MR.strings.continue_to_next_step), painterResource(MR.images.ic_chevron_right), click = onClick) -} - -@Composable -private fun SkipButton(onClick: () -> Unit) { - SimpleButtonIconEnded(stringResource(MR.strings.dont_create_address), painterResource(MR.images.ic_chevron_right), click = onClick) - TextBelowButton(stringResource(MR.strings.you_can_create_it_later)) -} - @Composable private fun TextBelowButton(text: String) { Text( text, Modifier .fillMaxWidth() - .padding(horizontal = DEFAULT_PADDING * 3), + .padding(horizontal = DEFAULT_PADDING * 3, vertical = DEFAULT_PADDING_HALF), style = MaterialTheme.typography.subtitle1, + color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center, ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt index 28fa1d3a48..9c7e2bdce7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/HowItWorks.kt @@ -23,9 +23,10 @@ import dev.icerock.moko.resources.StringResource @Composable fun HowItWorks(user: User?, onboardingStage: SharedPreference? = null) { - ColumnWithScrollBar(Modifier - .fillMaxWidth() - .padding(horizontal = DEFAULT_PADDING), + ColumnWithScrollBar( + Modifier + .fillMaxWidth() + .padding(DEFAULT_PADDING), ) { AppBarTitle(stringResource(MR.strings.how_simplex_works), withPadding = false) ReadableText(MR.strings.many_people_asked_how_can_it_deliver) @@ -46,6 +47,7 @@ fun HowItWorks(user: User?, onboardingStage: SharedPreference? } Spacer(Modifier.fillMaxHeight().weight(1f)) } + Spacer(Modifier.height(DEFAULT_PADDING)) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt index 4e3b70405d..f0e34218d1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/LinkAMobileView.kt @@ -10,9 +10,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel +import chat.simplex.common.platform.BackHandler import chat.simplex.common.platform.chatModel import chat.simplex.common.ui.theme.DEFAULT_PADDING +import chat.simplex.common.ui.theme.themedBackground import chat.simplex.common.views.helpers.* import chat.simplex.common.views.remote.AddingMobileDevice import chat.simplex.common.views.remote.DeviceNameField @@ -56,7 +59,13 @@ private fun LinkAMobileLayout( staleQrCode: MutableState, updateDeviceName: (String) -> Unit, ) { - Column(Modifier.padding(top = 20.dp)) { + Column(Modifier.themedBackground()) { + CloseSheetBar(close = { + appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) + }) + BackHandler(onBack = { + appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) + }) AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles)) Row(Modifier.weight(1f).padding(horizontal = DEFAULT_PADDING * 2), verticalAlignment = Alignment.CenterVertically) { Column( @@ -66,7 +75,7 @@ private fun LinkAMobileLayout( SectionView(generalGetString(MR.strings.this_device_name).uppercase()) { DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) } SectionTextFooter(generalGetString(MR.strings.this_device_name_shared_with_mobile)) - PreferenceToggle(stringResource(MR.strings.multicast_discoverable_via_local_network), remember { ChatModel.controller.appPrefs.offerRemoteMulticast.state }.value) { + PreferenceToggle(stringResource(MR.strings.multicast_discoverable_via_local_network), checked = remember { ChatModel.controller.appPrefs.offerRemoteMulticast.state }.value) { ChatModel.controller.appPrefs.offerRemoteMulticast.set(it) } } @@ -82,11 +91,5 @@ private fun LinkAMobileLayout( } } } - SimpleButtonDecorated( - text = stringResource(MR.strings.about_simplex), - icon = painterResource(MR.images.ic_arrow_back_ios_new), - textDecoration = TextDecoration.None, - fontWeight = FontWeight.Medium - ) { chatModel.controller.appPrefs.onboardingStage.set(OnboardingStage.Step1_SimpleXInfo) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt index 9124808959..1903b3cf81 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetNotificationsMode.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.AnnotatedString import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -16,36 +17,56 @@ import androidx.compose.ui.unit.sp import chat.simplex.common.model.ChatModel import chat.simplex.common.model.NotificationsMode import chat.simplex.common.platform.ColumnWithScrollBar +import chat.simplex.common.platform.appPlatform import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.usersettings.changeNotificationsMode import chat.simplex.res.MR -import dev.icerock.moko.resources.StringResource @Composable fun SetNotificationsMode(m: ChatModel) { - ColumnWithScrollBar( - modifier = Modifier - .fillMaxSize() - .padding(vertical = 14.dp) + val handler = remember { AppBarHandler() } + CompositionLocalProvider( + LocalAppBarHandler provides handler ) { - //CloseSheetBar(null) - AppBarTitle(stringResource(MR.strings.onboarding_notifications_mode_title)) - val currentMode = rememberSaveable { mutableStateOf(NotificationsMode.default) } - Column(Modifier.padding(horizontal = DEFAULT_PADDING * 1f)) { - Text(stringResource(MR.strings.onboarding_notifications_mode_subtitle), Modifier.fillMaxWidth(), textAlign = TextAlign.Center) - Spacer(Modifier.height(DEFAULT_PADDING * 2f)) - NotificationButton(currentMode, NotificationsMode.OFF, MR.strings.onboarding_notifications_mode_off, MR.strings.onboarding_notifications_mode_off_desc) - NotificationButton(currentMode, NotificationsMode.PERIODIC, MR.strings.onboarding_notifications_mode_periodic, MR.strings.onboarding_notifications_mode_periodic_desc) - NotificationButton(currentMode, NotificationsMode.SERVICE, MR.strings.onboarding_notifications_mode_service, MR.strings.onboarding_notifications_mode_service_desc) - } - Spacer(Modifier.fillMaxHeight().weight(1f)) - Box(Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), contentAlignment = Alignment.Center) { - OnboardingActionButton(MR.strings.use_chat, OnboardingStage.OnboardingComplete, false) { - changeNotificationsMode(currentMode.value, m) + ModalView({}, showClose = false) { + ColumnWithScrollBar( + modifier = Modifier + .fillMaxSize() + .themedBackground() + ) { + Box(Modifier.align(Alignment.CenterHorizontally)) { + AppBarTitle(stringResource(MR.strings.onboarding_notifications_mode_title)) + } + val currentMode = rememberSaveable { mutableStateOf(NotificationsMode.default) } + Column(Modifier.padding(horizontal = DEFAULT_PADDING * 1f)) { + Text(stringResource(MR.strings.onboarding_notifications_mode_subtitle), Modifier.fillMaxWidth(), textAlign = TextAlign.Center) + Spacer(Modifier.height(DEFAULT_PADDING * 2f)) + SelectableCard(currentMode, NotificationsMode.OFF, stringResource(MR.strings.onboarding_notifications_mode_off), annotatedStringResource(MR.strings.onboarding_notifications_mode_off_desc)) { + currentMode.value = NotificationsMode.OFF + } + SelectableCard(currentMode, NotificationsMode.PERIODIC, stringResource(MR.strings.onboarding_notifications_mode_periodic), annotatedStringResource(MR.strings.onboarding_notifications_mode_periodic_desc)) { + currentMode.value = NotificationsMode.PERIODIC + } + SelectableCard(currentMode, NotificationsMode.SERVICE, stringResource(MR.strings.onboarding_notifications_mode_service), annotatedStringResource(MR.strings.onboarding_notifications_mode_service_desc)) { + currentMode.value = NotificationsMode.SERVICE + } + } + Spacer(Modifier.fillMaxHeight().weight(1f)) + Column(Modifier.widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { + OnboardingActionButton( + modifier = if (appPlatform.isAndroid) Modifier.padding(horizontal = DEFAULT_PADDING * 2).fillMaxWidth() else Modifier, + labelId = MR.strings.use_chat, + onboarding = OnboardingStage.OnboardingComplete, + onclick = { + changeNotificationsMode(currentMode.value, m) + } + ) + // Reserve space + TextButtonBelowOnboardingButton("", null) + } } } - Spacer(Modifier.fillMaxHeight().weight(1f)) } SetNotificationsModeAdditions() } @@ -54,22 +75,22 @@ fun SetNotificationsMode(m: ChatModel) { expect fun SetNotificationsModeAdditions() @Composable -private fun NotificationButton(currentMode: MutableState, mode: NotificationsMode, title: StringResource, description: StringResource) { +fun SelectableCard(currentValue: State, newValue: T, title: String, description: AnnotatedString, onSelected: (T) -> Unit) { TextButton( - onClick = { currentMode.value = mode }, - border = BorderStroke(1.dp, color = if (currentMode.value == mode) MaterialTheme.colors.primary else MaterialTheme.colors.secondary.copy(alpha = 0.5f)), + onClick = { onSelected(newValue) }, + border = BorderStroke(1.dp, color = if (currentValue.value == newValue) MaterialTheme.colors.primary else MaterialTheme.colors.secondary.copy(alpha = 0.5f)), shape = RoundedCornerShape(35.dp), ) { - Column(Modifier.padding(horizontal = 10.dp).padding(top = 4.dp, bottom = 8.dp)) { + Column(Modifier.padding(horizontal = 10.dp).padding(top = 4.dp, bottom = 8.dp).fillMaxWidth()) { Text( - stringResource(title), + title, style = MaterialTheme.typography.h3, fontWeight = FontWeight.Medium, - color = if (currentMode.value == mode) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, + color = if (currentValue.value == newValue) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, modifier = Modifier.padding(bottom = 8.dp).align(Alignment.CenterHorizontally), textAlign = TextAlign.Center ) - Text(annotatedStringResource(description), + Text(description, Modifier.align(Alignment.CenterHorizontally), fontSize = 15.sp, color = MaterialTheme.colors.onBackground, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt index 65bd89b11b..858ca68af3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SetupDatabasePassphrase.kt @@ -1,11 +1,8 @@ package chat.simplex.common.views.onboarding -import SectionBottomSpacer import SectionTextFooter import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.verticalScroll import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -20,6 +17,7 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.database.* @@ -31,7 +29,6 @@ import kotlinx.coroutines.delay fun SetupDatabasePassphrase(m: ChatModel) { val progressIndicator = remember { mutableStateOf(false) } val prefs = m.controller.appPrefs - val saveInPreferences = remember { mutableStateOf(prefs.storeDBPassphrase.get()) } val initialRandomDBPassphrase = remember { mutableStateOf(prefs.initialRandomDBPassphrase.get()) } // Do not do rememberSaveable on current key to prevent saving it on disk in clear text val currentKey = remember { mutableStateOf(if (initialRandomDBPassphrase.value) DatabaseUtils.ksDatabasePassword.get() ?: "" else "") } @@ -58,7 +55,16 @@ fun SetupDatabasePassphrase(m: ChatModel) { prefs.storeDBPassphrase.set(false) val newKeyValue = newKey.value - val success = encryptDatabase(currentKey, newKey, confirmNewKey, mutableStateOf(true), saveInPreferences, mutableStateOf(true), progressIndicator, false) + val success = encryptDatabase( + currentKey = currentKey, + newKey = newKey, + confirmNewKey = confirmNewKey, + initialRandomDBPassphrase = mutableStateOf(true), + useKeychain = mutableStateOf(false), + storedKey = mutableStateOf(true), + progressIndicator = progressIndicator, + migration = false + ) if (success) { startChat(newKeyValue) nextStep() @@ -98,86 +104,91 @@ private fun SetupDatabasePassphraseLayout( onConfirmEncrypt: () -> Unit, nextStep: () -> Unit, ) { - ColumnWithScrollBar( - Modifier.fillMaxSize().padding(top = DEFAULT_PADDING), - horizontalAlignment = Alignment.CenterHorizontally, + val handler = remember { AppBarHandler() } + CompositionLocalProvider( + LocalAppBarHandler provides handler ) { - AppBarTitle(stringResource(MR.strings.setup_database_passphrase)) + ModalView({}, showClose = false) { + ColumnWithScrollBar( + Modifier.fillMaxSize().themedBackground().padding(bottom = DEFAULT_PADDING * 2), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AppBarTitle(stringResource(MR.strings.setup_database_passphrase)) - Spacer(Modifier.weight(1f)) + Spacer(Modifier.weight(1f)) - Column(Modifier.width(600.dp)) { - val focusRequester = remember { FocusRequester() } - val focusManager = LocalFocusManager.current - LaunchedEffect(Unit) { - delay(100L) - focusRequester.requestFocus() - } - PassphraseField( - newKey, - generalGetString(MR.strings.new_passphrase), - modifier = Modifier - .padding(horizontal = DEFAULT_PADDING) - .focusRequester(focusRequester) - .onPreviewKeyEvent { - if ((it.key == Key.Enter || it.key == Key.NumPadEnter) && it.type == KeyEventType.KeyUp) { - focusManager.moveFocus(FocusDirection.Down) - true - } else { - false + Column(Modifier.width(600.dp)) { + val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + LaunchedEffect(Unit) { + delay(100L) + focusRequester.requestFocus() + } + PassphraseField( + newKey, + generalGetString(MR.strings.new_passphrase), + modifier = Modifier + .padding(horizontal = DEFAULT_PADDING) + .focusRequester(focusRequester) + .onPreviewKeyEvent { + if ((it.key == Key.Enter || it.key == Key.NumPadEnter) && it.type == KeyEventType.KeyUp) { + focusManager.moveFocus(FocusDirection.Down) + true + } else { + false + } + }, + showStrength = true, + isValid = ::validKey, + keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), + ) + val onClickUpdate = { + // Don't do things concurrently. Shouldn't be here concurrently, just in case + if (!progressIndicator.value) { + encryptDatabaseAlert(onConfirmEncrypt) } - }, - showStrength = true, - isValid = ::validKey, - keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), - ) - val onClickUpdate = { - // Don't do things concurrently. Shouldn't be here concurrently, just in case - if (!progressIndicator.value) { - encryptDatabaseAlert(onConfirmEncrypt) + } + val disabled = currentKey.value == newKey.value || + newKey.value != confirmNewKey.value || + newKey.value.isEmpty() || + !validKey(currentKey.value) || + !validKey(newKey.value) || + progressIndicator.value + + PassphraseField( + confirmNewKey, + generalGetString(MR.strings.confirm_new_passphrase), + modifier = Modifier + .padding(horizontal = DEFAULT_PADDING) + .onPreviewKeyEvent { + if (!disabled && (it.key == Key.Enter || it.key == Key.NumPadEnter) && it.type == KeyEventType.KeyUp) { + onClickUpdate() + true + } else { + false + } + }, + isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value }, + keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done) }), + ) + + Box(Modifier.align(Alignment.CenterHorizontally).padding(vertical = DEFAULT_PADDING)) { + SetPassphraseButton(disabled, onClickUpdate) + } + + Column { + SectionTextFooter(generalGetString(MR.strings.you_have_to_enter_passphrase_every_time)) + SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase)) + } + } + + Spacer(Modifier.weight(1f)) + SkipButton(progressIndicator.value) { + chatModel.desktopOnboardingRandomPassword.value = true + nextStep() } } - val disabled = currentKey.value == newKey.value || - newKey.value != confirmNewKey.value || - newKey.value.isEmpty() || - !validKey(currentKey.value) || - !validKey(newKey.value) || - progressIndicator.value - - PassphraseField( - confirmNewKey, - generalGetString(MR.strings.confirm_new_passphrase), - modifier = Modifier - .padding(horizontal = DEFAULT_PADDING) - .onPreviewKeyEvent { - if (!disabled && (it.key == Key.Enter || it.key == Key.NumPadEnter) && it.type == KeyEventType.KeyUp) { - onClickUpdate() - true - } else { - false - } - }, - isValid = { confirmNewKey.value == "" || newKey.value == confirmNewKey.value }, - keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done) }), - ) - - Box(Modifier.align(Alignment.CenterHorizontally).padding(vertical = DEFAULT_PADDING)) { - SetPassphraseButton(disabled, onClickUpdate) - } - - Column { - SectionTextFooter(generalGetString(MR.strings.you_have_to_enter_passphrase_every_time)) - SectionTextFooter(annotatedStringResource(MR.strings.impossible_to_recover_passphrase)) - } } - - Spacer(Modifier.weight(1f)) - SkipButton(progressIndicator.value) { - chatModel.desktopOnboardingRandomPassword.value = true - nextStep() - } - - SectionBottomSpacer() } } @@ -201,7 +212,8 @@ private fun SkipButton(disabled: Boolean, onClick: () -> Unit) { stringResource(MR.strings.you_can_change_it_later), Modifier .fillMaxWidth() - .padding(horizontal = DEFAULT_PADDING * 3), + .padding(horizontal = DEFAULT_PADDING * 3) + .padding(top = DEFAULT_PADDING, bottom = DEFAULT_PADDING - 5.dp), style = MaterialTheme.typography.subtitle1, color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt index 84c48bbc1b..c176950902 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.kt @@ -1,23 +1,26 @@ package chat.simplex.common.views.onboarding +import androidx.compose.animation.core.animateDpAsState import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.layout.ContentScale import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.* import chat.simplex.common.model.* -import chat.simplex.common.platform.ColumnWithScrollBar -import chat.simplex.common.platform.chatModel +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.views.migration.MigrateToDeviceView @@ -27,64 +30,67 @@ import dev.icerock.moko.resources.StringResource @Composable fun SimpleXInfo(chatModel: ChatModel, onboarding: Boolean = true) { - SimpleXInfoLayout( - user = chatModel.currentUser.value, - onboardingStage = if (onboarding) chatModel.controller.appPrefs.onboardingStage else null, - showModal = { modalView -> { if (onboarding) ModalManager.fullscreen.showModal { modalView(chatModel) } else ModalManager.start.showModal { modalView(chatModel) } } }, - ) + if (onboarding) { + ModalView({}, showClose = false, endButtons = { + IconButton({ ModalManager.fullscreen.showModal { HowItWorks(chatModel.currentUser.value, null) }}) { + Icon(painterResource(MR.images.ic_info), null, Modifier.size(28.dp), tint = MaterialTheme.colors.primary) + } + }) { + SimpleXInfoLayout( + user = chatModel.currentUser.value, + onboardingStage = chatModel.controller.appPrefs.onboardingStage + ) + } + } else { + SimpleXInfoLayout( + user = chatModel.currentUser.value, + onboardingStage = null + ) + } } @Composable fun SimpleXInfoLayout( user: User?, - onboardingStage: SharedPreference?, - showModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), + onboardingStage: SharedPreference? ) { ColumnWithScrollBar( Modifier .fillMaxSize() - .padding(start = DEFAULT_PADDING , end = DEFAULT_PADDING, top = DEFAULT_PADDING), + .padding(horizontal = DEFAULT_PADDING), horizontalAlignment = Alignment.CenterHorizontally ) { - Box(Modifier.fillMaxWidth().padding(top = 8.dp, bottom = 10.dp), contentAlignment = Alignment.Center) { + Box(Modifier.widthIn(max = if (appPlatform.isAndroid) 250.dp else 500.dp).padding(top = DEFAULT_PADDING + 8.dp), contentAlignment = Alignment.Center) { SimpleXLogo() } - Text(stringResource(MR.strings.next_generation_of_private_messaging), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = 48.dp).padding(horizontal = 36.dp), textAlign = TextAlign.Center) + Spacer(Modifier.weight(1f)) + + Text( + stringResource(MR.strings.next_generation_of_private_messaging), + style = MaterialTheme.typography.h3, + color = MaterialTheme.colors.secondary, + textAlign = TextAlign.Center + ) + + Spacer(Modifier.weight(1f)) Column { - InfoRow(painterResource(MR.images.privacy), MR.strings.privacy_redefined, MR.strings.first_platform_without_user_ids, width = 80.dp) - InfoRow(painterResource(MR.images.shield), MR.strings.immune_to_spam_and_abuse, MR.strings.people_can_connect_only_via_links_you_share) + InfoRow(painterResource(MR.images.privacy), MR.strings.privacy_redefined, MR.strings.first_platform_without_user_ids, width = 60.dp) + InfoRow(painterResource(MR.images.shield), MR.strings.immune_to_spam_and_abuse, MR.strings.people_can_connect_only_via_links_you_share, width = 46.dp) InfoRow(painterResource(if (isInDarkTheme()) MR.images.decentralized_light else MR.images.decentralized), MR.strings.decentralized, MR.strings.opensource_protocol_and_code_anybody_can_run_servers) } Spacer(Modifier.fillMaxHeight().weight(1f)) if (onboardingStage != null) { - Box(Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) { + Column(Modifier.padding(horizontal = DEFAULT_PADDING).widthIn(max = if (appPlatform.isAndroid) 450.dp else 1000.dp).align(Alignment.CenterHorizontally), horizontalAlignment = Alignment.CenterHorizontally) { OnboardingActionButton(user, onboardingStage) + TextButtonBelowOnboardingButton(stringResource(MR.strings.migrate_from_another_device)) { + chatModel.migrationState.value = MigrationToState.PasteOrScanLink + ModalManager.fullscreen.showCustomModal { close -> MigrateToDeviceView(close) } + } } - Spacer(Modifier.fillMaxHeight().weight(1f)) - - Box( - Modifier - .fillMaxWidth() - .padding(top = DEFAULT_PADDING), contentAlignment = Alignment.Center - ) { - SimpleButtonDecorated(text = stringResource(MR.strings.migrate_from_another_device), icon = painterResource(MR.images.ic_download), - click = { - chatModel.migrationState.value = MigrationToState.PasteOrScanLink - ModalManager.fullscreen.showCustomModal { close -> MigrateToDeviceView(close) } }) - } - } - - Box( - Modifier - .fillMaxWidth() - .padding(bottom = DEFAULT_PADDING.times(1.5f), top = if (onboardingStage == null) DEFAULT_PADDING else 0.dp), contentAlignment = Alignment.Center - ) { - SimpleButtonDecorated(text = stringResource(MR.strings.how_it_works), icon = painterResource(MR.images.ic_info), - click = showModal { HowItWorks(user, onboardingStage) }) } } LaunchedEffect(Unit) { @@ -99,21 +105,23 @@ fun SimpleXLogo() { Image( painter = painterResource(if (isInDarkTheme()) MR.images.logo_light else MR.images.logo), contentDescription = stringResource(MR.strings.image_descr_simplex_logo), + contentScale = ContentScale.FillWidth, modifier = Modifier .padding(vertical = DEFAULT_PADDING) - .fillMaxWidth(0.60f) + .fillMaxWidth() ) } @Composable -private fun InfoRow(icon: Painter, titleId: StringResource, textId: StringResource, width: Dp = 76.dp) { +private fun InfoRow(icon: Painter, titleId: StringResource, textId: StringResource, width: Dp = 58.dp) { Row(Modifier.padding(bottom = 27.dp), verticalAlignment = Alignment.Top) { + Spacer(Modifier.width((4.dp + 58.dp - width) / 2)) Image(icon, contentDescription = null, modifier = Modifier - .width(width) - .padding(top = 8.dp, start = 8.dp, end = 24.dp)) - Column { + .width(width)) + Spacer(Modifier.width((4.dp + 58.dp - width) / 2 + DEFAULT_PADDING_HALF + 7.dp)) + Column(Modifier.padding(top = 4.dp), verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF)) { Text(stringResource(titleId), fontWeight = FontWeight.Bold, style = MaterialTheme.typography.h3, lineHeight = 24.sp) - Text(stringResource(textId), lineHeight = 24.sp, style = MaterialTheme.typography.body1) + Text(stringResource(textId), lineHeight = 24.sp, style = MaterialTheme.typography.body1, color = MaterialTheme.colors.secondary) } } } @@ -123,38 +131,53 @@ expect fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference @Composable fun OnboardingActionButton( + modifier: Modifier = Modifier, labelId: StringResource, onboarding: OnboardingStage?, - border: Boolean, + enabled: Boolean = true, icon: Painter? = null, - iconColor: Color = MaterialTheme.colors.primary, + iconColor: Color = Color.White, onclick: (() -> Unit)? ) { - val modifier = if (border) { - Modifier - .border(border = BorderStroke(1.dp, MaterialTheme.colors.primary), shape = RoundedCornerShape(50)) - .padding( - horizontal = if (icon == null) DEFAULT_PADDING * 2 else DEFAULT_PADDING_HALF, - vertical = 4.dp - ) - } else { - Modifier - } - - SimpleButtonFrame(click = { - onclick?.invoke() - if (onboarding != null) { - ChatController.appPrefs.onboardingStage.set(onboarding) - } - }, modifier) { + Button( + onClick = { + onclick?.invoke() + if (onboarding != null) { + appPrefs.onboardingStage.set(onboarding) + } + }, + modifier = modifier, + shape = CircleShape, + enabled = enabled, +// elevation = ButtonDefaults.elevation(defaultElevation = 0.dp, focusedElevation = 0.dp, pressedElevation = 0.dp, hoveredElevation = 0.dp), + contentPadding = PaddingValues(horizontal = if (icon == null) DEFAULT_PADDING * 2 else DEFAULT_PADDING * 1.5f, vertical = DEFAULT_PADDING), + colors = ButtonDefaults.buttonColors(MaterialTheme.colors.primary, disabledBackgroundColor = MaterialTheme.colors.secondary) + ) { if (icon != null) { Icon(icon, stringResource(labelId), Modifier.padding(end = DEFAULT_PADDING_HALF), tint = iconColor) } - Text(stringResource(labelId), style = MaterialTheme.typography.h2, color = MaterialTheme.colors.primary, fontSize = 20.sp) - Icon( - painterResource(MR.images.ic_arrow_forward_ios), "next stage", tint = MaterialTheme.colors.primary, - modifier = Modifier.padding(start = DEFAULT_PADDING.div(4)).size(20.dp) - ) + Text(stringResource(labelId), style = MaterialTheme.typography.h2, color = Color.White, fontSize = 18.sp, fontWeight = FontWeight.Medium) + } +} + +@Composable +fun TextButtonBelowOnboardingButton(text: String, onClick: (() -> Unit)?) { + val state = getKeyboardState() + val topPadding by animateDpAsState(if (appPlatform.isAndroid && state.value == KeyboardState.Opened) 0.dp else DEFAULT_PADDING) + val bottomPadding by animateDpAsState(if (appPlatform.isAndroid && state.value == KeyboardState.Opened) 0.dp else DEFAULT_PADDING * 2) + if ((appPlatform.isAndroid && state.value == KeyboardState.Closed) || topPadding > 0.dp) { + TextButton({ onClick?.invoke() }, Modifier.padding(top = topPadding, bottom = bottomPadding).clip(CircleShape), enabled = onClick != null) { + Text( + text, + Modifier.padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING_HALF, bottom = 5.dp), + color = MaterialTheme.colors.primary, + fontWeight = FontWeight.Medium, + textAlign = TextAlign.Center + ) + } + } else { + // Hide from view when keyboard is open and move the view down + Spacer(Modifier.height(DEFAULT_PADDING * 2)) } } @@ -168,8 +191,7 @@ fun PreviewSimpleXInfo() { SimpleXTheme { SimpleXInfoLayout( user = null, - onboardingStage = null, - showModal = { {} } + onboardingStage = null ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt index df6c66deb4..be9acedd89 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/onboarding/WhatsNewView.kt @@ -18,7 +18,7 @@ import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.common.model.ChatModel -import chat.simplex.common.platform.ColumnWithScrollBar +import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.res.MR @@ -30,7 +30,7 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) { val currentVersion = remember { mutableStateOf(versionDescriptions.lastIndex) } @Composable - fun featureDescription(icon: Painter, titleId: StringResource, descrId: StringResource, link: String?) { + fun featureDescription(icon: ImageResource?, titleId: StringResource, descrId: StringResource?, link: String?, subfeatures: List>) { @Composable fun linkButton(link: String) { val uriHandler = LocalUriHandler.current @@ -47,7 +47,7 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) { horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(bottom = 4.dp) ) { - Icon(icon, stringResource(titleId), tint = MaterialTheme.colors.secondary) + if (icon != null) Icon(painterResource(icon), stringResource(titleId), tint = MaterialTheme.colors.secondary) Text( generalGetString(titleId), maxLines = 2, @@ -59,7 +59,17 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) { linkButton(link) } } - Text(generalGetString(descrId), fontSize = 15.sp) + if (descrId != null) Text(generalGetString(descrId), fontSize = 15.sp) + for ((si, sd) in subfeatures) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(bottom = 4.dp) + ) { + Icon(painterResource(si), stringResource(sd), tint = MaterialTheme.colors.secondary) + Text(generalGetString(sd), fontSize = 15.sp) + } + } } } @@ -115,16 +125,13 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) { AppBarTitle(String.format(generalGetString(MR.strings.new_in_version), v.version), bottomPadding = DEFAULT_PADDING) v.features.forEach { feature -> - featureDescription(painterResource(feature.icon), feature.titleId, feature.descrId, feature.link) + if (feature.show) { + featureDescription(feature.icon, feature.titleId, feature.descrId, feature.link, feature.subfeatures) + } } - val uriHandler = LocalUriHandler.current if (v.post != null) { - Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = DEFAULT_PADDING.div(4))) { - Text(stringResource(MR.strings.whats_new_read_more), color = MaterialTheme.colors.primary, - modifier = Modifier.clickable { uriHandler.openUriCatching(v.post) }) - Icon(painterResource(MR.images.ic_open_in_new), stringResource(MR.strings.whats_new_read_more), tint = MaterialTheme.colors.primary) - } + ReadMoreButton(v.post) } if (!viaSettings) { @@ -149,11 +156,23 @@ fun WhatsNewView(viaSettings: Boolean = false, close: () -> Unit) { } } +@Composable +fun ReadMoreButton(url: String) { + val uriHandler = LocalUriHandler.current + Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = DEFAULT_PADDING.div(4))) { + Text(stringResource(MR.strings.whats_new_read_more), color = MaterialTheme.colors.primary, + modifier = Modifier.clickable { uriHandler.openUriCatching(url) }) + Icon(painterResource(MR.images.ic_open_in_new), stringResource(MR.strings.whats_new_read_more), tint = MaterialTheme.colors.primary) + } +} + private data class FeatureDescription( - val icon: ImageResource, + val icon: ImageResource?, val titleId: StringResource, - val descrId: StringResource, - val link: String? = null + val descrId: StringResource?, + var subfeatures: List> = listOf(), + val link: String? = null, + val show: Boolean = true ) private data class VersionDescription( @@ -271,7 +290,6 @@ private val versionDescriptions: List = listOf( icon = MR.images.ic_translate, titleId = MR.strings.v4_5_italian_interface, descrId = MR.strings.v4_5_italian_interface_descr, - link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat" ) ) ), @@ -308,7 +326,6 @@ private val versionDescriptions: List = listOf( icon = MR.images.ic_translate, titleId = MR.strings.v4_6_chinese_spanish_interface, descrId = MR.strings.v4_6_chinese_spanish_interface_descr, - link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat" ) ) ), @@ -330,7 +347,6 @@ private val versionDescriptions: List = listOf( icon = MR.images.ic_translate, titleId = MR.strings.v5_0_polish_interface, descrId = MR.strings.v5_0_polish_interface_descr, - link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat" ) ) ), @@ -362,7 +378,6 @@ private val versionDescriptions: List = listOf( icon = MR.images.ic_translate, titleId = MR.strings.v5_1_japanese_portuguese_interface, descrId = MR.strings.whats_new_thanks_to_users_contribute_weblate, - link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat" ) ) ), @@ -427,7 +442,6 @@ private val versionDescriptions: List = listOf( icon = MR.images.ic_translate, titleId = MR.strings.v5_3_new_interface_languages, descrId = MR.strings.v5_3_new_interface_languages_descr, - link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat" ) ) ), @@ -491,7 +505,6 @@ private val versionDescriptions: List = listOf( icon = MR.images.ic_translate, titleId = MR.strings.v5_5_new_interface_languages, descrId = MR.strings.whats_new_thanks_to_users_contribute_weblate, - link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat" ) ) ), @@ -554,10 +567,88 @@ private val versionDescriptions: List = listOf( icon = MR.images.ic_translate, titleId = MR.strings.v5_7_new_interface_languages, descrId = MR.strings.whats_new_thanks_to_users_contribute_weblate, - link = "https://github.com/simplex-chat/simplex-chat/tree/stable#help-translating-simplex-chat" ) ) ), + VersionDescription( + version = "v5.8", + post = "https://simplex.chat/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.html", + features = listOf( + FeatureDescription( + icon = MR.images.ic_settings_ethernet, + titleId = MR.strings.v5_8_private_routing, + descrId = MR.strings.v5_8_private_routing_descr + ), + FeatureDescription( + icon = MR.images.ic_palette, + titleId = MR.strings.v5_8_chat_themes, + descrId = MR.strings.v5_8_chat_themes_descr + ), + FeatureDescription( + icon = MR.images.ic_security, + titleId = MR.strings.v5_8_safe_files, + descrId = MR.strings.v5_8_safe_files_descr + ), + FeatureDescription( + icon = MR.images.ic_battery_3_bar, + titleId = MR.strings.v5_8_message_delivery, + descrId = MR.strings.v5_8_message_delivery_descr + ), + FeatureDescription( + icon = MR.images.ic_translate, + titleId = MR.strings.v5_8_persian_ui, + descrId = MR.strings.whats_new_thanks_to_users_contribute_weblate + ) + ) + ), + VersionDescription( + version = "v6.0", + post = "https://simplex.chat/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.html", + features = listOf( + FeatureDescription( + icon = null, + titleId = MR.strings.v6_0_new_chat_experience, + descrId = null, + subfeatures = listOf( + MR.images.ic_add_link to MR.strings.v6_0_connect_faster_descr, + MR.images.ic_inventory_2 to MR.strings.v6_0_your_contacts_descr, + MR.images.ic_delete to MR.strings.v6_0_delete_many_messages_descr, + MR.images.ic_match_case to MR.strings.v6_0_increase_font_size + ) + ), + FeatureDescription( + icon = null, + titleId = MR.strings.v6_0_new_media_options, + descrId = null, + subfeatures = listOf( + MR.images.ic_play_arrow_filled to MR.strings.v6_0_chat_list_media, + MR.images.ic_blur_on to MR.strings.v6_0_privacy_blur, + ) + ), + FeatureDescription( + icon = MR.images.ic_toast, + titleId = MR.strings.v6_0_reachable_chat_toolbar, + descrId = MR.strings.v6_0_reachable_chat_toolbar_descr, + show = appPlatform.isAndroid + ), + FeatureDescription( + icon = MR.images.ic_settings_ethernet, + titleId = MR.strings.v5_8_private_routing, + descrId = MR.strings.v6_0_private_routing_descr + ), + FeatureDescription( + icon = MR.images.ic_wifi_tethering, + titleId = MR.strings.v6_0_connection_servers_status, + descrId = MR.strings.v6_0_connection_servers_status_descr + ), + FeatureDescription( + icon = MR.images.ic_upgrade, + titleId = MR.strings.v6_0_upgrade_app, + descrId = MR.strings.v6_0_upgrade_app_descr, + show = appPlatform.isDesktop + ), + ), + ) ) private val lastVersion = versionDescriptions.last().version diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt index 76f522c614..b5349e826d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectDesktopView.kt @@ -15,6 +15,7 @@ import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager @@ -166,6 +167,24 @@ private fun ConnectingDesktop(session: RemoteCtrlSession, rc: RemoteCtrlInfo?) { SectionView { DisconnectButton(onClick = ::disconnectDesktop) } + + ProgressIndicator() +} + +@Composable +private fun ProgressIndicator() { + Box( + Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + Modifier + .padding(horizontal = 2.dp) + .size(30.dp), + color = MaterialTheme.colors.secondary, + strokeWidth = 3.dp + ) + } } @Composable @@ -413,14 +432,14 @@ private fun LinkedDesktopsView(remoteCtrls: SnapshotStateList) { SectionDividerSpaced() SectionView(stringResource(MR.strings.linked_desktop_options).uppercase()) { - PreferenceToggle(stringResource(MR.strings.verify_connections), remember { controller.appPrefs.confirmRemoteSessions.state }.value) { + PreferenceToggle(stringResource(MR.strings.verify_connections), checked = remember { controller.appPrefs.confirmRemoteSessions.state }.value) { controller.appPrefs.confirmRemoteSessions.set(it) } - PreferenceToggle(stringResource(MR.strings.discover_on_network), remember { controller.appPrefs.connectRemoteViaMulticast.state }.value) { + PreferenceToggle(stringResource(MR.strings.discover_on_network), checked = remember { controller.appPrefs.connectRemoteViaMulticast.state }.value) { controller.appPrefs.connectRemoteViaMulticast.set(it) } if (remember { controller.appPrefs.connectRemoteViaMulticast.state }.value) { - PreferenceToggle(stringResource(MR.strings.multicast_connect_automatically), remember { controller.appPrefs.connectRemoteViaMulticastAuto.state }.value) { + PreferenceToggle(stringResource(MR.strings.multicast_connect_automatically), checked = remember { controller.appPrefs.connectRemoteViaMulticastAuto.state }.value) { controller.appPrefs.connectRemoteViaMulticastAuto.set(it) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt index e13b86258d..92503f273e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/remote/ConnectMobileView.kt @@ -89,18 +89,15 @@ fun ConnectMobileLayout( connectDesktop: () -> Unit, deleteHost: (RemoteHostInfo) -> Unit, ) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { + ColumnWithScrollBar(Modifier.fillMaxWidth()) { AppBarTitle(stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles)) SectionView(generalGetString(MR.strings.this_device_name).uppercase()) { DeviceNameField(deviceName.value ?: "") { updateDeviceName(it) } SectionTextFooter(generalGetString(MR.strings.this_device_name_shared_with_mobile)) - PreferenceToggle(stringResource(MR.strings.multicast_discoverable_via_local_network), remember { controller.appPrefs.offerRemoteMulticast.state }.value) { + PreferenceToggle(stringResource(MR.strings.multicast_discoverable_via_local_network), checked = remember { controller.appPrefs.offerRemoteMulticast.state }.value) { controller.appPrefs.offerRemoteMulticast.set(it) } - SectionDividerSpaced(maxBottomPadding = false) + SectionDividerSpaced() } SectionView(stringResource(MR.strings.devices).uppercase()) { if (chatModel.localUserCreated.value == true) { @@ -179,10 +176,7 @@ private fun ConnectMobileViewLayout( refreshQrCode: () -> Unit = {}, UnderQrLayout: @Composable () -> Unit = {}, ) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { + ColumnWithScrollBar(Modifier.fillMaxWidth()) { if (title != null) { AppBarTitle(title) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt index 9b6e3a0937..3c8ab2b70a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt @@ -2,39 +2,54 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionCustomFooter +import SectionDividerSpaced import SectionItemView +import SectionItemWithValue import SectionView +import SectionViewSelectableCards import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalDensity import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* -import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.platform.ColumnWithScrollBar +import chat.simplex.common.platform.chatModel import chat.simplex.res.MR import java.text.DecimalFormat @Composable -fun AdvancedNetworkSettingsView(chatModel: ChatModel) { - val currentCfg = remember { mutableStateOf(chatModel.controller.getNetCfg()) } +fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> Unit, close: () -> Unit) { + val currentRemoteHost by remember { chatModel.currentRemoteHost } + val developerTools = remember { appPrefs.developerTools.get() } + + // Will be actual once the screen is re-opened + val savedCfg = remember { mutableStateOf(controller.getNetCfg()) } + // Will have an edited state when the screen is re-opened + val currentCfg = remember { stateGetOrPut("currentCfg") { controller.getNetCfg() } } val currentCfgVal = currentCfg.value // used only on initialization + + val onionHosts = remember { mutableStateOf(currentCfgVal.onionHosts) } + val sessionMode = remember { mutableStateOf(currentCfgVal.sessionMode) } + val smpProxyMode = remember { mutableStateOf(currentCfgVal.smpProxyMode) } + val smpProxyFallback = remember { mutableStateOf(currentCfgVal.smpProxyFallback) } + + val networkUseSocksProxy: MutableState = remember { mutableStateOf(currentCfgVal.useSocksProxy) } val networkTCPConnectTimeout = remember { mutableStateOf(currentCfgVal.tcpConnectTimeout) } val networkTCPTimeout = remember { mutableStateOf(currentCfgVal.tcpTimeout) } val networkTCPTimeoutPerKb = remember { mutableStateOf(currentCfgVal.tcpTimeoutPerKb) } - var networkRcvConcurrency = remember { mutableStateOf(currentCfgVal.rcvConcurrency) } + val networkRcvConcurrency = remember { mutableStateOf(currentCfgVal.rcvConcurrency) } val networkSMPPingInterval = remember { mutableStateOf(currentCfgVal.smpPingInterval) } val networkSMPPingCount = remember { mutableStateOf(currentCfgVal.smpPingCount) } val networkEnableKeepAlive = remember { mutableStateOf(currentCfgVal.enableKeepAlive) } @@ -63,9 +78,11 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) { } return NetCfg( socksProxy = currentCfg.value.socksProxy, - hostMode = currentCfg.value.hostMode, - requiredHostMode = currentCfg.value.requiredHostMode, - sessionMode = currentCfg.value.sessionMode, +// hostMode = currentCfg.value.hostMode, +// requiredHostMode = currentCfg.value.requiredHostMode, + sessionMode = sessionMode.value, + smpProxyMode = smpProxyMode.value, + smpProxyFallback = smpProxyFallback.value, tcpConnectTimeout = networkTCPConnectTimeout.value, tcpTimeout = networkTCPTimeout.value, tcpTimeoutPerKb = networkTCPTimeoutPerKb.value, @@ -73,10 +90,14 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) { tcpKeepAlive = tcpKeepAlive, smpPingInterval = networkSMPPingInterval.value, smpPingCount = networkSMPPingCount.value - ) + ).withOnionHosts(onionHosts.value) } fun updateView(cfg: NetCfg) { + onionHosts.value = cfg.onionHosts + sessionMode.value = cfg.sessionMode + smpProxyMode.value = cfg.smpProxyMode + smpProxyFallback.value = cfg.smpProxyFallback networkTCPConnectTimeout.value = cfg.tcpConnectTimeout networkTCPTimeout.value = cfg.tcpTimeout networkTCPTimeoutPerKb.value = cfg.tcpTimeoutPerKb @@ -95,40 +116,80 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) { } } - fun saveCfg(cfg: NetCfg) { + fun saveCfg(cfg: NetCfg, close: (() -> Unit)? = null) { withBGApi { - chatModel.controller.apiSetNetworkConfig(cfg) - currentCfg.value = cfg - chatModel.controller.setNetCfg(cfg) + if (chatModel.controller.apiSetNetworkConfig(cfg)) { + currentCfg.value = cfg + savedCfg.value = cfg + chatModel.controller.setNetCfg(cfg) + close?.invoke() + } } } fun reset() { val newCfg = if (currentCfg.value.useSocksProxy) NetCfg.proxyDefaults else NetCfg.defaults updateView(newCfg) - saveCfg(newCfg) + currentCfg.value = newCfg } - AdvancedNetworkSettingsLayout( - networkTCPConnectTimeout, - networkTCPTimeout, - networkTCPTimeoutPerKb, - networkRcvConcurrency, - networkSMPPingInterval, - networkSMPPingCount, - networkEnableKeepAlive, - networkTCPKeepIdle, - networkTCPKeepIntvl, - networkTCPKeepCnt, - resetDisabled = if (currentCfg.value.useSocksProxy) currentCfg.value == NetCfg.proxyDefaults else currentCfg.value == NetCfg.defaults, - reset = { showUpdateNetworkSettingsDialog(::reset) }, - footerDisabled = buildCfg() == currentCfg.value, - revert = { updateView(currentCfg.value) }, - save = { showUpdateNetworkSettingsDialog { saveCfg(buildCfg()) } } - ) + val saveDisabled = buildCfg() == savedCfg.value + + ModalView( + close = { + if (saveDisabled) { + close() + } else { + showUnsavedChangesAlert({ + saveCfg(buildCfg(), close) + }, close) + } + }, + ) { + AdvancedNetworkSettingsLayout( + currentRemoteHost = currentRemoteHost, + networkUseSocksProxy = networkUseSocksProxy, + developerTools = developerTools, + onionHosts = onionHosts, + useOnion = { onionHosts.value = it; currentCfg.value = currentCfg.value.withOnionHosts(it) }, + sessionMode = sessionMode, + smpProxyMode = smpProxyMode, + smpProxyFallback = smpProxyFallback, + networkTCPConnectTimeout, + networkTCPTimeout, + networkTCPTimeoutPerKb, + networkRcvConcurrency, + networkSMPPingInterval, + networkSMPPingCount, + networkEnableKeepAlive, + networkTCPKeepIdle, + networkTCPKeepIntvl, + networkTCPKeepCnt, + updateSessionMode = { sessionMode.value = it; currentCfg.value = currentCfg.value.copy(sessionMode = it) }, + updateSMPProxyMode = { smpProxyMode.value = it; currentCfg.value = currentCfg.value.copy(smpProxyMode = it) }, + updateSMPProxyFallback = { smpProxyFallback.value = it; currentCfg.value = currentCfg.value.copy(smpProxyFallback = it) }, + showModal = showModal, + resetDisabled = if (currentCfg.value.useSocksProxy) buildCfg() == NetCfg.proxyDefaults else buildCfg() == NetCfg.defaults, + reset = ::reset, + saveDisabled = saveDisabled, + save = { + showUpdateNetworkSettingsDialog { + saveCfg(buildCfg()) + } + } + ) + } } @Composable fun AdvancedNetworkSettingsLayout( + currentRemoteHost: RemoteHostInfo?, + networkUseSocksProxy: State, + developerTools: Boolean, + onionHosts: MutableState, + useOnion: (OnionHosts) -> Unit, + sessionMode: MutableState, + smpProxyMode: MutableState, + smpProxyFallback: MutableState, networkTCPConnectTimeout: MutableState, networkTCPTimeout: MutableState, networkTCPTimeoutPerKb: MutableState, @@ -139,10 +200,13 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) { networkTCPKeepIdle: MutableState, networkTCPKeepIntvl: MutableState, networkTCPKeepCnt: MutableState, + updateSessionMode: (TransportSessionMode) -> Unit, + updateSMPProxyMode: (SMPProxyMode) -> Unit, + updateSMPProxyFallback: (SMPProxyFallback) -> Unit, + showModal: (ModalData.() -> Unit) -> Unit, resetDisabled: Boolean, reset: () -> Unit, - footerDisabled: Boolean, - revert: () -> Unit, + saveDisabled: Boolean, save: () -> Unit ) { val secondsLabel = stringResource(MR.strings.network_option_seconds_label) @@ -152,14 +216,43 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) { .fillMaxWidth(), ) { AppBarTitle(stringResource(MR.strings.network_settings_title)) - SectionView { - SectionItemView { - ResetToDefaultsButton(reset, disabled = resetDisabled) + + if (currentRemoteHost == null) { + SectionView(generalGetString(MR.strings.settings_section_title_private_message_routing)) { + SMPProxyModePicker(smpProxyMode, showModal, updateSMPProxyMode) + SMPProxyFallbackPicker(smpProxyFallback, showModal, updateSMPProxyFallback, enabled = remember { derivedStateOf { smpProxyMode.value != SMPProxyMode.Never } }) + SettingsPreferenceItem(painterResource(MR.images.ic_arrow_forward), stringResource(MR.strings.private_routing_show_message_status), chatModel.controller.appPrefs.showSentViaProxy) } + SectionCustomFooter { + Text(stringResource(MR.strings.private_routing_explanation)) + } + SectionDividerSpaced(maxTopPadding = true) + } + + if (currentRemoteHost == null && networkUseSocksProxy.value) { + SectionView(stringResource(MR.strings.network_socks_proxy).uppercase()) { + UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion) + SectionCustomFooter { + Column { + Text(annotatedStringResource(MR.strings.disable_onion_hosts_when_not_supported)) + } + } + } + SectionDividerSpaced(maxTopPadding = true) + } + + if (currentRemoteHost == null && developerTools) { + SectionView(stringResource(MR.strings.network_session_mode_transport_isolation).uppercase()) { + SessionModePicker(sessionMode, showModal, updateSessionMode) + } + SectionDividerSpaced() + } + + SectionView(stringResource(MR.strings.network_option_tcp_connection).uppercase()) { SectionItemView { TimeoutSettingRow( stringResource(MR.strings.network_option_tcp_connection_timeout), networkTCPConnectTimeout, - listOf(10_000000, 15_000000, 20_000000, 25_000000, 35_000000, 50_000000), secondsLabel + listOf(10_000000, 15_000000, 20_000000, 30_000000, 45_000000, 60_000000, 90_000000), secondsLabel ) } SectionItemView { @@ -175,12 +268,12 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) { listOf(2_500, 5_000, 10_000, 15_000, 20_000, 30_000), secondsLabel ) } - SectionItemView { - IntSettingRow( - stringResource(MR.strings.network_option_rcv_concurrency), networkRcvConcurrency, - listOf(1, 2, 4, 8, 12, 16, 24), "" - ) - } + // SectionItemView { + // IntSettingRow( + // stringResource(MR.strings.network_option_rcv_concurrency), networkRcvConcurrency, + // listOf(1, 2, 4, 8, 12, 16, 24), "" + // ) + // } SectionItemView { TimeoutSettingRow( stringResource(MR.strings.network_option_ping_interval), networkSMPPingInterval, @@ -218,23 +311,92 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) { } } } - SectionCustomFooter { - SettingsSectionFooter(revert, save, footerDisabled) + + SectionDividerSpaced(maxBottomPadding = false) + + SectionView { + SectionItemView(reset, disabled = resetDisabled) { + Text(stringResource(MR.strings.network_options_reset_to_defaults), color = if (resetDisabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) + } + SectionItemView(save, disabled = saveDisabled) { + Text(stringResource(MR.strings.network_options_save_and_reconnect), color = if (saveDisabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) + } } SectionBottomSpacer() } } @Composable -fun ResetToDefaultsButton(reset: () -> Unit, disabled: Boolean) { - val modifier = if (disabled) Modifier else Modifier.clickable { reset() } - Row( - modifier.fillMaxSize(), - verticalAlignment = Alignment.CenterVertically - ) { - val color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary - Text(stringResource(MR.strings.network_options_reset_to_defaults), color = color) +private fun SMPProxyModePicker( + smpProxyMode: MutableState, + showModal: (@Composable ModalData.() -> Unit) -> Unit, + updateSMPProxyMode: (SMPProxyMode) -> Unit, +) { + val density = LocalDensity.current + val values = remember { + SMPProxyMode.values().map { + when (it) { + SMPProxyMode.Always -> ValueTitleDesc(SMPProxyMode.Always, generalGetString(MR.strings.network_smp_proxy_mode_always), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_always_description), density)) + SMPProxyMode.Unknown -> ValueTitleDesc(SMPProxyMode.Unknown, generalGetString(MR.strings.network_smp_proxy_mode_unknown), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_unknown_description), density)) + SMPProxyMode.Unprotected -> ValueTitleDesc(SMPProxyMode.Unprotected, generalGetString(MR.strings.network_smp_proxy_mode_unprotected), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_unprotected_description), density)) + SMPProxyMode.Never -> ValueTitleDesc(SMPProxyMode.Never, generalGetString(MR.strings.network_smp_proxy_mode_never), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_mode_never_description), density)) + } + } } + + SectionItemWithValue( + generalGetString(MR.strings.network_smp_proxy_mode_private_routing), + smpProxyMode, + values, + icon = painterResource(MR.images.ic_settings_ethernet), + onSelected = { + showModal { + ColumnWithScrollBar( + Modifier.fillMaxWidth(), + ) { + AppBarTitle(stringResource(MR.strings.network_smp_proxy_mode_private_routing)) + SectionViewSelectableCards(null, smpProxyMode, values, updateSMPProxyMode) + } + } + } + ) +} + +@Composable +private fun SMPProxyFallbackPicker( + smpProxyFallback: MutableState, + showModal: (@Composable ModalData.() -> Unit) -> Unit, + updateSMPProxyFallback: (SMPProxyFallback) -> Unit, + enabled: State, +) { + val density = LocalDensity.current + val values = remember { + SMPProxyFallback.values().map { + when (it) { + SMPProxyFallback.Allow -> ValueTitleDesc(SMPProxyFallback.Allow, generalGetString(MR.strings.network_smp_proxy_fallback_allow), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_fallback_allow_description), density)) + SMPProxyFallback.AllowProtected -> ValueTitleDesc(SMPProxyFallback.AllowProtected, generalGetString(MR.strings.network_smp_proxy_fallback_allow_protected), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_fallback_allow_protected_description), density)) + SMPProxyFallback.Prohibit -> ValueTitleDesc(SMPProxyFallback.Prohibit, generalGetString(MR.strings.network_smp_proxy_fallback_prohibit), escapedHtmlToAnnotatedString(generalGetString(MR.strings.network_smp_proxy_fallback_prohibit_description), density)) + } + } + } + + SectionItemWithValue( + generalGetString(MR.strings.network_smp_proxy_fallback_allow_downgrade), + smpProxyFallback, + values, + icon = painterResource(MR.images.ic_arrows_left_right), + enabled = enabled, + onSelected = { + showModal { + ColumnWithScrollBar( + Modifier.fillMaxWidth(), + ) { + AppBarTitle(stringResource(MR.strings.network_smp_proxy_fallback_allow_downgrade)) + SectionViewSelectableCards(null, smpProxyFallback, values, updateSMPProxyFallback) + } + } + } + ) } @Composable @@ -284,7 +446,7 @@ fun IntSettingRow(title: String, selection: MutableState, values: List ) Spacer(Modifier.size(4.dp)) Icon( - if (!expanded.value) painterResource(MR.images.ic_expand_more) else painterResource(MR.images.ic_expand_less), + if (!expanded.value) painterResource(MR.images.ic_arrow_drop_down) else painterResource(MR.images.ic_arrow_drop_up), generalGetString(MR.strings.invite_to_group_button), modifier = Modifier.padding(start = 8.dp), tint = MaterialTheme.colors.secondary @@ -344,7 +506,7 @@ fun TimeoutSettingRow(title: String, selection: MutableState, values: List ) Spacer(Modifier.size(4.dp)) Icon( - if (!expanded.value) painterResource(MR.images.ic_expand_more) else painterResource(MR.images.ic_expand_less), + if (!expanded.value) painterResource(MR.images.ic_arrow_drop_down) else painterResource(MR.images.ic_arrow_drop_up), generalGetString(MR.strings.invite_to_group_button), modifier = Modifier.padding(start = 8.dp), tint = MaterialTheme.colors.secondary @@ -375,43 +537,6 @@ fun TimeoutSettingRow(title: String, selection: MutableState, values: List } } -@Composable -fun SettingsSectionFooter(revert: () -> Unit, save: () -> Unit, disabled: Boolean) { - Row( - Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - FooterButton(painterResource(MR.images.ic_replay), stringResource(MR.strings.network_options_revert), revert, disabled) - FooterButton(painterResource(MR.images.ic_check), stringResource(MR.strings.network_options_save), save, disabled) - } -} - -@Composable -fun FooterButton(icon: Painter, title: String, action: () -> Unit, disabled: Boolean) { - Surface( - shape = RoundedCornerShape(20.dp), - color = Color.Black.copy(alpha = 0f), - contentColor = LocalContentColor.current - ) { - val modifier = if (disabled) Modifier else Modifier.clickable { action() } - Row( - modifier.padding(8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - icon, - title, - tint = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary - ) - Text( - title, - color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary - ) - } - } -} - fun showUpdateNetworkSettingsDialog(action: () -> Unit) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.update_network_settings_question), @@ -421,11 +546,27 @@ fun showUpdateNetworkSettingsDialog(action: () -> Unit) { ) } +private fun showUnsavedChangesAlert(save: () -> Unit, revert: () -> Unit) { + AlertManager.shared.showAlertDialogStacked( + title = generalGetString(MR.strings.update_network_settings_question), + confirmText = generalGetString(MR.strings.network_options_save_and_reconnect), + dismissText = generalGetString(MR.strings.exit_without_saving), + onConfirm = save, + onDismiss = revert, + ) +} + @Preview @Composable fun PreviewAdvancedNetworkSettingsLayout() { SimpleXTheme { AdvancedNetworkSettingsLayout( + currentRemoteHost = null, + networkUseSocksProxy = remember { mutableStateOf(false) }, + developerTools = false, + sessionMode = remember { mutableStateOf(TransportSessionMode.User) }, + smpProxyMode = remember { mutableStateOf(SMPProxyMode.Never) }, + smpProxyFallback = remember { mutableStateOf(SMPProxyFallback.Allow) }, networkTCPConnectTimeout = remember { mutableStateOf(10_000000) }, networkTCPTimeout = remember { mutableStateOf(10_000000) }, networkTCPTimeoutPerKb = remember { mutableStateOf(10_000) }, @@ -436,10 +577,15 @@ fun PreviewAdvancedNetworkSettingsLayout() { networkTCPKeepIdle = remember { mutableStateOf(10) }, networkTCPKeepIntvl = remember { mutableStateOf(10) }, networkTCPKeepCnt = remember { mutableStateOf(10) }, + onionHosts = remember { mutableStateOf(OnionHosts.PREFER) }, + useOnion = {}, + updateSessionMode = {}, + updateSMPProxyMode = {}, + updateSMPProxyFallback = {}, + showModal = {}, resetDisabled = false, reset = {}, - footerDisabled = false, - revert = {}, + saveDisabled = false, save = {} ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt index 78e24e5e7e..d8993307d2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Appearance.kt @@ -1,37 +1,54 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer +import SectionDividerSpaced import SectionItemView import SectionItemViewSpaceBetween import SectionSpacer import SectionView -import androidx.compose.foundation.Image +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.grid.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.material.MaterialTheme.colors import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.* import androidx.compose.ui.graphics.* -import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource -import androidx.compose.ui.unit.dp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.ui.theme.* import chat.simplex.common.views.helpers.* import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.ThemeManager.colorFromReadableHex +import chat.simplex.common.ui.theme.ThemeManager.toReadableHex +import chat.simplex.common.views.chat.item.PreviewChatItemView import chat.simplex.res.MR -import com.godaddy.android.colorpicker.* +import com.godaddy.android.colorpicker.ClassicColorPicker +import com.godaddy.android.colorpicker.HsvColor +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.datetime.Clock import kotlinx.serialization.encodeToString +import java.io.File import java.net.URI import java.util.* import kotlin.collections.ArrayList +import kotlin.math.* @Composable -expect fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) +expect fun AppearanceView(m: ChatModel) object AppearanceScope { @Composable @@ -55,6 +72,7 @@ object AppearanceScope { onValueChange = { val diff = it % 2.5f appPreferences.profileImageCornerRadius.set(it + (if (diff >= 1.25f) -diff + 2.5f else -diff)) + saveThemeToDatabase(null) }, colors = SliderDefaults.colors( activeTickColor = Color.Transparent, @@ -66,90 +84,491 @@ object AppearanceScope { } @Composable - fun ThemesSection( - systemDarkTheme: SharedPreference, - showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - editColor: (ThemeColor, Color) -> Unit - ) { - val currentTheme by CurrentColors.collectAsState() - SectionView(stringResource(MR.strings.settings_section_title_themes)) { - val darkTheme = isSystemInDarkTheme() - val state = remember { derivedStateOf { currentTheme.name } } - ThemeSelector(state) { - ThemeManager.applyTheme(it, darkTheme) - } - if (state.value == DefaultTheme.SYSTEM.name) { - DarkThemeSelector(remember { systemDarkTheme.state }) { - ThemeManager.changeDarkTheme(it, darkTheme) + fun FontScaleSection() { + val localFontScale = remember { mutableStateOf(appPrefs.fontScale.get()) } + SectionView(stringResource(MR.strings.appearance_font_size).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Row(Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically) { + Box(Modifier.size(60.dp) + .background(MaterialTheme.colors.surface, RoundedCornerShape(percent = 22)) + .clip(RoundedCornerShape(percent = 22)) + .clickable { + localFontScale.value = 1f + appPrefs.fontScale.set(localFontScale.value) + }, + contentAlignment = Alignment.Center) { + CompositionLocalProvider( + LocalDensity provides Density(LocalDensity.current.density, localFontScale.value) + ) { + Text("Aa", color = if (localFontScale.value == 1f) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground) + } + } + Spacer(Modifier.width(10.dp)) + // Text("${(localFontScale.value * 100).roundToInt()}%", Modifier.width(70.dp), textAlign = TextAlign.Center, fontSize = 12.sp) + if (appPlatform.isAndroid) { + Slider( + localFontScale.value, + valueRange = 0.75f..1.25f, + steps = 11, + onValueChange = { + val diff = it % 0.05f + localFontScale.value = String.format(Locale.US, "%.2f", it + (if (diff >= 0.025f) -diff + 0.05f else -diff)).toFloatOrNull() ?: 1f + }, + onValueChangeFinished = { + appPrefs.fontScale.set(localFontScale.value) + }, + colors = SliderDefaults.colors( + activeTickColor = Color.Transparent, + inactiveTickColor = Color.Transparent, + ) + ) + } else { + Slider( + localFontScale.value, + valueRange = 0.7f..1.5f, + steps = 9, + onValueChange = { + val diff = it % 0.1f + localFontScale.value = String.format(Locale.US, "%.1f", it + (if (diff >= 0.05f) -diff + 0.1f else -diff)).toFloatOrNull() ?: 1f + }, + onValueChangeFinished = { + appPrefs.fontScale.set(localFontScale.value) + }, + colors = SliderDefaults.colors( + activeTickColor = Color.Transparent, + inactiveTickColor = Color.Transparent, + ) + ) } } } - SectionItemView(showSettingsModal { _ -> CustomizeThemeView(editColor) }) { Text(stringResource(MR.strings.customize_theme_title)) } } @Composable - fun CustomizeThemeView(editColor: (ThemeColor, Color) -> Unit) { + fun ChatThemePreview( + theme: DefaultTheme, + wallpaperImage: ImageBitmap?, + wallpaperType: WallpaperType?, + backgroundColor: Color? = MaterialTheme.wallpaper.background, + tintColor: Color? = MaterialTheme.wallpaper.tint, + withMessages: Boolean = true + ) { + val themeBackgroundColor = MaterialTheme.colors.background + val backgroundColor = backgroundColor ?: wallpaperType?.defaultBackgroundColor(theme, MaterialTheme.colors.background) + val tintColor = tintColor ?: wallpaperType?.defaultTintColor(theme) + Column(Modifier + .drawWithCache { + if (wallpaperImage != null && wallpaperType != null && backgroundColor != null && tintColor != null) { + chatViewBackground(wallpaperImage, wallpaperType, backgroundColor, tintColor) + } else { + onDrawBehind { + drawRect(themeBackgroundColor) + } + } + } + .padding(DEFAULT_PADDING_HALF) + ) { + if (withMessages) { + val alice = remember { ChatItem.getSampleData(1, CIDirection.DirectRcv(), Clock.System.now(), generalGetString(MR.strings.wallpaper_preview_hello_bob)) } + PreviewChatItemView(alice) + PreviewChatItemView( + ChatItem.getSampleData(2, CIDirection.DirectSnd(), Clock.System.now(), stringResource(MR.strings.wallpaper_preview_hello_alice), + quotedItem = CIQuote(alice.chatDir, alice.id, sentAt = alice.meta.itemTs, formattedText = alice.formattedText, content = MsgContent.MCText(alice.content.text)) + ) + ) + } else { + Box(Modifier.fillMaxSize()) + } + } + } + + @Composable + fun WallpaperPresetSelector( + selectedWallpaper: WallpaperType?, + baseTheme: DefaultTheme, + activeBackgroundColor: Color? = null, + activeTintColor: Color? = null, + currentColors: (WallpaperType?) -> ThemeManager.ActiveTheme, + onChooseType: (WallpaperType?) -> Unit, + ) { + val cornerRadius = 22 + + @Composable + fun Plus(tint: Color = MaterialTheme.colors.primary) { + Icon(painterResource(MR.images.ic_add), null, Modifier.size(25.dp), tint = tint) + } + + val backgrounds = PresetWallpaper.entries.toList() + + fun LazyGridScope.gridContent(width: Dp, height: Dp) { + @Composable + fun BackgroundItem(background: PresetWallpaper?) { + val checked = (background == null && (selectedWallpaper == null || selectedWallpaper == WallpaperType.Empty)) || selectedWallpaper?.samePreset(background) == true + Box( + Modifier + .size(width, height) + .clip(RoundedCornerShape(percent = cornerRadius)) + .border(1.dp, if (checked) MaterialTheme.colors.primary.copy(0.8f) else MaterialTheme.colors.onBackground.copy(if (isInDarkTheme()) 0.2f else 0.1f), RoundedCornerShape(percent = cornerRadius)) + .clickable { onChooseType(background?.toType(baseTheme)) }, + contentAlignment = Alignment.Center + ) { + if (background != null) { + val type = background.toType(baseTheme, if (checked) selectedWallpaper?.scale else null) + SimpleXThemeOverride(remember(background, selectedWallpaper, CurrentColors.collectAsState().value) { currentColors(type) }) { + ChatThemePreview( + baseTheme, + type.image, + type, + withMessages = false, + backgroundColor = if (checked) activeBackgroundColor ?: MaterialTheme.wallpaper.background else MaterialTheme.wallpaper.background, + tintColor = if (checked) activeTintColor ?: MaterialTheme.wallpaper.tint else MaterialTheme.wallpaper.tint + ) + } + } + } + } + + @Composable + fun OwnBackgroundItem(type: WallpaperType?) { + val overrides = remember(type, baseTheme, CurrentColors.collectAsState().value.wallpaper) { + currentColors(WallpaperType.Image("", null, null)) + } + val appWallpaper = overrides.wallpaper + val backgroundColor = appWallpaper.background + val tintColor = appWallpaper.tint + val wallpaperImage = appWallpaper.type.image + val checked = type is WallpaperType.Image && wallpaperImage != null + val remoteHostConnected = chatModel.remoteHostId != null + Box( + Modifier + .size(width, height) + .clip(RoundedCornerShape(percent = cornerRadius)) + .border(1.dp, if (type is WallpaperType.Image) MaterialTheme.colors.primary.copy(0.8f) else MaterialTheme.colors.onBackground.copy(0.1f), RoundedCornerShape(percent = cornerRadius)) + .clickable { onChooseType(WallpaperType.Image("", null, null)) }, + contentAlignment = Alignment.Center + ) { + + if (checked || wallpaperImage != null) { + ChatThemePreview( + baseTheme, + wallpaperImage, + if (checked) type else appWallpaper.type, + backgroundColor = if (checked) activeBackgroundColor ?: backgroundColor else backgroundColor, + tintColor = if (checked) activeTintColor ?: tintColor else tintColor, + withMessages = false + ) + } else if (remoteHostConnected) { + Plus(MaterialTheme.colors.error) + } else { + Plus() + } + } + } + + item { + BackgroundItem(null) + } + items(items = backgrounds) { background -> + BackgroundItem(background) + } + item { + OwnBackgroundItem(selectedWallpaper) + } + } + + SimpleXThemeOverride(remember(selectedWallpaper, CurrentColors.collectAsState().value) { currentColors(selectedWallpaper) }) { + ChatThemePreview( + baseTheme, + MaterialTheme.wallpaper.type.image, + selectedWallpaper, + backgroundColor = activeBackgroundColor ?: MaterialTheme.wallpaper.background, + tintColor = activeTintColor ?: MaterialTheme.wallpaper.tint, + ) + } + + if (appPlatform.isDesktop) { + val itemWidth = (DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier - DEFAULT_PADDING * 2 - DEFAULT_PADDING_HALF * 3) / 4 + val itemHeight = (DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier - DEFAULT_PADDING * 2) / 4 + val rows = ceil((PresetWallpaper.entries.size + 2) / 4f).roundToInt() + LazyVerticalGrid( + columns = GridCells.Fixed(4), + Modifier.height(itemHeight * rows + DEFAULT_PADDING_HALF * (rows - 1) + DEFAULT_PADDING * 2), + contentPadding = PaddingValues(DEFAULT_PADDING), + verticalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF), + horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF), + ) { + gridContent(itemWidth, itemHeight) + } + } else { + LazyHorizontalGrid( + rows = GridCells.Fixed(1), + Modifier.height(80.dp + DEFAULT_PADDING * 2), + contentPadding = PaddingValues(DEFAULT_PADDING), + horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING_HALF), + ) { + gridContent(80.dp, 80.dp) + } + } + } + + @Composable + fun ThemesSection(systemDarkTheme: SharedPreference) { + val currentTheme by CurrentColors.collectAsState() + val baseTheme = currentTheme.base + val wallpaperType = MaterialTheme.wallpaper.type + val themeUserDestination: MutableState?> = rememberSaveable(stateSaver = serializableSaver()) { + val currentUser = chatModel.currentUser.value + mutableStateOf( + if (currentUser?.uiThemes?.preferredMode(!currentTheme.colors.isLight) == null) null else currentUser.userId to currentUser.uiThemes + ) + } + val perUserTheme = remember(CurrentColors.collectAsState().value.base, chatModel.currentUser.value) { + mutableStateOf( + chatModel.currentUser.value?.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) ?: ThemeModeOverride() + ) + } + + fun updateThemeUserDestination() { + var (userId, themes) = themeUserDestination.value ?: return + themes = if (perUserTheme.value.mode == DefaultThemeMode.LIGHT) { + (themes ?: ThemeModeOverrides()).copy(light = perUserTheme.value) + } else { + (themes ?: ThemeModeOverrides()).copy(dark = perUserTheme.value) + } + themeUserDestination.value = userId to themes + } + + val onTypeCopyFromSameTheme = { type: WallpaperType? -> + if (themeUserDestination.value == null) { + ThemeManager.saveAndApplyWallpaper(baseTheme, type) + } else { + val wallpaperFiles = setOf(perUserTheme.value.wallpaper?.imageFile) + ThemeManager.copyFromSameThemeOverrides(type, null, perUserTheme) + val wallpaperFilesToDelete = wallpaperFiles - perUserTheme.value.wallpaper?.imageFile + wallpaperFilesToDelete.forEach(::removeWallpaperFile) + updateThemeUserDestination() + } + saveThemeToDatabase(themeUserDestination.value) + true + } + + val onTypeChange = { type: WallpaperType? -> + if (themeUserDestination.value == null) { + ThemeManager.saveAndApplyWallpaper(baseTheme, type) + } else { + ThemeManager.applyWallpaper(type, perUserTheme) + updateThemeUserDestination() + } + saveThemeToDatabase(themeUserDestination.value) + } + + val onImport = { to: URI -> + val filename = saveWallpaperFile(to) + if (filename != null) { + if (themeUserDestination.value == null) { + removeWallpaperFile((currentTheme.wallpaper.type as? WallpaperType.Image)?.filename) + } else { + removeWallpaperFile((perUserTheme.value.type as? WallpaperType.Image)?.filename) + } + onTypeChange(WallpaperType.Image(filename, 1f, WallpaperScaleType.FILL)) + } + } + + val currentColors = { type: WallpaperType? -> + // If applying for : + // - all themes: no overrides needed + // - specific user: only user overrides for currently selected theme are needed, because they will NOT be copied when other wallpaper is selected + val perUserOverride = if (themeUserDestination.value == null) null else if (wallpaperType.sameType(type)) chatModel.currentUser.value?.uiThemes else null + ThemeManager.currentColors(type, null, perUserOverride, appPrefs.themeOverrides.get()) + } + + val onChooseType: (WallpaperType?, FileChooserLauncher) -> Unit = { type: WallpaperType?, importWallpaperLauncher: FileChooserLauncher -> + when { + // don't have image in parent or already selected wallpaper with custom image + type is WallpaperType.Image && + ((wallpaperType is WallpaperType.Image && themeUserDestination.value?.second != null && chatModel.remoteHostId() == null) || + currentColors(type).wallpaper.type.image == null || + (currentColors(type).wallpaper.type.image != null && CurrentColors.value.wallpaper.type is WallpaperType.Image && themeUserDestination.value == null)) -> + withLongRunningApi { importWallpaperLauncher.launch("image/*") } + type is WallpaperType.Image && themeUserDestination.value == null -> onTypeChange(currentColors(type).wallpaper.type) + type is WallpaperType.Image && chatModel.remoteHostId() != null -> { /* do nothing when remote host connected */ } + type is WallpaperType.Image -> onTypeCopyFromSameTheme(currentColors(type).wallpaper.type) + (themeUserDestination.value != null && themeUserDestination.value?.second?.preferredMode(!CurrentColors.value.colors.isLight)?.type != type) || CurrentColors.value.wallpaper.type != type -> onTypeCopyFromSameTheme(type) + else -> onTypeChange(type) + } + } + + SectionView(stringResource(MR.strings.settings_section_title_themes)) { + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + ThemeDestinationPicker(themeUserDestination) + Spacer(Modifier.height(DEFAULT_PADDING_HALF)) + + val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? -> + if (to != null) onImport(to) + } + + WallpaperPresetSelector( + selectedWallpaper = wallpaperType, + baseTheme = currentTheme.base, + currentColors = { type -> + currentColors(type) + }, + onChooseType = { onChooseType(it, importWallpaperLauncher) }, + ) + val type = MaterialTheme.wallpaper.type + if (type is WallpaperType.Image && (themeUserDestination.value == null || perUserTheme.value.wallpaper?.imageFile != null)) { + SectionItemView(disabled = chatModel.remoteHostId != null && themeUserDestination.value != null, click = { + if (themeUserDestination.value == null) { + val defaultActiveTheme = ThemeManager.defaultActiveTheme(appPrefs.themeOverrides.get()) + ThemeManager.saveAndApplyWallpaper(baseTheme, null) + ThemeManager.removeTheme(defaultActiveTheme?.themeId) + removeWallpaperFile(type.filename) + } else { + removeUserThemeModeOverrides(themeUserDestination, perUserTheme) + } + saveThemeToDatabase(themeUserDestination.value) + }) { + Text( + stringResource(MR.strings.theme_remove_image), + color = if (chatModel.remoteHostId != null && themeUserDestination.value != null) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + ) + } + SectionSpacer() + } + + val state: State = remember(appPrefs.currentTheme.get()) { + derivedStateOf { + if (appPrefs.currentTheme.get() == DefaultTheme.SYSTEM_THEME_NAME) null else currentTheme.base.mode + } + } + ColorModeSelector(state) { + val newTheme = when (it) { + null -> DefaultTheme.SYSTEM_THEME_NAME + DefaultThemeMode.LIGHT -> DefaultTheme.LIGHT.themeName + DefaultThemeMode.DARK -> appPrefs.systemDarkTheme.get()!! + } + ThemeManager.applyTheme(newTheme) + saveThemeToDatabase(null) + } + + // Doesn't work on desktop when specified like remember { systemDarkTheme.state }, this is workaround + val darkModeState: State = remember(systemDarkTheme.get()) { derivedStateOf { systemDarkTheme.get() } } + DarkModeThemeSelector(darkModeState) { + ThemeManager.changeDarkTheme(it) + if (appPrefs.currentTheme.get() == DefaultTheme.SYSTEM_THEME_NAME) { + ThemeManager.applyTheme(appPrefs.currentTheme.get()!!) + } else if (appPrefs.currentTheme.get() != DefaultTheme.LIGHT.themeName) { + ThemeManager.applyTheme(appPrefs.systemDarkTheme.get()!!) + } + saveThemeToDatabase(null) + } + } + SectionItemView(click = { + val user = themeUserDestination.value + if (user == null) { + ModalManager.start.showModal { + val importWallpaperLauncher = rememberFileChooserLauncher(true) { to: URI? -> + if (to != null) onImport(to) + } + CustomizeThemeView { onChooseType(it, importWallpaperLauncher) } + } + } else { + ModalManager.start.showModalCloseable { close -> + UserWallpaperEditorModal(chatModel.remoteHostId(), user.first, close) + } + } + }) { + Text(stringResource(MR.strings.customize_theme_title)) + } + } + + @Composable + fun CustomizeThemeView(onChooseType: (WallpaperType?) -> Unit) { ColumnWithScrollBar( Modifier.fillMaxWidth(), ) { val currentTheme by CurrentColors.collectAsState() AppBarTitle(stringResource(MR.strings.customize_theme_title)) + val wallpaperImage = MaterialTheme.wallpaper.type.image + val wallpaperType = MaterialTheme.wallpaper.type + val baseTheme = CurrentColors.collectAsState().value.base - SectionView(stringResource(MR.strings.theme_colors_section_title)) { - SectionItemViewSpaceBetween({ editColor(ThemeColor.PRIMARY, currentTheme.colors.primary) }) { - val title = generalGetString(MR.strings.color_primary) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.primary) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.PRIMARY_VARIANT, currentTheme.colors.primaryVariant) }) { - val title = generalGetString(MR.strings.color_primary_variant) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.primaryVariant) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.SECONDARY, currentTheme.colors.secondary) }) { - val title = generalGetString(MR.strings.color_secondary) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.secondary) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.SECONDARY_VARIANT, currentTheme.colors.secondaryVariant) }) { - val title = generalGetString(MR.strings.color_secondary_variant) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.secondaryVariant) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.BACKGROUND, currentTheme.colors.background) }) { - val title = generalGetString(MR.strings.color_background) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.background) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.SURFACE, currentTheme.colors.surface) }) { - val title = generalGetString(MR.strings.color_surface) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = colors.surface) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.TITLE, currentTheme.appColors.title) }) { - val title = generalGetString(MR.strings.color_title) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.appColors.title) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.SENT_MESSAGE, currentTheme.appColors.sentMessage) }) { - val title = generalGetString(MR.strings.color_sent_message) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.appColors.sentMessage) - } - SectionItemViewSpaceBetween({ editColor(ThemeColor.RECEIVED_MESSAGE, currentTheme.appColors.receivedMessage) }) { - val title = generalGetString(MR.strings.color_received_message) - Text(title) - Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.appColors.receivedMessage) - } + val editColor = { name: ThemeColor -> + editColor( + name, + wallpaperType, + wallpaperImage, + onColorChange = { color -> + ThemeManager.saveAndApplyThemeColor(baseTheme, name, color) + saveThemeToDatabase(null) + } + ) } - val isInDarkTheme = isInDarkTheme() - if (currentTheme.base.hasChangedAnyColor(currentTheme.colors, currentTheme.appColors)) { - SectionItemView({ ThemeManager.resetAllThemeColors(darkForSystemTheme = isInDarkTheme) }) { + + WallpaperPresetSelector( + selectedWallpaper = wallpaperType, + baseTheme = currentTheme.base, + currentColors = { type -> + ThemeManager.currentColors(type, null, null, appPrefs.themeOverrides.get()) + }, + onChooseType = onChooseType + ) + + val type = MaterialTheme.wallpaper.type + if (type is WallpaperType.Image) { + SectionItemView(disabled = chatModel.remoteHostId != null, click = { + val defaultActiveTheme = ThemeManager.defaultActiveTheme(appPrefs.themeOverrides.get()) + ThemeManager.saveAndApplyWallpaper(baseTheme, null) + ThemeManager.removeTheme(defaultActiveTheme?.themeId) + removeWallpaperFile(type.filename) + saveThemeToDatabase(null) + }) { + Text( + stringResource(MR.strings.theme_remove_image), + color = if (chatModel.remoteHostId == null) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + ) + } + SectionSpacer() + } + + SectionView(stringResource(MR.strings.settings_section_title_chat_colors).uppercase()) { + WallpaperSetupView( + wallpaperType, + baseTheme, + MaterialTheme.wallpaper, + MaterialTheme.appColors.sentMessage, + MaterialTheme.appColors.sentQuote, + MaterialTheme.appColors.receivedMessage, + MaterialTheme.appColors.receivedQuote, + editColor = { name -> + editColor(name) + }, + onTypeChange = { type -> + ThemeManager.saveAndApplyWallpaper(baseTheme, type) + saveThemeToDatabase(null) + }, + ) + } + SectionDividerSpaced() + + CustomizeThemeColorsSection(currentTheme) { name -> + editColor(name) + } + + SectionDividerSpaced(maxBottomPadding = false) + + val currentOverrides = remember(currentTheme) { ThemeManager.defaultActiveTheme(appPrefs.themeOverrides.get()) } + val canResetColors = currentTheme.base.hasChangedAnyColor(currentOverrides) + if (canResetColors) { + SectionItemView({ + ThemeManager.resetAllThemeColors() + saveThemeToDatabase(null) + }) { Text(generalGetString(MR.strings.reset_color), color = colors.primary) } + SectionSpacer() } - SectionSpacer() + SectionView { val theme = remember { mutableStateOf(null as String?) } val exportThemeLauncher = rememberFileChooserLauncher(false) { to: URI? -> @@ -161,9 +580,11 @@ object AppearanceScope { } } SectionItemView({ - val overrides = ThemeManager.currentThemeOverridesForExport(isInDarkTheme) - theme.value = yaml.encodeToString(overrides) - withLongRunningApi { exportThemeLauncher.launch("simplex.theme")} + val overrides = ThemeManager.currentThemeOverridesForExport(null, null/*chatModel.currentUser.value?.uiThemes*/) + val lines = yaml.encodeToString(overrides).lines() + // Removing theme id without using custom serializer or data class + theme.value = lines.subList(1, lines.size).joinToString("\n") + withLongRunningApi { exportThemeLauncher.launch("simplex.theme") } }) { Text(generalGetString(MR.strings.export_theme), color = colors.primary) } @@ -171,7 +592,8 @@ object AppearanceScope { if (to != null) { val theme = getThemeFromUri(to) if (theme != null) { - ThemeManager.saveAndApplyThemeOverrides(theme, isInDarkTheme) + ThemeManager.saveAndApplyThemeOverrides(theme) + saveThemeToDatabase(null) } } } @@ -184,49 +606,339 @@ object AppearanceScope { } } - @Composable - fun ColorEditor( - name: ThemeColor, - initialColor: Color, - close: () -> Unit, - ) { - Column( - Modifier - .fillMaxWidth() - ) { - AppBarTitle(name.text) - var currentColor by remember { mutableStateOf(initialColor) } - ColorPicker(initialColor) { - currentColor = it + private var updateBackendJob: Job = Job() + private fun saveThemeToDatabase(themeUserDestination: Pair?) { + val remoteHostId = chatModel.remoteHostId() + val oldThemes = chatModel.currentUser.value?.uiThemes + if (themeUserDestination != null) { + // Update before save to make it work seamless + chatModel.updateCurrentUserUiThemes(remoteHostId, themeUserDestination.second) + } + updateBackendJob.cancel() + updateBackendJob = withBGApi { + delay(300) + if (themeUserDestination == null) { + controller.apiSaveAppSettings(AppSettings.current.prepareForExport()) + } else if (!controller.apiSetUserUIThemes(remoteHostId, themeUserDestination.first, themeUserDestination.second)) { + // If failed to apply for some reason return the old themes + chatModel.updateCurrentUserUiThemes(remoteHostId, oldThemes) } + } + } - SectionSpacer() - val isInDarkTheme = isInDarkTheme() - TextButton( - onClick = { - ThemeManager.saveAndApplyThemeColor(name, currentColor, isInDarkTheme) - close() - }, - Modifier.align(Alignment.CenterHorizontally), - colors = ButtonDefaults.textButtonColors(contentColor = currentColor) - ) { - Text(generalGetString(MR.strings.save_color)) + fun editColor(name: ThemeColor, wallpaperType: WallpaperType, wallpaperImage: ImageBitmap?, onColorChange: (Color?) -> Unit) { + ModalManager.start.showModal { + val baseTheme = CurrentColors.collectAsState().value.base + val wallpaperBackgroundColor = MaterialTheme.wallpaper.background ?: wallpaperType.defaultBackgroundColor(baseTheme, MaterialTheme.colors.background) + val wallpaperTintColor = MaterialTheme.wallpaper.tint ?: wallpaperType.defaultTintColor(baseTheme) + val initialColor: Color = when (name) { + ThemeColor.WALLPAPER_BACKGROUND -> wallpaperBackgroundColor + ThemeColor.WALLPAPER_TINT -> wallpaperTintColor + ThemeColor.PRIMARY -> MaterialTheme.colors.primary + ThemeColor.PRIMARY_VARIANT -> MaterialTheme.colors.primaryVariant + ThemeColor.SECONDARY -> MaterialTheme.colors.secondary + ThemeColor.SECONDARY_VARIANT -> MaterialTheme.colors.secondaryVariant + ThemeColor.BACKGROUND -> MaterialTheme.colors.background + ThemeColor.SURFACE -> MaterialTheme.colors.surface + ThemeColor.TITLE -> MaterialTheme.appColors.title + ThemeColor.PRIMARY_VARIANT2 -> MaterialTheme.appColors.primaryVariant2 + ThemeColor.SENT_MESSAGE -> MaterialTheme.appColors.sentMessage + ThemeColor.SENT_QUOTE -> MaterialTheme.appColors.sentQuote + ThemeColor.RECEIVED_MESSAGE -> MaterialTheme.appColors.receivedMessage + ThemeColor.RECEIVED_QUOTE -> MaterialTheme.appColors.receivedQuote + } + ColorEditor(name, initialColor, baseTheme, MaterialTheme.wallpaper.type, wallpaperImage, currentColors = { CurrentColors.value }, + onColorChange = onColorChange + ) + } + } + + @Composable + fun ModalData.UserWallpaperEditorModal(remoteHostId: Long?, userId: Long, close: () -> Unit) { + val themes = remember(chatModel.currentUser.value) { mutableStateOf(chatModel.currentUser.value?.uiThemes ?: ThemeModeOverrides()) } + val globalThemeUsed = remember { stateGetOrPut("globalThemeUsed") { false } } + val initialTheme = remember(CurrentColors.collectAsState().value.base) { + val preferred = themes.value.preferredMode(!CurrentColors.value.colors.isLight) + globalThemeUsed.value = preferred == null + preferred ?: ThemeManager.defaultActiveTheme(chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) + } + UserWallpaperEditor( + initialTheme, + applyToMode = if (themes.value.light == themes.value.dark) null else initialTheme.mode, + globalThemeUsed = globalThemeUsed, + save = { applyToMode, newTheme -> + save(applyToMode, newTheme, themes.value, userId, remoteHostId) + }) + KeyChangeEffect(chatModel.currentUser.value?.userId, chatModel.remoteHostId) { + close() + } + } + + suspend fun save( + applyToMode: DefaultThemeMode?, + newTheme: ThemeModeOverride?, + themes: ThemeModeOverrides?, + userId: Long, + remoteHostId: Long? + ) { + val unchangedThemes: ThemeModeOverrides = themes ?: ThemeModeOverrides() + val wallpaperFiles = setOf(unchangedThemes.light?.wallpaper?.imageFile, unchangedThemes.dark?.wallpaper?.imageFile) + var changedThemes: ThemeModeOverrides? = unchangedThemes + val changed = newTheme?.copy(wallpaper = newTheme.wallpaper?.withFilledWallpaperPath()) + changedThemes = when (applyToMode) { + null -> changedThemes?.copy(light = changed?.copy(mode = DefaultThemeMode.LIGHT), dark = changed?.copy(mode = DefaultThemeMode.DARK)) + DefaultThemeMode.LIGHT -> changedThemes?.copy(light = changed?.copy(mode = applyToMode)) + DefaultThemeMode.DARK -> changedThemes?.copy(dark = changed?.copy(mode = applyToMode)) + } + changedThemes = if (changedThemes?.light != null || changedThemes?.dark != null) { + val light = changedThemes.light + val dark = changedThemes.dark + val currentMode = CurrentColors.value.base.mode + // same image file for both modes, copy image to make them as different files + if (light?.wallpaper?.imageFile != null && dark?.wallpaper?.imageFile != null && light.wallpaper.imageFile == dark.wallpaper.imageFile) { + val imageFile = if (currentMode == DefaultThemeMode.LIGHT) { + dark.wallpaper.imageFile + } else { + light.wallpaper.imageFile + } + val filePath = saveWallpaperFile(File(getWallpaperFilePath(imageFile)).toURI()) + changedThemes = if (currentMode == DefaultThemeMode.LIGHT) { + changedThemes.copy(dark = dark.copy(wallpaper = dark.wallpaper.copy(imageFile = filePath))) + } else { + changedThemes.copy(light = light.copy(wallpaper = light.wallpaper.copy(imageFile = filePath))) + } + } + changedThemes + } else { + null + } + + val wallpaperFilesToDelete = wallpaperFiles - changedThemes?.light?.wallpaper?.imageFile - changedThemes?.dark?.wallpaper?.imageFile + wallpaperFilesToDelete.forEach(::removeWallpaperFile) + + val oldThemes = chatModel.currentUser.value?.uiThemes + // Update before save to make it work seamless + chatModel.updateCurrentUserUiThemes(remoteHostId, changedThemes) + updateBackendJob.cancel() + updateBackendJob = withBGApi { + delay(300) + if (!controller.apiSetUserUIThemes(remoteHostId, userId, changedThemes)) { + // If failed to apply for some reason return the old themes + chatModel.updateCurrentUserUiThemes(remoteHostId, oldThemes) } } } @Composable - fun ColorPicker(initialColor: Color, onColorChanged: (Color) -> Unit) { - ClassicColorPicker(modifier = Modifier - .fillMaxWidth() - .height(300.dp), - color = HsvColor.from(color = initialColor), showAlphaBar = true, - onColorChanged = { color: HsvColor -> - onColorChanged(color.toColor()) + fun ThemeDestinationPicker(themeUserDestination: MutableState?>) { + val themeUserDest = remember(themeUserDestination.value?.first) { mutableStateOf(themeUserDestination.value?.first) } + LaunchedEffect(themeUserDestination.value) { + if (themeUserDestination.value == null) { + // Easiest way to hide per-user customization. + // Otherwise, it would be needed to make global variable and to use it everywhere for making a decision to include these overrides into active theme constructing or not + chatModel.currentUser.value = chatModel.currentUser.value?.copy(uiThemes = null) + } else { + chatModel.updateCurrentUserUiThemes(chatModel.remoteHostId(), chatModel.users.firstOrNull { it.user.userId == chatModel.currentUser.value?.userId }?.user?.uiThemes) } - ) + } + DisposableEffect(Unit) { + onDispose { + // Skip when Appearance screen is not hidden yet + if (ModalManager.start.hasModalsOpen()) return@onDispose + // Restore user overrides from stored list of users + chatModel.updateCurrentUserUiThemes(chatModel.remoteHostId(), chatModel.users.firstOrNull { it.user.userId == chatModel.currentUser.value?.userId }?.user?.uiThemes) + themeUserDestination.value = if (chatModel.currentUser.value?.uiThemes == null) null else chatModel.currentUser.value?.userId!! to chatModel.currentUser.value?.uiThemes + } + } + + val values by remember(chatModel.users.toList()) { mutableStateOf( + listOf(null as Long? to generalGetString(MR.strings.theme_destination_app_theme)) + + + chatModel.users.filter { it.user.activeUser }.map { + it.user.userId to it.user.chatViewName + }, + ) + } + if (values.any { it.first == themeUserDestination.value?.first }) { + ExposedDropDownSettingRow( + generalGetString(MR.strings.chat_theme_apply_to_mode), + values, + themeUserDest, + icon = null, + enabled = remember { mutableStateOf(true) }, + onSelected = { userId -> + themeUserDest.value = userId + if (userId != null) { + themeUserDestination.value = userId to chatModel.users.firstOrNull { it.user.userId == userId }?.user?.uiThemes + } else { + themeUserDestination.value = null + } + if (userId != null && userId != chatModel.currentUser.value?.userId) { + withBGApi { + controller.showProgressIfNeeded { + chatModel.controller.changeActiveUser(chatModel.remoteHostId(), userId, null) + } + } + } + } + ) + } else { + themeUserDestination.value = null + } } + @Composable + fun CustomizeThemeColorsSection(currentTheme: ThemeManager.ActiveTheme, editColor: (ThemeColor) -> Unit) { + SectionView(stringResource(MR.strings.theme_colors_section_title)) { + SectionItemViewSpaceBetween({ editColor(ThemeColor.PRIMARY) }) { + val title = generalGetString(MR.strings.color_primary) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.colors.primary) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.PRIMARY_VARIANT) }) { + val title = generalGetString(MR.strings.color_primary_variant) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.colors.primaryVariant) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.PRIMARY_VARIANT2) }) { + val title = generalGetString(MR.strings.color_primary_variant2) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.appColors.primaryVariant2) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.SECONDARY) }) { + val title = generalGetString(MR.strings.color_secondary) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.colors.secondary) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.SECONDARY_VARIANT) }) { + val title = generalGetString(MR.strings.color_secondary_variant) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.colors.secondaryVariant) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.BACKGROUND) }) { + val title = generalGetString(MR.strings.color_background) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.colors.background) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.SURFACE) }) { + val title = generalGetString(MR.strings.color_surface) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.colors.surface) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.TITLE) }) { + val title = generalGetString(MR.strings.color_title) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = currentTheme.appColors.title) + } + } + } + + @Composable + fun ColorEditor( + name: ThemeColor, + initialColor: Color, + theme: DefaultTheme, + wallpaperType: WallpaperType?, + wallpaperImage: ImageBitmap?, + previewBackgroundColor: Color? = MaterialTheme.wallpaper.background, + previewTintColor: Color? = MaterialTheme.wallpaper.tint, + currentColors: () -> ThemeManager.ActiveTheme, + onColorChange: (Color?) -> Unit, + ) { + ColumnWithScrollBar( + Modifier + .fillMaxWidth() + ) { + AppBarTitle(name.text) + + val supportedLiveChange = name in listOf(ThemeColor.SECONDARY, ThemeColor.BACKGROUND, ThemeColor.SURFACE, ThemeColor.RECEIVED_MESSAGE, ThemeColor.SENT_MESSAGE, ThemeColor.SENT_QUOTE, ThemeColor.WALLPAPER_BACKGROUND, ThemeColor.WALLPAPER_TINT) + if (supportedLiveChange) { + SimpleXThemeOverride(currentColors()) { + ChatThemePreview(theme, wallpaperImage, wallpaperType, previewBackgroundColor, previewTintColor) + } + SectionSpacer() + } + + var currentColor by remember { mutableStateOf(initialColor) } + val togglePicker = remember { mutableStateOf(false) } + Box(Modifier.padding(horizontal = DEFAULT_PADDING)) { + if (togglePicker.value) { + ColorPicker(currentColor, showAlphaBar = wallpaperType is WallpaperType.Image || currentColor.alpha < 1f) { + currentColor = it + onColorChange(currentColor) + } + } else { + ColorPicker(currentColor, showAlphaBar = wallpaperType is WallpaperType.Image || currentColor.alpha < 1f) { + currentColor = it + onColorChange(currentColor) + } + } + } + var allowReloadPicker by remember { mutableStateOf(false) } + KeyChangeEffect(wallpaperType) { + allowReloadPicker = true + } + KeyChangeEffect(initialColor) { + if (initialColor != currentColor && allowReloadPicker) { + currentColor = initialColor + togglePicker.value = !togglePicker.value + } + allowReloadPicker = false + } + val clipboard = LocalClipboardManager.current + val hexTrimmed = currentColor.toReadableHex().replaceFirst("#ff", "#") + val savedColor by remember(wallpaperType) { mutableStateOf(initialColor) } + + Row(Modifier.padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF).height(DEFAULT_MIN_SECTION_ITEM_HEIGHT)) { + Box(Modifier.weight(1f).fillMaxHeight().background(savedColor).clickable { + currentColor = savedColor + onColorChange(currentColor) + togglePicker.value = !togglePicker.value + }) + Box(Modifier.weight(1f).fillMaxHeight().background(currentColor).clickable { + clipboard.shareText(hexTrimmed) + }) + } + if (appPrefs.developerTools.get()) { + Row(Modifier.fillMaxWidth().padding(start = DEFAULT_PADDING_HALF, end = DEFAULT_PADDING), horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically) { + val textFieldState = remember { mutableStateOf(TextFieldValue(hexTrimmed)) } + KeyChangeEffect(hexTrimmed) { + textFieldState.value = textFieldState.value.copy(hexTrimmed) + } + DefaultBasicTextField( + Modifier.fillMaxWidth(), + textFieldState, + leadingIcon = { + IconButton(onClick = { clipboard.shareText(hexTrimmed) }) { + Icon(painterResource(MR.images.ic_content_copy), generalGetString(MR.strings.copy_verb), Modifier.size(26.dp), tint = MaterialTheme.colors.primary) + } + }, + onValueChange = { value -> + val color = value.text.trim('#', ' ') + if (color.length == 6 || color.length == 8) { + currentColor = if (color.length == 6) ("ff$color").colorFromReadableHex() else color.colorFromReadableHex() + onColorChange(currentColor) + textFieldState.value = value.copy(currentColor.toReadableHex().replaceFirst("#ff", "#")) + togglePicker.value = !togglePicker.value + } else { + textFieldState.value = value + } + } + ) + } + } + SectionItemView({ + allowReloadPicker = true + onColorChange(null) + }) { + Text(generalGetString(MR.strings.reset_single_color), color = colors.primary) + } + SectionSpacer() + } + } + + + @Composable fun LangSelector(state: State, onSelected: (String) -> Unit) { // Should be the same as in app/build.gradle's `android.defaultConfig.resConfigs` @@ -238,6 +950,7 @@ object AppearanceScope { "cs" to "Čeština", "de" to "Deutsch", "es" to "Español", + "fa" to "فارسی", "fi" to "Suomi", "fr" to "Français", "hu" to "Magyar", @@ -254,7 +967,7 @@ object AppearanceScope { "uk" to "Українська", "zh-CN" to "简体中文" ) - val values by remember(ChatController.appPrefs.appLanguage.state.value) { mutableStateOf(supportedLanguages.map { it.key to it.value }) } + val values by remember(appPrefs.appLanguage.state.value) { mutableStateOf(supportedLanguages.map { it.key to it.value }) } ExposedDropDownSettingRow( generalGetString(MR.strings.settings_section_title_language).lowercase().replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.US) else it.toString() }, values, @@ -266,13 +979,18 @@ object AppearanceScope { } @Composable - private fun ThemeSelector(state: State, onSelected: (String) -> Unit) { - val darkTheme = chat.simplex.common.ui.theme.isSystemInDarkTheme() - val values by remember(ChatController.appPrefs.appLanguage.state.value) { - mutableStateOf(ThemeManager.allThemes(darkTheme).map { it.second.name to it.third }) + private fun ColorModeSelector(state: State, onSelected: (DefaultThemeMode?) -> Unit) { + val values by remember(appPrefs.appLanguage.state.value) { + mutableStateOf( + listOf( + null to generalGetString(MR.strings.color_mode_system), + DefaultThemeMode.LIGHT to generalGetString(MR.strings.color_mode_light), + DefaultThemeMode.DARK to generalGetString(MR.strings.color_mode_dark) + ) + ) } ExposedDropDownSettingRow( - generalGetString(MR.strings.theme), + generalGetString(MR.strings.color_mode), values, state, icon = null, @@ -282,15 +1000,16 @@ object AppearanceScope { } @Composable - private fun DarkThemeSelector(state: State, onSelected: (String) -> Unit) { + private fun DarkModeThemeSelector(state: State, onSelected: (String) -> Unit) { val values by remember { val darkThemes = ArrayList>() - darkThemes.add(DefaultTheme.DARK.name to generalGetString(MR.strings.theme_dark)) - darkThemes.add(DefaultTheme.SIMPLEX.name to generalGetString(MR.strings.theme_simplex)) + darkThemes.add(DefaultTheme.DARK.themeName to generalGetString(MR.strings.theme_dark)) + darkThemes.add(DefaultTheme.SIMPLEX.themeName to generalGetString(MR.strings.theme_simplex)) + darkThemes.add(DefaultTheme.BLACK.themeName to generalGetString(MR.strings.theme_black)) mutableStateOf(darkThemes.toList()) } ExposedDropDownSettingRow( - generalGetString(MR.strings.dark_theme), + generalGetString(MR.strings.dark_mode_colors), values, state, icon = null, @@ -303,3 +1022,109 @@ object AppearanceScope { //} } +@Composable +fun WallpaperSetupView( + wallpaperType: WallpaperType?, + theme: DefaultTheme, + initialWallpaper: AppWallpaper?, + initialSentColor: Color, + initialSentQuoteColor: Color, + initialReceivedColor: Color, + initialReceivedQuoteColor: Color, + editColor: (ThemeColor) -> Unit, + onTypeChange: (WallpaperType?) -> Unit, +) { + if (wallpaperType is WallpaperType.Image) { + val state = remember(wallpaperType.scaleType, initialWallpaper?.type) { mutableStateOf(wallpaperType.scaleType ?: (initialWallpaper?.type as? WallpaperType.Image)?.scaleType ?: WallpaperScaleType.FILL) } + val values = remember { + WallpaperScaleType.entries.map { it to generalGetString(it.text) } + } + ExposedDropDownSettingRow( + stringResource(MR.strings.wallpaper_scale), + values, + state, + onSelected = { scaleType -> + onTypeChange(wallpaperType.copy(scaleType = scaleType)) + } + ) + } + + if (wallpaperType is WallpaperType.Preset || (wallpaperType is WallpaperType.Image && wallpaperType.scaleType == WallpaperScaleType.REPEAT)) { + val state = remember(wallpaperType, initialWallpaper?.type?.scale) { mutableStateOf(wallpaperType.scale ?: initialWallpaper?.type?.scale ?: 1f) } + Row(Modifier.padding(horizontal = DEFAULT_PADDING), verticalAlignment = Alignment.CenterVertically) { + Text("${state.value}".substring(0, min("${state.value}".length, 4)), Modifier.width(50.dp)) + Slider( + state.value, + valueRange = 0.5f..2f, + onValueChange = { + if (wallpaperType is WallpaperType.Preset) { + onTypeChange(wallpaperType.copy(scale = it)) + } else if (wallpaperType is WallpaperType.Image) { + onTypeChange(wallpaperType.copy(scale = it)) + } + } + ) + } + } + + if (wallpaperType is WallpaperType.Preset || wallpaperType is WallpaperType.Image) { + val wallpaperBackgroundColor = initialWallpaper?.background ?: wallpaperType.defaultBackgroundColor(theme, MaterialTheme.colors.background) + SectionItemViewSpaceBetween({ editColor(ThemeColor.WALLPAPER_BACKGROUND) }) { + val title = generalGetString(MR.strings.color_wallpaper_background) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = wallpaperBackgroundColor) + } + val wallpaperTintColor = initialWallpaper?.tint ?: wallpaperType.defaultTintColor(theme) + SectionItemViewSpaceBetween({ editColor(ThemeColor.WALLPAPER_TINT) }) { + val title = generalGetString(MR.strings.color_wallpaper_tint) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = wallpaperTintColor) + } + SectionSpacer() + } + + SectionItemViewSpaceBetween({ editColor(ThemeColor.SENT_MESSAGE) }) { + val title = generalGetString(MR.strings.color_sent_message) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = initialSentColor) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.SENT_QUOTE) }) { + val title = generalGetString(MR.strings.color_sent_quote) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = initialSentQuoteColor) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.RECEIVED_MESSAGE) }) { + val title = generalGetString(MR.strings.color_received_message) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = initialReceivedColor) + } + SectionItemViewSpaceBetween({ editColor(ThemeColor.RECEIVED_QUOTE) }) { + val title = generalGetString(MR.strings.color_received_quote) + Text(title) + Icon(painterResource(MR.images.ic_circle_filled), title, tint = initialReceivedQuoteColor) + } +} + +@Composable +private fun ColorPicker(initialColor: Color, showAlphaBar: Boolean, onColorChanged: (Color) -> Unit) { + ClassicColorPicker(modifier = Modifier + .fillMaxWidth() + .height(300.dp), + color = HsvColor.from(color = initialColor), + showAlphaBar = showAlphaBar, + onColorChanged = { color: HsvColor -> + onColorChanged(color.toColor()) + } + ) +} + +private fun removeUserThemeModeOverrides(themeUserDestination: MutableState?>, perUserTheme: MutableState) { + val dest = themeUserDestination.value ?: return + perUserTheme.value = ThemeModeOverride() + themeUserDestination.value = dest.first to null + val wallpaperFilesToDelete = listOf( + (chatModel.currentUser.value?.uiThemes?.light?.type as? WallpaperType.Image)?.filename, + (chatModel.currentUser.value?.uiThemes?.dark?.type as? WallpaperType.Image)?.filename + ) + wallpaperFilesToDelete.forEach(::removeWallpaperFile) +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt index 94415b0ee0..468a192f09 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/CallSettings.kt @@ -36,10 +36,7 @@ fun CallSettingsLayout( callOnLockScreen: SharedPreference, editIceServers: () -> Unit, ) { - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { + ColumnWithScrollBar(Modifier.fillMaxWidth()) { AppBarTitle(stringResource(MR.strings.your_calls)) val lockCallState = remember { mutableStateOf(callOnLockScreen.get()) } SectionView(stringResource(MR.strings.settings_section_title_settings)) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt index ef8c30b43a..9dfdf23085 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/DeveloperView.kt @@ -1,6 +1,7 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer +import SectionDividerSpaced import SectionSpacer import SectionTextFooter import SectionView @@ -9,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler import chat.simplex.common.model.* @@ -30,9 +32,11 @@ fun DeveloperView( AppBarTitle(stringResource(MR.strings.settings_developer_tools)) val developerTools = m.controller.appPrefs.developerTools val devTools = remember { developerTools.state } + val unchangedHints = mutableStateOf(unchangedHintPreferences()) SectionView { InstallTerminalAppItem(uriHandler) ChatConsoleItem { withAuth(generalGetString(MR.strings.auth_open_chat_console), generalGetString(MR.strings.auth_log_in_using_credential), showCustomModal { it, close -> TerminalView(it, close) }) } + ResetHintsItem(unchangedHints) SettingsPreferenceItem(painterResource(MR.images.ic_code), stringResource(MR.strings.show_developer_options), developerTools) SectionTextFooter( generalGetString(if (devTools.value) MR.strings.show_dev_options else MR.strings.hide_dev_options) + " " + @@ -40,7 +44,7 @@ fun DeveloperView( ) } if (devTools.value) { - SectionSpacer() + SectionDividerSpaced(maxTopPadding = true) SectionView(stringResource(MR.strings.developer_options_section).uppercase()) { SettingsPreferenceItem(painterResource(MR.images.ic_drive_folder_upload), stringResource(MR.strings.confirm_database_upgrades), m.controller.appPrefs.confirmDBUpgrades) if (appPlatform.isDesktop) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt index 4dd406c398..b97a686e22 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/HiddenProfileView.kt @@ -3,6 +3,7 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionItemView import SectionItemViewSpaceBetween +import SectionItemViewWithoutMinPadding import SectionSpacer import SectionTextFooter import SectionView @@ -18,6 +19,7 @@ import androidx.compose.ui.unit.dp import chat.simplex.common.model.ChatModel import chat.simplex.common.model.User import chat.simplex.common.platform.ColumnWithScrollBar +import chat.simplex.common.platform.ntfManager import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chatlist.UserProfileRow import chat.simplex.common.views.database.PassphraseField @@ -36,6 +38,9 @@ fun HiddenProfileView( withBGApi { try { val u = m.controller.apiHideUser(user, hidePassword) + if (!u.activeUser) { + ntfManager.cancelNotificationsForUser(u.userId) + } m.updateUser(u) close() } catch (e: Exception) { @@ -70,10 +75,10 @@ private fun HiddenProfileLayout( val confirmValid by remember { derivedStateOf { confirmHidePassword.value == "" || hidePassword.value == confirmHidePassword.value } } val saveDisabled by remember { derivedStateOf { hidePassword.value == "" || !passwordValid || confirmHidePassword.value == "" || !confirmValid } } SectionView(stringResource(MR.strings.hidden_profile_password).uppercase()) { - SectionItemView { + SectionItemViewWithoutMinPadding { PassphraseField(hidePassword, generalGetString(MR.strings.password_to_show), isValid = { passwordValid }, showStrength = true) } - SectionItemView { + SectionItemViewWithoutMinPadding { PassphraseField(confirmHidePassword, stringResource(MR.strings.confirm_password), isValid = { confirmValid }, dependsOn = hidePassword) } SectionItemViewSpaceBetween({ saveProfilePassword(hidePassword.value) }, disabled = saveDisabled, minHeight = TextFieldDefaults.MinHeight) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt index 4d33040e29..74e6bf5910 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/NetworkAndServers.kt @@ -8,7 +8,6 @@ import SectionItemWithValue import SectionView import SectionViewSelectable import TextIconSpaced -import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardActions import androidx.compose.material.* @@ -19,19 +18,17 @@ import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.* -import androidx.compose.ui.text.font.* import androidx.compose.ui.text.input.* import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* -import chat.simplex.common.views.chat.item.ClickableText import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.helpers.annotatedStringResource import chat.simplex.res.MR @Composable @@ -40,19 +37,14 @@ fun NetworkAndServersView() { // It's not a state, just a one-time value. Shouldn't be used in any state-related situations val netCfg = remember { chatModel.controller.getNetCfg() } val networkUseSocksProxy: MutableState = remember { mutableStateOf(netCfg.useSocksProxy) } - val developerTools = chatModel.controller.appPrefs.developerTools.get() - val onionHosts = remember { mutableStateOf(netCfg.onionHosts) } - val sessionMode = remember { mutableStateOf(netCfg.sessionMode) } val proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } } NetworkAndServersLayout( currentRemoteHost = currentRemoteHost, - developerTools = developerTools, networkUseSocksProxy = networkUseSocksProxy, - onionHosts = onionHosts, - sessionMode = sessionMode, - proxyPort = proxyPort, toggleSocksProxy = { enable -> + val def = NetCfg.defaults + val proxyDef = NetCfg.proxyDefaults if (enable) { AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.network_enable_socks), @@ -60,11 +52,22 @@ fun NetworkAndServersView() { confirmText = generalGetString(MR.strings.confirm_verb), onConfirm = { withBGApi { - val conf = NetCfg.proxyDefaults.withHostPort(chatModel.controller.appPrefs.networkProxyHostPort.get()) + var conf = controller.getNetCfg().withHostPort(controller.appPrefs.networkProxyHostPort.get()) + if (conf.tcpConnectTimeout == def.tcpConnectTimeout) { + conf = conf.copy(tcpConnectTimeout = proxyDef.tcpConnectTimeout) + } + if (conf.tcpTimeout == def.tcpTimeout) { + conf = conf.copy(tcpTimeout = proxyDef.tcpTimeout) + } + if (conf.tcpTimeoutPerKb == def.tcpTimeoutPerKb) { + conf = conf.copy(tcpTimeoutPerKb = proxyDef.tcpTimeoutPerKb) + } + if (conf.rcvConcurrency == def.rcvConcurrency) { + conf = conf.copy(rcvConcurrency = proxyDef.rcvConcurrency) + } chatModel.controller.apiSetNetworkConfig(conf) chatModel.controller.setNetCfg(conf) networkUseSocksProxy.value = true - onionHosts.value = conf.onionHosts } } ) @@ -75,118 +78,64 @@ fun NetworkAndServersView() { confirmText = generalGetString(MR.strings.confirm_verb), onConfirm = { withBGApi { - val conf = NetCfg.defaults + var conf = controller.getNetCfg().copy(socksProxy = null) + if (conf.tcpConnectTimeout == proxyDef.tcpConnectTimeout) { + conf = conf.copy(tcpConnectTimeout = def.tcpConnectTimeout) + } + if (conf.tcpTimeout == proxyDef.tcpTimeout) { + conf = conf.copy(tcpTimeout = def.tcpTimeout) + } + if (conf.tcpTimeoutPerKb == proxyDef.tcpTimeoutPerKb) { + conf = conf.copy(tcpTimeoutPerKb = def.tcpTimeoutPerKb) + } + if (conf.rcvConcurrency == proxyDef.rcvConcurrency) { + conf = conf.copy(rcvConcurrency = def.rcvConcurrency) + } chatModel.controller.apiSetNetworkConfig(conf) chatModel.controller.setNetCfg(conf) networkUseSocksProxy.value = false - onionHosts.value = conf.onionHosts } } ) } - }, - useOnion = { - if (onionHosts.value == it) return@NetworkAndServersLayout - val prevValue = onionHosts.value - onionHosts.value = it - val startsWith = when (it) { - OnionHosts.NEVER -> generalGetString(MR.strings.network_use_onion_hosts_no_desc_in_alert) - OnionHosts.PREFER -> generalGetString(MR.strings.network_use_onion_hosts_prefer_desc_in_alert) - OnionHosts.REQUIRED -> generalGetString(MR.strings.network_use_onion_hosts_required_desc_in_alert) - } - showUpdateNetworkSettingsDialog( - title = generalGetString(MR.strings.update_onion_hosts_settings_question), - startsWith, - onDismiss = { - onionHosts.value = prevValue - } - ) { - withBGApi { - val newCfg = chatModel.controller.getNetCfg().withOnionHosts(it) - val res = chatModel.controller.apiSetNetworkConfig(newCfg) - if (res) { - chatModel.controller.setNetCfg(newCfg) - onionHosts.value = it - } else { - onionHosts.value = prevValue - } - } - } - }, - updateSessionMode = { - if (sessionMode.value == it) return@NetworkAndServersLayout - val prevValue = sessionMode.value - sessionMode.value = it - val startsWith = when (it) { - TransportSessionMode.User -> generalGetString(MR.strings.network_session_mode_user_description) - TransportSessionMode.Entity -> generalGetString(MR.strings.network_session_mode_entity_description) - } - showUpdateNetworkSettingsDialog( - title = generalGetString(MR.strings.update_network_session_mode_question), - startsWith, - onDismiss = { sessionMode.value = prevValue } - ) { - withBGApi { - val newCfg = chatModel.controller.getNetCfg().copy(sessionMode = it) - val res = chatModel.controller.apiSetNetworkConfig(newCfg) - if (res) { - chatModel.controller.setNetCfg(newCfg) - sessionMode.value = it - } else { - sessionMode.value = prevValue - } - } - } } ) } @Composable fun NetworkAndServersLayout( currentRemoteHost: RemoteHostInfo?, - developerTools: Boolean, networkUseSocksProxy: MutableState, - onionHosts: MutableState, - sessionMode: MutableState, - proxyPort: State, toggleSocksProxy: (Boolean) -> Unit, - useOnion: (OnionHosts) -> Unit, - updateSessionMode: (TransportSessionMode) -> Unit, ) { val m = chatModel - ColumnWithScrollBar( - Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { + ColumnWithScrollBar(Modifier.fillMaxWidth()) { + val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.start.showModal(content = it) } + val showCustomModal = { it: @Composable (close: () -> Unit) -> Unit -> ModalManager.start.showCustomModal { close -> it(close) }} + AppBarTitle(stringResource(MR.strings.network_and_servers)) if (!chatModel.desktopNoUserNoRemote) { SectionView(generalGetString(MR.strings.settings_section_title_messages)) { - SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.smp_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.SMP, close) } }) + SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.message_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.SMP, close) } }) - SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) } }) + SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.media_and_file_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) } }) if (currentRemoteHost == null) { - val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.start.showModal(content = it) } - UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showModal, chatModel.controller.appPrefs.networkProxyHostPort, false) - UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion) - if (developerTools) { - SessionModePicker(sessionMode, showModal, updateSessionMode) + UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy) + SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, appPrefs.networkProxyHostPort, false, it) }}) + SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), { ModalManager.start.showCustomModal { AdvancedNetworkSettingsView(showModal, it) } }) + if (networkUseSocksProxy.value) { + SectionCustomFooter { + Column { + Text(annotatedStringResource(MR.strings.socks_proxy_setting_limitations)) + } + } + SectionDividerSpaced(maxTopPadding = true) + } else { + SectionDividerSpaced() } - SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), { ModalManager.start.showModal { AdvancedNetworkSettingsView(m) } }) } } } - if (currentRemoteHost == null && networkUseSocksProxy.value) { - SectionCustomFooter { - Column { - Text(annotatedStringResource(MR.strings.disable_onion_hosts_when_not_supported)) - Spacer(Modifier.height(DEFAULT_PADDING_HALF)) - Text(annotatedStringResource(MR.strings.socks_proxy_setting_limitations)) - } - } - Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 32.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp)) - } else if (!chatModel.desktopNoUserNoRemote) { - Divider(Modifier.padding(start = DEFAULT_PADDING_HALF, top = 24.dp, end = DEFAULT_PADDING_HALF, bottom = 30.dp)) - } SectionView(generalGetString(MR.strings.settings_section_title_calls)) { SettingsActionItem(painterResource(MR.images.ic_electrical_services), stringResource(MR.strings.webrtc_ice_servers), { ModalManager.start.showModal { RTCServersView(m) } }) @@ -211,13 +160,14 @@ fun NetworkAndServersView() { onionHosts: MutableState, sessionMode: MutableState, networkProxyHostPort: SharedPreference, - proxyPort: State, toggleSocksProxy: (Boolean) -> Unit, useOnion: (OnionHosts) -> Unit, updateSessionMode: (TransportSessionMode) -> Unit, ) { val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.fullscreen.showModal(content = it) } - UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showModal, networkProxyHostPort, true) + val showCustomModal = { it: @Composable (close: () -> Unit) -> Unit -> ModalManager.fullscreen.showCustomModal { close -> it(close) }} + UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy) + SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, networkProxyHostPort, true, it) } }) UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion) if (developerTools) { SessionModePicker(sessionMode, showModal, updateSessionMode) @@ -227,11 +177,7 @@ fun NetworkAndServersView() { @Composable fun UseSocksProxySwitch( networkUseSocksProxy: MutableState, - proxyPort: State, toggleSocksProxy: (Boolean) -> Unit, - showModal: (@Composable ModalData.() -> Unit) -> Unit, - networkProxyHostPort: SharedPreference = chatModel.controller.appPrefs.networkProxyHostPort, - migration: Boolean = false, ) { Row( Modifier.fillMaxWidth().padding(end = DEFAULT_PADDING), @@ -248,32 +194,7 @@ fun UseSocksProxySwitch( tint = MaterialTheme.colors.secondary ) TextIconSpaced(false) - val text = buildAnnotatedString { - append(generalGetString(MR.strings.network_socks_toggle_use_socks_proxy) + " (") - val style = SpanStyle(color = MaterialTheme.colors.primary) - val disabledStyle = SpanStyle(color = MaterialTheme.colors.onBackground) - withAnnotation(tag = "PORT", annotation = generalGetString(MR.strings.network_proxy_port).format(proxyPort.value)) { - withStyle(if (networkUseSocksProxy.value || !migration) style else disabledStyle) { - append(generalGetString(MR.strings.network_proxy_port).format(proxyPort.value)) - } - } - append(")") - } - ClickableText( - text, - style = TextStyle(color = MaterialTheme.colors.onBackground, fontSize = 16.sp, fontFamily = Inter, fontWeight = FontWeight.Normal), - onClick = { offset -> - text.getStringAnnotations(tag = "PORT", start = offset, end = offset) - .firstOrNull()?.let { _ -> - if (networkUseSocksProxy.value || !migration) { - showModal { SockProxySettings(chatModel, networkProxyHostPort, migration) } - } - } - }, - shouldConsumeEvent = { offset -> - text.getStringAnnotations(tag = "PORT", start = offset, end = offset).any() - } - ) + Text(generalGetString(MR.strings.network_socks_toggle_use_socks_proxy)) } DefaultSwitch( checked = networkUseSocksProxy.value, @@ -283,53 +204,67 @@ fun UseSocksProxySwitch( } @Composable -fun SockProxySettings( - m: ChatModel, - networkProxyHostPort: SharedPreference = m.controller.appPrefs.networkProxyHostPort, +fun SocksProxySettings( + networkUseSocksProxy: Boolean, + networkProxyHostPort: SharedPreference = appPrefs.networkProxyHostPort, migration: Boolean, + close: () -> Unit ) { - ColumnWithScrollBar( - Modifier - .fillMaxWidth() - ) { - val defaultHostPort = remember { "localhost:9050" } - AppBarTitle(generalGetString(MR.strings.network_socks_proxy_settings)) - val hostPortSaved by remember { networkProxyHostPort.state } - val hostUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) { - mutableStateOf(TextFieldValue(hostPortSaved?.split(":")?.firstOrNull() ?: "localhost")) - } - val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) { - mutableStateOf(TextFieldValue(hostPortSaved?.split(":")?.lastOrNull() ?: "9050")) - } - val save = { + val defaultHostPort = remember { "localhost:9050" } + val hostPortSaved by remember { networkProxyHostPort.state } + val hostUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue(hostPortSaved?.split(":")?.firstOrNull() ?: "localhost")) + } + val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) { + mutableStateOf(TextFieldValue(hostPortSaved?.split(":")?.lastOrNull() ?: "9050")) + } + val save = { + val oldValue = networkProxyHostPort.get() + networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text) + if (networkUseSocksProxy && !migration) { withBGApi { - networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text) - if (m.controller.appPrefs.networkUseSocksProxy.get() && !migration) { - m.controller.apiSetNetworkConfig(m.controller.getNetCfg()) + if (!controller.apiSetNetworkConfig(controller.getNetCfg())) { + networkProxyHostPort.set(oldValue) } } } - SectionView { - SectionItemView { - ResetToDefaultsButton({ - val reset = { - networkProxyHostPort.set(defaultHostPort) - val newHost = defaultHostPort.split(":").first() - val newPort = defaultHostPort.split(":").last() - hostUnsaved.value = hostUnsaved.value.copy(newHost, TextRange(newHost.length)) - portUnsaved.value = portUnsaved.value.copy(newPort, TextRange(newPort.length)) - save() - } - if (m.controller.appPrefs.networkUseSocksProxy.get() && !migration) { - showUpdateNetworkSettingsDialog { - reset() - } - } else { - reset() - } - }, disabled = hostPortSaved == defaultHostPort) + } + val saveAndClose = { + val oldValue = networkProxyHostPort.get() + networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text) + if (networkUseSocksProxy && !migration) { + withBGApi { + if (controller.apiSetNetworkConfig(controller.getNetCfg())) { + close() + } else { + networkProxyHostPort.set(oldValue) + } } - SectionItemView { + } + } + val saveDisabled = hostPortSaved == (hostUnsaved.value.text + ":" + portUnsaved.value.text) || + remember { derivedStateOf { !validHost(hostUnsaved.value.text) } }.value || + remember { derivedStateOf { !validPort(portUnsaved.value.text) } }.value + val resetDisabled = hostUnsaved.value.text + ":" + portUnsaved.value.text == defaultHostPort + ModalView( + close = { + if (saveDisabled) { + close() + } else { + showUnsavedSocksHostPortAlert( + confirmText = generalGetString(if (networkUseSocksProxy && !migration) MR.strings.network_options_save_and_reconnect else MR.strings.network_options_save), + save = saveAndClose, + close = close + ) + } + }, + ) { + ColumnWithScrollBar( + Modifier + .fillMaxWidth() + ) { + AppBarTitle(generalGetString(MR.strings.network_socks_proxy_settings)) + SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { DefaultConfigurableTextField( hostUnsaved, stringResource(MR.strings.host_verb), @@ -338,8 +273,6 @@ fun SockProxySettings( keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }), keyboardType = KeyboardType.Text, ) - } - SectionItemView { DefaultConfigurableTextField( portUnsaved, stringResource(MR.strings.port_verb), @@ -349,28 +282,42 @@ fun SockProxySettings( keyboardType = KeyboardType.Number, ) } + + SectionDividerSpaced(maxBottomPadding = false) + + SectionView { + SectionItemView({ + val newHost = defaultHostPort.split(":").first() + val newPort = defaultHostPort.split(":").last() + hostUnsaved.value = hostUnsaved.value.copy(newHost, TextRange(newHost.length)) + portUnsaved.value = portUnsaved.value.copy(newPort, TextRange(newPort.length)) + }, disabled = resetDisabled) { + Text(stringResource(MR.strings.network_options_reset_to_defaults), color = if (resetDisabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) + } + SectionItemView( + click = { if (networkUseSocksProxy && !migration) showUpdateNetworkSettingsDialog { save() } else save() }, + disabled = saveDisabled + ) { + Text(stringResource(if (networkUseSocksProxy && !migration) MR.strings.network_options_save_and_reconnect else MR.strings.network_options_save), color = if (saveDisabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) + } + } + SectionBottomSpacer() } - SectionCustomFooter { - NetworkSectionFooter( - revert = { - val prevHost = hostPortSaved?.split(":")?.firstOrNull() ?: "localhost" - val prevPort = hostPortSaved?.split(":")?.lastOrNull() ?: "9050" - hostUnsaved.value = hostUnsaved.value.copy(prevHost, TextRange(prevHost.length)) - portUnsaved.value = portUnsaved.value.copy(prevPort, TextRange(prevPort.length)) - }, - save = { if (m.controller.appPrefs.networkUseSocksProxy.get() && !migration) showUpdateNetworkSettingsDialog { save() } else save() }, - revertDisabled = hostPortSaved == (hostUnsaved.value.text + ":" + portUnsaved.value.text), - saveDisabled = hostPortSaved == (hostUnsaved.value.text + ":" + portUnsaved.value.text) || - remember { derivedStateOf { !validHost(hostUnsaved.value.text) } }.value || - remember { derivedStateOf { !validPort(portUnsaved.value.text) } }.value - ) - } - SectionBottomSpacer() } } +private fun showUnsavedSocksHostPortAlert(confirmText: String, save: () -> Unit, close: () -> Unit) { + AlertManager.shared.showAlertDialogStacked( + title = generalGetString(MR.strings.update_network_settings_question), + confirmText = confirmText, + dismissText = generalGetString(MR.strings.exit_without_saving), + onConfirm = save, + onDismiss = close, + ) +} + @Composable -private fun UseOnionHosts( +fun UseOnionHosts( onionHosts: MutableState, enabled: State, showModal: (@Composable ModalData.() -> Unit) -> Unit, @@ -419,7 +366,7 @@ private fun UseOnionHosts( } @Composable -private fun SessionModePicker( +fun SessionModePicker( sessionMode: MutableState, showModal: (@Composable ModalData.() -> Unit) -> Unit, updateSessionMode: (TransportSessionMode) -> Unit, @@ -452,18 +399,6 @@ private fun SessionModePicker( ) } -@Composable -private fun NetworkSectionFooter(revert: () -> Unit, save: () -> Unit, revertDisabled: Boolean, saveDisabled: Boolean) { - Row( - Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - FooterButton(painterResource(MR.images.ic_replay), stringResource(MR.strings.network_options_revert), revert, revertDisabled) - FooterButton(painterResource(MR.images.ic_check), stringResource(MR.strings.network_options_save), save, saveDisabled) - } -} - // https://stackoverflow.com/a/106223 private fun validHost(s: String): Boolean { val validIp = Regex("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])[.]){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$") @@ -477,7 +412,7 @@ fun validPort(s: String): Boolean { return s.isNotBlank() && s.matches(validPort) } -private fun showUpdateNetworkSettingsDialog( +fun showUpdateNetworkSettingsDialog( title: String, startsWith: String = "", message: String = generalGetString(MR.strings.updating_settings_will_reconnect_client_to_all_servers), @@ -500,14 +435,8 @@ fun PreviewNetworkAndServersLayout() { SimpleXTheme { NetworkAndServersLayout( currentRemoteHost = null, - developerTools = true, networkUseSocksProxy = remember { mutableStateOf(true) }, - proxyPort = remember { mutableStateOf(9050) }, toggleSocksProxy = {}, - onionHosts = remember { mutableStateOf(OnionHosts.PREFER) }, - sessionMode = remember { mutableStateOf(TransportSessionMode.User) }, - useOnion = {}, - updateSessionMode = {}, ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt index cd0e40f5d0..96a0bdcda3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/Preferences.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.Modifier import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.views.helpers.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.ColumnWithScrollBar import chat.simplex.res.MR @@ -33,7 +34,9 @@ fun PreferencesView(m: ChatModel, user: User, close: () -> Unit,) { if (updated != null) { val (updatedProfile, updatedContacts) = updated m.updateCurrentUser(user.remoteHostId, updatedProfile, preferences) - updatedContacts.forEach { m.updateContact(user.remoteHostId, it) } + withChats { + updatedContacts.forEach { updateContact(user.remoteHostId, it) } + } currentPreferences = preferences } afterSave() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt index b376285259..084dfc20d2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/PrivacySettings.kt @@ -1,6 +1,7 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer +import SectionCustomFooter import SectionDividerSpaced import SectionItemView import SectionTextFooter @@ -21,6 +22,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.res.MR import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.ui.theme.* import chat.simplex.common.views.ProfileNameField import chat.simplex.common.views.helpers.* @@ -30,7 +32,10 @@ import chat.simplex.common.views.isValidDisplayName import chat.simplex.common.views.localauth.SetAppPasscodeView import chat.simplex.common.views.onboarding.ReadableText import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.ChatModel.withChats import chat.simplex.common.platform.* +import kotlin.math.min +import kotlin.math.roundToInt enum class LAMode { SYSTEM, @@ -63,10 +68,6 @@ fun PrivacySettingsView( SectionDividerSpaced() SectionView(stringResource(MR.strings.settings_section_title_chats)) { - SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.encrypt_local_files), chatModel.controller.appPrefs.privacyEncryptLocalFiles, onChange = { enable -> - withBGApi { chatModel.controller.apiSetEncryptLocalFiles(enable) } - }) - SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages) SettingsPreferenceItem(painterResource(MR.images.ic_travel_explore), stringResource(MR.strings.send_link_previews), chatModel.controller.appPrefs.privacyLinkPreviews) SettingsPreferenceItem( painterResource(MR.images.ic_chat_bubble), @@ -91,6 +92,25 @@ fun PrivacySettingsView( chatModel.simplexLinkMode.value = it }) } + SectionDividerSpaced() + + SectionView(stringResource(MR.strings.settings_section_title_files)) { + SettingsPreferenceItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.encrypt_local_files), chatModel.controller.appPrefs.privacyEncryptLocalFiles, onChange = { enable -> + withBGApi { chatModel.controller.apiSetEncryptLocalFiles(enable) } + }) + SettingsPreferenceItem(painterResource(MR.images.ic_image), stringResource(MR.strings.auto_accept_images), chatModel.controller.appPrefs.privacyAcceptImages) + BlurRadiusOptions(remember { appPrefs.privacyMediaBlurRadius.state }) { + appPrefs.privacyMediaBlurRadius.set(it) + } + SettingsPreferenceItem(painterResource(MR.images.ic_security), stringResource(MR.strings.protect_ip_address), chatModel.controller.appPrefs.privacyAskToApproveRelays) + } + SectionTextFooter( + if (chatModel.controller.appPrefs.privacyAskToApproveRelays.state.value) { + stringResource(MR.strings.app_will_ask_to_confirm_unknown_file_servers) + } else { + stringResource(MR.strings.without_tor_or_vpn_ip_address_will_be_visible_to_file_servers) + } + ) val currentUser = chatModel.currentUser.value if (currentUser != null) { @@ -102,14 +122,16 @@ fun PrivacySettingsView( chatModel.currentUser.value = currentUser.copy(sendRcptsContacts = enable) if (clearOverrides) { // For loop here is to prevent ConcurrentModificationException that happens with forEach - for (i in 0 until chatModel.chats.size) { - val chat = chatModel.chats[i] - if (chat.chatInfo is ChatInfo.Direct) { - var contact = chat.chatInfo.contact - val sendRcpts = contact.chatSettings.sendRcpts - if (sendRcpts != null && sendRcpts != enable) { - contact = contact.copy(chatSettings = contact.chatSettings.copy(sendRcpts = null)) - chatModel.updateContact(currentUser.remoteHostId, contact) + withChats { + for (i in 0 until chats.size) { + val chat = chats[i] + if (chat.chatInfo is ChatInfo.Direct) { + var contact = chat.chatInfo.contact + val sendRcpts = contact.chatSettings.sendRcpts + if (sendRcpts != null && sendRcpts != enable) { + contact = contact.copy(chatSettings = contact.chatSettings.copy(sendRcpts = null)) + updateContact(currentUser.remoteHostId, contact) + } } } } @@ -124,15 +146,17 @@ fun PrivacySettingsView( chatModel.controller.appPrefs.privacyDeliveryReceiptsSet.set(true) chatModel.currentUser.value = currentUser.copy(sendRcptsSmallGroups = enable) if (clearOverrides) { - // For loop here is to prevent ConcurrentModificationException that happens with forEach - for (i in 0 until chatModel.chats.size) { - val chat = chatModel.chats[i] - if (chat.chatInfo is ChatInfo.Group) { - var groupInfo = chat.chatInfo.groupInfo - val sendRcpts = groupInfo.chatSettings.sendRcpts - if (sendRcpts != null && sendRcpts != enable) { - groupInfo = groupInfo.copy(chatSettings = groupInfo.chatSettings.copy(sendRcpts = null)) - chatModel.updateGroup(currentUser.remoteHostId, groupInfo) + withChats { + // For loop here is to prevent ConcurrentModificationException that happens with forEach + for (i in 0 until chats.size) { + val chat = chats[i] + if (chat.chatInfo is ChatInfo.Group) { + var groupInfo = chat.chatInfo.groupInfo + val sendRcpts = groupInfo.chatSettings.sendRcpts + if (sendRcpts != null && sendRcpts != enable) { + groupInfo = groupInfo.copy(chatSettings = groupInfo.chatSettings.copy(sendRcpts = null)) + updateGroup(currentUser.remoteHostId, groupInfo) + } } } } @@ -141,11 +165,11 @@ fun PrivacySettingsView( } if (!chatModel.desktopNoUserNoRemote) { - SectionDividerSpaced() + SectionDividerSpaced(maxTopPadding = true) DeliveryReceiptsSection( currentUser = currentUser, setOrAskSendReceiptsContacts = { enable -> - val contactReceiptsOverrides = chatModel.chats.fold(0) { count, chat -> + val contactReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat -> if (chat.chatInfo is ChatInfo.Direct) { val sendRcpts = chat.chatInfo.contact.chatSettings.sendRcpts count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) @@ -160,7 +184,7 @@ fun PrivacySettingsView( } }, setOrAskSendReceiptsGroups = { enable -> - val groupReceiptsOverrides = chatModel.chats.fold(0) { count, chat -> + val groupReceiptsOverrides = chatModel.chats.value.fold(0) { count, chat -> if (chat.chatInfo is ChatInfo.Group) { val sendRcpts = chat.chatInfo.groupInfo.chatSettings.sendRcpts count + (if (sendRcpts == null || sendRcpts == enable) 0 else 1) @@ -204,6 +228,30 @@ private fun SimpleXLinkOptions(simplexLinkModeState: State, onS ) } +@Composable +private fun BlurRadiusOptions(state: State, onSelected: (Int) -> Unit) { + val choices = listOf(0, 12, 24, 48) + val pickerValues = choices + if (choices.contains(state.value)) emptyList() else listOf(state.value) + val values = remember { + pickerValues.map { + when (it) { + 0 -> it to generalGetString(MR.strings.privacy_media_blur_radius_off) + 12 -> it to generalGetString(MR.strings.privacy_media_blur_radius_soft) + 24 -> it to generalGetString(MR.strings.privacy_media_blur_radius_medium) + 48 -> it to generalGetString(MR.strings.privacy_media_blur_radius_strong) + else -> it to "$it" + } + } + } + ExposedDropDownSettingRow( + generalGetString(MR.strings.privacy_media_blur_radius), + values, + state, + icon = painterResource(MR.images.ic_blur_on), + onSelected = onSelected + ) +} + @Composable expect fun PrivacyDeviceSection( showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServerView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServerView.kt index 399895870f..3a1a1cb8f3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServerView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServerView.kt @@ -110,7 +110,7 @@ private fun PresetServer( ) } } - SectionDividerSpaced(maxTopPadding = true) + SectionDividerSpaced() UseServerSection(true, testing, server, testServer, onUpdate, onDelete) } @@ -150,7 +150,7 @@ private fun CustomServer( } } } - SectionDividerSpaced() + SectionDividerSpaced(maxTopPadding = true) UseServerSection(valid.value, testing, server, testServer, onUpdate, onDelete) if (valid.value) { @@ -175,8 +175,16 @@ private fun UseServerSection( Text(stringResource(MR.strings.smp_servers_test_server), color = if (valid && !testing) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) ShowTestStatus(server) } + val enabled = rememberUpdatedState(server.enabled) - PreferenceToggle(stringResource(MR.strings.smp_servers_use_server_for_new_conn), enabled.value) { onUpdate(server.copy(enabled = it)) } + PreferenceToggle( + stringResource(MR.strings.smp_servers_use_server_for_new_conn), + disabled = server.tested != true && !server.preset, + checked = enabled.value + ) { + onUpdate(server.copy(enabled = it)) + } + SectionItemView(onDelete, disabled = testing) { Text(stringResource(MR.strings.smp_servers_delete_server), color = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.error) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt index fe847432fb..5d5f1d039a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ProtocolServersView.kt @@ -28,7 +28,7 @@ import chat.simplex.res.MR @Composable fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: ServerProtocol, close: () -> Unit) { - var presetServers by remember(rhId) { mutableStateOf(emptyList()) } + var presetServers by remember(rhId) { mutableStateOf(emptyList()) } var servers by remember { stateGetOrPut("servers") { emptyList() } } var serversAlreadyLoaded by remember { stateGetOrPut("serversAlreadyLoaded") { false } } val currServers = remember(rhId) { mutableStateOf(servers) } @@ -198,12 +198,42 @@ private fun ProtocolServersLayout( ) { AppBarTitle(stringResource(if (serverProtocol == ServerProtocol.SMP) MR.strings.your_SMP_servers else MR.strings.your_XFTP_servers)) - SectionView(stringResource(if (serverProtocol == ServerProtocol.SMP) MR.strings.smp_servers else MR.strings.xftp_servers).uppercase()) { - for (srv in servers) { - SectionItemView({ showServer(srv) }, disabled = testing) { - ProtocolServerView(serverProtocol, srv, servers, testing) + val configuredServers = servers.filter { it.preset || it.enabled } + val otherServers = servers.filter { !(it.preset || it.enabled) } + + if (configuredServers.isNotEmpty()) { + SectionView(stringResource(if (serverProtocol == ServerProtocol.SMP) MR.strings.smp_servers_configured else MR.strings.xftp_servers_configured).uppercase()) { + for (srv in configuredServers) { + SectionItemView({ showServer(srv) }, disabled = testing) { + ProtocolServerView(serverProtocol, srv, servers, testing) + } } } + SectionTextFooter( + remember(currentUser?.displayName) { + buildAnnotatedString { + append(generalGetString(MR.strings.smp_servers_per_user) + " ") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append(currentUser?.displayName ?: "") + } + append(".") + } + } + ) + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + } + + if (otherServers.isNotEmpty()) { + SectionView(stringResource(if (serverProtocol == ServerProtocol.SMP) MR.strings.smp_servers_other else MR.strings.xftp_servers_other).uppercase()) { + for (srv in otherServers.filter { !(it.preset || it.enabled) }) { + SectionItemView({ showServer(srv) }, disabled = testing) { + ProtocolServerView(serverProtocol, srv, servers, testing) + } + } + } + } + + SectionView { SettingsActionItem( painterResource(MR.images.ic_add), stringResource(MR.strings.smp_servers_add), @@ -212,19 +242,9 @@ private fun ProtocolServersLayout( textColor = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary, iconColor = if (testing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary ) + SectionDividerSpaced(maxTopPadding = false, maxBottomPadding = false) } - SectionTextFooter( - remember(currentUser?.displayName) { - buildAnnotatedString { - append(generalGetString(MR.strings.smp_servers_per_user) + " ") - withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { - append(currentUser?.displayName ?: "") - } - append(".") - } - } - ) - SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) + SectionView { SectionItemView(resetServers, disabled = serversUnchanged) { Text(stringResource(MR.strings.reset_verb), color = if (!serversUnchanged) MaterialTheme.colors.onBackground else MaterialTheme.colors.secondary) @@ -285,21 +305,21 @@ private fun uniqueAddress(s: ServerCfg, address: ServerAddress, servers: List, servers: List, m: ChatModel): Boolean = +private fun hasAllPresets(presetServers: List, servers: List, m: ChatModel): Boolean = presetServers.all { hasPreset(it, servers) } ?: true -private fun addAllPresets(rhId: Long?, presetServers: List, servers: List, m: ChatModel): List { +private fun addAllPresets(rhId: Long?, presetServers: List, servers: List, m: ChatModel): List { val toAdd = ArrayList() for (srv in presetServers) { if (!hasPreset(srv, servers)) { - toAdd.add(ServerCfg(remoteHostId = rhId, srv, preset = true, tested = null, enabled = true)) + toAdd.add(srv) } } return toAdd } -private fun hasPreset(srv: String, servers: List): Boolean = - servers.any { it.server == srv } +private fun hasPreset(srv: ServerCfg, servers: List): Boolean = + servers.any { it.server == srv.server } private suspend fun testServers(testing: MutableState, servers: List, m: ChatModel, onUpdated: (List) -> Unit) { val resetStatus = resetTestStatus(servers) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt index 502b579d64..7c2c578d6a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/ScanProtocolServer.kt @@ -25,7 +25,7 @@ fun ScanProtocolServerLayout(rhId: Long?, onNext: (ServerCfg) -> Unit) { QRCodeScanner { text -> val res = parseServerAddress(text) if (res != null) { - onNext(ServerCfg(remoteHostId = rhId, text, false, null, true)) + onNext(ServerCfg(remoteHostId = rhId, text, false, null, false)) } else { AlertManager.shared.showAlertMsg( title = generalGetString(MR.strings.smp_servers_invalid_address), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt index f0960c1511..2f6c0395ec 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SetDeliveryReceiptsView.kt @@ -73,33 +73,30 @@ private fun SetDeliveryReceiptsLayout( skip: () -> Unit, userCount: Int, ) { + // This view located in the left panel which means it has to have a padding from right side in order + // to see scroll bar. And this padding should be applied to upper element, not scrollable column modifier val endPadding = if (appPlatform.isDesktop) 56.dp else 0.dp - val (scrollBarAlpha, scrollModifier, scrollJob) = platform.desktopScrollBarComponents() - val scrollState = rememberScrollState() - Column( - Modifier.fillMaxSize().verticalScroll(scrollState).then(if (appPlatform.isDesktop) scrollModifier else Modifier).padding(top = DEFAULT_PADDING, end = endPadding), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - AppBarTitle(stringResource(MR.strings.delivery_receipts_title)) + Box(Modifier.padding(top = DEFAULT_PADDING, end = endPadding)) { + ColumnWithScrollBar( + Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + AppBarTitle(stringResource(MR.strings.delivery_receipts_title)) - Spacer(Modifier.weight(1f)) + Spacer(Modifier.weight(1f)) - EnableReceiptsButton(enableReceipts) - if (userCount > 1) { - TextBelowButton(stringResource(MR.strings.sending_delivery_receipts_will_be_enabled_all_profiles)) - } else { - TextBelowButton(stringResource(MR.strings.sending_delivery_receipts_will_be_enabled)) - } + EnableReceiptsButton(enableReceipts) + if (userCount > 1) { + TextBelowButton(stringResource(MR.strings.sending_delivery_receipts_will_be_enabled_all_profiles)) + } else { + TextBelowButton(stringResource(MR.strings.sending_delivery_receipts_will_be_enabled)) + } - Spacer(Modifier.weight(1f)) + Spacer(Modifier.weight(1f)) - SkipButton(skip) + SkipButton(skip) - SectionBottomSpacer() - } - if (appPlatform.isDesktop) { - Box(Modifier.fillMaxSize().padding(end = endPadding)) { - platform.desktopScrollBar(scrollState, Modifier.align(Alignment.CenterEnd).fillMaxHeight(), scrollBarAlpha, scrollJob, false) + SectionBottomSpacer() } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt index 298eb39737..b50e905f39 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt @@ -3,7 +3,6 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionDividerSpaced import SectionItemView -import SectionItemViewWithIcon import SectionView import TextIconSpaced import androidx.compose.desktop.ui.tooling.preview.Preview @@ -73,6 +72,9 @@ fun SettingsView(chatModel: ChatModel, setPerformLA: (Boolean) -> Unit, drawerSt withAuth = ::doWithAuth, drawerState = drawerState, ) + KeyChangeEffect(chatModel.updatingProgress.value != null) { + drawerState.close() + } } val simplexTeamUri = @@ -97,90 +99,84 @@ fun SettingsLayout( ) { val scope = rememberCoroutineScope() val closeSettings: () -> Unit = { scope.launch { drawerState.close() } } + val view = LocalMultiplatformView() if (drawerState.isOpen) { BackHandler { closeSettings() } + LaunchedEffect(Unit) { + hideKeyboard(view) + } } val theme = CurrentColors.collectAsState() val uriHandler = LocalUriHandler.current - Box(Modifier.fillMaxSize()) { - ColumnWithScrollBar( - Modifier - .fillMaxSize() - .themedBackground(theme.value.base) - .padding(top = if (appPlatform.isAndroid) DEFAULT_PADDING else DEFAULT_PADDING * 3) - ) { - AppBarTitle(stringResource(MR.strings.your_settings)) + ColumnWithScrollBar( + Modifier + .fillMaxSize() + .themedBackground(theme.value.base) + ) { + AppBarTitle(stringResource(MR.strings.your_settings)) - SectionView(stringResource(MR.strings.settings_section_title_you)) { - val profileHidden = rememberSaveable { mutableStateOf(false) } - if (profile != null) { - SectionItemView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) { - ProfilePreview(profile, stopped = stopped) - } - SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.your_chat_profiles), { withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { showSettingsModalWithSearch { it, search -> UserProfilesView(it, search, profileHidden, drawerState) } } }, disabled = stopped, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_qr_code), stringResource(MR.strings.your_simplex_contact_address), showCustomModal { it, close -> UserAddressView(it, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped, extraPadding = true) - ChatPreferencesItem(showCustomModal, stopped = stopped) - } else if (chatModel.localUserCreated.value == false) { - SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.create_chat_profile), { withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.center.showModalCloseable { close -> - LaunchedEffect(Unit) { - closeSettings() + SectionView(stringResource(MR.strings.settings_section_title_you)) { + val profileHidden = rememberSaveable { mutableStateOf(false) } + if (profile != null) { + SectionItemView(showCustomModal { chatModel, close -> UserProfileView(chatModel, close) }, 80.dp, padding = PaddingValues(start = 16.dp, end = DEFAULT_PADDING), disabled = stopped) { + ProfilePreview(profile, stopped = stopped) + } + SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.your_chat_profiles), { withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { showSettingsModalWithSearch { it, search -> UserProfilesView(it, search, profileHidden, drawerState) } } }, disabled = stopped) + SettingsActionItem(painterResource(MR.images.ic_qr_code), stringResource(MR.strings.your_simplex_contact_address), showCustomModal { it, close -> UserAddressView(it, shareViaProfile = it.currentUser.value!!.addressShared, close = close) }, disabled = stopped) + ChatPreferencesItem(showCustomModal, stopped = stopped) + } else if (chatModel.localUserCreated.value == false) { + SettingsActionItem(painterResource(MR.images.ic_manage_accounts), stringResource(MR.strings.create_chat_profile), { + withAuth(generalGetString(MR.strings.auth_open_chat_profiles), generalGetString(MR.strings.auth_log_in_using_credential)) { + ModalManager.center.showModalCloseable { close -> + LaunchedEffect(Unit) { + closeSettings() + } + CreateProfile(chatModel, close) } - CreateProfile(chatModel, close) - } } }, disabled = stopped, extraPadding = true) - } - if (appPlatform.isDesktop) { - SettingsActionItem(painterResource(MR.images.ic_smartphone), stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles), showModal { ConnectMobileView() }, disabled = stopped, extraPadding = true) - } else { - SettingsActionItem(painterResource(MR.images.ic_desktop), stringResource(MR.strings.settings_section_title_use_from_desktop), showCustomModal{ it, close -> ConnectDesktopView(close) }, disabled = stopped, extraPadding = true) - } - SettingsActionItem(painterResource(MR.images.ic_ios_share), stringResource(MR.strings.migrate_from_device_to_another_device), { withAuth(generalGetString(MR.strings.auth_open_migration_to_another_device), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.fullscreen.showCustomModal { close -> MigrateFromDeviceView(close) } }}, disabled = stopped, extraPadding = true) + } + }, disabled = stopped) } - SectionDividerSpaced() - - SectionView(stringResource(MR.strings.settings_section_title_settings)) { - SettingsActionItem(painterResource(if (notificationsMode.value == NotificationsMode.OFF) MR.images.ic_bolt_off else MR.images.ic_bolt), stringResource(MR.strings.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showSettingsModal { NetworkAndServersView() }, disabled = stopped, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it, showSettingsModal) }, extraPadding = true) - DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped) + if (appPlatform.isDesktop) { + SettingsActionItem(painterResource(MR.images.ic_smartphone), stringResource(if (remember { chatModel.remoteHosts }.isEmpty()) MR.strings.link_a_mobile else MR.strings.linked_mobiles), showModal { ConnectMobileView() }, disabled = stopped) + } else { + SettingsActionItem(painterResource(MR.images.ic_desktop), stringResource(MR.strings.settings_section_title_use_from_desktop), showCustomModal { it, close -> ConnectDesktopView(close) }, disabled = stopped) } - SectionDividerSpaced() - - SectionView(stringResource(MR.strings.settings_section_title_help)) { - SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName ?: "") }, disabled = stopped, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_add), stringResource(MR.strings.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close) }, disabled = stopped, extraPadding = true) - SettingsActionItem(painterResource(MR.images.ic_info), stringResource(MR.strings.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) }, extraPadding = true) - if (!chatModel.desktopNoUserNoRemote) { - SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openVerifiedSimplexUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped, extraPadding = true) - } - SettingsActionItem(painterResource(MR.images.ic_mail), stringResource(MR.strings.send_us_an_email), { uriHandler.openUriCatching("mailto:chat@simplex.chat") }, textColor = MaterialTheme.colors.primary, extraPadding = true) - } - SectionDividerSpaced() - - SectionView(stringResource(MR.strings.settings_section_title_support)) { - ContributeItem(uriHandler) - RateAppItem(uriHandler) - StarOnGithubItem(uriHandler) - } - SectionDividerSpaced() - - SettingsSectionApp(showSettingsModal, showCustomModal, showVersion, withAuth) - SectionBottomSpacer() + SettingsActionItem(painterResource(MR.images.ic_ios_share), stringResource(MR.strings.migrate_from_device_to_another_device), { withAuth(generalGetString(MR.strings.auth_open_migration_to_another_device), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.fullscreen.showCustomModal { close -> MigrateFromDeviceView(close) } } }, disabled = stopped) } - if (appPlatform.isDesktop) { - Box( - Modifier - .fillMaxWidth() - .background(MaterialTheme.colors.background) - .background(if (isInDarkTheme()) ToolbarDark else ToolbarLight) - .padding(start = 4.dp, top = 8.dp) - ) { - NavigationButtonBack(closeSettings) - } + SectionDividerSpaced() + + SectionView(stringResource(MR.strings.settings_section_title_settings)) { + SettingsActionItem(painterResource(if (notificationsMode.value == NotificationsMode.OFF) MR.images.ic_bolt_off else MR.images.ic_bolt), stringResource(MR.strings.notifications), showSettingsModal { NotificationsSettingsView(it) }, disabled = stopped) + SettingsActionItem(painterResource(MR.images.ic_wifi_tethering), stringResource(MR.strings.network_and_servers), showSettingsModal { NetworkAndServersView() }, disabled = stopped) + SettingsActionItem(painterResource(MR.images.ic_videocam), stringResource(MR.strings.settings_audio_video_calls), showSettingsModal { CallSettingsView(it, showModal) }, disabled = stopped) + SettingsActionItem(painterResource(MR.images.ic_lock), stringResource(MR.strings.privacy_and_security), showSettingsModal { PrivacySettingsView(it, showSettingsModal, setPerformLA) }, disabled = stopped) + SettingsActionItem(painterResource(MR.images.ic_light_mode), stringResource(MR.strings.appearance_settings), showSettingsModal { AppearanceView(it) }) + DatabaseItem(encrypted, passphraseSaved, showSettingsModal { DatabaseView(it, showSettingsModal) }, stopped) } + SectionDividerSpaced() + + SectionView(stringResource(MR.strings.settings_section_title_help)) { + SettingsActionItem(painterResource(MR.images.ic_help), stringResource(MR.strings.how_to_use_simplex_chat), showModal { HelpView(userDisplayName ?: "") }, disabled = stopped) + SettingsActionItem(painterResource(MR.images.ic_add), stringResource(MR.strings.whats_new), showCustomModal { _, close -> WhatsNewView(viaSettings = true, close) }, disabled = stopped) + SettingsActionItem(painterResource(MR.images.ic_info), stringResource(MR.strings.about_simplex_chat), showModal { SimpleXInfo(it, onboarding = false) }) + if (!chatModel.desktopNoUserNoRemote) { + SettingsActionItem(painterResource(MR.images.ic_tag), stringResource(MR.strings.chat_with_the_founder), { uriHandler.openVerifiedSimplexUri(simplexTeamUri) }, textColor = MaterialTheme.colors.primary, disabled = stopped) + } + SettingsActionItem(painterResource(MR.images.ic_mail), stringResource(MR.strings.send_us_an_email), { uriHandler.openUriCatching("mailto:chat@simplex.chat") }, textColor = MaterialTheme.colors.primary) + } + SectionDividerSpaced() + + SectionView(stringResource(MR.strings.settings_section_title_support)) { + ContributeItem(uriHandler) + RateAppItem(uriHandler) + StarOnGithubItem(uriHandler) + } + SectionDividerSpaced() + + SettingsSectionApp(showSettingsModal, showCustomModal, showVersion, withAuth) + SectionBottomSpacer() } } @@ -193,18 +189,19 @@ expect fun SettingsSectionApp( ) @Composable private fun DatabaseItem(encrypted: Boolean, saved: Boolean, openDatabaseView: () -> Unit, stopped: Boolean) { - SectionItemViewWithIcon(openDatabaseView) { + SectionItemView(openDatabaseView) { Row( Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { - Row(Modifier.weight(1f)) { + Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) { Icon( painterResource(MR.images.ic_database), contentDescription = stringResource(MR.strings.database_passphrase_and_export), tint = if (encrypted && (appPlatform.isAndroid || !saved)) MaterialTheme.colors.secondary else WarningOrange, ) - TextIconSpaced(true) + TextIconSpaced(false) Text(stringResource(MR.strings.database_passphrase_and_export)) } if (stopped) { @@ -228,8 +225,7 @@ expect fun SettingsSectionApp( PreferencesView(m, m.currentUser.value ?: return@showCustomModal, close) }() }), - disabled = stopped, - extraPadding = true + disabled = stopped ) } @@ -244,27 +240,26 @@ fun ChatLockItem( click = showSettingsModal { SimplexLockView(ChatModel, currentLAMode, setPerformLA) }, icon = if (performLA.value) painterResource(MR.images.ic_lock_filled) else painterResource(MR.images.ic_lock), text = stringResource(MR.strings.chat_lock), - iconColor = if (performLA.value) SimplexGreen else MaterialTheme.colors.secondary, - extraPadding = false, + iconColor = if (performLA.value) SimplexGreen else MaterialTheme.colors.secondary ) { Text(if (performLA.value) remember { currentLAMode.state }.value.text else generalGetString(MR.strings.la_mode_off), color = MaterialTheme.colors.secondary) } } @Composable private fun ContributeItem(uriHandler: UriHandler) { - SectionItemViewWithIcon({ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat#contribute") }) { + SectionItemView({ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat#contribute") }) { Icon( painterResource(MR.images.ic_keyboard), contentDescription = "GitHub", tint = MaterialTheme.colors.secondary, ) - TextIconSpaced(extraPadding = true) + TextIconSpaced() Text(generalGetString(MR.strings.contribute), color = MaterialTheme.colors.primary) } } @Composable private fun RateAppItem(uriHandler: UriHandler) { - SectionItemViewWithIcon({ + SectionItemView({ runCatching { uriHandler.openUriCatching("market://details?id=chat.simplex.app") } .onFailure { uriHandler.openUriCatching("https://play.google.com/store/apps/details?id=chat.simplex.app") } } @@ -274,19 +269,19 @@ fun ChatLockItem( contentDescription = "Google Play", tint = MaterialTheme.colors.secondary, ) - TextIconSpaced(extraPadding = true) + TextIconSpaced() Text(generalGetString(MR.strings.rate_the_app), color = MaterialTheme.colors.primary) } } @Composable private fun StarOnGithubItem(uriHandler: UriHandler) { - SectionItemViewWithIcon({ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat") }) { + SectionItemView({ uriHandler.openUriCatching("https://github.com/simplex-chat/simplex-chat") }) { Icon( painter = painterResource(MR.images.ic_github), contentDescription = "GitHub", tint = MaterialTheme.colors.secondary, ) - TextIconSpaced(extraPadding = true) + TextIconSpaced() Text(generalGetString(MR.strings.star_on_github), color = MaterialTheme.colors.primary) } } @@ -304,7 +299,7 @@ fun ChatLockItem( } @Composable fun TerminalAlwaysVisibleItem(pref: SharedPreference, onChange: (Boolean) -> Unit) { - SettingsActionItemWithContent(painterResource(MR.images.ic_engineering), stringResource(MR.strings.terminal_always_visible), extraPadding = false) { + SettingsActionItemWithContent(painterResource(MR.images.ic_engineering), stringResource(MR.strings.terminal_always_visible)) { DefaultSwitch( checked = remember { pref.state }.value, onCheckedChange = onChange, @@ -324,9 +319,34 @@ fun ChatLockItem( } } +@Composable fun ResetHintsItem(unchangedHints: MutableState) { + SectionItemView({ + resetHintPreferences() + unchangedHints.value = true + }, disabled = unchangedHints.value) { + Icon( + painter = painterResource(MR.images.ic_lightbulb), + contentDescription = "Lightbulb", + tint = MaterialTheme.colors.secondary, + ) + TextIconSpaced() + Text(generalGetString(MR.strings.reset_all_hints), color = if (unchangedHints.value) MaterialTheme.colors.secondary else MaterialTheme.colors.primary) + } +} + +private fun resetHintPreferences() { + for ((pref, def) in appPreferences.hintPreferences) { + pref.set(def) + } +} + +fun unchangedHintPreferences(): Boolean = appPreferences.hintPreferences.all { (pref, def) -> + pref.state.value == def +} + @Composable fun AppVersionItem(showVersion: () -> Unit) { - SectionItemViewWithIcon(showVersion) { AppVersionText() } + SectionItemView(showVersion) { AppVersionText() } } @Composable fun AppVersionText() { @@ -413,13 +433,15 @@ fun SettingsPreferenceItem( @Composable fun PreferenceToggle( text: String, + disabled: Boolean = false, checked: Boolean, onChange: (Boolean) -> Unit = {}, ) { - SettingsActionItemWithContent(null, text, extraPadding = true,) { + SettingsActionItemWithContent(null, text, disabled = disabled) { DefaultSwitch( checked = checked, onCheckedChange = onChange, + enabled = !disabled ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt index 63baece5cf..1ac0cd7ecd 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressLearnMore.kt @@ -13,11 +13,12 @@ import chat.simplex.res.MR @Composable fun UserAddressLearnMore() { - ColumnWithScrollBar(Modifier - .fillMaxHeight() - .padding(horizontal = DEFAULT_PADDING) + ColumnWithScrollBar( + Modifier + .fillMaxHeight() + .padding(horizontal = DEFAULT_PADDING) ) { - AppBarTitle(stringResource(MR.strings.simplex_address)) + AppBarTitle(stringResource(MR.strings.simplex_address), withPadding = false) ReadableText(MR.strings.you_can_share_your_address) ReadableText(MR.strings.you_wont_lose_your_contacts_if_delete_address) ReadableText(MR.strings.you_can_accept_or_reject_connection) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt index b595bd4e0e..b357272e16 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt @@ -174,7 +174,7 @@ private fun UserAddressLayout( saveAas: (AutoAcceptState, MutableState) -> Unit, ) { ColumnWithScrollBar { - AppBarTitle(stringResource(MR.strings.simplex_address), hostDevice(user?.remoteHostId), withPadding = false) + AppBarTitle(stringResource(MR.strings.simplex_address), hostDevice(user?.remoteHostId)) Column( Modifier.fillMaxWidth().padding(bottom = DEFAULT_PADDING_HALF), horizontalAlignment = Alignment.CenterHorizontally, @@ -185,7 +185,7 @@ private fun UserAddressLayout( CreateAddressButton(createAddress) SectionTextFooter(stringResource(MR.strings.create_address_and_let_people_connect)) } - SectionDividerSpaced(maxBottomPadding = false) + SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = false) SectionView { LearnMoreButton(learnMore) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt index b40cc7db92..4a9af6e822 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserProfilesView.kt @@ -4,6 +4,7 @@ import SectionBottomSpacer import SectionDivider import SectionItemView import SectionItemViewSpaceBetween +import SectionItemViewWithoutMinPadding import SectionSpacer import SectionTextFooter import SectionView @@ -277,7 +278,7 @@ private fun ProfileActionView(action: UserProfileAction, user: User, doAction: ( @Composable fun PasswordAndAction(label: StringResource, color: Color = MaterialTheme.colors.primary) { SectionView() { - SectionItemView { + SectionItemViewWithoutMinPadding { PassphraseField(actionPassword, generalGetString(MR.strings.profile_password), isValid = { passwordValid }, showStrength = true) } SectionItemViewSpaceBetween({ doAction(actionPassword.value) }, disabled = !actionEnabled, minHeight = TextFieldDefaults.MinHeight) { @@ -307,7 +308,7 @@ private fun filteredUsers(m: ChatModel, searchTextOrPassword: String): List - if ((u.user.activeUser || !u.user.hidden) && (s == "" || u.user.chatViewName.lowercase().contains(lower))) { + if ((u.user.activeUser || !u.user.hidden) && (s == "" || u.user.anyNameContains(lower))) { true } else { correctPassword(u.user, s) @@ -367,6 +368,7 @@ private suspend fun doRemoveUser(m: ChatModel, user: User, users: List, de } } m.removeUser(user) + ntfManager.cancelNotificationsForUser(user.userId) } catch (e: Exception) { AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_deleting_user), e.stackTraceToString()) } diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml index 52dd8e6479..7fb812ade9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ar/strings.xml @@ -15,7 +15,8 @@ لا يمكن التراجع عن هذا الإجراء - سيتم فقد ملف التعريف وجهات الاتصال والرسائل والملفات الخاصة بك بشكل نهائي. هذه المجموعة لم تعد موجودة. رمز QR هذا ليس رابطًا! - الجيل القادم من الرسائل الخاصة + الجيل القادم من +\nالرسائل الخاصة لا يمكن التراجع عن هذا الإجراء - سيتم حذف جميع الملفات والوسائط المستلمة والمرسلة. ستبقى الصور منخفضة الدقة. لا يمكن التراجع عن هذا الإجراء - سيتم حذف الرسائل المرسلة والمستلمة قبل التحديد. قد تأخذ عدة دقائق. ينطبق هذا الإعداد على الرسائل الموجودة في ملف تعريف الدردشة الحالي الخاص بك @@ -31,7 +32,7 @@ أضِف إلى جهاز آخر سيتم حذف جميع الدردشات والرسائل - لا يمكن التراجع عن هذا! الوصول إلى الخوادم عبر بروكسي SOCKS على المنفذ %d؟ يجب بدء تشغيل الوكيل قبل تمكين هذا الخيار. - أضِف خادم… + أضِف خادم إعدادات الشبكة المتقدمة سيبقى جميع أعضاء المجموعة على اتصال. السماح باختفاء الرسائل فقط إذا سمحت جهة اتصالك بذلك. @@ -97,11 +98,11 @@ يمكنك أنت وجهة اتصالك إرسال رسائل تختفي. مكالمتك تحت الإجراء لا يمكّن استلام الملف - جيد للبطارية . خدمة الخلفية تتحقق من الرسائل كل 10 دقائق. قد تفوتك مكالمات أو رسائل عاجلة.]]> + جيد للبطارية. يتحقق التطبيق من الرسائل كل 10 دقائق. قد تفوتك مكالمات أو رسائل عاجلة.]]> عريض مكالمات الصوت (ليست مُعمّاة بين الطرفين) الأفضل للبطارية . ستستلم إشعارات فقط عندما يكون التطبيق قيد التشغيل (لا توجد خدمة في الخلفية).]]> - تستهلك المزيد من البطارية ! تعمل خدمة الخلفية دائمًا - تظهر الإشعارات بمجرد توفر الرسائل.]]> + يستهلك المزيد من البطارية! يعمل التطبيق دائمًا في الخلفية - يتم عرض الإشعارات على الفور.]]> انتهت المكالمة بالفعل! تجزئة رسالة سيئة معرّف رسالة سيئ @@ -295,7 +296,7 @@ المنشئ خطأ في إضافة الأعضاء خطأ في إنشاء العنوان - خطأ في حذف اتصال جهة الاتصال المعلق + خطأ في حذف اتصال جهة الاتصال المنتظر أدخل رسالة ترحيب… متصل جار الاتصال @@ -318,7 +319,7 @@ ملون لدى جهة الاتصال التعمية بين الطريفين إنشاء - إنشاء حسابك الشخصي + إنشاء ملف تعريف مكالمة جارية... تفعيل التدمير الذاتي الموافقة على التعمية… @@ -334,7 +335,7 @@ خطأ في تغيير الدور %1$d فشل فك تعمية الرسائل. سمة داكنة - حُذِفت + حُذفت عبارة مرور قاعدة البيانات وتصديرها حذف جميع الملفات حذف بعد @@ -353,8 +354,8 @@ حذف المجموعة حذف المجموعة؟ حذف الرابط - حُذِفت في: %s - المجموعة المحذوفة + حُذفت في: %s + المجموعة حُذفت حذف الصورة تخصيص السمات حذف قاعدة البيانات @@ -398,7 +399,7 @@ عبارة المرور الحالية… سيتم تحديث عبارة مرور تعمية قاعدة البيانات وتخزينها في Keystore. معرّف قاعدة البيانات - حُذِفت في + حُذفت في %d يوم %d أيام مخصص @@ -412,7 +413,7 @@ حذف الخادم خطأ في تحديث رابط المجموعة الوصف - توسيع اختيار الدور + توسيع تحديد الدور انتهت صلاحية دعوة المجموعة المجموعة غير موجودة! تصدير السمة @@ -513,7 +514,7 @@ إخفاء شاشة التطبيق في التطبيقات الحديثة. تحسن الخصوصية والأمان مسح رمز QR في مكالمة الفيديو، أو يمكن لجهة الاتصال مشاركة رابط الدعوة.]]> - محصن ضد البريد العشوائي وسوء المعاملة + محصن ضد الإزعاج (spam) التخفي عبر رابط لمرة واحدة أرسلت صورة صورة @@ -743,7 +744,7 @@ كتم ردود الفعل الرسائل ممنوعة في هذه المجموعة. المزيد - إعدادات الشبكة + إعدادات متقدّمة مكالمة فائتة مسودة الرسالة ملفات تعريف دردشة متعددة @@ -773,7 +774,6 @@ أرشيف قاعدة البيانات القديمة غير مفعّل رابط دعوة لمرة واحدة - سوف تكون مضيفات البصل مطلوبة للاتصال. المراقب لا يوجد نص دور عضو جديد @@ -789,11 +789,9 @@ لا سوف تكون مضيفات البصل مطلوبة للاتصال. \nيُرجى ملاحظة: أنك لن تتمكن من الاتصال بالخوادم بدون عنوان onion. - سيتم استخدام مضيفات البصل عند توفرها. - لن يتم استخدام مضيفات البصل. اسم عرض جديد: عبارة مرور جديدة… - يرجى الانتظار + قيد الانتظار كلمة المرور مطلوبة ألصِق الرابط الذي استلمته فقط مالكي المجموعة يمكنهم تفعيل الملفات والوسائط. @@ -824,13 +822,13 @@ افتح وحدة تحكم الدردشة إدخال كلمة المرور افتح SimpleX Chat للرد على المكالمة - بروتوكول وكود مفتوح المصدر - يمكن لأي شخص تشغيل الخوادم. + يمكن لأي شخص استضافة الخوادم. كلمة المرور للإظهار ندّ لِندّ - يمكن للناس التواصل معك فقط عبر الرابط الذي تقوم بمشاركته - مكالمة في الانتظار + أنت تقرر من يمكنه الاتصال. + مكالمة قيد الانتظار تعمية ثنائية الطبقات من بين الطريفين.]]> - إعادة تعيين الألوان + صفّر الألوان حفظ عنوان الخادم المحدد مسبقًا حفظ وإشعار أعضاء المجموعة @@ -847,7 +845,6 @@ افتح ملفات تعريف الدردشة اسحب الوصول حفظ الأرشيف - حفظ اللون كشف سيتم إيقاف استلام الملف. رفض @@ -893,7 +890,6 @@ حٌديثت السجل في حٌديثت السجل في: %s استعادة - إرجاع يرى المستلمون التحديثات أثناء كتابتها. استلمت، ممنوع حفظ @@ -911,7 +907,7 @@ استلمت في إزالة العضو إزالة - إعادة التعيين إلى الإعدادات الافتراضية + صفّر إلى الإعدادات الافتراضية بينج الفاصل الزمني كلمة مرور الملف الشخصي منع إرسال الرسائل التي تختفي. @@ -939,7 +935,7 @@ يرجى مطالبة جهة اتصالك بتفعيل إرسال الرسائل الصوتية. العنصر النائب لصورة الملف الشخصي رمز QR - إعادة التعيين + صفّر المنفذ %d خادم محدد مسبقًا قراءة المزيد في مستودعنا على GitHub. @@ -982,7 +978,7 @@ مشاركة إرسال حفظ كلمة المرور وفتح الدردشة - اختيار جهات اتصال + حدد جهات اتصال تعيين يوم واحد ثواني رسالة مرسلة @@ -1002,7 +998,7 @@ تم تغيير رمز الأمان تقارير الارسال تم إرساله في - اختيار + حدد إرسال تقارير الاستلام سيتم تفعيله لجميع جهات الاتصال. سيتم تفعيل إرسال تقارير الاستلام لجميع جهات الاتصال ذات حسابات دردشة ظاهرة قائمة انتظار آمنة @@ -1043,7 +1039,7 @@ تخطي دعوة الأعضاء إيقاف الدردشة؟ عرض - حدثت بعض الأخطاء غير الفادحة أثناء الاستيراد - قد ترى وحدة تحكم الدردشة لمزيد من التفاصيل. + حدثت بعض الأخطاء غير الفادحة أثناء الاستيراد: وكيل SOCKS تم تدقيق أمان SimpleX Chat بواسطة Trail of Bits. إيقاف @@ -1112,7 +1108,7 @@ للتواصل عبر الرابط للاتصال، يمكن لجهة الاتصال مسح رمز QR أو استخدام الرابط في التطبيق. خوادم الاختبار - المنصة الأولى بدون أي معرفات للمستخدم - صمّمناه ليكون خاصًا. + لا معرّفات مُستخدم دعم SIMPLEX CHAT تبديل العنوان الرئيسي @@ -1124,7 +1120,7 @@ السمات بفضل المستخدمين - المساهمة عبر Weblate! قاعدة البيانات لا تعمل بشكل صحيح. انقر لمعرفة المزيد - ألوان السمة + ألوان الواجهة انقر لتنشيط الملف الشخصي. عزل النقل هذه السلسلة ليست رابط اتصال! @@ -1166,7 +1162,6 @@ ملفات تعريف الدردشة الخاصة بك عنوان SimpleX الخاص بك خوادم SMP الخاصة بك - هل تريد تحديث إعداد مضيفي onion.؟ عندما يكون التطبيق قيد التشغيل عبر المُرحل لقد انضممت إلى هذه المجموعة @@ -1404,7 +1399,7 @@ سيتم إخفاء كافة الرسائل الجديدة من %s! محظور حظر أعضاء المجموعة - جهة الاتصال المحذوفة + جهة الاتصال حُذفت أنشِئ مجموعة باستخدام ملف تعريف عشوائي. أنشِئ مجموعة أنشِئ ملف تعريف @@ -1767,4 +1762,309 @@ شكل الصور الشخصية واجهة المستخدم الليتوانية مربع أو دائرة أو أي شيء بينهما. + عنوان الخادم غير متوافق مع إعدادات الشبكة. + إصدار الخادم غير متوافق مع إعدادات الشبكة. + مفتاح خاطئ أو اتصال غير معروف - على الأرجح حُذف هذا الاتصال. + تم تجاوز السعة - لم يتلق المُستلم الرسائل المُرسلة مسبقًا. + خطأ في خادم الوجهة: %1$s + خطأ: %1$s + خادم إعادة التوجيه: %1$s +\nخطأ في الخادم الوجهة: %2$s + خادم إعادة التوجيه: %1$s +\nخطأ: %2$s + تحذير تسليم الرسالة + مشكلات الشبكة - انتهت صلاحية الرسالة بعد عِدة محاولات لإرسالها. + نعم + لحماية عنوان IP الخاص بك، يستخدم التوجيه الخاص خوادم SMP الخاصة بك لتسليم الرسائل. + توجيه الرسائل الخاصة + التوجيه الخاص + غير محمي + السماح بالرجوع إلى إصدار سابق + لا تستخدم التوجيه الخاص. + وضع توجيه الرسائل + لا + عندما يكون IP مخفيًا + لا ترسل رسائل مباشرةً، حتى لو كان خادمك أو خادم الوجهة لا يدعم التوجيه الخاص. + أرسل الرسائل مباشرة عندما لا يدعم الخادم الوجهة الخاص بك أو الخادم الوجهة التوجيه الخاص. + احتياطي توجيه الرسالة + أظهِر حالة الرسالة + احمِ عنوان IP + بدون تور أو VPN، سيكون عنوان IP الخاص بك مرئيًا لخوادم الملفات. + استخدم التوجيه الخاص مع خوادم غير معروفة. + استخدم التوجيه الخاص مع خوادم غير معروفة عندما لا يكون عنوان IP محميًا. + دائمًا + استخدم دائمًا التوجيه الخاص. + الملفات + مطلقًا + سيطلب التطبيق تأكيد التنزيلات من خوادم ملفات غير معروفة (باستثناء .onion أو عند تفعيل وكيل SOCKS). + أرسل الرسائل مباشرة عندما يكون عنوان IP محميًا ولا يدعم الخادم الوجهة لديك التوجيه الخاص. + خوادم غير معروفة + خوادم غير معروفة! + بدون تور أو VPN، سيكون عنوان IP الخاص بك مرئيًا لمُرحلات XFTP هذه: +\n%1$s. + أظهِر قائمة الدردشة في نافذة جديدة + ألوان الدردشة + سمة الدردشة + تلقى الرد + أزِل الصورة + تكرار + صفّر اللون + أرسلت رد + تعيين السمة الافتراضية + النظام + لون تمييز خلفية الشاشة + لون إضافي ثانوي 2 + الإعدادات المتقدمة + جميع أوضاع الألوان + أسود + وضع اللون + داكن + الوضع الداكن + ألوان الوضع الداكن + ملائمة + طاب يومك! + صباح الخير! + صورة خلفية الشاشة + الوضع الفاتح + السمة الملف الشخصي + فاتح + طبّق لِ + ملء + المقياس + لا شيء + توجيه الرسائل الخاصة 🚀 + اجعل محادثاتك تبدو مختلفة! + تلقي الملفات بأمان + واجهة المستخدم الفارسية + صفّر إلى سمة التطبيق + سمة التطبيق + تأكيد الملفات من خوادم غير معروفة. + صفّر إلى سمة المستخدم + معلومات قائمة انتظار الخادم: %1$s +\n +\nآخر رسالة تم استلامها: %2$s + تسليم التصحيح + معلومات قائمة انتظار الرسائل + احمِ عنوان IP الخاص بك من مُرحلات المُراسلة التي اختارتها جهات الاتصال الخاصة بك. +\nفعّل في إعدادات *الشبكة والخوادم*. + سمات دردشة جديدة + حدث خطأ أثناء تهيئة WebView. حدّث نظامك إلى الإصدار الجديد. يُرجى التواصل بالمطورين. +\nError: %s + تحسين تسليم الرسائل + مع انخفاض استخدام البطارية. + مفتاح خاطئ أو عنوان مجموعة الملف غير معروف - على الأرجح حُذف الملف. + خطأ في خادم الملفات: %1$s + خطأ في الملف + خطأ في الملف مؤقت + حالة الرسالة + لم يتم العثور على الملف - على الأرجح حُذف الملف أو إلغاؤه. + حالة الملف + حالة الملف: %s + حالة الرسالة: %s + خطأ في النسخ + تم استخدام هذا الرابط مع جهاز محمول آخر، يُرجى إنشاء رابط جديد على سطح المكتب. + يُرجى التحقق من اتصال الهاتف المحمول وسطح المكتب بنفس الشبكة المحلية، وأن جدار حماية سطح المكتب يسمح بالاتصال. +\nيُرجى مشاركة أي مشاكل أُخرى مع المطورين. + لا يمكن إرسال الرسالة + تفضيلات الدردشة المحدّدة تحظر هذه الرسالة. + التفاصيل + بدءًا من %s. +\nجميع البيانات خاصة بجهازك. + أرسلت الإجمالي + الحجم + الملفات المرفوعة + يُرجى المحاولة لاحقا. + خطأ في التوجيه الخاص + عنوان الخادم غير متوافق مع إعدادات الشبكة: %1$s. + إصدار الخادم غير متوافق مع تطبيقك: %1$s. + العضو غير نشط + رسالة محوّلة + لا يوجد اتصال مباشر حتى الآن، يتم تحويل من قِبل المشرف. + امسح / ألصِق الرابط + خوادم SMP المهيأة + خوادم SMP أخرى + خوادم XFTP المهيأة + خوادم XFTP أخرى + أظهِر النسبة المئوية + مُعطّل + مستقرّ + يتوفر تحديث: %s + التمس التحديثات + نزّل %s (%s) + ثُبّت بنجاح + افتح مكان الملف + يُرجى إعادة تشغيل التطبيق. + تذكر لاحقا + تخطي هذه النسخة + أُلغيت تنزيل التحديث + مُعطّل + غير نشط + معلومات الخوادم + عرض المعلومات ل + الأخطاء + الرسائل المُرسلة + الإجمالي + الخوادم المتصلة سابقًا + حدث خطأ أثناء إعادة الاتصال بالخادم + أعِد توصيل الخادم؟ + أعِد التوصيل بالخادم لفرض تسليم الرسالة. يستخدم حركة مرور إضافية. + صفّر جميع الإحصائيات + صفّر جميع الإحصائيات؟ + نُزّلت + الرسائل المُستلمة + الرسائل المُرسلة + سيتم تصفير إحصائيات الخوادم - لا يمكن التراجع عن هذا! + رُفع + صفّر + بدءًا من %s. + خادم SMP + خادم XFTP + معترف به + حُذفت القطع + نُزّلت القطع + اكتملت + الاتصالات + أُنشئت + أخطاء فك التعمية + حُذفت + الملفات التي نُزّلت + أخطاء التنزيل + منتهية الصلاحيّة + افتح إعدادات الخادم + أخرى + موّكل + مؤمن + أرسِل الأخطاء + أُرسلت مباشرةً + مُرسَل عبر الوكيل + مشترك + أخطاء الاشتراك + رفع الأخطاء + التمس التحديثات + أخطاء معترف بها + نُزّل تحديث التطبيق + جميع ملفات التعريف + المحاولات + تجريبي + رُفع القطع + متصل + الخوادم المتصلة + جارِ الاتصال + الاتصالات النشطة + ملف التعريف الحالي + أخطاء الحذف + إحصائيات مفصلة + عطّل + خطأ في تصفير الإحصائيات + التكرارات + خطأ + حدث خطأ أثناء إعادة الاتصال بالخوادم + جارٍ تنزيل تحديث التطبيق، لا تغلق التطبيق + الملفات + حجم الخط + ثبّت التحديث + قد يتم تسليم الرسالة لاحقًا إذا أصبح العضو نشطًا. + الرسائل المُستلمة + استقبال الرسائل + لا توجد معلومات، حاول إعادة التحميل + أخطاء أخرى + قيد الانتظار + أعِد التوصيل + أعِد توصيل كافة الخوادم المتصلة لفرض تسليم الرسالة. يستخدم حركة مرور إضافية. + خوادم موّكلة + تلقي الأخطاء + تلقى الإجمالي + أعِد توصيل جميع الخوادم + أعِد توصيل الخوادم؟ + عنوان الخادم + الإحصائيات + تم تجاهل الاشتراكات + جلسات النقل + لكي يتم إعلامك بالإصدارات الجديدة، شغّل الفحص الدوري للإصدارات المستقرة أو التجريبية. + أنت غير متصل بهذه الخوادم. يتم استخدام التوجيه الخاص لتسليم الرسائل إليهم. + قرّب + حدث خطأ أثناء الاتصال بخادم التحويل %1$s. يُرجى المحاولة لاحقا. + عنوان خادم التحويل غير متوافق مع إعدادات الشبكة: %1$s. + عنوان خادم الوجهة %1$s غير متوافق مع إعدادات خادم التحويل %2$s. + إصدار الخادم الوجهة %1$s غير متوافق مع خادم التحويل %2$s. + فشل خادم التحويل %1$s في الاتصال بالخادم الوجهة %2$s. يُرجى المحاولة لاحقا. + إصدار خادم التحويل غير متوافق مع إعدادات الشبكة: %1$s. + مطفي + قوي + تمويه الوسائط + متوسط + ناعم + المكالمة + اتصل + الرسالة + افتح + بحث + الإعدادات + فيديو + تأكيد حذف جهة الاتصال؟ + حُذفت جهة الاتصال! + سيتم حذف جهة الاتصال - لا يمكن التراجع عن هذا! + حُذفت المحادثة! + احذف دون إشعار + أبقِ المحادثة + احذف المحادثة فقط + بإمكانك إرسال رسائل إلى %1$s من جهات الاتصال المؤرشفة. + ألصق الرابط + جهات اتصالك + شريط أدوات الدردشة القابل للوصول + حُذفت جهة الاتصال. + السماح بالمكالمات؟ + أرسل رسالة لتفعيل المكالمات. + المكالمات ممنوعة! + لا يمكن مكالمة أحد أعضاء المجموعة + لا يمكن إرسال رسالة إلى عضو المجموعة + جارِ الاتصال بجهة الاتصال، يُرجى الانتظار أو التحقق لاحقًا! + جهات الاتصال المؤرشفة + ادعُ + لا توجد جهات اتصال مُصفاة + لا يمكن مكالمة جهة الاتصال + لا يزال بإمكانك عرض المحادثة مع %1$s في قائمة الدردشات. + يجب عليك السماح لجهات اتصالك بالاتصال حتى تتمكن من الاتصال بها. + يُرجى الطلب من جهة اتصالك تفعيل المكالمات. + حذف %d رسائل الأعضاء؟ + سيتم وضع علامة على الرسائل للحذف. سيتمكن المستلم/(المستلمون) من الكشف عن هذه الرسائل. + حدد + سيتم حذف الرسائل لجميع الأعضاء. + سيتم وضع علامة على الرسائل على أنها تحت الإشراف لجميع الأعضاء. + الرسالة + لا شيء محدد + محدّد %d + خوادم الوسائط والملفات + خوادم الرسائل + متابعة + وكيل SOCKS + يمكنك ترحيل قاعدة البيانات المُصدرة. + يمكنك حفظ الأرشيف المُصدر. + حالة الاتصال والخوادم. + تواصل مع أصدقائك بشكل أسرع + شريط أدوات الدردشة يمكن الوصول إليه + أرشفة جهات الاتصال للدردشة لاحقًا. + استخدم التطبيق بيد واحدة. + صُدرت قاعدة بيانات الدردشة + التحكم في شبكتك + حذف ما يصل إلى 20 رسالة في وقت واحد. + لم يتم تصدير بعض الملفات + يحمي عنوان IP الخاص بك واتصالاتك. + اتصال TCP + حفظ وإعادة الاتصال + أنشِئ + تجربة دردشة جديدة 🎉 + تمويه من أجل خصوصية أفضل. + كبّر حجم الخط + رسالة جديدة + ادعُ + خيارات الوسائط الجديدة + شغّل من قائمة الدردشة. + تبديل قائمة الدردشة: + يمكنك تغييره في إعدادات المظهر. + نزّل الإصدارات الجديدة من GitHub. + ترقية التطبيق تلقائيًا + صفّر كافة التلميحات + يُرجى التأكد من أن رابط SimpleX صحيح. + الرابط غير صالح \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 6ca14aace2..f5272ce7fc 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -13,6 +13,8 @@ You will connect to all group members. Connect Connect incognito + Invalid link + Please check that SimpleX link is correct. Opening database… @@ -112,6 +114,16 @@ Connection timeout Connection error Please check your network connection with %1$s and try again. + Server address is incompatible with network settings: %1$s. + Server version is incompatible with your app: %1$s. + Private routing error + Error connecting to forwarding server %1$s. Please try later. + Forwarding server address is incompatible with network settings: %1$s. + Forwarding server version is incompatible with network settings: %1$s. + Forwarding server %1$s failed to connect to destination server %2$s. Please try later. + Destination server address of %1$s is incompatible with forwarding server %2$s settings. + Destination server version of %1$s is incompatible with forwarding server %2$s. + Please try later. Error sending message Error creating message Error loading details @@ -119,6 +131,8 @@ Error joining group Cannot receive file Sender cancelled file transfer. + Unknown servers! + Without Tor or VPN, your IP address will be visible to these XFTP relays:\n%1$s. Error receiving file Error creating address Contact already exists @@ -255,8 +269,25 @@ Message delivery error + Message delivery warning Most likely this contact has deleted the connection with you. + + Error: %1$s + Wrong key or unknown connection - most likely this connection is deleted. + Capacity exceeded - recipient did not receive previously sent messages. + Network issues - message expired after many attempts to send it. + Destination server error: %1$s + Forwarding server: %1$s\nError: %2$s + Forwarding server: %1$s\nDestination server error: %2$s + Server address is incompatible with network settings. + Server version is incompatible with network settings. + + + Wrong key or unknown file chunk address - most likely file is deleted. + File not found - most likely file was deleted or cancelled. + File server error: %1$s + Reply Share @@ -282,14 +313,19 @@ Hide Allow Moderate + Select Expand Delete message? Delete %d messages? Message will be deleted - this cannot be undone! Message will be marked for deletion. The recipient(s) will be able to reveal this message. + Messages will be marked for deletion. The recipient(s) will be able to reveal these messages. Delete member message? + Delete %d messages of members? The message will be deleted for all members. + The messages will be deleted for all members. The message will be marked as moderated for all members. + The messages will be marked as moderated for all members. Delete for me For everyone Stop file @@ -305,6 +341,11 @@ Forward Download + Message forwarded + No direct connection yet, message is forwarded by admin. + Member inactive + Message may be delivered later if member becomes active. + edited sent @@ -317,6 +358,7 @@ Welcome! This text is available in settings Chats + Settings connecting… send direct message you are invited to group @@ -333,12 +375,16 @@ No selected chat + Nothing selected + Selected %d Share message… Share media… Share file… Forward message… + Cannot send message + Selected chat preferences prohibit this message. Attach @@ -361,6 +407,7 @@ SimpleX links not allowed Files and media not allowed Voice messages not allowed + Message Image @@ -394,6 +441,8 @@ Error saving file Loading the file Please, wait while the file is being loaded from the linked mobile + File error + Temporary file error Voice message @@ -404,10 +453,25 @@ Notifications + connect + open + message + call + search + video Delete contact? Contact and all messages will be deleted - this cannot be undone! + Contact will be deleted - this cannot be undone! + Keep conversation + Only delete conversation + Confirm contact deletion? Delete and notify contact + Delete without notification Delete contact + Conversation deleted! + You can send messages to %1$s from Archived contacts. + Contact deleted! + You can still view conversation with %1$s in the list of chats. Set contact name… Connected Disconnected @@ -593,7 +657,10 @@ New chat + New message Add contact + Scan / Paste link + Paste link One-time invitation link 1-time link SimpleX address @@ -612,6 +679,10 @@ Invalid QR code The code you scanned is not a SimpleX link QR code. + Archived contacts + No filtered contacts + Your contacts + Scan code Incorrect security code! @@ -639,10 +710,13 @@ Send us email SimpleX Lock Chat console + Message servers SMP servers + Configured SMP servers + Other SMP servers Preset server address Add preset servers - Add server… + Add server Test server Test servers Save servers @@ -661,8 +735,13 @@ Delete server The servers for new connections of your current chat profile Save servers? + Media & file servers XFTP servers + Configured XFTP servers + Other XFTP servers + Show percentage Install SimpleX Chat for terminal + Reset all hints Star on GitHub Contribute Rate the app @@ -681,7 +760,8 @@ Save Network & servers Advanced network settings - Network settings + Advanced settings + SOCKS proxy SOCKS proxy settings Use SOCKS proxy port %d @@ -691,7 +771,6 @@ Access the servers via SOCKS proxy on port %d? Proxy must be started before enabling this option. Use direct Internet connection? If you confirm, the messaging servers will be able to see your IP address, and your provider - which servers you are connecting to. - Update .onion hosts setting? Use .onion hosts When available No @@ -699,9 +778,6 @@ Onion hosts will be used when available. Onion hosts will not be used. Onion hosts will be required for connection.\nPlease note: you will not be able to connect to the servers without .onion address. - Onion hosts will be used when available. - Onion hosts will not be used. - Onion hosts will be required for connection. Transport isolation Chat profile Connection @@ -710,14 +786,53 @@ Update transport isolation mode? Use .onion hosts to No if SOCKS proxy does not support them.]]> Please note: message and file relays are connected via SOCKS proxy. Calls and sending link previews use direct connection.]]> + Private routing + Always + Unknown servers + Unprotected + Never + Always use private routing. + Use private routing with unknown servers. + Use private routing with unknown servers when IP address is not protected. + Do NOT use private routing. + Message routing mode + Allow downgrade + Yes + When IP hidden + No + Send messages directly when your or destination server does not support private routing. + Send messages directly when IP address is protected and your or destination server does not support private routing. + Do NOT send messages directly, even if your or destination server does not support private routing. + Message routing fallback + Show message status + To protect your IP address, private routing uses your SMP servers to deliver messages. Appearance Customize theme - THEME COLORS + INTERFACE COLORS App version App version: v%s App build: %s Core version: v%s simplexmq: v%s (%2s) + Check for updates + Disabled + Stable + Beta + Update available: %s + Download %s (%s) + Skip this version + Downloading app update, don\'t close the app + App update is downloaded + Open file location + Install update + Installed successfully + Please restart the app. + Update download canceled + Remind later + Check for updates + To be notified about the new releases, turn on periodic check for Stable or Beta versions. + Disable + Show: Hide: Show developer options @@ -757,6 +872,7 @@ Don\'t create address You can create it later You can make it visible to your SimpleX contacts via Settings. + Invite Profile name: @@ -791,6 +907,7 @@ Enter your name: Create Create profile + Create Invalid name! Correct name to %s? About SimpleX @@ -840,15 +957,16 @@ Speaker Headphones Bluetooth + Error initializing WebView. Update your system to the new version. Please contact developers.\nError: %s - The next generation of private messaging + The next generation\nof private messaging Privacy redefined - The 1st platform without any user identifiers – private by design. - Immune to spam and abuse - People can connect to you only via the links you share. + No user identifiers. + Immune to spam + You decide who can connect. Decentralized - Open-source protocol and code – anybody can run the servers. + Anybody can host servers. Create your profile Make a private connection Migrate from another device @@ -871,8 +989,8 @@ Periodic Instant Best for battery. You will receive notifications only when the app is running (NO background service).]]> - Good for battery. Background service checks messages every 10 minutes. You may miss calls or urgent messages.]]> - Uses more battery! Background service always runs – notifications are shown as soon as messages are available.]]> + Good for battery. App checks messages every 10 minutes. You may miss calls or urgent messages.]]> + Uses more battery! App always runs in background – notifications are shown instantly.]]> Setup database passphrase @@ -960,6 +1078,9 @@ Protect app screen Encrypt local files Auto-accept images + Protect IP address + The app will ask to confirm downloads from unknown file servers (except .onion or when SOCKS proxy is enabled). + Without Tor or VPN, your IP address will be visible to file servers. Send link previews Show last messages Message draft @@ -1015,6 +1136,11 @@ Disable (keep group overrides) Enable for all groups Disable for all groups + Blur media + Off + Soft + Medium + Strong YOU @@ -1024,17 +1150,23 @@ APP DEVICE CHATS + FILES SEND DELIVERY RECEIPTS TO Restart Shutdown Developer tools Experimental features SOCKS PROXY + INTERFACE LANGUAGE APP ICON THEMES Profile images + Chat theme + Profile theme + Chat colors MESSAGES AND FILES + PRIVATE MESSAGE ROUTING CALLS Network connection Incognito mode @@ -1069,7 +1201,7 @@ Error importing chat database Chat database imported Restart the app to use imported chat database. - Some non-fatal errors occurred during import - you may see Chat console for more details. + Some non-fatal errors occurred during import: Delete chat profile? This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost. Chat database deleted @@ -1095,6 +1227,11 @@ This action cannot be undone - the messages sent and received earlier than selected will be deleted. It may take several minutes. Delete messages Error changing setting + Chat database exported + You may save the exported archive. + You may migrate the exported database. + Some file(s) were not exported + Continue Save passphrase in Keystore @@ -1161,7 +1298,11 @@ Database downgrade Incompatible database version Confirm database upgrades + Reachable chat toolbar + Toggle chat list: + You can change it in Appearance settings. Show console in new window + Show chat list in new window Invalid migration confirmation Upgrade and open chat Downgrade and open chat @@ -1352,12 +1493,16 @@ disabled Receipts are disabled This group has over %1$d members, delivery receipts are not sent. + Invite FOR CONSOLE Local name Database ID + Debug delivery Record updated at + Message status + File status Sent at Created at Received at @@ -1366,6 +1511,8 @@ Disappears at Database ID: %d Record updated at: %s + Message status: %s + File status: %s Sent at: %s Created at: %s Received at: %s @@ -1401,6 +1548,8 @@ Messages from %s will be shown! Blocked by admin blocked + disabled + inactive MEMBER Role Change role @@ -1418,6 +1567,20 @@ Connection direct indirect (%1$s) + Message queue info + none + server queue info: %1$s\n\nlast received msg: %2$s + + Can\'t call contact + Connecting to contact, please wait or check later! + Contact is deleted. + Allow calls? + You need to allow your contact to call to be able to call them. + Calls prohibited! + Please ask your contact to enable calls. + Can\'t call group member + Send message to enable calls. + Can\'t message group member Welcome message @@ -1456,6 +1619,7 @@ Error saving group profile + TCP connection Reset to defaults sec TCP connection timeout @@ -1465,8 +1629,8 @@ PING interval PING count Enable TCP keep-alive - Revert Save + Save and reconnect Update network settings? Updating settings will re-connect the client to all servers. Update @@ -1504,23 +1668,30 @@ When you share an incognito profile with somebody, this profile will be used for the groups they invite you to. + System + Light + Dark System Light Dark SimpleX + Black System Theme + Color mode Dark theme - Save color + Dark mode colors Import theme Import theme error Make sure the file has correct YAML syntax. Export theme to have an example of the theme file structure. Export theme Reset colors + Reset color + App theme Accent Additional accent Secondary @@ -1528,8 +1699,33 @@ Background Menus & alerts Title + Additional accent 2 Sent message + Sent reply Received message + Received reply + Wallpaper background + Wallpaper accent + Remove image + Font size + Zoom + + + Good afternoon! + Good morning! + Scale + Repeat + Fill + Fit + Advanced settings + Reset to app theme + Reset to user theme + Set default theme + Apply to + All color modes + Light mode + Dark mode + You allow @@ -1776,6 +1972,30 @@ Network management More reliable network connection. Lithuanian UI + Private message routing 🚀 + Protect your IP address from the messaging relays chosen by your contacts.\nEnable in *Network & servers* settings. + New chat themes + Make your chats look different! + Safely receive files + Confirm files from unknown servers. + Improved message delivery + With reduced battery usage. + Persian UI + New chat experience 🎉 + New media options + It protects your IP address and connections. + Archive contacts to chat later. + Reachable chat toolbar + Use the app with one hand. + Connect to your friends faster. + Delete up to 20 messages at once. + Play from the chat list. + Blur for better privacy. + Increase font size. + Upgrade app automatically + Download new versions from GitHub. + Control your network + Connection and servers status. seconds @@ -1823,6 +2043,9 @@ Connection stopped %s with the reason: %s]]> Disconnected with the reason: %s + Please check that mobile and desktop are connected to the same local network, and that desktop firewall allows the connection.\nPlease share any other issues with the developers. + This link was used with another mobile device, please create a new link on the desktop. + Copy error Disconnect desktop? Only one device can work at the same time Use from desktop in mobile app and scan QR code.]]> @@ -1972,4 +2195,85 @@ WiFi Wired ethernet Other + + + Servers info + Files + No info, try to reload + Showing info for + All profiles + Current profile + Transport sessions + Connected + Connecting + Errors + Statistics + Messages sent + Messages received + Details + Starting from %s.\nAll data is private to your device. + Message reception + Active connections + Pending + Total + Connected servers + Previously connected servers + Proxied servers + You are not connected to these servers. Private routing is used to deliver messages to them. + Reconnect servers? + Reconnect all connected servers to force message delivery. It uses additional traffic. + Reconnect server? + Reconnect server to force message delivery. It uses additional traffic. + Error reconnecting servers + Error reconnecting server + Error + Reconnect all servers + Reset all statistics + Reset all statistics? + Servers statistics will be reset - this cannot be undone! + Reset + Error resetting statistics + Uploaded + Downloaded + Detailed statistics + Sent messages + Sent total + Received messages + Received total + Receive errors + Starting from %s. + SMP server + XFTP server + Reconnect + attempts + Sent directly + Sent via proxy + Proxied + Send errors + expired + other + duplicates + decryption errors + other errors + Acknowledged + Acknowledgement errors + Connections + Created + Secured + Completed + Deleted + Deletion errors + Subscribed + Subscriptions ignored + Subscription errors + Uploaded files + Size + Chunks uploaded + Upload errors + Chunks deleted + Chunks downloaded + Downloaded files + Download errors + Server address + Open server settings \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml index c087e0d896..d43aee7c8a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/bg/strings.xml @@ -4,7 +4,7 @@ %1$s иска да се свърже с вас чрез 1 минута Добави сървъри чрез сканиране на QR кодове. - Добави сървър… + Добави сървър админ Добави съобщение при посрещане Всички данни се изтриват при въвеждане. @@ -800,13 +800,11 @@ Мрежови настройки Порт порт %d - Няма се използват Onion хостове. Задължително Не За свързване ще са необходими Onion хостове. \nМоля, обърнете внимание: няма да можете да се свържете със сървърите без .onion адрес. Ще се използват Onion хостове, когато са налични. - Ще се използват Onion хостове, когато са налични. Няма се използват Onion хостове. Нека да поговорим в SimpleX Chat Парола за показване @@ -878,7 +876,6 @@ Няма избран чат Известия Известията ще спрат да работят, докато не стартирате отново приложението - За свързване ще са необходими Onion хостове. Само 10 изображения могат да бъдат изпратени едновременно Само собствениците на групата могат да активират файлове и медията. Само собствениците на групата могат да активират гласови съобщения. @@ -1147,10 +1144,8 @@ Високоговорителят е изключен Спрете чата, за да активирате действията с базата данни. Роля - Отмени промените Запази Нулирай цветовете - Запази цвета Вторичен Избери За да започнете нов чат @@ -1226,7 +1221,6 @@ Вашите SMP сървъри Използвай SOCKS прокси Използвай SOCKS прокси\? - Актуализиране на настройката за .onion хостове\? Използване на директна интернет връзка\? Използвай .onion хостове Когато са налични diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/bn/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/bn/strings.xml index 13e322f078..bb448339bd 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/bn/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/bn/strings.xml @@ -28,7 +28,7 @@ আনুষঙ্গিক রং অ্যাডমিনরা গ্রুপে যোগদানের সংযোগ-সূত্র তৈরি করতে পারবেন। QR কোড স্ক্যান করে সার্ভার যুক্ত করুন। - সার্ভার যুক্ত করুন… + সার্ভার যুক্ত করুন ঠিকানা ঠিকানা পরিবর্তন বাতিল করা হবে। বার্তা গ্রহণের পুরনো ঠিকানা ব্যবহার করা হবে। অ্যাপের সকল তথ্য মুছে ফেলা হয়েছে। diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml index cd642f8828..591643a4da 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/cs/strings.xml @@ -8,7 +8,7 @@ Přidat přednastavené servery Pokročilá nastavení sítě Přijmout - Přidat server… + Přidat server Přistupovat k serverům přes SOCKS proxy na portu %d\? Před povolením této možnosti musí být spuštěna proxy. Přijmout Povolit svým kontaktům odesílat mizící zprávy. @@ -55,7 +55,6 @@ Aktualizovat nastavení sítě\? Inkognito Váš náhodný profil - Uložit barvu Obnovit barvu Zbarvení Povolujete @@ -229,7 +228,6 @@ Použít proxy server SOCKS\? Použít přímé připojení k internetu\? Ne - Onion hostitelé nebudou použiti. Chat profil Připojení simplexmq: v%s (%2s) @@ -358,7 +356,6 @@ Nebyl vybrán žádný kontakt Snažíte se pozvat kontakt, se kterým jste sdíleli inkognito profil, do skupiny, ve které používáte svůj hlavní profil Skupina - Vrátit Aktualizací nastavení se klient znovu připojí ke všem serverům. Nastavit 1 den Chyba spojení (AUTH) @@ -630,14 +627,11 @@ Ujistěte se, že adresy serverů WebRTC ICE jsou ve správném formátu, oddělené na řádcích a nejsou duplicitní. Uložit Síť a servery - Aktualizovat nastavení hostitelů .onion\? Použít hostitele .onion Když bude dostupný Povinné Onion hostitelé budou použiti, pokud jsou k dispozici. Onion hostitelé nebudou použiti. - Onion hostitelé budou použiti, pokud jsou k dispozici. - Pro připojení budou vyžadováni Onion hostitelé. Izolace přenosu for each chat profile you have in the app.]]> Oddělit TCP připojení (a SOCKS pověření) bude použito pro všechny kontakty a členy skupin. @@ -1770,4 +1764,101 @@ Kamera a mikrofon SimpleX odkazy jsou v této skupině zakázány. koncovým šifrováním s dokonalým dopředným utajením, odmítnutím a obnovením po vloupání.]]> + Pokročilé nastavení + Všechny barevné režimy + Překročená kapacita - příjemci neobdrží dříve poslané zprávy. + Vždy + Použít na + Potvrdit soubory z neznámých serverů. + Tmavý mód + Téma aplikace + Vždy užít soukromé směrování. + Povolit downgrade + Kopírovat chybu + Barvy chatu + Téma chatu + Další zbarvení 2 + Černé + Mód barvy + Tmavé + Mód tmavých barev + Informace o frontě zpráv + Světlý mód + Upravtesi svůj chat, aby vypadal jinak! + Chyba cílového serveru: %1$s + Chyba: %1$s + Předávací server: %1$s +\nChyba cílového serveru: %2$s + Předávací server: %1$s +\nChyba: %2$s + Upozornění doručování zpráv + Problémy se sítí - zpráva vypršela po mnoha pokusech o odeslání. + Soukromé směrování + Soubor nebyl nalezen - s největší pravděpodobností byl soubor odstraněn nebo zrušen. + Chyba souboru serveru: %1$s + Nelze odeslat zprávu + Chyba souboru + NEpoužívat soukromé směrování. + Záložní směrování zpráv + Režim přeposílání zpráv + Nikdy + Chyba inicializace WebView. Aktualizujte systém na novou verzi. Prosím kontaktujte vývojáře. +\nChyba: %s + Ochrana IP adresy + Status souboru + Status zprávy + Status souboru: %s + Stav zprávy: %s + žádné + Vyplnit + Opakovat + Obnovit uživatelské téma + Obnovit téma aplikace + Bezpečné přijímání souborů + Nové motivy chatu + Soukromé směrování zpráv 🚀 + Chraňte vaši IP adresu před relé zpráv, které jste si vybrali. +\nPovolit v nastavení *Síť & servery*. + Vylepšené doručování zpráv + Perské UI + Prosím zkontrolujte, že mobil a desktop jsou připojeny ke stejné místní síti, a že stolní firewall umožňuje připojení. +\nProsím sdělte jakékoli další problémy vývojářům. + Ne + NEposílejte zprávy přímo, i když váš nebo cílový server nepodporuje soukromé směrování. + SOUBORY + SOUKROMÉ SMĚROVÁNÍ ZPRÁV + Téma profilu + Přijata odpověď + Obnovit barvu + Dobré odpoledne! + Odebrat obrázek + Dobré ráno! + Světlé + Špatný klíč nebo neznámé spojení - pravděpodobně je spojení smazáno. + Špatný klíč nebo neznámá adresa části souboru - soubor je pravděpodobně odstraněn. + Nechráněno + Použít soukromé směrování s neznámými servery. + Použit soukromé směrování s neznámými servery, když IP adresa není chráněna. + Bez Tor nebo VPN bude vaše IP adresa viditelná souborovým serverům. + Ladit doručování + Snížena spotřeba baterie. + Neznámé servery! + Bez Tor nebo VPN bude vaše IP adresa viditelná pro tyto XFTP relé: +\n%1$s. + Když je IP adresa skryta + Ano + Zbarvení tapety + Pozadí tapety + Beta + volání + Rozmazat media + Kontaktu nelze volat + Všechny profily + pokusy + Aktualizace aplikace je stažena + Aktivní spojení + Povolit volání? + Volání zakázáno! + Nelze zavolat člena skupiny + Archivované kontakty \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml index 6f4afec2f3..64b7f9d8f7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/de/strings.xml @@ -74,7 +74,7 @@ Fehler beim Löschen des Kontakts Fehler beim Löschen der Gruppe Fehler beim Löschen der Kontaktanfrage - Fehler beim Löschen der anstehenden Kontaktaufnahme + Fehler beim Löschen der ausstehenden Kontaktaufnahme Fehler beim Wechseln der Empfängeradresse Der Test ist beim Schritt %s fehlgeschlagen. Um Warteschlangen zu erzeugen, benötigt der Server eine Authentifizierung. Bitte überprüfen Sie das Passwort. @@ -157,7 +157,7 @@ Verbergen Erlauben Die Nachricht löschen? - Nachricht wird gelöscht – dies kann nicht rückgängig gemacht werden! + Nachricht wird gelöscht. Dies kann nicht rückgängig gemacht werden! Die Nachricht wird zum Löschen markiert. Der/die Empfänger kann/können diese Nachricht aufdecken. Für mich löschen Für alle @@ -218,7 +218,7 @@ Benachrichtigungen Kontakt löschen? - Der Kontakt und alle Nachrichten werden gelöscht – dies kann nicht rückgängig gemacht werden! + Es wird der Kontakt und alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! Kontakt löschen Kontaktname festlegen… Verbunden @@ -278,7 +278,7 @@ Ablehnen Chatinhalte löschen? - Alle Nachrichten werden gelöscht - dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht. + Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! Die Nachrichten werden NUR bei Ihnen gelöscht. Löschen Chatinhalte löschen Chatinhalte löschen @@ -298,7 +298,8 @@ Die von Ihnen akzeptierte Verbindung wird abgebrochen! Ihr Kontakt ist noch nicht verbunden! - Ihr Kontakt muss online sein, damit die Verbindung hergestellt werden kann.\nSie können diese Verbindung abbrechen und den Kontakt entfernen (und es später nochmals mit einem neuen Link versuchen). + Ihr Kontakt muss online sein, damit die Verbindung hergestellt werden kann. +\nSie können diese Verbindung abbrechen, den Kontakt entfernen und es später nochmals mit einem neuen Link versuchen. möchte sich mit Ihnen verbinden! @@ -355,7 +356,7 @@ SMP-Server Voreingestellte Serveradresse Füge voreingestellte Server hinzu - Füge Server hinzu… + Füge Server hinzu Teste Server Teste alle Server Alle Server speichern @@ -390,23 +391,19 @@ Speichern Netzwerk & Server Erweiterte Netzwerkeinstellungen - Netzwerkeinstellungen + Erweiterte Einstellungen SOCKS-Proxy verwenden? Zugriff auf die Server über SOCKS-Proxy auf Port %d? Der Proxy muss gestartet werden, bevor diese Option aktiviert wird. Direkte Internetverbindung verwenden? Wenn Sie dies bestätigen, können die Messaging-Server Ihre IP-Adresse sowie Ihren Provider sehen und mit welchen Servern Sie sich verbinden. - Einstellung für .onion-Hosts aktualisieren? Verwende .onion-Hosts Wenn verfügbar Nein Erforderlich - Onion-Hosts werden verwendet, wenn sie verfügbar sind. + Wenn Onion-Hosts verfügbar sind, werden sie verwendet. Onion-Hosts werden nicht verwendet. Für die Verbindung werden Onion-Hosts benötigt. \nBitte beachten Sie: Ohne .onion-Adresse können Sie keine Verbindung mit den Servern herstellen. - Onion-Hosts werden verwendet, wenn sie verfügbar sind. - Onion-Hosts werden nicht verwendet. - Für die Verbindung werden Onion-Hosts benötigt. Erscheinungsbild Adresse erstellen @@ -465,13 +462,14 @@ Verbunden Beendet - Die nächste Generation von privatem Messaging + Die nächste Generation +\ndes privaten Messagings Datenschutz neu definiert - Die erste Plattform ohne Benutzerkennungen – privat per Design - Immun gegen Spam und Missbrauch - Verbindungen mit Kontakten sind nur über Links möglich, die Sie oder Ihre Kontakte untereinander teilen. + Keine Benutzerkennungen. + Immun gegen Spam + Sie entscheiden, wer sich mit Ihnen verbinden kann. Dezentral - Open-Source-Protokoll und -Code – Jede Person kann ihre eigenen Server aufsetzen und nutzen. + Jeder kann seine eigenen Server aufsetzen. Erstellen Sie Ihr Profil Stellen Sie eine private Verbindung her Wie es funktioniert @@ -502,7 +500,7 @@ Audio- & Videoanrufe Ihre Anrufe - Über ein Relais verbinden + Immer über ein Relais verbinden Anrufe auf Sperrbildschirm: Akzeptieren Anzeigen @@ -531,7 +529,7 @@ Lautsprecher an Kamera umdrehen - Anstehender Anruf + Ausstehender Anruf Verpasster Anruf Abgelehnter Anruf Anruf wird verbunden @@ -592,20 +590,20 @@ Fehler beim Exportieren der Chat-Datenbank Chat-Datenbank importieren? Ihre aktuelle Chat-Datenbank wird GELÖSCHT und durch die importierte ERSETZT. -\nDiese Aktion kann nicht rückgängig gemacht werden – Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren. +\nDiese Aktion kann nicht rückgängig gemacht werden! Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren. Importieren Fehler beim Löschen der Chat-Datenbank Fehler beim Importieren der Chat-Datenbank Chat-Datenbank importiert Starten Sie die App neu, um die importierte Chat-Datenbank zu verwenden. Chat-Profil löschen? - Diese Aktion kann nicht rückgängig gemacht werden – Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren. + Diese Aktion kann nicht rückgängig gemacht werden! Ihr Profil, Ihre Kontakte, Nachrichten und Dateien gehen unwiderruflich verloren. Chat-Datenbank gelöscht Starten Sie die App neu, um ein neues Chat-Profil zu erstellen. Sie dürfen die neueste Version Ihrer Chat-Datenbank NUR auf einem Gerät verwenden, andernfalls erhalten Sie möglicherweise keine Nachrichten mehr von einigen Ihrer Kontakte. Chat beenden, um Datenbankaktionen zu erlauben. Dateien und Medien löschen? - Diese Aktion kann nicht rückgängig gemacht werden – alle empfangenen und gesendeten Dateien und Medien werden gelöscht. Bilder mit niedriger Auflösung bleiben erhalten. + Diese Aktion kann nicht rückgängig gemacht werden! Es werden alle empfangenen und gesendeten Dateien und Medien gelöscht. Bilder mit niedriger Auflösung bleiben erhalten. Keine empfangenen oder gesendeten Dateien %d Datei(en) mit einem Gesamtspeicherverbrauch von %s nie @@ -615,7 +613,7 @@ %s Sekunde(n) Löschen der Nachrichten Automatisches Löschen von Nachrichten aktivieren? - Diese Aktion kann nicht rückgängig gemacht werden – alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, werden gelöscht. Dieser Vorgang kann mehrere Minuten dauern. + Diese Aktion kann nicht rückgängig gemacht werden! Es werden alle empfangenen und gesendeten Nachrichten, die über den ausgewählten Zeitraum hinaus gehen, gelöscht. Dieser Vorgang kann mehrere Minuten dauern. Nachrichten löschen Fehler beim Ändern der Einstellung @@ -666,7 +664,7 @@ Der Versuch, das Passwort der Datenbank zu ändern, konnte nicht abgeschlossen werden. Datenbanksicherung wiederherstellen Datenbanksicherung wiederherstellen? - Bitte geben Sie das vorherige Passwort ein, nachdem Sie die Datenbanksicherung wiederhergestellt haben. Diese Aktion kann nicht rückgängig gemacht werden. + Bitte geben Sie das vorherige Passwort ein, nachdem Sie die Datenbanksicherung wiederhergestellt haben. Diese Aktion kann nicht rückgängig gemacht werden! Wiederherstellen Fehler bei der Wiederherstellung der Datenbank Das Passwort wurde nicht im Schlüsselbund gefunden. Bitte geben Sie es manuell ein. Das kann passieren, wenn Sie die App-Daten mit einem Backup-Programm wieder hergestellt haben. Bitte nehmen Sie Kontakt mit den Entwicklern auf, wenn das nicht der Fall ist. @@ -766,8 +764,8 @@ Sie: %1$s Gruppe löschen Gruppe löschen? - Die Gruppe wird für alle Mitglieder gelöscht – dies kann nicht rückgängig gemacht werden! - Die Gruppe wird für Sie gelöscht – dies kann nicht rückgängig gemacht werden! + Die Gruppe wird für alle Mitglieder gelöscht. Dies kann nicht rückgängig gemacht werden! + Die Gruppe wird für Sie gelöscht. Dies kann nicht rückgängig gemacht werden! Gruppe verlassen Gruppenprofil bearbeiten Gruppen-Link @@ -786,7 +784,7 @@ Mitglied entfernen Direktnachricht senden - Das Mitglied wird aus der Gruppe entfernt – dies kann nicht rückgängig gemacht werden! + Das Mitglied wird aus der Gruppe entfernt. Dies kann nicht rückgängig gemacht werden! Entfernen MITGLIED Rolle @@ -825,7 +823,6 @@ Protokollzeitüberschreitung PING-Intervall TCP-Keep-Alive aktivieren - Zurückkehren Speichern Netzwerkeinstellungen aktualisieren? Die Aktualisierung der Einstellungen wird den Client wieder mit allen Servern verbinden. @@ -842,7 +839,6 @@ Dunkel Design - Farbe speichern Farben zurücksetzen Akzent @@ -896,7 +892,7 @@ LIVE Schauen Sie sich den Sicherheitscode an Sofort - Gute Option für die Batterieausdauer. Der Hintergrundservice überprüft alle 10 Minuten nach Nachrichten. Sie können eventuell Anrufe oder dringende Nachrichten verpassen.]]> + Gute Option für die Batterieausdauer. Die App prüft alle 10 Minuten auf neue Nachrichten. Sie können eventuell Anrufe oder dringende Nachrichten verpassen.]]> Beste Option für die Akkulaufzeit. Sie empfangen Benachrichtigungen nur, solange die App läuft (kein aktiver Hintergrundservice).]]> Senden %s wurde erfolgreich überprüft @@ -933,7 +929,7 @@ Fehler beim Laden des Chats Fehler beim Laden der Chats Bitte aktualisieren Sie die App und nehmen Sie Kontakt mit den Entwicklern auf. - Benötigt mehr Leistung Ihrer Batterie! Der Hintergrundservice läuft permanent ab. Benachrichtigungen werden Ihnen angezeigt, sobald Sie neue Nachrichten erhalten haben.]]> + Benötigt mehr Leistung Ihrer Batterie! Die App läuft permanent im Hintergrund ab. Benachrichtigungen werden Ihnen sofort angezeigt.]]> Gruppenlink erstellen Erlauben Sie Ihren Kontakten das Senden von verschwindenden Nachrichten. Das Senden von verschwindenden Nachrichten nicht erlauben. @@ -989,7 +985,7 @@ App-Version: v%s Core Version: v%s Profil hinzufügen - Alle Chats und Nachrichten werden gelöscht! Dies kann nicht rückgängig gemacht werden! + Es werden alle Chats und Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! Chat-Profil löschen für PING-Zähler Transport-Isolations-Modus aktualisieren\? @@ -1088,7 +1084,7 @@ Datenbank-Aktualisierung Unterschiedlicher Migrationsstand in der App/Datenbank: %s / %s Datenbank herabstufen und den Chat öffnen - Inkompatible Datenbank-Version + Datenbank-Version nicht kompatibel Warnung: Sie könnten einige Daten verlieren! Datenbank auf alte Version herabstufen Datenbank-IDs und Transport-Isolationsoption. @@ -1208,7 +1204,7 @@ Wenn Personen eine Verbindung anfordern, können Sie diese annehmen oder ablehnen. Sie werden Ihre damit verbundenen Kontakte nicht verlieren, wenn Sie diese Adresse später löschen. Design anpassen - DESIGN-FARBEN + INTERFACE-FARBEN Fügen Sie die Adresse Ihrem Profil hinzu, damit Ihre Kontakte sie mit anderen Personen teilen können. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet. Alle Ihre Kontakte bleiben verbunden. Es wird eine Profilaktualisierung an Ihre Kontakte gesendet. Erstellen Sie eine Adresse, damit sich Personen mit Ihnen verbinden können. @@ -1327,23 +1323,23 @@ Nachrichtenverlauf bearbeiten In diesem Chat sind Reaktionen auf Nachrichten nicht erlaubt. Kein Text - Während des Imports sind einige nicht schwerwiegende Fehler aufgetreten – weitere Details finden Sie in der Chat-Konsole. + Während des Imports sind nicht schwerwiegende Fehler aufgetreten: Herunterfahren\? Bis zum Neustart der App erhalten Sie keine Benachrichtigungen mehr APP Neustart Herunterfahren - Fehler beim Abbrechen des Adresswechsels - Abbrechen + Fehler beim Beenden des Adresswechsels + Beenden Dateien und Medien Das Senden von Dateien und Medien erlauben. Das Senden von Dateien und Medien nicht erlauben. - Wechsel der Empfängeradresse abbrechen - Wechsel der Empfängeradresse abbrechen? + Wechsel der Empfängeradresse beenden + Wechsel der Empfängeradresse beenden? Dateien und Medien sind nicht erlaubt! Nur Gruppenbesitzer können Dateien und Medien aktivieren. Gruppenmitglieder können Dateien und Medien senden. - Der Wechsel der Empfängeradresse wird abgebrochen. Die bisherige Adresse wird weiter verwendet. + Der Wechsel der Empfängeradresse wird beendet. Die bisherige Adresse wird weiter verwendet. In dieser Gruppe sind Dateien und Medien nicht erlaubt. Favorit entfernen Favorit @@ -1386,7 +1382,7 @@ Nach ungelesenen und favorisierten Chats filtern. Das Senden von Empfangsbestätigungen an alle Kontakte in allen sichtbaren Chat-Profilen wird aktiviert. Das Senden von Bestätigungen an %d Kontakte ist deaktiviert - Diese Einstellungen gelten für Ihr aktuelles Profil + Diese Einstellungen gelten für Ihr aktuelles Chat-Profil Sie können in den Kontakt- und Gruppeneinstellungen überschrieben werden. Kontakte Bestätigungen deaktivieren\? @@ -1430,8 +1426,8 @@ An dieses Gruppenmitglied wird eine Verbindungsanfrage gesendet. Direkt verbinden\? Inkognito verbinden - Das aktuelle Profil nutzen - Ein neues Inkognito-Profil nutzen + Aktuelles Chat-Profil nutzen + Neues Inkognito-Profil nutzen App-Akkuverbrauch / Unbeschränkt , um Anrufe im Hintergrund zu führen.]]> Fügen Sie den erhaltenen Link ein, um sich mit Ihrem Kontakt zu verbinden… Es wird ein neues Zufallsprofil geteilt. @@ -1460,7 +1456,7 @@ Datenbank-Ordner öffnen Das Passwort wird in Klartext in den Einstellungen gespeichert, nachdem Sie es geändert oder die App neu gestartet haben. Das Passwort wurde in Klartext in den Einstellungen gespeichert. - Bitte beachten Sie: Die Nachrichten- und Dateirelais sind per SOCKS Proxy verbunden. Anrufe und gesendete Link-Vorschaubilder nutzen eine direkte Verbindung.]]> + Bitte beachten Sie: Die Nachrichten- und Datei-Relais sind per SOCKS-Proxy verbunden. Anrufe und gesendete Link-Vorschaubilder nutzen eine direkte Verbindung.]]> Lokale Dateien verschlüsseln Öffnen Gespeicherte Dateien & Medien verschlüsseln @@ -1529,7 +1525,7 @@ Freigeben Ungültiger Datei-Pfad Sie haben über diese Adresse bereits eine Verbindung beantragt! - Die Konsole in einem neuen Fenster anzeigen + Konsole in einem neuen Fenster anzeigen Von %s werden alle neuen Nachrichten ausgeblendet! Blockiert Fehler bei der Neuverhandlung der Verschlüsselung @@ -1555,14 +1551,14 @@ Falsche Desktop-Adresse Geräte Desktop-Verbindung trennen? - Desktop-App-Version %s ist mit dieser App nicht kompatibel. + Die Desktop-App-Version %s ist nicht mit dieser App kompatibel. Neues Mobiltelefon-Gerät Nur ein Gerät kann gleichzeitig genutzt werden Verknüpfe Mobiltelefon- und Desktop-Apps! 🔗 Über ein sicheres quantenbeständiges Protokoll Vom Desktop aus nutzen und scannen Sie den QR-Code.]]> Um unerwünschte Nachrichten zu verbergen. - Inkompatible Version + Version nicht kompatibel (Neu)]]> Desktop entkoppeln? Verknüpfte Desktop-Optionen @@ -1670,13 +1666,13 @@ Ehemaliges Mitglied %1$s Die Ausführung dieser Funktion dauert zu lange: %1$d Sekunden: %2$s Langsame Funktion - Zeige langsame API-Aufrufe an + Langsame API-Aufrufe anzeigen unbekannt Optionen für Entwickler unbekannter Gruppenmitglieds-Status Mit verschlüsselten Dateien und Medien. Private Notizen - Es werden alle Nachrichten gelöscht. Dieser Vorgang kann nicht rückgängig gemacht werden! + Es werden alle Nachrichten gelöscht. Dies kann nicht rückgängig gemacht werden! Private Notizen löschen? %s wurde blockiert %s wurde freigegeben @@ -1729,10 +1725,10 @@ Link-Details werden heruntergeladen Archiv wird heruntergeladen Anwenden - Alle Ihre Kontakte, Unterhaltungen und Dateien werden sicher verschlüsselt und in Daten-Paketen auf die konfigurierten XTFP-Server hochgeladen. + Alle Ihre Kontakte, Unterhaltungen und Dateien werden sicher verschlüsselt und in Daten-Paketen auf die konfigurierten XTFP-Relais hochgeladen. Archivieren und Hochladen Warnung: Das Archiv wird gelöscht.]]> - Überprüfen Sie Ihre Internet-Verbindung und probieren Sie es nochmals + Überprüfen Sie Ihre Internetverbindung und probieren Sie es nochmals Datenbank wird archiviert Bitte beachten Sie: Aus Sicherheitsgründen wird die Nachrichtenentschlüsselung Ihrer Verbindungen abgebrochen, wenn Sie die gleiche Datenbank auf zwei Geräten nutzen.]]> Migration abbrechen @@ -1808,7 +1804,7 @@ weitergeleitet Netzwerkverbindung Keine Netzwerkverbindung - Zellulär + Mobilfunknetz Andere WiFi Kabelgebundenes Netzwerk @@ -1850,4 +1846,309 @@ Profil-Bilder Form der Profil-Bilder Quadratisch, kreisförmig oder irgendetwas dazwischen. + Fehler auf dem Ziel-Server: %1$s + Fehler: %1$s + Warnung bei der Nachrichtenzustellung + Falscher Schlüssel oder unbekannte Verbindung - höchstwahrscheinlich ist diese Verbindung gelöscht. + Die Server-Version ist nicht mit den Netzwerkeinstellungen kompatibel. + Kapazität überschritten - der Empfänger hat die zuvor gesendeten Nachrichten nicht empfangen. + Weiterleitungs-Server: %1$s +\nFehler auf dem Ziel-Server: %2$s + Weiterleitungs-Server: %1$s +\nFehler: %2$s + Netzwerk-Fehler - die Nachricht ist nach vielen Sende-Versuchen abgelaufen. + Die Server-Adresse ist nicht mit den Netzwerkeinstellungen kompatibel. + Immer + Privates Routing + Nie + Unbekannte Server + Ungeschützt + Sie nutzen privates Routing mit unbekannten Servern. + Sie nutzen KEIN privates Routing. + Modus für das Nachrichten-Routing + Ja + Nein + Wenn die IP-Adresse versteckt ist + Fallback für das Nachrichten-Routing + Nachrichtenstatus anzeigen + Herabstufung erlauben + Sie nutzen immer privates Routing. + Nachrichten werden nicht direkt versendet, selbst wenn Ihr oder der Ziel-Server kein privates Routing unterstützt. + PRIVATES NACHRICHTEN-ROUTING + Nachrichten werden direkt versendet, wenn die IP-Adresse geschützt ist, und Ihr oder der Ziel-Server kein privates Routing unterstützt. + Nachrichten werden direkt versendet, wenn Ihr oder der Ziel-Server kein privates Routing unterstützt. + Zum Schutz Ihrer IP-Adresse, wird für die Nachrichten-Auslieferung privates Routing über Ihre konfigurierten SMP-Server genutzt. + Sie nutzen privates Routing mit unbekannten Servern, wenn Ihre IP-Adresse nicht geschützt ist. + IP-Adresse schützen + DATEIEN + Die App wird bei unbekannten Datei-Servern nach einer Download-Bestätigung fragen (außer bei .onion oder wenn ein SOCKS-Proxy aktiviert ist). + Unbekannte Server! + Ohne Tor- oder VPN-Nutzung wird Ihre IP-Adresse für Datei-Server sichtbar sein. + Ohne Tor- oder VPN-Nutzung wird Ihre IP-Adresse für diese XFTP-Relais sichtbar sein: +\n%1$s. + Profil-Design + Schwarz + Farbvariante + Dunkel + Hell + Farbe zurücksetzen + Gesendete Antwort + System + Dunkle Variante + Füllen + Helle Variante + Empfangene Antwort + Bild entfernen + Wiederholen + Skalieren + Default-Design einstellen + Wallpaper-Akzent + Wallpaper-Hintergrund + Anwenden auf + Zusätzlicher Akzent 2 + Erweiterte Einstellungen + Alle Farbvarianten + Chat-Farben + Chat-Design + Passend + Chat-Liste in einem neuen Fenster anzeigen + Guten Nachmittag! + Guten Morgen! + Farben für die dunkle Variante + App-Design + Persische Bedienoberfläche + Auf das Benutzer-spezifische Design zurücksetzen + Fehler bei der Initialisierung von Webview. Aktualisieren Sie Ihr System auf die neue Version. Bitte kontaktieren Sie die Entwickler. +\nFehler: %s + Auf das App-Design zurücksetzen + Dateien von unbekannten Servern bestätigen. + Verbesserte Zustellung von Nachrichten + Gestalten Sie Ihre Chats unterschiedlich! + Neue Chat-Designs + Privates Nachrichten-Routing 🚀 + Schützen Sie Ihre IP-Adresse vor den Nachrichten-Relais , die Ihr Kontakt ausgewählt hat. +\nAktivieren Sie es in den *Netzwerk & Server* Einstellungen. + Dateien sicher empfangen + Mit reduziertem Akkuverbrauch. + Keine Information + Debugging-Zustellung + Nachrichten-Warteschlangen-Information + Server-Warteschlangen-Information: %1$s +\n +\nZuletzt empfangene Nachricht: %2$s + Datei nicht gefunden - höchstwahrscheinlich wurde die Datei gelöscht oder der Transfer abgebrochen. + Datei-Server Fehler: %1$s + Falscher Schlüssel oder unbekannte Daten-Paketadresse der Datei - höchstwahrscheinlich wurde die Datei gelöscht. + Datei-Fehler + Nachrichten-Status + Nachrichten-Status: %s + Datei-Status + Datei-Status: %s + Temporärer Datei-Fehler + Fehlermeldung kopieren + Dieser Link wurde schon mit einem anderen Mobiltelefon genutzt. Bitte erstellen sie einen neuen Link in der Desktop-App. + Bitte überprüfen Sie, ob sich das Mobiltelefon und die Desktop-App im gleichen lokalen Netzwerk befinden, und die Desktop-Firewall die Verbindung erlaubt. +\nBitte teilen Sie weitere mögliche Probleme den Entwicklern mit. + Nachricht wurde nicht gesendet + Diese Nachricht ist wegen der gewählten Chat-Einstellungen nicht erlaubt. + Bitte versuchen Sie es später erneut. + Fehler beim privaten Routing + Die Nachricht kann später zugestellt werden, wenn das Mitglied aktiv wird. + Bisher keine direkte Verbindung. Nachricht wird von einem Admin weitergeleitet. + Konfigurierte SMP-Server + Konfigurierte XFTP-Server + Andere SMP-Server + Andere XFTP-Server + Abgeschlossen + Inaktiv + Verbunden + Verbinden + Aktive Verbindungen + Aktuelles Profil + Detaillierte Statistiken + Details + Heruntergeladen + Fehler + Fehler beim Wiederherstellen der Verbindungen zu den Servern + Fehler beim Zurücksetzen der Statistiken + Fehler + Empfangene Nachrichten + Nachrichtenempfang + Ausstehend + Bisher verbundene Server + Proxy-Server + Empfangene Nachrichten + Summe aller empfangenen Nachrichten + Fehler bei der Bestätigung + Versuche + Daten-Pakete gelöscht + Daten-Pakete hochgeladen + Verbindungen + Erstellt + Entschlüsselungs-Fehler + Fehler beim Löschen + Heruntergeladene Dateien + Fehler beim Herunterladen + Duplikate + abgelaufen + Server-Einstellungen öffnen + Andere Fehler + Proxy + Fehler beim Empfang + Neu verbinden + Deaktiviert + Beta + App-Aktualisierung wird heruntergeladen. App nicht schließen! + Heruntergeladen %s (%s) + Nach Aktualisierungen suchen + Deaktivieren + Erfolgreich installiert + Aktualisierung installieren + Bitte starten Sie die App neu. + Bestätigt + Alle Profile + App-Aktualisierung wurde heruntergeladen + Nach Aktualisierungen suchen + Daten-Pakete heruntergeladen + Verbundene Server + Gelöscht + deaktiviert + Fehler beim Wiederherstellen der Verbindung zum Server + Nachricht weitergeleitet + Dateien + Schriftgröße + Mitglied inaktiv + Gesendete Nachrichten + Keine Information - es wird versucht neu zu laden + Dateispeicherort öffnen + andere + Die Server-Adresse ist nicht mit den Netzwerkeinstellungen kompatibel: %1$s. + Link scannen / einfügen + Alle Server neu verbinden + Server neu verbinden? + Alle Server neu verbinden? + Um die Auslieferung von Nachrichten zu erzwingen, werden alle Server neu verbunden. Dafür wird weiterer Datenverkehr benötigt. + Um die Auslieferung von Nachrichten zu erzwingen, wird der Server neu verbunden. Dafür wird weiterer Datenverkehr benötigt. + Zurücksetzen + Alle Statistiken zurücksetzen + Alle Statistiken zurücksetzen? + Gesendete Nachrichten + Summe aller gesendeten Nachrichten + Abgesichert + Fehler beim Senden + Direkt gesendet + Über einen Proxy gesendet + Server-Adresse + Später erinnern + Server-Informationen + Ihre App ist nicht mit der Server-Version kompatibel: %1$s. + Prozentualen Anteil anzeigen + Zoom + SMP-Server + Informationen zeigen für + Beginnend mit %s. +\nAlle Daten werden nur auf Ihrem Gerät gespeichert. + Statistiken + Transport-Sitzungen + Hochgeladen + Sie sind nicht mit diesen Servern verbunden. Zur Auslieferung von Nachrichten an diese Server wird privates Routing genutzt. + Summe aller Abonnements + Die Server-Statistiken werden zurückgesetzt. Dies kann nicht rückgängig gemacht werden! + Größe + Beginnend mit %s. + Abonniert + Fehler beim Abonnieren + Nicht beachtete Abonnements + Hochgeladene Dateien + Fehler beim Hochladen + XFTP-Server + Stabil + Diese Version überspringen + Aktualisierung verfügbar: %s + Aktivieren Sie die periodische Überprüfung auf stabile oder Beta-Versionen der App, um über neue Versionen benachrichtigt zu werden. + Herunterladen der Aktualisierung abgebrochen + Die Weiterleitungs-Server-Adresse ist nicht kompatibel mit den Netzwerkeinstellungen: %1$s. + Die Verbindung des Weiterleitungs-Servers %1$s zum Ziel-Server %2$s schlug fehl. Bitte versuchen Sie es später erneut. + Die Ziel-Server-Version von %1$s ist nicht mit dem Weiterleitungs-Server %2$s kompatibel. + Die Weiterleitungs-Server-Version ist nicht kompatibel mit den Netzwerkeinstellungen: %1$s. + Die Ziel-Server-Adresse von %1$s ist nicht mit den Einstellungen des Weiterleitungs-Servers %2$s kompatibel. + Fehler beim Verbinden zum Weiterleitungs-Server %1$s. Bitte versuchen Sie es später erneut. + Medium verpixeln + Weich + Stark + Mittel + Aus + Verbinden + Nachricht + Öffnen + Unterhaltung gelöscht! + Nur die Unterhaltung löschen + Suchen + Video + Sie können in der Chatliste weiterhin die Unterhaltung mit %1$s einsehen. + Link einfügen + Archivierte Kontakte + Keine gefilterten Kontakte + Ihre Kontakte + Erreichbare Chat-Symbolleiste + Bitten Sie Ihren Kontakt darum, Anrufe zu aktivieren. + Sie müssen Ihrem Kontakt Anrufe zu Ihnen erlauben, bevor Sie ihn selbst anrufen können. + Anrufe erlauben? + Anrufen + Kontakt kann nicht angerufen werden + Anrufe nicht zugelassen! + Löschen des Kontakts bestätigen? + Kontakt gelöscht! + Gruppenmitglied kann nicht angerufen werden + Nachricht an Gruppenmitglied nicht möglich + Verbinde mit Kontakt, bitte warten oder später erneut überprüfen! + Kontakt wurde gelöscht. + Kontakt wird gelöscht. Dies kann nicht rückgängig gemacht werden! + Ohne Benachrichtigung löschen + Einladen + Unterhaltung behalten + Nachricht senden, um Anrufe zu aktivieren. + Sie können aus den archivierten Kontakten heraus Nachrichten an %1$s versenden. + Einstellungen + Die Nachrichten werden für alle Mitglieder gelöscht. + Die Nachrichten werden für alle Mitglieder als moderiert markiert. + %d Nachrichten der Mitglieder löschen? + Nachricht + Nachrichten werden zur Löschung markiert. Der/Die Empfänger hat/haben die Möglichkeit, diese Nachrichten aufzudecken. + %d ausgewählt + Es wurde Nichts ausgewählt + Auswählen + Einladen + TCP-Verbindung + Schriftgröße erhöhen. + Neue Chat-Erfahrung 🎉 + Die App automatisch aktualisieren + Verbindungs- und Server-Status. + Für bessere Privatsphäre verpixeln. + Kontrollieren Sie Ihr Netzwerk + Neue Medien-Optionen + Speichern und neu verbinden + Erstellen + Laden Sie neue Versionen von GitHub herunter. + Direkt aus der Chat-Liste abspielen. + Chat-Liste umschalten: + Kann von Ihnen in den Erscheinungsbild-Einstellungen geändert werden. + Kontakte für spätere Chats archivieren. + Ihre IP-Adresse und Verbindungen werden geschützt. + Löschen Sie bis zu 20 Nachrichten auf einmal. + Erreichbare Chat-Symbolleiste + Die App mit einer Hand nutzen. + Schneller mit Ihren Freunden verbinden. + Chat-Datenbank wurde exportiert + Weiter + Medien- und Datei-Server + Nachrichten-Server + SOCKS-Proxy + Es wurden nicht alle Dateien exportiert + Sie können die exportierte Datenbank migrieren. + Sie können das exportierte Archiv speichern. + Alle Hinweise zurücksetzen + Neue Nachricht + Bitte überprüfen Sie, ob der SimpleX-Link korrekt ist. + Ungültiger Link \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml index 811257ff39..baa85f07f4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/el/strings.xml @@ -42,7 +42,7 @@ Αποδοχή αιτήματος σύνδεσης; αποδεκτή κλήση Πρόσβαση στους διακομιστές μέσω SOCKS proxy στην πόρτα %d; Ο διακομιστής μεσολάβησης (proxy server) πρέπει να είναι ενεργός πριν ενεργοποιηθεί αυτή η ρύθμιση. - Προσθήκη διακομιστή… + Προσθήκη διακομιστή Προχωρημένες ρυθμίσεις δικτύου Προσθήκη διακομιστών μέσω σάρωσης QR κωδικών. Οι διαχειριστές μπορούν να δημιουργήσουν τους συνδέσμους συμμετοχής σε ομάδες. diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml index 54512aca34..bc27b0a29f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/es/strings.xml @@ -4,7 +4,7 @@ Aceptar Configuración avanzada de red Óptimo para la batería. Recibirás notificaciones sólo cuando la aplicación esté abierta (SIN servicio en segundo plano).]]> - Bueno para la batería. El servicio en segundo plano comprueba si hay mensajes cada 10 minutos. Podrías no recibir a tiempo llamadas o mensajes urgentes.]]> + Bueno para la batería. La aplicación comprueba si hay mensajes cada 10 minutos. Podrías perderte llamadas o mensajes urgentes.]]> Aceptar Copia de seguridad de los datos de la aplicación un dia @@ -30,7 +30,7 @@ ¿Aceptar solicitud de conexión\? Aceptar incógnito Se eliminarán todos los mensajes SOLO para tí. ¡No podrá deshacerse! - Añadir servidor… + Añadir servidor ¿Acceder a los servidores a través del proxy SOCKS en el puerto %d\? El proxy debe iniciarse antes de activar esta opción. Todos tus contactos permanecerán conectados. Apariencia @@ -75,7 +75,7 @@ Solicita recibir la imagen Recuerda: NO podrás recuperar o cambiar la frase de contraseña si la pierdes.]]> Tanto tú como tu contacto podéis enviar mensajes de voz. - ¡Consume más batería! El servicio en segundo plano se ejecuta continuamente y las notificaciones se mostrarán de inmediato.]]> + ¡Consume más energía! La aplicación está siempre en segundo plano y las notificaciones se muestran de inmediato.]]> Tanto tú como tu contacto podéis eliminar los mensajes enviados de forma irreversible. (24 horas) Tanto tú como tu contacto podéis enviar mensajes temporales. Crear @@ -83,7 +83,7 @@ La contraseña de cifrado de la base de datos será actualizada. ID base de datos Los mensajes directos entre miembros del grupo no están permitidos. - La contraseña es distinta a la almacenada en Keystore + La contraseña de la base de datos es distinta a la almacenada en Keystore. La base de datos será cifrada y la contraseña se guardará en Keystore. ¿Eliminar contacto\? ¿Eliminar mensaje\? @@ -114,8 +114,8 @@ Eliminar mensaje ¡Base de datos cifrada! La base de datos está cifrada con una contraseña aleatoria, puedes cambiarla. - Error base de datos - Para abrir la aplicación se requiere la contraseña de la base de datos + Error en base de datos + Se requiere la contraseña de la base de datos para abrir la aplicación. conectado Crear enlace ¿Eliminar enlace\? @@ -153,7 +153,7 @@ Eliminar en %d seg El contácto ya existe - Error conexión (Autenticación) + Error de conexión (Autenticación) Eliminar para mí Desconectado Conectado @@ -220,7 +220,7 @@ conectado directa El contacto permite - predefinido (%s) + predeterminado (%s) Eliminar para todos activado Tus contactos sólo pueden marcar los mensajes para eliminar. Tu podrás verlos. @@ -271,16 +271,16 @@ No se puede iniciar la base de datos ¿Vaciar chat\? por perfil - Chat está parado + SimpleX está parado rol de %s cambiado a %s Cambiar rol - Mediante perfil (por defecto) o por conexión (BETA) + Mediante perfil (predeterminado) o por conexión (BETA) cambiando de servidor… Preferencias de Chat cancelado %s - Chat está parado + SimpleX está parado LLAMADAS - Chat está en ejecución + SimpleX está en ejecución está cambiando de servidor… habla con los desarrolladores Cancelar vista previa del archivo @@ -302,14 +302,14 @@ Llamadas en la ventana de bloqueo ¡No se pueden invitar contactos! Consola de Chat - BASE DE DATOS DE CHAT + BASE DE DATOS DE SIMPLEX Base de datos eliminada Base de datos importada Comprueba la dirección del servidor e inténtalo de nuevo. Confirma la contraseña nueva… Si confirmas los servidores de mensajería podrán ver tu IP, y tu proveedor de acceso a internet a qué servidores te estás conectando. Imagen guardada en la Galería - El archivo se recibirá cuando el contacto esté en línea, por favor espera o compruébalo más tarde. + El archivo se recibirá cuando el contacto esté en línea, por favor espera o revisa más tarde. Enlace de invitación de un uso Pegar el enlace recibido Error al guardar perfil de grupo @@ -328,7 +328,7 @@ Error al crear dirección Error al eliminar perfil Activar Bloqueo SimpleX - Enlace de invitación de un uso + Enlace de invitación de un solo uso Servidores SMP Características experimentales Error al importar base de datos @@ -337,7 +337,7 @@ ¡Error al cambiar perfil! Introduce el servidor manualmente Cómo usar los servidores - Error al parar Chat + Error al parar SimpleX Introduce la contraseña correcta. Introduce la contraseña… Grupo inactivo @@ -355,8 +355,8 @@ Si has recibido un enlace de invitación a SimpleX Chat puedes abrirlo en tu navegador: Si seleccionas rechazar, el remitente NO será notificado. ¡Enlace no válido! - escanear el código QR en la videollamada, o tu contacto puede compartir un enlace de invitación.]]> - muestra el código QR en la videollamada, o comparte el enlace.]]> + escanear el código QR por videollamada, o tu contacto puede compartir un enlace de invitación.]]> + muestra el código QR por videollamada, o comparte el enlace.]]> Cómo Ignorar Error al eliminar base de datos @@ -382,12 +382,12 @@ Para todos Archivo Imagen enviada - La imagen se recibirá cuando el contacto esté en línea, por favor espera o compruébalo más tarde. + La imagen se recibirá cuando el contacto esté en línea, por favor espera o revisa más tarde. vista previa del enlace Error al guardar servidores ICE Servidores ICE (uno por línea) Nombre completo: - Las personas pueden conectarse contigo sólo mediante los enlaces que compartes. + Tu decides quién se conecta. Cómo funciona SimpleX Colgar Archivos y multimedia @@ -446,8 +446,7 @@ La base de datos no está cifrada. Escribe una contraseña para protegerla. Asegúrate de que las direcciones del servidor SMP tienen el formato correcto, están separadas por líneas y no están duplicadas. Notificación instantánea - Configuración de red - No se usarán hosts .onion + Configuración avanzada cifrado de extremo a extremo de 2 capas .]]> Puedes cambiar estos ajustes más tarde en Configuración. Instantánea @@ -466,7 +465,6 @@ Contacto y texto MIEMBRO nunca - Se requieren hosts .onion para la conexión No se usarán hosts .onion Vista previa de notificaciones ¡Invitación caducada! @@ -511,8 +509,7 @@ Asegúrate de que las direcciones del servidor WebRTC ICE tienen el formato correcto, están separadas por líneas y no duplicadas. Se requieren hosts .onion para la conexión \nRecuerda: no podrás conectarte a servidores que no tengan dirección .onion. - Se usarán hosts .onion si están disponibles. - Inmune a spam y abuso + Inmune al spam si SimpleX no tiene identificadores de usuario, ¿cómo puede entregar los mensajes\?]]> Videollamada entrante has salido @@ -543,7 +540,7 @@ Contraseña nueva… ¿Unirte al grupo? Entrando al grupo - Error Keystore + Error en Keystore Invitar miembros observador miembro @@ -606,8 +603,7 @@ llamada rechazada secreto Abrir SimpleX Chat para aceptar llamada - Restablecer valores por defecto - Guardar color + Restablecer valores predetarminados Pendiente Notificaciones periódicas Guarda la contraseña de forma segura, NO podrás cambiarla si la pierdes. @@ -617,7 +613,7 @@ imagen del perfil No se permiten mensajes de voz. Proteger la pantalla de la aplicación - repositorio GitHub .]]> + repositorio GitHub .]]> Grabar mensaje de voz ha expulsado a %1$s Enviar previsualizacion de enlaces @@ -635,9 +631,8 @@ Rechazar Obligatorio Guardar y notificar contactos - Protocolo y código abiertos: cualquiera puede usar los servidores. + Cualquiera puede alojar servidores. Rol - Revertir Intervalo PING Contador PING Sólo tu contacto puede eliminar mensajes de forma irreversible (tu puedes marcarlos para eliminar). (24 horas) @@ -661,7 +656,7 @@ confirmación recibida… Periódico Privacidad redefinida - Saber más en nuestro repositorio GitHub. + Conoce más en nuestro repositorio GitHub. Rechazar Abrir Llamada pendiente @@ -671,13 +666,13 @@ Introduce la contraseña anterior después de restaurar la copia de seguridad de la base de datos. Esta acción no se puede deshacer. te ha expulsado Recibiendo vía - Tiempo de espera del protocolo + Timeout protocolo seg Datos del perfil y conexiones No se permiten mensajes temporales. Sólo tú puedes enviar mensajes de voz. Sólo tu contacto puede enviar mensajes de voz. - EJECUTAR CHAT + EJECUTAR SIMPLEX Reinicia la aplicación para poder usar la base de datos importada. Introduce la contraseña actual correcta. recepción no permitida @@ -708,8 +703,8 @@ Guardar y notificar contacto ¿Guardar preferencias\? Guardar y notificar grupo - A menos que tu contacto haya eliminado la conexión o que este enlace ya se haya usado, podría ser un error. Por favor, notifícalo. -\nPara conectarte, pide a tu contacto que cree otro enlace de conexión y comprueba que tienes buena conexión de red. + A menos que tu contacto haya eliminado la conexión o el enlace haya sido usado, podría ser un error. Por favor, notifícalo. +\nPara conectarte pide a tu contacto que cree otro enlace y comprueba la conexión de red. La aplicación recoge nuevos mensajes periódicamente lo que consume un pequeño porcentaje de batería al día. La aplicación no usa notificaciones push por tanto los datos de tu dispositivo no se envían a los servidores push. Bloqueo SimpleX Desbloquear @@ -746,11 +741,12 @@ Compartir enlace de un uso ¿Actualizar el modo de aislamiento de transporte\? Altavoz activado - Para habilitar las acciones sobre la base de datos, debes parar Chat + Para habilitar las acciones sobre la base de datos, debes parar SimpleX ¡La conexión que has aceptado se cancelará! - La base de datos no funciona correctamente. Pulsa para saber más + La base de datos no funciona correctamente. Pulsa para conocer más El mensaje será marcado como moderado para todos los miembros. - La nueva generación de mensajería privada + La nueva generación +\nde mensajería privada Esta acción es irreversible. Se eliminarán todos los archivos y multimedia recibidos y enviados. Las imágenes de baja resolución permanecerán. Esta acción es irreversible. Los mensajes enviados y recibidos anteriores a la selección serán eliminados. Podría tardar varios minutos. Esta configuración se aplica a los mensajes del perfil actual @@ -759,12 +755,12 @@ Configuración Altavoz desactivado Inciar chat nuevo - Para exportar, importar o eliminar la base de datos debes parar Chat. Mientra tanto no podrás recibir o enviar mensajes. + Para exportar, importar o eliminar la base de datos debes parar SimpleX. Mientra tanto no podrás recibir o enviar mensajes. Gracias por instalar SimpleX Chat! Para proteger tu privacidad, en lugar de los identificadores de usuario que usan el resto de plataformas, SimpleX dispone de identificadores para las colas de mensajes, independientes para cada uno de tus contactos. Para proteger tu información, activa el Bloqueo SimpleX. \nSe te pedirá que completes la autenticación antes de activar esta función. - Al actualizar la configuración, el cliente se reconectará a todos los servidores. + Al actualizar la configuración el cliente se reconectará a todos los servidores. ¿Usar servidores SimpleX Chat\? Enlace de grupo SimpleX Invitación única SimpleX @@ -776,7 +772,7 @@ Se muestran el nombre del contacto y el mensaje Se muestra sólo el nombre del contacto Bloqueo SimpleX activado - Parar Chat + Parar SimpleX El mensaje se eliminará para todos los miembros. Compartir archivo… ¡Demasiadas imágenes! @@ -785,7 +781,7 @@ Usar hosts .onion simplexmq: v%s (%2s) La plataforma de mensajería y aplicaciones que protege tu privacidad y seguridad. - La primera plataforma sin identificadores de usuario: diseñada para la privacidad. + Sin identificadores de usuario. Este grupo ya no existe. Establecer 1 día ¡Nuestro agradecimiento a todos los colaboradores! Puedes contribuir a través de Weblate. @@ -793,7 +789,7 @@ Para proteger la zona horaria, los archivos de imagen/voz usan la hora UTC. Aislamiento de transporte (para compartir con tu contacto) - Activar audio + Activar sonido %s está verificado %s no está verificado Probar servidor @@ -801,16 +797,15 @@ Estrella en GitHub Lista de servidores para las conexiones nuevas de tu perfil actual ¿Usar conexión directa a Internet\? - ¿Actualizar la configuración de los hosts .onion\? El perfil sólo se comparte con tus contactos. inicializando… Mensajes omitidos CONFIGURACIÓN - ¿Parar Chat? + ¿Parar SimpleX? %s segundo(s) Pulsa para unirte ha actualizado el perfil del grupo - Tiempo de espera de la conexión TCP agotado + Timeout de la conexión TCP Tema Establece preferencias de grupo SOPORTE SIMPLEX CHAT @@ -830,12 +825,12 @@ Compartir mensaje… Compartir medios… Mostrar - Error desconocido base de datos: %s + Error desconocido en la base de datos: %s El intento de cambiar la contraseña de la base de datos no se ha completado. Pulsa el botón Para iniciar un chat nuevo Cambiar servidor de recepción - Completamente descentralizado: sólo visible a los miembros. + Completamente descentralizado y sólo visible para los miembros. Para conectarte mediante enlace ¡Error en prueba del servidor! Algunos servidores no superaron la prueba: @@ -847,7 +842,7 @@ Mensaje de voz… Desactivar vídeo Activar vídeo - Contraseña de base de datos incorrecta + Contraseña incorrecta de la base de datos ¡Contraseña incorrecta! Te has unido a este grupo. Conectando con el emisor de la invitacíon. Estás usando un perfil incógnito en este grupo. Para evitar descubrir tu perfil principal no se permite invitar contactos @@ -882,9 +877,9 @@ La base de datos actual será ELIMINADA y SUSTITUIDA por la importada. \nEsta acción es irreversible. Tu perfil, contactos, mensajes y archivos actuales se perderán. Tu perfil aleatorio - Te conectarás cuando tu solicitud se acepte, por favor espera o compruébalo más tarde. - Te conectarás cuando el dispositivo de tu contacto esté en línea, por favor espera o compruébalo más tarde. - Se te pedirá identificarte cuándo inicies o continues usando la aplicación tras 30 segundos en segundo plano. + Te conectarás cuando tu solicitud se acepte, por favor espera o revisa más tarde. + Te conectarás cuando el dispositivo de tu contacto esté en línea, por favor espera o revisa más tarde. + Se te pedirá autenticarte cuando inicies la aplicación o sigas usándola tras 30 segundos en segundo plano. Estás intentando invitar a un contacto con el que compartes un perfil incógnito a un grupo en el que usas tu perfil principal Mediante navegador mediante %1$s @@ -907,7 +902,7 @@ Comprobar código de seguridad Has aceptado la conexión Has invitado a tu contacto - Te conectarás al grupo cuando el dispositivo anfitrión esté en línea, por favor espera o compruébalo más tarde. + Te conectarás al grupo cuando el dispositivo anfitrión esté en línea, por favor espera o revisa más tarde. Configuración Servidores SMP ¡Tú controlas tu chat! @@ -956,7 +951,7 @@ Tu servidor Dirección de tu servidor Tu perfil actual - Tu perfil se almacena en tu dispositivo y sólo se comparte con tus contactos. Los servidores SimpleX no pueden ver tu perfil. + Tu perfil es almacenado en tu dispositivo y solamente se comparte con tus contactos. Los servidores SimpleX no pueden ver tu perfil. Sistema Añadir mensaje de bienvenida Llamadas y videollamadas @@ -965,7 +960,7 @@ Guardar contraseña de perfil Contraseña para hacerlo visible Error al guardar contraseña de usuario - El retransmisor sólo se usa en caso de necesidad. Un tercero podría ver tu IP. + El servidor de retransmisión sólo se usa en caso de necesidad. Un tercero podría ver tu IP. El servidor de retransmisión protege tu IP pero puede ver la duración de la llamada. Introduce la contraseña Ocultar @@ -992,7 +987,7 @@ ¡Más mejoras en camino! Contraseña del perfil oculto ¿Guardar mensaje de bienvenida\? - Activar audio + Activar sonido Puedes ocultar o silenciar un perfil. Mantenlo pulsado para abrir el menú. Seguirás recibiendo llamadas y notificaciones de los perfiles silenciados cuando estén activos. Ahora los administradores pueden: @@ -1023,7 +1018,7 @@ Mostrar perfil Eliminar perfil Vídeo - El vídeo se recibirá cuando el contacto esté en línea, por favor espera o compruébalo más tarde. + El vídeo se recibirá cuando el contacto esté en línea, por favor espera o revisa más tarde. Esperando el vídeo Ha pedido recibir el video ¡Demasiados vídeos! @@ -1044,7 +1039,7 @@ Servidores XFTP Puerto puerto %d - Usar hosts .onion a No si el proxy SOCKS no los admite.]]> + Usar hosts .onion como No si el proxy SOCKS no los admite.]]> Descargar archivo Usar proxy SOCKS Host @@ -1117,14 +1112,14 @@ ¡Rápido y sin necesidad de esperar a que el remitente esté en línea! Abrir perfiles Más información - Si no puedes reunirte en persona, **muestra el código QR por videollamada**, o comparte el enlace. + Si no puedes reunirte en persona, muestra el código QR por videollamada o comparte el enlace. Para conectarse, tu contacto puede escanear el código QR o usar el enlace en la aplicación. Crear dirección SimpleX Auto aceptar Vista previa Abriendo base de datos… Error al introducir dirección - Guía de Usuario.]]> + Guía de Usuario.]]> Enlace un uso Dirección SimpleX Cuando alguien solicite conectarse podrás aceptar o rechazar su solicitud. @@ -1162,7 +1157,7 @@ Mensaje enviado Dejar de compartir ¿Dejar de compartir la dirección\? - COLORES DEL TEMA + COLORES DEL INTERFAZ Puedes crearla más tarde ¿Compartir la dirección con los contactos\? Compartir con contactos @@ -1244,10 +1239,10 @@ Personalizar y compartir temas de color. ¡Por fin los tenemos! 🚀 Reacciones a los mensajes - Saber más + Conoce más Interfaz en japonés y portugués sin texto - Algunos errores no críticos ocurrieron durante la importación - para más detalles puedes ver la consola de Chat. + Han ocurrido algunos errores no críticos durante la importación: ¿Cerrar\? Aplicación Reiniciar @@ -1269,7 +1264,7 @@ Se permite enviar archivos y multimedia Favorito Sólo los propietarios del grupo pueden activar los archivos y multimedia. - Timeout de protocolo por KB + Timeout protocolo por KB renegociación de cifrado permitida para %s cifrado acordado cifrado ok @@ -1307,7 +1302,7 @@ Activar ¿Desactivar confirmaciones\? ¿Activar confirmaciones\? - Se pueden anular en la configuración de grupos y contactos. + Se puede modificar desde la configuración particular de cada grupo o contacto. Activar para todos Activar (conservar anulaciones) Desactivar para todos @@ -1354,7 +1349,7 @@ Abrir configuración Se compartirá un perfil nuevo aleatorio. Pega el enlace recibido para conectar con tu contacto… - Tu perfil %1$s será compartido. + El perfil %1$s será compartido. Desactivar notificaciones Sin llamadas en segundo plano. SimpleX no puede funcionar en segundo plano. Sólo recibirás notificaciones con la aplicación abierta. @@ -1383,7 +1378,7 @@ Abrir Cifra archivos almacenados y multimedia Error al establecer contacto con el miembro - Recuerda: los servidores de retransmisión están conectados mediante SOCKS proxy. Las llamadas y las previsualizaciones de enlaces usan conexión directa.]]> + Recuerda: los servidores están conectados mediante proxy SOCKS, pero las llamadas y las previsualizaciones de enlaces usan conexión directa.]]> Cifra archivos locales Nueva aplicación para ordenador! 6 idiomas nuevos para el interfaz @@ -1432,7 +1427,7 @@ Renegociación de cifrado fallida. Ordenadores ¿Corregir el nombre a %s? - Elimina %d mensajes? + ¿Eliminar %d mensajes? Enlazar móvil ¿Conectar con %1$s? Bloquear @@ -1510,7 +1505,7 @@ autor Pegar dirección de ordenador %1$s!]]> - Verificar código con ordenador + Verifica el código en el ordenador Escanear código QR desde ordenador Desbloquear Detectable mediante red local @@ -1525,35 +1520,35 @@ Verificar conexión Ningún móvil conectado Error aplicación - error mostrando contenido + error al mostrar el contenido error al mostrar mensaje Puedes hacerlo visible para tus contactos de SimpleX en Configuración. El historial no se envía a miembros nuevos. Reintentar Cámara no disponible - Enviar hasta 100 últimos mensajes a los miembros nuevos. + Se envían hasta 100 mensajes más recientes a los miembros nuevos. Añadir contacto: crea un enlace de invitación nuevo o usa un enlace recibido.]]> - No enviar historial a miembros nuevos. - O mostrar este código + No se envía el historial a los miembros nuevos. + O muestra este código QR Hasta 100 últimos mensajes son enviados a los miembros nuevos. El código QR escaneado no es un enlace SimpleX. El texto pegado no es un enlace SimpleX. Permitir acceso a la cámara Podrás ver el enlace de invitación en detalles de conexión. ¿Guardar invitación no usada? - Compartir este enlace de un uso + Comparte este enlace de un solo uso Crear grupo: crea un grupo nuevo.]]> Historial visible Código acceso app Nuevo chat Cargando chats… Creando enlace… - O escanear código QR + O escanea el código QR Código QR no válido Añadir contacto Pulsa para escanear Guardar - Pulsa para pegar enlace + Pulsa para pegar el enlace Buscar o pegar enlace SimpleX Con uso reducido de batería. bloqueado por administrador @@ -1578,7 +1573,7 @@ Error al bloqear el miembro para todos Con cifrado de archivos y multimedia. %s agotado]]> - Chat parado. Si has usado esta base de datos en otro dispositivo, debes transferirla de vuelta antes de iniciar Chat. + SimpleX está parado. Si has usado esta base de datos en otro dispositivo, debes transferirla de vuelta antes de iniciar SimpleX. Este nombre mostrado no es válido. Por favor, selecciona otro nombre. Conexión parada Conexión parada @@ -1691,7 +1686,7 @@ Finalizar migración Atención: el archivo será eliminado.]]> Comprueba tu conexión a internet y vuelve a intentarlo - Confirma que recuerdas la frase de contraseña de la base de datos para migrarla. + Para migrar confirma que recuerdas la frase de contraseña de la base de datos. Error al verificar la frase de contraseña: Recuerda: usar la misma base de datos en dos dispositivos hará que falle el descifrado de mensajes como protección de seguridad.]]> Migrar desde otro dispositivo y escanea el código QR.]]> @@ -1702,7 +1697,7 @@ Error al exportar base de datos del chat Error al guardar ajustes El archivo exportado no existe - Para continuar, Chat debe estar parado. + Para continuar, SimpleX debe estar parado. cifrado de extremo a extremo con secreto perfecto hacía adelante, repudio y recuperación tras ataque.]]> cifrado de extremo a extremo resistente a tecnología cuántica con secreto perfecto hacía adelante, repudio y recuperación tras ataque.]]> Migrar aquí @@ -1740,10 +1735,10 @@ WiFi Ethernet por cable administradores - Activar para - No permitir el envío de enlaces SimpleX + Activado para + No se permite enviar enlaces SimpleX todos los miembros - Permitir enviar enlaces SimpleX. + Se permite enviar enlaces SimpleX. guardado guardado desde %s Guardado @@ -1767,7 +1762,312 @@ Al iniciar llamadas de audio y vídeo. ¡Será habilitado en los chats directos! Conexión de red más fiable. - Imágenes del perfil + Forma de los perfiles Dar forma a las imágenes de perfil Cuadrada, circular o cualquier forma intermedia. + Capacidad excedida - el destinatario no ha recibido los mensajes previos. + Error del servidor de destino: %1$s + Error: %1$s + Servidor de reenvío: %1$s +\nError del servidor de destino: %2$s + Servidor de reenvío: %1$s +\nError: %2$s + Problema en la red - el mensaje ha expirado tras muchos intentos de envío. + La versión del servidor es incompatible con la configuración de la red. + Enrutamiento privado + Servidores desconocidos + NO usar enrutamiento privado. + Enrutamiento de mensajes alternativo + Modo de enrutamiento de mensajes + No + Estado del mensaje + Usar enrutamiento privado con servidores desconocidos cuando tu dirección IP no está protegida. + Con IP oculta + Si + Enviar mensajes directamente cuando tu servidor o el de destino no admitan enrutamiento privado. + Para proteger tu dirección IP, el enrutamiento privado usa tu lista de servidores SMP para enviar mensajes. + NO enviar mensajes directamente incluso si tu servidor o el de destino no soportan enrutamiento privado. + Siempre + Permitir versión anterior + Usar siempre enrutamiento privado. + Aviso de entrega de mensaje + Nunca + ENRUTAMIENTO PRIVADO DE MENSAJES + La dirección del servidor es incompatible con la configuración de la red. + Con IP desprotegida + Clave incorrecta o conexión desconocida - probablemente esta conexión fue eliminada + Usar enrutamiento privado con servidores de retransmisión desconocidos. + Enviar mensajes directamente cuando tu dirección IP está protegida y tu servidor o el de destino no admitan enrutamiento privado. + ¡Servidores desconocidos! + Sin Tor o VPN, tu dirección IP será visible para estos relés XFTP: +\n%1$s. + Proteger dirección IP + Sin Tor o VPN, tu dirección IP será visible para los servidores de archivos. + ARCHIVOS + La aplicación pedirá que confirmes las descargas desde servidores de archivos desconocidos (excepto si son .onion o cuando esté habilitado el proxy SOCKS). + Colores del chat + Tema del chat + Aplicar a + Negro + Todos los modos + Modo de color + Oscuro + Modo oscuro + Colores en modo oscuro + Relleno + ¡Buenas tardes! + ¡Buenos días! + Claro + Modo claro + Respuesta recibida + Mosaico + Quitar imagen + Restablecer color + Escala + Respuesta enviada + Establecer tema predeterminado + Sistema + Color de fondo + Encaje + Color adicional 2 + Configuración avanzada + Tema del perfil + Listado del chat en ventana nueva + Color imagen de fondo + información cola del servidor: %1$s +\n +\núltimo mensaje recibido: %2$s + Restablecer al tema de la aplicación + Enrutamiento privado de mensajes 🚀 + Recibe archivos de forma segura + Mejora del envío de mensajes + Con uso reducido de la batería. + Tema de la app + Confirma archivos de servidores desconocidos. + Informe debug + ¡Cambia el aspecto de tus chats! + Nuevos temas de chat + Información cola de mensajes + ninguno + Protege tu dirección IP de los servidores de retransmisión elegidos por tus contactos. +\nActívalo en ajustes de *Servidores y Redes*. + Restablecer al tema del usuario + Error al inicializar WebView. Actualiza tu sistema a la última versión. Por favor, ponte en contacto con los desarrolladores. +\nError: %s + Interfaz en persa + Clave incorrecta o dirección del bloque del archivo desconocida. Es probable que el archivo se haya eliminado. + Archivo no encontrado, probablemente haya sido borrado o cancelado. + Error del servidor de archivos: %1$s + Error de archivo + Error en archivo temporal + Estado del mensaje: %s + Estado del mensaje + Estado del archivo: %s + Estado del archivo + Comprueba que el móvil y el ordenador están conectados a la misma red local y que el cortafuegos del ordenador permite la conexión. +\nPor favor, comparte cualquier otro problema con los desarrolladores. + Copiar error + Este enlace ha sido usado en otro dispositivo móvil, por favor crea un enlace nuevo en el ordenador. + No se puede enviar el mensaje + Las preferencias seleccionadas no permiten este mensaje. + Info servidores + Archivos + Mostrando + Suscrito + Errores de suscripción + Suscripciones ignoradas + Para ser notificado sobre versiones nuevas, activa el chequeo periódico para las versiones Estable o Beta. + Beta + Servidores SMP configurados + Servidores XFTP configurados + Servidores conectados + Conectando + Perfil actual + Zoom + Subido + Actualización disponible: %s + Descarga de actualización cancelada + Actualización descargada + Buscar actualizaciones + Buscar actualizaciones + Estadísticas + Total + Sesiones de transporte + Servidor XFTP + No estás conectado a estos servidores. Para enviarles mensajes se usa el enrutamiento privado. + Todos los perfiles + Conectadas + Estadísticas detalladas + Detalles + Archivos subidos + Errores de subida + intentos + Completadas + Conexiones + Creadas + errores de descifrado + Eliminadas + Errores de eliminación + desactivado + Mensaje reenviado + El mensaje puede ser entregado más tarde si el miembro vuelve a estar activo. + Miembro inactivo + Por favor, inténtalo más tarde. + Error de enrutamiento privado + La dirección del servidor es incompatible con la configuración de red: %1$s. + La versión del servidor es incompatible con tu aplicación: %1$s. + Tamaño fuente + Error al restablecer las estadísticas + Restablecer + Las estadísticas de los servidores serán restablecidas. ¡No podrá deshacerse! + Descargado + Servidor SMP + Aún no hay conexión directa, el mensaje es reenviado por el administrador. + Otros servidores SMP + Otros servidores XFTP + Escanear / Pegar enlace + Mostrar porcentaje + Desactivar + Desactivado + Descargando actualización, por favor no cierres la aplicación + Descarga %s (%s) + Instalación completada + Instalar actualización + Abrir ubicación del archivo + Por favor, reinicia la aplicación. + Recordar más tarde + Saltar esta versión + Estable + inactivo + Error + Error al reconectar con el servidor + Error al reconectar con los servidores + Errores + Recepción de mensajes + Mensajes recibidos + Mensajes enviados + Sin información, intenta recargar + Pendientes + Servidores conectados previamente + Servidores con proxy + Mensajes recibidos + Total recibidos + Errores de recepción + Reconectar todos los servidores + ¿Reconectar servidor? + ¿Reconectar servidores? + Reconectar el servidor para forzar la entrega de mensajes. Usa tráfico adicional. + Reconectar todos los servidores para forzar la entrega de mensajes. Usa tráfico adicional. + Restablecer todas las estadísticas + ¿Restablecer todas las estadísticas? + Mensajes enviados + Total enviados + Archivos descargados + Errores de descarga + duplicados + expirados + Abrir configuración del servidor + otros + otros errores + Como proxy + Reconectar + Aseguradas + Errores de envío + Directamente + Mediante proxy + Dirección del servidor + Tamaño + Conexiones activas + Iniciado el %s. + Iniciado el %s +\nTodos los datos son privados a tu dispositivo + Bloques eliminados + Bloques descargados + Bloques subidos + Confirmaciones + Errores de confirmación + Conectando con el contacto, por favor espera o revisa más tarde. + Estado de tu conexión y servidores. + Conecta más rápido con tus amigos + Controla tu red + Protege tu dirección IP y tus conexiones. + ¿Permitir llamadas? + Difuminar multimedia + ¡Llamadas no permitidas! + Base de datos exportada + Guardar y reconectar + Reiniciar todas las pistas + Pegar enlace + Medio + Suave + Barra de herramientas accesible + llamada + conectar + ¿Eliminar %d mensajes de miembros? + mensaje + Mensaje + Nada seleccionado + Los mensajes se marcarán para eliminar. El destinatario o destinatarios podrán revelar estos mensajes. + abrir + Seleccionar + Seleccionados %d + Configuración + ¿Confirmas la eliminación del contacto? + ¡Contacto eliminado! + El contacto será eliminado. ¡No podrá deshacerse! + ¡Conversación eliminada! + Elimina sin notificar + Sólo borrar la conversación + Conservar conversación + buscar + Contactos archivados + Servidores de mensajes + Servidores de archivos y multimedia + Proxy SOCKS + No + Continuar + Algunos archivos no han sido exportados + No se puede llamar al contacto + No se puede llamar al miembro del grupo + No se pueden enviar mensajes al miembro del grupo + El contacto está eliminado. + Invitar + Por favor, pide a tu contacto que active las llamadas. + Enviar mensaje para activar llamadas. + Elimina hasta 20 mensajes a la vez. + Barra de herramientas accesible + Archiva contactos para charlar más tarde. + Puedes guardar el archivo exportado. + Fuerte + Los mensajes serán eliminados para todos los miembros. + Los mensajes serán marcados como moderados para todos los miembros. + video + Puedes enviar mensajes a %1$s desde Contactos archivados + Aún puedes ver la conversación con %1$s en la lista de chats. + Puedes migrar la base de datos exportada. + Conexión TCP + Usa la aplicación con una sola mano. + La dirección del servidor de destino de %1$s es incompatible con la configuración del servidor de reenvío %2$s. + La versión del servidor de destino de %1$s es incompatible con el servidor de reenvío %2$s. + Tus contactos + Necesitas permitir que tus contacto llamen para poder llamarles. + Error al conectar con el servidor de reenvío %1$s. Por favor, inténtalo más tarde. + La dirección del servidor de reenvío es incompatible con la configuración de red: %1$s. + El servidor de reenvío %1$s no ha podido conectarse al servidor de destino %2$s. Por favor, intentalo más tarde. + La versión del servidor de reenvío es incompatible con la configuración de red: %1$s. + Ningún contacto filtrado + Difumina para mayor privacidad + Crear + Alternar lista de chats: + Ajusta el tamaño de la fuente. + Reproduce desde la lista de chats. + Actualizar la aplicación automáticamente + Invitar + Nueva experiencia de chat 🎉 + Nuevas opciones multimedia + Puedes cambiar la posición de la barra desde el menú Apariencia. + Descarga nuevas versiones desde GitHub. + Nuevo mensaje + Enlace no válido + Por favor, comprueba que el enlace SimpleX es correcto. \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml index 8ef94b53fe..2fe4ec452e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fa/strings.xml @@ -5,8 +5,8 @@ %1$d پیام از قلم افتاده %1$s عضو %1$s می‌خواهد به شما متصل شود، به وسیله - 1 روز - 1 دقیقه + ۱ روز + ۱ دقیقه لغو لغو تغییر نشانی تغییر نشانی را لغو می‌کنید؟ @@ -36,7 +36,7 @@ مسیر نامعتبر پرونده برنامه از کار افتاد در حال تلاش برای اتصال به سرور مورد استفاده برای دریافت پیام‌ها از این مخاطب (خطا: %1$s). - حذف شده + حذف شد علامت گذاشته شده به عنوان حذف شده توسط %s حذف شد مسدود @@ -49,15 +49,15 @@ داده نامعتبر خطا در نمایش محتوا خطا در رمزگشایی - 5 دقیقه + ۵ دقیقه درباره نشانی سیمپل‌اکس(SimpleX) لینک یک بار مصرف درباره سیمپل‌اکس چت(SimpleX Chat) %1$d پیام از قلم افتاد. - 1 ماه - 1 هفته - 6 زبان جدید برای رابط کاربری - 30 ثانیه + ۱ ماه + ۱ هفته + ۶ زبان جدید برای رابط کاربری + ۳۰ ثانیه شما لینک یک بار مصرف ناشناس به اشتراک گذاشتید به وسیله لینک گروه ناشناس به وسیله لینک گروه @@ -229,25 +229,25 @@ توقف دریافت پرونده متوقف خواهد شد. خوش آمدید! - خطا در رمزگشایی + خطا در کدبرداری ارسال پیام مستقیم برای اتصال لطفا، تا زمانی که پرونده در حال بارگیری از موبایل متصل است، منتظر باشید. حذف مخاطب - مشاهده رمز امنیتی - تایید رمز امنیتی + مشاهده کد امنیتی + تایید کد امنیتی ارسال پیام ناپدید شونده لینک دعوت یک‌بارمصرف ایجاد گروه محرمانه (برای اشتراک‌گذاری با مخاطبتان) اگر لینک دعوت SimpleX Chat دریافت کردید، می‌توانید آن را در مرورگر خود باز کنید: - ورود رمز عبور + ورود کد عبور %d ثانیه - رمز عبور فعلی + کد عبور فعلی کپی پیام به عنوان حذف شده علامت‌گذاری خواهد شد. گیرنده‌ها قادر خواهند بود این پیام را آشکار کنند. ارسال غیرموفق برای اتصال لمس کنید - امکان رمزگشایی ویدئو وجود ندارد. لطفا، ویدئوی دیگری را امتحان کنید یا با توسعه‌دهندگان تماس بگیرید. + امکان کدبرداری ویدئو وجود ندارد. لطفا، ویدئوی دیگری را امتحان کنید یا با توسعه‌دهندگان تماس بگیرید. تصویر پرونده پیدا نشد لمس دکمه @@ -278,10 +278,10 @@ تماس تصویری برای محافظت از اطلاعاتتان، قفل SimpleX را روشن کنید. \nاز شما خواسته خواهد شد قبل از فعال شدن این ویژگی، تصدیق را تکمیل کنید. - عدم وجود رمز عبور + عدم وجود کد عبور %d دقیقه فعال‌سازی قفل SimpleX - وارد کردن رمز عبور + وارد کردن کد عبور ذخیره شده فرستاده شده فرستاده شده از @@ -323,7 +323,7 @@ مخاطب و تمام پیام‌ها حذف خواهند شد - این عمل قابل برگشت نیست! پیام‌های صوتی مجازند؟ پیام‌های صوتی ممنوع هستند! - اسکن رمز QR.]]> + اسکن کد QR.]]> اشتراک‌گذاری پرونده… تعداد تصویر بیش از اندازه! برای اسکن لمس کنید @@ -376,7 +376,7 @@ در انتظار تصویر پیام صوتی… تعیین نام مخاطب… - قطع اتصال + قطع شد در حال انتظار نشانی‌ دریافت تغییر کند؟ تغییر نشانی‌ لغو خواهد شد. نشانی‌ دریافت پیشین استفاده خواهد شد. @@ -384,7 +384,7 @@ فقط صاحبان گروه می‌توانند پیام‌های صوتی را فعال کنند. تایید بازنشاندن - اسکن رمز QR + اسکن کد QR (اسکن یا الصاق از حافظه) (تنها ذخیره شده توسط اعضای گروه) فعال کردن دسترسی دوربین @@ -404,7 +404,7 @@ ضبط پیام صوتی ارسال پیام بدون جزئیات - اتصال به وسیله لینک / رمز QR + اتصال به وسیله لینک / کد QR دوربین موجود نیست تصویر ایجاد لینک دعوت یک‌بارمصرف @@ -413,14 +413,14 @@ اتصال از طریق لینک تصویر وقتی دریافت خواهد شد که مخاطبتان آنلاین شود، لطفا صبر کنید یا بعدا بررسی کنید! نوع قفل SimpleX - تصدیق سامانه - تغییر رمز عبور + تصدیق سیستم + تغییر کد عبور در پاسخ به فوری تصدیق دستگاه فعال نیست. زمانی که تصدیق دستگاه را فعال کنید می‌توانید قفل SimpleX را از طریق تنظیمات روشن کنید. اجازه دادن ضمیمه - امکان رمزگشایی تصویر وجود ندارد. لطفا، تصویر دیگری را امتحان کنید یا با توسعه‌دهندگان تماس بگیرید. + امکان کدبرداری تصویر وجود ندارد. لطفا، تصویر دیگری را امتحان کنید یا با توسعه‌دهندگان تماس بگیرید. پرونده‌ها و رسانه ممنوع است! تصویر ارسال شد در انتظار ویدئو @@ -470,7 +470,7 @@ راهنمای مارکداون لینک نامعتبر! اشتراک‌گذاری لینک یک بار مصرف - یا رمز QR را اسکن کنید + یا کد QR را اسکن کنید تلاش مجدد عبارت عبور و صدور پایگاه داده افزودن سرورهای از پیش تنظیم شده @@ -483,7 +483,7 @@ SimpleX Chat را برای ترمینال نصب کنید در GitHub ستاره بزنید همکاری کنید - رمز QR + کد QR یک نمایه تصادفی جدید به اشتراک گذاشته خواهد شد. اتصال از طریق لینک لینک دعوت یک‌بارمصرف @@ -506,11 +506,11 @@ وقتی دستگاه میزبان گروه آنلاین شد، به گروه متصل خواهید شد، لطفا صبر کنید یا بعدا بررسی کنید! لینکی که دریافت کردید را الصاق کنید تا به مخاطبتان متصل شوید… نمایه شما %1$s به اشتراک گذاشته خواهد شد. - برای اتصال، مخاطبتان می‌تواند رمز QR را اسکن یا از لینک در برنامه استفاده کند. - اگر نمی‌توانید ملاقات حضوری داشته باشید، رمز QR را در یک تماس تصویری نمایش دهید، یا لینک را به اشتراک بگذارید. - می‌توانید نشانی‌ خود را به صورت لینک یا رمز QR به اشتراک بگذارید - هر کسی می‌تواند به شما متصل شود. + برای اتصال، مخاطبتان می‌تواند کد QR را اسکن یا از لینک در برنامه استفاده کند. + اگر نمی‌توانید ملاقات حضوری داشته باشید، کد QR را در یک تماس تصویری نمایش دهید، یا لینک را به اشتراک بگذارید. + می‌توانید نشانی خود را به صورت لینک یا کد QR به اشتراک بگذارید - هر کسی می‌تواند به شما متصل شود. وقتی اشخاص درخواست اتصال کنند، شما می‌توانید آن را بپذیرید یا رد کنید. - یا این رمز را نشان دهید + یا این کد را نشان دهید می‌توانید دوباره لینک دعوت را در جزئیات اتصال مشاهده کنید. نگه‌داشتن در حال ایجاد لینک… @@ -520,31 +520,31 @@ سرور از پیش تنظیم شده نشانی‌ سرور نامعتبر! به‌کارگیری برای اتصال‌های جدید - رمز QR نامعتبر - رمز امنیتی نادرست! - رمز امنیتی را از برنامه مخاطبتان اسکن کنید. + کد QR نامعتبر + کد امنیتی نادرست! + کد امنیتی را از برنامه مخاطبتان اسکن کنید. علامت‌گذاری به عنوان تایید شده %s تایید نشده است برای تایید رمزگذاری سرتاسر، روی دستگاه‌های خود، کد را با مخاطبتان مقایسه(یا اسکن) کنید. سرورهای XFTP شما - چگونه + روش استفاده در حال استفاده از سرورهای SimpleX Chat. تنظیم سرورهای ICE می‌خواهد به شما متصل شود! وقتی دستگاه مخاطبتان آنلاین شد، متصل خواهید شد، لطفا صبر کنید یا بعدا بررسی کنید! - رمز QR را در تماس تصویری اسکن کنید، یا مخاطبتان می‌تواند یک لینک دعوت به اشتراک بگذارد.]]> + کد QR را در تماس تصویری اسکن کنید، یا مخاطبتان می‌تواند یک لینک دعوت به اشتراک بگذارد.]]> دعوت استعمال نشده نگه داشته شود؟ نشانی‌ SimpleX شما مارکداون در پیام‌ها ایده‌ها و سوالات را ارسال کنید به ما ایمیل بفرستید قفل SimpleX - افزودن سرور… + افزودن سرور آزمایش سرورها ذخیره سرورها عدم موفقیت آزمایش سرور! عدم موفقیت آزمایش چند سرور: - اسکن رمز QR سرور + اسکن کد QR سرور سرورهای SMP شما سرورهای XFTP از سرورهای SimpleX Chat استفاده شود؟ @@ -557,7 +557,7 @@ نشانی‌ SimpleX پاک‌سازی تایید نشانی‌ سرور از پیش تنظیم شده - رمز امنیتی + کد امنیتی به‌کارگیری از سرور سرورها برای اتصال‌های جدید نمایه گپ فعلی شما سرورها ذخیره شوند؟ @@ -567,17 +567,17 @@ لازم است مخاطبتان آنلاین باشد تا اتصال کامل شود. \nمی‌توانید این اتصال را لغو و مخاطب را حذف کنید (و بعدا با یک لینک جدید امتحان کنید). نشانی‌ SimpleX - این رمز QR یک لینک نیست! + این کد QR یک لینک نیست! راهنمای کاربر.]]> این رشته متن، یک لینک اتصال نیست! - رمزی که اسکن کردید یک رمز QR لینک SimpleX نیست. - اسکن رمز + کدی که اسکن کردید یک کد QR لینک SimpleX نیست. + اسکن کد تصویر نمایه بیشتر - نمایش رمز QR - رمز QR نامعتبر + نمایش کد QR + کد QR نامعتبر وقتی درخواست اتصال شما پذیرفته شد، متصل خواهید شد، لطفا صبر کنید یا بعدا بررسی کنید! - رمز QR را در تماس تصویری نمایش دهید، یا لینک را به اشتراک بگذارید.]]> + کد QR را در تماس تصویری نمایش دهید، یا لینک را به اشتراک بگذارید.]]> نمایه گپ شما ارسال خواهد شد \nبه مخاطبتان اطلاعات بیشتر @@ -588,7 +588,6 @@ میزبان‌های Onion برای اتصال الزامی خواهد بود. \nلطفا توجه داشته باشید: شما بدون نشانی‌ onion. قادر نخواهید بود به سرورها متصل شوید. ذخیره - تنظیمات میزبان‌های onion. به روز شود؟ پورت از پروکسی SOCKS استفاده شود؟ از اتصال مستقیم اینترنت استفاده شود؟ @@ -597,7 +596,6 @@ هاست الزامی از میزبان‌های Onion وقتی موجود باشند استفاده خواهد شد. - از میزبان‌های Onion وقتی موجود باشند استفاده خواهد شد. ظاهر نسخه برنامه: v%s نمایش گزینه‌های توسعه‌دهنده @@ -608,12 +606,11 @@ برای هر نمایه گپی که در برنامه دارید استفاده خواهد شد.]]> نمایش خطاهای داخلی نمایش تماس‌های کند API - از میزبان‌های Onion استفاده نخواهد شد. خیر استفاده از میزبان‌های onion. را روی «خیر» تنظیم کنید اگر پروکسی SOCKS از آنها پشتیبانی نمی‌کند.]]> سفارشی کردن تم نسخه برنامه - رنگ‌های تم + رنگ‌های رابط کاربری نسخه هسته: v%s simplexmq: v%s (%2s) اعلان‌ها از کار خواهند افتاد تا زمانی که برنامه را دوباره راه‌اندازی کنید @@ -630,7 +627,6 @@ اتصال ساختار برنامه: %s تنظیمات شبکه - میزبان‌های Onion برای اتصال الزامی خواهد بود. نمایش: لطفا توجه داشته باشید: واسطه‌های پیام و پرونده از طریق پروکسی SOCKS متصل می‌شوند. تماس‌ها و ارسال پیش‌نمایش‌های لینک از اتصال مستقیم استفاده می‌کنند.]]> گزینه‌های توسعه‌دهنده @@ -682,4 +678,1187 @@ یک نشانی ایجاد کنید تا اشخاص بتوانند به شما متصل شوند. نمایه شما روی دستگاهتان ذخیره شده و فقط با مخاطبانتان به اشتراک گذاشته می‌شود. سرورهای SimpleX قادر به دیدن نمایه شما نیستند. خطا در ذخیره کردن کلمه عبور کاربر + تماس‌های صوتی و تصویری + تماس‌های شما + همیشه از واسطه استفاده شود + همتا به همتا + متناوب + تماس در حال انتظار + تماس ناموفق + تماس پایان یافت + پاسخ به تماس + فوری + از طریق واسطه + صدا روشن + چرخش دوربین + پیش‌نویس پیام + وقتی برنامه در حال اجراست + تماس صوتی رمزگذاری سرتاسر شده + پذیرفتن + حریم خصوصی شما + حالت قفل + ارسال + کد عبور نادرست + کد عبور + مخاطبان + غیرفعال کردن (نگه‌داشتن مقدارهای جایگزین شده) + فعال کردن (نگه‌داشتن مقدارهای جایگزین شده گروه) + غیرفعال برای همه گروه‌ها + کمک + گپ‌ها + اجرای گپ + گپ در حال اجراست + توقف + حذف تمام پرونده‌ها + اصلاح نام به %s؟ + بعدا از طریق تنظیمات قابل تغییر است. + تماس پذیرفته نشده + هش پیام ناصحیح + هش پیام قبلی متفاوت است. + شناسه پیام ناصحیح + رسیدها فعال شوند؟ + شما + پیام‌ها و پرونده‌ها + تماس‌ها + حالت ناشناس + عبارت عبور پایگاه داده + صدور پایگاه داده + نمایه گپ حذف شود؟ + نام خود را وارد کنید: + ایجاد + روش استفاده از مارکداون + می‌توانید از مارکداون برای آرایش پیام‌ها استفاده کنید: + تماس بی‌پاسخ + تماس پذیرفته + نامتمرکز + نمایه خود را ایجاد کنید + SimpleX چگونه کار می‌کند + اگر SimpleX هیچ شناسه کاربری ندارد، چگونه می‌تواند پیام‌ها را تحویل دهد؟]]> + مطالعه بیشتر در مخزن GitHub ما. + مخزن GitHub ما.]]> + استفاده از گپ + بهترین گزینه برای باتری. شما اعلان‌ها را فقط وقتی دریافت می‌کنید که برنامه در حال اجراست (بدون سرویس پس‌زمینه).]]> + تماس‌ها روی صفحه قفل: + پذیرفتن + سرور واسط از نشانی IP شما محافظت می‌کند، اما سرور می‌تواند مدت تماس را مشاهده کند. + گشودن + رمزگذاری سرتاسر شده + قطع تماس + ویدئو خاموش + ویدئو روشن + صدا خاموش + بلندگو خاموش + بلندگو روشن + در حال اتصال تماس + فعال کردن قفل + کد عبور تعیین شد! + کد عبور تغییر کرد! + تغییر حالت قفل + خودتخریبی + فعال کردن کد عبور خودتخریبی + تغییر حالت خودتخریبی + کد عبور خودتخریبی فعال شد! + کد عبور خودتخریبی + نام نمایشی جدید: + اگر کد عبور خودتخریبی خود را زمان باز کردن برنامه وارد کنید: + تمام اطلاعات برنامه حذف می‌شود. + این تنظیمات برای نمایه فعلی شما هستند + ارسال رسید برای %d مخاطب فعال است + غیرفعال برای همه + فعال برای همه گروه‌ها + غیرفعال کردن (نگه‌داشتن مقدارهای جایگزین شده گروه) + توقف برنامه + تم‌ها + حذف پایگاه داده + پایگاه داده گپ وارد شد + %d پرونده با اندازه کل %s + رمزگذاری سرتاسر دو لایه را ذخیره می‌کنند.]]> + نادیده گرفتن + بلوتوث + وارد کردن پایگاه داده + اشخاص فقط از طریق لینک‌هایی که به اشتراک می‌گذارید می‌توانند به شما متصل شوند. + دریافت شوند و از چه سرورهایی به مخاطبان خود پیام می‌فرستید.]]> + تماس از پیش پایان یافته! + هش پیام ناصحیح + پذیرفتن خودکار تصاویر + ارسال پیش‌نمایش‌های لینک + سیستم + کد عبور برنامه + خاموش + ارسال رسید برای %d گروه فعال است + ارسال رسید برای %d گروه غیرفعال است + حمایت از SIMPLEX CHAT + پروکسی SOCKS + استفاده از کامپیوتر + آرشیو پایگاه داده جدید + آرشیو پایگاه داده قدیمی + خطا در شروع گپ + ایمن در برابر اسپم و سو استفاده + چگونه کار می‌کند + تماس تصویری + گوشی + بلندگو + هدفون‌ها + نسل بعدی پیام‌رسانی خصوصی + پایان یافت + خطا در باز کردن مرورگر + جهت صدور عبارت عبور تعیین کنید + محافظت از صفحه برنامه + شروع مجدد + ویژگی‌های آزمایشی + باز کردن پوشه پایگاه داده + گپ متوقف شده است + خطا در حذف پایگاه داده گپ + خطا در وارد کردن پایگاه داده گپ + شما گپ خود را کنترل می‌کنید! + نمایه، مخاطبان و پیام‌های تحویل داده شده شما روی دستگاهتان ذخیره می‌شوند. + نمایه فقط با مخاطبانتان به اشتراک گذاشته می‌شود. + نام نمایشی نمی‌تواند شامل نویسه‌های فاصله باشد. + ایجاد نمایه + نام نامعتبر! + برجسته + در انتظار تایید… + پاسخ دریافت شد… + تایید دریافت شد… + در حال اتصال… + در انتظار پاسخ… + تعیین عبارت عبور پایگاه داده + استفاده از عبارت عبور تصادفی + تماس صوتی + سرورهای ICE شما + تماس در جریان است + شناسه پیام ناصحیح + حریم خصوصی و امنیت + کد عبور جدید + تصدیق لغو شد + نمایش آخرین پیام‌ها + فعال کردن خودتخریبی + کد عبور برنامه با کد عبور خودتخریبی جایگزین می‌شود. + اگر کد عبور خودتخریبی خود را زمان باز کردن برنامه وارد کنید، تمام اطلاعات برنامه به صورت غیر قابل بازگشت حذف خواهد شد! + کد عبور خودتخریبی تغییر کرد! + رسیدها برای گروه‌ها فعال شوند؟ + فعال برای همه + آن‌ها در تنظیمات مخاطب و گروه قابل جایگزینی هستند. + گروه‌های کوچک (حداکثر ۲۰) + تنظیمات + اولین بن‌سازه بدون هیچ شناسه کاربری - با طرح‌ریزی خصوصی + اعلان‌های خصوصی + عبارت عبور تصادفی در تنظیمات به صورت متن آشکار ذخیره می‌شود. +\nمی‌توانید بعدا آن را تغییر دهید. + تماس تصویری رسیده + تماس صوتی رسیده + تماس تصویری رمزگذاری سرتاسر شده + تماس صوتی (رمزگذاری سرتاسر نشده) + بدون رمزگذاری سرتاسر + مخاطب رمزگذاری سرتاسر دارد + این اتفاق وقتی می‌افتد که شما یا اتصالتان از پشتیبان پایگاه داده قدیمی استفاده کرده باشید. + مذاکره مجدد رمزگذاری ناموفق بود. + رمزگذاری پرونده‌های محلی + پشتیبان‌گیری اطلاعات برنامه + قفل بعد از + تایید کد عبور + کد عبور تغییر نکرد! + ایجاد نمایه + مورب + ما هیچکدام از مخاطبان و پیام‌های(وقتی تحویل داده شدند) شما را روی سرورها ذخیره نمی‌کنیم. + رنگی + محرمانه + در حال تماس… + تماس ناموفق + تماس در جریان است + تماس پایان یافت %1$s + خطا در تماس + در حال راه‌اندازی… + متصل + در حال برقراری تماس… + تماس تصویری (رمزگذاری سرتاسر نشده) + رد کردن + تعیین کد عبور + رسیدها برای گروه‌ها غیرفعال شوند؟ + تغییر کد عبور خودتخریبی + رسیدهای تحویل ارسال شوند به + ابزارهای توسعه‌دهنده + برنامه + دستگاه + آزمایشی + اتصال شبکه + آیکون برنامه + پایگاه داده گپ + بن‌سازه پیام‌رسانی و کاربردی که از حریم خصوصی و امنیت شما محافظت می‌کند. + گزینه خوب برای باتری. سرویس پس‌زمینه هر ۱۰ دقیقه پیام‌ها را بررسی می‌کند. ممکن است تماس‌ها یا پیام‌های ضروری را از دست دهید.]]> + پیام‌ها از قلم افتادند + مرورگر وب پیش‌فرض برای تماس‌ها لازم است. لطفا مرورگر پیش‌فرض را در سیستم تنظیم کنید، و اطلاعات بیشتر را با توسعه‌دهندگان به اشتراک بگذارید. + سرور واسط فقط در زمان نیاز مورد استفاده قرار می‌گیرد. طرف دیگری قادر به مشاهده نشانی IP شما خواهد بود. + نمایش + غیرفعال + گشودن SimpleX Chat برای پذیرفتن تماس + تماس‌ها از صفحه قفل را از طریق تنظیمات فعال کنید. + مخاطب رمزگذاری سرتاسر ندارد + سرورهای WebRTC ICE + شناسه پیام بعدی نادرست است (کمتر یا برابر است با قبلی). +\nبروز این اتفاق می‌تواند به دلیل وجود اشکال نرم‌افزاری یا مورد حمله قرار گرفتن اتصال باشد. + لطفا آن را به توسعه‌دهندگان گزارش دهید. + پیام همسان + گپ متوقف شود؟ + پایگاه داده با استفاده از یک عبارت عبور تصادفی رمزگذاری شده، لطفا پیش از صدور آن را تغییر دهید. + خطا در متوقف کردن گپ + خطا در صدور پایگاه داده گپ + پایگاه داده گپ وارد شود؟ + وارد کردن + به منظور استفاده از پایگاه داده گپ وارد شده، برنامه را شروع مجدد کنید. + چند خطای غیر مهلک هنگام وارد کردن رخ داد - برای اطلاعات بیشتر می‌توانید کنسول گپ را ببینید. + پایگاه داده گپ حذف شد + به منظور ایجاد نمایه گپ جدید، برنامه را شروع مجدد کنید. + پرونده‌ها و رسانه + حذف پرونده‌ها برای تمام نمایه‌های گپ + پرونده‌ها و رسانه حذف شوند؟ + هیچ پرونده دریافتی یا ارسالی وجود ندارد + هرگز + %s ثانیه + اجرای اتصال خصوصی + جابه‌جایی از دستگاهی دیگر + یک نمایه گپ خالی با نام فراهم شده ایجاد می‌شود، و برنامه به طور معمول باز می‌شود. + اعطای اجازه‌ها در تنظیمات + این مجوز را در تنظیمات اندروید پیدا و به صورت دستی آن را اعطا کنید. + باز کردن تنظیمات + پروتکل و کد متن‌باز - هر کسی می‌تواند سرورها را راه‌اندازی کند. + رسیدها غیرفعال شوند؟ + فعال کردن (نگه‌داشتن مقدارهای جایگزین شده) + اعطای اجازه‌ها + میکروفون + دوربین + دوربین و میکروفون + اعطای اجازه‌ها برای برقراری تماس‌ها + حریم خصوصی باز تعریف شده + برای حفاظت از حریم خصوصی، به جای شناسه‌های کاربری مورد استفاده در بن‌سازه‌های دیگر، SimpleX شناسه‌هایی برای صفوف پیام دارد، جدا برای هر کدام از مخاطبان شما. + از باتری بیشتر استفاده می‌کند! سرویس پس‌زمینه همیشه در حال اجراست - اعلان‌ها به محض موجود شدن، نمایش داده می‌شوند.]]> + وقتی می‌تواند اتفاق بیفتد که: +\n۱. پیام‌ها در کلاینت فرستنده بعد از ۲ روز یا روی سرور بعد از ۳۰ روز منقضی شده باشند. +\n۲. رمزگشایی پیام ناموفق بود، چون شما یا مخاطبتان از پشتیبان پایگاه داده قدیمی استفاده استفاده کردید. +\n۳. اتصال مورد حمله قرار گرفته باشد. + تصاویر نمایه + به منظور صدور، ورود و حذف پایگاه داده گپ، گپ را متوقف کنید. هنگامی که گپ متوقف شده است، شما قادر به دریافت و ارسال پیام نخواهید بود. + پایگاه داده گپ فعلی شما حذف و توسط پایگاه داده وارد شده جایگزین خواهد شد. +\nاین عمل قابل برگشت نیست - نمایه، مخاطبان، پیام‌ها و پرونده‌های شما به صورت غیر قابل بازگشت از بین خواهند رفت. + پایگاه داده گپ شما + این عمل قابل برگشت نیست - نمایه، مخاطبان، پیام‌ها و پرونده‌های شما به صورت غیر قابل بازگشت از بین خواهند رفت. + ارسال رسید برای %d مخاطب غیرفعال است + این عمل قابل برگشت نیست - تمام پرونده‌ها و رسانه دریافتی حذف خواهند شد. عکس‌های با کیفیت پایین باقی خواهند ماند. + شما باید از تازه‌ترین نسخه پایگاه داده گپ خود روی فقط یک دستگاه استفاده کنید، در غیر این صورت ممکن است از بعضی از مخاطب‌ها ‌دیگر پیامی دریافت نکنید. + پیام‌ها + به منظور فعال‌سازی اقدامات پایگاه داده، گپ را متوقف کنید. + این عمل قابل برگشت نیست - پیام‌های ارسالی و دریافتی قدیمی‌تر از زمان انتخابی حذف خواهند شد. این کار ممکن است چندین دقیقه زمان ببرد. + خطا در تغییر تنظیمات + ذخیره عبارت عبور در تنظیمات + حذف پیام‌ها + ذخیره عبارت عبور در مخزن کلید + این تنظیمات بر پیام‌های موجود در نمایه گپ فعلی شما اعمال می‌شود + حذف خودکار پیام فعال شود؟ + برگرداندن + ارتقا و گشودن گپ + آرشیو گپ + ذخیره آرشیو + حذف آرشیو + دعوت به گروه %1$s + به گروه می‌پیوندید؟ + ترک + دریافت پیام‌ها از این گروه برای شما متوقف خواهد شد. تاریخچه گپ حفظ خواهد شد. + دعوت اعضا + %d رویداد گروه + %s، %s و %d عضو دیگر متصل شدند + %s و %s + %s، %s و %d عضو + و %d رویداد دیگر + گشودن + کد امنیتی تغییر پیدا کرد + وضعیت ناشناخته + سازنده + عبارت عبور جدید… + پایگاه داده رمزگذاری و عبارت عبور در تنظیمات ذخیره خواهد شد. + عبارت عبور رمزگذاری پایگاه داده به‌روز و در تنظیمات ذخیره خواهد شد. + امکان دسترسی مخزن کلید برای ذخیره کلمه عبور پایگاه داده وجود ندارد + خطای پایگاه داده ناشناخته: %s + تایید جابه‌جایی نامعتبر + شما به این گروه پیوستید + شما ترک کردید + نمایه گروه به‌روز شد + عضو + مدیر + صاحب + حذف شد + نقش عضو جدید + عبارت عبور از تنظیمات پاک شود؟ + لطفا توجه داشته باشید: اگر عبارت عبور را از دست بدهید، قادر نخواهید بود آن را بازیابی کنید یا تغییر دهید.]]> + عبارت عبور پایگاه داده اشتباه + پرونده: %s + عبارت عبور پایگاه داده برای گشودن گپ الزامی است. + خطای ناشناخته + گشودن گپ + تلاش برای تغییر عبارت عبور پایگاه داده کامل نشد. + ارتقای پایگاه داده + تنزل پایگاه داده + تایید ارتقای پایگاه داده + گروه را ترک می‌کنید؟ + امکان دعوت مخاطبان وجود ندارد! + در حال استفاده از نمایه ناشناس برای این گروه هستید - برای جلوگیری از اشتراک‌گذاری نمایه اصلی شما، دعوت مخاطبان مجاز نیست + مخاطب حذف شد + متصل شد + ترک کرد + %1$s حذف شد + شما حذف شدید + توافق رمزگذاری + ناظر + عضو پیشین %1$s + بسط دادن انتخاب نقش + عبارت عبور رمزگذاری پایگاه داده به‌روز خواهد شد. + مستقیما متصل شد + %s متصل شد + پیوستن به گروه + به‌روزرسانی + تایید عبارت عبور جدید… + پایگاه داده رمزگذاری شود؟ + نسخه پایگاه داده ناسازگار + نمایه گروه به‌روز شد + شما نشانی را برای %s تغییر دادید + شما نشانی را تغییر دادید + مذاکره مجدد رمزگذاری مجاز است + گروه حذف شد + در حال اتصال (دعوت معرفی) + رمزگذاری سرتاسر استاندارد + در حال اتصال (پذیرفته شد) + در حال اتصال (اعلام شد) + متصل شد + کامل + در حال اتصال + ناشناخته + دعوت به گروه + %d مخاطب انتخاب شد + امکان دعوت مخاطب وجود ندارد! + پیوستن + گروه غیرفعال + دعوت‌نامه گروه دیگر اعتبار ندارد، توسط فرستنده پاک شد. + گروه پیدا نشد! + شما به گروه دعوت شیده‌اید + شما دعوت گروه را رد کردید + از %1$s دعوت شده + نقش %s به %s تغییر کرد + گروه حذف شد + مذاکره مجدد رمزگذاری الزامی است + رمزگذاری برای %s بی‌عیب است + مذاکره مجدد رمزگذاری برای %s مجاز است + رمزگذاری سرتاسر مقاوم در برابر کوانتوم + مخاطبی برای افزودن وجود ندارد + رد شدن از دعوت اعضا + انتخاب مخاطبان + پایگاه داده رمزگذاری شده! + عبارت عبور فعلی… + عضو %1$s به %2$s تغییر کرد + رمزگذاری + به‌روزرسانی عبارت عبور پایگاه داده + تعیین عبارت عبور + از مخزن کلید اندروید برای ذخیره امن عبارت عبور استفاده می‌شود - به سرویس اعلان اجازه عمل می‌دهد. + باید هر بار که برنامه شروع می‌شود عبارت عبور را وارد کنید - در دستگاه ذخیره نمی‌شود. + پایگاه داده رمزگذاری شده + مخاطب بررسی شد + پاک کردن + نمایش کنسول در پنجره جدید + می‌توانید گپ را از طریق تنظیمات برنامه / پایگاه داده یا با شروع مجدد برنامه شروع کنید. + گپ شروع شود؟ + هشدار: ممکن است بعضی از اطلاعات را از دست بدهید! + گپ متوقف شده است + خطا در رمزگذاری پایگاه داده + عبارت عبور از مخزن کلید پاک شود؟ + اعلان‌ها فقط تا زمان توقف برنامه تحویل داده خواهند شد! + پاک کردن + تعیین عبارت عبور پایگاه داده + لطفا عبارت عبور فعلی درست را وارد کنید. + پایگاه داده گپ شما رمزگذاری نشده است - برای محافظت از آن عبارت عبور تعیین کنید. + عبارت عبور به صورت متن آشکار در تنظیمات ذخیره شده است. + بعد از تغییر عبارت عبور یا شروع مجدد برنامه، عبارت عبور به صورت متن آشکار در تنظیمات ذخیره خواهد شد. + عبارت عبور پایگاه داده تغییر داده شود؟ + پایگاه داده رمزگذاری خواهد شد. + لطفا عبارت عبور را به صورت امن ذخیره کنید، اگر آن را از دست دهید، قادر نخواهید بود به گپ دسترسی پیدا کنید. + خطا در پایگاه داده + خطا در Keychain + عبارت عبور پایگاه داده با آنچه در مخزن کلید ذخیره شده متفاوت است. + خطا: %s + عبارت عبور اشتباه! + ورود عبارت عبور… + ذخیره عبارت عبور و گشودن گپ + برگرداندن پشتیبان پایگاه داده + پشتیبان پایگاه داده برگردانده شود؟ + لطفا بعد از برگرداندن پشتیبان پایگاه داده، کلمه عبور قبلی را وارد کنید. این عمل قابل برگشت نیست. + خطا در برگرداندن پایگاه داده + نسخه پایگاه داده از برنامه جدیدتر است، اما بدون جابه‌جایی تنزلی برای: %s + جابه‌جایی متفاوت در برنامه/پایگاه داده: %s / %s + جابه‌جایی‌ها: %s + آرشیو گپ + ایجاد شده در %1$s + آرشیو گپ حذف شود؟ + شما به گروه دعوت شده‌اید. برای متصل شدن به اعضای گروه، به گروه بپیوندید. + پیوستن به صورت ناشناس + این گروه دیگر وجود ندارد. + شما دعوت گروه ارسال کردید + برای پیوستن لمس کنید + برای پیوستن به صورت ناشناس لمس کنید + %s مسدود شد + مسدود سازی %s لغو شد + نقش شما به %s تغییر کرد + شما نقش خود را به %s تغییر دادید + شما مسدود سازی %s را لغو کردید + %s و %s متصل شدند + %s، %s و %s متصل شدند + نشانی برای شما تغییر داده شد + در حال تغییر نشانی… + در حال تغییر نشانی برای %s… + در حال تغییر نشانی… + رمزگذاری بی‌عیب است + در حال توافق رمزگذاری برای %s… + توافق رمزگذاری برای %s + نویسنده + دعوت شد + در حال اتصال (معرفی شد) + نقش آغازین + مخاطبی انتخاب نشده + در حال توافق رمزگذاری… + ترک کرد + مخاطب %1$s به %2$s تغییر کرد + نشانی مخاطب حذف شد + تعیین نشانی مخاطب جدید + پایگاه داده با استفاده از عبارت عبور تصادفی رمزگذاری شده، می‌توانید آن را تغییر دهید. + بعد از شروع مجدد برنامه یا تغییر عبارت عبور، از مخزن کلید اندروید برای ذخیره امن عبارت عبور استفاده خواهد شد - اجازه دریافت اعلان‌ها را خواهد داد. + پایگاه داده رمزگذاری و عبارت عبور در مخزن کلید ذخیره خواهد شد. + عبارت عبور رمزگذاری پایگاه داده به‌روز و در مخزن کلید ذخیره خواهد شد. + عبارت عبور درست را وارد کنید. + لطفا عبارت عبور را به صورت امن ذخیره کنید، اگر آن را از دست دهید، قادر به تغییرش نخواهید بود. + عبارت عبور در مخزن کلید پیدا نشد، لطفا به صورت دستی آن را وارد کنید. دلیل این اتفاق ممکن است برگرداندن اطلاعات برنامه با استفاده از یک ابزار پشتیبان‌گیری باشد. اگر این طور نیست، لطفا با توسعه دهندگان تماس بگیرید. + تنزل و گشودن گپ + گپ متوقف شده است. اگر از پیش از این پایگاه داده روی دستگاه دیگری استفاده می‌کردید، بهتر است قبل از شروع گپ، آن را برگردانید. + به این گروه پیوستید. در حال اتصال به عضوی از گروه که از شما دعوت کرد. + دعوت منقضی شد! + دعوت گروه منقضی شد + از طریق لینک گروهتان دعوت شد + شما نقش %s را به %s تغییر دادید + شما %s را مسدود کردید + شما %1$s را حذف کردید + عکس نمایه حذف شد + تعیین عکس نمایه جدید + نمایه به‌روز شد + مذاکره مجدد رمزگذاری برای %s الزامی است + در حال ارسال از طریق + اتصال اصلاح شود؟ + اصلاح توسط عضو گروه پشتیبانی نمی‌شود + نمایه گپ شما به اعضای گروه ارسال خواهد شد + ایجاد لینک + خطا در به‌روزرسانی لینک گروه + زمان توقف پروتکل + می‌توانید این نشانی را با مخاطبان خود به اشتراک بگذارید تا به آن‌ها اجازه دهید به %s متصل شوند. + غیرفعال + رسیدها غیرفعال هستند + برای کنسول + حذف + به‌روزرسانی تنظیمات کلاینت را دوباره به سرورها متصل خواهد کرد. + نمایه گپ حذف شود؟ + خصوصی کردن نمایه! + می‌توانید نمایه کاربر را پنهان یا بی‌صدا کنید - برای نمایش منو لمس کنید و‍ نگه دارید. + لغو پنهان‌سازی نمایه + لغو پنهان‌سازی نمایه گپ + کلمه عبور نمایه + نمایه تصادفی شما + ابتدایی + پیام ارسالی + خیر + همیشه + روشن + پیام خوشامدگویی + حذف عضو + سیستم + تم + پیام دریافتی + لینک حذف شود؟ + می‌توانید یک لینک یا کد QR به اشتراک بگذارید - هر کسی می‌تواند به گروه بپیوندد. اگر بعدا گروه را حذف کنید، اعضای گروه را از دست نخواهید داد. + خطا در ایجاد لینک گروه + خطا در ارسال دعوت + اشتراک‌گذاری نشانی + این گروه بیش از %1$d عضو دارد، رسیدهای تحویل ارسال نمی‌شوند. + نام محلی + شناسه پایگاه داده + ایجاد شد در + دریافت شد در: %s + حذف شد در: %s + توسط مدیر حذف شد در: %s + ناپدید می‌شود در: %s + عضو حذف شود؟ + حذف عضو + مسدودسازی عضو + مسدودسازی + عضو برای همه مسدود شود؟ + لغو مسدودسازی + مسدودسازی عضو برای همه لغو شود؟ + مسدود شده توسط مدیر + مسدود + تغییر + تعویض + نقش گروه تغییر داده شود؟ + اتصال مستقیم؟ + درخواست اتصال به این عضو گروه ارسال خواهد شد. + پیام خوشامدگویی ذخیره شود؟ + مذاکره مجدد رمزگذاری + ذخیره نمایه گروه + به‌روزرسانی + شما هنوز تماس‌ها و اعلان‌های نمایه‌های بی‌صدا را وقتی فعال هستند دریافت می‌کنید. + ناشناس + سیستم + وارد کردن تم + شما اجازه می‌دهید + پیش‌فرض (%s) + گروه برای شما حذف خواهد شد - این عمل قابل برگشت نیست! + ترک گروه + لینک گروه + نشانی + در حال دریافت از طریق + بی‌صدا + بازنشاندن رنگ‌ها + اصلاح + شناسه پایگاه داده: %d + رکورد به‌روز شد در: %s + بدون متن + ارسال پیام مستقیم + پنهان کردن + ارسال شد در + روشن + خطا در وارد کردن تم + مخاطب اجازه می‌دهد + شما در حال دعوت از مخاطبی که با او نمایه ناشناسی به اشتراک گذاشته‌اید به گروهی هستید که در آن از نمایه اصلی خود استفاده می‌کنید + حذف گروه + افزودن پیام خوشامدگویی + وضعیت شبکه + نمایه گروه روی دستگاه‌های اعضا ذخیره می‌شود، نه روی سرورها. + اتصال‌های نمایه و سرور + (فعلی) + عضو + عضو از گروه حذف خواهد شد - این عمل قابل برگشت نیست! + مسدود برای همه + اصلاح توسط مخاطب پشتیبانی نمی‌شود + ثانیه + تمام گپ‌ها و پیام‌ها حذف خواهند شد - این عمل قابل برگشت نیست! + فقط اطلاعات نمایه محلی + دعوت اعضا + گروه حذف شود؟ + گروه برای تمام اعضا حذف خواهد شد - این عمل قابل برگشت نیست! + ایجاد لینک گروه + خطا در حذف لینک گروه + دریافت شد در + حذف شد در + توسط مدیر حذف شد در + ایجاد شد در: %s + عضو مسدود شود؟ + ویرایش نمایه گروه + حذف لینک + خطا در ایجاد مخاطب عضو + %s در %s + %s (فعلی) + نقش + ذخیره و به‌روزرسانی نمایه گروه + وقتی نمایه ناشناسی را با کسی به اشتراک می‌گذارید، این نمایه برای گروه‌هایی که شما را به آن‌ها دعوت می‌کند استفاده خواهد شد. + نام گروه را وارد کنید: + ایجاد گروه + خطا در ذخیره نمایه گروه + بازنشاندن به پیش‌فرض‌ها + صدور تم + خطا در حذف عضو + خطا در تغییر نقش + ثانوی + ارسال شد در: %s + %s: %s + پیام ذخیره شده + تغییر نقش + حذف نمایه + شما: %1$s + تمام اعضای گروه متصل باقی خواهند ماند. + تنها صاحبان گروه می‌توانند تنظیمات گروه را تغییر دهند. + رسیدهای ارسال + رکورد به‌روز شد در + ناپدید می‌شود در + تمام پیام‌های %s پنهان خواهند شد! + مسدودسازی عضو لغو شود؟ + لغو مسدودسازی عضو + لغو مسدودسازی برای همه + پیام‌های %s نشان داده خواهند شد! + نقش به «%s» تغییر داده خواهد شد. تمام افراد گروه مطلع خواهند شد. + نقش به «%s» تغییر داده خواهد شد. عضو یک دعوت جدید دریافت خواهد کرد. + خطا در مسدودسازی عضو برای همه + گروه + اتصال + مستقیم + غیرمستقیم (%1$s) + پیام خوشامدگویی + پیام خوشامدگویی بیش از حد طولانی است + پیش‌نمایش + پیام خوشامدگویی را وارد کنید… + پیام بیش از حد بزرگ است + سرورها + تغییر نشانی دریافتی + اصلاح اتصال + ایجاد گروه محرمانه + تماما نامتمرکز - قابل مشاهده فقط توسط اعضا. + نام کامل گروه: + زمان توقف پروتکل در کیلوبایت + دریافت همزمان + زمان توقف اتصال TCP + وقفه پینگ + شمار پینگ + فعال کردن زنده نگه‌داشتن TCP + ذخیره + تنظیمات شبکه به‌روزرسانی شود؟ + افزودن نمایه + لغو پنهان‌سازی + لغو بی‌صدا + کلمه عبور را در جستجو وارد کنید + برای فعال‌سازی نمایه لمس کنید. + دوباره نمایش داده نشود + بی‌صدا هنگام غیرفعال بودن! + حذف نمایه گپ + حالت ناشناس از حریم خصوصی شما با استفاده از یک نمایه تصادفی جدید برای هر مخاطب محافظت می‌کند. + اجازه می‌دهد اتصال‌های بی‌نام زیادی داشته باشید بدون اطلاعات مشترک بین آن‌ها در تنها یک نمایه گپ. + تاریک + SimpleX + تم تاریک + مطمئن شوید پرونده دارای ترکیب YAML صحیح است. برای داشتن یک نمونه از ساختار پرونده تم، تم را صادر کنید. + ثانوی اضافی + ابتدایی اضافی + پس‌زمینه + منوها و هشدارها + عنوان + بله + تنظیمات مخاطب + حذف برای همه + به مخاطبان خود اجازه حذف پیام‌های ارسالی به صورت غیرقابل برگشت دهید. (۲۴ ساعت) + به مخاطبان خود اجازه افزودن واکنش‌های پیام می‌دهید. + فقط مخاطب شما می‌توانید پیام‌های ناپدید شونده ارسال کنید. + فقط شما می‌توانید پیام‌ها را به صورت غیرقابل برگشت حذف کنید (مخاطبتان می‌تواند آن‌ها را برای حذف علامت‌گذاری کند). (۲۴ ساعت) + فقط مخاطبتان می‌تواند پیام‌ها را به صورت غیرقابل برگشت حذف کند (شما می‌توانید آن‌ها را برای حذف علامت‌گذاری کنید). (۲۴ ساعت) + فقط شما می‌توانید پیام‌های صوتی ارسال کنید. + هر دوی شما و مخاطبتان می‌توانید واکنش‌های پیام اضافه کنید. + فقط شما می‌توانید واکنش‌های پیام اضافه کنید. + ارسال پیام‌های ناپدید شونده را منع می‌کنید. + اجازه ارسال پیام‌های مستقیم را به اعضا می‌دهید. + اجازه ارسال پرونده‌ها و رسانه را می‌دهید. + اعضای گروه می‌توانند پیام‌های ارسالی را به صورت غیرقابل برگشت حذف کنند. (۲۴ ساعت) + حذف بعد از + تنظیمات گفت‌و‌گو + پرونده‌ها و رسانه + تماس‌های صوتی/تصویری + " +\nموجود در نسخه 5.1" + به مخاطبان خود اجازه ارسال پیام‌های صوتی می‌دهید. + فقط زمانی اجازه حذف پیام‌ها به صورت غیرقابل برگشت را می‌دهید که مخاطب شما این اجازه را به شما بدهد. (۲۴ ساعت) + پیام‌های ناپدید شونده در این گروه ممنوع هستند. + تعیین تنظیمات گروه + واکنش‌های پیام + حذف پیام به صورت غیرقابل برگشت در این گپ ممنوع است. + %d هفته + به مخاطبان خود اجازه ارسال پیام‌های ناپدید شونده دهید. + فقط وقتی پیام‌های صوتی را مجاز می‌دانید که مخاطب شما آن‌ها را مجاز می‌داند. + واکنش‌های پیام در این گپ ممنوع هستند. + ارسال پیام‌های صوتی را منع می‌کنید. + اعضای گروه می‌توانند پیام‌های صوتی ارسال کنند. + پذیرفتن + هر دوی شما و مخاطبتان می‌توانید پیام‌های ناپدید شونده ارسال کنید. + فقط شما می‌توانید پیام‌های ناپدید شونده ارسال کنید. + حذف پیام به صورت غیرقابل برگشت را منع می‌کنید. + واکنش‌های پیام‌ها را منع می‌کنید. + ارسال ۱۰۰ پیام آخر به اعضای جدید. + تا ۱۰۰ پیام آخر به اعضای جدید ارسال خواهد شد. + %d ماه + %d ماه + %d دقیقه + فقط وقتی تماس‌ها را مجاز می‌دانید که مخاطب شما آن‌ها را مجاز می‌داند. + منع تماس‌های صوتی/تصویری. + ارسال پرونده‌ها و رسانه را منع می‌کنید. + اجازه ارسال لینک‌های SimpleX را می‌دهید. + تاریخچه به اعضای جدید ارسال نمی‌شود. + اعضای گروه می‌توانند واکنش‌های پیام اضافه کنند. + اعضای گروه می‌توانند لینک‌های SimpleX ارسال کنند. + لینک‌های SimpleX در این گروه ممنوع هستند. + %d ثانیه + %d دقیقه + %d ماه + مدیران + صاحبان + جدید در %s + مطالعه بیشتر + هر دوی شما و مخاطبتان می‌توانید پیام‌های صوتی ارسال کنید. + فقط وقتی واکنش‌های پیام را مجاز می‌دانید که مخاطب شما آن‌ها را مجاز می‌داند. + منع واکنش‌های پیام. + فقط زمانی پیام‌های ناپدید شونده را مجاز می‌دانید که مخاطب شما آن‌ها را مجاز بداند. + ارسال پیام‌های ناپدید شونده را منع می‌کنید. + فقط مخاطبتان می‌تواند پیام‌های صوتی ارسال کند. + %d روز + مخاطبان می‌توانند پیام‌ها را برای حذف علامت بگذارند؛ شما قادر به مشاهده آن‌ها خواهید بود. + خاموش` + تنظیمات گروه + تنظیمات شما + پیام‌های ناپدید شونده + پیام‌های مستقیم + پیام‌های صوتی + لینک‌های SimpleX + تاریخچه قابل رویت + فعال + فعال برای شما + فعال برای مخاطب + خاموش + دریافتی، ممنوع + تعیین ۱ روز + منع ارسال پیام‌ها صوتی. + به مخاطبان خود اجازه تماس با شما را می‌دهید. + پیام‌های ناپدید شونده در این گپ ممنوع هستند. + هر دوی شما و مخاطبتان می‌توانید پیام‌ها را به صورتی غیرقابل برگشت حذف کنید. (۲۴ ساعت) + هر دوی شما و مخاطبتان می‌توانید تماس برقرار کنید. + فقط شما می‌توانید تماس برقرار کنید. + فقط مخاطبتان می‌تواند تماس برقرار کند. + تماس‌های صوتی/تصویری ممنوع هستند. + اجازه ارسال پیام‌های ناپدید شونده می‌دهید. + اجازه ارسال پیام‌های صوتی را می‌دهید. + اجازه واکنش‌های پیام را می‌دهید. + ارسال لینک‌های SimpleX را منع می‌کنید + عدم ارسال تاریخچه به اعضای جدید. + اعضای گروه می‌توانند پیام‌های ناپدید شونده ارسال کنند. + اعضای گروه می‌توانند پیام‌های مستقیم ارسال کنند. + پیام‌های مستقیم بین اعضا در این گروه ممنوع هستند. + حذف غیرقابل برگشت در این گروه ممنوع است. + پیام‌های صوتی در این گروه ممنوع هستند. + واکنش‌های پیام در این گروه ممنوع هستند. + اعضای گروه می‌توانند پرونده‌ها و رسانه ارسال کنند. + پرونده‌ها و رسانه در این گروه ممنوع هستند. + %d ثانیه + %d ساعت + %d ساعت + %d ساعت + %d روز + %d روز + %d هفته + %d هفته + پیشنهاد %s + پیشنهاد %s: %2s + لغو %s + تمام اعضا + فعال برای + چی جدید است + پیام‌های صوتی در این گپ ممنوع هستند. + فقط مخاطبتان می‌تواند واکنش‌های پیام اضافه کند. + اجازه حذف پیام‌های ارسالی به صورت غیرقابل برگشت را می‌دهید. (۲۴ ساعت) + ارسال پیام‌های مستقیم به اعضا را منع می‌کنید. + ظرفیت از محدودیت فراتر رفت - گیرنده پیام‌های ارسالی پیشین را دریافت نکرد. + خطای سرور مقصد: %1$s + خطا: %1$s + هم اکنون در حال پیوستن به گروه هستید! + خطای بسیار مهم + اتصال خاتمه یافت + از مسیریابی خصوصی استفاده نشود. + رسیدهای تحویل! + فعال نشود + خطا در فعال‌سازی رسیدهای تحویل! + دستگاه‌ها + اتصال کامپیوتر قطع شود؟اتصال کامپیوتر قطع شود؟ + قطع شد، به دلیل: %s + نسخه برنامه کامپیوتر %s با این برنامه سازگار نیست. + در انتظار کامپیوتر… + اتصال به کامپیوتر + کامپیوترهای متصل + اتصال کامپیوتر قطع شد + %s در وضع بدی است]]> + به خودتان متصل می‌شوید؟ + خطا در بارگیری آرشیو + لغو جابه‌جایی + هشدار: آرشیو حذف خواهد شد.]]> + هم اکنون در حال اتصال هستید! + همیشه + همیشه از مسیریابی خصوصی استفاده شود. + اعمال + کد عبور برنامه + تماس‌های صوتی و تصویری + سرورها را به وسیله اسکن کد QR اضافه کنید. + مدیران می‌توانند یک عضو را برای همه مسدود کنند. + اجازه تنزل + لطفا توجه داشته باشید: به منظور حفاظت امنیت، استفاده از پایگاه داده یکسان در دو دستگاه، رمزگشایی پیام‌های اتصال‌های شما را از کار خواهد انداخت.]]> + جابه‌جایی از دستگاهی دیگر را انتخاب و کد QR را اسکن کنید.]]> + تایید تنظیمات شبکه + تایید کنید که عبارت عبور پایگاه داده رای برای جابه‌جایی آن به خاطر دارید. + کامپیوتر متصل شد + در حال اتصال به کامپیوتر + اتصال متوقف شد + کامپیوتر غیرفعال است + اتصال متوقف شد + دستگاه‌های کامپیوتر + کامپیوتر پیدا شد + به کامپیوتر متصل شد + تنظیمات کامپیوتر متصل + اسکن کد QR از کامپیوتر + کامپیوتر دارای کد دعوت اشتباه است + کامپیوتر دارای نسخه پشتیبانی نشده است. لطفا، اطمینان حاصل کنید که از نسخه یکسان روی هر دو دستگاه استفاده می‌کنید. + از طریق لینک متصل می‌شوید؟ + رسیدهای تحویل غیرفعال هستند! + در حال بارگیری جزئیات لینک + عبارت عبور را وارد کنید + خطا در حذف پایگاه داده + کدهای امنیتی را با مخاطبان خود مقایسه کنید. + برنامه پرونده‌های جدید محلی (به جز ویدئوها) را رمزگذاری می‌کند. + رمزگذاری پرونده‌ها و رسانه ذخیره شده + به وسیله پروتکل امن مقاوم در برابر کوانتوم. + مسدودسازی اعضای گروه + خطا + %s قطع شد، به دلیل: %s]]> + کامپیوتر + رابط چینی و اسپانیایی + به وسیله نمایه گپ (پیش‌فرض) یا به وسیله اتصال (آزمایشی). + در حین اتصال به کامپیوتر، مهلت زمان اتصال تمام شد. + روز‍ + فعال کردن + سفارشی + قطع اتصال + تایید کد با کامپیوتر + خطا در بارگذاری آرشیو + در حال آرشیو پایگاه داده + ایجاد لینک آرشیو + حذف پایگاه داده از این دستگاه + اتصال به کامپیوتر در وضع بدی است + نام‌ها، آواتارها و انزوای ترابری متفاوت. + چند چیز دیگر + کامپیوتر مشغول است + پیام‌های ناپدید شونده + قطع اتصال + پیام‌های بهتر + اتصال به صورت خودکار + قابل کشف از طریق شبکه محلی + سلولی + ایجاد نمایه جدید در برنامه کامپیوتر. 💻 + نشانی کامپیوتر + الصاق نشانی کامپیوتر + یافتن از طریق شبکه محلی + تمام اطلاعات وقتی وارد می‌شوند پاک می‌شوند. + سفارشی کردن و اشتراک‌گذاری تم‌های رنگ. + تم‌های سفارشی + - اتصال به سرویس فهرست راهنما (آزمایشی)! +\n- رسیدهای تحویل (تا ۲۰ دقیقه). +\n- سریع‌تر و پایداری بیشتر. + گروه‌های بهتر + فعال کردن در گپ های مستقیم (آزمایشی)! + اتصال با کامپیوتر قطع شود؟ + به زودی! + بارگیری موفق نبود + گپ جابه‌جا شد! + آرشیو و بارگذاری + تایید بارگذاری + اتصال اینترنت خود را بررسی و دوباره امتحان کنید + برنامه کامپیوتر جدید + وصل کردن برنامه‌های موبایل و کامپیوتر! 🔗 + یافتن و پیوستن به گروه‌ها + ایجاد یک گروه با استفاده از یک نمایه تصادفی. + جابه‌جایی اطلاعات برنامه + استفاده از کامپیوتر را در برنامه موبایل باز و کد QR را اسکن کنید.]]> + در حال بارگیری آرشیو + خطا در صدور پایگاه داده گپ + خطا در ذخیره تنظیمات + تمام مخاطبان، مکالمات و پرونده‌های شما به صورت امن، رمزگذاری و به صورت بسته‌های داده به واسطه‌های XFTP تنظیم شده، بارگذاری خواهند شد. + موبایل متصل شد + مدیران می‌توانند لینک‌ها را برای پیوستن به گروه‌ها ایجاد کنند. + قطع اتصال موبایل‌ها + به موبایل متصل شد + نام این دستگاه را وارد کنید… + (جدید)]]> + پذیرفتن خودکار درخواست‌های مخاطب + نشانی کامپیوتر ناصحیح + نسخه ناسازگار + عربی، بلغاری، فنلاندی، عبری، تایلندی و اوکراینی - با سپاس از کاربران و Weblate + سرور فرستادن: %1$s +\nخطای سرور مقصد: %2$s + سرور فرستادن: %1$s +\nخطا: %2$s + هشدار تحویل پیام + مشکلات شبکه - پیام بعد از تلاش‌های زیاد برای ارسالش منقضی شد. + نشانی سرور با تنظیمات شبکه ناسازگار است. + نسخه سرور با تنظیمات شبکه ناسازگار است. + کلید اشتباه یا اتصال ناشناخته - به احتمال زیاد این اتصال حذف شده است. + لطفا آن را به توسعه‌دهندگان گزارش دهید: +\n%s + نگه‌داشتن اتصال‌های خود + هرگز + استفاده از مسیریابی خصوصی با سرورهای ناشناخته. + استفاده از مسیریابی خصوصی با سرورهای ناشناخته وقتی نشانی IP محافظت نشده است. + حالت مسیریابی پیام + بله + پیام‌ها مستقیما فرستاده نشود، حتی اگر سرور مقصد شما از مسیریابی خصوصی پشتیبانی نکند. + گزینه پس‌رفت مسیریابی پیام + نمایش وضعیت پیام + برای محافظت از نشانی IP شما، مسیریابی خصوصی از سرورهای SMP شما به منظور تحویل پیام‌ها استفاده می‌کند. + مسیریابی پیام خصوصی + مسیریابی خصوصی + تنظیمات سرور بهبودیافته + با پیام خوشامدگویی اختیاری. + مخاطبان شما می‌توانند اجازه حذف کامل پیام را بدهند. + چندین نمایه گپ + نام‌های پرونده خصوصی + پالایش گپ‌های خوانده نشده و برگزیده. + اصلاح رمزگذاری بعد از برگرداندن پشتیبان‌ها. + مدیریت گروه + رابط کاربری ژاپنی و پرتقالی + ناپدید کردن یک پیام + استفاده باتری کاهش یافته + به جای تصدیق سیستم آن را تعیین کنید. + پشتیبانی از بلوتوث و دیگر بهبودها. + - پیام‌های صوتی تا ۵ دقیقه. +\n- زمان سفارشی برای ناپدید کردن. +\n- ویرایش تاریخچه. + پیوستن سریع‌تر و پیام‌های قابل اطمینان تر. + باز فرستادن و ذخیره پیام‌ها + مربع، دایره، و هر چیزی در این بین. + در گپ‌های مستقیم فعال خواهد شد! + ارسال رسیدهای تحویل برای تمام مخاطبان فعال خواهد شد. + می‌توانید بعدا از طریق تنظیمات آن را فعال کنید + نام این دستگاه + تایید کد در موبایل + %s قطع شد]]> + این دستگاه + در انتظار متصل شدن موبایل: + %s مشغول است]]> + تصادفی + تجدید + %s نسخه پشتیبانی نشده دارد. لطفا، اطمینان حاصل کنید که از نسخه یکسان روی هر دو دستگاه استفاده می‌کنید]]> + %s قطع شد]]> + این ویژگی هنوز پشتیبانی نمی‌شود. انتشار بعدی را امتحان کنید. + %1$s هستید.]]> + گروه از قبل وجود دارد! + شما هم اکنون در حال پیوستن به گروه از طریق این لینک هستید. + در حال وارد کردن آرشیو + یا لینک آرشیو را الصاق کنید + الصاق لینک آرشیو + می‌توانید دوباره امتحان کنید. + پرونده حذف شد یا لینک نامعتبر است + جابه‌جایی دستگاه + در حال آماده‌سازی بارگذاری + نهایی‌سازی جابه‌جایی + یا لینک پرونده را به صورت امن به اشتراک بگذارید + شروع گپ + اترنت باسیم + بهبودهای بیشتر به زودی! + سازگار نیست! + تایید اتصال‌ها + %s غیرفعال است]]> + شما از پیش اتصال به وسیله این نشانی را درخواست کرده‌اید! + شروع مجدد گپ + حتی وقتی در مکالمه غیرفعال باشند. + خیر + موبایل متصلی وجود ندارد + مدیران حالا می‌توانند: +\n- پیام‌های اعضا را حذف کنند. +\n- اعضا را غیرفعال کنند ( نقش «ناظر») + لطفا تایید کنید که تنظیمات شبکه برای این دستگاه درست هستند. + محفوظ نگه داشتن پیش‌نویس پیام آخر، به همراه ضمیمه‌ها. + درخواست اتصال تکرار شود؟ + از موبایل اسکن کنید + ارسال رسیدهای تحویل برای تمام مخاطبان در تمام نمایه‌های گپ قابل مشاهده، فعال خواهد شد. + پیام‌ها مستقیما ارسال شود وقتی نشانی IP محافظت می‌شود و سرور مقصد شما از مسیریابی خصوصی پشتیبانی نمی‌کند. + پیام‌ها مستقیما ارسال شود وقتی سرور مقصد شما از مسیریابی خصوصی پشتیبانی نمی‌کند. + شکل دادن به تصاویر نمایه + با سپاس از کاربران - از طریق Weblate همکاری کنید! + با سپاس از کاربران - از طریق Weblate همکاری کنید! + این لینک یک‌بارمصرف خودتان است! + این نشانی‌ SimpleX خودتان است! + واسطه‌های ناشناخته + محافظت نشده + تایید اتصال + تایید عبارت عبور پایگاه داده + وقتی IP پنهان است + شما هم اکنون در حال اتصال از طریق لینک یک‌بارمصرف هستید! + نمایه‌های گپ پنهان + (این دستگاه v%s)]]> + متصل کردن یک موبایل + پیش‌نویس پیام + حداکثر ۴۰ ثانیه، دریافت فوری. + پیام‌های صوتی + پیام‌های ارسال شده بعد زمان تعیین شده حذف خواهند شد. + رابط فرانسوی + به وسیله یک کلمه عبور از نمایه‌های گپ خود محافظت کنید! + بهبودهای بیشتر به زودی! + با سپاس از کاربران - از طریق Weblate همکاری کنید! + رسیدهای تحویل پیام! + %s به پایان رسید]]> + لطفا آن را به توسعه‌دهندگان گزارش دهید: +\n%s +\n +\nپیشنهاد می‌شود که برنامه را شروع مجدد کنید. + رابط ایتالیایی + - تحویل پیام پایدارتر +\n- گروه‌های کمی بهتر +\n- و بیشتر! + کد عبور خودتخریبی + با سپاس از کاربران - از طریق Weblate همکاری کنید! + ویدئوها و پرونده‌ها تا ۱ گیگابایت + تایید عبارت عبور + لینک نامعتبر + هنگام برقراری تماس‌های صوتی و تصویری. + صداهای تماس + منبع پیام خصوصی باقی خواهد ماند. + رابط کاربری لیتوانی + دقیقه + اتصال شبکه پایدارتر. + مدیریت شبکه + هفته + ماه + انتخاب + دستگاه موبایل جدید + در حال متوقف کردن گپ + بدون اتصال شبکه + دیگر + تایید امنیت اتصال + در حال آماده‌سازی بارگیری + جابه‌جایی کامل شد + نباید از یک پایگاه داده روی دو دستگاه استفاده کنید.]]> + پیام خوشامدگویی گروه + - مطلع کردن اختیاری مخاطبان حذف شده. +\n- نام‌های نمایه شامل فاصله. +\n- و بیشتر! + رابط کاربری مجارستانی و ترکی + جابه‌جایی به دستگاه دیگر از طریق کد QR. + گشودن گروه + WiFi + تماس‌های تصویر در تصویر + استفاده از برنامه در حین مکالمه. + در حال جابه‌جایی + %1$s!]]> + امنیت و حریم خصوصی بهبودیافته + پنهان کردن صفحه برنامه در برنامه‌های اخیر. + پیام‌های زنده + گیرنده‌ها به‌روزرسانی‌ها را هم‌زمان با تایپ کردن شما مشاهده می‌کنند. + برای محافظت از منطقه زمانی، پرونده‌های تصویر/صدا از UTC استفاده می‌کنند. + سریع و بدون منتظر ماندن تا زمانی که فرستنده آنلاین شود. + کاهش بیشتر استفاده باتری + تعیین پیام نمایش داده شده به اعضای جدید! + رابط لهستانی + با سپاس از کاربران - از طریق Weblate همکاری کنید! + واکنش‌های پیام + بالاخره، ما آن‌ها را داریم! 🚀 + تیک دومی که ما نداشتیم! ✅ + برای پنهان کردن پیام‌های ناخواسته. + یادداشت‌های خصوصی + با پرونده‌ها و رسانه رمزگذاری شده. + تحویل پیام بهبود یافته + ساعت + می‌توانید بعدا از طریق تنظیمات حریم خصوصی و امنیت برنامه آن‌ها را فعال کنید. + گشودن پورت در فایروال + %s مفقود است]]> + برای اجازه دادن به برنامه موبایل به کامپیوتر متصل شوید، این پورت را در فایروال خود باز کنید، اگر فعال است + خطای داخلی + جابه‌جایی به اینجا + %1$s هستید.]]> + تکرار بارگیری + وارد کردن ناموفق بود + تکرار وارد کردن + نهایی‌سازی جابه‌جایی در دستگاه دیگر. + برای ادامه دادن، گپ باید متوقف شود. + تکرار بارگذاری + %s بارگذاری شد + بارگذاری ناموفق بود + در حال بارگذاری آرشیو + می‌توانید دوباره امتحان کنید. + خطا در تایید عبارت عبور: + ارزیابی امنیت + گروه‌های ناشناس + حالت ناشناس ساده‌شده + تغییر حالت ناشناس هنگام اتصال. + پیوستن به مکالمات گروه + جهت اتصال لینک را الصاق کنید + تاریخچه اخیر و روبات فهرست راهنمای بهبودیافته. + نوار جستجو لینک‌های دعوت قبول می‌کند. + با استفاده باتری کاهش یافته. + ثانیه + رمزگذاری مقاوم در برابر کوانتوم + گروه‌های امن‌تر + تنها یک دستگاه در هر زمان می‌تواند مورد استفاده قرار گیرد + درخواست پیوستن تکرار شود؟ + به گروه خود می‌پیوندید؟ + %s بارگیری شد + پرونده صادر شده وجود ندارد + جابه‌جایی به دستگاه دیگر + هشدار: شروع گپ روی چندین دستگاه پشتیبانی نمی‌شود و باعث عدم موفقیت در تحویل پیام خواهد شد + نام دستگاه با کلاینت موبایل متصل شده به اشتراک گذاشته خواهد شد. + پیدا کردن سریع‌تر گپ‌ها + لینک‌های گروه + حذف غیرقابل برگشت پیام + امنیت SimpleX Chat به وسیله Tails of Bits مورد سنجش قرار گرفت. + انزوای ترابری + کد نشست + %1$s هستید.]]> + موبایل‌های متصل + سرورهای ناشناخته! + حفاظت از نشانی IP + برنامه از شما خواهد خواست تا بارگیری‌ها از سرورهای پرونده ناشناخته را تایید کنید (به جز .onion یا وقتی پروکسی SOCKS فعال است). + پرونده‌ها + بدون تور یا VPN، نشانی IP شما برای سرورهای پرونده قابل رویت خواهد بود. + بدون تور یا VPN، نشانی IP شما برای این واسطه‌های XFTP قابل رویت خواهد بود: +\n%1$s. + هیچ + تحویل پیام بهبود یافته + رابط کاربری فارسی + با استفاده باتری کاهش یافته. + اشکال‌زدایی تحویل + اطلاعات صف پیام + تم برنامه + تایید پرونده‌ها از سرورهای ناشناخته. + اطلاعات صف سرور: %1$s +\n +\nآخرین پیام دریافتی: %2$s + نمایش فهرست گپ در پنجره جدید + حالت تاریک + سیاه + حالت رنگ + تاریک + رنگ‌های حالت تاریک + روشن + بازنشاندن رنگ + سیستم + خطا در مقداردهی اولیه WebView. سیستم خود را به نسخه جدید به روز کنید. لطفا با توسعه‌دهنگان تماس بگیرید. +\nخطا: 9%s + رنگ‌های گپ + تم گپ + تم نمایه + پس‌زمینه کاغذدیواری + ابتدایی اضافی ۲ + تنظیمات پیشرفته + پر کردن + گنجاندن + عصر به خیر! + صبح به خیر! + پاسخ دریافتی + حذف تصویر + تکرار + مقیاس + پاسخ ارسالی + ابتدایی کاغذدیواری + تعیین تم پیش‌فرض + بازنشاندن به تم برنامه + بازنشاندن به تم کاربر + تمام حالت‌های رنگ + اعمال بر + حالت روشن + مسیریابی پیام خصوصی 🚀 + ظاهر گپ‌های خود را متمایز کنید! + تم‌های جدید گپ + از نشانی IP خود در برابر واسطه‌های پیام‌رسانی انتخاب شده توسط مخاطبانتان محافظت کنید. +\nدر تنظیمات «شبکه و سرورها» فعال کنید. + دریافت امن پرونده‌ها + پرونده یافت نشد - احتمالا حذف یا لغو شده. + کلید اشتباه یا نشانی پرونده ناشناخته - به احتمال زیاد پرونده حذف شده است. + خطای پرونده + خطای پرونده موقت + وضعیت پرونده + وضعیت پیام + وضعیت پرونده: %s + وضعیت پیام: %s + خطای کپی + لطفا بررسی کنید که تلفن همراه و کامپیوتر به شبکه محلی یکسانی متصل هستند، و فایروال کامپیوتر شما اجازه اتصال را میدهد. +\nلطفا هر مشکل دیگری را با توسعه‌دهندگان به اشتراک بگذارید. + این لینک توسط موبایل دیگری استفاده شده است، لطفا لینک جدیدی در کامپیوتر بسازید. + خطای سرور پرونده:%1$s \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml index 1434963263..8200d8a0ff 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fi/strings.xml @@ -16,7 +16,7 @@ 30 sekuntia 5 minuuttia Salli katoavat viestit vain, jos kontaktisi sallii ne. - Lisää palvelin… + Lisää palvelin Lisää tervetuloviesti Salli ääniviestien lähettäminen. Kertakäyttölinkki @@ -94,7 +94,7 @@ Puhelu on jo päättynyt! Tumma teema Yhdistetäänkö kontaktilinkin kautta\? - Yhdistetäänkö ryhmälinkin kautta\? + Liitytäänkö ryhmään? Yhdistetäänkö kutsulinkin kautta\? Yhdistä yhdistää @@ -456,7 +456,7 @@ Virhe roolin vaihdossa %dw Ryhmän moderointi - Näyttönimi: + Profiilin nimi: Virhe käynnistettäessä keskustelua Virhe keskustelun lopettamisessa Virhe asetuksen muuttamisessa @@ -558,7 +558,7 @@ Virheellinen QR-koodi näytä QR-koodi videopuhelussa tai jaa linkki.]]> Virheellinen palvelimen osoite! - Näyttönimi + Kirjoita nimesi: Käännä kamera Se voi tapahtua, kun sinä tai kontaktisi käytitte vanhaa varmuuskopiota tietokannasta. Väärä pääsykoodi @@ -589,7 +589,7 @@ Vanhentunut kutsu! kutsuttu epäsuora (%1$s) - Ryhmän näyttönimi: + Kirjoita ryhmän nimi: Syötä salasana hakuun Ryhmän asetukset Ryhmän jäsenet voivat lisätä viestireaktioita. @@ -623,7 +623,6 @@ Aseta kontaktin nimi… Tallenna salasana Keystoreen Vain kontaktisi voi lähettää katoavia viestejä. - Onion-isäntiä käytetään, kun niitä on saatavilla. Portti portti %d Use .onion hosts arvoon Ei, jos SOCKS-välityspalvelin ei tue niitä.]]> @@ -661,7 +660,6 @@ TCP-yhteyden aikakatkaisu PING-määrä PING-väli - Palauta Profiili- ja palvelinyhteydet Aseta ryhmän asetukset jos SimpleX ei sisällä käyttäjätunnuksia, kuinka se voi toimittaa viestejä\?]]> @@ -873,7 +871,6 @@ Kiitos käyttäjille – osallistu Weblaten kautta! Profiilin salasana Oletusvärit - Tallenna väri Estä ääniviestien lähettäminen. Lähetetyt viestit poistetaan asetetun ajan kuluttua. Aloita uusi keskustelu @@ -917,8 +914,6 @@ Kiitos SimpleX Chatin asentamisesta! Asetukset Lisää - Onion-isäntiä ei käytetä. - Yhteyden muodostamiseen tarvitaan Onion-isäntiä. Yksityiset ilmoitukset Kaiutin pois päältä Kaiutin päällä @@ -957,7 +952,7 @@ Järjestelmä Vain sinä voit soittaa puheluita. Varmista, että WebRTC ICE -palvelinosoitteet ovat oikeassa muodossa, rivieroteltuina ja että ne eivät ole päällekkäisiä. - Verkko &; palvelimet + Verkko & palvelimet QR-koodi Skannaa koodi alkaa… @@ -1133,7 +1128,7 @@ SMP-palvelimesi XFTP-palvelimesi Käytä SimpleX Chat palvelimia\? - TEEMAN VÄRIT + KÄYTTÖLIITTYMÄN VÄRIT Päivitä kuljetuksen eristystila\? Voit luoda sen myöhemmin Voit paljastaa piilotetun profiilisi kirjoittamalla koko salasanan Keskusteluprofiilit-sivun hakukenttään. @@ -1157,7 +1152,6 @@ \nkontakteillesi ICE-palvelimesi Kun saatavilla - Päivitä .onion-isäntien asetus\? Käytä .onion-isäntiä Puhelusi Tätä ryhmää ei enää ole olemassa. @@ -1383,4 +1377,139 @@ 6 uutta käyttöliittymän kieltä Löydä ryhmiä ja liity niihin Luo uusi profiili työpöytäsovelluksessa. 💻 + Virheellinen linkki + Virheellinen QR-koodi + Ei verkkoyhteyttä + %s ladattu + Päivitä + %s ja %s + Istuntokoodi + Vahvista tietokannan tunnuslause + %1$s!]]> + Tämä on oma SimpleX-osoitteesi! + Vahvista tunnuslause + %1$s.]]> + Salli SimpleX-linkkien lähettäminen. + Lisää kontakti + Tai näytä tämä koodi + Lataus epäonnistui + Historiaa ei lähetetä uusille jäsenille. + Arkistoidaan tietokanta + Varoitus: arkisto poistetaan.]]> + Kamera ja mikrofoni + Tarkista internetyhteys ja yritä uudelleen + Yhdistä automaattisesti + Poista tietokanta tältä laitteelta + Virhe selainta avatessa + Tiedoston tila: %s + Laajenna + Hyvää huomenta! + Lisäasetukset + Aseta oletusteema + Vaalea tila + Virhe: %1$s + kaikki jäsenet + Aseta tunnuslause + Älä lähetä historiaa uusille jäsenille. + Kyllä + Tiedosto poistettiin tai linkki on virheellinen + Äänipuhelu + Lopeta puhelu + Videopuhelu + Napauta skannataksesi + Kehittäjävalinnat + Lataa + Tiedostovirhe + Näytä viestin tila + Bluetooth + Kaiutin + Kuulokkeet + Vahvista yhteydet + Sovelluksen teema + Kaikki väritilat + Ryhmäjäsenet voivat lähettää SimpleX-linkkejä. + (uusi)]]> + VIestiä ei voi lähettää + SimpleX-linkit eivät ole sallittuja + Ladataan tiedostoa + Kaikki viestit poistetaan - tätä ei voi perua! + Virheellinen nimi! + Profiilin teema + %s yhdistetty + %s, %s ja %d jäsentä + tuntematon tila + Virhe kutsua lähettäessä + Tiedoston tila + Viestin tila + Viestin tila: %s + Poistetaanko jäsen? + Luo ryhmä + Musta + Väritila + Tumma + Tumman tilan värit + Tumma tila + Hyvää iltapäivää! + Laitteet + Kirjoita tunnuslause + Virhe tietokantaa poistaessa + Viimeistele migraatio toisella laitteella. + tallennettu + Tallennettu + Toteuta + Tuonti epäonnistui + WiFi + Kiinteä ethernet + Sisäinen virhe + Vahvista verkkoasetukset + Viimeistele migraatio + Virheellinen näyttönimi! + Napauta liittääksesi linkin + Virheellinen tiedostopolku + Verkkoyhteys + Katkaise yhteys + Kamera ei saatavilla + Luodaan linkki… + Tai skannaa QR-koodi + Aina + Ei koskaan + Ei + Kamera + Avaa asetukset + Suojaa IP-osoite + TIEDOSTOT + Profiilikuvat + tuntematon + Poista jäsen + SimpleX-linkit + Kvanttiturvallinen salaus + Verkon hallinta + Kirjoita tämän laitteen nimi… + Vahvista yhteys + Työpöytälaitteet + Ei yhteensopiva! + Satunnainen + Ryhmä on jo olemassa! + Kriittinen virhe + Virhe asetuksia tallentaessa + Virhe + Tietokannan migraatio on käynnissä. +\nTämä saattaa kestää muutaman minuutin. + Yritä uudelleen + Näytä sisäiset virheet + Näytä hitaat API-kutsut + Luo profiili + Tämä on oma kertakäyttöinen linkkisi! + Virhe + Hidas funktio + Napauta yhdistääksesi + Viesti on liian pitkä + Virhe viestiä luotaessa + Mikrofoni + Vaalea + Järjestelmä + Poista kuva + Yhteensopimaton versio + Uusi mobiililaite + Tämä laite \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml index 77f0abbf2d..02487f8ba7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/fr/strings.xml @@ -89,7 +89,7 @@ L\'application récupère périodiquement les nouveaux messages - elle utilise un peu votre batterie chaque jour. L\'application n\'utilise pas les notifications push - les données de votre appareil ne sont pas envoyées aux serveurs. SimpleX service de fond - il utilise quelques pour cent de la batterie par jour.]]> Cacher - Afficher l\'aperçu + Aperçu affiché Nom du contact Contact masqué : nouveau message @@ -313,7 +313,7 @@ Scannez le code de sécurité depuis l\'application de votre contact. Pour vérifier le chiffrement de bout en bout avec votre contact, comparez (ou scannez) le code sur vos appareils. scanner un code QR lors d\'un appel vidéo, ou votre contact peut partager un lien d\'invitation.]]> - Ajouter un serveur… + Ajouter un serveur Markdown dans les messages Ajouter des serveurs prédéfinis Utiliser les serveurs SimpleX Chat \? @@ -321,8 +321,6 @@ Assurez-vous que les adresses des serveurs WebRTC ICE sont au bon format et ne sont pas dupliquées, un par ligne. Accéder aux serveurs via un proxy SOCKS sur le port %d \? Le proxy doit être démarré avant d\'activer cette option. Utiliser les hôtes .onions - Les hôtes .onion seront utilisés lorsqu\'ils sont disponibles. - Les hôtes .onion seront nécessaires pour la connexion. transmettre ainsi que par quel·s serveur·s vous pouvez recevoir les messages de vos contacts.]]> Vos paramètres SimpleX Lock @@ -344,7 +342,7 @@ Vos serveurs ICE Configurer les serveurs ICE Paramètres réseau avancés - Paramètres réseau + Paramètres avancés Utiliser un proxy SOCKS \? Utiliser une connexion Internet directe \? Si vous confirmez, les serveurs de messagerie seront en mesure de voir votre adresse IP, votre fournisseur ainsi que les serveurs auxquels vous vous connectez. @@ -372,7 +370,7 @@ réponse reçu… confimation reçu… connexion… - Protocole et code open-source – n\'importe qui peut heberger un serveur. + N\'importe qui peut heberger un serveur. Pour protéger votre vie privée, au lieu d\'IDs utilisés par toutes les autres plateformes, SimpleX possède des IDs pour les queues de messages, distinctes pour chacun de vos contacts. Plus d\'informations sur notre GitHub. Collez le lien que vous avez reçu @@ -414,12 +412,10 @@ Comment faire Serveurs ICE (un par ligne) Erreur lors de la sauvegarde des serveurs ICE - Mettre à jour le paramètre des hôtes .onion \? Quand disponible Les hôtes .onion ne seront pas utilisés. Les hôtes .onion seront nécessaires pour la connexion. \nAttention : vous ne pourrez pas vous connecter aux serveurs sans adresse .onion. - Les hôtes .onion ne seront pas utilisés. Supprimer l\'adresse \? Tous vos contacts resteront connectés. Partager le lien @@ -444,11 +440,12 @@ en attente de confirmation… connecté terminé - La nouvelle génération de messagerie privée + La nouvelle génération +\nde messagerie privée La vie privée redéfinie - La 1ère plateforme sans aucun identifiant d\'utilisateur – privée par design. - Protégé du spam et des abus - On ne peut se connecter à vous qu’avec les liens que vous partagez. + Aucun identifiant d\'utilisateur. + Protégé du spam + Vous choisissez qui peut se connecter. Décentralisé Créez votre profil Établir une connexion privée @@ -457,8 +454,8 @@ si SimpleX n\'a pas d\'identifiant d\'utilisateur, comment peut-il transmettre des messages \?]]> chiffrement de bout en bout à deux couches.]]> GitHub repository.]]> - Batterie peu utilisée. Le service de fond vérifie les messages toutes les 10 minutes. Vous risquez de manquer des appels ou des messages urgents.]]> - Batterie plus utilisée ! Le service de fond est toujours en cours d\'exécution - les notifications s\'affichent dès que les messages sont disponibles.]]> + Batterie peu utilisée. L\'app vérifie les messages toutes les 10 minutes. Vous risquez de manquer des appels ou des messages urgents.]]> + Consomme davantage de batterie L\'app fonctionne toujours en arrière-plan - les notifications s\'affichent instantanément.]]> %1$d message(s) manqué(s) ID de message incorrecte PARAMÈTRES @@ -571,7 +568,7 @@ Vous avez envoyé une invitation de groupe a quitté Haut-parleur ON - Envoi d\'aperçus de liens + Aperçu des liens Erreur lors de la suppression de la base de données du chat Erreur lors de l\'arrêt du chat Erreur lors de l\'exportation de la base de données du chat @@ -783,7 +780,6 @@ Clair Sombre Thème - Enregistrer la couleur Réinitialisation des couleurs Principale Vous autorisez @@ -834,7 +830,6 @@ directe Entièrement décentralisé – visible que par ses membres. Les membres du groupes peuvent envoyer des messages éphémères. - Revenir en arrière Interdire l’envoi de messages éphémères. Le mode incognito protège votre vie privée en utilisant un nouveau profil aléatoire pour chaque contact. La mise à jour des ces paramètres reconnectera le client à tous les serveurs. @@ -1024,7 +1019,7 @@ Trop de vidéos ! Vidéo Vidéo envoyée - La vidéo ne sera reçue que lorsque votre contact aura fini de la transférer. + La vidéo ne sera reçue que lorsque votre contact aura fini la mettre en ligne. En attente de la vidéo En attente de la vidéo La vidéo ne sera reçue que lorsque votre contact sera en ligne. Veuillez patienter ou vérifier plus tard ! @@ -1034,8 +1029,8 @@ Supprimer le fichier Erreur lors de la sauvegarde des serveurs XFTP Assurez-vous que les adresses des serveurs XFTP sont au bon format, séparées par des lignes et qu\'elles ne sont pas dupliquées. - Le serveur requiert une autorisation pour uploader, vérifiez le mot de passe - Transférer le fichier + Le serveur requiert une autorisation pour téléverser, vérifiez le mot de passe + Téléverser le fichier Serveurs XFTP Vos serveurs XFTP Comparer le fichier @@ -1121,7 +1116,7 @@ Vous ne perdrez pas vos contacts si vous supprimez votre adresse ultérieurement. Adresse SimpleX Vous pouvez accepter ou refuser les demandes de contacts. - COULEURS DU THÈME + COULEURS DE L\'INTERFACE Vos contacts resteront connectés. Partager l\'adresse avec vos contacts \? Partager avec vos contacts @@ -1136,7 +1131,7 @@ Partager l\'adresse Vous pouvez partager cette adresse avec vos contacts pour leur permettre de se connecter avec %s. Aperçu - Fond d\'écran + Fond Thème sombre Exporter le thème Importer un thème @@ -1246,13 +1241,13 @@ Envoyé le : %s Message envoyé aucun texte - Des erreurs non fatales se sont produites lors de l\'importation - vous pouvez consulter la console de chat pour plus de détails. + L\'importation a entraîné des erreurs non fatales : Les notifications ne fonctionnent pas tant que vous ne relancez pas l\'application Arrêt \? Mise à l\'arrêt Redémarrer APP - Annuler + Abandonner Erreur lors de l\'annulation du changement d\'adresse Abandonner le changement d\'adresse \? Annuler le changement d\'adresse @@ -1363,7 +1358,7 @@ Utiliser le profil actuel Utiliser un nouveau profil incognito Brouillon de message - Voir les derniers messages + Aperçu des derniers messages %s et %s sont connecté.es %s, %s et %d autres membres sont connectés %s, %s et %s sont connecté.es @@ -1646,7 +1641,7 @@ Erreur lors de l\'envoi de l\'archive Tous vos contacts, conversations et fichiers seront chiffrés en toute sécurité et transférés par morceaux vers les relais XFTP configurés. Appliquer - Archiver et transférer + Archiver et téléverser Archivage de la base de données Avertissement : l\'archive sera supprimée.]]> Remarque : l\'utilisation d\'une même base de données sur deux appareils interrompra le déchiffrement des messages provenant de vos connexions, par mesure de sécurité.]]> @@ -1769,4 +1764,309 @@ Carré, circulaire, ou toute autre forme intermédiaire. Lors des appels audio et vidéo. Interface utilisateur en lituanien + Avertissement sur la distribution des messages + L\'adresse du serveur est incompatible avec les paramètres du réseau. + La version du serveur est incompatible avec les paramètres du réseau. + Toujours utiliser le routage privé. + Ne pas utiliser de routage privé. + Utiliser le routage privé avec des serveurs inconnus lorsque l\'adresse IP n\'est pas protégée. + Envoyez les messages de manière directe lorsque votre serveur ou le serveur de destination ne prend pas en charge le routage privé. + Envoyer les messages de manière directe lorsque l\'adresse IP est protégée et que votre serveur ou le serveur de destination ne prend pas en charge le routage privé. + Problèmes de réseau - le message a expiré après plusieurs tentatives d\'envoi. + Clé erronée ou connexion non identifiée - il est très probable que cette connexion soit supprimée. + Serveur de transfert : %1$s +\nErreur au niveau du serveur de destination : %2$s + Serveur de transfert : %1$s +\nErreur : %2$s + Toujours + Routage privé + Autoriser la rétrogradation + Mode de routage des messages + Jamais + Serveurs inconnus + Non + Lorsque l\'IP est masquée + Oui + Rabattement du routage des messages + Afficher le statut du message + Protection de l\'adresse IP + FICHIERS + ROUTAGE PRIVÉ DES MESSAGES + Erreur au niveau du serveur de destination : %1$s + Erreur : %1$s + Capacité dépassée - le destinataire n\'a pas pu recevoir les messages envoyés précédemment. + L\'app demandera une confirmation pour les téléchargements depuis des serveurs de fichiers inconnus (sauf .onion ou lorsque le proxy SOCKS est activé). + Ne pas envoyer de messages directement, même si votre serveur ou le serveur de destination ne prend pas en charge le routage privé. + Pour protéger votre adresse IP, le routage privé utilise vos serveurs SMP pour délivrer les messages. + Non protégé + Serveurs inconnus ! + Sans Tor ou un VPN, votre adresse IP sera visible par les serveurs de fichiers. + Utiliser le routage privé avec des serveurs inconnus. + Sans Tor ou un VPN, votre adresse IP sera visible par ces relais XFTP : +\n%1$s. + Accentuation supplémentaire 2 + Paramètres avancés + Noir + Appliquer à + Debug de la distribution + Remplir + Bonjour Alice ! + Amélioration de la transmission des messages + Donnez à vos discussions un style différent ! + Info sur la file des messages + Nouveaux thèmes de discussion + aucun + Routage privé des messages 🚀 + Protégez votre adresse IP des relais de messagerie choisis par vos contacts. +\nActivez-le dans les paramètres *Réseau et serveurs*. + Réinitialiser au thème de l\'utilisateur + Afficher la liste des chats dans une nouvelle fenêtre + Teinte du fond d\'écran + Fond d\'écran + info sur la file du serveur : %1$s +\n +\ndernier message reçu : %2$s + Thème de l\'app + Mode de couleur + Sombre + Couleurs du mode sombre + Salut Bob ! + Réponse reçue + Retirer l\'image + Réinitialiser la couleur + Réponse envoyée + Répéter + Dimension + Tous les modes de couleur + Mode sombre + Adapter + Mode clair + Réinitialiser au thème de l\'app + Définir le thème par défaut + Confirmer les fichiers provenant de serveurs inconnus. + UI en persan + Réception de fichiers en toute sécurité + Consommation réduite de la batterie. + Couleurs de la discussion + Thème de la discussion + Thème de profil + Clair + Système + Erreur d\'initialisation de WebView. Mettez votre système à jour avec la nouvelle version. Veuillez contacter les développeurs. +\nErreur : %s + Fichier introuvable - le fichier a probablement été supprimé ou annulé. + Mauvaise clé ou adresse inconnue du bloc de données du fichier - le fichier est probablement supprimé. + Erreur du serveur de fichiers : %1$s + Erreur de fichier + Erreur de fichier temporaire + Statut du fichier + Statut du fichier: %s + Statut du message + Statut du message: %s + Erreur de copie + Veuillez vérifier que le mobile et l\'ordinateur sont connectés au même réseau local et que le pare-feu de l\'ordinateur autorise la connexion. +\nVeuillez faire part de tout autre problème aux développeurs. + Ce lien a été utilisé avec un autre appareil mobile, veuillez créer un nouveau lien sur le desktop. + Impossible d\'envoyer le message + Les paramètres de chat sélectionnés ne permettent pas l\'envoi de ce message. + Connections actives + Tous les profiles + Reçu avec accusé de réception + La mise à jour de l\'app est téléchargée + Complétées + Profil actuel + Erreurs de déchiffrement + désactivé + Supprimées + Désactiver + Téléchargement de la mise à jour de l\'appli, ne pas fermer l\'appli + Téléchargement %s (%s) + Erreur de reconnexion au serveur + inactif + Scanner / Coller le lien + Le message peut être transmis plus tard si le membre devient actif. + Reconnecter tous les serveurs connectés pour forcer la livraison des messages. Cette méthode utilise du trafic supplémentaire. + Sessions de transport + L\'adresse du serveur est incompatible avec les paramètres réseau : %1$s. + La version du serveur est incompatible avec votre application : %1$s. + Erreur de routage privé + Veuillez essayer plus tard. + Membre inactif + Message transféré + Pas de connexion directe pour l\'instant, le message est transmis par l\'administrateur. + Serveurs connectés + Serveurs précédemment connectés + Serveurs routés via des proxy + Vous n\'êtes pas connecté à ces serveurs. Le routage privé est utilisé pour leur délivrer des messages. + Reconnecter le serveur ? + Reconnecter les serveurs ? + Reconnecter le serveur pour forcer la livraison des messages. Utilise du trafic supplémentaire. + Erreur de réinitialisation des statistiques + Réinitialiser + Les statistiques des serveurs seront réinitialisées - il n\'est pas possible de revenir en arrière ! + Téléversé + Statistiques détaillées + Messages envoyés + Serveur SMP + Chunks supprimés + Chunks téléchargés + Fichiers téléchargés + Erreurs de téléchargement + Adresse du serveur + Erreurs de téléversement + Bêta + Vérifier les mises à jour + Vérifier les mises à jour + Serveurs SMP configurés + Serveurs XFTP configurés + Désactivé + Installé avec succès + Installer la mise à jour + Ouvrir l\'emplacement du fichier + Autres serveurs SMP + Autres serveurs XFTP + Veuillez redémarrer l\'application. + Rappeler plus tard + Afficher le pourcentage + Sauter cette version + Stable + Pour être informé des nouvelles versions, activez la vérification périodique des versions Stable ou Bêta. + Mise à jour disponible : %s + Téléchargement de la mise à jour annulé + Taille de police + Zoom + tentatives + Connecté + Connexion + Détails + Téléchargé + Erreur + Erreur de reconnexion des serveurs + Erreurs + Fichiers + Réception de message + Messages reçus + Messages envoyés + Pas d\'information, essayez de recharger + En attente + Routé via un proxy + Messages reçus + Total reçu + Erreurs de réception + Reconnecter + Reconnecter tous les serveurs + Réinitialiser toutes les statistiques + Réinitialiser toutes les statistiques ? + Envoyé directement + Total envoyé + Envoyé via un proxy + Infos serveurs + Afficher les informations pour + À partir de %s. + À partir de %s. +\nToutes les données restent confinées dans votre appareil. + Statistiques + Total + Serveur XFTP + Erreurs d\'accusé de réception + Chunks téléversés + Connexions + Créées + Erreurs de suppression + doublons + expiré + Ouvrir les paramètres du serveur + autre + autres erreurs + Sécurisées + Erreurs d\'envoi + Taille + Inscriptions + Erreurs d\'inscription + Inscriptions ignorées + Fichiers téléversés + Modéré + Flouter les médias + L\'adresse du serveur de destination %1$s est incompatible avec les paramètres du serveur de redirection %2$s. + La version du serveur de destination %1$s est incompatible avec le serveur de redirection %2$s. + Le serveur de redirection %1$s n\'a pas réussi à se connecter au serveur de destination %2$s. Veuillez réessayer plus tard. + L\'adresse du serveur de redirection est incompatible avec les paramètres du réseau : %1$s. + La version du serveur de redirection est incompatible avec les paramètres du réseau : %1$s. + Fort + Erreur de connexion au serveur de redirection %1$s. Veuillez réessayer plus tard. + Off + Léger + Paramètres + se connecter + message + ouvrir + Confirmer la suppression du contact ? + Le contact sera supprimé - il n\'est pas possible de revenir en arrière ! + Supprimer sans notification + Garder la conversation + Ne supprimer que la conversation + rechercher + vidéo + Contact supprimé ! + Vous pouvez envoyer des messages à %1$s à partir des contacts archivés. + Contacts archivés + Pas de contacts filtrés + Coller le lien + Vos contacts + Barre d\'outils accessible + Le contact est supprimé. + Les appels ne sont pas autorisés ! + Vous devez autoriser votre contact à appeler pour pouvoir l\'appeler. + Impossible d\'envoyer un message à ce membre du groupe + Vous pouvez toujours consulter la conversation avec %1$s dans la liste des conversation. + Autoriser les appels ? + appeler + Impossible d\'appeler le contact + Connexion au contact, veuillez patienter ou vérifier plus tard ! + Impossible d\'appeler ce membre du groupe + Conversation supprimée ! + Inviter + Veuillez demander à votre contact d\'autoriser les appels. + Envoyer un message pour activer les appels. + Archiver les contacts pour discuter plus tard. + Rendez les images floues et protégez-les contre les regards indiscrets. + Connectez-vous à vos amis plus rapidement. + État de la connexion et des serveurs. + Exportation de la base de données des discussions + Poursuivre + Les messages seront marqués comme étant à supprimer. Le(s) destinataire(s) pourra(ont) révéler ces messages. + Supprimer %d messages de membres ? + Message + Augmenter la taille de la police. + Créer + Rien n\'est sélectionné + Lien invalide + Nouveau message + Nouvelles options de médias + Inviter + Nouvelle expérience de discussion 🎉 + Téléchargez les nouvelles versions depuis GitHub. + Il protège votre adresse IP et vos connexions. + Maîtrisez votre réseau + Supprimez jusqu\'à 20 messages à la fois. + Serveurs de fichiers et de médias + Serveurs de messages + Veuillez vérifier que le lien SimpleX est exact. + Les messages seront supprimés pour tous les membres. + %d sélectionné(s) + Aperçu depuis la liste de conversation. + Les messages seront marqués comme modérés pour tous les membres. + Afficher la liste des conversations : + Vous pouvez choisir de le modifier dans les paramètres d\'apparence. + Rétablir tous les conseils + Mise à jour automatique de l\'app + Barre d\'outils accessible + Utiliser l\'application d\'une main. + Choisir + proxy SOCKS + Vous pouvez enregistrer l\'archive exportée. + Sauvegarder et se reconnecter + Connexion TCP + Certains fichiers n\'ont pas été exportés + Vous pouvez migrer la base de données exportée. \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hi/strings.xml index 56dfb51e2e..9c283a98e9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hi/strings.xml @@ -36,7 +36,7 @@ गुप्त स्वीकार करें पूर्वनिर्धारित सर्वर जोड़ें प्रोफ़ाइल जोड़ें - सर्वर जोड़े… + सर्वर जोड़े हमेशा बने रहें संलग्न करना उन्नत संजाल समायोजन @@ -69,7 +69,6 @@ स्वागत %1$s! शुरुआत भेजना - रंग बचाओ साझा करना अस्वीकार आवश्यक diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml index e3a1f124aa..db06d74378 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/hu/strings.xml @@ -6,19 +6,19 @@ %1$s TAG 1 hónap 1 hét - 6 új felületi nyelv + 6 új kezelőfelületi nyelv 5 perc 1 perc - A SimpleX azonosítóról + A SimpleX címről Címváltoztatás megszakítása? Megszakítás 30 másodperc Egyszer használatos hivatkozás %1$s szeretne kapcsolatba lépni önnel ezen keresztül: - A SimpleX Chat névjegye + A SimpleX Chat-ről 1 nap Címváltoztatás megszakítása - A SimpleX névjegye + A SimpleX-ről Kiemelés fogadott hívás Hozzáférés a kiszolgálókhoz SOCKS proxy segítségével a %d porton? A proxyt el kell indítani, mielőtt engedélyezné ezt az opciót. @@ -26,25 +26,25 @@ Elfogadás gombra fent, majd: Elfogadás inkognítóban - Kapcsolatfelvétel elfogadása? + Kapcsolódási kérelem elfogadása? Elfogadás Elfogadás - Azonosító hozzáadása a profilhoz, hogy az ismerősei megoszthassák másokkal. A profilfrissítés elküldésre kerül az ismerősök számára. + Cím hozzáadása a profilhoz, hogy az ismerősei megoszthassák másokkal. A profilfrissítés elküldésre kerül az ismerősök számára. További kiemelés hiba a hívásban Csoporttagok letiltása Hitelesítés Egy üres csevegési profil jön létre a megadott névvel, és az alkalmazás a szokásos módon megnyílik. - megszakítva %s + %s visszavonva Előre beállított kiszolgálók hozzáadása - A hang- és videóhívások le vannak tiltva. + A hívások kezdeményezése le van tiltva ebben a csevegésben. Külön TCP kapcsolat (és SOCKS bejelentkezési adatok) lesz használva minden ismerős és csoporttag számára. \nFigyelem: ha sok ismerőse van, az akkumulátor- és adathasználat jelentősen megnövekedhet és néhány kapcsolódási kísérlet sikertelen lehet. hivatkozás előnézet visszavonása az alkalmazásban minden csevegési profiljához .]]> Mindkét fél küldhet eltűnő üzeneteket. Az Android Keystore-t a jelmondat biztonságos tárolására használják - lehetővé teszi az értesítési szolgáltatás működését. - Téves üzenet hash + Hibás az üzenet ellenőrzőösszege Háttér Tudnivaló: az üzenet- és fájl átjátszók SOCKS proxy által vannak kapcsolatban. A hívások és URL hivatkozás előnézetek közvetlen kapcsolatot használnak.]]> Alkalmazásadatok biztonsági mentése @@ -52,15 +52,15 @@ Ismerőseivel kapcsolatban marad. A profil változtatások frissítésre kerülnek az ismerősöknél. A csevegési profil által (alap beállítás), vagy a kapcsolat által (BÉTA). Egy új véletlenszerű profil lesz megosztva. - Hangüzenetek küldésének engedélyezése kizárólag abban az esetben, ha az ismerőse is engedélyezi. + A hangüzenetek küldése kizárólag abban az esetben van engedélyezve, ha az ismerőse is engedélyezi. Az alkalmazás build száma: %s Hang-/videóhívások Speciális hálózati beállítások - Hangüzenetek küldésének engedélyezése az ismerősei számára. + A hangüzenetek küldése engedélyezve van az ismerősei számára. Hang- és videóhívások Az alkalmazás titkosítja a helyi fájlokat (a videók kivételével). Hívás fogadása - Eltűnő üzenetek engedélyezése az ismerősei számára. + Az eltűnő üzenetek küldésének engedélyezése az ismerősei számára. Kapcsolódás folyamatban! Nem lehet fogadni a fájlt Hitelesítés elérhetetlen @@ -74,17 +74,17 @@ Minden üzenet törlésre kerül - ez a művelet nem vonható vissza! Az üzenetek CSAK az ön számára törlődnek. Hívás befejeződött HÍVÁSOK - és %d további esemény + és további %d esemény Cím Csatlakozás folyamatban! Automatikus elfogadás - A háttérszolgáltatás mindig fut - az értesítések azonnal megjelennek, amint üzenetek vannak. + A háttérszolgáltatás mindig fut - az értesítések megjelennek, amint az üzenetek elérhetővé válnak. Az elküldött üzenetek végleges törlése engedélyezve van. (24 óra) Mindkét fél küldhet hangüzeneteket. Téves üzenet ID - Ismerősök általi üzenetreakciók küldésének engedélyezése. + Az üzenetreakciók küldése engedélyezve van az ismerősei számára. A hangüzenetek küldése engedélyezve van. - Üzenetreakciók engedélyezése kizárólag abban az esetben, ha az ismerőse is engedélyezi. + Az üzenetreakciók küldése kizárólag abban az esetben van engedélyezve, ha az ismerőse is engedélyezi. Vissza Kikapcsolható a beállításokban – az értesítések továbbra is megjelenítésre kerülnek amíg az alkalmazás fut.]]> Az adminok hivatkozásokat hozhatnak létre a csoportokhoz való csatlakozáshoz. @@ -92,7 +92,7 @@ titkosítás elfogadása… Ismerősök meghívása le van tiltva! téves üzenet ID - Ismerős jelölések automatikus elfogadása + Kapcsolódási kérelmek automatikus elfogadása Figyelem: NEM fogja tudni helyreállítani, vagy megváltoztatni a jelmondatot abban az esetben, ha elveszíti.]]> hívás… További másodlagos @@ -100,15 +100,15 @@ Az üzenetreakciók küldése engedélyezve van. Fájl előnézet visszavonása Minden csoporttag kapcsolatban marad. - Több akkumulátort használ! Háttérszolgáltatás mindig fut - az értesítések megjelennek, amint az üzenetek elérhetővé válnak.]]> + Több akkumulátort használ! Az alkalmazás mindig fut a háttérben - az értesítések azonnal megjelennek.]]> Letiltás admin Fénykép előnézet visszavonása A jelkód megadása után minden adat törlésre kerül. Felkérték a videó fogadására - Tag letiltása + Letiltás Még néhány dolog - Hitelesítés megszakítva + Hitelesítés visszavonva A fájlok- és a médiatartalom küldése engedélyezve van. Minden csevegés és üzenet törlésre kerül - ez a művelet nem vonható vissza! hanghívás @@ -122,50 +122,50 @@ Engedélyezés Minden ismerősével kapcsolatban marad. Élő csevegési üzenet visszavonása - Üzenet végleges törlésének engedélyezése kizárólag abban az esetben, ha az ismerőse is engedélyezi. (24 óra) + Az üzenetek végleges törlése kizárólag abban az esetben van engedélyezve, ha az ismerőse is engedélyezi. (24 óra) Hang- és videóhívások - téves üzenet hash - Mindig bekapcsolva + hibás az üzenet ellenőrzőösszege + Mindig fut Az Android Keystore biztonságosan fogja tárolni a jelmondatot az alkalmazás újraindítása, vagy a jelmondat megváltoztatás után - lehetővé téve az értesítések fogadását. Minden alkalmazásadat törölve. - Legjobb akkumulátoridő. Csak akkor kap értesítést, ha az alkalmazás fut (NINCS háttérszolgáltatás).]]> + Legjobb akkumulátoridő. Csak akkor kap értesítéseket, amikor az alkalmazás meg van nyitva. (NINCS háttérszolgáltatás.)]]> Megjelenés Az akkumulátor optimalizálása aktív, mely kikapcsolja a háttérszolgáltatást és az új üzenetek rendszeres kérését. A beállításokon keresztül újra engedélyezhetők. - Tag letiltása? + Biztosan letiltja? %1$s hívása befejeződött - Jó akkumulátoridő. A háttérszolgáltatás 10 percenként ellenőrzi az új üzeneteket. Előfordulhat, hogy hívásokról, vagy a sürgős üzenetekről marad le.]]> + Jó akkumulátoridő. Az alkalmazás 10 percenként ellenőrzi az új üzeneteket. Előfordulhat, hogy hívásokról, vagy a sürgős üzenetekről marad le.]]> szerző - Elküldött üzenetek végleges törlésének engedélyezése az ismerősei számára. (24 óra) + Az elküldött üzenetek végleges törlése engedélyezve van az ismerősei számára. (24 óra) Mégse - Az alkalmazás csak akkor tud értesítéseket fogadni amikor fut, háttérszolgáltatás nem indul el + Az alkalmazás csak akkor tud értesítéseket fogadni, amikor meg van nyitva. A háttérszolgáltatás nem indul el. Jobb üzenetek A cím módosítása megszakad. A régi fogadási cím kerül felhasználásra. Engedélyezés - Hibás számítógép-azonosító + Hibás számítógép cím Profil hozzáadása Csatolás Alkalmazás jelkód Felkérték a kép fogadására - Fényképező + Kamera A Keystore-hoz nem sikerül hozzáférni az adatbázis jelszó mentése végett hívás folyamatban - Fotók automatikus elfogadása - Hívások engedélyezése az ismerősei számára. + Képek automatikus elfogadása + A hívások kezdeményezése engedélyezve van az ismerősei számára. ALKALMAZÁS IKON Kiszolgáló hozzáadása QR-kód beolvasásával. Az eltűnő üzenetek küldése engedélyezve van. - Eltűnő üzenetek engedélyezése kizárólag abban az esetben, ha az ismerőse is engedélyezi. + Az eltűnő üzenetek küldése kizárólag abban az esetben van engedélyezve, ha az ismerőse is engedélyezi. Hang kikapcsolva - A közvetlen üzenetek küldése a tagok számára engedélyezve van. + A közvetlen üzenetek küldése a tagok között engedélyezve van. Alkalmazás Hívás folyamatban Mindkét fél küldhet üzenetreakciókat. - Mindkét fél tud hívásokat indítani. + Mindkét fél tud hívásokat kezdeményezni. Sikertelen hitelesítés Minden %s által írt új üzenet elrejtésre kerül! Alkalmazás verzió: v%s - Hívások engedélyezése kizárólag abban az esetben, ha az ismerőse is engedélyezi. - Kiszolgáló hozzáadása… + A hívások kezdeményezése kizárólag abban az esetben van engedélyezve, ha az ismerőse is engedélyezi. + Kiszolgáló hozzáadása Hang bekapcsolva hanghívás (nem e2e titkosított) letiltva @@ -200,13 +200,13 @@ az ismerős e2e titkosítással rendelkezik Csoport létrehozása véletlenszerű profillal. Az ismerős és az összes üzenet törlésre kerül - ez a művelet nem vonható vissza! - Az ismerősök törlésre jelölhetnek üzeneteket ; megtekintheti őket. + Az ismerősei törlésre jelölhetnek üzeneteket; ön majd meg tudja nézni azokat. Kapcsolódás egyszer használatos hivatkozással? Kapcsolódás egy hivatkozás / QR-kód által Kapcsolódási hiba (AUTH) Ismerős neve - Kapcsolódik a kapcsolattartási azonosítón keresztül? - Azonosító létrehozása + Kapcsolódik a kapcsolattartási címen keresztül? + Cím létrehozása Másolás Folytatás Kapcsolódás egy hivatkozáson keresztül? @@ -240,9 +240,9 @@ \n- gyorsabb és stabilabb. Hozzájárulás kapcsolódás (bemutatkozó meghívó) - SimpleX azonosító létrehozása + SimpleX cím létrehozása törölt ismerős - Csoporttag üzenet törlése? + Csoporttag üzenetének törlése? A csevegés fut Egyszer használatos meghívó hivatkozás létrehozása Törlés @@ -276,7 +276,7 @@ Jelenleg támogatott legnagyobb fájl méret: %1$s. Fájl törlése Hamarosan! - azonosító megváltoztatása erre: %s… + cím megváltoztatása nála: %s … Csevegési adatbázis importálva CSEVEGÉSI ARCHÍVUM Üzenetek törlése @@ -291,17 +291,17 @@ ICE kiszolgálók beállítása Csoport törlése Hitelesítés törlése - szerző + készítő Megerősítés - Törlés nálam + Csak nálam %d üzenet törlése? Egyedi témák kapcsolódás (elfogadva) Kiszolgáló címének ellenőrzése és újrapróbálkozás. Csoport törlése? - Adatbázis frissítés megerősítése + Adatbázis fejlesztésének megerősítése Saját profil létrehozása - azonosító megváltoztatása… + cím megváltoztatása… kapcsolódás… Hívás kapcsolása Fájlok és a médiatartalmak törlése? @@ -331,7 +331,7 @@ Várólista törlése Ismerős törlése Létrehozva ekkor: %1$s - címek megváltoztatása… + cím megváltoztatása… Csatlakoztatva a mobilhoz Jelenlegi jelmondat… Fájl kiválasztás @@ -341,16 +341,16 @@ Kiürítés Ismerős törlése? Kiürítés - Azonosító létrehozása, hogy az emberek kapcsolatba léphessenek önnel. + Cím létrehozása, hogy az emberek kapcsolatba léphessenek önnel. Biztonsági kódok összehasonlítása az ismerőseiével. Fájl összehasonlítás Csevegések Üzenet törlése? Függő kapcsolatfelvételi kérések törlése? Adatbázis titkosítva! - Üzenetek törlése? - Visszatérés a korábbi adatbázis verzióra - Üzenetek törlése + Üzenetek kiürítése? + Adatbázis visszafejlesztése + Üzenetek kiürítése Adatbázis titkosítási jelmondat frissítve lesz. Kapcsolódás automatikusan Adatbázis hiba @@ -361,27 +361,27 @@ Az adatbázis titkosítás jelmondata megváltoztatásra és mentésre kerül a Keystore-ban. Az adatbázis titkosításra kerül és a jelmondat eltárolásra a beállításokban. Kiszolgáló törlése - Eszközhitelesítés kikapcsolva. SimpleX zárolás kikapcsolása. + A készüléken nincs beállítva a képernyőzár. A SimpleX zár ki van kapcsolva. Letiltás Letiltás minden csoport számára - Engedélyezés minden csoport részére - engedélyezve ismerős részére + Engedélyezés minden csoport számára + engedélyezve az ismerős számára Az eltűnő üzenetek küldése le van tiltva ebben a csoportban. - Azonosító törlése + Cím törlése %d hét - Számítógép azonosítója + Számítógép címe %dmp Kézbesítési jelentések! - Eszközhitelesítés nem engedélyezett.A SimpleX zárolás bekapcsolható a Beállításokon keresztül, miután az eszköz hitelesítés engedélyezésre került. + A készüléken nincs beállítva a képernyőzár. A SimpleX zár az „Adatvédelem és biztonság” menüben kapcsolható be, miután beállította a képernyőzárat az eszközén. Titkosítás visszafejtési hiba Eltűnik ekkor: %s szerkesztve Törlés %d óra %d hónap - Azonosító törlése? + Cím törlése? Üzenet kézbesítési jelentések letiltása? - Az adatbázis jelmondat eltérő a Keystore-ba elmentettől. + Az adatbázis jelmondata eltér a Keystore-ban lévőtől. Közvetlen üzenetek E-mail Letiltás mindenki számára @@ -396,9 +396,9 @@ Helyi csoportok felfedezése és csatlakozás %1$d üzenet moderálva lett %2$s által Eltűnő üzenet - Ne hozzon létre azonosítót + Ne hozzon létre címet Ne mutasd újra - SimpleX zárolás kikapcsolása + SimpleX zár kikapcsolása e2e titkosított ESZKÖZ e2e titkosított videóhívás @@ -408,7 +408,7 @@ %d ismerős kiválasztva Engedélyezés %dhónap - Ebben a csoportban tiltott a tagok közötti közvetlen üzenetek küldése. + A közvetlen üzenetek küldése a tagok között le van tiltva ebben a csoportban. %d perc Az adatbázis egy véletlenszerű jelmondattal van titkosítva. Exportálás előtt változtassa meg a jelmondatot. Üzenet kézbesítés jelentéseket letiltása a csoportok számára? @@ -424,7 +424,7 @@ engedélyezve az ön számára Eltűnő üzenetek Törlés - Törlés és ismerős értesítése + Törlés, és az ismerős értesítése letiltva %d másodperc Minden fájl törlése @@ -453,17 +453,17 @@ Látható helyi hálózaton Ne engedélyezze Archívum törlése - Az eltűnő üzenetek le vannak tiltva ebben a csevegésben. + Az eltűnő üzenetek küldése le van tiltva ebben a csevegésben. alap (%s) - duplikálódott üzenet + duplikált üzenet Számítógép leválasztása? Számítógép kliens verziója %s nem kompatibilis ezzel az alkalmazással. Kézbesítés %d fájl %s összméretben - Adatbázis jelmondat szükséges a csevegés megnyitásához. + A csevegés megnyitásához adja meg az adatbázis jelmondatát. %dnap - Engedélyezés mindenki részére - Kézbesítési jelentések kikapcsolva! + Engedélyezés mindenki számára + A kézbesítési jelentések le vannak tiltva! Kibontás Hiba az üzenet küldésekor Jelkód megadása @@ -473,7 +473,7 @@ Hiba a csoport törlésekor Kilépés mentés nélkül Tárolt fájlok és médiatartalmak titkosítása - Hiba az azonosító beállításakor + Hiba a cím beállításakor A csoport meghívó lejárt Hiba az ICE kiszolgálók mentésekor Hiba @@ -481,13 +481,13 @@ Hiba az XFTP kiszolgálók betöltésekor Hiba az SMP kiszolgálók betöltésekor Hiba a hálózat konfigurációjának frissítésekor - TCP életben tartásának engedélyezése - Fényképezőgép megfordítása + TCP életben tartása + Kamera váltás Üdv! \nCsatlakozzon hozzám SimpleX Chat-en keresztül: %s A megjelenített név nem tartalmazhat szóközöket. Csoport - Üdvözlő üzenetet megadása… (opcionális) + Üdvözlő üzenet megadása… (opcionális) Hiba a csevegési adatbázis exportálásakor Hiba a fájl mentésekor Helyi fájlok titkosítása @@ -508,7 +508,7 @@ Titkosít Csoport nem található! Hiba az SMP kiszolgálók mentésekor - Visszatérés a korábbi verzióra és a csevegés megnyitása + Visszafejlesztés és a csevegés megnyitása A csoport inaktív Gyors és nem kell várni, amíg a feladó online lesz! Hiba a csoporthoz való csatlakozáskor @@ -521,12 +521,12 @@ Kísérleti funkciók Engedélyezés (felülírások megtartásával) Helyes jelmondat bevitele. - A csoport törlésre kerül az ön részére - ez a művelet nem vonható vissza! + A csoport törlésre kerül az ön számára - ez a művelet nem vonható vissza! Adatbázis titkosítása? A zárolási képernyőn megjelenő hívások engedélyezése a Beállításokban. titkosítás egyeztetve Üzenet kézbesítési jelentések engedélyezése? - Hiba a csoport profil mentésekor + Hiba a csoportprofil mentésekor hiba A fájl törölve lesz a kiszolgálóról. Akkor is, ha le van tiltva a beszélgetésben. @@ -544,7 +544,7 @@ Sikertelen titkosítás-újraegyeztetés. Hiba a felhasználói profil törlésekor Csoporttag általi javítás nem támogatott - Üdvözlő üzenetet megadása… + Üdvözlő üzenet megadása… Titkosított adatbázis Jelszó megadása a keresőben A fájl akkor érkezik meg, amikor a küldője befejezte annak feltöltését. @@ -565,11 +565,11 @@ Hiba a tag(-ok) hozzáadásakor Fájl A csoport tagjai küldhetnek fájlokat és médiatartalmakat. - Törlés miután + Törlés ennyi idő után Hiba a beállítás megváltoztatásakor Hiba a csoport hivatkozás frissítésekor a csoport törölve - csoport profil frissítve + csoportprofil frissítve Hiba a függőben lévő ismerős kapcsolatának törlésekor Hiba a csevegési adatbázis importálásakor Hiba a kézbesítési jelentések engedélyezésekor! @@ -590,7 +590,7 @@ segítség Önmegsemmisítő jelkód engedélyezése KÍSÉRLETI - Hiba az azonosító megváltoztatásának megszakításakor + Hiba a cím megváltoztatásának megszakításakor Hiba a fájl fogadásakor titkosítás rendben Hiba az ismerős kérelem törlésekor @@ -608,14 +608,14 @@ Titkosítás javítása az adatmentések helyreállítása után. Hiba a csevegési adatbázis törlésekor Teljes hivatkozás - Hiba az azonosító megváltoztatásakor + Hiba a cím megváltoztatásakor A csoport tagjai küldhetnek hangüzeneteket. Csoport beállítások Hiba: %s Eltűnő üzenetek - SimpleX zárolás engedélyezése - Hiba a kapcsolat szinkronizálása során - Hiba az azonosító létrehozásakor + SimpleX zár bekapcsolása + Hiba a kapcsolat szinkronizálása közben + Hiba a cím létrehozásakor engedélyezve Hiba a részletek betöltésekor Hiba történt a kapcsolatfelvételi kérelem elfogadásakor @@ -663,7 +663,7 @@ Azonnali értesítések Inkognitó mód Csevegési adatbázis importálása? - Azonnali értesítések kikapcsolva! + Az azonnali értesítések le vannak tiltva! Azonnali értesítések! Kép A fájlok- és a médiatartalom küldése le van tiltva ebben a csoportban. @@ -673,24 +673,24 @@ ICE-kiszolgálók (soronként egy) beolvashatja a QR-kódot a videohívásban, vagy az ismerőse megoszthat egy meghívó hivatkozást.]]> Ha az alkalmazás megnyitásakor megadja ezt a jelkódot, az összes alkalmazásadat véglegesen törlődik! - Ha nem tud személyesen találkozni, mutassa meg a QR-kódot egy videohívás során, vagy ossza meg a hivatkozást. + Ha nem tud személyesen találkozni, mutassa meg a QR-kódot egy videohívás közben, vagy ossza meg a hivatkozást. mutassa meg a QR-kódot a videohívásban, vagy ossza meg a hivatkozást.]]> Megerősítés esetén az üzenetküldő kiszolgálók látni fogják az IP-címét és a szolgáltatóját – azt, hogy mely kiszolgálókhoz kapcsolódik. A kép akkor érkezik meg, amikor a küldője befejezte annak feltöltését. QR kód beolvasásával.]]> - Kapott SimpleX Chat meghívó hivatkozását megnyithatja böngészőjében: - Ha az alkalmazás megnyitásakor az önmegsemmisítő jelkódot megadásra kerül: + A kapott SimpleX Chat meghívó hivatkozását megnyithatja böngészőjében: + Ha az alkalmazás megnyitásakor megadja az önmegsemmisítő jelkódot: Megtalált számítógép Számítógépek A markdown használata Csevegő profil létrehozása - Spam és visszaélések elleni védelem + Levélszemét elleni védelem Mobilok leválasztása Különböző nevek, avatarok és átviteli izoláció. Elutasítás esetén a feladó NEM kap értesítést. Szerepkör kiválasztásának bővítése A kép akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később! - meghívta + meghíva Érvénytelen kapcsolati hivatkozás Némítás nincsenek részletek @@ -700,11 +700,11 @@ Markdown segítség új üzenet Régi adatbázis archívum - Hálózati beállítások + Haladó beállítások Nincs kézbesítési információ moderált A tag eltávolítása a csoportból - ez a művelet nem vonható vissza! - Győződjön meg róla, hogy az XFTP-kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nem duplikáltak. + Győződjön meg róla, hogy az XFTP kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nincsenek duplikálva. Nem kerültek ismerősök kiválasztásra Nincsenek fogadott, vagy küldött fájlok Megnyitás mobil alkalmazásban, majd koppintson a Kapcsolódás gombra az alkalmazásban.]]> @@ -724,37 +724,35 @@ Társítsa össze a mobil és az asztali alkalmazásokat! 🔗 közvetett (%1$s) Hamarosan további fejlesztések érkeznek! - Az üzenetreakciók ebben a csevegésben le vannak tiltva. + Az üzenetreakciók küldése le van tiltva ebben a csevegésben. Helytelen biztonsági kód! - Ez akkor fordulhat elő, ha ön, vagy az ismerőse régi adatbázis biztonsági mentést használt. + Ez akkor fordulhat elő, ha ön vagy az ismerőse régi adatbázis biztonsági mentést használt. Új asztali alkalmazás! Most már az adminok is: \n- törölhetik a tagok üzeneteit. -\n- letilthatnak tagokat (\"megfigyelő\" szerepkör) - meghívta %1$s-t - Ebben a csoportban az üzenetreakciók le vannak tiltva. +\n- letilthatnak tagokat („megfigyelő” szerepkör) + meghívta őt: %1$s + Az üzenetreakciók küldése le van tiltva ebben a csoportban. Nem nincs szöveg TAG Ez később a beállításokon keresztül módosítható. Új tag szerepköre - Ki + Kikapcsolva Érvénytelen hivatkozás! - A kapcsolódáshoz Onion kiszolgálókra lesz szükség. Változások a %s verzióban - Onion kiszolgálók használata, ha azok rendelkezésre állnak. Érvénytelen kiszolgálócím! k soha (új)]]> - Győződjön meg arról, hogy az SMP-kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nincsenek duplikálva. + Győződjön meg arról, hogy az SMP kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nincsenek duplikálva. Onion kiszolgálók nem lesznek használva. perc Tudjon meg többet Új kapcsolattartási kérelem Csatlakozás a csoporthoz Összekapcsolt számítógép beállítások - meghívta a csoport hivatkozásán keresztül + meghíva az ön csoport hivatkozásán keresztül elhagyta a csoportot Összekapcsolt számítógépek Nincs alkalmazás jelkód @@ -762,10 +760,9 @@ A meghívó lejárt! (csak a csoporttagok tárolják) Moderálás - be - Japán és Portugál kezelőfelület - Ebben a csoportban az üzenetek végleges törlése le van tiltva. - Onion kiszolgálók nem lesznek használva. + bekapcsolva + Japán és portugál kezelőfelület + Az üzenetek végleges törlése le van tiltva ebben a csoportban. %s eszközzel megszakadt a kapcsolat]]> hónap Üzenetvázlat @@ -774,9 +771,9 @@ Egyszerre csak 10 videó küldhető el Csak ön adhat hozzá üzenetreakciókat. elhagyta a csoportot - Ebben a csevegésben az üzenetek végleges törlése le van tiltva. + Az üzenetek végleges törlése le van tiltva ebben a csevegésben. Max 40 másodperc, azonnal fogadható. - inkognitó a kapcsolattartási azonosító-hivatkozáson keresztül + inkognitó a kapcsolattartási cím-hivatkozáson keresztül A kapcsolódáshoz Onion kiszolgálókra lesz szükség. \nFigyelem: .onion cím nélkül nem fog tudni kapcsolódni a kiszolgálókhoz. Olasz kezelőfelület @@ -785,11 +782,11 @@ Összekapcsolt mobil eszközök Lehetővé teszi, hogy egyetlen csevegőprofilon belül több anonim kapcsolat legyen, anélkül, hogy megosztott adatok lennének közöttük. Az üzenet törlésre lesz jelölve. A címzett(ek) képes(ek) lesz(nek) felfedni ezt az üzenetet. - Elhagy + Elhagyás Rendben Nincsenek szűrt csevegések érvénytelen adat - Győződjön meg arról, hogy a WebRTC ICE-kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nem duplikáltak. + Győződjön meg arról, hogy a WebRTC ICE kiszolgáló címei megfelelő formátumúak, sorszeparáltak és nincsenek duplikálva. Csak a csoporttulajdonosok engedélyezhetik a fájlok- és a médiatartalmak küldését. A fájl betöltése Nincs hozzáadandó ismerős @@ -804,7 +801,7 @@ Üzenetkézbesítési hiba Több csevegőprofil töröltnek jelölve - Elnémítás + Némítás Egy mobil összekapcsolása Értesítési szolgáltatás Csak a csoporttulajdonosok engedélyezhetik a hangüzenetek küldését. @@ -813,15 +810,15 @@ Csak a csoporttulajdonosok módosíthatják a csoportbeállításokat. Nincsenek előzmények Érvénytelen QR-kód - Megjelölés olvasottként + Olvasottnak jelölés ÉLŐ - Olvasatlannak jelölve + Olvasatlannak jelölés Több Bejelentkezés hitelesítő adatokkal érvénytelen üzenet formátum Csatlakozás Az értesítések az alkalmazás elindításáig nem fognak működni - ki` + kikapcsolva` (ez az eszköz: v%s)]]> ajánlott %s Csoport elhagyása @@ -847,13 +844,13 @@ Beszélgessünk a SimpleX Chat-ben Moderálva lett ekkor: Élő üzenetek - Ellenőrzöttként jelölve + Hitelesítés Üzenetkézbesítési bizonylatok! hivatkozás előnézeti képe Csoport elhagyása? nem Hamarosan további fejlesztések érkeznek! - ki + kikapcsolva SimpleX Chat telepítése a terminálhoz Új megjelenített név: Új jelmondat… @@ -896,11 +893,11 @@ Kapcsolódási kérés megismétlése? Véglegesen csak ön törölhet üzeneteket (ismerőse csak törlésre jelölheti őket ). (24 óra) Szerepkör - SimpleX kapcsolattartási azonosító + SimpleX kapcsolattartási cím Megállítás Előre beállított kiszolgáló Új csevegés kezdése - Nyílt forráskódú protokoll és forráskód – bárki üzemeltethet kiszolgálókat. + Bárki üzemeltethet kiszolgálókat. Megnyitás Protokoll időtúllépés titkos @@ -911,11 +908,11 @@ PING időköze Eltűnő üzenet küldése Önmegsemmisítési jelkód - Mentés és csoport profil frissítése + Mentés és csoportprofil frissítése Adatvédelem - SimpleX azonosító + Az ön SimpleX címe Jelentse a fejlesztőknek. - Az emberek csak az ön által megosztott hivatkozáson keresztül kapcsolódhatnak. + Ön dönti el, hogy kivel beszélget. Az eltűnő üzenetek küldése le van tiltva. Csak ön tud hangüzeneteket küldeni. Frissítés @@ -984,7 +981,7 @@ egyszer használatos hivatkozást osztott meg A hivatkozás megnyitása a böngészőben gyengítheti az adatvédelmet és a biztonságot. A megbízhatatlan SimpleX hivatkozások pirossal vannak kiemelve. ICE kiszolgálók - Kapcsolódás elfogadva + Kapcsolat létrehozása Elutasítás Ismerős és üzenet megjelenítése BEÁLLÍTÁSOK @@ -1008,7 +1005,7 @@ Küldés %s másodperc %s: %s - A SimpleX nem tud futni a háttérben. Csak akkor fog értesítéseket kapni, ha az alkalmazás fut. + A SimpleX nem tud a háttérben futni. Csak akkor fog értesítéseket kapni, amikor az alkalmazás meg van nyitva. Túl sok kép! Archívum mentése %s, %s és %d tag @@ -1019,14 +1016,14 @@ Elküldve ekkor: %s Jelenlegi profil használata Ez az eszköz - Megosztja az azonosítót az ismerőseivel? + Megosztja a címet az ismerőseivel? Profiljelszó Téma Jelmondat eltávolítása a beállításokból? SimpleX csoport hivatkozás Képre várakozás Önmegsemmisítés - várakozás válaszra… + várakozás a válaszra… Ismerős nevének beállítása… Tag feloldása QR-kód beolvasása @@ -1040,21 +1037,21 @@ Biztonsági kód Adja meg a helyes aktuális jelmondatát. Az elküldött üzenetek végleges törlése le van tiltva. - Üzenetreakció tiltása. + Az üzenetreakciók küldése le van tiltva. Véletlenszerű jelmondat használata egyenrangú CSEVEGÉSI SZOLGÁLTATÁS INDÍTÁSA Fogadott hivatkozás beillesztése Kiszolgálók mentése? A SimpleX Chat biztonsága a Trail of Bits által lett auditálva. - módosított csoport profil + frissítette a csoport profilját TÁMOGASSA A SIMPLEX CHATET SimpleX Chat szolgáltatás Nem lehet üzeneteket küldeni! %s ellenőrzött Jelszó megjelenítése Adatvédelem és biztonság - Tag eltávolítása + Eltávolítás A jelkód beállítva! Elküldött üzenet Ismerősök kiválasztása @@ -1078,14 +1075,14 @@ Ennek az eszköznek a neve Jelenlegi profil Fájl feltöltése - Hang- és videóhívások tiltása. - Megkövetelt + A hívások kezdeményezése le van tiltva. + Szükséges SimpleX Chat üzenetek Visszaállítás Adatbázis jelmondat beállítása Elküldött üzenet Időszakosan indul - Ez a SimpleX azonosítója! + Ez az ön SimpleX címe! eltávolítva Megosztás SimpleX csapat @@ -1097,14 +1094,13 @@ SimpleX egyszer használatos meghívó Hívások nem sikerült elküldeni - TÉMA SZÍNEK - Visszaállít + KEZELŐFELÜLET SZÍNEI Előző jelszó megadása az adatbázis biztonsági mentésének visszaállítása után. Ez a művelet nem vonható vissza. Másodlagos SOCKS PROXY Mentés Újraindítás - Üzenetküldő (SMP) kiszolgálók + SMP kiszolgálók Videó Automatikus elfogadási beállítások mentése Újraegyzetetés @@ -1117,12 +1113,12 @@ Ismeretlen hiba Saját kiszolgáló cím Csevegés konzol megnyitása - Tag eltávolítása + Eltávolítás Adatbázis jelmondat beállítása Biztonsági kód megtekintése Tag feloldása? A küldő törölhette a kapcsolódási kérelmet. - Téves adatbázis jelmondat + Hibás adatbázis jelmondat SMP kiszolgálók Az üzenet kézbesítési jelentések le vannak tiltva Adatbázis mappa megnyitása @@ -1135,7 +1131,7 @@ ÖN port %d Kapcsolódás hivatkozáson keresztül - Azonosító megosztása + Cím megosztása A kiszolgáló QR-kódjának beolvasása Megállítás Címmegosztás megállítása? @@ -1143,10 +1139,10 @@ Csatlakozási kérés megismétlése? Képre várakozás Hangüzenetek - Tag eltávolítása? + Biztosan eltávolítja? Biztonsági kód ellenőrzése eltávolítottak - SimpleX azonosító + SimpleX cím Megjelenítés: fogadott válasz… Adatbázismentés visszaállítása? @@ -1158,18 +1154,18 @@ Változáslista Csoport megnyitása Elküldve ekkor: - Hangüzenetek küldése le van tiltva. + A hangüzenetek küldése le van tiltva. Utolsó üzenetek megjelenítése Az előre beállított kiszolgáló címe Rendszeres értesítések letiltva! A jelkód megváltozott! - Akkor fut, ha az alkalmazás nyitva van + Akkor fut, amikor az alkalmazás meg van nyitva Ez a QR-kód nem egy hivatkozás! Fájlra várakozás simplexmq: v%s (%2s) Szétkapcsolás Véletlenszerű profil - Téves jelmondat! + Hibás jelmondat! Az üzenetreakciók küldése le van tiltva. Rendszer olvasatlan @@ -1192,11 +1188,10 @@ Színek alaphelyzetbe állítása Mentés Váltás - Kapott hivatkozás beillesztése az ismerősökhöz történő kapcsolódáshoz… - Kód beolvasása + A kapott hivatkozás beillesztése az ismerősökhöz történő kapcsolódáshoz… + Beolvasás Port megnyitása a tűzfalon indítás… - Szín mentése Leállítás elküldve SOCKS proxy használata @@ -1206,9 +1201,9 @@ Alkalmazás képernyőjének védelme QR-kód megjelenítése videóhívás - Nem kedvenc + Kedvenc törlése Üzenet kézbesítési jelentések küldése - SimpleX azonosító + SimpleX cím Koppintson a Mentés és ismerős értesítése Elutasított hívás @@ -1218,7 +1213,7 @@ Eltávolítás Tor .onion kiszolgálók használata Felfedés - SimpleX zárolási mód + Zárolási mód Fájl visszavonása XFTP kiszolgálók A fájlok- és a médiatartalom küldése le van tiltva. @@ -1229,8 +1224,8 @@ eltávolította őt: %1$s Jelmondat mentése és csevegés megnyitása Beállítások mentése? - Az első csevegési rendszer bármiféle felhasználó azonosító nélkül - privátra lett tervezre. - A közvetlen üzenetek küldése a tagok számára le van tiltva. + Nincsenek felhasználói azonosítók. + A közvetlen üzenetek küldése a tagok között le van tiltva. SOCKS proxy használata? Hangszóró kikapcsolva hét @@ -1257,28 +1252,28 @@ Lengyel kezelőfelület Kiszolgáló használata Fogadva ekkor: %s - SimpleX zárolás + SimpleX zár Mentés és csoporttagok értesítése Alaphelyzetbe állítás Csak az ismerőse tud üzenetreakciókat küldeni. Hangüzenetek elhagyta a csoportot Hangüzenet rögzítése - SimpleX zárolás bekapcsolva + SimpleX zár bekapcsolva közvetlen üzenet küldése Beolvasás mobilról Kapcsolatok ellenőrzése Üzenet megosztása… másodperc - SimpleX zárolás nincs engedélyezve! - SimpleX zárolás + A SimpleX zár nincs bekapcsolva! + SimpleX zár Beállítások Csevegési adatbázis - %1$s eltávolítva - Sikertelen kiszolgáló-teszt! + eltávolította őt: %1$s + Sikertelen kiszolgáló teszt! Kapcsolat ellenőrzése Tudjon meg többet - A küldő megszakította a fájl átvitelt. + A fájl küldője visszavonta az átvitelt. Csevegési szolgáltatás megállítása? Fogadva ekkor: Beállítva 1 nap @@ -1291,7 +1286,7 @@ Csevegési profil felfedése Videók és fájlok 1Gb méretig TCP kapcsolat időtúllépés - A(z) %1$s nevű profiljának SimpleX azonosítója megosztásra fog kerülni. + A(z) %1$s nevű profiljának SimpleX címe megosztásra fog kerülni. Ön már kapcsolódott ehhez: %1$s. Jelenlegi csevegési adatbázis TÖRLÉSRE és FELCSERÉLÉSRE kerül az importált által! \nEz a művelet nem vonható vissza - profiljai, ismerősei, csevegési üzenetei és fájljai véglegesen törölve lesznek! @@ -1299,25 +1294,26 @@ Figyelmeztetés: néhány adat elveszhet! Koppintson az új csevegés indításához Várakozás a számítógépre… - A privát üzenetküldés következő generációja + A privát üzenetküldés +\nkövetkező generációja Hálózati beállítások megváltoztatása? Várakozás a mobiltelefon társítására: Kapcsolat biztonságának ellenőrzése fájlok küldése egyelőre még nem támogatott - azonosítója erre változott: %s + cím megváltoztatva nála: %s fájlok fogadása egyelőre még nem támogatott - Csoport profil mentése + Csoportprofil mentése Alaphelyzetbe állítás Hacsak az ismerőse nem törölte a kapcsolatot, vagy ez a hivatkozás már használatban volt, lehet hogy ez egy hiba – jelentse a problémát. \nA kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsolati hivatkozást, és ellenőrizze, hogy a hálózati kapcsolat stabil-e. videóhívás (nem e2e titkosított) Alkalmazás új kapcsolatokhoz Az új üzenetek rendszeresen letöltésre kerülnek az alkalmazás által – naponta néhány százalékot használ az akkumulátorból. Az alkalmazás nem használ push értesítéseket – az eszközről származó adatok nem kerülnek elküldésre a kiszolgálóknak. - Számítógép azonosítójának beillesztése - kapcsolattartási azonosító-hivatkozáson keresztül + Számítógép címének beillesztése + kapcsolattartási cím-hivatkozáson keresztül SimpleX háttérszolgáltatást használja - az akkumulátor néhány százalékát használja naponta.]]> - Az ismerősnek online kell lennie ahhoz, hogy a kapcsolat létrejöjjön. -\nMegszakíthatja ezt a kapcsolatfelvételt és törölheti az ismerőst (ezt később ismét megpróbálhatja egy új hivatkozással) + Az ismerősének online kell lennie ahhoz, hogy a kapcsolat létrejöjjön. +\nVisszavonhatja ezt a kapcsolatfelvételt és törölheti az ismerőst (ezt később ismét megpróbálhatja egy új hivatkozással). A jelszó nem található a Keystore-ban, ezért kézzel szükséges megadni. Ez akkor történhetett meg, ha visszaállította az alkalmazás adatait egy biztonsági mentési eszközzel. Ha nem így történt, akkor lépjen kapcsolatba a fejlesztőkkel. Az ismerősei továbbra is kapcsolódva maradnak. A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze jelszavát @@ -1339,11 +1335,11 @@ A kiszolgálónak engedélyre van szüksége a várólisták létrehozásához, ellenőrizze jelszavát Kapcsolódni fog a csoport összes tagjához. Lehetséges, hogy a kiszolgáló címében szereplő tanúsítvány-ujjlenyomat helytelen - Az adatavédelem érdekében kapcsolja be a SimpleX zárolás funkciót. -\nA funkció engedélyezése előtt a rendszer felszólítja a hitelesítés befejezésére. + A biztonsága érdekében kapcsolja be a SimpleX zár funkciót. +\nA funkció bekapcsolása előtt a rendszer felszólítja a képernyőzár beállítására az eszközén. A videó akkor érkezik meg, amikor a küldője elérhető lesz, várjon, vagy ellenőrizze később! Hálózati kapcsolat ellenőrzése a következővel: %1$s, és próbálja újra. - A SimpleX zárolás a Beállításokon keresztül kapcsolható be. + A SimpleX zár az „Adatvédelem és biztonság” menüben kapcsolható be. Az alkalmazás összeomlott Ellenőrizze, hogy a megfelelő hivatkozást használta-e, vagy kérje meg ismerősét, hogy küldjön egy másikat. A kép nem dekódolható. Próbálja meg egy másik képpel, vagy lépjen kapcsolatba a fejlesztőkkel. @@ -1361,7 +1357,7 @@ %1$s.]]> Profil felfedése Ez a hivatkozás nem érvényes kapcsolati hivatkozás! - A végpontok közötti titkosítás ellenőrzéséhez hasonlítsa össze (vagy szkennelje be) az ismerőse eszközén lévő kódot. + A végpontok közötti titkosítás ellenőrzéséhez hasonlítsa össze (vagy olvassa be a QR-kódot) az ismerőse eszközén lévő kóddal. A csevegési adatbázis legfrissebb verzióját CSAK egy eszközön kell használnia, ellenkező esetben előfordulhat, hogy az üzeneteket nem fogja megkapni valamennyi ismerősétől. Ez a beállítás a jelenlegi csevegési profilban lévő üzenetekre érvényes Meghívást kapott a csoportba. Csatlakozzon, hogy kapcsolatba léphessen a csoport tagjaival. @@ -1377,7 +1373,7 @@ Egy olyan ismerősét próbálja meghívni, akivel inkognitóprofilt osztott meg abban a csoportban, amelyben a saját fő profilja van használatban %1$s csoporthoz.]]> Amikor az alkalmazás fut - Inkognító profilt használ ehhez a csoporthoz - fő profilja megosztásának elkerülése érdekében meghívók küldése tiltott + Inkognító profilt használ ehhez a csoporthoz - fő profilja megosztásának elkerülése érdekében a meghívók küldése le van tiltva Kapcsolat izolációs mód Akkor lesz kapcsolódva, ha a kapcsolódási kérelme elfogadásra kerül, várjon, vagy ellenőrizze később! A hangüzenetek küldése le van tiltva ebben a csoportban. @@ -1386,12 +1382,12 @@ - hangüzenetek 5 percig. \n- egyedi eltűnési időhatár. \n- előzmény szerkesztése. - Használat számítógépről menüpontot a mobil alkalmazásban és olvassa be a QR-kódot.]]> + Használat számítógépről menüt a mobil alkalmazásban és olvassa be a QR-kódot.]]> %s ekkor: %s Akkor lesz kapcsolódva, amikor az ismerősének az eszköze online lesz, várjon, vagy ellenőrizze később! Kéretlen üzenetek elrejtése. Használja az .onion kiszolgálókat NEM értékre, ha a SOCKS proxy nem támogatja őket.]]> - Megoszthatja azonosítóját hivatkozásként vagy QR-kódként – így bárki kapcsolódhat önhöz. + Megoszthatja a címét egy hivatkozásként vagy egy QR-kódként – így bárki kapcsolódhat önhöz. Létrehozás később Profilja az eszközön van tárolva és csak az ismerőseivel kerül megosztásra. A SimpleX kiszolgálók nem láthatják a profilját. %s szerepkörét megváltoztatta erre: %s @@ -1405,14 +1401,14 @@ A csevegési adatbázis nem titkosított - állítson be egy jelmondatot annak védelméhez. Közvetlen internet kapcsolat használata? Továbbra is kap hívásokat és értesítéseket a némított profiloktól, ha azok aktívak. - A fő csevegési profilja megküldésre kerül a csoporttagok számára - Később engedélyezheti őket az alkalmazás Adatvédelem és biztonság menüpontban. - Rejtett profiljának felfedéséhez írja be a teljes jelszót a Csevegési profilok oldal keresőmezőjébe. - A csevegés frissítése és megnyitása + A fő csevegési profilja elküldésre kerül a csoporttagok számára + Később engedélyezheti őket az „Adatvédelem és biztonság” menüben. + Rejtett profilja felfedéséhez írja be a teljes jelszavát a keresőmezőbe a „Csevegési profilok” menüben. + Fejlesztés és a csevegés megnyitása Hangüzeneteket küldéséhez engedélyeznie kell azok küldését az ismerősei számára. fogadja az üzeneteket, ismerősöket – a kiszolgálók, amelyeket az üzenetküldéshez használ.]]> %1$s csoport tagja.]]> - azonosítója megváltoztatva + cím megváltoztatva Ismerősei engedélyezhetik a teljes üzenet törlést. A jelmondatot minden alkalommal meg kell adnia, amikor az alkalmazás elindul - nem az eszközön kerül tárolásra. Ha engedélyezni szeretné, hogy egy mobilalkalmazás csatlakozzon a számítógéphez, akkor nyissa meg ezt a portot a tűzfalában, ha engedélyezte azt @@ -1421,30 +1417,29 @@ Ez a karakterlánc nem egy meghívó hivatkozás! Új csevegés kezdése A kapcsolódás már folyamatban van ezen az egyszer használatos hivatkozáson keresztül! - Nem veszíti el az ismerőseit, ha később törli az azonosítóját. + Nem veszíti el az ismerőseit, ha később törli a címét. A beállítások frissítése a kiszolgálókhoz való újra kapcsolódással jár. kapcsolatba akar lépni veled! saját szerepköre erre változott: %s - A csevegési szolgáltatás elindítható a Beállítások / Adatbázis menüpontban vagy az alkalmazás újraindításával. + A csevegési szolgáltatás elindítható a Beállítások / Adatbázis menüben vagy az alkalmazás újraindításával. Kód ellenőrzése a mobilon Csatlakozott ehhez a csoporthoz. Kapcsolódás a meghívó csoporttaghoz. a SimpleX Chat fejlesztőivel, ahol bármiről kérdezhet és értesülhet az újdonságokról.]]> Opcionális üdvözlő üzenettel. Ismeretlen adatbázis hiba: %s - Elrejthet vagy némíthat egy felhasználói profilt - tartsa lenyomva a menühöz. + Elrejtheti vagy lenémíthatja a felhasználó profiljait - koppintson (vagy asztali alkalmazásban kattintson) hosszan a profilra a felugró menühöz. Inkognító mód kapcsolódáskor. - Tor .onion kiszolgálók beállításainak frissítése? Megoszthat egy hivatkozást vagy QR-kódot - így bárki csatlakozhat a csoporthoz. Ha a csoport később törlésre kerül, akkor nem fogja elveszíteni annak tagjait. Csatlakozott ehhez a csoporthoz %1$s csoporthoz!]]> - A hangüzenetek le vannak tiltva ebben a csevegésben. + A hangüzenetek küldése le van tiltva ebben a csevegésben. Ön irányítja csevegését! Kód ellenőrzése a számítógépen Az időzóna védelme érdekében a kép-/hangfájlok UTC-t használnak. - Csoporttag részére a kapcsolódási kérelem elküldésre kerül. + A kapcsolódási kérelem elküldésre kerül ezen csoporttag számára Inkognitóprofil megosztása esetén a rendszer azt a profilt fogja használni azokhoz a csoportokhoz, amelyekbe meghívást kapott. - Már kért egy kapcsolódási kérelmet ezen az azonosítón keresztül! - Megoszthatja ezt a SimpleX azonosítót az ismerőseivel, hogy kapcsolatba léphessenek vele: %s. + Már kért egy kapcsolódási kérelmet ezen a címen keresztül! + Megoszthatja ezt a SimpleX címet az ismerőseivel, hogy kapcsolatba léphessenek vele: %s. Amikor az emberek kapcsolódást kérelmeznek, ön elfogadhatja vagy elutasíthatja azokat. Megjelenítendő üzenet beállítása az új tagok számára! Köszönet a felhasználóknak - hozzájárulás a Weblaten! @@ -1455,7 +1450,7 @@ Profilja csak az ismerőseivel kerül megosztásra. Néhány kiszolgáló megbukott a teszten: Koppintson a csatlakozáshoz - Ez a művelet nem vonható vissza - az összes fogadott és küldött fájl a médiatartalommal együtt törlésre kerülnek. Az alacsony felbontású fotók viszont megmaradnak. + Ez a művelet nem vonható vissza - az összes fogadott és küldött fájl a médiatartalommal együtt törlésre kerülnek. Az alacsony felbontású képek viszont megmaradnak. Kézbesítési jelentések engedélyezve vannak %d ismerősnél Küldés ezen keresztül: Köszönet a felhasználóknak - hozzájárulás a Weblaten! @@ -1471,31 +1466,31 @@ Koppintson az inkognitóban való kapcsolódáshoz Jelmondat beállítása az exportáláshoz Kézbesítési jelentések le vannak tiltva a(z) %d csoportban - Néhány nem végzetes hiba történt az importálás során – további részletekért a csevegési konzolban olvashat. + Néhány nem végzetes hiba történt az importálás közben: Köszönet a felhasználóknak - hozzájárulás a Weblaten! - Az átjátszó kiszolgáló csak szükség esetén kerül használatra. Egy másik fél megfigyelheti az IP-címét. + Az átjátszó kiszolgáló csak szükség esetén kerül használatra. Egy másik fél megfigyelheti az IP-címet. Rendszerhitelesítés helyetti beállítás. A fogadó cím egy másik kiszolgálóra változik. A címváltoztatás a feladó online állapotba kerülése után fejeződik be. A csevegés megállítása a csevegő adatbázis exportálásához, importálásához, vagy törléséhez. A csevegés megállítása alatt nem tud üzeneteket fogadni és küldeni. - Jelmondat mentése a kulcstárolóba + Jelmondat mentése a Keystore-ba Köszönet a felhasználóknak - hozzájárulás a Weblaten! Jelmondat mentése a beállításokban Ennek a csoportnak több mint %1$d tagja van, a kézbesítési jelentések nem kerülnek elküldésre. A második jelölés, amit kihagytunk! ✅ - Az átjátszó kiszolgáló megvédi IP-címét, de megfigyelheti a hívás időtartamát. + Az átjátszó kiszolgáló megvédi az IP-címet, de megfigyelheti a hívás időtartamát. További információ a GitHub tárolónkban. Az utolsó üzenet tervezetének megőrzése a mellékletekkel együtt. A mentett WebRTC ICE-kiszolgálók eltávolításra kerülnek. Kézbesítési jelentések engedélyezve vannak a(z) %d csoportban - A szerepkör meg fog változni erre: \"%s\". A csoportban mindenki értesítve lesz. + A szerepkör meg fog változni erre: „%s”. A csoportban mindenki értesítve lesz. Profil és kiszolgálókapcsolatok - Üzenetküldő és alkalmazásplatform, amely védi az ön adatvédelmét és biztonságát. + Egy üzenetküldő- és alkalmazásplatform, amely védi az ön adatait és biztonságát. A profil aktiválásához koppintson az ikonra. Kézbesítési jelentések le vannak tiltva %d ismerősnél Munkamenet kód Köszönet a felhasználóknak - hozzájárulás a Weblaten! Kis csoportok (max. 20 tag) - Az ön által elfogadott kapcsolat megszakad! + Az ön által elfogadott kapcsolat vissza lesz vonva! Élő üzenet küldése - a címzett(ek) számára frissül, ahogy beírja A KÉZBESÍTÉSI JELENTÉSEKET A KÖVETKEZŐ CÍMRE KELL KÜLDENI A következő üzenet azonosítója hibás (kisebb vagy egyenlő az előzővel). @@ -1507,7 +1502,7 @@ A jelenlegi csevegési profilhoz tartozó új kapcsolatok kiszolgálói Fogadás ezen keresztül: Tárolja el biztonságosan jelmondát, mert ha elveszti azt, akkor NEM férhet hozzá a csevegéshez. - A tag szerepköre erre fog változni: \"%s\". A tag új meghívót fog kapni. + A tag szerepköre erre fog változni: „%s”. A tag új meghívót fog kapni. profilkép helyőrző A titkosítás működik, és új titkosítási egyezményre nincs szükség. Ez kapcsolati hibákat eredményezhet! Ez a művelet nem vonható vissza - profiljai, ismerősei, üzenetei és fájljai véglegesen törlésre kerülnek. @@ -1515,7 +1510,7 @@ Használati útmutatóban olvasható.]]> A jelmondat a beállításokban egyszerű szövegként van tárolva. Konzol megjelenítése új ablakban - Az előző üzenet hash-e más. + Az előző üzenet ellenőrzőösszege különbözik. Ezek a beállítások a jelenlegi profiljára vonatkoznak Várjon, amíg a fájl betöltődik a csatolt mobilról GitHub tárolónkban.]]> @@ -1537,14 +1532,14 @@ A csevegés leállt. Ha már használta ezt az adatbázist egy másik eszközön, úgy visszaállítás szükséges a csevegés megkezdése előtt. Az előzmények nem kerülnek elküldésre az új tagok számára. Újrapróbálkozás - A fényképező nem elérhető + A kamera nem elérhető Az utolsó 100 üzenet elküldése az új tagok számára. Az előzmények ne kerüljenek elküldésre az új tagok számára. Vagy mutassa meg ezt a kódot Kamera hozzáférés engedélyezése Fel nem használt meghívó megtartása? Egyszer használatos meghívó hivatkozás megosztása - Új beszélgetés + Új csevegés Csevegések betöltése… Hivatkozás létrehozása… Vagy QR-kód beolvasása @@ -1586,19 +1581,19 @@ Fejlesztői beállítások A funkció végrehajtása túl sokáig tart: %1$d másodperc: %2$s %s mobil eszköz elfoglalt]]> - Legutóbbi tag %1$s + Már nem tag - %1$s ismeretlen státusz %1$s megváltoztatta a nevét erre: %2$s - törölt kapcsolattartási azonosító - törölt profilkép - új kapcsolattartási azonosító beállítása - új profilkép beállítása + törölt kapcsolattartási cím + törölte a profilképét + új kapcsolattartási cím beállítása + új profilképet állított be frissített profil %1$s megváltoztatta a nevét erre: %2$s Privát jegyzetek Hiba a privát jegyzetek törlésekor Hiba az üzenet létrehozásakor - Privát jegyzetek törlése? + Privát jegyzetek kiürítése? Létrehozva ekkor: Mentett üzenet Megosztva ekkor: %s @@ -1612,19 +1607,19 @@ Csökkentett akkumulátorhasználattal. Magyar és török felhasználói felület A közelmúlt eseményei és továbbfejlesztett jegyzék bot. - %s letiltása feloldva - %s letiltását visszavonta + feloldotta %s letiltását + ön feloldotta %s letiltását letiltva letiltva az admin által Letiltva az admin által - %s letiltva - Mindenki számára letiltva - Tag letiltása mindenki számára? + letiltotta %s-t + Letiltás mindenki számára + Mindenki számára letiltja ezt a tagot? %d üzenet letiltva az admin által Letiltás feloldása mindenki számára Mindenki számára feloldja a tag letiltását? - letiltotta %s-t - Hiba a tag mindenki számára való letiltása során + ön letiltotta %s-t + Hiba a tag mindenki számára való letiltása közben Az üzenet túl nagy Az üdvözlő üzenet túl hosszú Az adatbázis átköltöztetése folyamatban van. @@ -1680,7 +1675,7 @@ Csevegés indítása Nem szabad ugyanazt az adatbázist használni egyszerre két eszközön.]]> Erősítse meg, hogy emlékszik az adatbázis jelmondatára az átköltöztetéshez. - Átköltöztetés egy másik eszközről opciót az új eszközön és szkennelje be a QR-kódot.]]> + Átköltöztetés egy másik eszközről opciót az új eszközön és olvassa be a QR-kódot.]]> Átköltöztetés véglegesítése Átköltöztetés véglegesítése egy másik eszközön. Letöltés előkészítése @@ -1701,15 +1696,15 @@ Átköltöztetés befejezve Átköltöztetés egy másik eszközre QR-kód használatával. Átköltöztetés - Megjegyzés: ha két eszközön is ugyanazt az adatbázist használja, akkor biztonsági védelemként megszakítja a kapcsolataiból érkező üzenetek visszafejtését.]]> + Megjegyzés: ha két eszközön is ugyanazt az adatbázist használja, akkor biztonsági védelemként megszakítja az ismerőseitől érkező üzenetek visszafejtését.]]> Megpróbálhatja még egyszer. Hibás hivatkozás végpontok közötti kvantumrezisztens titkosítás Ez a csevegés végpontok közötti titkosítással védett. Átköltöztetési párbeszédablak megnyitása Ez a csevegés végpontok közötti kvantumrezisztens tikosítással védett. - végpontok közötti titkosítással és sérülés utáni titkosságvédelemmel, visszautasítással és sérülés utáni helyreállítással védi.]]> - végpontok közötti kvantumrezisztens titkosítással és sérülés utáni titkosságvédelemmel, visszautasítással és sérülés utáni helyreállítással védi.]]> + végpontok közötti titkosítással, sérülés utáni titkosság-védelemmel és -helyreállítással, továbbá visszautasítással védi.]]> + végpontok közötti kvantumrezisztens titkosítással, sérülés utáni titkosság-védelemmel és -helyreállítással, továbbá visszautasítással védi.]]> Hiba az értesítés megjelenítésekor, lépjen kapcsolatba a fejlesztőkkel. Keresse meg ezt az engedélyt az Android beállításaiban, és adja meg kézzel. Engedélyezés a beállításokban @@ -1733,7 +1728,7 @@ minden tag SimpleX hivatkozás A hangüzenetek küldése le van tiltva - A SimpleX hivatkozások küldése ebben a csoportban le van tiltva. + A SimpleX hivatkozások küldése le van tiltva ebben a csoportban. A SimpleX hivatkozások küldése le van tiltva Fájlok és média tartalom küldése le van tiltva A SimpleX hivatkozások küldése engedélyezve van. @@ -1764,4 +1759,308 @@ Profilképek Profilkép alakzat Négyzet, kör vagy bármi a kettő között. + Célkiszolgáló hiba: %1$s + Továbbító kiszolgáló: %1$s +\nHiba: %2$s + Hálózati problémák - az üzenet többszöri elküldési kísérlet után lejárt. + A kiszolgáló verziója nem kompatibilis a hálózati beállításokkal. + Hibás kulcs vagy ismeretlen kapcsolat - valószínűleg ez a kapcsolat törlődött. + Továbbító kiszolgáló: %1$s +\nCélkiszolgáló hiba: %2$s + Hiba: %1$s + Kapacitás túllépés - a címzett nem kapta meg a korábban elküldött üzeneteket. + Üzenet kézbesítési figyelmeztetés + A kiszolgáló címe nem kompatibilis a hálózati beállításokkal. + Soha + Ismeretlen kiszolgálók + Ha az IP-cím rejtett + Üzenet állapot megjelenítése + Visszafejlesztés engedélyezése + Mindig + Nem + Nem védett + Igen + Ne használjon privát útválasztást. + Privát útválasztás + Használjon privát útválasztást ismeretlen kiszolgálókkal. + Mindig használjon privát útválasztást. + Üzenet útválasztási mód + Közvetlen üzenetküldés, ha az IP-cím védett és az ön kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást. + Közvetlen üzenetküldés, ha az ön kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást. + Az IP-cím védelmének érdekében a privát útválasztás az SMP-kiszolgálókat használja az üzenetek kézbesítéséhez. + Üzenet útválasztási tartalék + PRIVÁT ÜZENET ÚTVÁLASZTÁS + Privát útválasztás használata ismeretlen kiszolgálókkal, ha az IP-cím nem védett. + Ne küldjön üzeneteket közvetlenül, még akkor sem, ha az ön kiszolgálója vagy a célkiszolgáló nem támogatja a privát útválasztást. + Tor vagy VPN nélkül az IP-címe látható lesz a fájlkiszolgálók számára. + FÁJLOK + IP-cím védelem + Az alkalmazás kérni fogja az ismeretlen fájlkiszolgálókról történő letöltések megerősítését (kivéve, ha az .onion vagy a SOCKS proxy engedélyezve van). + Ismeretlen kiszolgálók! + Tor vagy VPN nélkül az IP-címe látható lesz az XFTP átjátszók számára: +\n%1$s. + Minden színmód + Fekete + Színmód + Sötét + Sötét mód + Sötét mód színei + Illesztés + Jó napot! + Jó reggelt! + Haladó beállítások + Alkalmazás erre + Csevegés színei + Csevegés témája + Kitöltés + Profiltéma + Csevegőlista megjelenítése új ablakban + Világos + Világos mód + Fogadott válasz + Kép eltávolítása + Mozaik + Szín visszaállítása + Méretezés + Elküldött válasz + Alapértelmezett téma beállítása + Rendszer + Háttérkép kiemelés + Háttérkép háttérszíne + További kiemelés 2 + Alkalmazás téma + Perzsa kezelőfelület + Védje IP-címét az ismerősei által kiválasztott üzenetküldő átjátszókkal szemben. +\nEngedélyezze a „Beállítások -> Hálózat és kiszolgálók” menüben. + Ismeretlen kiszolgálókról származó fájlok jóváhagyása. + Javított üzenetkézbesítés + Alkalmazás témájának visszaállítása + Tegye egyedivé a csevegéseit! + Új csevegési témák + Privát üzenet útválasztás 🚀 + Fájlok biztonságos fogadása + Csökkentett akkumulátor-használattal. + Hiba a WebView inicializálásában. Frissítse rendszerét az új verzióra. Kérjük, lépjen kapcsolatba a fejlesztőkkel. +\nHiba: %s + Felhasználó által létrehozott téma visszaállítása + Üzenet várakoztatási információ + nincs + Kézbesítési hibák felderítése + Kiszolgáló várakoztatási infó: %1$s +\nUtoljára kézbesített üzenet: %2$s + Hibás kulcs vagy ismeretlen fájltöredék cím - valószínűleg a fájl törlődött. + Ideiglenes fájlhiba + Üzenetállapot + Üzenetállapot: %s + Fájlhiba + A fájl nem található - valószínűleg a fájlt törölték vagy visszavonták. + Fájlkiszolgáló hiba: %1$s + Fájlállapot + Fájlállapot: %s + Másolási hiba + Ezt a hivatkozást egy másik mobilleszközön már használták, hozzon létre egy új hivatkozást az asztali számítógépén. + Ellenőrizze, hogy a mobil és az asztali számítógép ugyanahhoz a helyi hálózathoz csatlakozik-e, valamint az asztali számítógép tűzfalában engedélyezve van-e a kapcsolat. +\nMinden további problémát osszon meg a fejlesztőkkel. + Nem lehet üzenetet küldeni + A kiválasztott csevegési beállítások tiltják ezt az üzenetet. + Próbálja meg később. + A kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %1$s. + Inaktív tag + Továbbított üzenet + Az üzenet később is kézbesíthető, ha a tag aktívvá válik. + Még nincs közvetlen kapcsolat, az üzenetet az admin továbbítja. + Hivatkozás beolvasása / beillesztése + Beállított SMP-kiszolgálók + Egyéb SMP-kiszolgálók + Egyéb XFTP-kiszolgálók + letiltva + inaktív + Nagyítás + információk a kiszolgálókról + Kapcsolódás + Hibák + Függőben + Ekkortól kezdve: %s. +\nMinden adat biztonságban van a készülékén. + Elküldött üzenetek + Proxyzott kiszolgálók + Újrakapcsolódás a kiszolgálókhoz? + Újrakapcsolódás a kiszolgálóhoz? + Hiba a kiszolgálóhoz való újrakapcsolódáskor + Újrakapcsolódás minden kiszolgálóhoz + Hiba a statisztikák visszaállításakor + Visszaállítás + Minden statisztika visszaállítása + Minden statisztika visszaállítása? + A kiszolgálók statisztikái visszaállnak - ez nem vonható vissza! + Részletes statisztikák + Letöltve + lejárt + egyéb + Összes fogadott + Üzenetfogadási hibák + Újrakapcsolás + Üzenetküldési hibák + Közvetlenül küldött + Összes elküldött + Proxyn keresztül küldve + SMP-kiszolgáló + Ekkortól kezdve: %s. + Feltöltve + XFTP-kiszolgáló + Proxyzott + duplikációk + egyéb hibák + Kapcsolatok + Létrehozva + Biztosítva + Törlési hibák + Méret + Feltöltött fájlok + Letöltött fájltöredékek + Letöltött fájlok + Kiszolgáló beállításainak megnyitása + Kiszolgáló címe + Feltöltési hibák + Nyugtázva + Nyugtázott hibák + próbálkozások + Törölt fájltöredékek + Összes profil + Feltöltött fájltöredékek + Elkészült + Kapcsolódott kiszolgálók + Beállított XFTP-kiszolgálók + Kapcsolódva + Jelenlegi profil + Részletek + visszafejtési hibák + Törölve + Fogadott üzenetek + Letöltési hibák + Hiba + Hiba a kiszolgálókhoz való újrakapcsolódáskor + Fájlok + Betűméret + Nincs információ, próbálja meg újratölteni + Korábban kapcsolódott kiszolgálók + Privát útválasztási hiba + Fogadott üzenetek + Az összes kiszolgálóhoz való újrakapcsolás az üzenetek kézbesítésének kikényszerítéséhez. Ez további adatforgalmat használ. + A kiszolgálóhoz való újrakapcsolódás az üzenet kézbesítésének kikényszerítéséhez. Ez további adatforgalmat használ. + Elküldött üzenetek + Munkamenetek átvitele + Összesen + Statisztikák + Információk megjelenítése ehhez: + A kiszolgáló verziója nem kompatibilis az alkalmazással: %1$s. + Ön nem kapcsolódik ezekhez a kiszolgálókhoz. A privát útválasztás az üzenetek kézbesítésére szolgál. + Aktív kapcsolatok száma + Üzenetjelentés + Feliratkozva + Feliratkozási hibák + Elutasított feliratkozások + Százalék megjelenítése + Alkalmazásfrissítés letöltve + Frissítések keresése + Frissítések keresése + Alkalmazásfrissítés letöltése, ne zárja be az alkalmazást + Letöltés %s (%s) + Sikeresen telepítve + Frissítés telepítése + Fájl helyének megnyitása + Indítsa újra az alkalmazást. + Emlékeztessen később + Hagyja ki ezt a verziót + Ha értesítést szeretne kapni az új kiadásokról, kapcsolja be a stabil vagy béta verziók időszakos ellenőrzését. + Frissítés érhető el: %s + A frissítés letöltése megszakítva + Béta + Letiltás + Letiltva + Stabil + Hiba a(z) %1$s továbbító kiszolgálóhoz való kapcsolódáskor. Próbálja meg később. + A(z) %1$s célkiszolgáló verziója nem kompatibilis a(z) %2$s továbbító kiszolgálóval. + A(z) %1$s továbbító kiszolgáló nem tudott csatlakozni a(z) %2$s célkiszolgálóhoz. Próbálja meg később. + A(z) %1$s célkiszolgáló címe nem kompatibilis a(z) %2$s továbbító kiszolgáló beállításaival. + Média elhomályosítása + Közepes + Kikapcsolva + Enyhe + Erős + A továbbító kiszolgáló címe nem kompatibilis a hálózati beállításokkal: %1$s. + A továbbító kiszolgáló verziója nem kompatibilis a hálózati beállításokkal: %1$s. + hívás + Az ismerős törlésre fog kerülni - ez a művelet nem vonható vissza! + Csak a beszélgetés törlése + megnyitás + Beszélgetés törölve! + Ismerős törölve! + Archivált ismerősök + Nincsenek szűrt ismerősök + Hivatkozás beillesztése + A hívások le vannak tiltva! + Nem lehet felhívni az ismerőst + Nem lehet üzenetet küldeni a csoporttagnak + Kapcsolódás az ismerőshöz, várjon vagy ellenőrizze később! + Törölt ismerős. + Nem lehet felhívni a csoporttagot + Hívások engedélyezése? + Meghívás + üzenet + Beszélgetés megtartása + Biztosan törli az ismerőst? + kapcsolódás + Könnyen elérhető eszköztár + Törlés értesítés nélkül + Beállítások + keresés + videó + Az „Archivált ismerősökből” továbbra is küldhet üzeneteket neki: %1$s. + Ismerősök + Kérje meg az ismerősét, hogy engedélyezze a hívásokat. + Üzenet küldése a hívások engedélyezéséhez. + Engedélyeznie kell a hívásokat az ismerőse számára, hogy fel tudják hívni egymást. + A(z) %1$s nevű ismerősével folytatott beszélgetéseit továbbra is megtekintheti a csevegések listájában. + Üzenet + Kiválaszt + Az üzenetek minden tag számára moderáltként lesznek megjelölve. + Nincs kiválasztva semmi + Az üzenetek törlésre lesznek jelölve. A címzett(ek) képes(ek) lesz(nek) felfedni ezt az üzenetet. + Törli a tagok %d üzenetét? + %d kiválasztva + Az üzenetek minden tag számára törlésre kerülnek. + Csevegési adatbázis exportálva + Kapcsolatok- és kiszolgálók állapotának megjelenítése. + Kapcsolódjon gyorsabban az ismerőseihez + Folytatás + Ellenőrízze a hálózatát + Média- és fájlkiszolgálók + Legfeljebb 20 üzenet törlése egyszerre. + Védi az IP-címét és a kapcsolatokat. + Könnyen elérhető eszköztár + Üzenetkiszolgálók + SOCKS proxy + Néhány fájl nem került exportálásra: + Az exportált adatbázist átköltöztetheti. + Mentés és újrakapcsolódás + Használja az alkalmazást egy kézzel. + Ismerősök archiválása a későbbi csevegéshez. + TCP kapcsolat + Az exportált archívumot elmentheti. + Tippek visszaállítása + Csevegőlista átváltása: + Ezt a „Megjelenés” menüben módosíthatja. + Új médiabeállítások + Lejátszás a csevegési listából + Elhomályosítás a jobb adatvédelemért + Automatikus frissítés + Létrehozás + Új verziók letöltése a GitHub-ról + Betűméret növelése. + Meghívás + Új csevegési élmény 🎉 + Új üzenet + Érvénytelen hivatkozás + Ellenőrizze, hogy a SimpleX hivatkozás helyes-e. \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_account_box.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_account_box.svg index 9680161c4f..07f5645f79 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_account_box.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_account_box.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_account_circle_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_account_circle_filled.svg index f63966fbc8..db9ba45fab 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_account_circle_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_account_circle_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add.svg index 7da1daade8..da3f68daa4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_link.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_link.svg index 6e3ec1e453..1530035ead 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_link.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_link.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_photo.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_photo.svg index 3197ae8bb6..be7fcc8f54 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_photo.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_photo.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_reaction.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_reaction.svg index 62ee67166c..ecca4f4872 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_reaction.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_reaction.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_reaction_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_reaction_filled.svg index 0475962f63..47b4790bc5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_reaction_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_add_reaction_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_all_inclusive.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_all_inclusive.svg index 4675bb6642..7628baecda 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_all_inclusive.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_all_inclusive.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_back_ios_new.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_back_ios_new.svg index 6940e4d004..f4f2bc772f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_back_ios_new.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_back_ios_new.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_downward.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_downward.svg index c09c1c4d66..e9bd252628 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_downward.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_downward.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_drop_down.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_drop_down.svg new file mode 100644 index 0000000000..f2cfb3cbdd --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_drop_down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_drop_up.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_drop_up.svg new file mode 100644 index 0000000000..90c085ae7b --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_drop_up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_forward.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_forward.svg new file mode 100644 index 0000000000..0993b22658 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_forward.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_forward_ios.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_forward_ios.svg index dd07f2bad9..58b93e79a1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_forward_ios.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_forward_ios.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_outward.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_outward.svg new file mode 100644 index 0000000000..2391aba06c --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_outward.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_upward.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_upward.svg index 88b20124f3..0a54ed27db 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_upward.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_upward.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrows_left_right.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrows_left_right.svg new file mode 100644 index 0000000000..4b5a982e41 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrows_left_right.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_article.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_article.svg index 05d4d6fe6e..4cd46fc7cb 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_article.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_article.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_attach_file_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_attach_file_filled.svg index 5ff6754f22..dcebed008e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_attach_file_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_attach_file_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_attach_file_filled_500.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_attach_file_filled_500.svg index 04ef1d379f..d0897cd912 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_attach_file_filled_500.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_attach_file_filled_500.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_avg_pace.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_avg_pace.svg index d3052a6c84..78d1b86471 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_avg_pace.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_avg_pace.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_back_hand.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_back_hand.svg index 41013ff66c..b9a3887c8f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_back_hand.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_back_hand.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_backspace.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_backspace.svg index 5bb12b42ca..a82b16c3ce 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_backspace.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_backspace.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_backup.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_backup.svg index 5d4b0c0dc6..e076c55993 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_backup.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_backup.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_battery_2_bar.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_battery_2_bar.svg index fcdb3dfd50..3278d1d2dc 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_battery_2_bar.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_battery_2_bar.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_battery_3_bar.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_battery_3_bar.svg index 819fe590c9..9f73bc20f9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_battery_3_bar.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_battery_3_bar.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bluetooth.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bluetooth.svg index 53bc5becaa..d39581566e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bluetooth.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bluetooth.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_blur_on.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_blur_on.svg new file mode 100644 index 0000000000..d729371159 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_blur_on.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt.svg index 4c686c282a..aa276fa8c1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt_filled.svg index 012a8e2a45..2fd1fdcb38 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt_off.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt_off.svg index 7b003c8e7c..4278ea764a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt_off.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_bolt_off.svg @@ -1,8 +1,8 @@ \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cable.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cable.svg index 1ca40515b7..f786d0c2e9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cable.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cable.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call.svg index 19c840a5cf..05577c3d0b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_500.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_500.svg index f2e1e9af6b..c18f78b148 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_500.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_500.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_end.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_end.svg index 0f21f36e83..2bcb5949f9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_end.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_end.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_end_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_end_filled.svg index ceb32f98f5..2d1e207e0e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_end_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_end_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_filled.svg index c0277fd5b6..100c933b7b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_call_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_camera_enhance.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_camera_enhance.svg index b07fbd7a87..baea2e28e9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_camera_enhance.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_camera_enhance.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cancel_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cancel_filled.svg index 44e901ff3c..41c39e11ef 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cancel_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_cancel_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat.svg index c263166229..82e58ead79 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat_bubble.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat_bubble.svg index 132c514e92..78e3bf42dc 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat_bubble.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat_bubble.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check.svg index e3d6eab069..a5445b0d5f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check_circle.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check_circle.svg new file mode 100644 index 0000000000..813adcea2d --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check_circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check_circle_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check_circle_filled.svg index 42f1a37725..ac2f200c6f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check_circle_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check_circle_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check_filled.svg index e3d6eab069..a5445b0d5f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_check_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chevron_right.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chevron_right.svg index ac6025580b..3c33462417 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chevron_right.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chevron_right.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chevron_right_2.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chevron_right_2.svg new file mode 100644 index 0000000000..ef2a9e9865 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chevron_right_2.svg @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_circle.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_circle.svg index d0da456188..dd6d17343d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_circle.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_circle.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_circle_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_circle_filled.svg index e8eebbdc7c..65d942e3ad 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_circle_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_circle_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_close.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_close.svg index f3803e3cac..b24eaf6cb9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_close.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_close.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_code.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_code.svg index 8554e557b0..918bf1a005 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_code.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_code.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_collapse_all.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_collapse_all.svg index a380594e79..673cb66084 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_collapse_all.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_collapse_all.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_content_copy.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_content_copy.svg index 2f4b964368..f8dde3e39f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_content_copy.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_content_copy.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_content_paste.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_content_paste.svg index 84a0be536b..b162257525 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_content_paste.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_content_paste.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_database.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_database.svg index 8b47047b60..1195169889 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_database.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_database.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_delete.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_delete.svg index 1709888116..ea3f90edaa 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_delete.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_delete.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_delete_forever.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_delete_forever.svg index 519fbc87fb..af3f3c5fb3 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_delete_forever.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_delete_forever.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_delete_forever_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_delete_forever_filled.svg index c8c1d4cea5..8b46878258 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_delete_forever_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_delete_forever_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_desktop.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_desktop.svg index e9c30f5199..aa60447d7e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_desktop.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_desktop.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_dns.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_dns.svg index c1056cefc6..932f45e6b9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_dns.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_dns.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_do_not_disturb_on.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_do_not_disturb_on.svg index 9a0d038ba5..14df478255 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_do_not_disturb_on.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_do_not_disturb_on.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_do_not_touch.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_do_not_touch.svg index 5ea1a5f2e3..698df39e9c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_do_not_touch.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_do_not_touch.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_done_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_done_filled.svg index e3d6eab069..a5445b0d5f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_done_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_done_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_double_check.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_double_check.svg index 2c651607df..a5c0797f3e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_double_check.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_double_check.svg @@ -1,6 +1,6 @@ - + Created with Fabric.js 5.3.0 diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_download.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_download.svg index 8d62bebda6..65e1af1fb6 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_download.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_download.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_draft.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_draft.svg index bbe311364f..edced6eb83 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_draft.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_draft.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_draft_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_draft_filled.svg index 06140a055d..f40173226c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_draft_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_draft_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_drive_folder_upload.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_drive_folder_upload.svg index ec3d03510e..94a21b1a9d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_drive_folder_upload.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_drive_folder_upload.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_edit.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_edit.svg index 400707272e..143f7e8e0b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_edit.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_edit.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_edit_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_edit_filled.svg index e87e2eccaf..7550e08df5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_edit_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_edit_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_edit_note.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_edit_note.svg index 0c66d1bc4d..cb0488d834 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_edit_note.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_edit_note.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_electrical_services.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_electrical_services.svg index e6ab5bb608..03e014417d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_electrical_services.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_electrical_services.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_engineering.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_engineering.svg index 0848537041..ea2c5c3843 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_engineering.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_engineering.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_error.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_error.svg index 60296ca034..7a91448c9d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_error.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_error.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_error_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_error_filled.svg index 6aaa01f0d3..7940dc756a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_error_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_error_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_expand_all.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_expand_all.svg index 75b2874d5b..ec76dfb115 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_expand_all.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_expand_all.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_expand_less.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_expand_less.svg deleted file mode 100644 index 23a6402630..0000000000 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_expand_less.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_expand_more.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_expand_more.svg deleted file mode 100644 index 77becd0856..0000000000 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_expand_more.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_filter_list.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_filter_list.svg index a54ee0dc55..4a9c6e8420 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_filter_list.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_filter_list.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_flag.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_flag.svg index 35dd4ac780..d2f84f3b3a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_flag.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_flag.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_flip_camera_android_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_flip_camera_android_filled.svg index da9d686ce9..0244be139f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_flip_camera_android_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_flip_camera_android_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_filled.svg index f4c284a5f2..ab47ff4136 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_filled.svg @@ -1,8 +1,8 @@ \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_pen.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_pen.svg index f1aba5bee6..7f2fb6e38e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_pen.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_folder_pen.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_forum.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_forum.svg index 0f556da867..5271a02284 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_forum.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_forum.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_forward.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_forward.svg index 7ad0b14b70..174b80bcc7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_forward.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_forward.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_github.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_github.svg index 70baacb727..996fbd9743 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_github.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_github.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_group.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_group.svg index 74d6cdcc02..88c24ca67a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_group.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_group.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_headphones.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_headphones.svg index 2bda1e9d74..8487893d8d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_headphones.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_headphones.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_help.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_help.svg index 3bf2406ab8..b654c8eee5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_help.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_help.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_history.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_history.svg index f98f4db386..6edaf1512c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_history.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_history.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_id_card.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_id_card.svg new file mode 100644 index 0000000000..e2539c3608 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_id_card.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_image.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_image.svg index 408668d204..aa42e1a978 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_image.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_image.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_info.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_info.svg index aa3e5a2f4e..b854b30359 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_info.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_info.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_inventory_2.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_inventory_2.svg index c7e8aa21c1..20c39b2ed2 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_inventory_2.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_inventory_2.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_ios_share.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_ios_share.svg index 099b5149bf..33f52db54b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_ios_share.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_ios_share.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard.svg index 35689927b1..a39e9c7e0b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard_arrow_down.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard_arrow_down.svg index c92d244c17..aaa188bed7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard_arrow_down.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard_arrow_down.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard_voice.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard_voice.svg index a938a4250f..769198e13c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard_voice.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard_voice.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard_voice_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard_voice_filled.svg index bd28745400..44c0f89fa0 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard_voice_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_keyboard_voice_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_light_mode.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_light_mode.svg index 858ee788cf..44bad28fc9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_light_mode.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_light_mode.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lightbulb.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lightbulb.svg new file mode 100644 index 0000000000..e84b1bd476 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lightbulb.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_link.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_link.svg index b8063c1c8c..7de4497d5d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_link.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_link.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock.svg index 2bc91dde71..cec12d0d4a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_filled.svg index 396e11fac6..798a92145a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_open_right.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_open_right.svg index 3188cf798e..0805267813 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_open_right.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_lock_open_right.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_login.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_login.svg index be56f0f451..d675b77073 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_login.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_login.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_logout.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_logout.svg index 5808094655..734242b294 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_logout.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_logout.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mail.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mail.svg index 0a4b8adbd8..f45ed237f3 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mail.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mail.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mail_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mail_filled.svg new file mode 100644 index 0000000000..c51057b8cb --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mail_filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_manage_accounts.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_manage_accounts.svg index e1cc6816b1..6a5b60424c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_manage_accounts.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_manage_accounts.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_maps_ugc.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_maps_ugc.svg index 469d44a2de..282b36ba25 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_maps_ugc.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_maps_ugc.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mark_chat_unread.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mark_chat_unread.svg index 5e1e746ee9..a89a956b29 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mark_chat_unread.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mark_chat_unread.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_match_case.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_match_case.svg new file mode 100644 index 0000000000..20adb34c10 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_match_case.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_menu.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_menu.svg index a192e32f34..28070d177d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_menu.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_menu.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mic.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mic.svg index a938a4250f..769198e13c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mic.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mic.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mic_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mic_filled.svg index bd28745400..44c0f89fa0 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mic_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mic_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mic_off.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mic_off.svg index 1ea755eb4e..f7f2c074f4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mic_off.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_mic_off.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_more_horiz.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_more_horiz.svg index cc4f97431d..8d36d622c0 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_more_horiz.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_more_horiz.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_more_vert.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_more_vert.svg index 9a9c7d193b..dc55c23649 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_more_vert.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_more_vert.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_music_note.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_music_note.svg index 766dc76be8..9e127f18f8 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_music_note.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_music_note.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_note_add.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_note_add.svg index 8459292f6e..76ba771f33 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_note_add.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_note_add.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notifications.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notifications.svg index bcba2ae944..afe98fc544 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notifications.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notifications.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notifications_off.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notifications_off.svg index 556013c364..ca1de72e82 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notifications_off.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notifications_off.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notifications_off_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notifications_off_filled.svg index 647ad7dd3d..98a24503d7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notifications_off_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_notifications_off_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_open_in_new.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_open_in_new.svg index 9669ee635e..0edc51abef 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_open_in_new.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_open_in_new.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_outline_terminal.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_outline_terminal.svg index 07f8078e0c..f384ef36bd 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_outline_terminal.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_outline_terminal.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_palette.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_palette.svg new file mode 100644 index 0000000000..a0da2dcca5 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_palette.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_pause_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_pause_filled.svg index eaa4573780..dc742eb7e8 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_pause_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_pause_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_pending.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_pending.svg index 90b9dc7204..7ee6422616 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_pending.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_pending.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_pending_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_pending_filled.svg index 00fe0d41a7..eee4c2c323 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_pending_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_pending_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person.svg index 0830a9963b..0a87c6589e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_add.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_add.svg index 4b2a249f23..4e22be2b55 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_add.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_add.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_add_500.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_add_500.svg index 179dce95fb..1c67f0813a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_add_500.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_add_500.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_off.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_off.svg new file mode 100644 index 0000000000..54cfd3e1c0 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_phone_bluetooth_speaker.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_phone_bluetooth_speaker.svg index 513bb38c40..48fe86d1ec 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_phone_bluetooth_speaker.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_phone_bluetooth_speaker.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_phone_in_talk_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_phone_in_talk_filled.svg index b43c7d9c15..45cbd61bbd 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_phone_in_talk_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_phone_in_talk_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_photo_camera.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_photo_camera.svg index aa99ac888a..ddb231b4bb 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_photo_camera.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_photo_camera.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_play_arrow_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_play_arrow_filled.svg index 6b3ed2a581..9b5f303e35 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_play_arrow_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_play_arrow_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_power_settings_new.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_power_settings_new.svg index 0af51ad2ab..c125143e41 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_power_settings_new.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_power_settings_new.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_priority_high.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_priority_high.svg index 42b93fba83..408b4b4376 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_priority_high.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_priority_high.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_qr_code.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_qr_code.svg index d7f8763c87..621fe3c413 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_qr_code.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_qr_code.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_question_mark.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_question_mark.svg index 9c2e7e110b..4346924f9d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_question_mark.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_question_mark.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radio_button_unchecked.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radio_button_unchecked.svg new file mode 100644 index 0000000000..7a3a36dc89 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radio_button_unchecked.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radiowaves_up_forward_1_bar.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radiowaves_up_forward_1_bar.svg new file mode 100644 index 0000000000..a7dd37c4e6 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radiowaves_up_forward_1_bar.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radiowaves_up_forward_2_bar.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radiowaves_up_forward_2_bar.svg new file mode 100644 index 0000000000..cb6e344be9 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radiowaves_up_forward_2_bar.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radiowaves_up_forward_3_bar.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radiowaves_up_forward_3_bar.svg new file mode 100644 index 0000000000..4bde8511f6 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radiowaves_up_forward_3_bar.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radiowaves_up_forward_4_bar.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radiowaves_up_forward_4_bar.svg new file mode 100644 index 0000000000..131569e9bc --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_radiowaves_up_forward_4_bar.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_redeem.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_redeem.svg index a0093692cc..3178938a86 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_redeem.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_redeem.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_refresh.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_refresh.svg index a1f27d5798..bcbe9c4f34 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_refresh.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_refresh.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_repeat_one.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_repeat_one.svg index d37aa28022..014453c240 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_repeat_one.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_repeat_one.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_replay.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_replay.svg index 9f8c033f89..3e1b60666c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_replay.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_replay.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_reply.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_reply.svg index 7298dc984e..bffa9f33a2 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_reply.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_reply.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_report.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_report.svg index 8695857b97..c4c54f0e02 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_report.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_report.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_report_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_report_filled.svg index 914c74a40a..8148ca77f2 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_report_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_report_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_restart_alt.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_restart_alt.svg index a28c95673e..d2f91c991c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_restart_alt.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_restart_alt.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_ring_volume.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_ring_volume.svg index 92bc814040..4b01530b52 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_ring_volume.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_ring_volume.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_safety_divider.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_safety_divider.svg index 5517bcd740..9b532e6420 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_safety_divider.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_safety_divider.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_schedule.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_schedule.svg index 4bd8e90cb6..2e806eb315 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_schedule.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_schedule.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_schedule_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_schedule_filled.svg index 368e2fcc1f..b7afaf420b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_schedule_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_schedule_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_search.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_search.svg index 8a74c6d43f..7a6e668cf5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_search.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_search.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_search_500.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_search_500.svg index 18e56def19..2e36a5dec4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_search_500.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_search_500.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_security.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_security.svg index 13b5c1914e..2801bacad7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_security.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_security.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings.svg index 04b250537d..edc27762a0 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings_backup_restore.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings_backup_restore.svg index a3cf4db7e1..22e8bcbbfb 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings_backup_restore.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings_backup_restore.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings_ethernet.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings_ethernet.svg index 828c7b8a8b..8fd3d489c9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings_ethernet.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings_ethernet.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings_phone.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings_phone.svg index 7280127ceb..6231a35d21 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings_phone.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_settings_phone.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_share.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_share.svg index f24d145a50..32d2875894 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_share.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_share.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_share_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_share_filled.svg index 16b4c0ecf2..86424776cb 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_share_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_share_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_shield.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_shield.svg index ca1ed6828c..e99e34a8d3 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_shield.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_shield.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_smart_display.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_smart_display.svg index 195c3b2bea..4027f2f1f9 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_smart_display.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_smart_display.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_smartphone.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_smartphone.svg index 93094d1445..bb13e7f818 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_smartphone.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_smartphone.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_smartphone_300.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_smartphone_300.svg index 7d8553db11..6d9d70650c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_smartphone_300.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_smartphone_300.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_star.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_star.svg index 2a2733830a..72d7962366 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_star.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_star.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_star_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_star_filled.svg index bf9bd2b4b5..22afbba02c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_star_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_star_filled.svg @@ -1,2 +1,2 @@ - \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_star_off.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_star_off.svg index d17a1e37d9..f353793d2a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_star_off.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_star_off.svg @@ -1,8 +1,8 @@ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_start.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_start.svg index 42983718ac..f60449580b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_start.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_start.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_stop_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_stop_filled.svg index 0e8823dab5..bda4d1e10c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_stop_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_stop_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_supervised_user_circle_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_supervised_user_circle_filled.svg index ecfb7002a1..162ef06670 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_supervised_user_circle_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_supervised_user_circle_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_swap_horizontal_circle.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_swap_horizontal_circle.svg index 0b842e8716..05de300ed2 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_swap_horizontal_circle.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_swap_horizontal_circle.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_swap_horizontal_circle_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_swap_horizontal_circle_filled.svg index 3509f870de..a71845eac7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_swap_horizontal_circle_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_swap_horizontal_circle_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_sync_problem.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_sync_problem.svg index 79a0441792..4f85d90659 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_sync_problem.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_sync_problem.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_tag.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_tag.svg index 4b7e39b8ec..535d97a168 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_tag.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_tag.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_task.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_task.svg index eab8e8f09d..e8120529a3 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_task.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_task.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_theater_comedy.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_theater_comedy.svg index 24267d0fa8..e6a78e6923 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_theater_comedy.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_theater_comedy.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_theater_comedy_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_theater_comedy_filled.svg index 401e9affac..c1a3753674 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_theater_comedy_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_theater_comedy_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_timer.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_timer.svg index e530ef1e83..8f50769f03 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_timer.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_timer.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_timer_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_timer_filled.svg index 4e0a99775a..a7576c6ee2 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_timer_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_timer_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_toast.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_toast.svg new file mode 100644 index 0000000000..dccf6dff3d --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_toast.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_toggle_on.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_toggle_on.svg index de240dce5f..02e068f30a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_toggle_on.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_toggle_on.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_translate.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_translate.svg index 8ad343c088..6b05619e01 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_translate.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_translate.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_travel_explore.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_travel_explore.svg index 023ce42a26..1cacbed166 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_travel_explore.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_travel_explore.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_upgrade.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_upgrade.svg new file mode 100644 index 0000000000..ab8fb7d951 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_upgrade.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_upload_file.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_upload_file.svg index 1840509fb1..d84e8bc513 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_upload_file.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_upload_file.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_usb.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_usb.svg index 068bfc1a82..0663015ae4 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_usb.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_usb.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_verified_user.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_verified_user.svg index cd6e89db86..7506004c1d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_verified_user.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_verified_user.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_videocam.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_videocam.svg index 9a9edf61ce..d0ebb2fd41 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_videocam.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_videocam.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_videocam_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_videocam_filled.svg index 0606c6a06f..9f3b877e0d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_videocam_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_videocam_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_videocam_off.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_videocam_off.svg index 77c85ed987..992f052ed0 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_videocam_off.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_videocam_off.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility.svg index 50095bc6e5..24c3544621 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility_filled.svg index 2bc6fbd9a5..1f07b63790 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility_off.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility_off.svg index 459ea40242..7cacfafdbf 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility_off.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility_off.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility_off_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility_off_filled.svg index 7269dffdd3..9a52f493a7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility_off_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_visibility_off_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_volume_down.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_volume_down.svg index 44b8f89e91..51f7d1a35d 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_volume_down.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_volume_down.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_volume_up.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_volume_up.svg index 8d9f2ebecf..292a1fe27e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_volume_up.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_volume_up.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_vpn_key_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_vpn_key_filled.svg index adb8814e12..f40ab9c6e1 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_vpn_key_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_vpn_key_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_vpn_key_off_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_vpn_key_off_filled.svg index 6ae367024e..0fe5d9a21e 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_vpn_key_off_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_vpn_key_off_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_warning.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_warning.svg index 7148c5740b..065e2f883c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_warning.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_warning.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_warning_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_warning_filled.svg index 673b7453f6..7806a17d7f 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_warning_filled.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_warning_filled.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_wifi.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_wifi.svg index 2fb4750af5..313748293b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_wifi.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_wifi.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_wifi_off.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_wifi_off.svg index 814077e485..31354b7ed2 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_wifi_off.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_wifi_off.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_wifi_tethering.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_wifi_tethering.svg index f6007d3f02..b6b6eedd9c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_wifi_tethering.svg +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_wifi_tethering.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_cats@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_cats@4x.png new file mode 100644 index 0000000000..9bff3eb3d0 Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_cats@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_flowers@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_flowers@4x.png new file mode 100644 index 0000000000..e0ee4b057d Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_flowers@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_hearts@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_hearts@4x.png new file mode 100644 index 0000000000..35da7c7aed Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_hearts@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_kids@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_kids@4x.png new file mode 100644 index 0000000000..f5f15d3643 Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_kids@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_school@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_school@4x.png new file mode 100644 index 0000000000..f6e1cce383 Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_school@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_travel@4x.png b/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_travel@4x.png new file mode 100644 index 0000000000..64ec137331 Binary files /dev/null and b/apps/multiplatform/common/src/commonMain/resources/MR/images/wallpaper_travel@4x.png differ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml new file mode 100644 index 0000000000..b6d6770250 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/in/strings.xml @@ -0,0 +1,115 @@ + + + %1$s ANGGOTA + Alamat + %1$d pesan dihapus admin %2$s + 1 menit + 30 detik + 5 menit + Semua pesan akan dihapus - ini tidak bisa dikembalikan! Pesan akan HANYA dihapus untukmu. + Terima permintaan relasi? + Tentang alamat SimpleX + Tambah kontak + Tentang SimpleX + %1$d gagal mendekripsi pesan. + Tolong laporkan hal ini ke pengembang. + 1 bulan + 1 minggu + Terima + Anda sudah bergabung dalam grup <b>%1$s</b>. + Panggilan suara + Batal + izinkan pesan suara? + Terima + Versi aplikasi + Versi aplikasi: v%s + panggilan suara + 1 hari + Buat pesan sambutan + Batalkan penggantian alamat + Tambah profil + Corak + Tambahan sekunder + Motif aplikasi + selalu + diizinkan mengirim pesan suara. + semua anggota + Panggilan suara dan video + Hal-hal lain + Sudah menghubungi! + Sudah bergabung dengan grup! + %1$s ingin menghubungimu lewat + Batalkan penggantian alamat? + diatas, lalu: + Tambahkan ke perangkat lain + Boleh + Selalu + APLIKASI + Tampilan + Tentang SimpleX Chat + Terima + Terima + Batalkan + Kamera + tebal + menelepon… + Bluetooth + Panggilan ditutup + Ubah + <b>Peringatan</b>: arsip akan dihapus. + <b>Buat grup</b>: untuk membuat grup baru. + Blokir anggota + Blokir + Kamera + Hitam + Blokir anggota? + Ubah alamat penerima + Beta + Blokir untuk semua + Blokir anggota untuk semua? + Panggilan telah ditutup! + Seluler + Hubungkan melalui tautan satu kali? + Gabung Grup? + Gunakan profil saat ini + Gunakan profil penyamaran baru + Profil Anda akan dikirim ke kontak yang menerima tautan ini. + Anda akan terhubung ke semua anggota grup. + Hubungkan + Hubungkan penyamaran + Membuka basis data… + k + Hubungkan melalui alamat kontak? + Bersihkan + Bersihkan + Bersihkan + Cek koneksi internetmu dan coba lagi + Hubungkan + terhubung + Hubungkan + Terhubung + Terhubung + terhubung + Hubungkan langsung? + tersambung + selesai + terhubung + Komputer yang terhubung + Terhubung + Selesai + Ponsel yang terhubung + Hubungkan secara otomatis + menyambungkan… + Menghubungkan + Tautan tidak valid + Periksa apakah tautan SimpleX sudah benar. + Anda terhubung ke server yang digunakan untuk menerima pesan dari kontak ini. + Mencoba menyambung ke server yang digunakan untuk menerima pesan dari kontak ini (error: %1$s). + Migrasi basis data sedang berlangsung, +\nmemerlukan waktu beberapa menit. + menghubungkan + Lokasi file tidak valid + Tampilan macet + Anda membagikan lokasi file yang tidak valid. Laporkan masalah ini ke pengembang aplikasi. + error + \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml index 310a021ff2..34ee1721f0 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/it/strings.xml @@ -249,7 +249,7 @@ Accetta in incognito Tutti i messaggi verranno eliminati, non è reversibile! I messaggi verranno eliminati SOLO per te. Aggiungi server preimpostati - Aggiungi server… + Aggiungi server Impostazioni di rete avanzate Riguardo SimpleX chiamata accettata @@ -271,7 +271,7 @@ Database della chat eliminato ICONA APP <b>Ideale per la batteria</b>. Riceverai notifiche solo quando l\'app è in esecuzione (NO servizio in secondo piano). - <b>Consuma più batteria</b>! Servizio in secondo piano sempre attivo: le notifiche sono mostrate non appena i messaggi sono disponibili. + <b>Consuma più batteria</b>! L\'app funziona sempre in secondo piano: le notifiche vengono mostrate istantaneamente. chiamata… annulla anteprima link Impossibile accedere al Keystore per salvare la password del database @@ -351,7 +351,7 @@ Come usare il markdown chiamata audio chiamata audio (non crittografata e2e) - <b>Buono per la batteria</b>. Il servizio in secondo piano cerca messaggi ogni 10 minuti. Potresti perdere chiamate o messaggi urgenti. + <b>Buono per la batteria</b>. L\'app cerca messaggi ogni 10 minuti. Potresti perdere chiamate o messaggi urgenti. Chiamata già terminata! Crea il tuo profilo Decentralizzato @@ -606,22 +606,18 @@ Installa SimpleX Chat per terminale Assicurati che gli indirizzi dei server WebRTC ICE siano nel formato corretto, uno per riga e non doppi. Rete e server - Impostazioni di rete + Impostazioni avanzate No Gli host Onion saranno necessari per la connessione. \nNota bene: non potrai connetterti ai server senza indirizzo .onion . - Gli host Onion saranno necessari per la connessione. Gli host Onion verranno usati quando disponibili. - Gli host Onion verranno usati quando disponibili. Gli host Onion non verranno usati. - Gli host Onion non verranno usati. Valuta l\'app Obbligatorio Salva I server WebRTC ICE salvati verranno rimossi. Condividi link Dai una stella su GitHub - Aggiornare l\'impostazione degli host .onion\? Usare una connessione internet diretta\? Usa gli host .onion Usare il proxy SOCKS\? @@ -653,7 +649,7 @@ Il tuo profilo, i contatti e i messaggi recapitati sono memorizzati sul tuo dispositivo. Il tuo profilo è memorizzato sul tuo dispositivo e condiviso solo con i tuoi contatti. I server di SimpleX non possono vedere il tuo profilo. Ignora - Immune a spam e abusi + Immune allo spam Chiamata in arrivo Videochiamata in arrivo Istantaneo @@ -661,17 +657,18 @@ Crea una connessione privata Molte persone hanno chiesto: <i>se SimpleX non ha identificatori utente, come può recapitare i messaggi\?</i> Solo i dispositivi client memorizzano i profili utente, i contatti, i gruppi e i messaggi inviati con <b>crittografia end-to-end a 2 livelli</b>. - Protocollo e codice open source: chiunque può gestire i server. + Chiunque può installare i server. Incolla il link che hai ricevuto - Le persone possono connettersi a te solo tramite i link che condividi. + Sei tu a decidere chi può connettersi. Periodico Privacy ridefinita Notifiche private Maggiori informazioni nel nostro <font color="#0088ff">repository GitHub</font>. Maggiori informazioni nel nostro repository GitHub. Rifiuta - La prima piattaforma senza alcun identificatore utente – privata by design. - La nuova generazione di messaggistica privata + Nessun identificatore utente. + La nuova generazione +\ndi messaggistica privata Per proteggere la privacy, invece degli ID utente usati da tutte le altre piattaforme, SimpleX dispone di identificatori per le code dei messaggi, separati per ciascuno dei tuoi contatti. Usa la chat videochiamata @@ -816,7 +813,6 @@ Scadenza del protocollo Ricezione via Ripristina i predefiniti - Ripristina Salva Salva il profilo del gruppo sec @@ -847,7 +843,6 @@ Proibisci l\'invio di messaggi vocali. ricevuto, vietato Ripristina i colori - Salva colore Imposta 1 giorno Imposta le preferenze del gruppo Tema @@ -1120,7 +1115,7 @@ Per connettervi, il tuo contatto può scansionare il codice QR o usare il link nell\'app. Quando le persone chiedono di connettersi, puoi accettare o rifiutare. Indirizzo SimpleX - COLORI DEL TEMA + COLORI DELL\'INTERFACCIA I tuoi contatti resteranno connessi. Aggiungi l\'indirizzo al tuo profilo, in modo che i tuoi contatti possano condividerlo con altre persone. L\'aggiornamento del profilo verrà inviato ai tuoi contatti. Crea un indirizzo per consentire alle persone di connettersi con te. @@ -1148,7 +1143,7 @@ Secondario Messaggio ricevuto Messaggio inviato - Titolo + Titoli Link una tantum Info sull\'indirizzo SimpleX Tutti i tuoi contatti resteranno connessi. L\'aggiornamento del profilo verrà inviato ai tuoi contatti. @@ -1246,7 +1241,7 @@ Scompare il Scompare il: %s nessun testo - Si sono verificati alcuni errori non gravi durante l\'importazione: vedi la console della chat per i dettagli. + Si sono verificati alcuni errori non fatali durante l\'importazione: Riavvia APP Le notifiche smetteranno di funzionare fino a quando non riavvierai l\'app @@ -1769,4 +1764,309 @@ La fonte del messaggio resta privata. Forma delle immagini del profilo Quadrata, circolare o qualsiasi forma tra le due + Server di inoltro: %1$s +\nErrore del server di destinazione: %2$s + Server di inoltro: %1$s +\nErrore: %2$s + Problemi di rete - messaggio scaduto dopo molti tentativi di inviarlo. + L\'indirizzo del server non è compatibile con le impostazioni di rete. + La versione del server non è compatibile con le impostazioni di rete. + Chiave sbagliata o connessione sconosciuta - molto probabilmente questa connessione è stata eliminata. + Errore del server di destinazione: %1$s + Errore: %1$s + Quota superata - il destinatario non ha ricevuto i messaggi precedentemente inviati. + Avviso di consegna del messaggio + Instradamento privato + Mai + Server sconosciuti + Usa l\'instradamento privato con server sconosciuti. + Modalità instradamento messaggio + Usa l\'instradamento privato con server sconosciuti quando l\'indirizzo IP non è protetto. + + Invia messaggi direttamente quando il tuo server o quello di destinazione non supporta l\'instradamento privato. + Quando l\'IP è nascosto + Ripiego instradamento messaggio + Mostra stato del messaggio + Consenti downgrade + Sempre + Usa sempre l\'instradamento privato. + NON inviare messaggi direttamente, anche se il tuo server o quello di destinazione non supporta l\'instradamento privato. + NON usare l\'instradamento privato. + No + INSTRADAMENTO PRIVATO DEI MESSAGGI + Invia messaggi direttamente quando l\'indirizzo IP è protetto e il tuo server o quello di destinazione non supporta l\'instradamento privato. + Per proteggere il tuo indirizzo IP, l\'instradamento privato usa i tuoi server SMP per consegnare i messaggi. + Non protetto + Server sconosciuti! + Proteggi l\'indirizzo IP + L\'app chiederà di confermare i download da server di file sconosciuti (eccetto .onion o quando il proxy SOCKS è attivo). + Senza Tor o VPN, il tuo indirizzo IP sarà visibile ai server di file. + FILE + Senza Tor o VPN, il tuo indirizzo IP sarà visibile a questi relay XFTP: +\n%1$s. + Tema della chat + Nero + Modalità di colore + Scura + Modalità scura + Principale aggiuntivo 2 + Tutte le modalità di colore + Applica a + Colori modalità scura + Tema del profilo + Risposta ricevuta + Colori della chat + Riempi + Chiara + Modalità chiara + Ripristina colore + Impostazioni avanzate + Rimuovi immagine + Ripeti + Scala + Adatta + Risposta inviata + Imposta tema predefinito + Mostra la lista di chat in una nuova finestra + Sistema + Tinta dello sfondo + Buon pomeriggio! + Buongiorno! + Retro dello sfondo + Instradamento privato dei messaggi 🚀 + Proteggi il tuo indirizzo IP dai relay di messaggistica scelti dai tuoi contatti. +\nAttivalo nelle impostazioni *Rete e server*. + Errore di inizializzazione di WebView. Aggiorna il sistema ad una nuova versione. Contatta gli sviluppatori. +\nErrore: %s + Tema dell\'app + Ripristina al tema dell\'app + Ripristina al tema dell\'utente + Conferma i file da server sconosciuti. + Consegna dei messaggi migliorata + Cambia l\'aspetto delle tue chat! + Nuovi temi delle chat + Interfaccia in persiano + Ricevi i file in sicurezza + Con consumo di batteria ridotto. + Info coda messaggi + nessuna + info coda server: %1$s +\n +\nultimo msg ricevuto: %2$s + Debug della consegna + Stato del messaggio: %s + Stato del file: %s + Chiave sbagliata o indirizzo sconosciuto per frammento del file - probabilmente il file è stato eliminato. + File non trovato - probabilmente è stato eliminato o annullato. + Stato del messaggio + Stato del file + Errore del server del file: %1$s + Errore del file + Errore del file temporaneo + Controlla che mobile e desktop siano collegati alla stessa rete locale e che il firewall del desktop consenta la connessione. +\nSi prega di condividere qualsiasi altro problema con gli sviluppatori. + Questo link è stato usato con un altro dispositivo mobile, creane uno nuovo sul desktop. + Copia errore + Impossibile inviare il messaggio + Le preferenze della chat selezionata vietano questo messaggio. + Connessioni + Connessioni attive + Creato + errori di decifrazione + Statistiche dettagliate + doppi + Errore + Errore di riconnessione al server + Errore di riconnessione ai server + scaduto + Ricezione messaggi + altro + altri errori + In attesa + Via proxy + Server via proxy + Totale ricevuto + Errori di ricezione + Riconnetti + Riconnetti tutti i server connessi per forzare la consegna dei messaggi. Usa traffico aggiuntivo. + Riconnettere il server? + Riconnettere i server? + Riconnetti il server per forzare la consegna dei messaggi. Usa traffico aggiuntivo. + Errori di invio + Inviato direttamente + Messaggi inviati + Inviato via proxy + Le statistiche dei server verranno azzerate - è irreversibile! + Azzera tutte le statistiche + Azzerare tutte le statistiche? + Azzera + Errore di azzeramento statistiche + Server SMP + Inizio da %s. + Totale + Inviato + Server XFTP + Riconosciuto + Errori di riconoscimento + Blocchi eliminati + Blocchi scaricati + Blocchi inviati + Eliminato + Errori di eliminazione + File scaricati + Apri impostazioni server + Protetto + Indirizzo server + Dimensione + Iscritto + Errori di iscrizione + Iscrizioni ignorate + File inviati + Errori di invio + Errore di instradamento privato + La versione del server non è compatibile con la tua app: %1$s. + Membro inattivo + Il messaggio può essere consegnato più tardi se il membro diventa attivo. + Server SMP configurati + Altri server SMP + Mostra percentuale + inattivo + Zoom + Connesso + In connessione + Profilo attuale + Dettagli + Errori + Messaggi ricevuti + Messaggi inviati + Nessuna informazione, prova a ricaricare + Info dei server + Informazioni di + Statistiche + Sessioni di trasporto + Tutti i profili + tentativi + Server XFTP configurati + Completato + Server connessi + disattivato + Scaricato + Messaggi ricevuti + Errori di scaricamento + Riconnetti tutti i server + L\'indirizzo del server non è compatibile con le impostazioni di rete: %1$s. + File + Scansiona / Incolla link + Dimensione carattere + Totale inviato + Messaggio inoltrato + Inizio da %s. +\nTutti i dati sono privati, nel tuo dispositivo. + Ancora nessuna connessione diretta, il messaggio viene inoltrato dall\'amministratore. + Non sei connesso/a a questi server. L\'instradamento privato è usato per consegnare loro i messaggi. + Altri server XFTP + Server precedentemente connessi + Riprova più tardi. + Beta + Disattiva + Disattivato + Scaricamento dell\'aggiornamento, non chiudere l\'app + Scarica %s (%s) + Installato correttamente + Apri percorso file + Riavvia l\'app. + Ricordamelo più tardi + Salta questa versione + Stabile + Per essere avvisato sulle nuove versioni, attiva il controllo periodico di versioni stabili o beta. + Aggiornamento disponibile: %s + Scaricamento aggiornamento annullato + Cerca aggiornamenti + Aggiornamento dell\'app scaricato + Cerca aggiornamenti + Installa aggiornamento + Il server di inoltro %1$s non è riuscito a connettersi al server di destinazione %2$s. Riprova più tardi. + L\'indirizzo del server di inoltro è incompatibile con le impostazioni di rete: %1$s. + L\'indirizzo del server di destinazione di %1$s è incompatibile con le impostazioni del server di inoltro %2$s. + La versione del server di destinazione di %1$s è incompatibile con il server di inoltro %2$s. + Errore di connessione al server di inoltro %1$s. Riprova più tardi. + La versione server di inoltro è incompatibile con le impostazioni di rete: %1$s. + Off + Sfocatura file multimediali + Leggera + Media + Forte + chiama + messaggio + apri + cerca + Impostazioni + Elimina senza avvisare + Tieni la conversazione + Elimina solo la conversazione + Puoi inviare messaggi a %1$s dai contatti archiviati. + Contatti archiviati + Nessun contatto filtrato + Incolla link + I tuoi contatti + Barra degli strumenti di chat accessibile + Invita + Consentire le chiamate? + Chiamate proibite! + Impossibile chiamare il contatto + Impossibile chiamare il membro del gruppo + In collegamento con il contatto, attendi o controlla più tardi! + Il contatto è stato eliminato. + Chiedi al contatto di attivare le chiamate. + Devi consentire le chiamate al tuo contatto per poterlo chiamare. + Impossibile inviare messaggi al membro del gruppo + Il contatto verrà eliminato - non è reversibile! + Conversazione eliminata! + Invia un messaggio per attivare le chiamate. + video + Puoi ancora vedere la conversazione con %1$s nell\'elenco delle chat. + connetti + Contatto eliminato! + Confermare l\'eliminazione del contatto? + Messaggio + Nessuna selezione + Seleziona + Selezionato %d + I messaggi verranno eliminati per tutti i membri. + I messaggi verranno contrassegnati come moderati per tutti i membri. + Eliminare %d messaggi dei membri? + I messaggi saranno contrassegnati per l\'eliminazione. Il/I destinatario/i sarà/saranno in grado di rivelare questi messaggi. + Database della chat esportato + Continua + Server di multimediali e file + Server dei messaggi + Proxy SOCKS + Alcuni file non sono stati esportati + Puoi migrare il database esportato. + Puoi salvare l\'archivio esportato. + Connessione TCP + Connettiti più velocemente ai tuoi amici + Prendi il controllo della tua rete + Elimina fino a 20 messaggi contemporaneamente. + Barra degli strumenti di chat accessibile + Archivia contatti per chattare più tardi. + Usa l\'app con una mano sola. + Stato della connessione e dei server. + Protegge il tuo indirizzo IP e le connessioni. + Salva e riconnetti + Ripristina tutti i suggerimenti + Cambia l\'elenco delle chat: + Puoi cambiarlo nelle impostazioni dell\'aspetto. + Riproduci dall\'elenco delle chat. + Aumenta la dimensione dei caratteri. + Aggiorna l\'app automaticamente + Invita + Sfoca per una privacy maggiore. + Una nuova esperienza di chat 🎉 + Nuove opzioni multimediali + Crea + Scarica nuove versioni da GitHub. + Nuovo messaggio + Link non valido + Controlla che il link SimpleX sia corretto. \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml index 7899c374b2..e4901dd875 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/iw/strings.xml @@ -18,7 +18,7 @@ כל ההודעות יימחקו – לא ניתן לבטל זאת! ההודעות יימחקו רק עבורך. אשר זהות נסתרת הוסף שרתים מוגדרים מראש - הוסף שרת… + הוסף שרת לגשת לשרתים דרך פרוקסי SOCKS בפורט %d\? הפרוקסי חייב לפעול לפני הפעלת אפשרות זו. הגדרות רשת מתקדמות מראה @@ -604,8 +604,6 @@ בהיר ודאו שלקובץ יש תחביר YAML תקין. ייצאו ערכת נושא כדי לקבל דוגמה למבנה תקין של קובץ ערכת נושא. ככל הנראה איש קשר זה מחק את החיבור איתך. - ייעשה שימוש במארחי Onion כאשר יהיו זמינים. - מארחי Onion יידרשו לחיבור. נחסם על ידי %s אין קוד גישה לאפליקציה ניתן לשלוח רק 10 סרטונים בו־זמנית @@ -674,7 +672,6 @@ יידרשו מארחי onion לחיבור. \nשימו לב: לא תוכלו להתחבר לשרתים ללא כתובת .onion. לא ייעשה שימוש במארחי Onion. - לא ייעשה שימוש במארחי Onion. שיחה שלא נענתה רק מכשירי לקוח מאחסנים פרופילי משתמש, אנשי קשר, קבוצות, והודעות שנשלחו עם <b>הצפנה מקצה־לקצה דו־שכבתית</b>. ללא הצפנה מקצה־לקצה @@ -898,12 +895,10 @@ שמור סיסמה ופתח את הצ׳אט שמור ארכיון בחירת אנשי קשר - שמור צבע קוד גישה להשמדה עצמית שניות אישור תפקיד - ביטול שמור ועדכן את פרופיל הקבוצה לשמור הודעת פתיחה\? %s (נוכחי) @@ -1078,7 +1073,6 @@ (כדי לשתף עם איש הקשר שלך) כדי להתחיל צ׳אט חדש השתמש עבור חיבורים חדשים - לעדכן הגדרות מארחי ‪.onion‬\?‬ כדי לאמת הצפנה מקצה־לקצה עם איש הקשר שלכם, יש להשוות (או לסרוק) את הקוד במכשירים שלכם. פרופילי צ׳אט לעדכן מצב בידוד תעבורה\? @@ -1093,7 +1087,7 @@ שימוש בצ׳אט עדכן שגיאת מסד נתונים לא ידועה: %s - פרופיל קבוצה עודכן + עידכן את פרופיל הקבוצה עדכן בטל הסתרת פרופיל צ׳אט בטל הסתרת פרופיל @@ -1716,4 +1710,223 @@ אתה כבר מצטרף לקבוצה <b>%1$s</b>. הסר משתתף הסתיים פסק הזמן הקצוב להתחברות למחשב השולחני + חיבור רשת יותר אמין. + מתי שהIP מוסתר + השתמש במסלול פרטי עם שרתים לא ידועים + אזהרה על אופן שליחת ההודעה + הראה סטטוס הודעה + מסלול פרטי להודעה 🚀 + סטטוס הודעה:%s + אנא וודא שהמכשיר והמחשב מחוברים לאותה רשת מקומית, ושהפיירוול של המחשב מאפשר את החיבור. +\nאנא תשתף כל בעיות אחרות עם המפתחים. + "אשר קבלת קבצים משרתים לא מוכרים" + שגיאת רשת - ההודעה פגה תוקף לאחר ריבוי ניסיונות שליחה + שגיאה:%1$s + כל החברים + הודעת המקור נשארת פרטית + רינגטון ל שיחה נכנסת + קובץ לא נמצא - ככל הנראה הקובץ נמחק או בוטל + שגיאת שרת קבצים:%1$s + הורדה + לא + ערכת נושא חדשה לצא\'ט + מרובע, עיגול, או כל דבר ביניהם + העבר ושמור הודעות + מתי שמתחבר שחיות קוליות ווידאו. + לא מצליח לשלוח הודעה + הודעות קוליות לא מאופשרות + שגיאת קובץ זמני + קבצים ומדיה לא מאופשרים + שגיאת קובץ + השתמש במסלול פרטי עם שרתים לא ידועים מתי שכתובת הIP לא מוגנת. + שלח הודעות ישירות מתי שכתובת הIP מוגנת ואתה או שרת היעד לא תומך במסלול פרטי. + כדי להגן על כתובת הIP שלך, מסלול פרטי משתמש בשרתי הSMP שלך כדי להעביר את ההודעות. + אל תשתמש במסלול פרטי + תמיד השתמש בנתיב פרטי + תאפשר בהגדרות + תאפשר הרשאות בשביל להתקשר + מצלמה ומיקרופון + פתח הגדרות + מצא את ההרשאה בהגדרות המכשיר ותאפשר אותה ידנית + רמקול + אוזניות + אוזניות + ערכת נושא לפרופיל + צבעי הצא\'ט + הראה רשימת צא\'טים בחלון חדש + מצב הקובץ + סטטוס הודעה + מצב הקובץ:%s + ריק + כהה + מצב צבעוני + שחור + בהיר + אפס צבע + ערכת נושא לאפליקציה + שלח תגובה + התקבל תגובה + טפט רקע + הסר תמונה + חזור + מלא + אפס ערכת נושא לאפליקציה + התאם + צהריים טובים + בוקר טוב! + הגדרות מתקדמות + הגדר ערכת נושא ברירת מחדל + אפס ערכת נושא למשתמש + החל ל + מצב כל הצבעים + חברי הקבוצה יכולים לשלוח קישורי SimpleX + עשה שהצאט\'ים שלך יראו אחרת! + הגדרות רשת + הקישור הזה שומש כבר במכשיר אחר, אנא צור קישור חדש במחשב. + חיבור קווי + סלולרי + נשמר + נשמר מ%s + הועבר + הועבר + מחובר לרשת + Wi-Fi אלחוטי + שגיאה בהעתקה + ערכת נושא כהה + ערכת נושא + צבעי מצב כהה + שגיאה בשרת היעד:%1$s + "אל תשלח הודעות ישירות אפילו אם שרת היעד לא תומך במסלול פרטי" + שיפור בשליחת הודעות + מצלמה + אפשר שליחת קישורי SimpleX + בעלים + מנהלים + מופעל עבור + שרתים לא ידועים! + בלי טור או VPN, כתובת הIP שלך תהיה חשופה למתווכי XFTP האלה: +\n%1$s + הועבר מ + נשמר + נשמר מ + מקבלי ההודעה לא יוכלו לראות מי שלח את ההודעה + העבר + מעביר הודעה… + קישורי SimpleX לא מאופשרים + מסלול פרטי + לא מוגן + תמיד + מתווכים לא ידועים + לעולם לא + כן + אפשר שינמוך + שלח הודעות ישירות מתי שאתה או שרת היעד לא תומך במסלול פרטי + מיקרופון + הרשאות שניתנו + כתובת IP מוגנת + האפליקציה תשאל כדי לאשר הורדות משרתי קבצים לא ידועים (למעט שרתי טור או מתי שפרוקסי SOCKS מופעל). + בלי טור או VPN, כתובת הIP שלך תהיה גלויה לשרתי קבצים. + קבצים + תמונות פרופיל + מסלול הודעה פרטית + קישורי SimpleX + אין חיבור לרשת + אחר + ערכת נושא בהירה + מערכת + שגיאה בהצגת התראה, צור קשר עם המפתחים + שגיאה בהתחברות לשרת %1$s, אנא נסה מאוחר יותר + אין עדיין חיבור ישיר, ההודעה תעובר ע\"י מנהל. + חבר לא פעיל + שרתי XFTP אחרים + הראה אחוזים + מושבת + יציבה + הותקן בהצלחה + התקן עדכון + פתח מיקום קובץ + אנא הפעל מחדש את האפילקציה. + הזכר מאוחר יותר + דלג על הגרסא הזאת + השבת + גדול פונט + פרופיל נוכחי + הודעות שנשלחו + פג תוקף + שגיאות בשליחה + גודל + קבצים שהועלו + יחול בצ\'אטים ישירים! + בלוטוס + טשטש מדיה + כבוי + בינוני + חזק + מושבת + לא פעיל + פרטים + שגיאה + שגיאה בהתחברות מחדש לשרת + שגיאה בהתחברות מחדש לשרתים + שגיאות + מקבל ההודעות + שרתי פרוקסי + התחבר מחדש לכל השרתים + להתחבר מחדש לשרת? + להתחבר מחדש לשרתים? + סטטיסטיקות + סך הכל + ניסיונות + הושלם + חיבורים + נמחק + שגיאות במחיקה + שגיאה באיפוס הסטטיסטיקה + אחר + מאובטח + שלח ישירות + נשלח דרך פרוקסי + קבצים שהורדו + שגיאות בהורדה + פתח הגדרות שרת + כתובת שרת + חריגה מהקיבולת - הנמען לא קיבל הודעות שנשלחו בעבר. + בדוק עבור עדכונים + מחובר + שרתים מחוברים + בודק עבור עדכונים חדשים + מתחבר + נוצר + הודעה הועברה + ההודעה תוכל להימסר מאוחר יותר אם החבר יהפוך לפעיל. + הודעות שהתקבלו + אפס את כל הסטטיסטיקות + גודל + קישורי SimpleX לא מאופשרים בקבוצה הזו. + סרוק/ הדבק קישור + אנא נסה מאוחר יותר + שרתי SMP אחרים + אפס + הועלה + סטטיסטיקה מפורטת + בטא + לאפס את כל הסטטיסטיקות? + שגיאות בפענוח + שגיאות אחרות + שגיאה בהעלאה + יורד עדכון לאפליקציה + מוריד עדכון לאפליקציה, אל תסגור את האפליקציה + כל הפרופילים + קבצים + אין מידע, נסה לרענן + מידע על השרתים + התקבל סה\"כ + התקבלו שגיאות + התחבר מחדש + שלח הודעות + נשלח בסה\"כ + שרת SMP + שרת XFTP + חלש + אנשי קשר בארכיון \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml index 83d59b3705..38df6a8b16 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ja/strings.xml @@ -21,7 +21,7 @@ QRコードでサーバを追加 別の端末に追加 プロフィールを追加 - サーバを追加… + サーバを追加 SOCKSプロキシ(ポート%d)経由で接続しますか?(※設定する前にプロキシ起動が必要※) 全チャットとメッセージが削除されます(※元に戻せません※)! 送信相手からの音声メッセージを許可する。 @@ -204,7 +204,6 @@ 接続済み リンク経由で繋がる。 接続エラー - 接続にオニオンのホストが必要となります。 接続待ち (紹介済み) 接続エラー (AUTH) 接続タイムアウト @@ -222,7 +221,6 @@ 追加情報アイコン オニオンのホストが利用可能時に使われます。 オニオンのホストが使われません。 - オニオンのホストが利用可能時に使われます。 画像を1回で最大10枚を送信できます。 <b>2層エンドツーエンド暗号化</b>で送信されたプロフィール、連絡先、グループ、メッセージは、クライント端末にしか保存されません。 グループ設定を変えられるのはグループのオーナーだけです。 @@ -298,7 +296,7 @@ ファイル保存でエラー発生 接続中 確認待ち - 中止 + キャンセル 使い捨ての招待リンク 音声メッセージを録音 ギャラリーから @@ -315,7 +313,6 @@ ICEサーバ (1行に1サーバ) ネットワークとサーバ ネットワーク設定 - オニオンのホストが使われません。 アドレスを削除 保存せずに閉じる 表示の名前には空白が使用できません。 @@ -735,7 +732,6 @@ あなたのICEサーバ 直接にインタネットに繋がりますか? SOCKSプロキシを使いますか? - .onionのホスト設定を更新しますか? .onionホストを使う 利用可能時に トランスポート隔離 @@ -792,7 +788,6 @@ 保存 ネットワーク設定を更新しますか? 設定を更新すると、全サーバにクライントの再接続が行われます。 - 色を保存 あなたが次を許可しています: オン グループ設定を行う @@ -884,7 +879,6 @@ アプリ起動時にパスフレーズを入力しなければなりません。端末に保存されてません。 データベースのパスフレーズ変更が完了してません。 リンク、またはQRコードを共有できます。誰でもグループに参加できます。後で削除しても、グループのメンバーがそのままのこります。 - 元に戻す 更新 1日に設定 一定時間が経ったら送信されたメッセージが削除されます。 @@ -1035,7 +1029,7 @@ メンバーのメッセージを削除しますか? テーマのインポート ポート - テーマカラー + インターフェースカラー 共有を停止 友人を招待する 後からでも作成できます @@ -1085,7 +1079,7 @@ Bluetoothのサポートおよびその他の機能も改善されました。 中継サーバーは必要な場合にのみ使用されます。 別の当事者があなたの IP アドレスを監視できます。 アドレス共有の停止? - 送信 + 送信メッセージ 受信したメッセージ 以前のメッセージとハッシュ値が異なります。 次のメッセージの ID が正しくありません (前のメッセージより小さいか等しい)。 @@ -1595,8 +1589,8 @@ ステータス不明 プライベートノート メッセージ配信の改善 - メッセージ、ファイル、通話は、前方秘匿性、否認防止および及び侵入復元性を備えた <b>エンドツーエンドの暗号化</b>によって保護されます。 - メッセージ、ファイル、通話は、前方秘匿性、否認防止および及び侵入復元性を備えた <b>耐量子E2E暗号化</b>によって保護されます。 + メッセージ、ファイル、通話は、前方秘匿性、否認防止および侵入復元性を備えた <b>エンドツーエンドの暗号化</b>によって保護されます。 + メッセージ、ファイル、通話は、前方秘匿性、否認防止および侵入復元性を備えた <b>耐量子E2E暗号化</b>によって保護されます。 このチャットはエンドツーエンド暗号化により保護されています。 このチャットは耐量子エンドツーエンド暗号化により保護されています。 プライベートノート @@ -1769,4 +1763,78 @@ プロフィール画像 正方形、円形またはその中間 プロフィール画像をシェイプ + 宛先サーバエラー: %1$s + エラー: %1$s + プライベートルーティング + 不明なリレー + 未保護 + ダウングレードを許可 + 常時 + 常時プライベートルーティングを使用 + プライベートメッセージルーティング + メッセージステータスを表示 + システム + ブラック + 色設定 + ダーク + ダークモードカラー + ライト + アプリのテーマ + ダークモード + ライトモード + 適用先 + 追加のアクセント2 + 高度な設定 + こんにちは! + おはよう! + 壁紙のアクセント + 壁紙の背景 + チャットカラー + チャットテーマ + ファイルエラー + ファイルサーバーエラー: %1$s + ファイルステータス + ファイルステータス: %s + ペルシャ語UI + ファイルが見つかりません - 削除されたかキャンセルされた可能性があります。 + チャットの見た目を変更できます! + ファイル + ネットワークエラー - 複数回送信が試行されましたが、メッセージが期限切れになりました + 無効 + メッセージステータス + ファイルの安全な受け取り + プロフィールテーマ + メッセージステータス:%s + 色のリセット + 受信した返信元メッセージ + 送信した返信元メッセージ + 画像を削除 + 拡大縮小 + フィットさせる + 画像全体 + アプリのテーマをリセット + ユーザーテーマをリセット + デフォルトのテーマを設定 + プライベートメッセージルーティング 🚀 + 不明なサーバーからのファイルを確認できます + バッテリーの使用量が減少しました + メッセージ配信の改善 + 不明なサーバーです! + プライベートルーティングを使用しない + IPアドレス保護 + コピーエラー + 新しいチャットテーマ + 連絡先が選択したメッセージリレーからあなたのIPアドレスを保護します。 +\n*ネットワークとサーバー*設定から有効にして下さい。 + いいえ + はい + メッセージルーティングモード + フォントサイズ + ベータ + アップデートを確認 + アップデートを確認 + 完了 + SMPサーバーの構成 + 接続中 + XFTPサーバーの構成 \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml index af3972035f..ffd62b4755 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ko/strings.xml @@ -43,7 +43,7 @@ 연결 요청이 전송되었습니다! 링크로 연결 프리셋 서버 추가 - 서버 추가… + 서버 추가 채팅 콘솔 서버 주소를 확인 후 다시 시도해 주세요. ICE 서버 설정 @@ -635,7 +635,6 @@ 이 설정은 현재 내 프로필의 메시지에 적용되어요. 멤버 역할이 \"%s\"(으)로 변경되고, 회원은 새로운 초대를 받게 될 거예요. - 되돌리기 이 채팅에서는 메시지 영구 삭제가 허용되지 않았어요. 나가기 큰 파일! @@ -667,9 +666,6 @@ 사용 가능한 경우 Onion 호스트가 사용될 거예요. Onion 호스트가 사용되지 않을 거예요. 전송 격리 - Onion 호스트가 사용되지 않을 거예요. - 사용 가능한 경우 Onion 호스트가 사용될 거예요. - 연결하려면 Onion 호스트가 필요해요. 차세대 사생활 보호 메시징 새 비밀번호… TCP 연결 유지 활성화 @@ -757,7 +753,6 @@ 저장하고 대화 상대에게 알리기 지우기 아카이브 저장하기 - 색상 저장하기 암호 저장소에 비밀번호 저장하기 데이터베이스 백업 복원하기 데이터베이스 백업을 복원한 후 이전 비밀번호를 입력해 주세요. 이 작업은 되돌릴 수 없어요. @@ -939,4 +934,33 @@ 이미지 기다리는 중 음성 메시지 %1$d개의 메시지의 해독에 실패했습니다. + 추가 강조 색상 2 + 백업 복원 후 암호화 수정. + 채팅 더 빠르게 찾기 + 그룹 링크 + 읽지 않은 채팅과 즐겨찾기 채팅 필터링. + 고급 설정 + 연결 유지하기 + 소유자 + %1$d개의 메시지가 %2$s에 의해 삭제되었습니다 + 6개의 새로운 인터페이스 언어 + 연락처 추가 + 관리자 + 관리자는 모든 멤버를 위해 특정 멤버를 차단할 수 있습니다. + 메시지 전달 확인서! + 우리가 놓친 두 번째 체크! ✅ + 주소 변경 중지 + - 최대 5분의 음성 메시지. +\n- 사용자 정의 소멸 시간. +\n- 편집 기록. + 사용자 여러분께 감사드립니다 – Weblate를 통해 기여하세요! + 주소 변경이 중지됩니다. 이전 수신 주소가 사용됩니다. + 일본어 및 포르투갈어 UI + 주소 변경을 중지하시겠습니까? + 모든 멤버 + 위해 활성화됨 + 새로운 기능 + 더 보기 + 보안 평가 + SimpleX Chat 보안은 Trail of Bits에 의해 감사되었습니다. \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml index 43237fa2a9..6139eb5133 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/lt/strings.xml @@ -6,7 +6,7 @@ 1 diena a + b Apie SimpleX - Pridėti serverį… + Pridėti serverį Pridėti serverius nuskanuojant QR kodus. Išvaizda Programėlės versija @@ -130,7 +130,6 @@ grupės profilis atnaujintas Grupė Ištrinti pokalbio profilį\? - Sugrąžinti Įrašyti įjungta Ištrinti po @@ -189,7 +188,6 @@ Daugiau neberodyti Tamsus Atstatyti spalvas - Įrašyti spalvą Grupės parinktys Ištrinti visiems Tiesioginės žinutės @@ -1374,7 +1372,6 @@ (nuskanuokite ar įklijuokite iš iškarpinės) Priėmėte prisijungimą Jūs pakvietėte kontaktą - Onion serveriai bus reikalingi ryšiui. Onion serveriai bus naudojami, kai tik bus. Išeiti neišsaugant Jūs kontroliuojate savo pokalbį! @@ -1566,7 +1563,6 @@ Jūsų SimpleX adresas Nuskanuoti serverio QR kodą Reikalingi - Onion serveriai bus naudojami, kai tik bus. Onion serveriai nebus naudojami. Savaiminis susinaikinimas Senas duomenų bazės archyvas @@ -1629,8 +1625,6 @@ Moderuoti nori prisijungti prie jūsų! nuorodos peržiūros nuotrauka - Onion serveriai nebus naudojami. - Atnaujinti .onion serverių nustatymą? Kai programėlė yra paleista Užrakto režimas Galite paleisti pokalbius per programėlės nustatymus/ duomenų bazę arba paleisdami programėlę iš naujo. @@ -1764,4 +1758,26 @@ SimpleX nuorodos neleidžiamos Kai jungiami garso ir vaizdo skambučiai. Bus įjungta tiesioginiuose pokalbiuose! + Juodo režimo spalvos + Patvirtinkite failus iš nežinomų serverių. + Paskirties serverio klaida: %1$s + Klaida: %1$s + Tamsu + Spalvos režimas + Programėlės tema + Pridėtinis akcentas 2 + Pažangūs nustatymai + Visi spalvų režimai + Visada + Pokalbio spalvos + Juodas režimas + Juoda + Talpa viršyta – gavėjas negavo anksčiau išsiųstų žinučių. + Pokalbio tema + NESIŲSTI žinučių tiesiogiai, net jei jūsų ar paskirties serveris nepalaiko privataus maršruto. + Visada naudoti privatų maršrutą. + NENAUDOTI privataus maršruto. + Taip + Kopijavimo klaida + Pritaikyti prie \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ml/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ml/strings.xml index 92bd3e381a..d97cf4b7ad 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ml/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ml/strings.xml @@ -109,7 +109,7 @@ തിരികെ നിശബ്ദമാക്കുക സഹായം - സെർവർ ചേർക്കുക… + സെർവർ ചേർക്കുക മറ്റൊരു ഉപകരണത്തിലേക്ക് ചേർക്കുക സ്വയമേവ സ്വീകരിക്കുക സ്ഥിരീകരണത്തിനായി കാത്തിരിക്കുന്നു… @@ -320,10 +320,8 @@ സ്വീകരിക്കുന്ന വിലാസം മാറുക സെർവറുകൾ സംരക്ഷിക്കുക - പഴയപടിയാക്കുക സംവിധാനം സംവിധാനം - നിറം സംരക്ഷിക്കുക ശീർഷകം രണ്ടാംതരമായ അതെ diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml index ab10a01a66..97b6586844 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/nl/strings.xml @@ -39,7 +39,7 @@ Accepteer incognito Vooraf ingestelde servers toevoegen Profiel toevoegen - Server toevoegen… + Server toevoegen Toevoegen aan een ander apparaat Beheerders kunnen de uitnodiging links naar groepen aanmaken. Servers toevoegen door QR-codes te scannen. @@ -64,7 +64,7 @@ Sta toe dat uw contacten spraak berichten verzenden. Al uw contacten blijven verbonden. Spraak berichten toestaan\? - <b>Goed voor de batterij</b>. Achtergrondservice controleert berichten elke 10 minuten. Mogelijk mist u oproepen of dringende berichten. + <b>Goed voor de batterij</b>. App controleert berichten elke 10 minuten. U kunt oproepen of urgente berichten missen. Onjuiste bericht hash Onjuiste bericht-ID Oproep al beëindigd! @@ -101,7 +101,7 @@ Zowel jij als je contact kunnen verdwijnende berichten sturen. Zowel jij als je contact kunnen spraak berichten verzenden. <b>Let op</b>: u kunt het wachtwoord NIET herstellen of wijzigen als u het kwijt raakt. - <b>Gebruikt meer batterij</b>! Achtergrondservice wordt altijd uitgevoerd - meldingen worden weergegeven zodra berichten beschikbaar zijn. + <b>Gebruikt meer batterij</b>! App draait altijd op de achtergrond – meldingen worden direct weergegeven. link voorbeeld annuleren oproep beëindigd %1$s Kan de database niet initialiseren @@ -425,7 +425,7 @@ Hoe te gebruiken Markdown hulp Markdown in berichten - Netwerk instellingen + Geavanceerde instellingen Markdown gebruiken cursief Hoe het werkt @@ -513,7 +513,7 @@ Zorg ervoor dat WebRTC ICE server adressen de juiste indeling hebben, regel gescheiden zijn en niet gedupliceerd zijn. Als u bevestigt, kunnen de berichten servers en uw provider uw IP-adres zien en met welke servers u verbinding maakt. Nee - Immuun voor spam en misbruik + Immuun voor spam Maak een privéverbinding Het onomkeerbaar verwijderen van berichten is verboden in dit gesprek. Nieuw in %s @@ -546,7 +546,6 @@ Eenmalige uitnodiging link Plakken Vooraf ingesteld server adres - Onion hosts worden niet gebruikt. Privé meldingen Plak de link die je hebt ontvangen Periodiek @@ -573,7 +572,6 @@ Wachtwoord is nodig uit Eenmalige uitnodiging link - Onion hosts zijn vereist voor verbinding. Alleen groep eigenaren kunnen groep voorkeuren wijzigen. (alleen opgeslagen door groepsleden) Vooraf ingestelde server @@ -584,10 +582,9 @@ OK Onion hosts zijn vereist voor verbinding. Onion hosts worden gebruikt indien beschikbaar. - Onion hosts worden gebruikt indien beschikbaar. Onion hosts worden niet gebruikt. - Open-source protocol en code. Iedereen kan de servers draaien. - Mensen kunnen alleen verbinding met u maken via de links die u deelt. + Iedereen kan servers hosten. + Jij bepaalt wie er verbinding mag maken. Alleen uw contact kan berichten onherroepelijk verwijderen (u kunt ze markeren voor verwijdering). (24 uur) Alleen jij kunt spraak berichten verzenden. Alleen uw contact kan spraak berichten verzenden. @@ -649,7 +646,7 @@ verstuurd ongeoorloofd verzenden Ongelezen - Tik om een nieuw gesprek te starten + Tik hier om een nieuw gesprek te starten Deze tekst is beschikbaar in instellingen Welkom! je bent uitgenodigd voor de groep @@ -722,7 +719,8 @@ U kunt markdown gebruiken voor opmaak in berichten: geweigerde oproep geheim - De volgende generatie privéberichten + De volgende generatie +\nprivéberichten wachten op antwoord… Om de privacy te beschermen, heeft SimpleX in plaats van gebruikers-ID\'s die door alle andere platforms worden gebruikt, ID\'s voor berichten wachtrijen, afzonderlijk voor elk van uw contacten. Gebruik chat @@ -789,8 +787,8 @@ Onbekende fout Verkeerd wachtwoord! U bent uitgenodigd voor de groep. Word lid om in contact te komen met de groepsleden. - Tik om lid te worden - Tik om incognito lid te worden + Tik hier om lid te worden + Tik hier om incognito lid te worden Je bent lid geworden van deze groep. Verbinding maken met uitnodigend lid. Je hebt een groep uitnodiging verzonden jij bent vertrokken @@ -808,13 +806,11 @@ Resetten naar standaardwaarden Ontvangst adres wijzigen Protocol timeout - Terugdraaien Opslaan sec Wanneer je een incognito profiel met iemand deelt, wordt dit profiel gebruikt voor de groepen waarvoor ze je uitnodigen. Thema Kleuren resetten - Kleur opslaan Systeem ja gekregen, verboden @@ -860,11 +856,11 @@ Transportisolatiemodus updaten\? bevestiging ontvangen… Wachten op bevestiging… - Het eerste platform zonder gebruikers-ID\'s, privé door ontwerp. + Geen gebruikers-ID\'s. Verbied het sturen van directe berichten naar leden. Verbieden het verzenden van spraak berichten. De beveiliging van SimpleX Chat is gecontroleerd door Trail of Bits. - Met optioneel welkomst bericht. + Met optioneel welkom bericht. Spraak berichten Uw contacten kunnen volledige verwijdering van berichten toestaan. U moet elke keer dat de app start het wachtwoord invoeren, deze wordt niet op het apparaat opgeslagen. @@ -905,7 +901,6 @@ U kunt <font color="#0088ff">verbinding maken met SimpleX Chat ontwikkelaars om vragen te stellen en updates te ontvangen</font>. Tenzij uw contact de verbinding heeft verwijderd of deze link al is gebruikt, kan het een bug zijn. Meld het alstublieft. \nOm verbinding te maken, vraagt u uw contact om een andere verbinding link te maken en te controleren of u een stabiele netwerkverbinding heeft. - .onion hosts-instelling updaten\? SimpleX Chat servers gebruiken\? Spraak berichten zijn verboden in deze groep. Welkom %1$s! @@ -962,12 +957,12 @@ Chinese en Spaanse interface Voer wachtwoord in bij zoeken Fout bij opslaan gebruikers wachtwoord - Welkomst bericht toevoegen + Welkom bericht toevoegen Niet meer weergeven Groep moderatie Fout bij updaten van gebruikers privacy Verder verminderd batterij verbruik - Groep welkomst bericht + Groep welkom bericht Verborgen chat profielen Profiel verbergen Verbergen @@ -985,16 +980,16 @@ Servers opslaan\? Bewaar profiel wachtwoord Stel het getoonde bericht in voor nieuwe leden! - Welkomst bericht opslaan\? + Welkom bericht opslaan? Ondersteuning voor bluetooth en andere verbeteringen. - Tik om profiel te activeren. + Tik hier om profiel te activeren. Dank aan de gebruikers – draag bij via Weblate! U kunt een gebruikers profiel verbergen of dempen - houd het vast voor het menu. zichtbaar maken Dempen opheffen - Welkomst bericht + Welkom bericht Om uw verborgen profiel te onthullen, voert u een volledig wachtwoord in een zoekveld in op de pagina Uw chat profielen. - Welkomst bericht + Welkom bericht U ontvangt nog steeds oproepen en meldingen van gedempte profielen wanneer deze actief zijn. Database downgraden Ongeldige migratie bevestiging @@ -1136,13 +1131,13 @@ Laten we praten in SimpleX Chat Sla instellingen voor automatisch accepteren op Instellingen opslaan\? - Voer welkomst bericht in... (optioneel) + Voer welkom bericht in... (optioneel) Maak geen adres aan Hoi! \nMaak verbinding met mij via SimpleX Chat: %s U kan het later maken Adres delen - Welkomst bericht invoeren… + Welkom bericht invoeren… Thema importeren SimpleX Extra accent @@ -1165,7 +1160,7 @@ Zorg ervoor dat het bestand de juiste YAML-syntaxis heeft. Exporteer het thema om een voorbeeld te hebben van de themabestandsstructuur. Database openen… Lees meer in de <font color="#0088ff">Gebruikershandleiding</font>. - THEMA KLEUREN + INTERFACE KLEUREN U kunt uw adres delen als een link of QR-code - iedereen kan verbinding met u maken. Alle app-gegevens worden verwijderd. Er wordt een leeg chatprofiel met de opgegeven naam gemaakt en de app wordt zoals gewoonlijk geopend. @@ -1182,18 +1177,18 @@ Als u uw zelfvernietigings wachtwoord invoert tijdens het openen van de app: Als u deze toegangscode invoert bij het openen van de app, worden alle app-gegevens onomkeerbaar verwijderd! Toegangscode instellen - Berichtreacties verbieden. - Alleen jij kunt berichtreacties toevoegen. + Bericht reacties verbieden. + Alleen jij kunt bericht reacties toevoegen. Reacties op berichten zijn verboden in deze groep. Berichten reacties verbieden. - Sta berichtreacties alleen toe als uw contact dit toestaat. - Sta uw contactpersonen toe om berichtreacties toe te voegen. - Sta berichtreacties toe. - Groepsleden kunnen berichtreacties toevoegen. - Zowel u als uw contact kunnen berichtreacties toevoegen. + Sta bericht reacties alleen toe als uw contact dit toestaat. + Sta uw contactpersonen toe om bericht reacties toe te voegen. + Sta bericht reacties toe. + Groepsleden kunnen bericht reacties toevoegen. + Zowel u als uw contact kunnen bericht reacties toevoegen. Reacties op berichten Reacties op berichten zijn verboden in deze chat. - Alleen uw contact kan berichtreacties toevoegen. + Alleen uw contact kan bericht reacties toevoegen. dagen uren minuten @@ -1243,7 +1238,7 @@ \n- aangepaste tijd om te verdwijnen. \n- bewerkingsgeschiedenis. geen tekst - Er zijn enkele niet-fatale fouten opgetreden tijdens het importeren - mogelijk ziet u Chatconsole voor meer informatie. + Er zijn enkele niet-fatale fouten opgetreden tijdens het importeren: Afsluiten\? APP Herstarten @@ -1415,7 +1410,7 @@ %s, %s en %d leden %1$d berichten gemodereerd door %2$s Verbinding maken met jezelf? - Tik om verbinding te maken + Tik hier om verbinding te maken Juiste naam voor %s? %d berichten verwijderen? Verbinding maken met %1$s? @@ -1548,9 +1543,9 @@ Of scan de QR-code Ongeldige QR-code Contact toevoegen - Tik om te scannen + Tik hier om te scannen Bewaar - Tik om de link te plakken + Tik hier om de link te plakken Zoeken of plak een SimpleX link De chat is gestopt. Als u deze database al op een ander apparaat heeft gebruikt, moet u deze terugzetten voordat u met chatten begint. Begin chat? @@ -1629,7 +1624,7 @@ je hebt %s geblokkeerd je hebt %s gedeblokkeerd Bericht te groot - Welkomstbericht is te lang + Welkom bericht is te lang De databasemigratie wordt uitgevoerd. \nDit kan enkele minuten duren. Audio oproep @@ -1767,4 +1762,309 @@ Vorm profiel afbeeldingen Profiel afbeeldingen Vierkant, cirkel of iets daartussenin. + Capaciteit overschreden - ontvanger heeft eerder verzonden berichten niet ontvangen. + Fout met bestemmingsserver: %1$s + Fout: %1$s + Doorstuurserver: %1$s +\nBestemmingsserverfout: %2$s + Doorstuurserver: %1$s +\nFout: %2$s + Waarschuwing voor berichtbezorging + Netwerkproblemen - bericht is verlopen na vele pogingen om het te verzenden. + Serveradres is niet compatibel met netwerkinstellingen. + Serverversie is incompatibel met netwerkinstellingen. + Verkeerde sleutel of onbekende verbinding - hoogstwaarschijnlijk is deze verbinding verwijderd. + Altijd + Privéroutering + Onbekende servers + Nooit + Onbeschermd + Gebruik altijd privéroutering. + Gebruik privéroutering met onbekende servers. + Gebruik GEEN privéroutering. + Berichtrouteringsmodus + Gebruik privéroutering met onbekende servers wanneer het IP-adres niet beveiligd is. + Downgraden toestaan + Wanneer IP verborgen is + Ja + Nee + Stuur berichten rechtstreeks wanneer uw of de doelserver geen privéroutering ondersteunt. + Toon berichtstatus + Om uw IP-adres te beschermen, gebruikt privéroutering uw SMP-servers om berichten te bezorgen. + Stuur GEEN berichten rechtstreeks, zelfs als uw of de bestemmingsserver geen privéroutering ondersteunt. + Terugval op berichtroutering + PRIVÉBERICHT ROUTING + Stuur berichten rechtstreeks als het IP-adres beschermd is en uw of bestemmingsserver geen privéroutering ondersteunt. + Onbekende servers! + Zonder Tor of VPN is uw IP-adres zichtbaar voor deze XFTP-relays: +\n%1$s. + Zonder Tor of VPN is uw IP-adres zichtbaar voor bestandsservers. + BESTANDEN + Bescherm het IP-adres + De app vraagt om downloads van onbekende bestandsservers te bevestigen (behalve .onion of wanneer SOCKS-proxy is ingeschakeld). + Fout bij het initialiseren van WebView. Update uw systeem naar de nieuwe versie. Neem contact op met ontwikkelaars. +\nFout: %s + Achtergrond accent + Vullen + Passen + Goedemiddag! + Goedemorgen! + Verwijder afbeelding + Schaal + Alle kleurmodi + Toepassen op + Lichte modus + Laat uw chats er anders uitzien! + Nieuwe chatthema\'s + Routing van privéberichten🚀 + Bevestig bestanden van onbekende servers. + Verbeterde bezorging van berichten + Perzische gebruikersinterface + Veilig bestanden ontvangen + Met verminderd batterijgebruik. + App thema + Terugzetten naar app thema + Terugzetten naar gebruikersthema + Donkere modus + Geavanceerde instellingen + Bescherm uw IP-adres tegen de berichtenrelais die door uw contacten zijn gekozen. +\nSchakel dit in in *Netwerk en servers*-instellingen. + Herhalen + Chat kleuren + Profiel thema + Chat thema + Extra accent 2 + Zwart + Kleur mode + Donker + Kleuren in donkere modus + Licht + Antwoord ontvangen + Kleur opnieuw instellen + Antwoord verzonden + Systeem + Wallpaper achtergrond + Stel het standaard thema in + Toon chatlijst in nieuw venster + geen + Foutopsporing bezorging + Informatie over berichtenwachtrij + informatie over serverwachtrij: %1$s +\n +\nlaatst ontvangen bericht: %2$s + Bestand niet gevonden - hoogstwaarschijnlijk is het bestand verwijderd of geannuleerd. + Verkeerde sleutel of onbekend bestanddeeladres - hoogstwaarschijnlijk is het bestand verwijderd. + Bestandsserverfout: %1$s + Bestandsfout + Tijdelijke bestandsfout + Kopieerfout + Controleer of mobiel en desktop met hetzelfde lokale netwerk zijn verbonden en of de desktopfirewall de verbinding toestaat. +\nDeel eventuele andere problemen met de ontwikkelaars. + Deze link is gebruikt met een ander mobiel apparaat. Maak een nieuwe link op de desktop. + Bestandsstatus + Berichtstatus: %s + Bestandsstatus: %s + Berichtstatus + Kan bericht niet verzenden + Geselecteerde chat voorkeuren verbieden dit bericht. + Fout in privéroutering + Serverversie is niet compatibel met uw app: %1$s. + Bericht doorgestuurd + Nog geen directe verbinding, bericht wordt doorgestuurd door beheerder. + Overige XFTP servers + Link scannen/plakken + Zoom + Huidig profiel + Bestanden + Server informatie + Informatie weergeven voor + Fouten + Statistieken + Transportsessies + Actieve verbindingen + Details + Berichten ontvangen + Bericht ontvangst + Beginnend vanaf %s. +\nAlle gegevens zijn privé op uw apparaat. + Verbonden servers + in behandeling + Eerder verbonden servers + Proxied servers + Totaal + Server opnieuw verbinden? + Servers opnieuw verbinden? + Maak opnieuw verbinding met de server om de bezorging van berichten te forceren. Er wordt gebruik gemaakt van extra data. + U bent niet verbonden met deze servers. Privéroutering wordt gebruikt om berichten bij hen af te leveren. + Maak opnieuw verbinding met alle servers + Reset + Reset alle statistieken + Alle statistieken resetten? + Geüpload + Gedetailleerde statistieken + Ontvangen berichten + Verzonden berichten + Totaal verzonden + Proxied + Fouten ontvangen + opnieuw verbinden + Direct verzonden + Verzonden via proxy + SMP server + XFTP server + Erkend + Bevestigingsfouten + Verbindingen + Gemaakt + decoderingsfouten + duplicaten + verlopen + overige fouten + overig + Verzend fouten + Stukken gedownload + Stukken geüpload + Verwijderd + Verwijderingsfouten + Gedownloade bestanden + Beveiligd + Maat + Ingeschreven + Geüploade bestanden + Upload fouten + Downloadfouten + Server instellingen openen + Server adres + Alle profielen + pogingen + Stukken verwijderd + voltooid + Verbonden + Verbinden + Fout bij opnieuw verbinding maken met de server + Letter grootte + inactief + Lid inactief + Gedownload + Fout + Fout bij opnieuw verbinden van servers + Fout bij het resetten van statistieken + Het bericht kan later worden bezorgd als het lid actief wordt. + Berichten verzonden + Geen info, probeer opnieuw te laden + Overige SMP servers + Totaal ontvangen + Probeer het later. + Maak opnieuw verbinding met alle verbonden servers om de bezorging van berichten te forceren. Er wordt gebruik gemaakt van extra data. + Het serveradres is niet compatibel met de netwerkinstellingen: %1$s. + Serverstatistieken worden gereset - dit kan niet ongedaan worden gemaakt! + Beginnend vanaf %s. + Geconfigureerde SMP-servers + Geconfigureerde XFTP-servers + Percentage weergeven + Uitgeschakeld + App update downloaden. Sluit de app niet + %s downloaden (%s) + Succesvol geïnstalleerd + Installeer update + Open de bestandslocatie + Herstart de app. + Herinner later + Sla deze versie over + uitgeschakeld + App update is gedownload + Beta + Controleer op updates + Controleer op updates + Uitschakelen + Inschrijving fouten + Inschrijvingen genegeerd + Stabiel + Update beschikbaar: %s + Downloaden van update geannuleerd + Als u op de hoogte wilt worden gehouden van de nieuwe releases, schakelt u periodieke controle op stabiele of bètaversies in. + Vervaag media + Medium + uit + Soft + Krachtig + Het doelserveradres van %1$s is niet compatibel met de instellingen van de doorstuurserver %2$s. + De doelserverversie van %1$s is incompatibel met de doorstuurserver %2$s. + Fout bij verbinden met doorstuurserver %1$s. Probeer het later opnieuw. + Doorstuurserver %1$s kon geen verbinding maken met bestemmingsserver %2$s. Probeer het later opnieuw. + Het doorstuuradres is niet compatibel met de netwerkinstellingen: %1$s. + De doorstuurserverversie is niet compatibel met de netwerkinstellingen: %1$s. + Kan contact niet bellen + Oproepen toestaan? + bellen + Kan geen groepslid bellen + Bellen verboden! + Contact verwijderen bevestigen? + Gesprek verwijderd! + Verwijderen zonder melding + Gearchiveerde contacten + Contact is verwijderd. + Er wordt verbinding gemaakt met het contact. Even geduld of controleer het later! + Kan geen bericht sturen naar groepslid + %d berichten van leden verwijderen? + Bericht + Berichten worden gemarkeerd voor verwijdering. De ontvanger(s) kunnen deze berichten onthullen. + Niets geselecteerd + verbinden + Contact verwijderd! + Het contact wordt verwijderd. Dit kan niet ongedaan worden gemaakt! + Behoud het gesprek + bericht + Alleen conversatie verwijderen + open + Plak de link + Uitnodiging + Toegankelijke chatwerkbalk + Vraag uw contactpersoon om oproepen in te schakelen. + Geen gefilterde contacten + Selecteer + Geselecteerd %d + Stuur een bericht om oproepen mogelijk te maken. + zoekopdracht + U kunt berichten naar %1$s sturen vanuit gearchiveerde contacten. + Instellingen + De berichten worden voor alle leden verwijderd. + De berichten worden voor alle leden als gemodereerd gemarkeerd. + video + Je kunt nog steeds het gesprek met %1$s bekijken in de lijst met chats. + U moet uw contactpersoon toestemming geven om te bellen, zodat hij/zij je kan bellen. + Jouw contacten + Berichtservers + Media- en bestandsservers + Chat database geëxporteerd + Doorgaan + SOCKS proxy + U kunt de geëxporteerde database migreren. + U kunt het geëxporteerde archief opslaan. + Opslaan en opnieuw verbinden + TCP verbinding + Het beschermt uw IP-adres en verbindingen. + Toegankelijke chatwerkbalk + Gebruik de app met één hand. + Beheer uw netwerk + Maak sneller verbinding met je vrienden + Archiveer contacten om later te chatten. + Verbindings- en serverstatus. + Verwijder maximaal 20 berichten tegelijk. + Sommige bestanden zijn niet geëxporteerd + Alle hints resetten + Chatlijst wisselen: + U kunt dit wijzigen in de instellingen onder uiterlijk + Creëren + Vervagen voor betere privacy. + Afspelen via de gesprekken lijst. + Download nieuwe versies van GitHub. + Vergroot het lettertype. + App automatisch upgraden + Nieuwe chatervaring 🎉 + Nieuwe media-opties + Uitnodigen + Nieuw bericht + Ongeldige link + Controleer of de SimpleX-link correct is. \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml index d667126730..d0b0ca0d1c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pl/strings.xml @@ -181,7 +181,7 @@ jesteś obserwatorem Nie możesz wysyłać wiadomości! Połączony - Obecnie maksymalny obsługiwany rozmiar pliku to %1$s . + Obecnie maksymalny obsługiwany rozmiar pliku to %1$s. Usuń kontakt Usunąć kontakt\? Rozłączony @@ -290,7 +290,7 @@ Zostaniesz połączony do grupy, gdy urządzenie gospodarza grupy będzie online, proszę czekać lub sprawdzić później! Zostaniesz połączony, gdy urządzenie Twojego kontaktu będzie online, proszę czekać lub sprawdzić później! Dodaj gotowe serwery - Dodaj serwer… + Dodaj serwer Dodaj do innego urządzenia Konsola czatu Sprawdź adres serwera i spróbuj ponownie. @@ -344,18 +344,15 @@ Serwery ICE (po jednym na linię) Jeśli potwierdzisz, serwery wiadomości będą mogły zobaczyć Twój adres IP, a Twój dostawca - z jakimi serwerami się łączysz. Sieć i serwery - Ustawienia sieci + Zaawansowane ustawienia Nie Hosty onion będą wymagane do połączenia. \nUwaga: nie będziesz mógł połączyć się z serwerami bez adresu .onion. - Hosty onion będą wymagane do połączenia. Hosty onion będą używane, gdy będą dostępne. - Hosty onion będą używane, gdy będą dostępne. Hosty onion nie będą używane. Wymagane Zapisz Izolacja transportu - Zaktualizować ustawienie hostów .onion\? Zaktualizować tryb izolacji transportu\? Użyć bezpośredniego połączenia z Internetem\? Użyj hostów .onion @@ -411,11 +408,11 @@ Zdecentralizowane zakończona Jak korzystać z markdown - Odporność na spam i nadużycia + Odporność na spam kursywa nieodebrane połączenie - Otwarto źródłowy protokół i kod - każdy może uruchomić serwery. - Ludzie mogą się z Tobą połączyć tylko poprzez linki, które udostępniasz. + Każdy może hostować serwery. + Ty decydujesz, kto może się połączyć. Redefinicja prywatności otrzymano odpowiedź… otrzymano potwierdzenie… @@ -423,8 +420,9 @@ sekret uruchamianie… strajk - Pierwsza platforma bez żadnych identyfikatorów użytkowników – z założenia prywatna. - Następna generacja prywatnych wiadomości + Brak identyfikatorów użytkownika. + Następna generacja +\nprywatnych wiadomości oczekiwanie na odpowiedź… oczekiwanie na potwierdzenie… Możesz używać markdown do formatowania wiadomości: @@ -443,7 +441,7 @@ Użyj czatu Gdy aplikacja jest uruchomiona Kontrolujesz przez który serwer(y) <b>odbierać</b> wiadomości, Twoje kontakty - serwery, których używasz do wysyłania im wiadomości. - <b>Zużywa więcej baterii</b>! Usługa zawsze działa w tle - powiadomienia są wyświetlane, gdy tylko wiadomości są dostępne. + <b>Zużywa więcej baterii</b>! Aplikacja zawsze działa w tle - powiadomienia są wyświetlane natychmiastowo. Przychodzące połączenie audio Przychodzące połączenie wideo Wklej link, który otrzymałeś @@ -758,7 +756,6 @@ Profil i połączenia z serwerem Limit czasu protokołu Przywróć wartości domyślne - Przywrócić Zapisz sek Dotknij, aby aktywować profil. @@ -799,7 +796,6 @@ Zabroń wysyłania znikających wiadomości. otrzymane, zabronione Resetuj kolory - Zapisz kolor Ustaw 1 dzień System System @@ -918,7 +914,7 @@ Optymalizacja baterii jest aktywna, wyłącza usługi w tle i okresowe żądania nowych wiadomości. Możesz je ponownie włączyć za pośrednictwem ustawień. <b>Można je wyłączyć poprzez ustawienia</b> - powiadomienia nadal będą pokazywane podczas działania aplikacji. <b>Najlepsze dla baterii</b>. Będziesz otrzymywać powiadomienia tylko wtedy, gdy aplikacja jest uruchomiona (NIE w tle). - <b>Dobry dla baterii</b>. Usługa w tle sprawdza wiadomości co 10 minut. Możesz przegapić połączenia lub pilne wiadomości. + <b>Dobry dla baterii</b>. Aplikacja sprawdza wiadomości co 10 minut. Możesz przegapić połączenia lub pilne wiadomości. Zarówno Ty, jak i Twój kontakt możecie wysyłać wiadomości głosowe. Według profilu czatu (domyślnie) lub połączenia (BETA). Nie można zaprosić kontaktów! @@ -954,7 +950,6 @@ Wideo zaproponował %s: %2s Tylko właściciele grup mogą włączyć wiadomości głosowe. - Hosty onion nie będą używane. Tylko Twój kontakt może nieodwracalnie usunąć wiadomości (możesz oznaczyć je do usunięcia). (24 godziny) Hasło nie zostało znalezione w Keystore, wprowadź je ręcznie. Może się tak zdarzyć, gdy przywrócisz dane aplikacji za pomocą narzędzia do kopii zapasowych. Jeśli tak nie jest, skontaktuj się z programistami. Członkowie grupy mogą wysyłać znikające wiadomości. @@ -1154,7 +1149,7 @@ Kiedy ludzie proszą o połączenie, możesz je zaakceptować lub odrzucić. Nie stracisz kontaktów, jeśli później usuniesz swój adres. Dostosuj motyw - KOLORY MOTYWU + KOLORY INTERFEJSU Twoje kontakty pozostaną połączone. Dodaj adres do swojego profilu, aby Twoje kontakty mogły go udostępnić innym osobom. Aktualizacja profilu zostanie wysłana do Twoich kontaktów. Utwórz adres, aby ludzie mogli się z Tobą połączyć. @@ -1245,7 +1240,7 @@ \n- niestandardowy czas zniknięcia. \n- historia edycji. brak tekstu - Podczas importu wystąpiły niekrytyczne błędy - więcej szczegółów można znaleźć w konsoli czatu. + Podczas importu wystąpiły niekrytyczne błędy: Restart APLIKACJA Powiadomienia przestaną działać do momentu ponownego uruchomienia aplikacji. @@ -1769,4 +1764,309 @@ Kwadrat, okrąg lub cokolwiek pomiędzy. Źródło wiadomości pozostaje prywatne. Zostanie włączone w czatach bezpośrednich! + Zawsze używaj prywatnego trasowania. + Zezwól na obniżenie wersji + Zawsze + Przekroczono pojemność - odbiorca nie otrzymał wcześniej wysłanych wiadomości. + Błąd serwera docelowego: %1$s + Błąd: %1$s + Serwer przekazujący: %1$s +\nBłąd serwera docelowego: %2$s + Serwer przekazujący: %1$s +\nBłąd: %2$s + Ostrzeżenie dostarczenia wiadomości + Błąd sieciowy - wiadomość wygasła po wielu próbach wysłania jej. + Adres serwera jest niekompatybilny z ustawieniami sieciowymi. + Wersja serwera jest niekompatybilna z ustawieniami sieciowymi. + Zły klucz lub nieznane połączenie - najprawdopodobniej to połączenie jest usunięte. + Nigdy + Niezabezpieczony + NIE używaj prywatnego trasowania. + Tryb trasowania wiadomości + Tak + Nie + Gdy IP ukryty + Pokaż status wiadomości + TRASOWANIE PRYWATNYCH WIADOMOŚCI + NIE wysyłaj wiadomości bezpośrednio, nawet jeśli serwer docelowy nie obsługuje prywatnego trasowania. + Aby chronić Twój adres IP, prywatne trasowanie używa Twoich serwerów SMP, aby dostarczyć wiadomości. + Nieznane serwery + Używaj prywatnego trasowania z nieznanymi serwerami. + Rezerwowe trasowania wiadomości + Prywatne trasowanie + Wysyłaj wiadomości bezpośrednio, gdy adres IP jest chroniony i Twój lub docelowy serwer nie obsługuje prywatnego trasowania. + Wysyłaj wiadomości bezpośrednio, gdy Twój lub docelowy serwer nie obsługuje prywatnego trasowania. + Używaj prywatnego trasowania z nieznanymi serwerami, gdy adres IP nie jest chroniony. + Nieznane serwery! + Bez Tor lub VPN, Twój adres IP będzie widoczny dla tych przekaźników XFTP: +\n%1$s. + Chroń adres IP + Aplikacja będzie prosić o potwierdzenie pobierań z nieznanych serwerów plików (z wyjątkiem .onion lub gdy proxy SOCKS jest włączone). + Bez Tor lub VPN, Twój adres IP będzie widoczny do serwerów plików. + PLIKI + Motyw profilu + Pokaż listę czatów w nowym oknie + Kolory ciemnego trybu + Jasny + Otrzymano odpowiedź + Usuń obraz + Zresetuj kolory + Wypełnij + Dopasuj + Dzień dobry! + Dzień dobry! + Jasny tryb + Powtórz + Dodatkowy akcent 2 + Wszystkie tryby kolorów + Ciemny tryb + Ustaw domyślny motyw + Systemowy + Tło tapety + Zaawansowane ustawienia + Zastosuj dla + Skaluj + Czarny + Akcent tapety + Wyślij odpowiedź + Kolory czatu + Motyw czatu + Tryb koloru + Ciemny + Błąd inicjacji WebView. Zaktualizuj swój system do nowej wersji. Proszę skontaktować się z deweloperami. +\nBłąd: %s + nic + Nowy motywy czatu + Trasowanie prywatnych wiadomości🚀 + Bezpiecznie otrzymuj pliki + Ulepszona dostawa wiadomości + Perski interfejs użytkownika + Potwierdzaj pliki z nieznanych serwerów. + Dostarczenie debugowania + Zrób wygląd Twoich czatów inny! + Chroni Twój adres IP przed przekaźnikami wiadomości wybranych przez Twoje kontakty. +\nWłącz w ustawianiach *Sieć i serwery* . + Informacje kolejki wiadomości + Informacje kolejki serwera: %1$s +\n +\nostatnia otrzymana wiadomość: %2$s + Ze zredukowanym zużyciem baterii. + Motyw aplikacji + Zresetuj do motywu aplikacji + Zresetuj do motywu użytkownika + Nie odnaleziono pliku - najprawdopodobniej plik został usunięty lub anulowany. + Status pliku + Status wiadomości + Status pliku: %s + Status wiadomości: %s + Kopiuj błąd + Ten link dostał użyty z innym urządzeniem mobilnym, proszę stworzyć nowy link na komputerze. + Błąd pliku + Błąd serwera plików: %1$s + Sprawdź, czy telefon i komputer są podłączone do tej samej sieci lokalnej i czy zapora sieciowa komputera umożliwia połączenie. +\nProszę podzielić się innymi problemami z deweloperami. + Tymczasowy błąd pliku + Zły klucz lub nieznany adres fragmentu pliku - najprawdopodobniej plik został usunięty. + Nie można wysłać wiadomości + Wybrane preferencje czatu zabraniają tej wiadomości. + Błąd połączenia z serwerem przekierowania %1$s. Spróbuj ponownie później. + Adres serwera docelowego %1$s jest niekompatybilny z ustawieniami serwera przekazującego %2$s. + Serwer przekazujący %1$s nie mógł połączyć się z serwerem docelowym %2$s. Spróbuj ponownie później. + Wersja serwera przekierowującego jest niekompatybilna z ustawieniami sieciowymi: %1$s. + Wersja serwera docelowego %1$s jest niekompatybilna z serwerem przekierowującym %2$s. + Członek nieaktywny + Wiadomość przekazana + Wiadomość może zostać dostarczona później jeśli członek stanie się aktywny. + Beta + Sprawdź aktualizacje + Wyłączony + Pobierz %s (%s) + Aktualizacja aplikacji jest pobrana + Sprawdź aktualizacje + Pobieranie aktualizacji aplikacji, nie zamykaj aplikacji + Zainstalowano pomyślnie + Zainstaluj aktualizacje + Wyłącz + wyłączony + nieaktywny + Rozmiar czcionki + Połączony + Bieżący profil + Otrzymane wiadomości + Odebranie wiadomości + Błąd resetowania statystyk + duplikaty + Zakończono + Połączenia + Utworzono + Błędy usuwania + Fragmenty pobrane + Fragmenty przesłane + Pobrane pliki + Skonfigurowane serwery SMP + Potwierdzono + Błędy potwierdzenia + Usunięto + Aktywne połączenia + Wszystkie profile + Skonfigurowane serwery XFTP + błąd odszyfrowywania + Szczegółowe statystyki + Szczegóły + Błędy pobierania + Adres serwera przekierowującego jest niekompatybilny z ustawieniami sieciowymi: %1$s. + Pliki + Łączenie + Błędy + Połączone serwery + Błąd + Błąd ponownego łączenia z serwerem + Błąd ponownego łączenia serwerów + Pobrane + próby + wygasły + Fragmenty usunięte + Brak bezpośredniego połączenia, wiadomość została przekazana przez administratora. + Inne serwery SMP + Inne serwery XFTP + Pokaż procent + Stabilny + Aktualizacja dostępna: %s + Otwórz lokalizację pliku + Proszę zrestartować aplikację. + Przypomnij później + Pomiń tę wersję + Pobieranie aktualizacji anulowane + Aby otrzymywać powiadomienia o nowych wersjach, włącz okresowe sprawdzanie wersji Stabilnych lub Beta. + Przybliż + Brak informacji, spróbuj przeładować + Informacje o serwerach + Wyświetlanie informacji dla + Statystyki + Sesje transportowe + Zaczynanie od %s. +\nWszystkie dane są prywatne na Twoim urządzeniu. + Połącz ponownie wszystkie serwery + Połączyć ponownie serwer? + Nie jesteś połączony z tymi serwerami. Prywatne trasowanie jest używane do dostarczania do nich wiadomości. + Otrzymane wiadomości + Resetuj + Resetuj wszystkie statystyki + Wysłane wiadomości + Statystyki serwerów zostaną zresetowane - nie można tego cofnąć! + Przesłane + inne + Trasowane przez proxy + Otrzymano łącznie + inne błędy + Zabezpieczone + Zasubskrybowano + Przesłane pliki + Otwórz ustawienia serwera + Adres serwera + Wysłane wiadomości + Ponownie połącz ze wszystkimi połączonymi serwerami w celu wymuszenia dostarczenia wiadomości. Wykorzystuje to dodatkowy ruch. + Zresetować wszystkie statystyki? + Błędy subskrypcji + Subskrypcje zignorowane + Wysłano łącznie + Adres serwera jest niekompatybilny z ustawieniami sieci: %1$s. + Proszę spróbować później. + Błąd prywatnego trasowania + Wersja serwera jest niekompatybilna z aplikacją: %1$s. + Skanuj / Wklej link + Łącznie + Oczekujące + Wcześniej połączone serwery + Serwery trasowane przez proxy + Połączyć ponownie serwery? + Ponownie połącz z serwerem w celu wymuszenia dostarczenia wiadomości. Wykorzystuje to dodatkowy ruch. + Wysłano przez proxy + Serwer XFTP + Serwer SMP + Błędy otrzymania + Połącz ponownie + Wysłano bezpośrednio + Zaczynanie od %s. + Wyślij błędy + Rozmiar + Błędy przesłania + Zwiększ rozmiar czcionki. + wiadomość + Wiadomość + otwórz + szukaj + Zaznaczono %d + wideo + Brak filtrowanych kontaktów + Zaproś + Pobieraj nowe wersje z GitHub. + Nowe opcje mediów + Twoje kontakty + Nowa wiadomość + Aktualizuj aplikację automatycznie + Rozmycie dla lepszej prywatności. + Możesz to zmienić w ustawieniach wyglądu. + Zaproś + Nie można wysłać wiadomości do członka grupy + Wyślij wiadomość aby włączyć połączenia. + Nowe możliwości czatu 🎉 + Korzystaj z aplikacji jedną ręką. + Odtwórz z listy czatów. + Zarchiwizowane kontakty + zadzwoń + połącz + Stan połączenia i serwerów. + Utwórz + Nieprawidłowy link + Zapisz i połącz ponownie + Ustawienia + Połączenie TCP + Nic nie jest zaznaczone + Sprawdź czy link SimpleX jest poprawny. + Przełącz listę czatów: + Archiwizuj kontakty aby porozmawiać później. + Chroni Twój adres IP i połączenia. + Osiągalny pasek narzędzi czatu + Silne + Rozmycie mediów + Średni + Wyłącz + Łagodny + Zachowaj rozmowę + Usuń tylko rozmowę + Możesz wysyłać wiadomości do %1$s ze zarchiwizowanych kontaktów. + Nadal możesz przeglądać rozmowę z %1$s na liście czatów. + Potwierdzić usunięcie kontaktu? + Kontakt usunięty! + Kontakt zostanie usunięty – nie można tego cofnąć! + Rozmowa usunięta! + Usuń bez powiadomienia + Wklej link + Zezwolić na połączenia? + Nie można zadzwonić do kontaktu + Łączenie z kontaktem, poczekaj lub sprawdź później! + Kontakt jest usunięty. + Aby móc dzwonić, musisz zezwolić kontaktowi na połączenia. + Połączenia zakazane! + Nie można zadzwonić do członka grupy + Poproś kontakt o włącznie połączeń. + Usunąć %d wiadomości członków? + Wiadomości zostaną oznaczone do usunięcia. Odbiorca(y) będą mogli ujawnić te wiadomości. + Zaznacz + Wiadomości zostaną usunięte dla wszystkich członków. + Wiadomości zostaną oznaczone jako moderowane dla wszystkich członków. + Osiągalny pasek narzędzi czatu + Wyeksportowano bazę danych czatu + Kontynuuj + Serwery mediów i plików + Serwery wiadomości + Proxy SOCKS + Niektóre plik(i) nie zostały wyeksportowane + Możesz zmigrować wyeksportowaną bazy danych. + Możesz zapisać wyeksportowane archiwum. + Zresetuj wszystkie wskazówki + Szybciej łącz się ze znajomymi. + Kontroluj swoją sieć + Usuń do 20 wiadomości na raz. \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml index cc0bd49454..2fa9f85599 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt-rBR/strings.xml @@ -110,7 +110,7 @@ Conectar via link/QR code Todas as mensagens serão excluídas - isso não pode ser desfeito! As mensagens serão excluídas APENAS para você. Adicionar servidores pré-definidos - Adicionar servidor… + Adicionar servidor Crie seu perfil Ícone de contexto Contato e todas as mensagens serão excluídas - isso não pode ser desfeito! @@ -482,10 +482,8 @@ Arquivo de banco de dados antigo Convidar membros Nenhum contato selecionado - Reverter Salvar Redefinir cores - Salvar cor interface italiana Notificações periódicas Câmera @@ -648,7 +646,6 @@ Onion hosts não serão usados. Os hosts Onion serão necessários para a conexão. \nAtenção: você não será capaz de se conectar aos servidores sem um endereço .onion - Os hosts Onion serão necessários para a conexão. Versão principal: v%s repositório do GitHub.]]> Pode ser mudado mais tarde via configurações. @@ -762,9 +759,7 @@ Proteja seus perfis de bate-papo com uma senha! Este texto está disponível nas configurações Escanear código - Hosts Onion não serão usados. Os hosts Onion serão usados quando disponíveis. - Os hosts Onion serão usados quando disponíveis. Seu perfil atual Privacidade redefinida Notificações privadas @@ -964,7 +959,6 @@ Compartilhar mensagem… Bem-vindo(a) %1$s! você está convidado para o grupo - Atualizar configuração de hosts .onion\? Usar bate-papo Mensagens de voz são proibidas neste chat. Vídeo @@ -1638,4 +1632,31 @@ Criar novo perfil no aplicativo de desktop. 💻 Criptografar arquivos armazenados & arquivos de mídia Novo aplicativo de desktop! + Arquivando banco de dados + Cancelar migração + Por Favor, note que: usando o mesmo banco de dados em dois dispositivos vai quebrar a descriptografia das mensagens das suas conexões, como proteção de segurança.]]> + Aplicar + Tema do aplicativo + Preto + Bluetooth + Arquivar e enviar + Aplicar para + Administradores podem bloquear um membro para todos. + Migração de dados do aplicativo + Todos os seus contatos,conversas e arquivos irão ser criptografados seguramente e enviados em partes para relays de XFTP configurados. + Aviso: o arquivo irá ser deletado.]]> + Rede móvel + Sempre + Sempre usar roteamento privado. + Câmera + Câmera e microfone + Permitir o envio de links do SimpleX. + Todos os membros + Configurações avançadas + Verificar atualizações + Completado + Servidores SMP configurados + Servidores XFTP configurados + Verifique sua conexão de internet e tente novamente + Verificar atualizações \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml index 7889ef396e..e652ab27e5 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/pt/strings.xml @@ -39,7 +39,7 @@ Apenas dados de perfil local Realçar Você permite - Mensagens que desaparecem + Mensagens temporárias sempre não Definir preferências de grupo @@ -62,12 +62,12 @@ %d min %d mês %d meses - %dmês + %dº mês Administradores podem criar as ligações para entrar em grupos. Mensagens de voz Aceitar automaticamente pedidos de contato Adicionar servidores lendo QR codes. - Mensagens que desaparecem + Mensagens temporárias Mensagens ao vivo As mensagens enviadas serão eliminadas após o tempo definido. Mensagem de rascunho @@ -122,7 +122,7 @@ 1 semana Aceitar aceitar chamada - Adicionar servidor… + Adicionar servidor Aceitar Aceitar pedido de ligação\? Aceitar modo anónimo @@ -138,17 +138,17 @@ Arquivo de conversa Eliminar Eliminar todos os ficheiros - Eliminar ficheiro + Eliminar arquivo Eliminar arquivo de conversa\? Eliminar base de dados BASE DE DADOS DE CONVERSA Base de dados de conversa eliminada Nome para Exibição Mostrar: - eliminada + eliminado grupo eliminado Nome do grupo: - O nome para exibição não pode conter espaços em branco. + O nome de exibição não pode conter espaços em branco. %dm Não mostrar novamente Mostrar opções de desenvolvedor @@ -157,9 +157,9 @@ Permitir enviar mensagens que desaparecem. Nome para Exibição: Mostrar código QR - Mensagens que desaparecem são proibidas neste grupo. - Conectar via ligação / código QR - Mensagens que desaparecem são proibidas nesta conversa. + Mensagens temporárias são proibidas neste grupo. + Conectar via link / código QR + Mensagens temporárias são proibidas nesta conversa. Enviar AO VIVO Enviar uma mensagem ao vivo - ela será atualizada para o(s) destinatário(s) à medida que você a digita @@ -251,7 +251,7 @@ Base de dados de conversa importada A conversa está parada Verifique o endereço do servidor e tente novamente. - O seu perfil será enviado para o contato do qual você recebeu esta ligação. + O seu perfil será enviado para o contacto do qual você recebeu esta ligação. Erro ao eliminar a base de dados de conversa Erro ao eliminar ligação de grupo Perfil de conversa @@ -270,14 +270,14 @@ Alterar função Alterar a função no grupo\? Erro ao alterar função - O contato permite + O contacto permite Preferências de conversa SimpleX m - Conectar através da ligação de contato\? - Conectar via convite de ligação\? + Conectar através do endereço de contacto? + Conectar via link de convite? Conectar através da ligação do grupo\? - Você irá juntar-se a um grupo ao qual esta ligação se refere e conectar-se aos membros do grupo. + Você irá conectar-se a todos os membros do grupo. erro Erro ao criar perfil! Erro ao adicionar membro(s) @@ -334,7 +334,7 @@ conectando (aceite) conectando (anunciado) conectando - Contato verificado + Contacto verificado Ligação de grupo conectando… conexão estabelecida @@ -348,10 +348,10 @@ Conectado conectando… Tempo limite de conexão - Preferências de contato - Contato escondido: - O contato ainda não está conectado! - Nome do contato + Preferências de contacto + contacto escondido: + O contacto ainda não está conectado! + Nome do contacto Contribuir Copiar Versão principal: v%s @@ -362,17 +362,16 @@ Conectar via ligação Erro de conexão conexão %1$d - O contato já existe + O contacto já existe Convite de ligação de utilização única Salvar Modo anónimo convidado através da ligação do seu grupo - Você está a tentar convidar um contato com quem partilhou um perfil anónimo para o grupo no qual voçê está a usar o seu perfil principal + Você está a tentar convidar um contacto com quem partilhou um perfil anónimo para um grupo no qual você está a usar o seu perfil principal Conexão O modo anónimo protege a privacidade do nome e da imagem do seu perfil principal — para cada novo contato um novo perfil aleatório é criado. - Salvar cor - você partilhou ligação de utilização única - você partilhou ligação anónima de utilização única + você partilhou a ligação de utilização única + você partilhou a ligação anónima de utilização única Ligação de conexão inválida Salvar Se você recebeu convite de ligação do SimpleX Chat, você pode abri-lo no seu navegador: @@ -398,9 +397,9 @@ Descrição Ligação completa anónimo via ligação de endereço de contato - Abrir a ligação no navegador pode reduzir a privacidade e a segurança da ligação. As ligações Simplex não confiáveis serão vermelhas. + Abrir a ligação no browser poderá reduzir a privacidade e a segurança da ligação. As ligações Simplex não confiáveis serão vermelhas. Confirmar credenciais - Contato e todas as mensagens serão eliminadas - esta acção não pode ser revertida! + O contacto e todas as mensagens serão eliminadas - esta ação é irreversível! ligação de visualização de imagem Ligação inválida! Guia de Utilizador.]]> @@ -415,12 +414,12 @@ Convite de ligação de utilização única Todos os seus contatos permanecerão conectados. A atualização do perfil será enviada aos seus contatos. Adicione endereço ao seu perfil, para que os seus contatos possam partilhá-lo com outras pessoas. A atualização do perfil será enviada aos seus contatos. - Os contatos podem marcar mensagens para eliminar; você será capaz de as ver. + Os contactos podem marcar mensagens para eliminar; você será capaz de as ver. Criar convite de ligação de utilização única anónimo via ligação de utilização única anónimo via ligação de grupo via ligação de utilização única - Você está a usar um perfil anónimo para este grupo - para impedir a partilha do seu perfil principal não é permitido convidar contatos + Você está a usar um perfil anónimo para este grupo - para impedir a partilha do seu perfil principal, não é permitido convidar contactos Ligação para 1 utilização Criar ligação Apagar ligação\? @@ -434,8 +433,8 @@ Partilhar ligação de utilização única GitHub.]]> Criar perfil - Criar perfil - %d contato(s) selecionado(s) + Criar o seu perfil + %d contacto(s) selecionado(s) Eliminar perfil de conversa para Eliminar para todos %dd @@ -448,16 +447,16 @@ Eliminar ficheiros de todos os perfis de conversa Eliminar mensagens %d ficheiros(s) com tamanho total de %s - Senha atual… - Eliminar conexão pendente\? + Palavra-passe atual… + Eliminar ligação pendente? Eliminar mensagens após Eliminar perfil de conversa\? Eliminar perfil de conversa %d hora %d horas - erro de base de dados - A senha da base de dados é diferente da guardada na Keystore. - A senha da base de dados é necessária para abrir a conversa. + Erro de base de dados + A palavra-passe da base de dados é diferente da armazenada na Keystore. + A palavra-passe da base de dados é necessária para abrir a conversa. Eliminar grupo Eliminar grupo\? direta @@ -465,20 +464,20 @@ Eliminar perfil Criar fila Eliminar para mim - Desabilitar o bloqueio do SimpleX + Desativar o bloqueio do SimpleX Atualmente o tamanho máximo de ficheiro suportado é %1$s. - Eliminar contato + Eliminar contacto Criar grupo secreto Eliminar servidor Personalizar tema Eliminar imagem Não criar endereço - Desabilitar - Senha da base de dados + Desativar + Palavra-passe da base de dados Criar endereço SimpleX ID da base de dados Eliminar ficheiro - Eliminar contato\? + Eliminar contacto? DISPOSITIVO Mensagens diretas Descentralizado @@ -488,7 +487,7 @@ %ds %d seg %dw - Nome para exibição duplicado! + Nome de exibição duplicado! %d segundos Ficheiro guardado Atualização da base de dados @@ -500,7 +499,7 @@ Ficheiro Ficheiro não encontrado Ficheiro - Você não perderá os seus contatos se eliminar o seu endereço mais tarde. + Você não irá perder os seus contatos se eliminar o seu endereço mais tarde. Você pode esconder ou silenciar um perfil de utilizador - pressione-o para o menu. Vídeo ligado Servidores XFTP @@ -526,7 +525,7 @@ Senha da base de dados incorreta esquerda Senha errada! - Você deixará de receber mensagens deste grupo. O histórico de mensagens será preservado. + Você irá deixar de receber mensagens deste grupo. O histórico de mensagens será preservado. Juntar-se ao grupo\? Com mensagem de boas-vindas opcional. via %1$s @@ -559,7 +558,7 @@ Esta ação não pode ser revertida - o seu perfil, contatos, mensagens e ficheiros serão irreversivelmente perdidos. Esta ação não pode ser revertida - as mensagens enviadas e recebidas antes da seleção serão eliminadas. Pode demorar vários minutos. A sua base de dados atual de conversas será ELIMINADA e SUBSTITUÍDA pela importada. -\nEsta ação não pode ser revertida - o seu perfil, contatos, mensagens e ficheiros serão irreversivelmente perdidos. +\nEsta ação é irreversível - o seu perfil, contactos, mensagens e ficheiros serão irreversivelmente perdidos. Marcar como não lido membro MEMBRO @@ -579,7 +578,6 @@ Pré-visualização de notificação Muito provavelmente este contato eliminou a conexão consigo. Este texto está disponível nas definições - Atualizar definições de servidores .onion\? Pode ser alterado mais tarde através das definições. AJUDA SUPORTE SIMPLEX CHAT @@ -592,29 +590,28 @@ Novidades %s Novo pedido de contato Novo arquivo de base de dados - A base de dados está encriptada com uma senha aleatória, você pode alterá-la. + A base de dados está encriptada com uma palavra-passe aleatória, você pode alterá-la. Insira a senha correta. A tentativa de alterar a senha da base de dados não foi concluída. - A sua base de dados de conversas não está encriptada - defina a senha para a proteger. + A sua base de dados de conversas não está encriptada - defina a palavra-passe para a proteger. Insira a senha… - Você tem que inserir a senha sempre que a aplicação é iniciada - ela não é guardada no dispositivo. + Você tem que inserir a palavra-passe sempre que a aplicação é iniciada - ela não é armazenada no dispositivo. Nova senha… Remover Remover senha da Keystore\? Insira a senha atual correta. Atualizar senha da base de dados nova mensagem - Senha da base de dados & exportação - A base de dados está encriptada com uma senha aleatória. Por favor, altere-a antes de exportar. - A senha de encriptação da base de dados será atualizada. + Palavra-passe da base de dados & exportação + A base de dados está encriptada com uma palavra-passe aleatória. Por favor, altere-a antes de exportar. + A palavra-passe de encriptação da base de dados será atualizada. Por favor armazene a senha de forma segura, você NÃO será capaz de a alterar se a perder. - A senha de encriptação da base de dados será atualizada e armazenada na Keystore. + A palavra-passe de encriptação da base de dados será atualizada e armazenada nas definições. A base de dados será encriptada e a senha armazenada na Keystore. Por favor armazene a senha de forma segura, você NÃO será capaz de aceder às conversas se a perder. Para receber notificações, por favor, digite a senha da base de dados Hosts Onion não serão usados. Hosts Onion serão usados quando disponíveis. - Hosts Onion serão necessários para a conexão. chamada de vídeo (sem encriptação ponta a ponta) chamada de áudio (não encriptada ponta a ponta) chamada de áudio encriptada ponta a ponta @@ -624,12 +621,11 @@ oferecido %s Arquivo de base de dados antigo Hosts Onion serão necessários para a conexão. - Hosts Onion serão usados quando disponíveis. Pode acontecer quando você ou sua conexão usaram o backup de base de dados antigo. encriptado ponta a ponta desligado Ler código QR.]]> - contato não tem encriptação ponta a ponta + o contacto não tem encriptação ponta a ponta oferecido %s: %2s desligado ligado @@ -645,9 +641,8 @@ Para verificar a encriptação de ponta a ponta com o seu contato, compare (ou leia) o código nos seus dispositivos. Ler o código de segurança a partir da aplicação do seu contacto. Ler o código QR do servidor - Hosts Onion não serão usados. encriptação de ponta a ponta de 2 camadas.]]> - contato tem encriptação ponta a ponta + o contacto tem encriptação ponta a ponta sem encriptação ponta a ponta criador meses @@ -686,51 +681,51 @@ Email Isolamento do Transporte terminado - Ferramentas de desenvolvedor + Ferramentos de programador Encriptar Fazer downgrade e abrir chat - Activar TCP manter-vivo + Ativar TCP keep-alive predefinido (%s) Isolamento do Transporte A tentar connectar ao servidor usado para receber mensagens deste contacto. Erro de descodificação IDs das bases de dados e opções de isolamento do Transporte - Downgrade da base de dados - Activar SimpleX Lock + Regressão da base de dados + Ativar SimpleX Lock Editar editado Mensagem temporária Editar imagem - Activar bloqueio - Activar código de acesso auto-destrutivo - Activar auto-destruição + Ativar bloqueio + Ativar código de acesso auto-destrutivo + Ativar auto-destruição Um perfil vazio é criado com o nome fornecido, e a aplicação abre como de costume. - a versão da base de dados é mais recente do que a aplicação, mas sem migração para baixo para: %s - Desaparecerá: %s + a versão da base de dados é mais recente do que a aplicação, mas sem migração de regressão para: %s + Desaparecerá a: %s Eliminar ficheiros e multimédia\? Eliminado a: %s - A autenticação do dispositivo está desactivada. A desligar SimpleX Lock. - A autenticação do dispositivo não está activada. Pode activar o SimpleX Lock através das Definições, depois de activar a autenticação do dispositivo. + A autenticação do dispositivo está desativada. A desligar SimpleX Lock. + A autenticação do dispositivo não está ativa. Pode ativar o SimpleX Lock através das Definições, depois de ativar a autenticação do dispositivo. migração diferente na aplicação/base de dados: %s / %s - Activar chamadas a partir do ecrã de bloqueio através das Definições. + Ativar chamadas a partir do ecrã de bloqueio através das Definições. Encriptar base de dados\? Introduzir o servidor manualmente - Activar a eliminação automática de mensagens\? + Ativar a eliminação automática de mensagens? Editar perfil de grupo Erro de desencriptação O código de acesso é substituído por um código auto-destrutivo. ID da base de dados: %d - Nomes, avatares e isolamento de transporte diferentes. + Nomes, fotos de perfil e isolamento de transporte diferentes. Secundário adicional Realce adicional Fundo - Customizar e partilhar temas de cor. + Personalizar e partilhar temas de cor. Temas personalizados Eliminado a - Desaparecerá - activado - activado para contacto - activado para si + Desaparecerá a + Ativo + ativo para contacto + ativo para si Pesquisar Desativado O teste falhou na etapa %s. @@ -780,4 +775,178 @@ Parar conversa Obrigado aos usuários – contribuam via Weblate! Função lenta + Entrega + As suas preferências + Desconectar dispositivos móveis + %d mensagens bloqueadas pelo administrador + Abrir porta na firewall + A sua privacidade + Ativar (manter sobreposição de grupo) + Abrir ecrã de migração + Erro de servidor do destino: %1$s + Você pode ativar o SimpleX Lock através das Definições. + Eliminar %d mensagens? + Criar perfil de chat + Abrir definições + Contactos + você saiu + você removeu %1$s + encriptação ok + Cancelar mudança de endereço + Você continuará a receber chamadas e notificações de perfis silenciados quando estes estão ativos. + Ativar em conversas diretas (BETA)! + Endereço de computador + Descobrível na rede local + Desktop tem uma versão não suportada. Por favor, confirme que usa a mesma versão em ambos os dispositivos + Desktop tem o código de convite errado + A transferir arquivo + A transferir detalhes de ligação + A criar ligação de arquivo + duplicados(as) + Abrir definições do servidor + O seu perfil de conversa será enviado +\npara o seu contacto + Conectar com %1$s? + Auricular + Dispositivos + Usar a partir de computador na aplicação mobile e ler código QR.]]> + Você enviou um convite de grupo + você mudou o seu cargo para %s + você mudou o endereço para %s + 6 novos idiomas de interface + A versão %s da aplicação de computador não é compatível com esta aplicação. + %s com a razão: %s]]> + O seu perfil %1$s vai ser partilhado. + Transferir + Cancelar + Opções de programador + Ativar + Computador + O seu contacto enviou um ficheiro que é maior que o tamanho máximo suportado atualmente (%1$s). + O seu servidor + O endereço do seu servidor + As tuas chamadas + Não enviar histórico a novos membros. + Você partilhou um caminho de ficheiro inválido. Relate o problema aos programadores da aplicação. + Você não tem conversas + Criar grupo + O seu perfil de conversa será enviado para os membros do grupo + Desktop foi disconectado + Transferência falhada + Desativar notificações + Abir SimpleX Chat para aceitar a chamada + Ativar (manter sobreposições) + Criado em + Ativo para + Os seus contactos podem permitir eliminação total de mensagens. + Desconectado com a razão: %s + Você já solicitou ligação através deste endereço! + Código e protocolo open-source - qualquer indivíduo pode ser anfitrião dos servidores. + %d mensagens bloqueadas + %d mensagens marcadas como eliminadas + Você não pode ser verificado; por favor tente novamente. + contacto eliminado + Cancelar mudança de endereço? + Você convidou este contacto + Corrigir nome para %s? + Descobrir na rede local + Ativar recibos para grupos? + Desativar para todos + Você desbloqueou %s + Você será conectado quando o pedido de ligação for aceite, por favor aguarde ou volte mais tarde! + O seu perfil atual + O seu perfil será armazenado no seu dispositivo e partilhado apenas com os seus contactos. Os servidores SimpleX não conseguem ver o seu perfil. + Você tem que usar a versão mais recente da sua base de dados de conversas em APENAS UM dispositivo, caso contrário poderá deixar de receber mensagens de alguns contactos. + A base de dados será encriptada e a palavra-passe armazenada nas definições. + Você juntou-se a este group. A conectar ao membro que o convidou. + você mudou o cargo de %s para %s + contacto %1$s mudou para %2$s + você mudou de endereço + desativado + Criado em: %s + desativado + Descobrir e juntar a grupos + Dispositivos desktop + Compreendido + errors de desencriptação + Ligações + Criado + Eliminado + Erros de eliminação + Erro de cópia + Criar novo perfil na aplicação de computador. 💻 + Recibos de entrega! + Recibos de entrega desativados! + Não ativar + Você não pode enviar mensagens! + Eliminar e notificar contacto + Eliminar sem notificação + Conversas eliminadas + O seu endereço SimpleX + Os seus contactos + Os seus perfis de conversa + Você pode ver a ligação de convite outra vez nos detalhes da ligação. + A transferir atualização da app, não feche a aplicação + Transferir %s (%s) + Os seus servidores ICE + O seu perfil, contactos e mensagens entregues são armazenados no seu dispositivo. + Os seus servidores ICE + Desativar (manter as sobreposições de grupo) + Desativar (manter sobreposições) + Desativar recibos? + Desativar recibos para grupos? + Ativar para todos + Ativar para todos os grupos + Ativar recibos? + Você rejeitou o convite de grupo + Você juntou-se a este grupo + %d eventos de grupo + encriptação ok para %s + O pedido de ligação vai ser enviado para este membro do grupo. + O contacto foi eliminado. + Você precisa de permitir que o seu contacto inicie chamada de voz para poder iniciar uma chamada de voz. + Escuro + Cores de modo escuro + Modo escuro + Aproximar + Desktop está inativo + Perfil atual + Eliminar base de dados deste dispositivo + Estatísticas detalhadas + Transferido + Ficheiros transferidos + Erros de transferência + Conectar via link? + Criar perfil + A criar ligação… + Contacto eliminado! + Conversa eliminada! + Erro crítico + Nome deste dispositivo + O contacto será eliminado - esta ação é irreversível! + A palavra-passe de encriptação da base de dados será atualizada e armazenada nas definições. + Criar um grupo utilizando um perfil aleatório. + Migração da base de dados em progresso. +\nPode demorar alguns minutos. + Você será conectado quando o dispositivo do seu contacto estiver online, por favor aguarde ou volte mais tarde! + Desconectar + Desativado + Os seus servidores XFTP + Você controla a conversa! + Você poderá ver a conversa com %1$s na lista de conversas. + Os seus servidores SMP + Você pode usar markdown para formatar mensagens: + Você poderá enviar mensagens para %1$s através das conversas Eliminadas. + O seu contacto precisa de estar online para a ligação ser completada. +\nVocê pode cancelar esta ligação e remover o contacto (e tentar mais tarde com uma nova ligação). + Os seus contactos permanecerão conectados. + Você terá que autenticar-se quando iniciar ou abrir a aplicação após 30 segundos em segundo plano. + Ativar acesso à câmara + Você precisa de permitir que o seu contacto envie mensagens de aúdio para poder enviá-las. + Desativar + Desativar para todos os grupos + A tua base de dados de conversas + Detalhes + Você pode partilhar o seu endereço com os seus contactos para permitir que se conectem com %s. + Você pode partilhar o seu endereço como uma ligação ou código QR - qualquer pessoa pode conectar-se a si. \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml index b6f268e6f1..5ebc60f5a7 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ro/strings.xml @@ -5,7 +5,7 @@ 30 secunde Acceptă Acceptă incognito - Adaugă server… + Adaugă server Setări de rețea avansate %1$s dorește să se conecteze cu tine prin Acceptă @@ -154,5 +154,524 @@ autor Arabă, Bulgară, Finlandeză, Ebraică, Thailandeză și Ucraineană - mulțumită utilizatorilor și Weblate. Apelurile audio/video sunt interzise. - (prezent) + (actual) + Poză de profil eliminată + Temă întunecată + eliminat + Întunecată + Adresă de contact eliminată + Personalizează și distribuie teme colorate. + Teme personalizate + Repetă descărcarea + Timp personalizat + Elimină membru + Repetă importarea + Personalizează tema + Elimină + Elimină + Elimină membru + Elimini membrul? + Resetează la implicit + Resetează culoarea + Resetează culorile + Elimină imagine + Repetă încărcarea + apel respins + Elimini fraza de acces din Keystore? + Elimini fraza de acces din setări? + Repetă cererea de conectare? + Necesar + Reîncearcă + Primește fișiere în siguranță + Grupuri mai sigure + Restabilește + Reîmprospătează + Revoci fișierul? + Revocă + Renegociezi criptarea? + Resetează + Respinge + Salvează fraza de acces în setări + Salvează și actualizează profilul grupului + Repetă cererea de alăturare? + Repornește conversația + salvat + Salvat de la %s + Salvează + Salvat + Salvat de la + Renegociază + Salvează servere + Servere WebRTC ICE salvate vor fi eliminate. + Salvează parola profilului + Salvează fraza de acces și deschide conversația + %s și %s + Renegociază criptarea + Salvează și notifică contactul + Salvează și notifică contactele + Salvează și notifică membrii grupului + Rulează când aplicația este pornită + Răspunde + Revocă fișierul + Salvează + Salvezi setările? + Salvezi preferințe? + Repornire + Restabilește copia de rezervă a bazei de date + Restabilești copia de rezervă a bazei de date? + Eroare la restabilirea bazei de date + %1$s eliminat + %s și %s conectați + Rol + Salvează + Arată + Respinge + Salvezi servere? + Apel respins + Mesaj salvat + Salvează arhiva + Repornește aplicația pentru a crea un nou profil + Salvează fraza de acces în Keystore + Salvează profilul grupului + Repetă + Trimite previzualizări ale link-ului + Setează frază de acces + Distribuie adresă + Trimis la + Secundar + Mesaj trimis + Setează preferințele grupului + trimis + Adresa serverului este incompatibilă cu setările de rețea. + Versiunea serverului este incompatibilă cu setările de rețea. + Trimițând prin + trimiterea de fișiere nu este acceptată încă + Scanează codul de securitate din aplicația contactului tău + Selectează contacte + Autodistrugere + Răspuns trimis + Distribuie media… + Distribuie mesaj… + Arată lista conversațiilor într-o fereastră nouă + Arată consola într-o fereastră nouă + Setează fraza de acces a bazei de date + Setează fraza de acces a bazei de date + setează adresă de contact nouă + %s (actual) + Cod de sesiune + Expeditorul a anulat transferul de fișiere. + Serverul necesită autorizație pentru a crea cozi, verifică parola + Distribuie + trimitere eșuată + Caută sau lipește link SimpleX + Setează numele de contact + Salvezi mesajul de bun venit? + Evaluare de securitate + Scanează cod QR de pe desktop + Caută + Trimite un mesaj live - se va actualiza pentru destinatar(i) în timp ce îl tastezi + Distribuie fișier + Trimite până la ultimele 100 de mesaje membrilor noi. + Bara de căutare acceptă link-uri de invitație. + secunde + Scanează de pe mobil + Mesaj trimis + Scanează cod QR + Trimite + Trimite întrebări și idei + Arată opțiuni dezvoltator + sec + Mesajele trimise vor fi șterse după timpul setat. + Serverul necesită autorizație pentru a încărca, verifică parola + Arată contact și mesaje + Distribuie fișier… + Setează numele de contact… + Trimite mesaj + Trimite mesaj temporar + (scanează sau lipește din clipboard) + Arată cod QR + Test server eșuat! + Arată: + Arată erori interne + secret + SETĂRI + %s conectat + setează imagine de profil + Trimis către: %s + SERVERE + Trimite mesaj live + %s descărcat + Distribui adresa cu contactele? + Arată previzualizare + trimite mesaj direct + Trimite mesaj direct pentru a te conecta + Selectează + Trimiterea de fișiere va fi oprită. + Trimite + Setări + Scanează cod + Cod de securitate + Trimite-ne email + Scanează codul QR al serverului + Distribuie contactelor + Arată + cod de securitate schimbat + Arată ultimul mesaj + Trimite mesaj direct + Setează tema implicită + SimpleX + SimpleX nu poate rula în fundal. Vei primi notificările doar când aplicația rulează. + Serviciu SimpleX Chat + Adresă SimpleX + Închide + Link-uri SimpleX + Link-urile SimpleX sunt interzise în acest grup. + Securitatea SimpleX Chat a fost verificată de Trail of Bits. + Mesaje SimpleX Chat + Apeluri SimpleX Chat + Adresă SimpleX + simplexmq: v%s (%2s) + Link-uri SimpleX nepermise + Echipa SimpleX + Închizi? + Adresă de contact SimpleX + Link de grup SimpleX + Link-uri SimpleX + Invitație unică SimpleX + Siglă SimpleX + Grupuri mici (max 20) + Mod incognito simplificat + Pătrat, cerc, sau orice între. + %s nu este verificat + %s este verificat + Servere SMP + %s, %s și %d alți membri s-au conectat + %s, %s și %d membri + Difuzor + %s, %s și %s s-au conectat + %s: %s + Niște servere au eșuat testul: + Difuzor oprit + Difuzor pornit + Copie de rezervă a datelor aplicației + Tema aplicației + Accent suplimentar 2 + Începe conversația + Toate modurile de culoare + Folosește mereu releu + Aplică pentru + Începe o nouă conversație + Stea pe GitHub + Criptare de la capăt la capăt standard + Pornește periodic + Mereu + Folosește mereu rutare privată. + Toate contactele vor rămâne conectate. Actualizarea profilului va fi trimisă contactelor tale. + pornire… + %s secunde + Începi conversația? + Setări avansate + Adresă desktop rea + ID de mesaj incorect + Hash de mesaj incorect + ID de mesaj incorect + Hash de mesaj incorect + Schimbă adresa de primire + Conversația este oprită. Dacă ai folosit deja această bază de date pe alt dispozitiv, ar trebui să o transferi înapoi înainte de a porni conversația. + APELURI + ai schimbat rolul pentru tine la %s + Capacitate depășită - destinatarul nu a primit mesajele trimise anterior. + Schimbă codul de acces autodistructibil + Conversația este oprită + se schimbă adresa… + Contact verificat + Creat la + Migrează de pe alt dispozitiv pe dispozitivul nou și scanează codul QR.]]> + Te conectezi prin adresa de contact? + Te conectezi printr-un link unic? + Conectare incognito + Contactul deja există + Schimbă codul de acces + Poți porni Blocare SimpleX din Setări. + Conversații + Alege un fișier + Contactul tău trebuie să fie online pentru a se completa conexiunea. +\nPoți anula această conexiune și elimina contactul (și poți încerca mai târziu cu un nou link). + eroare apel + Apelurile tale + Schimbă + Schimbă rolul + Contactul permite + Grupuri mai bune + Desktop conectat + Preferințe contact + Te conectezi cu %1$s? + Anulează previzualizarea imaginii + Discută cu dezvoltatorii + Trebuie să introduci fraza de acces de fiecare dată când aplicația pornește - nu este stocată pe dispozitiv. + blocat + Preferințe conversație + Mod întunecat + Interfață chineză și spaniolă + Crează un grup folosind un profil aleatoriu. + Blochează membrii grupului + Mobil conectat + Conectat la desktop + Anulează migrarea + Celular + Nu poți trimite mesaje! + Schimbi adresa de primire? + Contactul nu este conectat încă! + Conectare prin link + Poți vedea linkul de invitație din nou în detaliile conexiunii. + Profilurile tale de conversație + Crează profil de conversație + apel terminat %1$s + Crează + Bluetooth + Camera + Apeluri pe ecranul de blocare: + contactul are criptare e2e + contactul nu are criptare e2e + Contacte + CONVERSAȚII + BAZĂ DE DATE CONVERSAȚIE + Baza de date a conversației ștearsă + Conversația rulează + Baza ta de date a conversațiilor + Baza ta de date a conversațiilor nu este criptată - setează frază de acces pentru a o proteja. + Fraza de acces de criptare a bazei de date va fi actualizată și stocată în setări. + Creat pe %1$s + ai schimbat rolul %s la %s + Nu se pot invita contactele! + Te-ai alăturat grupului + Nu se poate invita contactul! + se conectează (acceptat) + Creat la: %s + Blochează + Blochează membru + Conectare directă? + Profilul tău de conversație va fi trimis membrilor grupului + Întunecat + Culori mod întunecat + Și tu și contactul tău puteți face apeluri. + %s anulat + Conectat la mobil + Eroare copiere + Te conectezi prin link? + Nu ai putut fi verificat(ă); te rog încearcă din nou. + Copiază + Anulează mesajul live + Conectare prin link / cod QR + Contactele tale vor rămâne conectate. + Fraza de acces de criptare a bazei de date va fi actualizată. + Contactele tale pot permite ștergerea totală a mesajelor. + Trebuie să permiți contactului tău să trimită mesaje vocale pentru a le putea trimite. + Versiunede bază: v%s + Creează o adresă pentru a permite oamenilor să se conecteze cu tine. + %s blocat + ai schimbat adresa + ai schimbat adresa pentru %s + se schimbă adresa… + se schimbă adresa pentru %s… + Și tu și contactul tău puteți trimite mesaje temporare. + Nu se pot primi fișiere + Poate fi dezactivat în setări – notificările vor fi încă afișate cât timp aplicația rulează.]]> + Verifică mesajele noi la fiecare 10 minute timp de până la 1 minut + Nu ai conversații + Contactul și toate mesajele vor fi șterse - acest lucru nu poate fi anulat! + Copiat în clipboard + Camera + Ai invitat un contact + Profilul tău de conversație va fi trimis +\ncontactului tău + Contribuie + Profil conversație + aldin + Poți folosi markdown pentru a formata mesaje: + Apelând… + Schimbă modul de autodistrugere + Baza de date a conversației importată + Trebuie să folosești cea mai recentă versiune a bazei de date a conversațiilor DOAR pe un singur dispozitiv, altfel se poate să nu mai primești mesajele de la unele contacte. + Nu se poate accesa Keystore pentru a salva parola bazei de date + Conversație migrată! + Continuă + apel în curs + Apel în curs + Și tu și contactul tău puteți trimite mesaje vocale. + Contact ascuns: + Nume contact + Crează adresă + Bază de date criptată! + Schimbi fraza de acces a bazei de date? + Conversația este oprită + Poți porni conversația prin Setările aplicației / Bază de date sau repornind aplicația. + ai ieșit + Blochezi membrul? + Blocat de admin + Și tu și contactul tău puteți adăuga reacții la mesaje. + Mesaje mai bune + blocat + blocat de admin + Nu se poate inițializa baza de date + Contactul tău a trimis un fișier care este mai mare decât dimensiunea maximă suportată în prezent (%1$s). + anulează previzualizarea link-ului + Verifică adresa serverului și încearcă din nou. + Tu îți controlezi conversația! + Apel deja terminat! + Apel terminat + Blochează pentru toți + Ai cerut deja conexiunea prin această adresă! + Te conectezi la tine? + Verifică conexiunea la internet și încearcă din nou + Culori conversație + Temă conversație + Bun pentru baterie. +\nServiciul în fundal verifică mesaje la fiecare 10 minute. Ai putea rata apeluri sau mesaje urgente. + Negru + Blochezi membrul pentru toți? + Și tu și contactul tău puteți șterge ireversibil mesajele trimise. (24 de ore) + Folosește mai multă baterie! +\nServiciul în fundal rulează mereu – notificările sunt afișate imediat ce mesajele sunt disponibile. + Nu se poate trimite mesajul + Ștergeți + Confirmați fișiere de la servere necunoscute. + schimbat adresa pentru dumneavoastră + Confirmați parola nouă… + Conectare + conexiune %1$d + conexiune stabilită + Eroare de conexiune + Conexiune expirată + se conectează + Confirmare + În curând! + se conectează… + Schimbați modul de blocare + Versiunea aplicației: %s + pentru fiecare profil de conversație pe care le aveți în aplicație]]> + Permiteți downgrade-ul + O conexiune separată TCP (și SOCKS credential) va fi folosită pentru fiecare contact și membru de grup +\nVa rugăm considerați că: dacă aveți prea multe conexiuni, consumul dumneavoastră de baterie și trafic de internet pot fi considerabil mai mari, iar unele conexiuni pot eșua. + Conexiune terminată + Se conectează la desktop + Rugat să primească imaginea + Cerere de conexiune trimisă! + Vă rugăm să rețineți: mesajele și releurile pentru fișiere sunt conectate printr-un proxy SOCKS. Apelurile ți trimiterea de previzualizări a link-urilor folosesc o conexiune directă.]]> + conectat + Confirmați codul de access + Confirmare actualizare bază de date + "schimbat rolul lui %s la %s" + conectat + se conectează (introdus) + conectat + se conectează + Schimbați rolul de grup? + Modul color + Prin profil de conversație (implicit) sau prin conexiune (BETA). + Comparați codurile de securitate cu contactele dumneavoastră. + Conexiune oprită + Conexiune oprită + Conectare la desktop + Conexiunea la desktop este într-o stare proastă + Conectare + Conexiune + Ștergeți notițele private? + Conectare automată + se conectează apelul… + se conectează (invetație la introducere) + se conectează… + %s este într-o stare proastă]]> + Confirmați setările de rețea + se conectează… + Eroare de conexiune (AUTENTIFICARE) + Optimizarea pentru baterie este activă, vom opri serviciile din fundal și cererile periodice pentru mesaje noi. Le puteți reactiva din setări. + conectat + Crează un grup: pentru a crea un nou grup.]]> + Ștergeți conversația? + Ștergeți + Ștergeți conversația + Conectare + Consolă conversație + Confirmați parola + colorat + Arhivă conversație + Toate contactele, conversațiile și fișierele dumneavoastră vor fi encriptate într-un mod sigur și încărcate pe bucăți pe releurile XFTP configurate. + Rugat să primească videoclipul + Comparați fișierul + Vă rugăm să rețineți: nu veți putea recupera sau schimba parola dacă o veți pierde.]]> + schimbat rolul dumneavoastră la %s + conectat + se conectează + conectat + Ștergeți + Buton de închidere + Ștergeți verificarea + Configurare servere ICE + conectat direct + se conectează (anunțat) + Cel mai bun pentru baterie. Veți primi notificări doar când aplicația rulează (FĂRĂ servicii de fundal).]]> + Se conectează apelul + complet + Vă rugăm să rețineți: folosind aceeași bază de date pe două dispozitive, va intrerupe decripția mesajelor de la conexiunile dumneavoastră, ca protecție de securitate.]]> + Confirmați încărcarea + Confirmați că țineți minte parola de la baza de date pentru a o migra. + ARHIVĂ CONVERSAȚIE + Conexiune + Ștergi profilul de conversație? + Șterge pentru mine + %dd + șters + Șterge contact + Șters la + Versiunea aplicației desktop %s nu este compatibilă cu această aplicație. + Ștergi mesajul membrului? + Șterge arhiva + Șterge grup + Șterge și notifică contactele + Decentralizat + grup șters + %d contact(e) selectat(e) + Șters la: %s + Ștergi profilul de conversație? + Șterge profil + implicit (%s) + Confirmări de livrare! + Confirmările de livrare sunt dezactivate! + Adresă desktop + Dispozitive desktop + Desktop + Eroare decriptare + Șterge imagine + Șterge după + Șterge pentru toată lumea + Descriere + Șterge fișier + Șterge coadă + Ștergi contactul? + Șterge + Șterge + Ștergi fișiere și media? + Ștergi arhiva conversației? + %d zile + %d zi + Șterge adresa + Șterge mesaje + Ștergi %d mesaje? + Ștergi adresa? + Șterge toate fișierele + Șterge link + Ștergi link? + zile + Șterge + Ștergi mesajul? + Livrare + Ștergi conexiunea în așteptare? + Șterge server + Șterge baza de date + contact șters + Ștergi grupul? + Șterge baza de date de pe acest dispozitiv + Șterge profil de conversație + Șterge fișiere pentru toate profilurile de conversație \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml index a71638eae0..8c5adae3e3 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/ru/strings.xml @@ -354,7 +354,7 @@ SMP серверы Адрес сервера по умолчанию Добавить серверы по умолчанию - Добавить сервер… + Добавить сервер Тестировать сервер Тестировать серверы Сохранить серверы @@ -394,7 +394,6 @@ Соединяться с серверами через SOCKS прокси через порт %d? Прокси должен быть запущен до включения этой опции. Использовать прямое соединение с Интернет? Если Вы подтвердите, серверы смогут видеть Ваш IP адрес, а провайдер - с какими серверами Вы соединяетесь. - Обновить настройки .onion хостов? Использовать .onion хосты Когда возможно Нет @@ -403,9 +402,6 @@ Onion хосты не используются. Подключаться только к onion хостам. \nОбратите внимание: Вы не сможете соединиться с серверами, у которых нет .onion адреса. - Onion хосты используются, если возможно. - Onion хосты не используются. - Подключаться только к onion хостам. Интерфейс Создать адрес @@ -467,13 +463,13 @@ соединено завершен - Новое поколение приватных сообщений + Новое поколение\nприватных сообщений Более конфиденциальный - Первая в мире платформа без идентификаторов пользователей. + Без идентификаторов пользователей. Защищен от спама - С Вами можно соединиться только через созданные Вами ссылки. + Вы определяете, кто может соединиться. Децентрализованный - Открытый протокол и код - кто угодно может запустить сервер. + Кто угодно может запустить сервер. Создать профиль Добавьте контакт Как это работает @@ -827,11 +823,10 @@ Сбросить настройки сек - Таймаут TCP соединения + Таймаут TCP-соединения Таймаут протокола Интервал PING Включить TCP keep-alive - Отменить изменения Сохранить Обновить настройки сети? Обновление настроек приведет к переподключению клиента ко всем серверам. @@ -848,7 +843,6 @@ Темная Тема - Сохранить цвет Сбросить цвета Акцент @@ -904,8 +898,8 @@ Когда приложение запущено Периодически Мгновенно - Больше расход батареи! Фоновый сервис постоянно запущен - уведомления будут показаны как только есть сообщения.]]> - Меньше расход батареи. Фоновый сервис проверяет сообщения каждые 10 минут. Вы можете пропустить звонки и срочные сообщения.]]> + Больше расход батареи! Приложение постоянно запущено в фоне - уведомления будут показаны сразу же.]]> + Меньше расход батареи. Приложение проверяет сообщения каждые 10 минут. Вы можете пропустить звонки и срочные сообщения.]]> Можно изменить позже в настройках. LIVE Отправить живое сообщение @@ -1250,7 +1244,7 @@ Запретить реакции на сообщения. Запретить реакции на сообщения. секунд - ЦВЕТА ТЕМЫ + ЦВЕТА ИНТЕРФЕЙСА Поделиться адресом с контактами\? Обновлённый профиль будет отправлен Вашим контактам. Об адресе SimpleX @@ -1328,7 +1322,7 @@ Давайте поговорим в SimpleX Chat Открытие базы данных… Запись обновлена - Во время импорта произошли некоторые ошибки - для получения более подробной информации вы можете обратиться к консоли. + Во время импорта произошли некоторые ошибки: нет текста Поиск Отключено @@ -1693,6 +1687,7 @@ Внутренняя ошибка Очистить личные заметки? Новый чат + Новое сообщение Или отсканируйте QR код Вы можете увидеть ссылку-приглашение снова открыв соединение. Показывать медленные вызовы API @@ -1851,4 +1846,308 @@ Форма картинок профилей Квадрат, круг и все, что между ними. Будет включено в прямых разговорах! + ФАЙЛЫ + Новые темы чатов + нет + Светлая + Системная + Цвета тёмного режима + Получайте файлы безопасно + Конфиденциальная доставка 🚀 + Улучшенная доставка сообщений + Уменьшенный расход батареи. + Версия сервера несовместима с настройками сети. + Неверный ключ или неизвестное соединение - скорее всего, это соединение удалено. + Превышено количество сообщений - предыдущие сообщения не доставлены. + Ошибка сервера получателя: %1$s + Ошибка: %1$s + Пересылающий сервер: %1$s +\nОшибка сервера получателя: %2$s + Пересылающий сервер: %1$s +\nОшибка: %2$s + Предупреждение доставки сообщения + Ошибка сети - сообщение не было отправлено после многократных попыток. + Адрес сервера несовместим с настройками сети. + информация сервера об очереди: %1$s +\n +\nпоследнее полученное сообщение: %2$s + Показать список чатов в новом окне + Приложение будет запрашивать подтверждение загрузки с неизвестных серверов (за исключением .onion адресов или когда SOCKS-прокси включен). + Незащищённый + Без Тора или ВПН, Ваш IP адрес будет доступен серверам файлов. + Отправлять сообщения напрямую, когда IP адрес защищен, и Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку. + Черная + Тёмный режим + Не отправлять сообщения напрямую, даже если сервер получателя не поддерживает конфиденциальную доставку. + Режим цветов + Разрешить прямую доставку + Всегда + Подтверждать файлы с неизвестных серверов. + Всегда использовать конфиденциальную доставку. + Тёмная + Отладка доставки + Ошибка инициализации WebView. Обновите Вашу систему до новой версии. Свяжитесь с разработчиками. +\nОшибка: %s + Светлый режим + Сделайте ваши чаты разными! + Информация об очереди сообщений + Персидский интерфейс + Защитить IP адрес + Защитите ваш IP адрес от серверов сообщений, выбранных Вашими контактами. +\nВключите в настройках Сеть и серверы. + Отправьте сообщения напрямую, когда Ваш сервер или сервер получателя не поддерживает конфиденциальную доставку. + Конфиденциальная доставка + Использовать конфиденциальную доставку с неизвестными серверами. + Использовать конфиденциальную доставку с неизвестными серверами, когда IP адрес не защищен. + Когда IP защищен + Да + Чтобы защитить ваш IP адрес, приложение использует Ваши SMP серверы для конфиденциальной доставки сообщений. + Изображения профилей + Все режимы + Тема приложения + Сбросить на тему приложения + Сбросить на тему пользователя + Неизвестные серверы! + Без Тора или ВПН, Ваш IP адрес будет доступен этим серверам файлов: +\n%1$s. + Не использовать конфиденциальную маршрутизацию. + Никогда + Неизвестные серверы + Нет + Показать статус сообщения + Прямая доставка сообщений + Режим доставки сообщений + КОНФИДЕНЦИАЛЬНАЯ ДОСТАВКА СООБЩЕНИЙ + Цвета чата + Тема чата + Тема профиля + Дополнительный акцент 2 + Дополнительные настройки + Обрезать + Полностью + Добрый день! + Доброе утро! + Полученный ответ + Удалить изображение + Повторить + Сбросить цвет + Масштаб + Отправленный ответ + Установить тему по умолчанию + Рисунок обоев + Фон обоев + Применить к + Не удается отправить сообщение + Бета + Соединeно + попытки + Готово + Потвердить удаление контакта? + Размытие изображений + Все профили + Проверка на наличие обновлений + Разрешить звонки? + Звонки запрещены! + Не удается позвонить члену группы + Обновление скачано + звонок + Не удается позвонить контакту + Не удается написать члену группы + Проверять обновления + соединиться + Адрес сервера назначения %1$s несовместим с настройками пересылающего сервера %2$s. + Ошибка подключения к пересылающему серверу %1$s. Попробуйте позже. + Пересылающий сервер %1$s не смог подключиться к серверу назначения %2$s. Попробуйте позже. + Версия пересылающего сервера несовместима с настройками сети: %1$s. + Версия сервера назначения %1$s несовместима с пересылающим сервером %2$s. + Неверный ключ или неизвестный адрес блока файла - скорее всего, файл удален. + Выбранные настройки чата запрещают это сообщение. + Ошибка файла + Сканировать / Вставить ссылку + Другие XFTP серверы + Настроенные XFTP серверы + Загрузка %s (%s) + Доступно обновление: %s + Выключить + Статус сообщения: %s + Размер шрифта + Пожалуйста, проверьте, что мобильный и компьютер находятся в одной и той же локальной сети, и что брандмауэр компьютера разрешает подключение. +\nПожалуйста, поделитесь любыми другими ошибками с разработчиками. + Файлы + Соединяется + Подробности + Всего + Активные соединения + Прием сообщений + В ожидании + Загружено + Статистика серверов будет сброшена - это нельзя отменить! + Всего отправлено + Переподключить + SMP сервер + Начиная с %s. + XFTP сервер + дубликаты + истекло + другое + Соединения + Ошибки удаления + Создано + Удалено + Защищено + Подписано + Блоков загружено + Ошибки загрузки + Размер + Ошибки приема + Принятые файлы + Адрес сервера + Ошибка сервера файлов: %1$s + Статус файла + Временная ошибка файла + Сильное + Подтверждено + Ошибки подтверждения + ошибки расшифровки + другие ошибки + Проксировано + Ошибки отправки + Блоков удалено + Блоков принято + Подписок игнорировано + Ошибка копирования + видеозвонок + Контакт будет удален — это нельзя отменить! + Оставить разговор + Удалить только разговор + Удалить без уведомления + Вы можете отправлять сообщения %1$s из Архивированных контактов. + Вставить ссылку + Нет отфильтрованных контактов + Ваши контакты + Архивированные контакты + Настроенные SMP серверы + Показать процент + Слабое + Среднее + Выключено + Доступная панель чата + Текущий профиль + Нет информации, попробуйте перезагрузить + Информация о серверах + Информация по + Подключенные серверы + Ранее подключенные серверы + Проксированные серверы + Начиная с %s. +\nВсе данные хранятся только на вашем устройстве. + Переподключить сервер для устранения неполадок доставки сообщений. Это использует дополнительный трафик. + Ошибка + Ошибка переподключения к серверу + Ошибка переподключения к серверам + Сбросить + Подробная статистика + Принято + Ошибка сброса статистики + Сбросить всю статистику? + Пожалуйста, попробуйте позже. + Ошибка конфиденциальной доставки + Адрес сервера несовместим с сетевыми настройками: %1$s. + Версия сервера несовместима с вашим приложением: %1$s. + Ошибки подписки + Отправленные файлы + Удалить %d сообщений членов группы? + Сообщения будут помечены на удаление. Получатель(и) смогут посмотреть эти сообщения. + Выбрать + Сообщения будут удалены для всех членов группы. + Сообщения будут помечены как удаленные для всех членов группы. + Контакт удален! + Разговор удален! + Член неактивен + Прямого соединения пока нет, сообщение переслано или будет переслано админом. + Ничего не выбрано + открыть + поиск + Выбрано %d + Настройки + Вы по-прежнему можете просмотреть разговор с %1$s в списке чатов. + Другие SMP серверы + Выключено + Установлено успешно + Установить обновление + Открыть расположение файла + Пожалуйста, перезапустите приложение. + Напомнить позже + Стабильная версия + Загрузка обновления отменена + Статус файла: %s + Данные чата экспортированы + Продолжить + Серверы файлов и медиа + Серверы сообщений + SOCKS прокси + Некоторые файл(ы) не были экспортированы + Вы можете мигрировать экспортированную базу данных. + Вы можете сохранить экспортированный архив. + выключен + неактивен + Пригласить + Статус сообщения + Контакт соединяется, подождите или проверьте позже! + Контакт удален. + Попросите Вашего контакта разрешить звонки. + Сохранить и переподключиться + Отправьте сообщение, чтобы включить звонки. + TCP-соединение + Чтобы включить звонки, разрешите их Вашему контакту. + Масштабирование + Эта ссылка была использована на другом мобильном, пожалуйста, создайте новую ссылку на компьютере. + Ошибки + Получено сообщений + Сообщений отправлено + Переподключить все подключенные серверы для устранения неполадок доставки сообщений. Это использует дополнительный трафик. + Переподключить все серверы + Переподключить сервер? + Переподключить серверы? + Сбросить всю статистику + Статистика + Вы не подключены к этим серверам. Для доставки сообщений на них используется конфиденциальная доставка. + Соединяйтесь с друзьями быстрее + Управляйте своей сетью + Защищает ваш IP адрес и соединения. + Открыть настройки серверов + Полученные сообщения + Ошибки приема + Архивируйте контакты чтобы продолжить переписку. + Отправлено напрямую + Отправлено через прокси + Транспортные сессии + Состояние соединения и серверов. + Удаляйте до 20 сообщений за раз. + Загрузка обновления, не закрывайте приложение. + Файл не найден - скорее всего, файл был удален или отменен. + Адрес пересылающего сервера несовместим с настройками сети: %1$s. + написать + Сообщение + Сообщение может быть доставлено позже, если член группы станет активным. + Сообщение переслано + Доступная панель чата + Всего получено + Пропустить эту версию + Чтобы получать уведомления об обновлениях, включите периодическую проверку стабильных или бета-версий. + Используйте приложение одной рукой. + Отправленные сообщения + Размыть для конфиденциальности. + Создать + Новые медиа-опции + Пригласить + Новое сообщение + Переключите список чатов: + Обновление приложения + Загружать новые версии из GitHub. + Увеличить размер шрифтов. + Новый интерфейс 🎉 + Открыть из списка чатов. + Сбросить все подсказки. + Вы можете изменить это в настройках Интерфейса. \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml index 5701c0ca78..058f0305a8 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/th/strings.xml @@ -46,7 +46,7 @@ ขอรับภาพ ขอรับวิดีโอ ข้อความทั้งหมดจะถูกลบ - การดำเนินการนี้ไม่สามารถยกเลิกได้! ข้อความจะถูกลบสำหรับคุณเท่านั้น - เพิ่มเซิร์ฟเวอร์… + เพิ่มเซิร์ฟเวอร์ เวอร์ชันแอป เวอร์ชันแอป: v%s ผู้ติดต่อทั้งหมดของคุณจะยังคงเชื่อมต่ออยู่. @@ -665,10 +665,7 @@ การตั้งค่าเครือข่าย จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ ไม่ - จำเป็นต้องมีโฮสต์หัวหอมสำหรับการเชื่อมต่อ - โฮสต์หัวหอมจะถูกใช้เมื่อมี โฮสต์หัวหอมจะไม่ถูกใช้ - โฮสต์หัวหอมจะไม่ถูกใช้ รหัสผ่านที่จะแสดง สายที่ไม่ได้รับ ผู้คนสามารถเชื่อมต่อกับคุณผ่านลิงก์ที่คุณแบ่งปันเท่านั้น @@ -852,10 +849,8 @@ บันทึกและอัปเดตโปรไฟล์กลุ่ม กำลังรับผ่าน รีเซ็ตเป็นค่าเริ่มต้น - เปลี่ยนกลับ บันทึก รีเซ็ตสี - บันทึกสี ได้รับ, ห้าม ผู้รับจะเห็นการอัปเดตเมื่อคุณพิมพ์ ลดการใช้แบตเตอรี่ @@ -1148,7 +1143,6 @@ ใช้พร็อกซี SOCKS ใช้การเชื่อมต่ออินเทอร์เน็ตโดยตรงหรือไม่\? ใช้พร็อกซี SOCKS หรือไม่\? - อัปเดตการตั้งค่าโฮสต์ .onion ไหม\? ใช้โฮสต์ .onion เมื่อพร้อมใช้งาน อัปเดตโหมดการแยกการขนส่งไหม\? diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml index 9ae8707c11..5413afa82b 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/tr/strings.xml @@ -38,7 +38,7 @@ Adres değişikliği iptal edilecek. Eski alıcı adresi kullanılacaktır. 1 dakika Tüm mesajlar silinecektir. Bu, geri alınamaz! Mesajlar, YALNIZCA senin için silinecektir. - Sunucu ekle… + Sunucu ekle Veri tabanı ayarları tek kullanımlık bağlantı Gelişmiş ağ ayarları @@ -87,7 +87,7 @@ Konuştuğunuz kişinin uygulamasından güvenlik kodunu okut. WebRTC ICE sunucu adreslerinin doğru formatta olduğundan emin olun: Satırlara ayrılmış ve yinelenmemiş şekilde. Kaydet - TEMA RENKLERİ + ARAYÜZ RENKLERİ Otomatik-kabul ayarlarını kaydet Ayarlar kaydedilsin mi? Kaydet ve konuştuğun kişilere bildir @@ -122,7 +122,6 @@ SimpleX Koyu tema Tema - Rengi kaydet Temayı içe aktar Temayı içe aktarırken hata oluştu Dosyanın doğru YAML sözdizimine sahip olduğundan emin olun. Tema dosyası yapısının bir örneğine sahip olmak için temayı dışa aktarın. @@ -150,7 +149,7 @@ Yetki Sesli mesajlara izin verilsin mi? Profil ekle - Üyelere direkt mesaj gönderilmesine izin ver. + Üyelere doğrudan mesaj gönderilmesine izin ver. Kendiliğinden yok olan mesajlar göndermeye izin ver. Gönderilen mesajların kalıcı olarak silinmesine izin ver. (24 saat içinde) Dosya ve medya göndermeye izin ver. @@ -506,7 +505,7 @@ Kendiliğinden şu sürede yok olacak Kendiliğinden şu sürede yok olacak: %s etkin - Direkt mesaj + Doğrudan mesajlar Devre dışı bırak Görünen ad, boşluk gibi aralıklama türleri içeremez. İsmini gir: @@ -530,7 +529,7 @@ %s üyesi için şifreleme kabul edildi doğrudan Yeniden gösterme - Bu grupta üyeler arası direkt mesajlar yasaklıdır. + Bu grupta üyeler arası doğrudan mesajlaşma yasaklıdır. konuşulan kişi için etkinleşti senin için etkinleştirildi %d sn @@ -653,7 +652,7 @@ Grup adını gir: Grup tam adı: Dosya ve medya - Grup üyeleri direkt mesaj gönderebilir. + Grup üyeleri doğrudan mesaj gönderebilir. Grup üyeleri, gönderilen mesajları kalıcı olarak silebilir. (24 saat içinde) Grup üyeleri sesli mesaj gönderebilirler. Bu toplu konuşmada, dosya ve medya yasaklanmıştır. @@ -1007,7 +1006,7 @@ Mesaj tepkilerini yasakla. Sesli mesaj göndermeyi yasakla. Geri alınamaz mesaj silme işlemini yasakla. - Üyelere direkt mesaj göndermeyi yasakla. + Üyelere doğrudan mesaj göndermeyi yasakla. Dosya ve medya göndermeyi yasakla. Canlı mesajlar Arayüz geliştirildi @@ -1146,7 +1145,6 @@ Bağlantı paylaş SimpleX Ekibi %s, %s ve %s bağlandı - Geri al SOCKS VEKİLİ Masaüstür cihazlar SMP sunucuları @@ -1216,7 +1214,7 @@ Önceki mesajın hash\'i farklı. SimpleX Kilit aktif değil! SimpleX Kilit - Direkt bağlanılsın mı? + Doğrudan bağlanılsın mı? Bu ayarlar mevcut profiliniz içindir Sunucu testi başarısız! Bağlantıyı onayla @@ -1231,7 +1229,7 @@ Bluetooth desteği ve diğer iyileştirmeler. Ayarlar AYARLAR - Bağlanmak için direkt mesaj gönderin + Bağlanmak için doğrudan mesaj gönderin Güvenlik kodu Daha hızlı gruplara katılma ve daha güvenilir mesajlar. Sohbet profiliniz grup üyelerine gönderilecek @@ -1319,14 +1317,12 @@ Kayıt %s te güncellendi Uygulama yeni yerel dosyaları şifreler (videolar dışında). %s , %s de - Bağlantı için Onion ana bilgisayarları gerekli olacaktır. Alıcılar devre dışı bırakılsın mı? Bağlantı yeniden senkronizasyonunu şifrele? Grup ayarlarından ve kişilerden geçersiz kılınmış olabilirler Yeni üyelere geçmiş gönderilmedi. Güvenlik değerlendirmesi Yeniden dene - Onion ana bilgisayarları mümkün olduğunda kullanılacaktır. Sunuculara %d bağlantı noktasındaki vekil SOCKS aracılığıyla erişilsin mi? Vekil bu seçeneği etkinleştirmeden önce başlatılmak zorundadır. İstenmeyen mesajları gizlemek için. Test %s adımında hata yaşandı. @@ -1363,7 +1359,6 @@ Ayarlardaki parola silinsin mi? Alıcılar etkinleştirilsin mi? Yeni bir sohbet başlatmak için tıkla - Onion ana bilgisayarları kullanılmayacaktır. Kişinin engelini kaldır Yönlendirici sunucusu sadece lazım ise kullanılacak. Diğer taraf IP adresini görebilir. %s ın bağlantısı kesildi]]> @@ -1404,7 +1399,7 @@ Yapıştırdığın bağlantı bir SimpleX bağlantısı değil. Gönderilmiş mesaj Gruplar için alıcılar devre dışı bırakılsın mı? - Aktarıcı sunucu IP adresinizi korur, ancak aramanın süresini gözlemleyebilir. + Yönlendirici sunucu IP adresinizi korur, ancak aramanın süresini gözlemleyebilir. Dosya yükleniyor Masaüstüne bağlanıyor Eklenecek kişi yok @@ -1511,7 +1506,6 @@ Telefona bağlandı Bağlanırken takma ada geçiş yap. Mesaj gönderildi! - .onion ana bilgisayarları ayarı güncellensin mi? Yeni bağlantılar için kullan senin için adres değiştirildi SOCKS vekilini kullan? @@ -1694,7 +1688,7 @@ Uyarı: Birden fazla cihazda sohbet başlatmak desteklenmez ve mesaj iletimi başarısızlıklara neden olabilir. Veritabanı parolasını doğrulayın Parolayı doğrulayın - Tüm kişileriniz, konuşmalarınız ve dosyalarınız güvenli bir şekilde şifrelenir ve yapılandırılmış XFTP rölelerine parçalar halinde yüklenir. + Tüm kişileriniz, konuşmalarınız ve dosyalarınız güvenli bir şekilde şifrelenir ve yapılandırılmış XFTP yönlendiricilerine parçalar halinde yüklenir. Arşivle ve yükle Uyarı: arşiv silinecektir.]]> Taşımak için veritabanı parolasını hatırladığınızı doğrulayın. @@ -1770,4 +1764,94 @@ Profil resimleri Profil resimlerini şekillendir Kare,daire, veya aralarında herhangi bir şey. + Kapasite aşıldı - alıcı önceden gönderilen mesajları almadı. + Hedef sunucu hatası: %1$s + Hata: %1$s + Yönlendirme sunucusu: %1$s +\nHedef sunucu hatası: %2$s + Yönlendirme sunucusu: %1$s +\nHata: %2$s + Mesaj iletimi uyarısı + Ağ sorunları - birçok gönderme denemesinden sonra mesajın süresi doldu. + Sunucu adresi ağ ayarlarıyla uyumlu değil. + Sunucu sürümü ağ ayarlarıyla uyumlu değil. + Yanlış anahtar veya bilinmeyen bağlantı - büyük olasılıkla bu bağlantı silinmiştir. + Gizli yönlendirme + Bilinmeyen yönlendiriciler + Her zaman gizli yönlendirmeyi kullan. + Gizli yönlendirmeyi KULLANMA. + Mesaj yönlendirme modu + Hiçbir zaman + Bilinmeyen sunucularla gizli yönlendirme kullan. + Sürüm düşürmeye izin ver + Hayır + Mesaj yönlendirme yedeklemesi + Her zaman + Sizin veya hedef sunucunun özel yönlendirmeyi desteklememesi durumunda bile mesajları doğrudan GÖNDERMEYİN. + IP adresi korumalı olduğunda ve sizin veya hedef sunucunun özel yönlendirmeyi desteklemediği durumlarda mesajları doğrudan gönderin. + Sizin veya hedef sunucunun özel yönlendirmeyi desteklemediği durumlarda mesajları doğrudan gönderin. + GİZLİ MESAJ YÖNLENDİRME + Mesaj durumunu göster + IP adresinizi korumak için,gizli yönlendirme mesajları iletmek için SMP sunucularınızı kullanır. + Korumasız + IP adresi korunmadığında bilinmeyen sunucularla gizli yönlendirme kullan. + IP gizliyken + Evet + Sohbet teması + Profil teması + Siyah + Renk modu + Karanlık mod renkleri + Aydınlık + Sistem + Ek vurgu 2 + Bütün renk modları + Şuna uygula + Karanlık mod + Doldur + Ölçeklendir + Gönderilen cevap + Varsayılan temaya ayarla + Gelişmiş ayarlar + Günaydın! + Karanlık + Aydınlık mod + IP adresini koru + DOSYALAR + Sohbet renkleri + Sığdır + Alınan cevap + İyi öğlenler! + Resmi kaldır + Tekrarla + Rengi sıfırla + Sohbet listesini yeni pencerede göster + Bilinmeyen sunucular! + Tor veya VPN olmadan, IP adresiniz bu XFTP yönlendiricileri tarafından görülebilir: +\n%1$s. + Tor veya VPN olmadan, IP adresiniz dosya sunucularına görülebilir. + Duvar kağıdı vurgusu + Duvar kağıdı arkaplanı + Uygulama, bilinmeyen dosya sunucularından indirmeleri onaylamanızı isteyecektir (.onion veya SOCKS vekilleri etkin değilse). + WebView başlatılırken hata oluştu. Sisteminizi yeni sürüme güncelleyin. Lütfen geliştiricilerle iletişime geçin. +\nHata: %s + Sohbetlerinizin farklı görünmesini sağlayın! + Farsça Arayüz + Kullanıcı temasına sıfırla + IP adresinizi kişileriniz tarafından seçilen mesajlaşma yönlendiricilerinden koruyun. +\n*Ağ ve sunucular* ayarlarında etkinleştirin. + Gizli mesaj yönlendirme 🚀 + Bilinmeyen sunuculardan gelen dosyaları onayla. + Geliştirilmiş mesaj iletimi + Yeni sohbet temaları + Dosyaları güvenle alın + Azaltılmış pil kullanımı ile. + Uygulama teması + Uygulama temasına sıfırla + Hata ayıklama teslimatı + Mesaj kuyruğu bilgisi + hiçbiri + sunucu kuyruk bilgisi: %1$s +\n +\nson alınan msj: %2$s \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml index 948f91b89a..ee77905611 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/uk/strings.xml @@ -55,7 +55,7 @@ Після перезапуску додатка або зміни ключової фрази буде використано сховище ключів Android для безпечного збереження ключової фрази - це дозволить отримувати сповіщення. Дозвольте вашим контактам надсилати голосові повідомлення. Прийняти інкогніто - Додати сервер… + Додати сервер адміністратор Додати привітання Всі члени групи залишаться підключеними. @@ -80,7 +80,7 @@ Активована оптимізація батареї, вимикається фоновий сервіс і періодичні запити нових повідомлень. Ви можете знову увімкнути їх у налаштуваннях. Назад жирний - Добре для акумулятора. Сервіс фонового запуску перевіряє повідомлення кожні 10 хвилин. Ви можете пропустити виклики чи важливі повідомлення.]]> + Добре для акумулятора. Додаток перевіряє повідомлення кожні 10 хвилин. Ви можете пропустити виклики чи важливі повідомлення.]]> Аудіо та відеовиклики Аудіо вимкнено Аутентифікація недоступна @@ -248,10 +248,9 @@ Збережені сервери WebRTC ICE будуть видалені. Налаштувати сервери ICE Мережа та сервери - Налаштування мережі + Розширені налаштування Використовувати SOCKS-проксі? Використовувати прямий підключення до Інтернету? - Оновити налаштування .onion-хостів? Використовувати .onion-хости Якщо доступно Ні @@ -267,7 +266,7 @@ Введіть своє ім\'я: дзвінок в процесі запуск… - Перша платформа без ідентифікаторів користувачів – приватна за конструкцією. + Ніяких ідентифікаторів користувачів. Децентралізована Використовувати чат Це можна змінити пізніше в налаштуваннях. @@ -412,7 +411,7 @@ Ім\'я профілю: очікування підтвердження… Приватність перевизначена - Люди можуть підключатися до вас лише за допомогою посилань, які ви надаєте. + Ви вирішуєте, хто може під\'єднатися. Як працює SimpleX Докладніше читайте в нашому репозиторії на GitHub. зашифрований e2e аудіовиклик @@ -450,7 +449,6 @@ Змінити роль у групі\? Помилка при вилученні учасника Ваш профіль чату буде відправлений учасникам групи - Зберегти колір Видалення для всіх Голосові повідомлення Голосові повідомлення заборонені в цьому чаті. @@ -641,8 +639,6 @@ Помилка збереження серверів ICE .Onion-хости будуть обов\'язковими для підключення. \nЗверніть увагу: ви не зможете підключитися до серверів без адреси .onion. - Хости .onion будуть використовуватися, якщо доступні. - Хости .onion будуть обов\'язковими для підключення. Показати параметри розробника Ідентифікатори бази даних та опція ізоляції транспорту. Сповіщення перестануть працювати, поки ви не перезапустите додаток @@ -663,7 +659,7 @@ отримувати повідомлення, ваші контакти – сервери, які ви використовуєте для надсилання повідомлень їм.]]> шифрування на двох рівнях.]]> Приватні сповіщення - Споживає більше заряду акумулятора! Фоновий сервіс завжди працює – сповіщення відображаються, як тільки повідомлення доступні.]]> + Споживає більше акумулятора! Додаток завжди працює у фоновому режимі – сповіщення відображаються миттєво.]]> Вставте отримане посилання Відео вимкнено Завершено виклик @@ -742,7 +738,7 @@ Показати профіль Показати профіль чату Коли ви ділитесь анонімним профілем з кимось, цей профіль буде використовуватися для груп, до яких вас запрошують. - Світла + Світлий Помилка імпорту теми Налаштування групи Повідомлення зникнення @@ -954,7 +950,6 @@ зображення попереднього перегляду посилання скасувати попередній перегляд посилання Налаштування - Хости .onion не будуть використовуватися. Виклик у процесі Помилка бази даних Відновити @@ -998,7 +993,6 @@ ДЛЯ КОНСОЛІ Учасника буде вилучено з групи - цю дію неможливо скасувати! Змінити роль - Відновити Ви все ще отримуватимете дзвінки та сповіщення від приглушених профілів, коли вони активні. %d місяці %dmth @@ -1038,7 +1032,7 @@ Хост Порт Обов\'язково - КОЛОРИ ТЕМИ + КОЛЬОРИ ІНТЕРФЕЙСУ Створіть адресу, щоб дозволити людям підключатися до вас. Ваші контакти залишаться підключеними. Створити SimpleX-адресу @@ -1059,7 +1053,7 @@ підключення… підключено завершено - Стійка до спаму та зловживань + Стійкий до спаму %1$s хоче підключитися до вас через зашифрований e2e відеовиклик Ігнорувати @@ -1178,8 +1172,9 @@ кольоровий дзвінок завершено %1$s помилка дзвінка - Наступне покоління приватного обміну повідомленнями - Відкритий протокол та код – кожен може запустити сервери. + Наступне покоління +\nприватних повідомлень + Кожен може хостити сервери. Інструменти розробника Експериментальні функції ДЗВІНКИ @@ -1240,7 +1235,7 @@ Використовувати .onion-хости на Ні, якщо SOCKS-проксі їх не підтримує.]]> Показати: Сховати: - Під час імпорту сталися деякі невідновні помилки - ви можете переглянути консоль чату для отримання більше деталей. + Під час імпорту відбулися деякі непередбачувані помилки: Цю дію неможливо відмінити - будуть видалені повідомлення, відправлені та отримані раніше вибраного часу. Це може зайняти декілька хвилин. Вам потрібно вводити ключову фразу кожен раз при запуску додатка - вона не зберігається на пристрої. Змінити ключову фразу бази даних? @@ -1664,7 +1659,7 @@ Імпорт архіву Повторний імпорт Завершіть міграцію на іншому пристрої. - Подати заявку + Застосувати Перенести пристрій Перехід на інший пристрій Помилка експорту бази даних чату @@ -1713,4 +1708,363 @@ Використовуйте додаток під час розмови. Підтвердіть парольну фразу Ви можете спробувати ще раз. + Перевищено ліміт - одержувач не отримав раніше надіслані повідомлення. + Помилка сервера призначення: %1$s + Помилка: %1$s + Попередження про доставку повідомлення + Проблеми з мережею - термін дії повідомлення закінчився після багатьох спроб надіслати його. + Сервер переадресації: %1$s +\nПомилка сервера призначення: %2$s + Сервер переадресації: %1$s +\nПомилка: %2$s + Мікрофон + Джерело повідомлення залишається приватним. + Завжди + Завжди використовуйте приватну маршрутизацію. + Режим маршрутизації повідомлень + НЕ використовуйте приватну маршрутизацію. + Ні + НЕ надсилайте повідомлення напряму, навіть якщо ваш сервер або сервер призначення не підтримує приватну маршрутизацію. + Камера + Надати в налаштуваннях + Навушники + Стільниковий + власники + Більш надійне з\'єднання з мережею. + Керування мережею + Пересилання та збереження повідомлень + Переадресувати повідомлення… + Дозволити зниження рейтингу + Тема програми + Темний режим + Завантажити + Навушник + Увімкнено для + Повідомлення про помилку, зв\'яжіться з розробниками. + Файли та медіафайли заборонені + Знайдіть цей дозвіл у налаштуваннях Android і надайте його вручну. + Переслати + Переслано + Переслано з + Учасники групи можуть надсилати посилання SimpleX. + Звуки вхідного дзвінка + Світлий режим + Запасний варіант маршрутизації повідомлень + МАРШРУТИЗАЦІЯ ПРИВАТНИХ ПОВІДОМЛЕНЬ + переслано + Інше + Дозволити надсилати посилання SimpleX. + Заборонити надсилання посилань SimpleX + Немає підключення до мережі + Ніколи + Приватна маршрутизація + Bluetooth + Камера та мікрофон + Надайте дозвіл(и) на здійснення дзвінків + Відкрити налаштування + ФАЙЛИ + Зображення профілю + Підключення до мережі + адміністратори + всі учасники + Литовський інтерфейс + Надавати дозволи + Кольори чату + Тема чату + Тема профілю + Темна + Додатковий акцент 2 + Розширені налаштування + Усі кольорові режими + Застосувати до + Колірний режим + Темна + Кольори темного режиму + Заповнити + Підходить + Доброго дня! + Доброго ранку! + Світлий + Видалити зображення + Помилка ініціалізації WebView. Оновіть систему до нової версії. Зверніться до розробників. +\nПомилка: %s + Підтвердити файли з невідомих серверів. + Покращена доставка повідомлень + Перський інтерфейс + Маршрутизація приватних повідомлень 🚀 + Захист IP-адреси + Захистіть свою IP-адресу від ретрансляторів повідомлень, обраних вашими контактами. +\nУвімкніть у налаштуваннях *Мережа та сервери*. + Отримано відповідь + Збережено + Програма попросить підтвердити завантаження з невідомих файлових серверів (крім .onion або коли ввімкнено SOCKS-проксі). + Надсилайте повідомлення напряму, якщо IP-адреса захищена, а ваш сервер або сервер призначення не підтримує приватну маршрутизацію. + Надсилайте повідомлення напряму, якщо ваш сервер або сервер призначення не підтримує приватну маршрутизацію. + Встановлення теми за замовчуванням + Надіслано відповідь + Показати список чату в новому вікні + Використовуйте приватну маршрутизацію з невідомими серверами. + Використовуйте приватну маршрутизацію з невідомими серверами, якщо IP-адреса не захищена. + Фон шпалер + Акцент на шпалерах + Повторити + Масштаб + Нехай ваші чати виглядають інакше! + Нові теми чату + Безпечне отримання файлів + З меншим споживанням заряду акумулятора. + Неправильний ключ або невідоме з\'єднання - швидше за все, це з\'єднання видалено. + Адреса сервера несумісна з налаштуваннями мережі. + Голосові повідомлення заборонені + WiFi + Спікер + Повернутися до теми програми + Повернутися до теми користувача + Посилання SimpleX + Квадрат, коло або щось середнє між ними. + Буде ввімкнено в прямих чатах! + збережено + збережено з %s + Дротова мережа Ethernet + Невідомі сервери! + Без Tor або VPN ваша IP-адреса буде видимою для цих XFTP-ретрансляторів: +\n%1$s. + Одержувач(и) не бачить, від кого це повідомлення. + Збережено з + Серверна версія несумісна з мережевими налаштуваннями. + Посилання SimpleX заборонені + Показати статус повідомлення + Щоб захистити вашу IP-адресу, приватна маршрутизація використовує ваші SMP-сервери для доставки повідомлень. + Невідомі сервери + Незахищений + Коли IP приховано + Так + Отримання паралелізму + У цій групі заборонені посилання на SimpleX. + Сформуйте зображення профілю + При підключенні аудіо та відеодзвінків. + Скинути колір + Система + Без Tor або VPN ваша IP-адреса буде видимою для файлових серверів. + немає + Доставка налагодження + Інформація про чергу повідомлень + інформація про чергу на сервері: %1$s +\n +\nостаннє отримане повідомлення: %2$s + Адреса сервера призначення %1$s несумісна з налаштуваннями сервера переадресації %2$s. + Файл не знайдено — ймовірно, файл був видалений або скасований. + Сканувати / Вставити посилання + Налаштовані XFTP сервери + Бета + Статус файлу + Повідомлення надіслано + Статистика + Попередньо підключені сервери + Помилковий ключ або невідома адреса чанка файлу - найбільш імовірно, що файл було видалено. + Недійсне посилання + Будь ласка, перевірте, чи правильне посилання SimpleX. + Адреса сервера несумісна з налаштуваннями мережі: %1$s. + Помилка підключення до сервера переадресації %1$s. Будь ласка, спробуйте пізніше. + Адреса сервера переадресації несумісна з налаштуваннями мережі: %1$s. + Версія сервера переадресації несумісна з налаштуваннями мережі: %1$s. + Помилка приватного маршрутизації + Версія сервера призначення %1$s несумісна з сервером переадресації %2$s. + Будь ласка, спробуйте пізніше. + Помилка сервера файлів: %1$s + Учасник неактивний + Повідомлення переслано + Поки що немає прямого з\'єднання, повідомлення пересилається адміністратором. + Вибрано %d + Повідомлення + Помилка файлу + Тимчасова помилка файлу + Контакт буде видалено - це неможливо скасувати! + Видалити лише розмову + Архівовані контакти + Нагадати пізніше + Стабільна + Новий досвід чату 🎉 + Адреса сервера + Підтвердити видалення контакту? + підключитися + Сервер переадресації %1$s не зміг з\'єднатися з цільовим сервером %2$s. Будь ласка, спробуйте пізніше. + Повідомлення може бути доставлено пізніше, якщо учасник стане активним. + Нічого не вибрано + Відкрити налаштування сервера + Версія сервера несумісна з вашим додатком: %1$s. + Повідомлення будуть позначені як модеровані для всіх учасників. + Оновлювати додаток автоматично + Не вдалося надіслати повідомлення + Вибрані налаштування чату забороняють це повідомлення. + Статус файлу: %s + Статус повідомлення: %s + Нове повідомлення + Створити + Запросити + Перемикнути список чатів: + Ви можете змінити це в налаштуваннях зовнішнього вигляду. + Статус повідомлення + Архівувати контакти, щоб поговорити пізніше. + Відтворити зі списку чатів. + Нові опції медіа + Розмиття для кращої конфіденційності. + Збільшити розмір шрифту. + Він захищає вашу IP-адресу та з\'єднання. + Перевірити, чи є оновлення + Завантажуйте нові версії з GitHub. + Дозволити дзвінки? + Контакт видалено. + TCP з\'єднання + Масштабування + Розмір шрифту + Будь ласка, попросіть вашого контакту увімкнути дзвінки. + Зберегти і перепідключитися + Видалити до 20 повідомлень за один раз. + Доступна панель інструментів чату + Користуватися застосунком однією рукою. + З\'єднуйтеся з друзями швидше. + Керуйте своєю мережею + Завантажено + Помилка скидання статистики + Перепідключити сервери? + Надіслані повідомлення + Статистика серверів буде скинута — це не можна буде відмінити! + Статус з\'єднання та серверів. + Докладна статистика + Отримати помилки + Надіслано безпосередньо + Надіслано загалом + Надіслано через проксі + Сервер SMP + Починаючи з %s. + Отримані повідомлення + Отримано загалом + Розмити медіа + Середній + Вимкнено + Слабке + Сильна + вимкнено + неактивний + Інформація про сервери + Файли + Ніякої інформації, спробуйте перезавантажити + Активні з\'єднання + Підключено + Підключені сервери + Підключення + Всього + Сесії передачі даних + Ви не підключені до цих серверів. Для доставки повідомлень до них використовується приватна маршрутизація. + Поточний профіль + Деталі + Помилки + Отримання повідомлення + Повідомлення отримано + В очікуванні + Проксіровані сервери + Показувати інформацію для + Починаючи з %s. +\nВсі дані зберігаються лише на вашому пристрою. + Перепідключити сервер для примусової доставки повідомлень. Це використовує додатковий трафік. + Скинути всю статистику + Скинути всю статистику? + Помилка + Помилка повторного підключення до сервера + Помилка повторного підключення до серверів + Перепідключити всі підключені сервери для примусової доставки повідомлень. Це використовує додатковий трафік. + Перепідключити всі сервери + Перепідключити сервер? + Помилки підтвердження + дублікати + інші помилки + Підтверджено + Підключення + помилки розшифрування + Видалено + Помилки підписки + Помилки видалення + Розмір + Підписано + Підписки проігноровані + Завантажені файли + Помилки завантаження + Завантажені файли + Помилки завантаження + Частини видалені + Частини завантажено + Частини завантажено + Ця посилання було використано на іншому мобільному пристрої, створіть нове посилання на комп\'ютері. + Помилка копіювання + Будь ласка, перевірте, що мобільний пристрій і комп\'ютер підключені до однієї локальної мережі, і що брандмауер комп\'ютера дозволяє з\'єднання. +\nБудь ласка, повідомте про будь-які інші проблеми розробникам. + Налаштування + відеодзвінок + повідомлення + дзвінок + Зберегти розмову + відкрити + пошук + Ви все ще можете переглядати розмову з %1$s у списку чатів. + Контакт видалено! + Розмову видалено! + Видалити без сповіщення + Ви можете надсилати повідомлення %1$s з архівованих контактів. + Налаштовані SMP сервери + Ніяких відфільтрованих контактів + Інші SMP сервери + Ваші контакти + Інші XFTP сервери + Показати відсоток + Перевірити оновлення + Вимкнено + Завантаження оновлення додатку, не закривайте додаток + Завантажити %s (%s) + Відкрити розташування файлу + Пропустити цю версію + Доступна панель інструментів чату + Не можна зателефонувати контакту + Підключення до контакту, будь ласка, зачекайте або перевірте пізніше! + Дзвінки заборонені! + Вам необхідно дозволити контакту викликати вас, щоб ви могли самі їм дзвонити. + Надіслати повідомлення, щоб увімкнути дзвінки. + Не можна зателефонувати учаснику групи + Не можна надіслати повідомлення учаснику групи + спроби + XFTP сервер + Перепідключитися + Створено + закінчився + Захищений + інший + Проксірований + Надіслати помилки + Завершено + Всі профілі + Скинути + Завантажено + Видалити %d повідомлень учасників? + Повідомлення будуть позначені для видалення. Одержувач(і) зможуть розкрити ці повідомлення. + Вибрати + Повідомлення будуть видалені для всіх учасників. + Вставити посилання + Вимкнути + Щоб отримувати повідомлення про нові випуски, увімкніть періодичну перевірку стабільної або бета-версії. + Продовжити + База даних чату експортована + Медіа та файлові сервери + Сервери повідомлень + SOCKS проксі + Деякі файли не були експортовані + Ви можете переїхати експортовану базу даних. + Ви можете зберегти експортований архів. + Запросити + Оновлення додатку завантажено + Встановлено успішно + Встановити оновлення + Будь ласка, перезапустіть додаток. + Скинути всі підказки + Доступно оновлення: %s + Завантаження оновлення скасовано \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml index 939071da2b..cd34f2efff 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/vi/strings.xml @@ -10,7 +10,7 @@ liên kết dùng một lần Thêm liên hệ Thông tin về SimpleX Chat - Thêm máy chủ… + Thêm máy chủ Thông tin về SimpleX quản trị viên Thêm lời chào @@ -24,7 +24,7 @@ Chấp nhận Thêm máy chủ bằng cách quét mã QR. Cài đặt mạng nâng cao - Tất cả các cuộc hội thoại và tin nhắn sẽ bị xóa - quá trình này không thể được hoàn tác! + Tất cả các cuộc trò chuyện và tin nhắn sẽ bị xóa - quá trình này không thể được hoàn tác! Hủy bỏ việc đổi địa chỉ? Việc thay đổi địa chỉ sẽ bị hủy bỏ. Địa chỉ nhận cũ tiếp tục được sử dụng. 30 giây @@ -96,12 +96,12 @@ ỨNG DỤNG Di chuyển dữ liệu ứng dụng Sao lưu dữ liệu ứng dụng - Mật mã ứng dụng đã được thay thế bằng mật mã tự hủy. + Mã truy cập ứng dụng đã được thay thế bằng mã tự hủy. Ứng dụng mã hóa các tệp cục bộ mới (trừ video). Áp dụng - Mật mã ứng dụng + Mã truy cập ứng dụng BIỂU TƯỢNG ỨNG DỤNG - Mật mã ứng dụng + Mã truy cập Phiên bản ứng dụng: v%s Phiên bản ứng dụng Tiếng Ả Rập, tiếng Bungari, tiếng Phần Lan, tiếng Do Thái, tiếng Thái và tiếng Ukraina - trân thành gửi lời cảm ơn tới các tình nguyện viên dịch thuật và Weblate. @@ -171,5 +171,557 @@ Cả bạn và liên hệ của bạn đều có thể gửi tin nhắn tự xóa. Cả bạn và liên hệ của bạn đều có thể thả cảm xúc tin nhắn. Xin lưu ý: relay tin nhắn và tệp được kết nối thông qua SOCKS proxy. Các cuộc gọi và bản xem trước liên kết sử dụng kết nối trực tiếp.]]> - Tốt cho pin. Dịch vụ nền kiểm tra tin nhắn 10 phút một lần. Bạn có thể bỏ lỡ các cuộc gọi hoặc tin nhắn khẩn cấp.]]> + Tốt cho pin. Ứng dụng kiểm tra tin nhắn 10 phút một lần. Bạn có thể bỏ lỡ các cuộc gọi hoặc tin nhắn khẩn cấp.]]> + Luôn luôn + đang gọi… + Theo hồ sơ trò chuyện (mặc định) hoặc theo kết nối (BETA). + cuộc gọi đang chờ + Sử dụng nhiều pin hơn! Ứng dụng luôn luôn chạy - thông báo sẽ được hiển thị ngay lập tức.]]> + Cảnh báo: kho lưu trữ sẽ bị xóa.]]> + Cuộc gọi đang chờ + Cho phép hạ cấp + Cuộc gọi đã kết thúc! + Luôn luôn sử dụng định tuyến riêng tư. + Cuộc gọi kết thúc + cuộc gọi kết thúc %1$s + lỗi cuộc gọi + CUỘC GỌI + Hủy xem trước ảnh + Hủy xem trước tệp + Hủy + Camera + Camera hiện đang bận + Camera + Cuộc gọi trên màn hình khóa: + Đen + Áp dụng cho + Biến thể của màu sơ cấp 2 + Cài đặt nâng cao + Tất cả chế độ màu + Camera và mic + Không thể mời liên hệ! + Không thể truy cập Keystore để lưu mật khẩu cơ sở dữ liệu + hủy bỏ xem trước liên kết + Hủy bỏ di chuyển + Chủ đề ứng dụng + Không thể nhận tệp + Không thể khởi tạo cơ sở dữ liệu + đã hủy bỏ %s + Không thể mời liên hệ! + Hủy bỏ tin nhắn động + đã thay đổi quyền hạn của %s thành %s + Dung lượng lưu trữ vượt quá giới hạn - người nhận không thể nhận được tin nhắn vừa gửi trước đó. + đã thay đổi địa chỉ cho bạn + Thay đổi mật khẩu cơ sở dữ liệu? + đã thay đổi quyền hạn của bạn thành %s + Thay đổi chế độ khóa + Di động + Đổi mật khẩu + Thay đổi quyền hạn của nhóm? + Thay đổi + Thay đổi địa chỉ nhận? + Thay đổi địa chỉ nhận + Thay đổi chế độ tự hủy + Thay đổi mã tự hủy + Thay đổi quyền hạn + đang thay đổi địa chỉ… + đang thay đổi địa chỉ… + KHO LƯU TRỮ SIMPLEX CHAT + Kho lưu trữ SimpleX Chat + Bảng điều khiển trò chuyện + Ứng dụng SimpleX Chat đang hoạt động + Cơ sở dữ liệu SimpleX Chat đã bị xóa + Ứng dụng SimpleX Chat đã được dừng lại. Nếu bạn đã sử dụng cơ sở dữ liệu này trên một thiết bị khác, bạn nên chuyển nó trở lại trước khi bắt đầu ứng dụng. + Cơ sở dữ liệu SimpleX Chat đã được nhập + Ứng dụng SimpleX Chat đã được dừng lại + đang thay đổi địa chỉ cho %s… + Tùy chọn trò chuyện + Màu trò chuyện + CƠ SỞ DỮ LIỆU SIMPLEX CHAT + Ứng dụng SimpleX Chat đã được dừng lại + Cơ sở dữ liệu đã được di chuyển! + Các cuộc trò chuyện + CÁC CUỘC TRÒ CHUYỆN + Kiểm tra tin nhắn mới mỗi 10 phút trong tối đa 1 phút + Giao diện Trung Quốc và Tây Ban Nha + Trò chuyện với nhà phát triển + Kiểm tra lại địa chỉ server và thử lại. + Chọn một tệp + Hồ sơ trò chuyện + Kiểm tra kết nối internet của bạn và thử lại + Chủ đề trò chuyện + Chế độ màu + Xác minh xóa + Xóa + Nút đóng + Xóa ghi chú riêng tư? + có màu + Xóa cuộc trò chuyện + Xóa + Sắp ra mắt! + Di chuyển từ một thiết bị kháctrên thiết bị mới và quét mã QR.]]> + Xóa + Xóa cuộc trò chuyện? + Cài đặt cấu hình cho các máy chủ ICE + Xác nhận các cài đặt mạng + Xác nhận mã truy cập + hoàn thành + Xác nhận mật khẩu + Xác nhận nâng cấp cơ sở dữ liệu + So sánh mã bảo mật với liên hệ của bạn. + Xác nhận + So sánh tệp + Xác nhận các tệp từ những máy chủ không xác định. + Xác nhận mật khẩu mới… + Xác nhận tải lên + Xác nhận rằng bạn nhớ mật khẩu cơ sở dữ liệu để chuyển nó đi. + Kết nối + đã kết nối + Đã kết nối + đã kết nối + Kết nối trực tiếp? + Kết nối + đã kết nối + Tự động kết nối + đã kết nối + Đã kết nối + Xác nhận thông tin đăng nhập của bạn + Kết nối + Máy tính đã được kết nối + đã kết nối trực tiếp + đang kết nối (đã được chấp nhận) + đang kết nối… + Đã kết nối tới điện thoại + Điện thoại đã được kết nối + đang kết nối + đang kết nối… + đang kết nối… + Kết nối ẩn danh + đang kết nối + Đã kết nối tới máy tính + đang kết nối (đã được thông báo) + đang kết nối… + đang kết nối (đã được giới thiệu) + đang kết nối (lời mời giới thiệu) + Kết nối tới máy tính đang ở trong tình trạng không tốt + Kết nối đã bị ngắt + Kết nối thông qua liên kết / mã QR + Kết nối tới chính bạn? + Yêu cầu kết nối đã được gửi! + Kết nối thông qua liên kết + Đang kết nối cuộc gọi + Kết nối đã bị ngắt + Đang kết nối tới máy tính + - kết nối tới dịch vụ thư mục (BETA)! +\n- đánh dấu đã nhận (tối đa 20 thành viên). +\n- nhanh hơn và ổn định hơn. + Kết nối đã bị ngắt + %s đang ở trong tình trạng không tốt]]> + Kết nối tới máy tính + Hết thời gian chờ kết nối + Lỗi kết nối + Kết nối + Kết nối thông qua liên kết? + kết nối %1$d + kết nối đã được tạo lập + Kết nối với %1$s? + Lỗi kết nối (AUTH) + Kết nối + đang kết nối cuộc gọi… + Kết nối thông qua địa chỉ liên lạc? + Kết nối thông qua liên kết dùng một lần? + Liên hệ đã được kiểm tra + Các liên hệ + Liên hệ có cho phép + Liên hệ đã tồn tại + liện hệ %1$s đã thay đổi thành %2$s + Liên hệ này vẫn chưa được kết nối! + Liên hệ ẩn: + Liên hệ và tất cả tin nhắn sẽ bị xóa - quá trình này không thể hoàn tác được! + Tên liên hệ + liên hệ có bảo mật đầu cuối + liên hệ không có bảo mật đầu cuối + Tùy chọn liên hệ + Sao chép + Tạo + Liên hệ có thể đánh dấu tin nhắn để xóa; bạn vẫn sẽ có thể xem được chúng. + Biểu tượng ngữ cảnh + Tiếp tục + Đóng góp + Đã sao chép vào bộ nhớ đệm + Phiên bản lõi: v%s + Sửa tên thành %s? + Sao chép lỗi + Tạo nhóm bằng một hồ sơ ngẫu nhiên. + Tạo hồ sơ mới trong ứng dụng trên máy tính. 💻 + Không thể gửi tin nhắn + Tạo tệp + Tạo một địa chỉ để cho mọi người kết nối với bạn. + Tạo liên kết nhóm + Tạo liên kết + Được tạo ra tại + Tạo địa chỉ + Được tạo ra vào %1$s + Được tạo ra tại: %s + Tạo hồ sơ trò chuyện + Tạo nhóm + Chủ đề tối + Mật khẩu hiện tại… + Cơ sở dữ liệu đã được mã hóa! + Mật khẩu mã hóa cơ sở dữ liệu sẽ đượ cập nhật và lưu trữ trong Keystore. + (hiện tại) + Tối + Các màu chế độ tối + Tối + Lỗi nghiêm trọng + Tạo liên kết lời mời dùng một lần + Đang tạo liên kết… + Tạo hồ sơ + Tùy chỉnh và chia sẻ các chủ đề màu sắc. + Các chủ đề tùy chỉnh + Kích cỡ tệp hiện đang được hỗ trợ tối đa là %1$s. + Tạo hồ sơ của bạn + Người sáng tạo + Tạo nhóm bí mật + Mã truy cập hiện tại + Tạo nhóm bí mật + Mật khẩu mã hóa cơ sở dữ liệu sẽ được cập nhật và lưu trữ trong cài đặt. + Thời lượng tùy chỉnh + Mật khẩu mã hóa cơ sở dữ liệu sẽ được cập nhật. + Tùy chỉnh chủ đề + Tạo địa chỉ SimpleX + Tạo hồ sơ + Tạo hàng đợi + Tạo liên kết lưu trữ + tùy chỉnh + Chủ đề tối + Hạ cấp cơ sở dữ liệu + %d liên hệ đã được chọn + ID cơ sở dữ liệu: %d + Cơ sở dữ liệu sẽ được mã hóa và mật khẩu thì được lưu trữ trong Keystore. + Nâng cấp cơ sở dữ liệu + Phiên bản cơ sở dữ liệu mới hơn so với ứng dụng, nhưng không có hạ cấp cho: %s + ID cơ sở dữ liệu + ngày + Cơ sở dữ liệu được mã hóa bằng một mật khẩu ngẫu nhiên. Vui lòng đổi mật khẩu trước khi xuất dữ liệu. + Cơ sở dữ liệu được mã hóa bằng một mật khẩu ngẫu nhiên, bạn có thể thay đổi nó. + Mật khẩu cơ sở dữ liệu + Cơ sở dữ liệu sẽ được mã hóa và mật khẩu thì được lưu trữ trong phần cài đặt. + %d ngày + Các ID cơ sở dữ liệu và tùy chọn cách ly truyền tải. + %dd + %d ngày + Cơ sở dữ liệu sẽ được mã hóa. + Việc di chuyển cơ sở dữ liệu đang diễn ra. +\nQuá trình này có thể mất một vài phút. + Mật khẩu cơ sở dữ liệu và xuất dữ liệu + Mật khẩu cơ sở dữ liệu khác với mật khẩu được lưu trong Keystore. + Mật khẩu cơ sở dữ liệu là cần thiết để mở ứng dụng SimpleX Chat. + Lỗi cơ sở dữ liệu + Xóa + Xóa địa chỉ? + Xóa hồ sơ trò chuyện? + Xóa %d tin nhắn? + Xóa địa chỉ + Phi tập trung + Xóa + Xóa cơ sở dữ liệu khỏi thiết bị này + Xóa sau + đã xóa liên hệ + Gỡ lỗi truyền tải + Đã xóa vào: %s + Lỗi chuyển đổi + Xóa và thông báo tới liên hệ + Xóa liên hệ + Xóa + Xóa cơ sở dữ liệu + Xóa tất cả các tệp + Xóa tệp và đa phương tiện? + Xóa kho lữu trữ + mặc định (%s) + Xóa kho lữu trữ SimpleX Chat? + đã xóa nhóm + Xóa tệp + Xóa liên hệ? + Đã xóa vào + Lỗi giải mã + đã xóa + Xóa hồ sơ trò chuyện + Xóa hồ sơ trò chuyện? + Xóa tin nhắn thành viên? + Xóa chỉ mình tôi + Xóa nhóm + Xóa kết nối đang chờ? + Xóa tệp cho tất cả các hồ sơ trò chuyện + Xóa liên kết? + Xóa hồ sơ + Xóa cho mọi người + Xóa liên kết + Xóa ảnh + Xóa các tin nhắn + Xóa nhóm? + Xóa tin nhắn? + Phiên bản ứng dụng trên máy tính %s không tương thích với ứng dụng này. + Máy tính đang bận + Máy tính đang không hoạt động + Máy tính đã bị ngắt kết nối + Quét mã QR.]]> + Xóa máy chủ + Xóa hàng đợi + Công cụ nhà phát triển + THIẾT BỊ + Tùy chọn nhà phát triển + Xác thực thiết bị đã bị vô hiệu hóa. Tắt Khóa SimpleX. + Lỗi máy chủ đích: %1$s + Chỉ báo đã nhận! + Chỉ báo đã nhận bị vô hiệu hóa! + Máy tính có mã mời sai + Mô tả + Chuyển gửi + Các thiết bị máy tính + Máy tính + Địa chỉ máy tính + Máy tính có một phiên bản không được hỗ trợ. Vui lòng đảm bảo rằng bạn sử dụng cùng một phiên bản ở cả hai thiết bị. + Đã xác nhận + Các kết nối đang hoạt động + Tất cả hồ sơ + Beta + Kiểm tra cập nhật + Đang kết nối + Các máy chủ SMP đã được cấu hình + Các máy chủ XFTP đã được cấu hình + Bản cập nhật ứng dụng đã được tải xuống + Kiểm tra cập nhật + Đã kết nối + thử + Các máy chủ đã kết nối + Lỗi xác nhận + Đã hoàn thành + Các khúc đã bị xóa + Các khúc đã được tải xuống + Các khúc đã được tải lên + Hồ sơ hiện tại + lỗi giải mã + Thống kê chi tiết + Chi tiết + Các kết nối + Đã tạo + Đã xóa + Lỗi xóa + %d sự kiện nhóm + Tin nhắn trực tiếp giữa các thành viên bị cấm trong nhóm này. + %d tệp với tổng kích thước là %s + phần di dời khác nhau trong ứng dụng/cơ sở dữ liệu: %s / %s + Tin nhắn trực tiếp + %dh + %d giờ + %d giờ + Tên, hình đại diện và cách ly truyền tải khác nhau. + Thiết bị + trực tiếp + Xác thực thiết bị không được bật. Bạn có thể bật SimpleX Lock thông qua phần Cài đặt, sau khi bạn bật xác thực thiết bị. + Tắt chỉ báo đã nhận? + Tin nhắn tự xóa + Tin nhắn tự xóa + Ngắt kết nối + Tắt (giữ thông tin ghi đè) + Biến mất vào lúc: %s + Tin nhắn tự xóa + Tắt cho tất cả + Tắt cho tất cả các nhóm + Biến mất vào lúc + Ngắt kết nối + Tắt thông báo + Tắt SimpleX Lock + Tắt (giữ thông tin ghi đè về nhóm) + Tin nhắn tự xóa bị cấm trong cuộc hội thoại này. + Tắt + Tắt chỉ báo đã nhận cho nhóm? + đã bị tắt + Đã bị tắt + Tắt + đã bị tắt + Tin nhắn tự xóa bị cấm trong nhóm này. + gọi + kết nối + Liên hệ sẽ bị xóa - điều này không thể hoàn tác! + Cuộc trò chuyện đã bị xóa! + Làm mờ phương tiện truyền thông + Không thể gọi liên hệ + Cho phép thực hiện cuộc gọi? + Cuộc gọi bị cấm! + Đang kết nối tới liên hệ, xin vui lòng đợi hoặc kiểm tra sau! + Liên hệ đã bị xóa. + Không thể nhắn tin cho thành viên nhóm + Không thể gọi thành viên nhóm + Xác nhận xóa liên hệ? + Liên hệ đã bị xóa! + Các liên hệ đã lưu trữ + Tạo + Mờ hình ảnh để riêng tư hơn. + Kiểm soát mạng của bạn + Kết nối và trạng thái máy chủ. + Cài đặt nâng cao + Bất kỳ ai cũng có thể tạo máy chủ. + Tiếp tục + Kết nối nhanh hơn với bạn bè. + Cơ sở dữ liệu SimpleX Chat đã được xuất + Lưu trữ các liên hệ để trò chuyện sau. + Ngắt kết nối máy tính? + Xóa tối đa 20 tin nhắn cùng một lúc. + Đã ngắt kết nối với lý do:%s + %s với lý do: %s]]> + Ngắt kết nối các thiết bị di động + Đã ngắt kết nối + Địa chỉ máy chủ đích của %1$s không tương thích với thiết lập máy chủ chuyển tiếp %2$s. + Phiên bản máy chủ đích của %1$s không tương thích với máy chủ chuyển tiếp %2$s. + Xóa mà không thông báo + Có thể tìm thấy qua mạng cục bộ + Xóa %d tin nhắn của các thành viên? + %d tháng + %dm + KHÔNG sử dụng định tuyến riêng tư. + Khám phá và tham gia nhóm + %dmth + Không gửi lịch sử đến các thành viên mới. + %d phút + KHÔNG gửi tin nhắn trực tiếp, kể cả khi máy chủ của bạn hoặc máy chủ đích không hỗ trợ định tuyến riêng tư. + %d tháng + %d phút + %d tin nhắn bị chặn + %d tin nhắn bị chặn bởi quản trị viên + %d tin nhắn được đánh dấu là đã xóa + Tên hiển thị không thể chứa khoảng trắng. + Khám phá qua mạng cục bộ + Tải về không thành công + Không tạo địa chỉ + Không hiển thị lại + Tải về tệp tin + Đang tải về kho lưu trữ + Hạ cấp và mở SimpleX Chat + Tải về + Không bật + Đã tải về + Đang tải về bản cập nhật ứng dụng, đừng đóng ứng dụng + Các tập tin đã tải về + Lỗi tải về + mã hóa đầu cuối + Tải xuống các phiên bản mới từ GitHub. + %d giây + %ds + Đang tải xuống chi tiết liên kết + Tên hiển thị trùng lặp! + %d tuần + %d tuần + %d giây + tin nhắn trùng lặp + %dw + cuộc gọi thoại mã hóa đầu cuối + các bản sao + Tải xuống %s (%s) + đã bật cho liên hệ + Tai nghe + Bật cho tất cả các nhóm + Chỉnh sửa hình ảnh + đã chỉnh sửa + Bật cho tất cả + đã bật + đã bật cho bạn + Đã bật cho + Cho phép truy cập camera + cuộc gọi video mã hóa đầu cuối + Cho phép nhận cuộc gọi từ màn hình khóa thông qua Cài đặt. + Cho phép + Chỉnh sửa hồ sơ nhóm + Cho phép xóa tin nhắn tự động? + Chỉnh sửa + Thư điện tử + Lỗi: %1$s + Lỗi + Tái đàm phán mã hóa thất bại. + mã hóa ok + cho phép tái đàm phán mã hóa với %s + cần tái đàm phán mã hóa cho %s + Bật chỉ báo đã nhận? + Mã hóa các tệp và phương tiện được lưu trữ + Lỗi + Kết thúc cuộc gọi + Bật trong cuộc trò chuyện trực tiếp (BETA)! + Bật mã truy cập tự hủy + Mã hóa cơ sở dữ liệu? + Cơ sở dữ liệu được mã hóa + Lỗi tái đàm phán mã hóa + Nhập mật khẩu + Bật (giữ thông tin ghi đè) + Mã hóa + mã hóa đã đồng nhất + cho phép tái đàm phán mã hóa + cần tái đàm phán mã hóa + Bật TCP keep-alive + Nhập mật khẩu trong tìm kiếm + Nhập tên của thiết bị này… + Bật SimpleX Lock + Nhập lời chào… + Nhập mã truy cập + Lỗi + Nhập lời chào... (không bắt buộc) + đã kết thúc + mã hóa đã đồng nhất cho %s + mã hóa ok cho %s + lỗi + Nhập máy chủ thủ công + Bật (giữ thông tin ghi đè nhóm) + Nhập đúng mật khẩu. + Nhập mật khẩu… + Mã hóa các tệp cục bộ + Bật tự hủy + Bật chỉ báo đã nhận cho các nhóm? + Nhập tên nhóm: + Nhập tên của bạn: + Bật khóa + Lỗi + Lỗi tạo hồ sơ! + Lỗi thay đổi cài đặt + Lỗi chặn thành viên cho tất cả + Lỗi thêm thành viên + Lỗi tạo tin nhắn + Lỗi tạo địa chỉ + Lỗi chấp nhận yêu cầu liên hệ + Lỗi thay đổi địa chỉ + Lỗi xóa cơ sở dữ liệu SimpleX Chat + Lỗi tạo liên hệ thành viên + Lỗi hủy bỏ thay đổi địa chỉ + Lỗi tạo liên kết nhóm + Lỗi thay đổi quyền hạn + Lỗi kết nối đến máy chủ chuyển tiếp %1$s. Vui lòng thử lại sau. + Lỗi xóa liên hệ + Lỗi xóa nhóm + Lỗi xóa liên kết nhóm + Lỗi xóa yêu cầu liên hệ + Lỗi xóa cơ sở dữ liệu + Lỗi xóa kết nối liên hệ đang chờ xử lý + Lỗi xuất cơ sở dữ liệu SimpleX Chat + Lỗi mã hóa cơ sở dữ liệu + Lỗi bật chỉ báo đã nhận! + Lỗi xóa ghi chú riêng tư + Lỗi xóa hồ sơ người dùng + Lỗi nhập cơ sở dữ liệu SimpleX Chat + Lỗi xuất cơ sở dữ liệu SimpleX Chat + Lỗi tải xuống kho lưu trữ + Lỗi khởi tạo WebView. Cập nhật hệ điều hành của bạn lên phiên bản mới. Vui lòng liên hệ với nhà phát triển. +\nLỗi: %s + Lỗi xóa thành viên + Lỗi: %s + Lỗi mở trình duyệt + Lỗi tham gia nhóm + Lỗi tải thông tin chi tiết + Lỗi nhận tệp + Lỗi lưu hồ sơ nhóm + Lỗi lưu tệp + Lỗi tải máy chủ SMP + Lỗi tải máy chủ XFTP + Lỗi kết nối lại máy chủ + Lỗi kết nối lại máy chủ + Lỗi + Lỗi khôi phục thống kê \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml index 536207892d..9e038995b3 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rCN/strings.xml @@ -15,7 +15,7 @@ 已接受通话 接受 通过在 %d 端口的 SOCKS 代理访问服务器?启用该选项前必须先启动代理。 - 添加服务器…… + 添加服务器 添加另一设备 管理员 扫描二维码来添加服务器。 @@ -69,7 +69,7 @@ 总是通过中继连接 允许您的联系人不不可逆地删除已发送消息。(24小时) 联系人允许 - 仅有您的联系人许可后才允许语音消息。 + 允许语音消息,前提是你的联系人允许这样的消息。 您: %1$s 允许您的联系人发送语音消息。 始终 @@ -87,8 +87,8 @@ 删除联系人? 已删除群组 删除图片 - 仅有您的联系人许可后才允许限时消息。 - 只有您的联系人同意才允许不可逆地删除消息。(24小时) + 允许限时消息,前提是你的联系人允许这样的消息。 + 允许不可逆的消息删除,前提是你的联系人允许这样做。(24小时) 允许不可逆地删除已发送消息。(24小时) 为此删除聊天资料 删除数据库 @@ -119,13 +119,13 @@ \n请注意:如果您有很多连接,您的电池和流量消耗可能会大大增加,并且某些连接可能会失败。 返回 最长续航 。您只会在应用程序运行时收到通知(无后台服务)。]]> - 较长续航 。后台服务每 10 分钟检查一次消息。您可能会错过来电或者紧急信息。]]> + 较长续航 。应用每 10 分钟检查一次消息。您可能会错过来电或者紧急信息。]]> 加粗 您和您的联系人都可以不可逆地删除已发送的消息。(24小时) 您和您的联系人都可以发送限时消息。 您和您的联系人都可以发送语音消息。 可以在设置里禁用它 - 应用程序运行时仍会显示通知。]]> - 使用更多电量 !后台服务始终运行——一旦收到消息,就会显示通知。]]> + 使用更多电量 !应用始终在后台运行——一即刻显示通知。]]> 请注意:如果您丢失密码,您将无法恢复或者更改密码。]]> 通话已结束! 无法邀请联系人! @@ -655,7 +655,7 @@ %d 分钟 %d 月 网络和服务器 - 网络设置 + 高级设置 已被管理员移除 Onion 主机将在可用时使用。 将不会使用 Onion 主机。 @@ -667,7 +667,6 @@ 关闭 连接需要 Onion 主机。 \n请注意:如果没有 .onion 地址,您将无法连接到服务器。 - Onion 主机将在可用时使用。 从不 已提供 %s 已提供 %s:%2s @@ -686,8 +685,6 @@ 没有联系人可添加 网络状态 关闭 - 将不会使用 Onion 主机。 - 连接需要 Onion 主机。 没有收到或发送的文件 发送人已取消文件传输。 分享 @@ -711,7 +708,7 @@ 在浏览器中打开链接可能会降低连接的隐私和安全性。SimpleX 上不受信任的链接将显示为红色。 恢复数据库备份后请输入之前的密码。 此操作无法撤消。 请更新应用程序并联系开发者。 - 开源协议和代码——任何人都可以运行服务器。 + 任何人都可以托管服务器。 粘贴 PING 次数 禁止发送语音消息。 @@ -731,7 +728,7 @@ SimpleX 消息 %s 未验证 感谢用户——通过 Weblate 做出贡献! - 第一个没有任何用户标识符的平台——专为隐私保护设计。 + 没有用户标识符。 完全去中心化 - 仅对成员可见。 图像无法解码。 请尝试不同的图像或联系开发者。 主题 @@ -744,7 +741,6 @@ 开始新的聊天 要与您的联系人验证端到端加密,请比较(或扫描)您设备上的代码。 取消静音 - 更新 .onion 主机设置? 更新传输隔离模式? (从剪贴板扫描或粘贴) 保护队列 @@ -771,7 +767,7 @@ 发送问题和想法 保护您的隐私和安全的消息传递和应用程序平台。 删去 - 人们只能通过您共享的链接与您建立联系。 + 你决定谁可以连接。 下一代私密通讯软件 粘贴您收到的链接 已跳过消息 @@ -829,9 +825,7 @@ 已删除 角色 - 恢复 重置颜色 - 保存颜色 减少电池使用量 为了保护时区,图像/语音文件使用 UTC。 使用聊天 @@ -1108,17 +1102,17 @@ 最大 1gb 的视频和文件 快速且无需等待发件人在线! 禁止音频/视频通话。 - 您和您的联系人都可以拨打电话。 - 只有您可以拨打电话。 - 只有您的联系人可以拨打电话。 - 允许您的联系人与您进行语音通话。 - 仅当您的联系人允许时才允许呼叫。 + 您和您的联系人都可以进行呼叫。 + 只有您可以进行呼叫。 + 只有您的联系人可以进行呼叫。 + 允许联系人呼叫你。 + 允许通话,前提是你的联系人允许它们。 禁止音频/视频通话。 1分钟 一次性链接 您和您的联系人都可以添加消息回应。 允许消息回应。 - 只有您的联系人允许时才允许消息回应。 + 允许消息回应,前提是你的联系人允许它们。 应用程序密码被替换为自毁密码。 更改自毁模式 关于 SimpleX 地址 @@ -1195,7 +1189,7 @@ 当人们请求连接时,您可以接受或拒绝它。 如果您以后删除您的地址,您不会丢失您的联系人。 用户指南中阅读更多。]]> - 主题颜色 + 界面颜色 与您的联系人保持连接。 与联系人分享 邀请朋友 @@ -1245,7 +1239,7 @@ - 语音消息最长5分钟。 \n- 自定义限时消息。 \n- 编辑消息历史。 - 导入过程中发生了一些非致命错误——您可以查看聊天控制台了解更多详细信息。 + 导入过程中发生了一些非致命错误: 应用程序 重启 通知将停止工作直到您重启应用程序 @@ -1769,4 +1763,309 @@ 个人资料图 改变个人资料图形状 方形、圆形、或两者之间的任意形状 + 超出了额度 — 收信人没收到之前发送的消息。 + 目标服务器错误:%1$s + 错误:%1$s + 转发服务器:%1$s +\n错误:%2$s + 消息传输警告 + 网络问题 — 许多发送消息的尝试后,消息过期了。 + 密钥错误或连接未知 — 连接被删除的可能性最大。 + 始终 + 从不 + 未知服务器 + 始终使用私密路由。 + 不使用私密路由。 + 在未知服务器上使用私密路由。 + 当 IP 地址不受保护时,在未知服务器上使用私密路由。 + 当 IP 隐藏时 + + + 当你的服务器或目标服务器不支持私密路由时直接发送消息。 + 备用消息路由 + 显示消息状态 + 为了保护你的 IP 地址,私密路由使用你的 SMP 服务器来传送消息。 + 私密消息路由 + 私密路由 + 当 IP 地址受保护且你的服务器或目标服务器不支持私密路由时,直接发送消息。 + 服务器地址和网络设置不兼容。 + 允许降级 + 服务器版本和网络设置不兼容。 + 未受保护 + 不直接发送消息,即便你的服务器或目标服务器不支持私密路由。 + 转发服务器:%1$s +\n目标服务器错误:%2$s + 消息路由模式 + 未知服务器! + 没有 Tor 或 VPN,这些 XFTP 中继可以看到你的 IP 地址: +\n%1$s. + 没有 Tor 或 VPN,文件服务器可以看到你的 IP 地址。 + 保护 IP 地址 + 文件 + 应用将请求确认来自未知服务器的下载(.onion 或启用 SOCKS 代理时除外)。 + 个人资料主题 + 在新窗口中显示聊天列表 + 所有颜色模式 + 应用到 + + 颜色模式 + 深色 + 深色模式 + 深色模式颜色 + 填充 + 适配 + 下午好! + 早上好! + 浅色 + 浅色模式 + 收到的回复 + 删除图片 + 已发送回复 + 设置默认主题 + 系统 + 壁纸强调色 + 壁纸背景色 + 额外的强调色2 + 高级设置 + 聊天颜色 + 重复 + 聊天主题 + 重置颜色 + 缩放 + Webview 初始化失败。更新你的系统到新版本。请联系开发者。 +\n错误:%s + 保护您的真实 IP 地址。不让你的联系人选择的消息中继看到它。 +\n在*网络&服务器*设置中开启。 + 确认来自未知服务器的文件。 + 安全地接收文件 + 改进了消息传递 + 让你的聊天看上去不同! + 私密消息路由🚀 + 新的聊天主题 + 波斯语用户界面 + 降低电池用量 + 主题 + 重置为应用主题 + 重置为用户主题 + 发送调试 + 消息队列信息 + 消息队列信息:%1$s +\n +\n上一则收到的信息:%2$s + + 未找到文件 - 最有可能的情况是文件被删或被取消了 + 错误的密钥或未知的文件块地址 - 最可能的情况是文件被删了。 + 文件错误 + 文件服务器错误:%1$s + 文件状态 + 消息状态 + 临时性文件错误 + 文件状态:%s + 消息状态:%s + 请检查移动设备和桌面设备连接到的是同一个本地网络,且桌面防火墙允许连接。 +\n请和开发者分享任何其他问题。 + 此链接用于另一台移动设备,请在桌面上创建新的链接。 + 复制错误 + 无法发送消息 + 选择的聊天首选项禁止此条消息。 + 请稍后尝试。 + 私密路由出错 + 已转发的消息 + 尚无直接连接,消息由管理员转发。 + 其他 SMP 服务器 + 其他 XFTP 服务器 + 扫描/粘贴链接 + 显示百分比 + 不活跃 + 缩放 + 所有配置文件 + 文件 + 没有信息,试试重新加载 + 服务器信息 + 尝试 + 已连接 + 已连接的服务器 + 连接中 + 活跃连接 + 详细统计数据 + 详情 + 已下载 + 错误 + 重连服务器出错 + 重连服务器出错 + 重设统计数据出错 + 错误 + 收到的消息 + 消息接收 + 待连接 + 先前连接的服务器 + 已代理的服务器 + 接收到的消息 + 接收总计 + 接收错误 + 重连 + 重连服务器? + 重连服务器? + 重连服务器强制消息传输。这会使用额外流量。 + 重置所有统计数据 + 重置所有统计数据吗? + 直接发送 + 已发送消息 + 发送总计 + 通过代理发送 + 服务器统计数据将被重置。此操作无法撤销! + XFTP 服务器 + 认可出错 + 块已删除 + 块已下载 + 已完毕 + 连接数 + 已创建 + 解密出错 + 已删除 + 删除错误 + 已下载的文件 + 下载出错 + 重复 + 已过期 + 其他 + 其他错误 + 已代理 + 已受保护 + 发送错误 + 服务器地址 + 大小 + 已上传的文件 + 上传出错 + 重新连接所有已连接的服务器来强制消息传输。这会使用额外流量。 + 你没有连接到这些服务器。私密路由被用于向它们传输消息。 + 重连所有服务器 + 重置 + 服务器地址不兼容网络设置:%1$s。 + 起始自 %s。 + 起始自 %s. +\n所有数据都是设备的私有数据。 + 已订阅 + 已认可 + 服务器版本不兼容你的应用:%1$s. + 信息主体 + SMP 服务器 + 统计数据 + 订阅错误 + 总计 + 块已上传 + 订阅被忽略 + 已配置的 SMP 服务器 + 已配置的 XFTP 服务器 + 当前配置文件 + 传输会话 + 已上传 + 已停用 + 字体大小 + 成员不活跃 + 如果成员变得活跃,可能会在之后传输消息。 + 发送的消息 + 打开服务器设置 + 检查更新 + 检查更新 + 停用 + 已停用 + 正在下载应用更新,不要关闭应用 + 下载 %s(%s) + 打开文件位置 + 请重启应用。 + 稍后提醒 + 跳过此版本 + 稳定版 + 有更新可用:%s + 取消了更新下载 + 要接收新版本通知,请打开“定期检查稳定或测试版本”。 + 应用更新已下载 + 测试版 + 安装成功 + 安装更新 + %1$s 的目的地服务器版本不兼容转发服务器 %2$s. + 转发服务器 %1$s 连接目的地服务器 %2$s 失败。请稍后尝试。 + 转发服务器地址不兼容网络设置:%1$s。 + 转发服务器版本不兼容网络设置:%1$s。 + %1$s 的目的地服务器地址不兼容转发服务器 %2$s 的设置 + 连接转发服务器 %1$s 出错。请稍后尝试。 + 模糊媒体文件 + 中度 + 关闭 + 轻柔 + 强烈 + 消息 + 呼叫 + 联系人将被删除 - 无法撤销此操作! + 保留对话 + 只删除对话 + 删除了联系人! + 删除了对话! + 不通知删除 + 你仍可以在聊天列表中查看与 %1$s 的对话。 + 粘贴链接 + 联系人 + 单手用户界面 + 正在连接联系人,请等候或稍后检查! + 联系人被删除了。 + 要能够呼叫联系人,你需要先允许联系人进行呼叫。 + 请让你的联系人启用通话。 + 无法呼叫联系人 + 无法呼叫群成员 + 无法给群成员发消息 + 确认删除联系人? + 连接 + 邀请 + 没有过滤的联系人 + 打开 + 搜索 + 发送消息来开启通话。 + 你可以发消息给来自已存档联系人的 %1$s。 + 已存档的联系人 + 允许通话? + 通话被禁止! + 设置 + 视频 + 消息将被标记为删除。收信人将可以揭示这些消息。 + 删除成员的 %d 条消息吗? + 选择 + 写消息 + 什么也没选中 + 已选中 %d + 将对所有成员删除这些消息。 + 这些消息将对所有成员标记为受管制。 + 更快地连接到你的好友 + 连接和服务器状态。 + 最多同时删除 20 条消息 + 它保护你的 IP 地址和连接。 + 切换聊天列表: + 你可以在“外观”设置中更改它。 + 保存并重新连接 + TCP 连接 + 控制你的网络 + 单手聊天工具栏 + 存档之后要聊天的联系人。 + 用一只手使用本应用。 + 已导出聊天数据库 + 继续 + 媒体和文件服务器 + 消息服务器 + SOCKS 代理 + 某些文件未导出 + 你可以迁移导出的数据库。 + 你可以保存导出的存档。 + 重置所有提示 + 模糊以增强隐私。 + 新的媒体选项 + 从聊天列表播放。 + 自动升级应用 + 创建 + 从 GitHub 下载新版。 + 增大字体尺寸。 + 邀请 + 新的聊天体验 🎉 + 新消息 + 请检查 Simple X 链接是否正确。 + 无效链接 \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml index c9d4298bc7..48efa52563 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/zh-rTW/strings.xml @@ -15,14 +15,14 @@ 管理員 然後,選按: 新增預設伺服器 - 新增伺服器… + 新增伺服器 接受 認證無效 允許 - 顯示名稱: + 個人資料名稱: 全名: - 使用更多電量!通知服務長期在背景中運行 – 有效的訊息就會即時顯示在通知內。]]> - 對電量友善。通知服務於每十分鐘檢查一次訊息。你可能會錯過通話和迫切的訊息。]]> + 使用更多電量!程式始終在背景中運行 – 通知會立即顯示。]]> + 對電量友善。程式每10分鐘檢查一次訊息。你可能會錯過電話或警急訊息。]]> 回應通話請求 清除 允許向群組內的成員傳送私訊。 @@ -69,7 +69,7 @@ 你目前的個人檔案 顯示的名稱中不能有空白。 儲存設定? - 顯示名稱 + 輸入你的名稱: 通話出錯 正在撥打… 通話中 @@ -130,7 +130,7 @@ 建立私密群組 退出群組 已連接 - 群組顯示名稱: + 輸入群組名稱: 群組全名: 對話設定 關閉 @@ -153,7 +153,7 @@ 群組設定 聯絡人設定 分享媒體… - 你和你的聯絡人都可以不可逆地刪除已經傳送的訊息。 + 你和你的聯絡人都可以不可逆地刪除已傳送的訊息。 已連接 簡介 完整連結 @@ -179,13 +179,13 @@ 檢查輸入的伺服器地址,然後再試一次。 終端機對話 於 Github 給個星星 - 匿名聊天模式會保護你的真實個人檔案名稱和頭像 — 當有新聯絡人的時候會自動建立一個隨機性的個人檔案。 + 隱身模式透過為每個聯絡人使用新的隨機設定檔來保護您的隱私。 這樣是允許每一個對話中擁有不同的顯示名稱,並且沒有任何的個人資料可用於分享或有機會外洩。 只有你的聯絡人允許的情況下,才允許自動銷毀訊息。 允許你的聯絡人傳送自動銷毀的訊息。 - 只有你的聯絡人允許的情況下,才允許不可逆地將訊息刪除。 - 允許你的聯絡人可以不可逆地刪除已發送的訊息。 - 允許將不可撤銷的訊息刪除。 + 只有你的聯絡人允許的情況下,才允許不可逆地將訊息刪除。(24小時) + 允許你的聯絡人不可逆地刪除已發送的訊息。(24小時) + 允許不可逆地將已傳送的訊息刪除。(24小時) 允許傳送語音訊息。 多久後刪除 群組內所有成員會保持連接。 @@ -231,9 +231,9 @@ 感謝用戶 - 使用 Weblate 的翻譯貢獻! SimpleX k - 透過連結連接聯絡人? - 透過邀請連結連接? - 透過邀請連結連接群組? + 透過聯絡人地址連接? + 透過一次性連結連接? + 加入群組? 你的個人檔案將傳送給你接收此連結的聯絡人。 你將加入此連結內的群組並且連接到此群組成為群組內的成員。 連接 @@ -412,8 +412,6 @@ ICE 伺服器(每行一個) 使用 .onion 主機 Onion 主機不會啟用。 - Onion 主機會在可用時啟用。 - Onion 主機不會啟用。 連接 刪除聯絡地址 顏色 @@ -437,7 +435,6 @@ 請確保你的 WebRTC ICE 伺服器地址是正確的格式,每行也有分隔和沒有重複。 使用直接互聯網連接? 如果你確定,你的訊息伺服器能夠看到你的 IP 位置,和你的網路供應商 - 你正在連接到哪些伺服器。 - 更新 .onion 主機設定? 刪除圖片 開始中 … 等待對方回應… @@ -496,7 +493,6 @@ 需要 Onion 主機會在可用時啟用。 - 連接時將需要使用 Onion 主機。 刪除聯絡地址? 你的個人檔案只會儲存於你的裝置和只會分享給你的聯絡人。 SimpleX 伺服器並不會看到你的個人檔案。 儲存並通知你的聯絡人 @@ -513,7 +509,7 @@ 通話完結 連接 網路 & 伺服器 - 網路設定 + 進階設定 使用 SOCKS 代理伺服器 儲存並通知群組內的聯絡人 退出並且不儲存記錄 @@ -551,7 +547,8 @@ 有一些伺服器測試失敗: 掃描伺服器的二維碼 你的伺服器 - 連接時將需要使用 Onion 主機。 + 連接時需要使用 Onion 主機。 +\n請注意:如果沒有 .onion 地址,您將無法連接到伺服器。 對話檔案 透過群組連結 透過群組連結使用匿名聊天模式 @@ -625,7 +622,7 @@ 沒有聯絡人可以選擇 群組連結 刪除連結 - 群組是完全去中心化的 - 只有群組內的成員能看到。 + 完全去中心化 - 只有成員能看到。 禁止傳送自動銷毀的訊息。 %d 個小時 更新內容 @@ -647,7 +644,7 @@ 儲存群組檔案 語音訊息於這個聊天室是禁用的。 允許你的聯絡人可以完全刪除訊息。 - 第一個沒有任何用戶識別符的通訊平台 – 以私隱為設計。 + 沒有用戶識別符。 新一代的私密訊息平台 去中心化的 人們只能在你分享了連結後,才能和你連接。 @@ -656,10 +653,10 @@ 這是如何運作 你可以之後透過設定修改。 私下連接 - 開放源碼協議和程式碼 – 任何人也可以運行伺服器。 + 任何人都可以託管伺服器。 無視 語音通話來電 - 貼上你接收到的連結 + 貼上你收到的連結 端對端加密 沒有端對端加密 關閉喇叭 @@ -761,7 +758,7 @@ 不可逆地刪除訊息於這個聊天室內是禁用的。 只有你可以傳送語音訊息。 私訊群組內的成員於這個群組內是禁用的。 - 群組內的成員可以不可逆地刪除訊息。 + 群組內的成員可以不可逆地刪除訊息。(24小時) 語音訊息 改善伺服器配置 當你切換至最近應用程式版面時,無法預覽程式畫面。 @@ -825,17 +822,15 @@ 自動銷毀訊息於這個群組內是禁用的。 已提供 %s 儲存群組檔案時出錯 - 恢復 主題 - 儲存顏色 你允許 修改群組內的設定 私訊 已啟用 已為你啟用 已為聯絡人啟用 - 只有你能不可逆地刪除訊息(你的聯絡人可以將它標記為刪除)。 - 只有你的聊絡人可以不可逆的刪除訊息(你可以將它標記為刪除)。 + 只有你能不可逆地刪除訊息(你的聯絡人可以將它標記為刪除)。(24小時) + 只有你的聊絡人可以不可逆的刪除訊息(你可以將它標記為刪除)。(24小時) 只有你的聯絡人可以傳送語音訊息。 禁止私訊群組內的成員。 不可逆地刪除訊息於這個群組內是禁用的。 @@ -905,7 +900,7 @@ 添加更多身份選項 聯絡人頭像 個人檔案頭像占位符 - 不受垃圾郵件和濫用行為影響 + 不受垃圾和騷擾訊息影響 %1$s 希望透過以下方式聯絡你 開啟視訊 翻轉相機 @@ -914,7 +909,7 @@ 你的通話 經由分程傳遞連接 在上鎖畫面顯示來電通知: - %1$d 你錯過了多個訊息 + %1$d 條訊息已跳過 錯誤的訊息雜湊值 你錯過了多個訊息 @@ -1109,7 +1104,7 @@ %1$d 訊息解密失敗。 使用SOCKS 代理伺服器 你的 XFTP 伺服器 - %1$d 錯過了多個訊息。 + %1$d 條訊息已跳過。 影片和檔案和最大上限為1gb 影片 呈交 @@ -1117,7 +1112,7 @@ 查看更多 SimpleX 聯絡地址 一次性連結 - 主題顏色 + 介面顏色 建立 SimpleX 的聯絡地址 更新了的個人檔案將傳送給你的聯絡人。 與你的聯絡人分享聯絡地址? @@ -1146,7 +1141,7 @@ 已傳送訊息 標題 關於 SimpleX 的聯絡地址 - 外加的顏色 + 額外的強調色 外加的輔助 聯絡地址 後台 @@ -1247,4 +1242,563 @@ \n- 編輯紀錄。 搜尋 已關閉 + 確認來自未知伺服器的檔案。 + 超出額度 - 收件人未收到先前傳送的訊息 + 應用程式資料轉移 + 應用 + 請在轉移之前確認你還記得數據庫密碼 + 被管理員封鎖 + 進階設定 + 封鎖群組成員 + 活躍連接 + 中止 + 和其他 %d 事件 + 封鎖成員? + 6種全新的介面語言 + 藍芽 + %2$s 審核了 %1$d 條訊息 + 已封鎖 + 將停止地址更改。將使用舊聯絡地址。 + 測試 + 檢查更新 + 相機和麥克風 + 封鎖 + 應用程式主題 + 管理員 + 模糊以增強隱私 + 所有成員 + 管理員可以為所有人封鎖一名成員 + 無法傳送訊息 + 為所有成員封鎖此成員? + + 中止更改地址? + 無法傳送訊息給群組成員 + 所有顏色模式 + 應用到 + 應用程式密碼 + 應用程式 + 聊天顏色 + 聊天已停止。如果你已經在另一台設備使用過此資料庫,你應該在啟動聊天前將數據庫傳輸回來。 + 即將推出! + 軟體更新以下載 + 儲存聯絡人以便稍後聊天 + 相機 + 選擇一個檔案 + 存檔並上傳 + 你的所有聯絡人、對話和檔案將被安全加密並切塊上傳到你設定的 XFTP 中繼 + 正在儲存資料庫 + 取消遷移 + 與 %s 協調加密中… + 允許 + 語音通話 + 相機不可用 + 所有訊息都將被刪除 - 這無法復原 + 請注意:訊息和檔案中繼通過 SOCKS 代理連接。通話和傳送連預覽使用直接連接。]]> + 封鎖全部 + 改進群組功能 + 行動網路 + 封鎖成員 + 警告:此存檔將被刪除。]]> + 清除私密筆記? + 添加聯絡人 + 總是 + 協調加密中… + 允許傳送檔案和媒體 + 允許傳送 SimpleX 連結 + 其他 + 已連接! + 中止更改地址 + 將分享新的隨機個人檔案 + 所有來自 %s 的新訊息都將被隱藏! + 已封鎖 %s + 作者 + 已封鎖 + 被管理員封鎖 + 額外的強調色2 + 阿拉伯語、保加利亞語、芬蘭語、希伯來語、泰國語和烏克蘭語——感謝使用者們與Weblate + 已加入群組! + 確認網路設定 + 嘗試 + 已確認 + 確認錯誤 + 完成 + 區塊已刪除 + 區塊已上傳 + 區塊已下載 + 添加聯絡人: 來創建新的邀請連結,或通過你收到的連結進行連接。]]> + 建立群組: 建立新的群組。]]> + 錯誤的桌面地址 + 已轉移聊天 + 從另一部設備轉移 並掃描QR code。]]> + 請注意: 作為安全保護措施,在兩部設備上使用同一數據庫會破壞解密來自你聯絡人的訊息。]]> + 確定刪除聯絡人? + 檢查更新 + 無法與聯絡人通話 + 通話被禁止! + 無法與群組成員通話 + 應用程式將為新的本機檔案(影片除外)加密。 + 檢查你的網路連接並重試 + 所有配置文件 + 已設定的 SMP 伺服器 + 聊天主題 + 通話 + 允許降級 + 始終使用私密路由。 + 以導出聊天資料庫 + 已設定的 XFTP 伺服器 + 色彩模式 + 已儲存的聯絡人 + 模糊媒體 + 允許通話? + 建立於:%s + Webview 初始化失敗。更新你的系統到新版本。請聯繫開發者。 +\n錯誤:%s + 已刪除聯絡人 + %d 個群事件 + 訊息太大 + 訊息傳送警告 + 錯誤:%1$s + 開發者選項 + 與 %s 的加密需要重協商 + %s 不活躍]]> + 最喜歡 + 訊息成功送達! + 檔案和媒體 + 連線停止 + %s的連接不穩定]]> + 聯絡人 + 適合 + 群組成員可以傳送檔案和媒體。 + 連結行動裝置 + 此群組禁止檔案和媒體 + 結束通話 + 刪除 %d 條訊息嗎? + 訊息草稿 + 允許重新協商加密 + 建立於 + 為所有人封鎖時出錯 + 桌面應用版本 %s 與此應用不相容 + 未找到檔案 - 檔案可能被刪除或被取消了 + 檔案伺服器錯誤:%1$s + 展開 + 啟用(保留組覆蓋) + 淺色 + 淺色模式 + 群組成員可傳送 SimpleX 連結。 + 深色 + 詳情 + 訊息接收 + 無效連結 + %d 條訊息被標記為刪除 + %d 條訊息已攔截 + 轉發伺服器地址不相容網路設定:%1$s。 + 轉發伺服器地址不相容網路設定:%1$s。 + %1$s 的目標伺服器地址不相容轉送伺服器 %2$s 的設定 + %1$s 的目地伺服器版本不相容於轉送伺服器 %2$s. + 關閉通知 + 網路問題 - 多次嘗試傳送訊息後,訊息已過期。 + 轉發伺服器:%1$s +\n目標伺服器錯誤:%2$s + 如果成員變得活躍,可能會在之後傳送訊息。 + 刪除了聯絡人! + 聯絡人將被刪除 - 無法復原此操作 + + 已停用 + 安裝成功 + 建立 + 打開瀏覽器出錯 + 啟用(保留覆蓋) + 禁用(保留組覆蓋) + 為群組啟用回執? + 網路連接 + 繼續 + 檔案狀態 + 深色模式顏色 + 字體大小 + 啟用於 + 立陶宛語使用者介面 + 新的聊天主題 + 連線和伺服器狀態 + 控制你的網路 + 從GitHub下載最新版本。 + 啟用 + 新的聊天體驗 🎉 + 新的媒體選項 + 以連接的行動裝置 + 連接桌面 + 連接到桌面 + 連線終止 + 連接到桌面 + 上傳存檔出錯 + 文件被刪除或鏈接無效 + 導入失敗 + 頭戴式耳機 + 耳機 + 對所有聯絡人關閉 + 深色模式 + 啟用已讀回條時出錯! + (此裝置 v%s)]]> + 不相容的版本 + PC版已斷線 + 轉移裝置 + 通話鈴聲 + 轉發並保存訊息 + 訊息來源保持私密 + 沒有已連接的行動裝置 + 禁止檔案和媒體! + 檔案載入中 + 檔案錯誤 + 備用訊息路由 + 如果你或你的目標伺服器不支持私密路由,將不直接傳送訊息。 + 建立個人資料 + 從另一台裝置轉移 + 成員姓名從 %1$s 改為了 %2$s + 同意加密 + 訊息狀態 + 連接請求將傳送給該組成員。 + - 更穩定的消息傳送。 +\n- 更好的群組。 +\n- 還有更多! + 匈牙利語和土耳其語用戶界面 + 轉移完成 + 從此裝置刪除數據庫 + 傳送 + 下載 + 轉發 + 已轉發 + 轉發自 + 不允許檔案和媒體 + 轉發訊息… + 建立連結中… + 保留 + 聯絡人姓名從 %1$s 改為了 %2$s + 新訊息 + 邀請 + 停用(保留覆蓋) + 訊息狀態:%s + 訊息隊列資訊 + 收到的訊息 + 加密本機檔案 + 檔案 + 對所有群組關閉 + 填充 + 讓你的聊天看上去不相同! + 增大字體大小。 + 輸入密碼短語 + 傳送的訊息 + PC版處理中 + 隱身模式連接 + 已刪除對話! + 目標伺服器錯誤:%1$s + 聊天載入中… + 已轉發的訊息 + 管理員封鎖了 %d 條訊息 + 連結轉發伺服器 %1$s 出錯。請稍候嘗試。 + 刪除資料庫出錯 + 轉發伺服器:%1$s +\n錯誤:%2$s + 轉發伺服器 %1$s 連結目標伺服器 %2$s 失敗。請稍後嘗試。 + 導入存檔中 + 改進訊息傳送 + 安裝更新 + 它保護你的 IP 位址和連線。 + 加入群組對話 + %s 的版本。請檢察兩台裝置安裝的是否版本相同]]> + 更可靠的網路連接 + 發現和加入群組 + 裝置 + 新行動裝置 + 保存設定出錯 + 導出的檔案不存在 + 導出資料庫時出錯 + 確認上傳 + 正在建立存檔連結 + 加密OK + 將顯示來自 %s 的訊息! + 送達回執! + 已轉發 + 端對端加密的保護,並具有完全的前向加密、不可否認性和入侵恢復。]]> + 加密協商錯誤 + 抗量子端對端加密保護。]]> + 加密重協商失敗 + 將更新資料庫密碼並儲存在設定中。 + 使用隨機身分建立群組 + 加入速度更快、訊息更可靠。 + 匿名群組 + 連接行動裝置 + 回復 + 和 %1$s 連接? + 在桌面應用裡建立新的帳號。💻 + 輸入此裝置名稱… + 已連結到行動裝置 + 可通過局域網發現 + (新)]]> + 嚴重錯誤 + 內部錯誤 + 驗證密碼短語出錯: + 顯示名稱無效! + 無效的檔案路徑 + 過濾未讀和收藏的聊天記錄。 + 斷開連結 + 斷開桌面連結? + 中止地址更改時出錯 + 顯示通知出錯,請聯繫開發者。 + 刪除並通知聯絡人 + 訊息路由模式 + 不使用私密路由。 + 要進行通話請授予一項或多項權限 + 在 Android 系統設定中找到此權限並手動授予權限。 + 在系統設定中授予 + 傳送邀請出錯 + 建立聯絡人時出錯 + 移除一條訊息 + 保持連接 + - 連接到目錄服務(BETA)! +\n- 發送回執(最多20名成員)。 +\n- 更快,更穩定。 + 連接行動端和桌面端應用程式! 🔗 + 改進訊息傳送 + 已關閉送達回執! + %s 斷開連接]]> + %s 未找到]]> + 存檔下載中 + 轉移中 + 下載失敗 + 為所有組啟用 + 需要重協商加密 + 關閉 + 修復連結 + 修復聯絡人不支援的問題 + 修復 + 修復連結? + 正在進行資料庫轉移。 +\n可能需要幾分鐘時間。 + 建立訊息出錯 + 刪除私密筆記錯誤 + 啟用相機訪問 + 新聊天 + 為所有人啟用 + 同步連接時出錯 + 顯示內容出錯 + 顯示訊息出錯 + 錯誤 + 無後台通話 + 建立聊天資料 + 已同意 %s 的加密 + 允許重新協商與 %s 的加密 + 與 %s 的加密OK + 桌面設備 + 連接桌面選項 + 連接桌面 + 直接連線中 + 邀請 + 建立群組 + 修復群組成員不支援的問題 + 更快地連接到你的好友 + 最多同時刪除20條訊息 + PC版非活躍 + 已連接的伺服器 + 詳細統計數據 + 已停用 + 不活躍 + 直接連接? + 午安! + 早安! + 不向新成員傳送歷史訊息 + 未發送歷史訊息給新成員。 + 在私聊中開啟(測試版)! + 透過QR code轉移到另一部裝置。 + 不啟用 + 錯誤 + 連線停止 + %s的連結斷開,原因是:%s]]> + 斷線原因:%s + 斷開行動裝置連接 + 桌面地址 + 通過局域網發現 + %s 處理中]]> + %s 斷開連接]]> + 下載連結詳情中 + 群組已存在! + 錯誤 + 已下載 + 重製統計數據出錯 + 已過期 + 連接數 + 已建立 + 刪除錯誤 + 解密出錯 + 已刪除 + 已下載的檔案 + 下載出錯 + 功能執行所花費的時間過長:%1$d 秒:%2$s + 無效名稱 + 正確名字為 %s? + 複製錯誤 + 正在連接到桌面 + 找到桌面 + 自動連接 + 與PC版的連接不穩定 + 桌面 + 已安裝的PC版本不支援。請確認兩台裝置所安裝的版本相同 + PC版邀請碼錯誤 + 通過連結連接? + 加入你的群組嗎? + 轉移到此處 + 無效連結 + 在另一部設備上完成轉移 + 下載存檔錯誤 + 轉移到另一部裝置 + 必須停止聊天才能繼續。 + 保留對話 + 不通知刪除 + 無效的QR code + 保留未使用的邀請嗎? + 停用回執? + 啟用回執? + 正在連接聯絡人,請等待或稍後檢查! + 聯絡人被刪除了。 + 傳送調試 + 為儲存的檔案和媒體加密 + 連接到你自己? + 完成轉移 + 目前配置文件 + 檔案 + 重連伺服器出錯 + 重連伺服器出錯 + 重複 + 檔案狀態:%s + 授予權限 + 麥克風 + 資料庫將被加密,密碼將儲存在設定中 + 刪除成員的 %d 條訊息嗎? + 成員非活躍 + 輸入訊息 + 訊息將被標記為刪除。收信人可以揭示這些訊息。 + 連接 + 訊息 + 停用 + 下載更新中,請不要關閉應用 + 下載 %s(%s) + 從不 + 中等 + 媒體和檔案伺服器 + 訊息伺服器 + 即使在對話中禁用。 + 更快的發起聊天 + 修復還原備份後的加密問題 + 網路管理 + 新的桌面應用! + 已連接 + 連接中 + 錯誤 + 為群組停用回執? + 過往的成員 %1$s + 私密訊息路由 🚀 + 貼上存檔連結 + 從桌面使用並掃描QR code。]]> + 無傳送資訊 + 或者顯示此碼 + 待連接 + 請檢查 Simple X 鏈接是否正確。 + 私密筆記 + 請稍後再試。 + 沒有過濾的聯絡人 + 打開檔案位置 + 打開 + 私密筆記 + 波斯語用戶界面 + 從聊天列表播放。 + 正在準備上傳 + 擁有者 + 不相容! + 請將它報告給開發者: +\n%s +\n +\n建議重啟應用。 + 從已連接行動裝置加載檔案時請稍候片刻 + 或者掃描QR code + 禁止傳送 SimpleX 連結 + 沒有選擇聊天 + 打開設定 + 這人資料主題 + 個人資料圖片 + 同一時刻只有一台裝置可工作 + 保護 IP 地址 + 其他 + 無網路連接 + 無歷史記錄 + 無過濾聊天 + 請將它報告給開發者: +\n%s + 打開應用程式設定 + 開啟轉移畫面 + 通知將停止,直到您重啟應用程式 + 禁止傳送檔案和媒體。 + 打開群組 + 只有群組所有者才能啟用檔案和媒體。 + 貼上你收到的連結以與你的聯絡人聯絡… + 先前連接的伺服器 + 其他 + + - 可選擇通知已刪除的聯絡人。 +\n- 帶空格的個人資料名稱。 +\n- 以及更多! + 貼上連結以連接! + 貼上桌面地址 + 請確認此裝置的網路設定是否正確。 + 其他錯誤 + 請檢查行動裝置和桌面設備連接到的是同一個本地網絡,且桌面防火牆允許連接。 +\n請和開發者分享任何其他問題。 + 在防火牆中打開端口 + 或貼上存檔連結 + 正在準備下載 + 只刪除對話 + 貼上連結 + 其他 SMP 伺服器 + 其他 XFTP 伺服器 + 請讓你的聯絡人啟用通話。 + 畫中畫通話 + 或安全分享此文件連結 + 無資訊,試試重新加載 + 打開伺服器設定 + 私密路由出錯 + 尚無直接連接,訊息由管理員轉發。 + 什麼也沒選中 + 打開 + 私密路由 + 打開資料庫文件夾 + 私密訊息路由 + 請重啟應用程式。 + 關閉 + 抗量子加密 + 刪除了聯繫地址 + 重連伺服器? + 接收到的訊息 + 接收錯誤 + 重新連接所有已連接的伺服器來強制傳送訊息。這會使用額外流量。 + 重連伺服器強制傳送訊息。這會使用額外流量。 + 並行接收 + 收件人看不到這條訊息來自誰。 + 刪除了資料圖片 + 可使用的聊天工具箱 + 可存取的聊天工具欄 + 最近歷史和改進的目錄機器人。 + 每 KB 協議超時 + 保護您的真實 IP 地址。不讓你聯絡人選擇的訊息中繼看到它。 +\n在*網絡&伺服器*設定中開啓。 + 隨機密碼以明文形式儲存在設定中。 +\n您可以稍後更改。 + 傳送回條已禁用 + 代理伺服器 + 重連伺服器? + 抗量子端到端加密 + 收到的回覆 + 重連所有伺服器 + 重連 + 代理 + 隨機 + 更新 + 接收總計 + 稍後提醒 \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ThemesTest.kt b/apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ThemesTest.kt new file mode 100644 index 0000000000..ae838dcff5 --- /dev/null +++ b/apps/multiplatform/common/src/commonTest/kotlin/chat/simplex/app/ThemesTest.kt @@ -0,0 +1,38 @@ +package chat.simplex.app + +import chat.simplex.common.ui.theme.* +import kotlin.test.Test +import kotlin.test.assertEquals + +// use this command for testing: +// ./gradlew desktopTest +class ThemesTest { + @Test + fun testSkipDuplicates() { + val r = ArrayList() + r.add(ThemeOverrides("UUID", DefaultTheme.DARK)) + r.add(ThemeOverrides("UUID", DefaultTheme.DARK)) + r.add(ThemeOverrides("UUID", DefaultTheme.LIGHT)) + r.add(ThemeOverrides("UUID2", DefaultTheme.DARK)) + r.add(ThemeOverrides("UUID3", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper())) + r.add(ThemeOverrides("UUID4", DefaultTheme.LIGHT, wallpaper = null)) + r.add(ThemeOverrides("UUID5", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper(preset = "something"))) + r.add(ThemeOverrides("UUID5", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper(preset = "something2"))) + r.add(ThemeOverrides("UUID6", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper(preset = "something2"))) + r.add(ThemeOverrides("UUID7", DefaultTheme.DARK, wallpaper = ThemeWallpaper(preset = "something2"))) + r.add(ThemeOverrides("UUID8", DefaultTheme.DARK, wallpaper = ThemeWallpaper(imageFile = "image"))) + r.add(ThemeOverrides("UUID9", DefaultTheme.DARK, wallpaper = ThemeWallpaper(imageFile = "image2"))) + r.add(ThemeOverrides("UUID10", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper(imageFile = "image"))) + assertEquals( + r.skipDuplicates(), listOf( + ThemeOverrides("UUID", DefaultTheme.DARK), + ThemeOverrides("UUID3", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper()), + ThemeOverrides("UUID5", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper(preset = "something")), + ThemeOverrides("UUID6", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper(preset = "something2")), + ThemeOverrides("UUID7", DefaultTheme.DARK, wallpaper = ThemeWallpaper(preset = "something2")), + ThemeOverrides("UUID8", DefaultTheme.DARK, wallpaper = ThemeWallpaper(imageFile = "image")), + ThemeOverrides("UUID10", DefaultTheme.LIGHT, wallpaper = ThemeWallpaper(imageFile = "image")) + ) + ) + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt index 7b7762eefc..df8887c4e1 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt @@ -4,8 +4,7 @@ import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text +import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -15,6 +14,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import androidx.compose.ui.window.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH import chat.simplex.common.ui.theme.SimpleXTheme @@ -27,6 +27,7 @@ import kotlinx.coroutines.* import java.awt.event.WindowEvent import java.awt.event.WindowFocusListener import java.io.File +import kotlin.math.sqrt import kotlin.system.exitProcess val simplexWindowState = SimplexWindowState() @@ -85,20 +86,25 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState) { position = WindowPosition(state.x.dp, state.y.dp) ) + val storingJob: MutableState = remember { mutableStateOf(Job()) } LaunchedEffect( windowState.position.x.value, windowState.position.y.value, windowState.size.width.value, windowState.size.height.value ) { - storeWindowState( - WindowPositionSize( - x = windowState.position.x.value.toInt(), - y = windowState.position.y.value.toInt(), - width = windowState.size.width.value.toInt(), - height = windowState.size.height.value.toInt() + storingJob.value.cancel() + storingJob.value = launch { + delay(1000L) + storeWindowState( + WindowPositionSize( + x = windowState.position.x.value.toInt(), + y = windowState.position.y.value.toInt(), + width = windowState.size.width.value.toInt(), + height = windowState.size.height.value.toInt() + ) ) - ) + } } simplexWindowState.windowState = windowState @@ -111,6 +117,7 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState) { false } }, title = "SimpleX") { +// val hardwareAccelerationDisabled = remember { listOf(GraphicsApi.SOFTWARE_FAST, GraphicsApi.SOFTWARE_COMPAT, GraphicsApi.UNKNOWN).contains(window.renderApi) } simplexWindowState.window = window AppScreen() if (simplexWindowState.openDialog.isAwaiting) { @@ -190,7 +197,8 @@ private fun ApplicationScope.AppWindow(closedByError: MutableState) { if (remember { ChatController.appPrefs.developerTools.state }.value && remember { ChatController.appPrefs.terminalAlwaysVisible.state }.value && remember { ChatController.appPrefs.appLanguage.state }.value != "") { var hiddenUntilRestart by remember { mutableStateOf(false) } if (!hiddenUntilRestart) { - val cWindowState = rememberWindowState(placement = WindowPlacement.Floating, width = DEFAULT_START_MODAL_WIDTH, height = 768.dp) + val cWindowState = rememberWindowState(placement = WindowPlacement.Floating, width = DEFAULT_START_MODAL_WIDTH * fontSizeSqrtMultiplier, height = + 768.dp) Window(state = cWindowState, onCloseRequest = { hiddenUntilRestart = true }, title = stringResource(MR.strings.chat_console)) { SimpleXTheme { TerminalView(ChatModel) { hiddenUntilRestart = true } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt index 3fab849361..3bd1506b4f 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/model/NtfManager.desktop.kt @@ -8,13 +8,16 @@ import chat.simplex.common.views.call.RcvCallInvitation import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import com.sshtools.twoslices.* +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import java.awt.* import java.awt.TrayIcon.MessageType import java.io.File import javax.imageio.ImageIO object NtfManager { - private val prevNtfs = arrayListOf>() + private val prevNtfs = arrayListOf, Slice>>() + private val prevNtfsMutex: Mutex = Mutex() fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean { if (simplexWindowState.windowFocused.value) return false @@ -42,34 +45,52 @@ object NtfManager { generalGetString(MR.strings.accept) to { ntfManager.acceptCallAction(invitation.contact.id) }, generalGetString(MR.strings.reject) to { ChatModel.callManager.endCall(invitation = invitation) } ) - displayNotificationViaLib(contactId, title, text, prepareIconPath(largeIcon), actions) { + displayNotificationViaLib(invitation.user.userId, contactId, title, text, prepareIconPath(largeIcon), actions) { ntfManager.openChatAction(invitation.user.userId, contactId) } return true } fun showMessage(title: String, text: String) { - displayNotificationViaLib("MESSAGE", title, text, null, emptyList()) {} + displayNotificationViaLib(-1, "MESSAGE", title, text, null, emptyList()) {} } fun hasNotificationsForChat(chatId: ChatId) = false//prevNtfs.any { it.first == chatId } fun cancelNotificationsForChat(chatId: ChatId) { - val ntf = prevNtfs.firstOrNull { it.first == chatId } - if (ntf != null) { - prevNtfs.remove(ntf) - /*try { - ntf.second.close() - } catch (e: Exception) { - // Can be java.lang.UnsupportedOperationException, for example. May do nothing - println("Failed to close notification: ${e.stackTraceToString()}") - }*/ + withBGApi { + prevNtfsMutex.withLock { + val ntf = prevNtfs.firstOrNull { (userChat) -> userChat.second == chatId } + if (ntf != null) { + prevNtfs.remove(ntf) + /*try { + ntf.second.close() + } catch (e: Exception) { + // Can be java.lang.UnsupportedOperationException, for example. May do nothing + println("Failed to close notification: ${e.stackTraceToString()}") + }*/ + } + } + } + } + + fun cancelNotificationsForUser(userId: Long) { + withBGApi { + prevNtfsMutex.withLock { + prevNtfs.filter { (userChat) -> userChat.first == userId }.forEach { + prevNtfs.remove(it) + } + } } } fun cancelAllNotifications() { // prevNtfs.forEach { try { it.second.close() } catch (e: Exception) { println("Failed to close notification: ${e.stackTraceToString()}") } } - prevNtfs.clear() + withBGApi { + prevNtfsMutex.withLock { + prevNtfs.clear() + } + } } fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String?, actions: List Unit>>) { @@ -84,12 +105,13 @@ object NtfManager { else -> base64ToBitmap(image) } - displayNotificationViaLib(chatId, title, content, prepareIconPath(largeIcon), actions.map { it.first.name to it.second }) { + displayNotificationViaLib(user.userId, chatId, title, content, prepareIconPath(largeIcon), actions.map { it.first.name to it.second }) { ntfManager.openChatAction(user.userId, chatId) } } private fun displayNotificationViaLib( + userId: Long, chatId: String, title: String, text: String, @@ -110,7 +132,11 @@ object NtfManager { builder.action(it.first, it.second) } try { - prevNtfs.add(chatId to builder.toast()) + withBGApi { + prevNtfsMutex.withLock { + prevNtfs.add(Pair(userId, chatId) to builder.toast()) + } + } } catch (e: Throwable) { Log.e(TAG, e.stackTraceToString()) if (e !is Exception) { diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt index 71f862b30a..38d87fc497 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/AppCommon.desktop.kt @@ -1,15 +1,19 @@ package chat.simplex.common.platform import chat.simplex.common.model.* +import chat.simplex.common.simplexWindowState import chat.simplex.common.views.call.RcvCallInvitation import chat.simplex.common.views.helpers.* import java.util.* import chat.simplex.res.MR +import java.io.File actual val appPlatform = AppPlatform.DESKTOP actual val deviceName = generalGetString(MR.strings.desktop_device) +actual fun isAppVisibleAndFocused() = simplexWindowState.windowFocused.value + @Suppress("ConstantLocale") val defaultLocale: Locale = Locale.getDefault() @@ -18,6 +22,7 @@ fun initApp() { override fun notifyCallInvitation(invitation: RcvCallInvitation): Boolean = chat.simplex.common.model.NtfManager.notifyCallInvitation(invitation) override fun hasNotificationsForChat(chatId: String): Boolean = chat.simplex.common.model.NtfManager.hasNotificationsForChat(chatId) override fun cancelNotificationsForChat(chatId: String) = chat.simplex.common.model.NtfManager.cancelNotificationsForChat(chatId) + override fun cancelNotificationsForUser(userId: Long) = chat.simplex.common.model.NtfManager.cancelNotificationsForUser(userId) override fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String?, actions: List Unit>>) = chat.simplex.common.model.NtfManager.displayNotification(user, chatId, displayName, msgText, image, actions) override fun androidCreateNtfChannelsMaybeShowAlert() {} override fun cancelCallNotification() {} @@ -26,7 +31,7 @@ fun initApp() { } applyAppLocale() if (DatabaseUtils.ksSelfDestructPassword.get() == null) { - initChatControllerAndRunMigrations() + initChatControllerOnStart() } // LALAL //testCrypto() diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt index 9b2368fcd3..eeeb13e5cc 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Files.desktop.kt @@ -13,8 +13,10 @@ actual val dataDir: File = File(desktopPlatform.dataPath) actual val tmpDir: File = File(System.getProperty("java.io.tmpdir") + File.separator + "simplex").also { it.deleteOnExit() } actual val filesDir: File = File(dataDir.absolutePath + File.separator + "simplex_v1_files") actual val appFilesDir: File = filesDir +actual val wallpapersDir: File = File(dataDir.absolutePath + File.separator + "simplex_v1_assets" + File.separator + "wallpapers").also { it.mkdirs() } actual val coreTmpDir: File = File(dataDir.absolutePath + File.separator + "tmp") actual val dbAbsolutePrefixPath: String = dataDir.absolutePath + File.separator + "simplex_v1" +actual val preferencesDir = File(desktopPlatform.configPath).also { it.parentFile.mkdirs() } actual val chatDatabaseFileName: String = "simplex_v1_chat.db" actual val agentDatabaseFileName: String = "simplex_v1_agent.db" @@ -24,9 +26,13 @@ actual val databaseExportDir: File = tmpDir actual val remoteHostsDir: File = File(dataDir.absolutePath + File.separator + "remote_hosts") actual fun desktopOpenDatabaseDir() { + desktopOpenDir(dataDir) +} + +actual fun desktopOpenDir(dir: File) { if (Desktop.isDesktopSupported()) { try { - Desktop.getDesktop().open(dataDir); + Desktop.getDesktop().open(dir); } catch (e: IOException) { Log.e(TAG, e.stackTraceToString()) AlertManager.shared.showAlertMsg( diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt index 9245f2b950..97f8bc129a 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Modifier.desktop.kt @@ -4,8 +4,7 @@ import androidx.compose.foundation.contextMenuOpenDetector import androidx.compose.runtime.Composable import androidx.compose.ui.* import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.input.pointer.PointerIcon -import androidx.compose.ui.input.pointer.pointerHoverIcon +import androidx.compose.ui.input.pointer.* import java.io.File import java.net.URI @@ -40,3 +39,8 @@ onExternalDrag(enabled) { actual fun Modifier.onRightClick(action: () -> Unit): Modifier = contextMenuOpenDetector { action() } actual fun Modifier.desktopPointerHoverIconHand(): Modifier = Modifier.pointerHoverIcon(PointerIcon.Hand) + +actual fun Modifier.desktopOnHovered(action: (Boolean) -> Unit): Modifier = + this then Modifier + .onPointerEvent(PointerEventType.Enter) { action(true) } + .onPointerEvent(PointerEventType.Exit) { action(false) } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt index 9217551a8d..97de08b07e 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Platform.desktop.kt @@ -8,12 +8,12 @@ private val unixConfigPath = (System.getenv("XDG_CONFIG_HOME") ?: "$home/.config private val unixDataPath = (System.getenv("XDG_DATA_HOME") ?: "$home/.local/share") + "/simplex" val desktopPlatform = detectDesktopPlatform() -enum class DesktopPlatform(val libExtension: String, val configPath: String, val dataPath: String) { - LINUX_X86_64("so", unixConfigPath, unixDataPath), - LINUX_AARCH64("so", unixConfigPath, unixDataPath), - WINDOWS_X86_64("dll", System.getenv("AppData") + File.separator + "SimpleX", System.getenv("AppData") + File.separator + "SimpleX"), - MAC_X86_64("dylib", unixConfigPath, unixDataPath), - MAC_AARCH64("dylib", unixConfigPath, unixDataPath); +enum class DesktopPlatform(val libExtension: String, val configPath: String, val dataPath: String, val githubAssetName: String) { + LINUX_X86_64("so", unixConfigPath, unixDataPath, "simplex-desktop-x86_64.AppImage"), + LINUX_AARCH64("so", unixConfigPath, unixDataPath, " simplex-desktop-aarch64.AppImage"), + WINDOWS_X86_64("dll", System.getenv("AppData") + File.separator + "SimpleX", System.getenv("AppData") + File.separator + "SimpleX", "simplex-desktop-windows-x86_64.msi"), + MAC_X86_64("dylib", unixConfigPath, unixDataPath, "simplex-desktop-macos-x86_64.dmg"), + MAC_AARCH64("dylib", unixConfigPath, unixDataPath, "simplex-desktop-macos-aarch64.dmg"); fun isLinux() = this == LINUX_X86_64 || this == LINUX_AARCH64 fun isWindows() = this == WINDOWS_X86_64 diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt index 3ca74a6d84..5b0db7c94a 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/PlatformTextField.desktop.kt @@ -1,9 +1,7 @@ package chat.simplex.common.platform -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.* @@ -17,8 +15,7 @@ import androidx.compose.ui.input.key.* import androidx.compose.ui.platform.* import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.input.KeyboardCapitalization -import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.input.* import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import chat.simplex.common.views.chat.* @@ -49,6 +46,8 @@ actual fun PlatformTextField( textStyle: MutableState, showDeleteTextButton: MutableState, userIsObserver: Boolean, + placeholder: String, + showVoiceButton: Boolean, onMessageChange: (String) -> Unit, onUpArrow: () -> Unit, onFilesPasted: (List) -> Unit, @@ -58,7 +57,6 @@ actual fun PlatformTextField( val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current val keyboard = LocalSoftwareKeyboardController.current - val padding = PaddingValues(12.dp, 12.dp, 45.dp, 0.dp) LaunchedEffect(cs.contextItem) { if (cs.contextItem !is ComposeContextItem.QuotedItem) return@LaunchedEffect // In replying state @@ -73,7 +71,20 @@ actual fun PlatformTextField( keyboard?.hide() } } - val isRtl = remember(cs.message) { isRtl(cs.message.subSequence(0, min(50, cs.message.length))) } + val lastTimeWasRtlByCharacters = remember { mutableStateOf(isRtl(cs.message.subSequence(0, min(50, cs.message.length)))) } + val isRtlByCharacters = remember(cs.message) { + if (cs.message.isNotEmpty()) isRtl(cs.message.subSequence(0, min(50, cs.message.length))) else lastTimeWasRtlByCharacters.value + } + LaunchedEffect(isRtlByCharacters) { + lastTimeWasRtlByCharacters.value = isRtlByCharacters + } + val isLtrGlobally = LocalLayoutDirection.current == LayoutDirection.Ltr + // Different padding here is for a text that is considered RTL with non-RTL locale set globally. + // In this case padding from right side should be bigger + val startEndPadding = if (cs.message.isEmpty() && showVoiceButton && isRtlByCharacters && isLtrGlobally) 95.dp else 50.dp + val startPadding = if (isRtlByCharacters && isLtrGlobally) startEndPadding else 0.dp + val endPadding = if (isRtlByCharacters && isLtrGlobally) 0.dp else startEndPadding + val padding = PaddingValues(startPadding, 12.dp, endPadding, 0.dp) var textFieldValueState by remember { mutableStateOf(TextFieldValue(text = cs.message)) } val textFieldValue = textFieldValueState.copy(text = cs.message) val clipboard = LocalClipboardManager.current @@ -165,28 +176,29 @@ actual fun PlatformTextField( }, cursorBrush = SolidColor(MaterialTheme.colors.secondary), decorationBox = { innerTextField -> - Surface( - shape = RoundedCornerShape(18.dp), - border = BorderStroke(1.dp, MaterialTheme.colors.secondary), - contentColor = LocalContentColor.current - ) { - Row( - Modifier.background(MaterialTheme.colors.background), - verticalAlignment = Alignment.Bottom + Row(verticalAlignment = Alignment.Bottom) { + CompositionLocalProvider( + LocalLayoutDirection provides if (isRtlByCharacters) LayoutDirection.Rtl else LocalLayoutDirection.current ) { - CompositionLocalProvider( - LocalLayoutDirection provides if (isRtl) LayoutDirection.Rtl else LocalLayoutDirection.current - ) { - Column(Modifier.weight(1f).padding(start = 12.dp, end = 32.dp)) { - Spacer(Modifier.height(8.dp)) - innerTextField() - Spacer(Modifier.height(10.dp)) - } + Column(Modifier.weight(1f).padding(start = startPadding, end = endPadding)) { + Spacer(Modifier.height(8.dp)) + TextFieldDefaults.TextFieldDecorationBox( + value = textFieldValue.text, + innerTextField = innerTextField, + placeholder = { Text(placeholder, style = textStyle.value.copy(color = MaterialTheme.colors.secondary)) }, + singleLine = false, + enabled = true, + isError = false, + trailingIcon = null, + interactionSource = remember { MutableInteractionSource() }, + contentPadding = PaddingValues(), + visualTransformation = VisualTransformation.None, + ) + Spacer(Modifier.height(10.dp)) } } } }, - ) showDeleteTextButton.value = cs.message.split("\n").size >= 4 && !cs.inProgress if (composeState.value.preview is ComposePreview.VoicePreview) { diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt index e1dba29f04..9f34891b37 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/RecAndPlay.desktop.kt @@ -25,18 +25,13 @@ actual class RecorderNative: RecorderInterface { } actual object AudioPlayer: AudioPlayerInterface { - val player by lazy { AudioPlayerComponent().mediaPlayer() } + private val player by lazy { AudioPlayerComponent().mediaPlayer() } - // Filepath: String, onProgressUpdate - private val currentlyPlaying: MutableState Unit>?> = mutableStateOf(null) + override val currentlyPlaying: MutableState = mutableStateOf(null) private var progressJob: Job? = null - enum class TrackState { - PLAYING, PAUSED, REPLACED - } - // Returns real duration of the track - private fun start(fileSource: CryptoFile, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? { + private fun start(fileSource: CryptoFile, smallView: Boolean, seek: Int? = null, onProgressUpdate: (position: Int?, state: TrackState) -> Unit): Int? { val absoluteFilePath = if (fileSource.isAbsolutePath) fileSource.filePath else getAppFilePath(fileSource.filePath) if (!File(absoluteFilePath).exists()) { Log.e(TAG, "No such file: ${fileSource.filePath}") @@ -46,7 +41,7 @@ actual object AudioPlayer: AudioPlayerInterface { VideoPlayerHolder.stopAll() RecorderInterface.stopRecording?.invoke() val current = currentlyPlaying.value - if (current == null || current.first != fileSource || !player.status().isPlayable) { + if (current == null || current.fileSource != fileSource || !player.status().isPlayable || smallView != current.smallView) { stopListener() player.stop() runCatching { @@ -66,7 +61,7 @@ actual object AudioPlayer: AudioPlayerInterface { } if (seek != null) player.seekTo(seek) player.start() - currentlyPlaying.value = fileSource to onProgressUpdate + currentlyPlaying.value = CurrentlyPlayingState(fileSource, onProgressUpdate, smallView) progressJob = CoroutineScope(Dispatchers.Default).launch { onProgressUpdate(player.currentPosition, TrackState.PLAYING) while(isActive && (player.isPlaying || player.status().state() == State.OPENING)) { @@ -80,7 +75,11 @@ actual object AudioPlayer: AudioPlayerInterface { onProgressUpdate(player.currentPosition, TrackState.PLAYING) } onProgressUpdate(null, TrackState.PAUSED) - currentlyPlaying.value?.first?.deleteTmpFile() + currentlyPlaying.value?.fileSource?.deleteTmpFile() + // Since coroutine is still NOT canceled, means player ended (no stop/no pause). + if (smallView && isActive) { + stopListener() + } } return player.duration } @@ -103,7 +102,7 @@ actual object AudioPlayer: AudioPlayerInterface { // FileName or filePath are ok override fun stop(fileName: String?) { - if (fileName != null && currentlyPlaying.value?.first?.filePath?.endsWith(fileName) == true) { + if (fileName != null && currentlyPlaying.value?.fileSource?.filePath?.endsWith(fileName) == true) { stop() } } @@ -111,8 +110,8 @@ actual object AudioPlayer: AudioPlayerInterface { private fun stopListener() { val afterCoroutineCancel: CompletionHandler = { // Notify prev audio listener about stop - currentlyPlaying.value?.second?.invoke(null, TrackState.REPLACED) - currentlyPlaying.value?.first?.deleteTmpFile() + currentlyPlaying.value?.onProgressUpdate?.invoke(null, TrackState.REPLACED) + currentlyPlaying.value?.fileSource?.deleteTmpFile() currentlyPlaying.value = null } /** Preventing race by calling a code AFTER coroutine ends, so [TrackState] will be: @@ -133,11 +132,12 @@ actual object AudioPlayer: AudioPlayerInterface { progress: MutableState, duration: MutableState, resetOnEnd: Boolean, + smallView: Boolean, ) { if (progress.value == duration.value) { progress.value = 0 } - val realDuration = start(fileSource, progress.value) { pro, state -> + val realDuration = start(fileSource, smallView = smallView, progress.value) { pro, state -> if (pro != null) { progress.value = pro } @@ -162,7 +162,7 @@ actual object AudioPlayer: AudioPlayerInterface { override fun seekTo(ms: Int, pro: MutableState, filePath: String?) { pro.value = ms - if (currentlyPlaying.value?.first?.filePath == filePath) { + if (currentlyPlaying.value?.fileSource?.filePath == filePath) { player.seekTo(ms) } } @@ -217,7 +217,7 @@ actual object SoundPlayer: SoundPlayerInterface { playing = true scope.launch { while (playing && sound) { - AudioPlayer.play(CryptoFile.plain(tmpFile.absolutePath), mutableStateOf(true), mutableStateOf(0), mutableStateOf(0), true) + AudioPlayer.play(CryptoFile.plain(tmpFile.absolutePath), mutableStateOf(true), mutableStateOf(0), mutableStateOf(0), resetOnEnd = true, smallView = false) delay(3500) } } @@ -239,7 +239,7 @@ actual object CallSoundsPlayer: CallSoundsPlayerInterface { SoundPlayer::class.java.getResource(soundPath)!!.openStream()!!.use { it.copyTo(tmpFile.outputStream()) } playingJob = scope.launch { while (isActive) { - AudioPlayer.play(CryptoFile.plain(tmpFile.absolutePath), mutableStateOf(true), mutableStateOf(0), mutableStateOf(0), true) + AudioPlayer.play(CryptoFile.plain(tmpFile.absolutePath), mutableStateOf(true), mutableStateOf(0), mutableStateOf(0), resetOnEnd = true, smallView = false) delay(delay) } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt index b758988227..951185dc98 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/Resources.desktop.kt @@ -1,16 +1,23 @@ package chat.simplex.common.platform import androidx.compose.runtime.* +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.toComposeImageBitmap import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.* import chat.simplex.common.simplexWindowState +import chat.simplex.common.views.helpers.* +import com.jthemedetecor.OsThemeDetector import com.russhwolf.settings.* +import dev.icerock.moko.resources.ImageResource import dev.icerock.moko.resources.StringResource import dev.icerock.moko.resources.desc.desc +import kotlinx.coroutines.* import java.io.File import java.util.* +import java.util.concurrent.Executors @Composable actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font = @@ -18,7 +25,15 @@ actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle) actual fun StringResource.localized(): String = desc().toString() -actual fun isInNightMode() = false +private val detector: OsThemeDetector = OsThemeDetector.getDetector() +actual fun isInNightMode() = try { + detector.isDark +} +catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) + /* On Mac this code can produce exception */ + false +} private val settingsFile = File(desktopPlatform.configPath + File.separator + "settings.properties") @@ -26,15 +41,31 @@ private val settingsFile = private val settingsThemesFile = File(desktopPlatform.configPath + File.separator + "themes.properties") .also { it.parentFile.mkdirs() } + private val settingsProps = Properties() - .also { try { it.load(settingsFile.reader()) } catch (e: Exception) { Properties() } } + .also { props -> + if (!settingsFile.exists()) return@also + + try { + settingsFile.reader().use { + // Force exception to happen + //it.close() + props.load(it) + } + } catch (e: Exception) { + Log.e(TAG, "Error reading settings file: ${e.stackTraceToString()}") + } + } private val settingsThemesProps = Properties() - .also { try { it.load(settingsThemesFile.reader()) } catch (e: Exception) { Properties() } } + .also { props -> try { settingsThemesFile.reader().use { props.load(it) } } catch (e: Exception) { /**/ } } -actual val settings: Settings = PropertiesSettings(settingsProps) { settingsProps.store(settingsFile.writer(), "") } -actual val settingsThemes: Settings = PropertiesSettings(settingsThemesProps) { settingsThemesProps.store(settingsThemesFile.writer(), "") } + +private val settingsWriterThread = Executors.newSingleThreadExecutor().asCoroutineDispatcher() + +actual val settings: Settings = PropertiesSettings(settingsProps) { CoroutineScope(settingsWriterThread).launch { settingsFile.writer().use { settingsProps.store(it, "") } } } +actual val settingsThemes: Settings = PropertiesSettings(settingsThemesProps) { CoroutineScope(settingsWriterThread).launch { settingsThemesFile.writer().use { settingsThemesProps.store(it, "") } } } actual fun windowOrientation(): WindowOrientation = if (simplexWindowState.windowState.size.width > simplexWindowState.windowState.size.height) { @@ -46,6 +77,9 @@ actual fun windowOrientation(): WindowOrientation = @Composable actual fun windowWidth(): Dp = simplexWindowState.windowState.size.width +@Composable +actual fun windowHeight(): Dp = simplexWindowState.windowState.size.height + actual fun desktopExpandWindowToWidth(width: Dp) { if (simplexWindowState.windowState.size.width >= width) return simplexWindowState.windowState.size = simplexWindowState.windowState.size.copy(width = width) @@ -58,3 +92,6 @@ actual fun isRtl(text: CharSequence): Boolean { dir == Character.DIRECTIONALITY_RIGHT_TO_LEFT || dir == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC } } + +actual fun ImageResource.toComposeImageBitmap(): ImageBitmap? = + image.toComposeImageBitmap() diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt index 1caf96902d..e6b26f9290 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/platform/ScrollableColumn.desktop.kt @@ -13,16 +13,18 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.* import androidx.compose.ui.unit.dp -import chat.simplex.common.views.helpers.detectCursorMove -import chat.simplex.common.views.helpers.mixWith +import chat.simplex.common.views.helpers.* import kotlinx.coroutines.* +import kotlinx.coroutines.flow.filter +import kotlin.math.absoluteValue @Composable actual fun LazyColumnWithScrollBar( modifier: Modifier, - state: LazyListState, + state: LazyListState?, contentPadding: PaddingValues, reverseLayout: Boolean, verticalArrangement: Arrangement.Vertical, @@ -30,42 +32,6 @@ actual fun LazyColumnWithScrollBar( flingBehavior: FlingBehavior, userScrollEnabled: Boolean, content: LazyListScope.() -> Unit -) { - if (appPlatform.isAndroid) { - LazyColumn(modifier, state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) - } else { - val scope = rememberCoroutineScope() - val scrollBarAlpha = remember { Animatable(0f) } - val scrollJob: MutableState = remember { mutableStateOf(Job()) } - val scrollModifier = remember { - Modifier - .pointerInput(Unit) { - detectCursorMove { - scope.launch { - scrollBarAlpha.animateTo(1f) - } - scrollJob.value.cancel() - scrollJob.value = scope.launch { - delay(1000L) - scrollBarAlpha.animateTo(0f) - } - } - } - } - LazyColumn(modifier.then(if (appPlatform.isDesktop) scrollModifier else Modifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) { - DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.align(Alignment.CenterEnd).fillMaxHeight(), scrollBarAlpha, scrollJob, reverseLayout) - } - } -} - -@Composable -actual fun ColumnWithScrollBar( - modifier: Modifier, - verticalArrangement: Arrangement.Vertical, - horizontalAlignment: Alignment.Horizontal, - state: ScrollState, - content: @Composable ColumnScope.() -> Unit ) { val scope = rememberCoroutineScope() val scrollBarAlpha = remember { Animatable(0f) } @@ -85,20 +51,95 @@ actual fun ColumnWithScrollBar( } } } - Column(modifier.verticalScroll(state).then(scrollModifier), verticalArrangement, horizontalAlignment, content) - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) { - DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.align(Alignment.CenterEnd).fillMaxHeight(), scrollBarAlpha, scrollJob, false) + val state = state ?: LocalAppBarHandler.current?.listState ?: rememberLazyListState() + val connection = LocalAppBarHandler.current?.connection + // When scroll bar is dragging, there is no scroll event in nested scroll modifier. So, listen for changes on lazy column state + // (only first visible row is useful because LazyColumn doesn't have absolute scroll position, only relative to row) + val scrollBarDraggingState = remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + snapshotFlow { state.firstVisibleItemScrollOffset } + .filter { state.firstVisibleItemIndex == 0 } + .collect { scrollPosition -> + val offset = connection?.appBarOffset + if (offset != null && ((offset + scrollPosition).absoluteValue > 1 || scrollBarDraggingState.value)) { + connection.appBarOffset = -scrollPosition.toFloat() +// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") + } + } + } + Box(if (connection != null) Modifier.nestedScroll(connection) else Modifier) { + LazyColumn(modifier.then(scrollModifier), state, contentPadding, reverseLayout, verticalArrangement, horizontalAlignment, flingBehavior, userScrollEnabled, content) + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) { + DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, reverseLayout, scrollBarDraggingState) + } } } @Composable -fun DesktopScrollBar(adapter: androidx.compose.foundation.v2.ScrollbarAdapter, modifier: Modifier, scrollBarAlpha: Animatable, scrollJob: MutableState, reversed: Boolean) { +actual fun ColumnWithScrollBar( + modifier: Modifier, + verticalArrangement: Arrangement.Vertical, + horizontalAlignment: Alignment.Horizontal, + state: ScrollState?, + maxIntrinsicSize: Boolean, + content: @Composable() (ColumnScope.() -> Unit) +) { + val scope = rememberCoroutineScope() + val scrollBarAlpha = remember { Animatable(0f) } + val scrollJob: MutableState = remember { mutableStateOf(Job()) } + val scrollModifier = remember { + Modifier + .pointerInput(Unit) { + detectCursorMove { + scope.launch { + scrollBarAlpha.animateTo(1f) + } + scrollJob.value.cancel() + scrollJob.value = scope.launch { + delay(1000L) + scrollBarAlpha.animateTo(0f) + } + } + } + } + val state = state ?: LocalAppBarHandler.current?.scrollState ?: rememberScrollState() + val connection = LocalAppBarHandler.current?.connection + // When scroll bar is dragging, there is no scroll event in nested scroll modifier. So, listen for changes on column state + // (exact scroll position is available but in Int, not Float) + val scrollBarDraggingState = remember { mutableStateOf(false) } + LaunchedEffect(Unit) { + snapshotFlow { state.value } + .collect { scrollPosition -> + val offset = connection?.appBarOffset + if (offset != null && ((offset + scrollPosition).absoluteValue > 1 || scrollBarDraggingState.value)) { + connection.appBarOffset = -scrollPosition.toFloat() +// Log.d(TAG, "Scrolling position changed from $offset to ${connection.appBarOffset}") + } + } + } + Box(if (connection != null) Modifier.nestedScroll(connection) else Modifier) { + Column( + if (maxIntrinsicSize) { + modifier.verticalScroll(state).height(IntrinsicSize.Max).then(scrollModifier) + } else { + modifier.verticalScroll(state).then(scrollModifier) + }, + verticalArrangement, horizontalAlignment, content) + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.CenterEnd) { + DesktopScrollBar(rememberScrollbarAdapter(state), Modifier.fillMaxHeight(), scrollBarAlpha, scrollJob, false, scrollBarDraggingState) + } + } +} + +@Composable +fun DesktopScrollBar(adapter: androidx.compose.foundation.v2.ScrollbarAdapter, modifier: Modifier, scrollBarAlpha: Animatable, scrollJob: MutableState, reversed: Boolean, updateDraggingState: MutableState = remember { mutableStateOf(false) }) { val scope = rememberCoroutineScope() val interactionSource = remember { MutableInteractionSource() } val isHovered by interactionSource.collectIsHoveredAsState() val isDragged by interactionSource.collectIsDraggedAsState() LaunchedEffect(isHovered, isDragged) { scrollJob.value.cancel() + updateDraggingState.value = isDragged if (isHovered || isDragged) { scrollBarAlpha.animateTo(1f) } else { diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt index 358c20d769..d7dc1ca859 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/ui/theme/Theme.desktop.kt @@ -10,6 +10,10 @@ private val detector: OsThemeDetector = OsThemeDetector.getDetector() registerListener(::reactOnDarkThemeChanges) } +// TODO: explore possibility to use +//@Composable +//actual fun isSystemInDarkTheme(): Boolean = androidx.compose.foundation.isSystemInDarkTheme() + @Composable actual fun isSystemInDarkTheme(): Boolean = try { detector.isDark diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt index d3bf1bf01e..d6331616cc 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt @@ -25,11 +25,6 @@ val connections = ArrayList() @Composable actual fun ActiveCallView() { - val endCall = { - val call = chatModel.activeCall.value - if (call != null) withBGApi { chatModel.callManager.endCall(call) } - } - BackHandler(onBack = endCall) val scope = rememberCoroutineScope() WebRTCController(chatModel.callCommand) { apiMsg -> Log.d(TAG, "received from WebRTCController: $apiMsg") diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.desktop.kt index 6da2078567..38054cb873 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chat/item/CIImageView.desktop.kt @@ -13,6 +13,7 @@ actual fun SimpleAndAnimatedImageView( imageBitmap: ImageBitmap, file: CIFile?, imageProvider: () -> ImageGalleryProvider, + smallView: Boolean, ImageView: @Composable (painter: Painter, onClick: () -> Unit) -> Unit ) { // LALAL make it animated too diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt index c2333393e5..9e4eeb0c96 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.desktop.kt @@ -22,53 +22,67 @@ import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.flow.MutableStateFlow @Composable -actual fun ActiveCallInteractiveArea(call: Call, newChatSheetState: MutableStateFlow) { - // if (call.callState == CallState.Connected && !newChatSheetState.collectAsState().value.isVisible()) { - if (!newChatSheetState.collectAsState().value.isVisible()) { - val showMenu = remember { mutableStateOf(false) } - val media = call.peerMedia ?: call.localMedia - CompositionLocalProvider( - LocalIndication provides NoIndication - ) { - Box( - Modifier - .fillMaxSize(), - contentAlignment = Alignment.BottomEnd - ) { - Box( - Modifier - .padding(end = 71.dp, bottom = 92.dp) - .size(67.dp) - .combinedClickable(onClick = { - val chat = chatModel.getChat(call.contact.id) - if (chat != null) { - withBGApi { - openChat(chat.remoteHostId, chat.chatInfo, chatModel) - } - } - }, - onLongClick = { showMenu.value = true }) - .onRightClick { showMenu.value = true }, - contentAlignment = Alignment.Center - ) { - Box(Modifier.background(MaterialTheme.colors.background, CircleShape)) { - ProfileImageForActiveCall(size = 56.dp, image = call.contact.profile.image) - } - Box(Modifier.padding().background(SimplexGreen, CircleShape).padding(4.dp).align(Alignment.TopEnd)) { - if (media == CallMediaType.Video) { - Icon(painterResource(MR.images.ic_videocam_filled), stringResource(MR.strings.icon_descr_video_call), Modifier.size(18.dp), tint = Color.White) - } else { - Icon(painterResource(MR.images.ic_call_filled), stringResource(MR.strings.icon_descr_audio_call), Modifier.size(18.dp), tint = Color.White) +actual fun ActiveCallInteractiveArea(call: Call) { + val showMenu = remember { mutableStateOf(false) } + val media = call.peerMedia ?: call.localMedia + CompositionLocalProvider( + LocalIndication provides NoIndication + ) { + Box( + Modifier + .fillMaxSize(), + contentAlignment = Alignment.BottomEnd + ) { + Box( + Modifier + .padding(end = 71.dp, bottom = 92.dp) + .size(67.dp) + .combinedClickable(onClick = { + val chat = chatModel.getChat(call.contact.id) + if (chat != null) { + withBGApi { + openChat(chat.remoteHostId, chat.chatInfo, chatModel) } } - DefaultDropdownMenu(showMenu) { - ItemAction(stringResource(MR.strings.icon_descr_hang_up), painterResource(MR.images.ic_call_end_filled), color = MaterialTheme.colors.error, onClick = { - withBGApi { chatModel.callManager.endCall(call) } - showMenu.value = false - }) - } + }, + onLongClick = { showMenu.value = true }) + .onRightClick { showMenu.value = true }, + contentAlignment = Alignment.Center + ) { + Box(Modifier.background(MaterialTheme.colors.background, CircleShape)) { + ProfileImageForActiveCall(size = 56.dp, image = call.contact.profile.image) + } + Box( + Modifier.padding().background(SimplexGreen, CircleShape).padding(4.dp) + .align(Alignment.TopEnd) + ) { + if (media == CallMediaType.Video) { + Icon( + painterResource(MR.images.ic_videocam_filled), + stringResource(MR.strings.icon_descr_video_call), + Modifier.size(18.dp), + tint = Color.White + ) + } else { + Icon( + painterResource(MR.images.ic_call_filled), + stringResource(MR.strings.icon_descr_audio_call), + Modifier.size(18.dp), + tint = Color.White + ) } } + DefaultDropdownMenu(showMenu) { + ItemAction( + stringResource(MR.strings.icon_descr_hang_up), + painterResource(MR.images.ic_call_end_filled), + color = MaterialTheme.colors.error, + onClick = { + withBGApi { chatModel.callManager.endCall(call) } + showMenu.value = false + }) + } } } + } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt new file mode 100644 index 0000000000..faef957705 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/AppUpdater.kt @@ -0,0 +1,394 @@ +package chat.simplex.common.views.helpers + +import SectionItemView +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import chat.simplex.common.BuildConfigCommon +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.ChatController.getNetCfg +import chat.simplex.common.model.json +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.WarningOrange +import chat.simplex.common.views.onboarding.ReadMoreButton +import chat.simplex.res.MR +import kotlinx.coroutines.* +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import okhttp3.OkHttpClient +import okhttp3.Request +import java.io.Closeable +import java.io.File +import java.net.InetSocketAddress +import java.net.Proxy + +@Serializable +data class GitHubRelease( + @SerialName("tag_name") + val tagName: String, + @SerialName("html_url") + val htmlUrl: String, + val name: String, + val draft: Boolean, + val prerelease: Boolean, + val body: String, + @SerialName("published_at") + val publishedAt: String, + val assets: List +) + +@Serializable +data class GitHubAsset( + @SerialName("browser_download_url") + val browserDownloadUrl: String, + val name: String, + val size: Long, + + val isAppImage: Boolean = name.lowercase().contains(".appimage") +) + +fun showAppUpdateNotice() { + AlertManager.shared.showAlertDialogButtonsColumn( + generalGetString(MR.strings.app_check_for_updates_notice_title), + text = generalGetString(MR.strings.app_check_for_updates_notice_desc), + buttons = { + Column { + SectionItemView({ + AlertManager.shared.hideAlert() + appPrefs.appUpdateChannel.set(AppUpdatesChannel.STABLE) + setupUpdateChecker() + }) { + Text(generalGetString(MR.strings.app_check_for_updates_stable), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + + SectionItemView({ + AlertManager.shared.hideAlert() + appPrefs.appUpdateChannel.set(AppUpdatesChannel.BETA) + setupUpdateChecker() + }) { + Text(generalGetString(MR.strings.app_check_for_updates_beta), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + + SectionItemView({ + AlertManager.shared.hideAlert() + appPrefs.appUpdateChannel.set(AppUpdatesChannel.DISABLED) + }) { + Text(generalGetString(MR.strings.app_check_for_updates_notice_disable), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + ) +} + +private var updateCheckerJob: Job = Job() +fun setupUpdateChecker() = withLongRunningApi { + updateCheckerJob.cancel() + if (appPrefs.appUpdateChannel.get() == AppUpdatesChannel.DISABLED) { + return@withLongRunningApi + } + checkForUpdate() + createUpdateJob() +} + +private fun createUpdateJob() { + updateCheckerJob = withLongRunningApi { + delay(24 * 60 * 60 * 1000) + checkForUpdate() + createUpdateJob() + } +} + + +fun checkForUpdate() { + Log.d(TAG, "Checking for update") + val client = setupHttpClient() + try { + val request = Request.Builder().url("https://api.github.com/repos/simplex-chat/simplex-chat/releases").addHeader("User-agent", "curl").build() + client.newCall(request).execute().use { response -> + response.body?.use { + val body = it.string() + val releases = json.decodeFromString>(body).filterNot { it.draft } + val release = when (appPrefs.appUpdateChannel.get()) { + AppUpdatesChannel.STABLE -> releases.firstOrNull { !it.prerelease } + AppUpdatesChannel.BETA -> releases.firstOrNull() + AppUpdatesChannel.DISABLED -> return + } ?: return + val currentVersionName = "v" + (if (appPlatform.isAndroid) BuildConfigCommon.ANDROID_VERSION_NAME else BuildConfigCommon.DESKTOP_VERSION_NAME) + val redactedCurrentVersionName = when { + currentVersionName.contains('-') && currentVersionName.substringBefore('-').count { it == '.' } == 1 -> "${currentVersionName.substringBefore('-')}.0-${currentVersionName.substringAfter('-')}" + currentVersionName.substringBefore('-').count { it == '.' } == 1 -> "${currentVersionName}.0" + else -> currentVersionName + } + if (release.tagName == appPrefs.appSkippedUpdate.get() || release.tagName == currentVersionName || release.tagName == redactedCurrentVersionName) { + Log.d(TAG, "Skipping update because of the same version or skipped version") + return + } + val assets = chooseGitHubReleaseAssets(release) + // No need to show an alert if no suitable packages were found. But for Flatpak users it's useful to see release notes anyway + if (assets.isEmpty() && !isRunningFromFlatpak()) { + Log.d(TAG, "No assets to download for current system") + return + } + val lines = ArrayList() + for (line in release.body.lines()) { + if (line == "Commits:") break + lines.add(line) + } + val text = lines.joinToString("\n") + AlertManager.shared.showAlertDialogButtonsColumn( + generalGetString(MR.strings.app_check_for_updates_update_available).format(release.name), + text = text, + textAlign = TextAlign.Start, + dismissible = false, + belowTextContent = { + ReadMoreButton(release.htmlUrl) + }, + buttons = { + Column { + for (asset in assets) { + SectionItemView({ + AlertManager.shared.hideAlert() + chatModel.updatingProgress.value = 0f + withLongRunningApi { + try { + downloadAsset(asset) + } finally { + chatModel.updatingProgress.value = null + } + } + }) { + Text( + generalGetString(MR.strings.app_check_for_updates_button_download).format( + if (asset.name.length > 34) "…" + asset.name.substringAfter("simplex-desktop-") else asset.name, + formatBytes(asset.size)), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary + ) + } + } + + SectionItemView({ + AlertManager.shared.hideAlert() + skipRelease(release) + }) { + Text(generalGetString(MR.strings.app_check_for_updates_button_skip), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = WarningOrange) + } + + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(generalGetString(MR.strings.app_check_for_updates_button_remind_later), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + ) + } + } + } catch (e: Exception) { + Log.e(TAG, "Failed to get the latest release: ${e.stackTraceToString()}") + } +} + +private fun setupHttpClient(): OkHttpClient { + val netCfg = getNetCfg() + var proxy: Proxy? = null + if (netCfg.useSocksProxy && netCfg.socksProxy != null) { + val hostname = netCfg.socksProxy.substringBefore(":").ifEmpty { "localhost" } + val port = netCfg.socksProxy.substringAfter(":").toIntOrNull() + if (port != null) { + proxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress(hostname, port)) + } + } + return OkHttpClient.Builder().proxy(proxy).followRedirects(true).build() +} + +private fun skipRelease(release: GitHubRelease) { + appPrefs.appSkippedUpdate.set(release.tagName) +} + +private suspend fun downloadAsset(asset: GitHubAsset) { + withContext(Dispatchers.Main) { + showToast(generalGetString(MR.strings.app_check_for_updates_download_started)) + } + val progressListener = object: ProgressListener { + override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { + if (contentLength != -1L) { + chatModel.updatingProgress.value = if (done) 1f else bytesRead / contentLength.toFloat() + } + } + } + val client = setupHttpClient().newBuilder() + .addNetworkInterceptor { chain -> + val originalResponse = chain.proceed(chain.request()) + val body = originalResponse.body + if (body != null) { + originalResponse.newBuilder().body(ProgressResponseBody(body, progressListener)).build() + } else { + originalResponse + } + } + .build() + + try { + val request = Request.Builder().url(asset.browserDownloadUrl).addHeader("User-agent", "curl").build() + val call = client.newCall(request) + chatModel.updatingRequest = Closeable { + call.cancel() + withApi { + showToast(generalGetString(MR.strings.app_check_for_updates_canceled)) + } + } + call.execute().use { response -> + response.body?.use { body -> + body.byteStream().use { stream -> + createTmpFileAndDelete { file -> + // It's important to close output stream (with use{}), otherwise, Windows cannot rename the file + file.outputStream().use { output -> + stream.copyTo(output) + } + val newFile = File(file.parentFile, asset.name) + file.renameTo(newFile) + + AlertManager.shared.showAlertDialogButtonsColumn( + generalGetString(MR.strings.app_check_for_updates_download_completed_title), + dismissible = false, + buttons = { + Column { + // It's problematic to install .deb package because it requires either root or GUI package installer which is not available on + // Debian by default. Let the user install it manually only + if (!asset.name.lowercase().endsWith(".deb")) { + SectionItemView({ + AlertManager.shared.hideAlert() + chatModel.updatingProgress.value = -1f + withLongRunningApi { + try { + installAppUpdate(newFile) + } finally { + chatModel.updatingProgress.value = null + } + } + }) { + Text(generalGetString(MR.strings.app_check_for_updates_button_install), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + + SectionItemView({ + desktopOpenDir(newFile.parentFile) + }) { + Text(generalGetString(MR.strings.app_check_for_updates_button_open), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + + SectionItemView({ + AlertManager.shared.hideAlert() + newFile.delete() + }) { + Text(generalGetString(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) + } + } + } + ) + } + } + } + } + } catch (e: Exception) { + Log.e(TAG, "Failed to download the asset from release: ${e.stackTraceToString()}") + } +} + +private fun isRunningFromFlatpak(): Boolean = System.getenv("container") == "flatpak" + +private fun chooseGitHubReleaseAssets(release: GitHubRelease): List { + val res = if (isRunningFromFlatpak()) { + // No need to show download options for Flatpak users + emptyList() + } else if (Runtime.getRuntime().exec("which dpkg").onExit().join().exitValue() == 0) { + // Show all available .deb packages and user will choose the one that works on his system (for Debian derivatives) + release.assets.filter { it.name.lowercase().endsWith(".deb") } + } else { + release.assets.filter { it.name == desktopPlatform.githubAssetName } + } + return res +} + +private suspend fun installAppUpdate(file: File) = withContext(Dispatchers.IO) { + when { + desktopPlatform.isLinux() -> { + val process = Runtime.getRuntime().exec("xdg-open ${file.absolutePath}").onExit().join() + val startedInstallation = process.exitValue() == 0 && process.children().count() > 0 + if (!startedInstallation) { + Log.e(TAG, "Error starting installation: ${process.inputReader().use { it.readLines().joinToString("\n") }}${process.errorStream.use { String(it.readAllBytes()) }}") + // Failed to start installation. show directory with the file for manual installation + desktopOpenDir(file.parentFile) + } else { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.app_check_for_updates_installed_successfully_title), + text = generalGetString(MR.strings.app_check_for_updates_installed_successfully_desc) + ) + file.delete() + } + } + desktopPlatform.isWindows() -> { + val process = Runtime.getRuntime().exec("msiexec /i ${file.absolutePath}"/* /qb */).onExit().join() + val startedInstallation = process.exitValue() == 0 + if (!startedInstallation) { + Log.e(TAG, "Error starting installation: ${process.inputReader().use { it.readLines().joinToString("\n") }}${process.errorStream.use { String(it.readAllBytes()) }}") + // Failed to start installation. show directory with the file for manual installation + desktopOpenDir(file.parentFile) + } else { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.app_check_for_updates_installed_successfully_title), + text = generalGetString(MR.strings.app_check_for_updates_installed_successfully_desc) + ) + file.delete() + } + } + desktopPlatform.isMac() -> { + // Default mount point if no other DMGs were mounted before + var volume = "/Volumes/SimpleX" + try { + val process = Runtime.getRuntime().exec("hdiutil mount ${file.absolutePath}").onExit().join() + val startedInstallation = process.exitValue() == 0 + val lines = process.inputReader().use { it.readLines() } + // This is needed for situations when mount point has non-default path. + // For example, when a user already had mounted SimpleX.dmg before and default mount point is not available. + // Mac will make volume like /Volumes/SimpleX 1 + val lastLine = lines.lastOrNull()?.substringAfterLast('\t') + if (!startedInstallation || lastLine == null || !lastLine.lowercase().contains("/volumes/")) { + Log.e(TAG, "Error starting installation: ${process.inputReader().use { it.readLines().joinToString("\n") }}${process.errorStream.use { String(it.readAllBytes()) }}") + // Failed to start installation. show directory with the file for manual installation + desktopOpenDir(file.parentFile) + return@withContext + } + volume = lastLine + File("/Applications/SimpleX.app").renameTo(File("/Applications/SimpleX-old.app")) + val process2 = Runtime.getRuntime().exec(arrayOf("cp", "-R", "${volume}/SimpleX.app", "/Applications")).onExit().join() + val copiedSuccessfully = process2.exitValue() == 0 + if (!copiedSuccessfully) { + Log.e(TAG, "Error copying the app: ${process2.inputReader().use { it.readLines().joinToString("\n") }}${process2.errorStream.use { String(it.readAllBytes()) }}") + // Failed to start installation. show directory with the file for manual installation + desktopOpenDir(file.parentFile) + } else { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.app_check_for_updates_installed_successfully_title), + text = generalGetString(MR.strings.app_check_for_updates_installed_successfully_desc) + ) + file.delete() + } + } finally { + try { + Runtime.getRuntime().exec(arrayOf("hdiutil", "unmount", volume)).onExit().join() + } finally { + if (!File("/Applications/SimpleX.app").exists()) { + File("/Applications/SimpleX-old.app").renameTo(File("/Applications/SimpleX.app")) + } else { + Runtime.getRuntime().exec("rm -rf /Applications/SimpleX-old.app").onExit().join() + } + } + } + } + } + Unit +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/OkHttpProgressListener.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/OkHttpProgressListener.kt new file mode 100644 index 0000000000..24fa0b8ef1 --- /dev/null +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/helpers/OkHttpProgressListener.kt @@ -0,0 +1,46 @@ +package chat.simplex.common.views.helpers + +import okhttp3.MediaType +import okhttp3.ResponseBody +import okio.* + +// https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/Progress.java +class ProgressResponseBody( + val responseBody: ResponseBody, + val progressListener: ProgressListener +): ResponseBody() { + private var bufferedSource: BufferedSource? = null + + override fun contentType(): MediaType? { + return responseBody.contentType() + } + + override fun contentLength(): Long { + return responseBody.contentLength() + } + + override fun source(): BufferedSource { + if (bufferedSource == null) { + bufferedSource = source(responseBody.source()).buffer() + } + return bufferedSource!! + } + + private fun source(source: Source): Source { + return object: ForwardingSource(source) { + var totalBytesRead = 0L + + override fun read(sink: Buffer, byteCount: Long): Long { + val bytesRead = super.read(sink, byteCount) + // read() returns the number of bytes read, or -1 if this source is exhausted. + totalBytesRead += if (bytesRead != -1L) bytesRead else 0L + progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1L) + return bytesRead; + } + } + } +} + +interface ProgressListener { + fun update(bytesRead: Long, contentLength: Long, done: Boolean); +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.desktop.kt index 74c490445e..1b9582b7d2 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/onboarding/SimpleXInfo.desktop.kt @@ -1,8 +1,9 @@ package chat.simplex.common.views.onboarding -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.* import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import chat.simplex.common.model.ChatModel.controller import chat.simplex.common.model.SharedPreference import chat.simplex.common.model.User @@ -15,10 +16,10 @@ import dev.icerock.moko.resources.compose.painterResource actual fun OnboardingActionButton(user: User?, onboardingStage: SharedPreference, onclick: (() -> Unit)?) { if (user == null) { Row(horizontalArrangement = Arrangement.spacedBy(DEFAULT_PADDING * 2.5f)) { - OnboardingActionButton(MR.strings.link_a_mobile, onboarding = if (controller.appPrefs.initialRandomDBPassphrase.get() && !chatModel.desktopOnboardingRandomPassword.value) OnboardingStage.Step2_5_SetupDatabasePassphrase else OnboardingStage.LinkAMobile, true, icon = painterResource(MR.images.ic_smartphone_300), onclick = onclick) - OnboardingActionButton(MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, true, icon = painterResource(MR.images.ic_desktop), onclick = onclick) + OnboardingActionButton(labelId = MR.strings.link_a_mobile, onboarding = if (controller.appPrefs.initialRandomDBPassphrase.get() && !chatModel.desktopOnboardingRandomPassword.value) OnboardingStage.Step2_5_SetupDatabasePassphrase else OnboardingStage.LinkAMobile, icon = painterResource(MR.images.ic_smartphone_300), onclick = onclick) + OnboardingActionButton(labelId = MR.strings.create_your_profile, onboarding = OnboardingStage.Step2_CreateProfile, icon = painterResource(MR.images.ic_desktop), onclick = onclick) } } else { - OnboardingActionButton(MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, true, onclick = onclick) + OnboardingActionButton(Modifier.widthIn(min = 300.dp), labelId = MR.strings.make_private_connection, onboarding = OnboardingStage.OnboardingComplete, onclick = onclick) } } diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt index 4e4846bc9f..38ffb137ed 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/Appearance.desktop.kt @@ -3,35 +3,35 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionDividerSpaced import SectionView +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel import chat.simplex.common.model.SharedPreference -import chat.simplex.common.platform.ColumnWithScrollBar -import chat.simplex.common.platform.defaultLocale -import chat.simplex.common.ui.theme.ThemeColor +import chat.simplex.common.platform.* +import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.helpers.* -import chat.simplex.common.views.usersettings.AppearanceScope.ColorEditor import chat.simplex.res.MR import dev.icerock.moko.resources.compose.stringResource import kotlinx.coroutines.delay import java.util.Locale +import kotlin.math.roundToInt @Composable -actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit)) { +actual fun AppearanceView(m: ChatModel) { AppearanceScope.AppearanceLayout( m.controller.appPrefs.appLanguage, m.controller.appPrefs.systemDarkTheme, - showSettingsModal = showSettingsModal, - editColor = { name, initialColor -> - ModalManager.start.showModalCloseable { close -> - ColorEditor(name, initialColor, close) - } - }, ) } @@ -39,8 +39,6 @@ actual fun AppearanceView(m: ChatModel, showSettingsModal: (@Composable (ChatMod fun AppearanceScope.AppearanceLayout( languagePref: SharedPreference, systemDarkTheme: SharedPreference, - showSettingsModal: (@Composable (ChatModel) -> Unit) -> (() -> Unit), - editColor: (ThemeColor, Color) -> Unit, ) { ColumnWithScrollBar( Modifier.fillMaxWidth(), @@ -62,11 +60,62 @@ fun AppearanceScope.AppearanceLayout( } } } - SectionDividerSpaced(maxTopPadding = true) + SectionDividerSpaced() + ThemesSection(systemDarkTheme) + + SectionDividerSpaced() ProfileImageSection() SectionDividerSpaced(maxTopPadding = true) - ThemesSection(systemDarkTheme, showSettingsModal, editColor) + FontScaleSection() + + SectionDividerSpaced(maxTopPadding = true) + DensityScaleSection() + SectionBottomSpacer() } } + +@Composable +fun DensityScaleSection() { + val localDensityScale = remember { mutableStateOf(appPrefs.densityScale.get()) } + SectionView(stringResource(MR.strings.appearance_zoom).uppercase(), padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + Row(Modifier.padding(top = 10.dp), verticalAlignment = Alignment.CenterVertically) { + Box(Modifier.size(60.dp) + .background(MaterialTheme.colors.surface, RoundedCornerShape(percent = 22)) + .clip(RoundedCornerShape(percent = 22)) + .clickable { + localDensityScale.value = 1f + appPrefs.densityScale.set(localDensityScale.value) + }, + contentAlignment = Alignment.Center) { + CompositionLocalProvider( + LocalDensity provides Density(LocalDensity.current.density * localDensityScale.value, LocalDensity.current.fontScale) + ) { + Text("${localDensityScale.value}", + color = if (localDensityScale.value == 1f) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground, + fontSize = 12.sp, + maxLines = 1 + ) + } + } + Spacer(Modifier.width(10.dp)) + Slider( + localDensityScale.value, + valueRange = 1f..2f, + steps = 11, + onValueChange = { + val diff = it % 0.1f + localDensityScale.value = String.format(Locale.US, "%.1f", it + (if (diff >= 0.05f) -diff + 0.1f else -diff)).toFloatOrNull() ?: 1f + }, + onValueChangeFinished = { + appPrefs.densityScale.set(localDensityScale.value) + }, + colors = SliderDefaults.colors( + activeTickColor = Color.Transparent, + inactiveTickColor = Color.Transparent, + ) + ) + } + } +} diff --git a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt index 5a42b4b756..ee8ae93de5 100644 --- a/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt +++ b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.desktop.kt @@ -1,9 +1,16 @@ package chat.simplex.common.views.usersettings import SectionView +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.model.ChatModel -import chat.simplex.common.views.helpers.ModalData +import chat.simplex.common.platform.AppUpdatesChannel +import chat.simplex.common.ui.theme.DEFAULT_PADDING_HALF +import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -16,7 +23,13 @@ actual fun SettingsSectionApp( withAuth: (title: String, desc: String, block: () -> Unit) -> Unit ) { SectionView(stringResource(MR.strings.settings_section_title_app)) { - SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) }, extraPadding = true) + SettingsActionItem(painterResource(MR.images.ic_code), stringResource(MR.strings.settings_developer_tools), showSettingsModal { DeveloperView(it, showCustomModal, withAuth) }) + val selectedChannel = remember { appPrefs.appUpdateChannel.state } + val values = AppUpdatesChannel.entries.map { it to it.text } + ExposedDropDownSettingRow(stringResource(MR.strings.app_check_for_updates), values, selectedChannel) { + appPrefs.appUpdateChannel.set(it) + setupUpdateChecker() + } AppVersionItem(showVersion) } } diff --git a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt index 7171f17991..f0679c0fa1 100644 --- a/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt +++ b/apps/multiplatform/desktop/src/jvmMain/kotlin/chat/simplex/desktop/Main.kt @@ -3,21 +3,27 @@ package chat.simplex.desktop import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.AnimationVector1D import androidx.compose.foundation.* -import androidx.compose.foundation.interaction.* import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.* import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.* +import chat.simplex.common.model.ChatController.appPrefs +import chat.simplex.common.model.size import chat.simplex.common.platform.* import chat.simplex.common.platform.DesktopPlatform import chat.simplex.common.showApp import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.onboarding.OnboardingStage import kotlinx.coroutines.* import java.io.File fun main() { + // Disable hardware acceleration + //System.setProperty("skiko.renderApi", "SOFTWARE") initHaskell() + runMigrations() + setupUpdateChecker() initApp() tmpDir.deleteRecursively() tmpDir.mkdir() @@ -43,35 +49,28 @@ private fun initHaskell() { platform = object: PlatformInterface { @Composable - override fun desktopScrollBarComponents(): Triple, Modifier, MutableState> { - val scope = rememberCoroutineScope() - val scrollBarAlpha = remember { Animatable(0f) } - val scrollJob: MutableState = remember { mutableStateOf(Job()) } - val modifier = remember { - Modifier.pointerInput(Unit) { - detectCursorMove { - scope.launch { - scrollBarAlpha.animateTo(1f) - } - scrollJob.value.cancel() - scrollJob.value = scope.launch { - delay(1000L) - scrollBarAlpha.animateTo(0f) - } - } + override fun desktopShowAppUpdateNotice() { + fun showNoticeIfNeeded() { + if ( + !chatModel.controller.appPrefs.appUpdateNoticeShown.get() + && chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete + && chatModel.chats.size > 3 + && chatModel.activeCallInvitation.value == null + ) { + appPrefs.appUpdateNoticeShown.set(true) + showAppUpdateNotice() + } + } + // Will show notice if chats were loaded before that moment and number of chats > 3 + LaunchedEffect(Unit) { + showNoticeIfNeeded() + } + // Will show notice if chats were loaded later (a lot of chats/slow query) and number of chats > 3 + KeyChangeEffect(chatModel.chats.size) { oldSize -> + if (oldSize == 0) { + showNoticeIfNeeded() } } - return Triple(scrollBarAlpha, modifier, scrollJob) - } - - @Composable - override fun desktopScrollBar(state: LazyListState, modifier: Modifier, scrollBarAlpha: Animatable, scrollJob: MutableState, reversed: Boolean) { - DesktopScrollBar(rememberScrollbarAdapter(scrollState = state), modifier, scrollBarAlpha, scrollJob, reversed) - } - - @Composable - override fun desktopScrollBar(state: ScrollState, modifier: Modifier, scrollBarAlpha: Animatable, scrollJob: MutableState, reversed: Boolean) { - DesktopScrollBar(rememberScrollbarAdapter(scrollState = state), modifier, scrollBarAlpha, scrollJob, reversed) } } } diff --git a/apps/multiplatform/gradle.properties b/apps/multiplatform/gradle.properties index 6e25eeafbc..51b6d4ce3b 100644 --- a/apps/multiplatform/gradle.properties +++ b/apps/multiplatform/gradle.properties @@ -26,11 +26,11 @@ android.enableJetifier=true kotlin.mpp.androidSourceSetLayoutVersion=2 kotlin.jvm.target=11 -android.version_name=5.7.5 -android.version_code=216 +android.version_name=6.0.2 +android.version_code=234 -desktop.version_name=5.7.5 -desktop.version_code=51 +desktop.version_name=6.0.2 +desktop.version_code=63 kotlin.version=1.9.23 gradle.plugin.version=8.2.0 diff --git a/apps/simplex-directory-service/src/Directory/Events.hs b/apps/simplex-directory-service/src/Directory/Events.hs index 76f57585a8..33b43a239b 100644 --- a/apps/simplex-directory-service/src/Directory/Events.hs +++ b/apps/simplex-directory-service/src/Directory/Events.hs @@ -14,6 +14,7 @@ module Directory.Events DirectoryRole (..), SDirectoryRole (..), crDirectoryEvent, + directoryCmdTag, viewName, ) where @@ -21,6 +22,8 @@ where import Control.Applicative ((<|>)) import Data.Attoparsec.Text (Parser) import qualified Data.Attoparsec.Text as A +import Data.Char (isSpace) +import Data.Either (fromRight) import Data.Functor (($>)) import Data.Text (Text) import qualified Data.Text as T @@ -32,15 +35,15 @@ import Simplex.Chat.Messages.CIContent import Simplex.Chat.Protocol (MsgContent (..)) import Simplex.Chat.Types import Simplex.Chat.Types.Shared +import Simplex.Messaging.Agent.Protocol (AgentErrorType (..)) import Simplex.Messaging.Encoding.String -import Simplex.Messaging.Util ((<$?>)) -import Data.Char (isSpace) -import Data.Either (fromRight) +import Simplex.Messaging.Protocol (BrokerErrorType (..)) +import Simplex.Messaging.Util (tshow, (<$?>)) data DirectoryEvent = DEContactConnected Contact | DEGroupInvitation {contact :: Contact, groupInfo :: GroupInfo, fromMemberRole :: GroupMemberRole, memberRole :: GroupMemberRole} - | DEServiceJoinedGroup {contactId :: ContactId, groupInfo :: GroupInfo, hostMember :: GroupMember} + | DEServiceJoinedGroup {contactId :: ContactId, groupInfo :: GroupInfo, hostMember :: GroupMember} | DEGroupUpdated {contactId :: ContactId, fromGroup :: GroupInfo, toGroup :: GroupInfo} | DEContactRoleChanged GroupInfo ContactId GroupMemberRole -- contactId here is the contact whose role changed | DEServiceRoleChanged GroupInfo GroupMemberRole @@ -52,6 +55,7 @@ data DirectoryEvent | DEItemEditIgnored Contact | DEItemDeleteIgnored Contact | DEContactCommand Contact ChatItemId ADirectoryCmd + | DELogChatResponse Text deriving (Show) crDirectoryEvent :: ChatResponse -> Maybe DirectoryEvent @@ -68,7 +72,7 @@ crDirectoryEvent = \case CRDeletedMemberUser {groupInfo} -> Just $ DEServiceRemovedFromGroup groupInfo CRGroupDeleted {groupInfo} -> Just $ DEGroupDeleted groupInfo CRChatItemUpdated {chatItem = AChatItem _ SMDRcv (DirectChat ct) _} -> Just $ DEItemEditIgnored ct - CRChatItemDeleted {deletedChatItem = AChatItem _ SMDRcv (DirectChat ct) _, byUser = False} -> Just $ DEItemDeleteIgnored ct + CRChatItemsDeleted {chatItemDeletions = ((ChatItemDeletion (AChatItem _ SMDRcv (DirectChat ct) _) _) : _), byUser = False} -> Just $ DEItemDeleteIgnored ct CRNewChatItem {chatItem = AChatItem _ SMDRcv (DirectChat ct) ci@ChatItem {content = CIRcvMsgContent mc, meta = CIMeta {itemLive}}} -> Just $ case (mc, itemLive) of (MCText t, Nothing) -> DEContactCommand ct ciId $ fromRight err $ A.parseOnly (directoryCmdP <* A.endOfInput) $ T.dropWhileEnd isSpace t @@ -76,6 +80,13 @@ crDirectoryEvent = \case where ciId = chatItemId' ci err = ADC SDRUser DCUnknownCommand + CRMessageError {severity, errorMessage} -> Just $ DELogChatResponse $ "message error: " <> severity <> ", " <> errorMessage + CRChatCmdError {chatError} -> Just $ DELogChatResponse $ "chat cmd error: " <> tshow chatError + CRChatError {chatError} -> case chatError of + ChatErrorAgent {agentError = BROKER _ NETWORK} -> Nothing + ChatErrorAgent {agentError = BROKER _ TIMEOUT} -> Nothing + _ -> Just $ DELogChatResponse $ "chat error: " <> tshow chatError + CRChatErrors {chatErrors} -> Just $ DELogChatResponse $ "chat errors: " <> T.intercalate ", " (map tshow chatErrors) _ -> Nothing data DirectoryRole = DRUser | DRSuperUser @@ -140,25 +151,26 @@ directoryCmdP = cmdStrP = (tagP >>= \(ADCT u t) -> ADC u <$> (cmdP t <|> pure (DCCommandError t))) <|> pure (ADC SDRUser DCUnknownCommand) - tagP = A.takeTill (== ' ') >>= \case - "help" -> u DCHelp_ - "h" -> u DCHelp_ - "next" -> u DCSearchNext_ - "all" -> u DCAllGroups_ - "new" -> u DCRecentGroups_ - "submit" -> u DCSubmitGroup_ - "confirm" -> u DCConfirmDuplicateGroup_ - "list" -> u DCListUserGroups_ - "ls" -> u DCListUserGroups_ - "delete" -> u DCDeleteGroup_ - "approve" -> su DCApproveGroup_ - "reject" -> su DCRejectGroup_ - "suspend" -> su DCSuspendGroup_ - "resume" -> su DCResumeGroup_ - "last" -> su DCListLastGroups_ - "exec" -> su DCExecuteCommand_ - "x" -> su DCExecuteCommand_ - _ -> fail "bad command tag" + tagP = + A.takeTill (== ' ') >>= \case + "help" -> u DCHelp_ + "h" -> u DCHelp_ + "next" -> u DCSearchNext_ + "all" -> u DCAllGroups_ + "new" -> u DCRecentGroups_ + "submit" -> u DCSubmitGroup_ + "confirm" -> u DCConfirmDuplicateGroup_ + "list" -> u DCListUserGroups_ + "ls" -> u DCListUserGroups_ + "delete" -> u DCDeleteGroup_ + "approve" -> su DCApproveGroup_ + "reject" -> su DCRejectGroup_ + "suspend" -> su DCSuspendGroup_ + "resume" -> su DCResumeGroup_ + "last" -> su DCListLastGroups_ + "exec" -> su DCExecuteCommand_ + "x" -> su DCExecuteCommand_ + _ -> fail "bad command tag" where u = pure . ADCT SDRUser su = pure . ADCT SDRSuperUser @@ -192,3 +204,23 @@ directoryCmdP = viewName :: String -> String viewName n = if ' ' `elem` n then "'" <> n <> "'" else n + +directoryCmdTag :: DirectoryCmd r -> Text +directoryCmdTag = \case + DCHelp -> "help" + DCSearchGroup _ -> "search" + DCSearchNext -> "next" + DCAllGroups -> "all" + DCRecentGroups -> "new" + DCSubmitGroup _ -> "submit" + DCConfirmDuplicateGroup {} -> "confirm" + DCListUserGroups -> "list" + DCDeleteGroup {} -> "delete" + DCApproveGroup {} -> "approve" + DCRejectGroup {} -> "reject" + DCSuspendGroup {} -> "suspend" + DCResumeGroup {} -> "resume" + DCListLastGroups _ -> "last" + DCExecuteCommand _ -> "exec" + DCUnknownCommand -> "unknown" + DCCommandError _ -> "error" diff --git a/apps/simplex-directory-service/src/Directory/Service.hs b/apps/simplex-directory-service/src/Directory/Service.hs index d158b57e22..2b12427638 100644 --- a/apps/simplex-directory-service/src/Directory/Service.hs +++ b/apps/simplex-directory-service/src/Directory/Service.hs @@ -2,9 +2,9 @@ {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE MultiWayIf #-} module Directory.Service ( welcomeGetOpts, @@ -15,6 +15,7 @@ where import Control.Concurrent (forkIO) import Control.Concurrent.Async import Control.Concurrent.STM +import Control.Logger.Simple import Control.Monad import qualified Data.ByteString.Char8 as B import Data.Maybe (fromMaybe, maybeToList) @@ -37,7 +38,7 @@ import Simplex.Chat.Options import Simplex.Chat.Protocol (MsgContent (..)) import Simplex.Chat.Types import Simplex.Chat.Types.Shared -import Simplex.Chat.View (serializeChatResponse, simplexChatContact) +import Simplex.Chat.View (serializeChatResponse, simplexChatContact, viewContactName, viewGroupName) import Simplex.Messaging.Encoding.String import Simplex.Messaging.TMap (TMap) import qualified Simplex.Messaging.TMap as TM @@ -64,7 +65,7 @@ data ServiceState = ServiceState newServiceState :: IO ServiceState newServiceState = do - searchRequests <- atomically TM.empty + searchRequests <- TM.emptyIO pure ServiceState {searchRequests} welcomeGetOpts :: IO DirectoryOpts @@ -96,9 +97,12 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi DEUnsupportedMessage _ct _ciId -> pure () DEItemEditIgnored _ct -> pure () DEItemDeleteIgnored _ct -> pure () - DEContactCommand ct ciId aCmd -> case aCmd of - ADC SDRUser cmd -> deUserCommand env ct ciId cmd - ADC SDRSuperUser cmd -> deSuperUserCommand ct ciId cmd + DEContactCommand ct ciId (ADC sUser cmd) -> do + logInfo $ "command received " <> directoryCmdTag cmd + case sUser of + SDRUser -> deUserCommand env ct ciId cmd + SDRSuperUser -> deSuperUserCommand ct ciId cmd + DELogChatResponse r -> logInfo r where withSuperUsers action = void . forkIO $ forM_ superUsers $ \KnownContact {contactId} -> action contactId notifySuperUsers s = withSuperUsers $ \contactId -> sendMessage' cc contactId s @@ -107,7 +111,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi withGroupReg GroupInfo {groupId, localDisplayName} err action = do atomically (getGroupReg st groupId) >>= \case Just gr -> action gr - Nothing -> putStrLn $ T.unpack $ "Error: " <> err <> ", group: " <> localDisplayName <> ", can't find group registration ID " <> tshow groupId + Nothing -> logError $ "Error: " <> err <> ", group: " <> localDisplayName <> ", can't find group registration ID " <> tshow groupId groupInfoText GroupProfile {displayName = n, fullName = fn, description = d} = n <> (if n == fn || T.null fn then "" else " (" <> fn <> ")") <> maybe "" ("\nWelcome message:\n" <>) d userGroupReference gr GroupInfo {groupProfile = GroupProfile {displayName}} = userGroupReference' gr displayName @@ -152,23 +156,25 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi deContactConnected :: Contact -> IO () deContactConnected ct = when (contactDirect ct) $ do - unless testing $ putStrLn $ T.unpack (localDisplayName' ct) <> " connected" + logInfo $ (viewContactName ct) <> " connected" sendMessage cc ct $ - "Welcome to " <> serviceName <> " service!\n\ - \Send a search string to find groups or */help* to learn how to add groups to directory.\n\n\ - \For example, send _privacy_ to find groups about privacy.\n\ - \Or send */all* or */new* to list groups.\n\n\ - \Content and privacy policy: https://simplex.chat/docs/directory.html" + ("Welcome to " <> serviceName <> " service!\n") + <> "Send a search string to find groups or */help* to learn how to add groups to directory.\n\n\ + \For example, send _privacy_ to find groups about privacy.\n\ + \Or send */all* or */new* to list groups.\n\n\ + \Content and privacy policy: https://simplex.chat/docs/directory.html" deGroupInvitation :: Contact -> GroupInfo -> GroupMemberRole -> GroupMemberRole -> IO () deGroupInvitation ct g@GroupInfo {groupProfile = GroupProfile {displayName, fullName}} fromMemberRole memberRole = do + logInfo $ "invited to group " <> viewGroupName g <> " by " <> viewContactName ct case badRolesMsg $ groupRolesStatus fromMemberRole memberRole of Just msg -> sendMessage cc ct msg - Nothing -> getDuplicateGroup g >>= \case - Just DGUnique -> processInvitation ct g - Just DGRegistered -> askConfirmation - Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g - Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers." + Nothing -> + getDuplicateGroup g >>= \case + Just DGUnique -> processInvitation ct g + Just DGRegistered -> askConfirmation + Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g + Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers." where askConfirmation = do ugrId <- addGroupReg st ct g GRSPendingConfirmation @@ -205,7 +211,8 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi _ -> Nothing deServiceJoinedGroup :: ContactId -> GroupInfo -> GroupMember -> IO () - deServiceJoinedGroup ctId g owner = + deServiceJoinedGroup ctId g owner = do + logInfo $ "service joined group " <> viewGroupName g withGroupReg g "joined group" $ \gr -> when (ctId `isOwner` gr) $ do setGroupRegOwner st gr owner @@ -214,7 +221,8 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi sendChatCmd cc (APICreateGroupLink groupId GRMember) >>= \case CRGroupLinkCreated {connReqContact} -> do setGroupStatus st gr GRSPendingUpdate - notifyOwner gr + notifyOwner + gr "Created the public link to join the group via this directory service that is always online.\n\n\ \Please add it to the group welcome message.\n\ \For example, add:" @@ -228,24 +236,26 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi _ -> notifyOwner gr $ unexpectedError "can't create group link" deGroupUpdated :: ContactId -> GroupInfo -> GroupInfo -> IO () - deGroupUpdated ctId fromGroup toGroup = + deGroupUpdated ctId fromGroup toGroup = do + logInfo $ "group updated " <> viewGroupName toGroup unless (sameProfile p p') $ do withGroupReg toGroup "group updated" $ \gr -> do let userGroupRef = userGroupReference gr toGroup readTVarIO (groupRegStatus gr) >>= \case GRSPendingConfirmation -> pure () GRSProposed -> pure () - GRSPendingUpdate -> groupProfileUpdate >>= \case - GPNoServiceLink -> - when (ctId `isOwner` gr) $ notifyOwner gr $ "The profile updated for " <> userGroupRef <> ", but the group link is not added to the welcome message." - GPServiceLinkAdded - | ctId `isOwner` gr -> groupLinkAdded gr - | otherwise -> notifyOwner gr "The group link is added by another group member, your registration will not be processed.\n\nPlease update the group profile yourself." - GPServiceLinkRemoved -> when (ctId `isOwner` gr) $ notifyOwner gr $ "The group link of " <> userGroupRef <> " is removed from the welcome message, please add it." - GPHasServiceLink -> when (ctId `isOwner` gr) $ groupLinkAdded gr - GPServiceLinkError -> do - when (ctId `isOwner` gr) $ notifyOwner gr $ "Error: " <> serviceName <> " has no group link for " <> userGroupRef <> ". Please report the error to the developers." - putStrLn $ "Error: no group link for " <> userGroupRef + GRSPendingUpdate -> + groupProfileUpdate >>= \case + GPNoServiceLink -> + when (ctId `isOwner` gr) $ notifyOwner gr $ "The profile updated for " <> userGroupRef <> ", but the group link is not added to the welcome message." + GPServiceLinkAdded + | ctId `isOwner` gr -> groupLinkAdded gr + | otherwise -> notifyOwner gr "The group link is added by another group member, your registration will not be processed.\n\nPlease update the group profile yourself." + GPServiceLinkRemoved -> when (ctId `isOwner` gr) $ notifyOwner gr $ "The group link of " <> userGroupRef <> " is removed from the welcome message, please add it." + GPHasServiceLink -> when (ctId `isOwner` gr) $ groupLinkAdded gr + GPServiceLinkError -> do + when (ctId `isOwner` gr) $ notifyOwner gr $ "Error: " <> serviceName <> " has no group link for " <> userGroupRef <> ". Please report the error to the developers." + logError $ "Error: no group link for " <> T.pack userGroupRef GRSPendingApproval n -> processProfileChange gr $ n + 1 GRSActive -> processProfileChange gr 1 GRSSuspended -> processProfileChange gr 1 @@ -288,7 +298,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi notifyOwner gr $ "The group " <> userGroupRef <> " is updated!\nIt is hidden from the directory until approved." notifySuperUsers $ "The group " <> groupRef <> " is updated." checkRolesSendToApprove gr n' - GPServiceLinkError -> putStrLn $ "Error: no group link for " <> groupRef <> " pending approval." + GPServiceLinkError -> logError $ "Error: no group link for " <> T.pack groupRef <> " pending approval." groupProfileUpdate = profileUpdate <$> sendChatCmd cc (APIGetGroupLink groupId) where profileUpdate = \case @@ -297,7 +307,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi groupLink2 = safeDecodeUtf8 $ strEncode $ simplexChatContact connReqContact hadLinkBefore = groupLink1 `isInfix` description p || groupLink2 `isInfix` description p hasLinkNow = groupLink1 `isInfix` description p' || groupLink2 `isInfix` description p' - in if + in if | hadLinkBefore && hasLinkNow -> GPHasServiceLink | hadLinkBefore -> GPServiceLinkRemoved | hasLinkNow -> GPServiceLinkAdded @@ -311,18 +321,20 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi sendToApprove :: GroupInfo -> GroupReg -> GroupApprovalId -> IO () sendToApprove GroupInfo {groupProfile = p@GroupProfile {displayName, image = image'}} GroupReg {dbGroupId, dbContactId} gaId = do - ct_ <- getContact cc dbContactId + ct_ <- getContact cc dbContactId gr_ <- getGroupAndSummary cc dbGroupId let membersStr = maybe "" (\(_, s) -> "_" <> tshow (currentMembers s) <> " members_\n") gr_ - text = maybe ("The group ID " <> tshow dbGroupId <> " submitted: ") (\c -> localDisplayName' c <> " submitted the group ID " <> tshow dbGroupId <> ": ") ct_ - <> "\n" <> groupInfoText p <> "\n" <> membersStr <> "\nTo approve send:" + text = + maybe ("The group ID " <> tshow dbGroupId <> " submitted: ") (\c -> localDisplayName' c <> " submitted the group ID " <> tshow dbGroupId <> ": ") ct_ + <> ("\n" <> groupInfoText p <> "\n" <> membersStr <> "\nTo approve send:") msg = maybe (MCText text) (\image -> MCImage {text, image}) image' withSuperUsers $ \cId -> do sendComposedMessage' cc cId Nothing msg sendMessage' cc cId $ "/approve " <> show dbGroupId <> ":" <> viewName (T.unpack displayName) <> " " <> show gaId deContactRoleChanged :: GroupInfo -> ContactId -> GroupMemberRole -> IO () - deContactRoleChanged g@GroupInfo {membership = GroupMember {memberRole = serviceRole}} ctId contactRole = + deContactRoleChanged g@GroupInfo {membership = GroupMember {memberRole = serviceRole}} ctId contactRole = do + logInfo $ "contact ID " <> tshow ctId <> " role changed in group " <> viewGroupName g <> " to " <> tshow contactRole withGroupReg g "contact role changed" $ \gr -> do let userGroupRef = userGroupReference gr g uCtRole = "Your role in the group " <> userGroupRef <> " is changed to " <> ctRole @@ -348,6 +360,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi deServiceRoleChanged :: GroupInfo -> GroupMemberRole -> IO () deServiceRoleChanged g serviceRole = do + logInfo $ "service role changed in group " <> viewGroupName g <> " to " <> tshow serviceRole withGroupReg g "service role changed" $ \gr -> do let userGroupRef = userGroupReference gr g uSrvRole = serviceName <> " role in the group " <> userGroupRef <> " is changed to " <> srvRole @@ -371,11 +384,12 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi srvRole = "*" <> B.unpack (strEncode serviceRole) <> "*" suSrvRole = "(" <> serviceName <> " role is changed to " <> srvRole <> ")." whenContactIsOwner gr action = - getGroupMember gr >>= - mapM_ (\cm@GroupMember {memberRole} -> when (memberRole == GROwner && memberActive cm) action) + getGroupMember gr + >>= mapM_ (\cm@GroupMember {memberRole} -> when (memberRole == GROwner && memberActive cm) action) deContactRemovedFromGroup :: ContactId -> GroupInfo -> IO () - deContactRemovedFromGroup ctId g = + deContactRemovedFromGroup ctId g = do + logInfo $ "contact ID " <> tshow ctId <> " removed from group " <> viewGroupName g withGroupReg g "contact removed" $ \gr -> do when (ctId `isOwner` gr) $ do setGroupStatus st gr GRSRemoved @@ -383,7 +397,8 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi notifySuperUsers $ "The group " <> groupReference g <> " is de-listed (group owner is removed)." deContactLeftGroup :: ContactId -> GroupInfo -> IO () - deContactLeftGroup ctId g = + deContactLeftGroup ctId g = do + logInfo $ "contact ID " <> tshow ctId <> " left group " <> viewGroupName g withGroupReg g "contact left" $ \gr -> do when (ctId `isOwner` gr) $ do setGroupStatus st gr GRSRemoved @@ -391,7 +406,8 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi notifySuperUsers $ "The group " <> groupReference g <> " is de-listed (group owner left)." deServiceRemovedFromGroup :: GroupInfo -> IO () - deServiceRemovedFromGroup g = + deServiceRemovedFromGroup g = do + logInfo $ "service removed from group " <> viewGroupName g withGroupReg g "service removed" $ \gr -> do setGroupStatus st gr GRSRemoved notifyOwner gr $ serviceName <> " is removed from the group " <> userGroupReference gr g <> ".\n\nThe group is no longer listed in the directory." @@ -402,11 +418,15 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi DCHelp -> sendMessage cc ct $ "You must be the owner to add the group to the directory:\n\ - \1. Invite " <> serviceName <> " bot to your group as *admin* (you can send `/list` to see all groups you submitted).\n\ - \2. " <> serviceName <> " bot will create a public group link for the new members to join even when you are offline.\n\ - \3. You will then need to add this link to the group welcome message.\n\ - \4. Once the link is added, service admins will approve the group (it can take up to 24 hours), and everybody will be able to find it in directory.\n\n\ - \Start from inviting the bot to your group as admin - it will guide you through the process" + \1. Invite " + <> serviceName + <> " bot to your group as *admin* (you can send `/list` to see all groups you submitted).\n\ + \2. " + <> serviceName + <> " bot will create a public group link for the new members to join even when you are offline.\n\ + \3. You will then need to add this link to the group welcome message.\n\ + \4. Once the link is added, service admins will approve the group (it can take up to 24 hours), and everybody will be able to find it in directory.\n\n\ + \Start from inviting the bot to your group as admin - it will guide you through the process" DCSearchGroup s -> withFoundListedGroups (Just s) $ sendSearchResults s DCSearchNext -> atomically (TM.lookup (contactId' ct) searchRequests) >>= \case @@ -434,13 +454,13 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi Nothing -> sendReply $ "Group ID " <> show ugrId <> " not found" Just g@GroupInfo {groupProfile = GroupProfile {displayName}} | displayName == gName -> - readTVarIO groupRegStatus >>= \case - GRSPendingConfirmation -> do - getDuplicateGroup g >>= \case - Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers." - Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g - _ -> processInvitation ct g - _ -> sendReply $ "Error: the group ID " <> show ugrId <> " (" <> T.unpack displayName <> ") is not pending confirmation." + readTVarIO groupRegStatus >>= \case + GRSPendingConfirmation -> do + getDuplicateGroup g >>= \case + Nothing -> sendMessage cc ct "Error: getDuplicateGroup. Please notify the developers." + Just DGReserved -> sendMessage cc ct $ groupAlreadyListed g + _ -> processInvitation ct g + _ -> sendReply $ "Error: the group ID " <> show ugrId <> " (" <> T.unpack displayName <> ") is not pending confirmation." | otherwise -> sendReply $ "Group ID " <> show ugrId <> " has the display name " <> T.unpack displayName DCListUserGroups -> atomically (getUserGroupRegs st $ contactId' ct) >>= \grs -> do @@ -462,7 +482,7 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi let gs' = takeTop searchResults gs moreGroups = length gs - length gs' more = if moreGroups > 0 then ", sending top " <> show (length gs') else "" - sendReply $ "Found " <> show (length gs) <> " group(s)" <> more <> "." + sendReply $ "Found " <> show (length gs) <> " group(s)" <> more <> "." updateSearchRequest (STSearch s) $ groupIds gs' sendFoundGroups gs' moreGroups sendAllGroups takeFirst sortName searchType = \case @@ -499,74 +519,76 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi msg = maybe (MCText text) (\image -> MCImage {text, image}) image_ sendComposedMessage cc ct Nothing msg when (moreGroups > 0) $ - sendComposedMessage cc ct Nothing $ MCText $ "Send */next* or just *.* for " <> tshow moreGroups <> " more result(s)." + sendComposedMessage cc ct Nothing $ + MCText $ + "Send */next* or just *.* for " <> tshow moreGroups <> " more result(s)." deSuperUserCommand :: Contact -> ChatItemId -> DirectoryCmd 'DRSuperUser -> IO () deSuperUserCommand ct ciId cmd | superUser `elem` superUsers = case cmd of - DCApproveGroup {groupId, displayName = n, groupApprovalId} -> do - getGroupAndReg groupId n >>= \case - Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)." - Just (g, gr) -> - readTVarIO (groupRegStatus gr) >>= \case - GRSPendingApproval gaId - | gaId == groupApprovalId -> do - getDuplicateGroup g >>= \case - Nothing -> sendReply "Error: getDuplicateGroup. Please notify the developers." - Just DGReserved -> sendReply $ "The group " <> groupRef <> " is already listed in the directory." - _ -> do - getGroupRolesStatus g gr >>= \case - Just GRSOk -> do - setGroupStatus st gr GRSActive - sendReply "Group approved!" - notifyOwner gr $ "The group " <> userGroupReference' gr n <> " is approved and listed in directory!\nPlease note: if you change the group profile it will be hidden from directory until it is re-approved." - Just GRSServiceNotAdmin -> replyNotApproved serviceNotAdmin - Just GRSContactNotOwner -> replyNotApproved "user is not an owner." - Just GRSBadRoles -> replyNotApproved $ "user is not an owner, " <> serviceNotAdmin - Nothing -> sendReply "Error: getGroupRolesStatus. Please notify the developers." - where - replyNotApproved reason = sendReply $ "Group is not approved: " <> reason - serviceNotAdmin = serviceName <> " is not an admin." - | otherwise -> sendReply "Incorrect approval code" - _ -> sendReply $ "Error: the group " <> groupRef <> " is not pending approval." - where - groupRef = groupReference' groupId n - DCRejectGroup _gaId _gName -> pure () - DCSuspendGroup groupId gName -> do - let groupRef = groupReference' groupId gName - getGroupAndReg groupId gName >>= \case - Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)." - Just (_, gr) -> - readTVarIO (groupRegStatus gr) >>= \case - GRSActive -> do - setGroupStatus st gr GRSSuspended - notifyOwner gr $ "The group " <> userGroupReference' gr gName <> " is suspended and hidden from directory. Please contact the administrators." - sendReply "Group suspended!" - _ -> sendReply $ "The group " <> groupRef <> " is not active, can't be suspended." - DCResumeGroup groupId gName -> do - let groupRef = groupReference' groupId gName - getGroupAndReg groupId gName >>= \case - Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)." - Just (_, gr) -> - readTVarIO (groupRegStatus gr) >>= \case - GRSSuspended -> do - setGroupStatus st gr GRSActive - notifyOwner gr $ "The group " <> userGroupReference' gr gName <> " is listed in the directory again!" - sendReply "Group listing resumed!" - _ -> sendReply $ "The group " <> groupRef <> " is not suspended, can't be resumed." - DCListLastGroups count -> - readTVarIO (groupRegs st) >>= \grs -> do - sendReply $ show (length grs) <> " registered group(s)" <> (if length grs > count then ", showing the last " <> show count else "") - void . forkIO $ forM_ (reverse $ take count grs) $ \gr@GroupReg {dbGroupId, dbContactId} -> do - ct_ <- getContact cc dbContactId - let ownerStr = "Owner: " <> maybe "getContact error" localDisplayName' ct_ - sendGroupInfo ct gr dbGroupId $ Just ownerStr - DCExecuteCommand cmdStr -> - sendChatCmdStr cc cmdStr >>= \r -> do - ts <- getCurrentTime - tz <- getCurrentTimeZone - sendReply $ serializeChatResponse (Nothing, Just user) ts tz Nothing r - DCCommandError tag -> sendReply $ "Command error: " <> show tag + DCApproveGroup {groupId, displayName = n, groupApprovalId} -> + getGroupAndReg groupId n >>= \case + Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)." + Just (g, gr) -> + readTVarIO (groupRegStatus gr) >>= \case + GRSPendingApproval gaId + | gaId == groupApprovalId -> do + getDuplicateGroup g >>= \case + Nothing -> sendReply "Error: getDuplicateGroup. Please notify the developers." + Just DGReserved -> sendReply $ "The group " <> groupRef <> " is already listed in the directory." + _ -> do + getGroupRolesStatus g gr >>= \case + Just GRSOk -> do + setGroupStatus st gr GRSActive + sendReply "Group approved!" + notifyOwner gr $ "The group " <> userGroupReference' gr n <> " is approved and listed in directory!\nPlease note: if you change the group profile it will be hidden from directory until it is re-approved." + Just GRSServiceNotAdmin -> replyNotApproved serviceNotAdmin + Just GRSContactNotOwner -> replyNotApproved "user is not an owner." + Just GRSBadRoles -> replyNotApproved $ "user is not an owner, " <> serviceNotAdmin + Nothing -> sendReply "Error: getGroupRolesStatus. Please notify the developers." + where + replyNotApproved reason = sendReply $ "Group is not approved: " <> reason + serviceNotAdmin = serviceName <> " is not an admin." + | otherwise -> sendReply "Incorrect approval code" + _ -> sendReply $ "Error: the group " <> groupRef <> " is not pending approval." + where + groupRef = groupReference' groupId n + DCRejectGroup _gaId _gName -> pure () + DCSuspendGroup groupId gName -> do + let groupRef = groupReference' groupId gName + getGroupAndReg groupId gName >>= \case + Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)." + Just (_, gr) -> + readTVarIO (groupRegStatus gr) >>= \case + GRSActive -> do + setGroupStatus st gr GRSSuspended + notifyOwner gr $ "The group " <> userGroupReference' gr gName <> " is suspended and hidden from directory. Please contact the administrators." + sendReply "Group suspended!" + _ -> sendReply $ "The group " <> groupRef <> " is not active, can't be suspended." + DCResumeGroup groupId gName -> do + let groupRef = groupReference' groupId gName + getGroupAndReg groupId gName >>= \case + Nothing -> sendReply $ "The group " <> groupRef <> " not found (getGroupAndReg)." + Just (_, gr) -> + readTVarIO (groupRegStatus gr) >>= \case + GRSSuspended -> do + setGroupStatus st gr GRSActive + notifyOwner gr $ "The group " <> userGroupReference' gr gName <> " is listed in the directory again!" + sendReply "Group listing resumed!" + _ -> sendReply $ "The group " <> groupRef <> " is not suspended, can't be resumed." + DCListLastGroups count -> + readTVarIO (groupRegs st) >>= \grs -> do + sendReply $ show (length grs) <> " registered group(s)" <> (if length grs > count then ", showing the last " <> show count else "") + void . forkIO $ forM_ (reverse $ take count grs) $ \gr@GroupReg {dbGroupId, dbContactId} -> do + ct_ <- getContact cc dbContactId + let ownerStr = "Owner: " <> maybe "getContact error" localDisplayName' ct_ + sendGroupInfo ct gr dbGroupId $ Just ownerStr + DCExecuteCommand cmdStr -> + sendChatCmdStr cc cmdStr >>= \r -> do + ts <- getCurrentTime + tz <- getCurrentTimeZone + sendReply $ serializeChatResponse (Nothing, Just user) ts tz Nothing r + DCCommandError tag -> sendReply $ "Command error: " <> show tag | otherwise = sendReply "You are not allowed to use this command" where superUser = KnownContact {contactId = contactId' ct, localDisplayName = localDisplayName' ct} @@ -577,8 +599,9 @@ directoryService st DirectoryOpts {superUsers, serviceName, searchResults, testi getGroup cc gId $>>= \g@GroupInfo {groupProfile = GroupProfile {displayName}} -> if displayName == gName - then atomically (getGroupReg st gId) - $>>= \gr -> pure $ Just (g, gr) + then + atomically (getGroupReg st gId) + $>>= \gr -> pure $ Just (g, gr) else pure Nothing sendGroupInfo :: Contact -> GroupReg -> GroupId -> Maybe Text -> IO () diff --git a/apps/simplex-directory-service/src/Directory/Store.hs b/apps/simplex-directory-service/src/Directory/Store.hs index 5082cab2ce..c810102e08 100644 --- a/apps/simplex-directory-service/src/Directory/Store.hs +++ b/apps/simplex-directory-service/src/Directory/Store.hs @@ -39,8 +39,8 @@ import Data.Text (Text) import Simplex.Chat.Types import Simplex.Messaging.Encoding.String import Simplex.Messaging.Util (ifM) -import System.IO (Handle, IOMode (..), openFile, BufferMode (..), hSetBuffering) -import System.Directory (renameFile, doesFileExist) +import System.Directory (doesFileExist, renameFile) +import System.IO (BufferMode (..), Handle, IOMode (..), hSetBuffering, openFile) data DirectoryStore = DirectoryStore { groupRegs :: TVar [GroupReg], @@ -112,7 +112,7 @@ addGroupReg st ct GroupInfo {groupId} grStatus = do let ugrId = 1 + foldl' maxUgrId 0 grs grData' = grData {userGroupRegId_ = ugrId} gr' = gr {userGroupRegId = ugrId} - in (grData', gr' : grs) + in (grData', gr' : grs) ctId = contactId' ct maxUgrId mx GroupReg {dbContactId, userGroupRegId} | dbContactId == ctId && userGroupRegId > mx = userGroupRegId @@ -311,14 +311,15 @@ readDirectoryData f = Right r -> case r of GRCreate gr@GroupRegData {dbGroupId_ = gId} -> do when (isJust $ M.lookup gId m) $ - putStrLn $ "Warning: duplicate group with ID " <> show gId <> ", group replaced." + putStrLn $ + "Warning: duplicate group with ID " <> show gId <> ", group replaced." pure $ M.insert gId gr m GRUpdateStatus gId groupRegStatus_ -> case M.lookup gId m of Just gr -> pure $ M.insert gId gr {groupRegStatus_} m - Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <>", status update ignored.") + Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <> ", status update ignored.") GRUpdateOwner gId grOwnerId -> case M.lookup gId m of Just gr -> pure $ M.insert gId gr {dbOwnerMemberId_ = Just grOwnerId} m - Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <>", owner update ignored.") + Nothing -> m <$ putStrLn ("Warning: no group with ID " <> show gId <> ", owner update ignored.") writeDirectoryData :: FilePath -> [GroupRegData] -> IO Handle writeDirectoryData f grs = do diff --git a/blog/20221206-simplex-chat-v4.3-voice-messages.md b/blog/20221206-simplex-chat-v4.3-voice-messages.md index 1ca25ce5d0..07a6e227f0 100644 --- a/blog/20221206-simplex-chat-v4.3-voice-messages.md +++ b/blog/20221206-simplex-chat-v4.3-voice-messages.md @@ -14,7 +14,7 @@ permalink: "/blog/20221206-simplex-chat-v4.3-voice-messages.html" ## SimpleX Chat reviews -Since we published [the security assessment of SimpleX Chat](https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html) completed by Trail of Bits in November, several sites published the reviews and included it in their recommendations: +Since we published [the security assessment of SimpleX Chat](./20221108-simplex-chat-v4.2-security-audit-new-website.md) completed by Trail of Bits in November, several sites published the reviews and included it in their recommendations: - Privacy Guides added SimpleX Chat to [the recommended private and secure messengers](https://www.privacyguides.org/real-time-communication/#simplex-chat). - Mike Kuketz – a well-known security expert – published [the review of SimpleX Chat](https://www.kuketz-blog.de/simplex-eindruecke-vom-messenger-ohne-identifier/) and added it to [the messenger matrix](https://www.messenger-matrix.de). diff --git a/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md b/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md index 0a66124934..690292d14c 100644 --- a/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md +++ b/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md @@ -40,9 +40,11 @@ Many large tech companies prioritizing value extraction over value creation earn ### How is it funded and what is the business model? -We started working full-time on the project in 2021 when [Portman Wills](https://www.linkedin.com/in/portmanwills/) and [Peter Briffett](https://www.linkedin.com/in/peterbriffett/) (the founders of [Wagestream](https://wagestream.com/en/) where I led the engineering team) supported the company very early on, and several other angel investors joined later. In July 2022 SimpleX Chat raised a pre-seed funding from the VC fund [Village Global](https://www.villageglobal.vc) - its co-founder [Ben Casnocha](https://casnocha.com) was very excited about our vision of privacy-first fully decentralized messaging and community platform, both for the individual users and for the companies, independent of any crypto-currencies, that might grow to replace large centralized platforms, such as WhatsApp, Telegram and Signal. +We started working full-time on the project in 2021 when [Portman Wills](https://www.linkedin.com/in/portmanwills/) and [Peter Briffett](https://www.linkedin.com/in/peterbriffett/) (the founders of [Wagestream](https://wagestream.com/en/) where I led the engineering team) supported the company very early on, and several other angel investors joined later. In July 2022 SimpleX Chat raised a pre-seed funding from the VC fund [Village Global](https://www.villageglobal.vc) - its co-founder [Ben Casnocha](https://www.villageglobal.vc/team/ben-casnocha) was very excited about our vision of privacy-first fully decentralized messaging and community platform, both for the individual users and for the companies, independent of any crypto-currencies, that might grow to replace large centralized platforms, such as WhatsApp, Telegram and Signal. -Overall we raised from our investors approximately $370,000 for a small share of the company to allow the project team working full time for almost two years, funding product design and development, infrastructure, and also [the security assessment by Trail of Bits](https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html). A large part of this money is not spent yet. +> Edit: please see the comment from Ben Casnocha about this investment in [our post from August 14, 2024](./20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md). + +Overall we raised from our investors approximately $370,000 for a small share of the company to allow the project team working full time for almost two years, funding product design and development, infrastructure, and also [the security assessment by Trail of Bits](./20221108-simplex-chat-v4.2-security-audit-new-website.md). A large part of this money is not spent yet. The project was hugely supported by the users as well - collectively, [you donated](https://github.com/simplex-chat/simplex-chat#help-us-with-donations) over $25,000. Without these donations the investment we raised would not be possible, because we believe that voluntary user donations can sustain the project in the long term – it already covers all infrastructure costs. There are only two ways an Internet service can exist - either users are paying for it, or the users data becomes the product for the real customers, as happened with many large Internet companies. In the latter case the users are losing much more money than they are saving by giving away their privacy and the rights to the content they create on the centralized platforms. diff --git a/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md b/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md index fc924a8706..0222c25d77 100644 --- a/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md +++ b/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.md @@ -50,7 +50,7 @@ Other limitations of the desktop app: - you cannot send voice messages. - there is no support for calls yet. -You can download the desktop app for Linux and Mac via [downloads page](https://simplex.chat/downloads). Windows version will be available soon. +You can download the desktop app for Linux and Mac via [downloads page](../docs/DOWNLOADS.md). Windows version will be available soon. ## Group directory service and other group improvements diff --git a/blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md b/blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md index 4fbfc400ad..7f50446bfa 100644 --- a/blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md +++ b/blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md @@ -40,7 +40,7 @@ This is only possible when both devices are connected to the same local network. **On desktop** -If you don't have desktop app installed yet, [download it](https://simplex.chat/downloads/) and create any chat profile - you don't need to use it, and when you create it there are no server requests sent and no accounts are created. Think about it as about user profile on your computer. +If you don't have desktop app installed yet, [download it](../docs/DOWNLOADS.md) and create any chat profile - you don't need to use it, and when you create it there are no server requests sent and no accounts are created. Think about it as about user profile on your computer. Then in desktop app settings choose *Link a mobile* - it will show a QR code. diff --git a/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md b/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md index 1e4c3adfb5..6d4c8b77a2 100644 --- a/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md +++ b/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md @@ -10,6 +10,8 @@ permalink: "/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ra # SimpleX Chat v5.6 beta: adding quantum resistance to Signal double ratchet algorithm +**Published:** Mar 14, 2024 + This is a major upgrade for SimpleX messaging protocols, we are really proud to present the results of the hard work of our whole team on the [Pi day](https://en.wikipedia.org/wiki/Pi_Day). This post also covers various aspects of end-to-end encryption, compares different messengers, and explains why and how quantum-resistant encryption is added to SimpleX Chat: @@ -101,7 +103,7 @@ This attack is much less understood by the users, and forward secrecy does not p Out of all encryption algorithms known to us only _Signal double ratchet algorithm_ (also referred to as _Signal algorithm_ or _double ratchet algorithm_, which is not the same as Signal messaging platform and protocols) provides the ability for the encryption security to recover after break-ins attacks. This recovery happens automatically and transparently to the users, without them doing anything special or even knowing about break-in, by simply sending messages. Every time one of the communication parties replies to another party message, new random keys are generated and previously stolen keys become useless. -Double ratchet algorithm is used in Signal, Cwtch and SimpleX Chat. This is why you cannot use SimpleX Chat profile on more than one device at the same time - the encryption scheme rotates the long term keys, randomly, and keys on another device become useless, as they would become useless for the attacker who stole them. Security always has some costs to the convenience. +Double ratchet algorithm is used in Signal, Cwtch and SimpleX Chat. But Signal app by allowing to use the same profile on multiple devices compromises the break-in recovery function of Signal algorithm, as explained in [this paper](https://eprint.iacr.org/2021/626.pdf). Because of break-in recovery you cannot use SimpleX Chat profile on more than one device at the same time - the encryption scheme rotates the long term keys, randomly, and keys on another device become useless, as they would become useless for the attacker who stole them. Security always has some costs to the convenience. ### 5. Man-in-the-middle attack - mitigated by two-factor key exchange diff --git a/blog/20240416-dangers-of-metadata-in-messengers.md b/blog/20240416-dangers-of-metadata-in-messengers.md index 3b30003798..b0832af4f7 100644 --- a/blog/20240416-dangers-of-metadata-in-messengers.md +++ b/blog/20240416-dangers-of-metadata-in-messengers.md @@ -33,7 +33,7 @@ For example, while WhatsApp messages are [end-to-end encrypted](https://faq.what This is called [metadata](https://en.wikipedia.org/wiki/Metadata). It reveals a wealth of information about you and your connections, and in the hands of a centralized monopoly, this can and does get misused in incredibly dangerous ways. Once such metadata is logged, it can create very detailed profiles about who you are, everywhere you’ve been, and everyone you’ve ever spoken to. In settling for apps that normalize this while giving you the illusion of privacy in their marketing, we are doing ourselves a disservice by accepting this as the default. Collectively, we aren’t doing enough to protect ourselves and our social graph from this invasive overreach. -When stored, aggregated and analyzed, this metadata provides ample information that could potentially incriminate someone or be submitted to authorities. When WhatsApp and Facebook Messenger enabled end-to-end encryption for messages, of course it was a welcome and widely celebrated change. But it’s important to remember that not all end-to-end encryption utilizes the same standards, [some implementations are more secure](https://simplex.chat/blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.html#how-secure-is-end-to-end-encryption-in-different-messengers) than others, so it’s something that shouldn’t necessarily be accepted at face value. More importantly: collecting and storing an obscene amount of metadata should invite global scrutiny, considering this data is often combined with whatever other information companies like Meta harvest about your identity (which is [a lot](https://www.vox.com/recode/23172691/meta-tracking-privacy-hospitals).) +When stored, aggregated and analyzed, this metadata provides ample information that could potentially incriminate someone or be submitted to authorities. When WhatsApp and Facebook Messenger enabled end-to-end encryption for messages, of course it was a welcome and widely celebrated change. But it’s important to remember that not all end-to-end encryption utilizes the same standards, [some implementations are more secure](./20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md#how-secure-is-end-to-end-encryption-in-different-messengers) than others, so it’s something that shouldn’t necessarily be accepted at face value. More importantly: collecting and storing an obscene amount of metadata should invite global scrutiny, considering this data is often combined with whatever other information companies like Meta harvest about your identity (which is [a lot](https://www.vox.com/recode/23172691/meta-tracking-privacy-hospitals).) @@ -45,8 +45,8 @@ But we also need to acknowledge that the world is becoming increasingly dangerou End-to-end encryption is a solid start, but it's just the beginning of our pursuit for true privacy and security. True privacy means that even when legal demands come knocking, there's no useful metadata to hand over. It's not enough to just protect the content of messages; we need consistent innovation in protecting metadata too. -Changing ingrained habits is tough, but your privacy is always worth the fight. Although giants like WhatsApp and Telegram may dominate global messaging for now, increasing concerns about data harvesting and AI-driven surveillance are fueling demand for alternatives. SimpleX Chat aims to be one of those strong alternatives, hence its radical focus on a decentralized framework with no user identifiers (in other words, nothing that uniquely identifies users on the protocol level to their contacts or to the relays) and extra optionality (self-hosting an [SMP server](https://simplex.chat/docs/server.html) or [XFTP server](https://simplex.chat/docs/xftp-server.html), access via Tor, [chat profiles](https://simplex.chat/docs/guide/chat-profiles.html) with incognito mode, etc.) +Changing ingrained habits is tough, but your privacy is always worth the fight. Although giants like WhatsApp and Telegram may dominate global messaging for now, increasing concerns about data harvesting and AI-driven surveillance are fueling demand for alternatives. SimpleX Chat aims to be one of those strong alternatives, hence its radical focus on a decentralized framework with no user identifiers (in other words, nothing that uniquely identifies users on the protocol level to their contacts or to the relays) and extra optionality (self-hosting an [SMP server](../docs/SERVER.md) or [XFTP server](../docs/XFTP-SERVER.md), access via Tor, [chat profiles](../docs/guide/chat-profiles.md) with incognito mode, etc.) As of today, most messaging alternatives, including SimpleX, will have some limitations. But with the limited resources we have, we are committed to daily progress towards creating a truly private messenger that anyone can use while maintaining the features that users have come to know and love in messaging interfaces. We want to be the prime example of a messenger that achieves genuine privacy without compromising it for convenience. We need to be able to reliably move away from small and niche use cases to endorsing and enforcing global standards for privacy and making it accessible for all users regardless of their technical expertise. -We’re grateful for the users and [donors](https://github.com/simplex-chat/simplex-chat#help-us-with-donations) who have been following along on this journey thus far and helping with feedback, anything from bug reports to identifying potential risks. Building in the open has always been a necessity for transparency and ongoing [auditability](https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html), because we don’t want anyone to just take our word for it. [See for yourself](https://github.com/simplex-chat) and engage in the discussions. We fully expect you to hold us accountable to our word. +We’re grateful for the users and [donors](https://github.com/simplex-chat/simplex-chat#help-us-with-donations) who have been following along on this journey thus far and helping with feedback, anything from bug reports to identifying potential risks. Building in the open has always been a necessity for transparency and ongoing [auditability](./20221108-simplex-chat-v4.2-security-audit-new-website.md), because we don’t want anyone to just take our word for it. [See for yourself](https://github.com/simplex-chat) and engage in the discussions. We fully expect you to hold us accountable to our word. diff --git a/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md b/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md index a321dbc6fa..cb3e5b2d10 100644 --- a/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md +++ b/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md @@ -10,6 +10,8 @@ permalink: "/blog/20240426-simplex-legally-binding-transparency-v5-7-better-user # SimpleX network: legally binding transparency, v5.7 released with better calls and messages +**Published:** Apr 26, 2024 + What's new in v5.7: - [quantum resistant end-to-end encryption](#quantum-resistant-end-to-end-encryption) with all contacts. - [forward and save messages](#forward-and-save-messages) without revealing the source. @@ -23,10 +25,10 @@ Also, we added Lithuanian interface language to the Android and desktop apps, th We are committed to open-source, privacy and security. Here are the recent changes we made: -- We now have a [Transparency Reports](https://simplex.chat/transparency/) page. -- We updated our [Privacy Policy](https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md) to remove undefined terms "impermissible" and "acceptable", which would allow us to remove anything we don't like, without any clarity on what that is. You can see the edits [here](https://github.com/simplex-chat/simplex-chat/pull/4076/files). -- We published a new page with [Frequently Asked Questions](https://simplex.chat/faq/), thanks to the guidance from users. -- We also have a new [Security Policy](https://simplex.chat/security/) – we welcome your feedback on it. +- We now have a [Transparency Reports](../docs/TRANSPARENCY.md) page. +- We updated our [Privacy Policy](../PRIVACY.md) to remove undefined terms "impermissible" and "acceptable", which would allow us to remove anything we don't like, without any clarity on what that is. You can see the edits [here](https://github.com/simplex-chat/simplex-chat/pull/4076/files). +- We published a new page with [Frequently Asked Questions](../docs/FAQ.md), thanks to the guidance from users. +- We also have a new [Security Policy](../docs/SECURITY.md) – we welcome your feedback on it. What do we mean by “legally binding transparency?”. It includes these principles: - Accountability: an empty promise or commitment to transparency that is not legally binding is just marketing, and can provide opportunities for the organizations to be misleading or not disclose important information that can affect their users privacy and security. diff --git a/blog/20240516-simplex-redefining-privacy-hard-choices.md b/blog/20240516-simplex-redefining-privacy-hard-choices.md new file mode 100644 index 0000000000..30c8a89e14 --- /dev/null +++ b/blog/20240516-simplex-redefining-privacy-hard-choices.md @@ -0,0 +1,79 @@ +--- +layout: layouts/article.html +title: "SimpleX: Redefining Privacy by Making Hard Choices" +date: 2024-05-16 +previewBody: blog_previews/20240516.html +image: images/simplex-explained.png +imageWide: true +permalink: "/blog/20240516-simplex-redefining-privacy-hard-choices.html" +--- + +# SimpleX: Redefining Privacy by Making Hard Choices + +**Published:** May 16, 2024 + +When it comes to open source privacy tools, the status quo often dictates the limitations of existing protocols and structures. However, these norms need to be challenged to radically shift how we approach genuinely private communication. This requires doing some uncomfortable things, like making hard choices as it relates to funding, alternative decentralization models, doubling down on privacy over convenience, and more. + +There will always be questions on why the SimpleX Chat and network makes the choices it makes, and that’s good! It’s important to question us and to understand the reasoning behind each decision, whether it’s technical, structural, financial or any other. + +In this post we explain a bit more about why SimpleX operates and makes decisions the way it does. + +## No user accounts + +Within SimpleX network there are no user accounts, and more importantly, no user profile identifiers whatsoever at the protocol level, not even random numbers or cryptographic keys used to identify the users. This means there is absolutely nothing that uniquely links users to their contacts or to the network relays. While it's accurate to say, "You need an address to send something," it's crucial to understand that this "address" serves merely as a transient delivery destination, and not as a user profile identifier in any sense. + +You can read more about how SimpleX works [here](https://simplex.chat/#how-simplex-works). + +## Privacy over convenience + +One of the main considerations often ignored in security and privacy comparisons between messaging applications is multi-device access. For example, in Signal’s case, the Sesame protocol used to support multi-device access has the vulnerability that is [explained in detail here](https://eprint.iacr.org/2021/626.pdf): + +_"We present an attack on the post-compromise security of the Signal messenger that allows to stealthily register a new device via the Sesame protocol. [...] This new device can send and receive messages without raising any ‘Bad encrypted message’ errors. Our attack thus shows that the Signal messenger does not guarantee post-compromise security at all in the multi-device setting"_. + + + +Solutions are possible, and even the quoted paper proposes improvements, but they are not implemented in any existing communication solutions. Unfortunately this results in most communication systems, even those in the privacy space, having compromised security in multi-device settings due to these limitations. That's the reason we are not rushing a full multi-device support, and currently only provide [the ability to use mobile app profiles via the desktop app](./20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md#link-mobile-and-desktop-apps-via-secure-quantum-resistant-protocol), while they are on the same network. + +Another choice that compromises privacy for convenience and usability is 3rd party push notifications. At SimpleX, we take a slow path of optimizing the network and battery consumption in the app, rather than simply hiding inefficiencies behind the quick fix solution of 3rd party push notifications that [increases vulnerability](https://www.wired.com/story/apple-google-push-notification-surveillance/), a path Signal and others chose. Like other choices, it has usability and optimization trade offs, but ultimately it’s the right thing to continue progressing towards a better solution as we explain [here](https://simplex.chat/blog/20220404-simplex-chat-instant-notifications.html). + +Whenever possible, we strive to achieve significantly higher levels of privacy and security. For example, unlike most, if not all, applications (including Signal), [we encrypt application files with per-file unique key](https://simplex.chat/blog/20230925-simplex-chat-v5-3-desktop-app-local-file-encryption-directory-service.html#encrypted-local-files-and-media-with-forward-secrecy). Consequently, once a message is deleted, there's no means to open a file that someone may have stolen in hopes of acquiring the key later. Similarly, apps like Session have done away with forward secrecy, a decision which caused them [not to be recommended](https://www.privacyguides.org/en/real-time-communication/#additional-options) for "long-term or sensitive communications". And [misinformation](https://simplifiedprivacy.com/spain-has-banned-telegram-defending-session/) around this makes it dangerous and irresponsible to recommend without such necessary disclosures for people’s awareness. + +Session’s decision was based on [the incorrect statements](https://getsession.org/blog/session-protocol-explained) about double ratchet being impossible in decentralized networks, and underplayed importance of forward secrecy, break-in recovery and deniability - the absence of these crucial qualities makes Session a much weaker choice for private messaging. For transparency, this was something that was debated with their team [here](https://twitter.com/SimpleXChat/status/1755216356159414602). We also made [a separate post](./20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md#end-to-end-encryption-security-attacks-and-defense) about these qualities of end-to-end encryption and their presence in different messengers, to show that not all end-to-end encrypted apps offer the same level of protections. + +## Network decentralization + + + +It's important to recognize that a model of decentralization where all servers are openly known and accessible to all clients, that some users ask for, actually results in a less decentralized network, and as the network grows it often requires an introduction of a central authority to protect from bad actors with malicious intent. Therefore, we've deliberately opted for a slower path towards achieving a higher degree of decentralization where there is no central server registry or network authority. For example, p2p designs may offer higher initial decentralization but often compromise on privacy and eventual decentralization. In essence, our approach prioritizes a balance between initial decentralization, privacy, and higher degree of decentralization down the line. + +Additionally, while it's true that we haven't yet established a model to incentivize other network operators, it's certainly on the roadmap. We see the decentralization of network operators offered within the app as a top priority.  + +Where it stands today, users have the freedom to select their preferred servers within the SimpleX network by configuring the app, with thousands of self-hosted servers in operation. Moreover, numerous third-party applications rely on our code for their in-app communications, operating independently of our servers, many of which we may not even be aware of. + +Decentralization is an ongoing journey, and we strive to proceed at a measured pace to ensure its proper implementation. While the immediate results may not always appear ideal, prioritizing a careful approach ensures that in the long run, the decisions made in this area align with our ultimate objectives of a private, efficient, reliable and fully decentralized network. + +## Funding and profitability + +We explain our rationale for funding [here](../docs/FAQ.md#funding-and-business-model). Funding sources is always one of the most difficult choices to make, and it’s important to underline that VC models don’t necessarily translate to a quest for control, interference of any kind, or overall influence on product roadmap and strategy. The vast majority of investors seek profitability. Irrespective of the organization type profitability is essential for a sustainable operation, and it can and should be done while adhering to the highest possible standards for privacy. For-profit vs. nonprofit is also not an accurate metric to measure a commitment towards privacy and open standards, which is further explained [here](./20240404-why-i-joined-simplex-chat-esraa-al-shafei.md).   + +To make a profit, satisfying customers is the key. Unlike the many companies that profit from selling customer data, we put user privacy first. Doing this at scale requires investments. If the investors don’t own or control a company, their participation becomes merely about profit for them, and not about how this profit is obtained. With the investors we have, we are completely aligned on this - they are betting on the future where privacy is the norm. They do not dictate on anything related to our model. We build SimpleX chat, protocols and network the way Internet should have been built if we as developers always put the privacy and empowerment of users first. + +## Company jurisdiction + + + +With regards to jurisdictions, nowhere is perfect. For that reason we plan to establish the foundations for protocol governance in [various jurisdictions](https://simplex.chat/blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.html#the-journey-to-the-decentralized-non-profit-protocol-governance). + +But we’d like to clarify some misconceptions about the UK, where SimpleX Chat Ltd. is registered, and the UK legislation. + +For example, the Online Safety Act (OSA). Some people believe that it applies only to UK companies. But the OSA applicability isn’t determined by the company’s jurisdiction - it applies based on the nature and characteristics of the business and its services, as well as the number of its users in the UK. In case of SimpleX network, the OSA doesn’t apply for both of these reasons. + +The UK’s position on communication encryption, and more specifically, on end-to-end encrypted messaging, remains the subject of political debates. But with the OSA, the legislative intent was to propose technical measures to block CSAM, and it was trying to explore ways to do this via client-side scanning, which of course would undermine the encryption. However, and thanks to the hard work of privacy experts, researchers, academics and rights organizations throughout the UK and the rest of the world, the Online Safety Bill did not prohibit end-to-end encrypted apps without such scanners. It is an open question whether such technology will ever be possible, and the UK government made a public commitment that client-side scanning won't be required until it is. + +For now, strong end-to-end encryption remains permissible and protected, and we hope to also add to the privacy advocacy and debates as a UK-based company to keep it legally protected. + +Overall, we view the UK as being better jurisdiction for privacy than many alternatives - there are some trade-offs everywhere. + +## Looking ahead  + +The future of the Internet should be based on decentralized infrastructure operated by commercially viable organizations. These operators need to possess minimal user data, so that users have genuine control over their identities, and free from lock-in by the operators, to support fair competition. This requires a drastic re-imagining of the current norms and newer, more privacy-minded protocols. All in all, private messaging is surrounded by very difficult challenges but it’s worth it to keep pushing the industry forward and not settle for the status quo and current trade offs, protocol limitations and vulnerabilities. The Internet deserves better standards, and so do users. diff --git a/blog/20240601-protecting-children-safety-requires-e2e-encryption.md b/blog/20240601-protecting-children-safety-requires-e2e-encryption.md new file mode 100644 index 0000000000..39a047f93f --- /dev/null +++ b/blog/20240601-protecting-children-safety-requires-e2e-encryption.md @@ -0,0 +1,32 @@ +--- +layout: layouts/article.html +title: "Protecting Children's Safety Requires End-to-End Encryption" +date: 2024-06-01 +previewBody: blog_previews/20240601.html +image: images/20240601-eu-privacy.png +permalink: "/blog/20240601-protecting-children-safety-requires-e2e-encryption.html" +--- + +# Protecting Children's Safety Requires End-to-End Encryption + +As lawmakers grapple with the serious issue of child exploitation online, some proposed solutions would fuel the very problem they aim to solve. Despite expert warnings, the Belgian Presidency persists in pushing for the implementation of client-side scanning on encrypted messaging services, rebranding the effort as "upload moderation". Their [latest proposal](https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=COM%3A2022%3A209%3AFIN&qid=1652451192472) mandates that providers of private communication services obtain user consent for AI-based scanning of their private chats. If users do not consent, they will be prohibited from sharing images, videos, and URLs. + +Privacy critics have long pushed for measures like centralized scanning of private photos and messaging data, arguing it could detect illicit content. However, invasive monitoring of private communications would create detrimental risks that far outweigh any perceived benefits. + +## Why we’re taking action + +SimpleX Chat signed a [joint statement](https://www.globalencryption.org/2024/05/joint-statement-on-the-dangers-of-the-may-2024-council-of-the-eu-compromise-proposal-on-eu-csam/) about the dangers of the EU compromise proposal on EU CSAM because maintaining end-to-end encryption is crucial for protecting privacy and security for everyone, including and especially children.  + +We urge the Ministers in the Council of the EU to stand firm against any scanning proposals that undermine end-to-end encryption, which would enable mass surveillance and misuse by bad actors, whether framed as client-side scanning, upload moderation, or any other terminology. Compromising this basic principle opens the door to devastating privacy violations. We also urge any organizations or individuals reading this to write to their representatives and voice their concerns. European Digital Rights has [outlined these issues](https://edri.org/our-work/be-scanned-or-get-banned/) in greater detail for anyone seeking more information. + +## Why compromising privacy endangers children + +The core issue is that compromising encryption and privacy makes innocent people vulnerable to malicious hackers and criminals seeking to exploit users data. Centralized scanning systems become a tempting target, potentially exposing millions of private family photos when breached. This would easily open up avenues for blackmail, abuse, and victimization of children. A case in point is the recent [criminal charges](https://techcrunch.com/2024/01/17/unredacted-meta-documents-reveal-historical-reluctance-to-protect-children-new-mexico-lawsuit/) against Meta in New Mexico, which highlights how the tech giant's algorithms enabled child exploitation by encouraging connections between minors and sexual predators. Privacy-eroding initiatives like client-side scanning would play into the hands of malicious actors by making more sensitive information accessible and weaponized in the same way that it has been on Meta platforms. + +## What should be done + +Rather than undermining privacy, to achieve child safety online users should be empowered with high standards for encryption and data control. For example, adopting a model where children (and users in general) cannot be discovered or approached on networks unless they or their parents permit it, similar to the SimpleX network privacy model. Intelligent multi-device synchronization could enable this oversight without compromising end-to-end encryption overall. It’s always possible to protect children without opening everyone, especially children themselves, to greater vulnerabilities due to such proposals. + +However, some recent legislative efforts have bizarrely moved in the opposite direction by seeking to limit parental access. The chilling truth is that the least private platforms have been major enablers of child exploitation. Eroding privacy protections on other services will only aid criminals further, not protect children. Preserving strong encryption and user privacy must be the foundation for any credible effort to combat online child exploitation. Initiatives trading privacy for supposed safety are not just technically flawed, but would achieve the exact opposite of their stated intent. We must avoid being gaslighted by narratives that defy logic, and instead provide users with the highest possible standards for privacy protections as a core principle. + +Protecting end-to-end encryption without carving out backdoors or vulnerabilities should be non-negotiable for children's and everyone’s safety. It is critical to redirect the discourse to focus on taking genuine privacy further by protecting against [metadata hoarding](https://simplex.chat/blog/20240416-dangers-of-metadata-in-messengers.html) and other means by which people’s data can be abused or subjected to surveillance. diff --git a/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md b/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md new file mode 100644 index 0000000000..9e915bd3c4 --- /dev/null +++ b/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md @@ -0,0 +1,170 @@ +--- +layout: layouts/article.html +title: "SimpleX network: private message routing, v5.8 released with IP address protection and chat themes" +date: 2024-06-04 +previewBody: blog_previews/20240604.html +image: images/20240604-routing.png +imageBottom: true +permalink: "/blog/20240604-simplex-chat-v5.8-private-message-routing-chat-themes.html" +--- + +# SimpleX network: private message routing, v5.8 released with IP address protection and chat themes + +**Published:** June 4, 2024 + +What's new in v5.8: +- [private message routing](#private-message-routing). +- [server transparency](#server-transparency). +- [protect IP address when downloading files & media](#protect-ip-address-when-downloading-files--media). +- [chat themes](#chat-themes) for better conversation privacy - in Android and desktop apps. +- [group improvements](#group-improvements) - reduced traffic and additional preferences. +- improved networking, message and file delivery. + +Also, we added Persian interface language to the Android and desktop apps, thanks to [our users and Weblate](https://github.com/simplex-chat/simplex-chat#help-translating-simplex-chat). + +## Private message routing + +### What's the problem? + + + +SimpleX network design has always been focussed on protecting user identity on the messaging protocol level - there is no user profile identifiers of any kind in the protocol design, not even random numbers or cryptographic keys. + +Until this release though, SimpleX network had no built-in protection of user transport identities - IP addresses. As previously the users could only choose which messaging relays to use to receive messages, these relays could observe the IP addresses of the senders, and if these relays were controlled by the recipients, the recipients themselves could observe them too - either by modifying server code or simply by tracking all connecting IP addresses. + +To work around this limitation, many users connected to SimpleX network relays via Tor or VPN - so that the recipients' relays could not observe IP addresses of the users when they send messages. Still, it was the most important and the most criticized limitation of SimpleX network for the users. + +### Why didn't we just embed Tor in the app? + +Tor is the best transport overlay network in existence, and it provides network anonymity for millions of Internet users. + +SimpleX Chat has many integration points with Tor: +- it allows [dual server addresses](./20220901-simplex-chat-v3.2-incognito-mode.md#using-onion-server-addresses-with-tor), when the same messaging relay can be reached both via Tor and via clearnet. +- it utilises Tor's SOCKS proxy "isolate-by-auth" feature to create a new Tor circuit for each user profile, and with an additional option - for each contact. Per-contact [transport isolation](./20230204-simplex-chat-v4-5-user-chat-profiles.md#transport-isolation) is still experimental, as it doesn't work if you connect to groups with many members, and it's only available if you enable developer tools. + +Many SimpleX network design ideas are borrowed from Tor network design: +- mitigation of [MITM attack](../docs/GLOSSARY.md#man-in-the-middle-attack) on client-server connection is done in the same way as Tor relays do it - the fingerprint of offline certificate is included in server address and validated by the client. +- the private routing itself uses the approach similar to onion routing, by adding encryption layers on each hop. +- we are also considering to implement Tor's [Proof-of-work DoS defence](https://blog.torproject.org/introducing-proof-of-work-defense-for-onion-services/) mechanism. + +So why didn't we just embed Tor into the messaging clients to provide IP address protection? + +We believe that Tor may be the wrong solution for some users for one of the reasons: +- much higher latency, error rate and resource usage. +- people who want to use Tor are better served by specialized apps, such as [Orbot](https://guardianproject.info/apps/org.torproject.android/). +- Tor usage is restricted in some networks, so it would require complex configuration in the app UI. +- some countries have legislative restrictions on Tor usage, so embedding Tor would require supporting multiple app versions, and it would leave the original problem unsolved in these countries. + +Also, while Tor solves the problem of IP address protection, it doesn't solve the problem of meta-data correlation by user's transport session. When the client connects to the messaging relays via Tor, the relays can still observe which messaging queues a user sends messages to via a single TCP connection. The client can mitigate it with per-contact transport isolation, but it uses too much traffic and battery for most users. + +So we believed we would create more value to the users of SimpleX network with private message routing. This new message routing protocol provides IP address and transport session protection out of the box, once released. It can also be extended to support delayed delivery and other functions, improving both usability and transport privacy in the future. + +At the same time, we plan to continue supporting Tor and other overlay networks. Any overlay network that supports SOCKS proxy with "isolate-by-auth" feature will work with SimpleX Chat app. + +### What is private message routing and how does it work? + +Private message routing is a major milestone for SimpleX network evolution. It is a new message routing protocol that protects both users' IP addresses and transport sessions from the messaging relays chosen by their contacts. Private message routing is, effectively, a 2-hop onion routing protocol inspired by Tor design, but with one important difference - the first (forwarding) relay is always chosen by message sender and the second (destination) - by the message recipient. In this way, neither side of the conversation can observe IP address or transport session of another. + +At the same time, the relays chosen by the sending clients to forward the messages cannot observe to which connections (messaging queues) the messages are sent, because of the additional layer of end-to-end encryption between the sender and the destination relay, similar to how onion routing works in Tor network, and also thanks to the protocol design that avoids any repeated or non-random identifiers associated with the messages, that would otherwise allow correlating the messages sent to different connections as sent by the same user. Each message forwarded to the destination relay is additionally encrypted with one-time ephemeral key, to be independent of messages sent to different connections. + +The routing protocol also prevents the possibility of MITM attack by the forwarding relay, which provides the certificate the session keys of the destination server to the sending client that are cryptographically signed by the same certificate that is included in destination server address, so the client can verify that the messages are sent to the intended destination, and not intercepted. + +The diagram below shows all the encryption layers used in private message routing: + +``` +----------------- ----------------- -- TLS -- ----------------- ----------------- +| | -- TLS -- | | -- f2d -- | | -- TLS -- | | +| | -- s2d -- | | -- s2d -- | | -- d2r -- | | +| Sending | -- e2e -- | sender's | -- e2e -- | recipient's | -- e2e -- | Receiving | +| client | message -> | Forwarding | message -> | Destination | message -> | client | +| | -- e2e -- | relay | -- e2e -- | relay | -- e2e -- | | +| | -- s2d -- | | -- s2d -- | | -- d2r -- | | +| | -- TLS -- | | -- f2d -- | | -- TLS -- | | +----------------- ----------------- -- TLS -- ----------------- ----------------- +``` + +**e2e** - two end-to-end encryption layers between **sending** and **receiving** clients, one of which uses double ratchet algorithm. These encryption layers are present in the previous version of message routing protocol too. + +**s2d** - encryption between the **sending** client and recipient's **destination** relay. This new encryption layer hides the message metadata (destination connection address and message notification flag) from the forwarding relay. + +**f2d** - additional new encryption layer between **forwarding** and **destination** relays, protecting from traffic correlation in case TLS is compromised - there are no identifiers or cyphertext in common between incoming and outgoing traffic of both relays inside TLS connection. + +**d2r** - additional encryption layer between destination relay and the recipient, also protecting from traffic correlation in case TLS is compromised. + +**TLS** - TLS 1.3 transport encryption. + +For private routing to work, both the forwardig and the destination relays should support the updated messaging protocol - it is supported from v5.8 of the messaging relays. It is already released to all relays preset in the app, and available as a self-hosted server. We updated [the guide](../docs/SERVER.md) about how to host your own messaging relays. + +Because many self-hosted relays did not upgrade yet, private routing is not enabled by default. To enable it, you can open *Network & servers* settings in the app and change the settings in *Private message routing* section. We recommend setting *Private routing* option to *Unprotected* (to use it only with unknown relays and when not connecting via Tor) and *Allow downgrade* to *Yes* (so messages can still be delivered to the messaging relays that didn't upgrade yet) or to *When IP hidden* (in which case the messages will fail to deliver to unknown relays that didn't upgrade yet unless you connect to them via Tor). + +Read more about the technical design of the private message routing in [this document](https://github.com/simplex-chat/simplexmq/blob/stable/rfcs/2023-09-12-second-relays.md). + +## Server transparency + + + +Even with very limited information available to the messaging relays, there are [several things](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md#simplex-messaging-protocol-server) that would reduce users' privacy that a compromised relay can do. + +We [wrote previously](https://github.com/simplex-chat/simplexmq/blob/master/rfcs/2024-03-20-server-metadata.md) that it is important that server operators commit to running unmodified server code or disclose any code modifications, and also disclose server ownership and any other relevant information. + +While we cannot require the operators of self-hosted and private servers to disclose any information about them (apart from which server code they use - this is the requirement of the AGPLv3 license to share this information with users connecting to the server), as we add other server operators to the app, it is important for the users to have all important information about these operators and servers location. + +This server release adds server information page where all this information can be made available to the users. For example, this is the information about one of the servers preset in the app. + +The updated server guide also includes [the instruction](../docs/SERVER.md#) about how to host this page for your server. It is generated as a static page when the server starts. We recommend using Caddy webserver to serve it. + +## More new things in v5.8 + +### Protect IP address when downloading files & media + +This version added the protection of your IP address when receiving files from unknown file servers without Tor. Images and voice messages won't automatically download from unknown servers too until you tap them, and confirm that you trust the file server where they were uploaded. + +### Chat themes + + + +In Android and desktop app you can now customize how the app looks by choosing wallpapers with one of the preset themes or choose your own image as a wallpaper. + +But this feature is not only about customization - it allows to set different colors and wallpaper for different user profiles and even specific conversations. You can also choose different themes for different chat profiles. + +In case you use different identities for different conversations, it helps avoiding mistakes. + +### Group improvements + +This version adds additional group configuration options to allow sending images, files and media, and also SimpleX links only to group administrators and owners. So with this release group owners can have more control over content shared in the groups. + +We also stopped unnecessary traffic caused by the members who became inactive without leaving the groups - it should substantially reduce traffic and battery consumption to the users who send messages in large groups. + +## SimpleX network + +Some links to answer the most common questions: + +[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers). + +[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). + +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). + +[Frequently asked questions](../docs/FAQ.md). + +Please also see our [website](https://simplex.chat). + +## Help us with donations + +Huge thank you to everybody who donates to SimpleX Chat! + +We are planning a 3rd party security audit for the protocols and cryptography design in July 2024, and also the security audit for an implementation in December 2024/January 2025, and it would hugely help us if some part of this $50,000+ expense is covered with donations. + +We are prioritizing users privacy and security - it would be impossible without your support. + +Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, - so anybody can build the future implementations of the clients and the servers. We are building SimpleX network based on the same principles as email and web, but much more private and secure. + +Your donations help us raise more funds – any amount, even the price of the cup of coffee, makes a big difference for us. + +See [this section](https://github.com/simplex-chat/simplex-chat/tree/master#help-us-with-donations) for the ways to donate. + +Thank you, + +Evgeny + +SimpleX Chat founder diff --git a/blog/20240704-future-of-privacy-enforcing-privacy-standards.md b/blog/20240704-future-of-privacy-enforcing-privacy-standards.md new file mode 100644 index 0000000000..681242e044 --- /dev/null +++ b/blog/20240704-future-of-privacy-enforcing-privacy-standards.md @@ -0,0 +1,51 @@ +--- +layout: layouts/article.html +title: "The Future of Privacy: Enforcing Privacy Standards" +date: 2024-07-04 +previewBody: blog_previews/20240704.html +image: images/20240704-privacy.jpg +imageWide: true +permalink: "/blog/20240704-future-of-privacy-enforcing-privacy-standards.html" +--- + +# The Future of Privacy: Enforcing Privacy Standards + +**Published:** Jul 4, 2024 + +Recent anti-privacy legislations and proposals in [Europe](https://www.theverge.com/2024/6/19/24181214/eu-chat-control-law-propose-scanning-encrypted-messages-csam), [the US](https://theconversation.com/section-702-foreign-surveillance-law-lives-on-but-privacy-fight-continues-229253) and [Australia](https://www.theguardian.com/technology/article/2024/jun/20/meredith-walker-signal-boss-government-encryption-laws) threaten to infringe our fundamental right to privacy and to create grave risks to the [safety](https://simplex.chat/blog/20240601-protecting-children-safety-requires-e2e-encryption.html) of children and vulnerable people. It's time we shift the focus: privacy should be a non-negotiable duty of technology providers, not just a right users must constantly fight to protect, and not something that users can be asked to consent away as a condition of access to a service. + +Tech giants are trying to normalize surveillance capitalism, often with little to no consequences globally. These companies are contributing to a growing ecosystem where opting out of invasive data hoarding practices is becoming increasingly challenging, if not outright impossible. We are being gaslit by the technology executives who try to justify profiteering from AI theft, from [Microsoft](https://www.computing.co.uk/news/4330395/microsoft-ai-chief-makes-questionable-claims-about-copyright-online-content) claiming all our content is fair game for their exploitation to unethical startups like [Perplexity](https://www.theverge.com/2024/6/27/24187405/perplexity-ai-twitter-lie-plagiarism) turning the word “[privacy](https://x.com/perplexity_ai/status/1789007907092066559)” into a marketable farce. + +## The AI Hype’s Impact on Privacy + +The exaggeration of AI’s actual capabilities and the continuous promotion of its “intelligence” is creating a rat race where tech companies and well-funded startups are evading accountability, as they eagerly collect and exploit more data than ever.  + +They're prioritizing AI development over user privacy and rights, setting a dangerous precedent for current and future online engagements. They've already normalized the use of AI to scan and analyze supposedly private communications - from emails to instant messages - repackaging this intrusion as "productivity tools”. Meanwhile, most consumers actually want [more data privacy](https://iapp.org/news/a/most-consumers-want-data-privacy-and-will-act-to-defend-it), not less, and are increasingly concerned by the lack of it. + +The legal push towards “client-side scanning”, attacks on end-to-end encryption and the support for pro-surveillance legislation gives credibility to these highly intrusive practices that literally endanger lives. And we know that moral obligations mean nothing to corporations benefiting from these exploitative models, so we have to ensure that our demands for privacy are legally enforceable and non-negotiable.  + +## Legal Action + +We are encouraged to see more legal pressure on companies that exploit user data on a daily basis. For example, the European Center for Digital Rights’ (Noyb) [complaints](https://noyb.eu/en/noyb-urges-11-dpas-immediately-stop-metas-abuse-personal-data-ai) against Meta’s abuse of personal data to train their AI and, and the demands from the Norwegian Consumer Council to data protection authority to ensure that applicable laws are enforced against Meta considering there is “[no way to remove personal data from AI models once Meta has begun the training](https://www.forbrukerradet.no/side/legal-complaint-against-metas-use-of-personal-content-for-ai-training/)”. + +Noyb is taking a strong stance against [other companies](https://noyb.eu/en/project/cases) with similar exploitative models, including facial recognition surveillance tools often misused by law enforcement agencies. Consider [supporting](https://noyb.eu/en/support) their ongoing efforts — we strongly believe legal action is one of the most effective means to hold these companies accountable for their persistent abuses, which are otherwise shielded by heavily funded self-serving lobby groups. + +## Privacy as a Legal Obligation + +We must shift from a defensive stance to a proactive one by proposing privacy legislation that puts users in direct control of their private data. + +This legislation should: +1. Establish non-negotiable provider duties for protecting user privacy, with hefty fines and consequences for service operators who do not comply. +2. Prevent providers from circumventing these duties through user consent clauses — it should be legally prohibited to ask for a consent to share user data or to use it for anything other than providing a service. +3. Prevent providers from asking for any more personal information from the users than technically necessary and legally required. For example, asking for a phone number as a condition of access to a service should be made illegal in most cases — it does not provide a sufficient security, exposes users' private information and allows simple aggregation of users' data across multiple services. +4. Create a strong legal framework that cannot be resisted or modified + +By codifying these principles into law, we can establish a strong technological framework that is built to create more value for end users, while protecting their privacy against data exploitation, criminal use and identity theft. We will continue the fight against illogical legislative proposals designed to normalize mass surveillance, but our efforts should equally gear towards creating and supporting new models and technological foundations that bring us far closer to the reality we urgently need. + +## Collective Action + +There is great work being done by advocacy organizations, and service providers need to contribute to this fight as well by shifting the narrative and reclaiming the term “privacy” from the tech giants who co-opted and corrupted it. We must play a bigger role in supporting users in setting stronger boundaries, making demands, and refusing anything less than genuine privacy and data ownership, while getting comfortable with holding providers accountable for any violations. + +Privacy should be seen as a fundamental obligation of technology providers, and legislators must actively enforce this expectation. The more consumers make this demand, the more pressure we put on anti-privacy lobbyists with rogue motives, the easier it will be to hold abusers accountable, and the more likely we can collectively ensure that a privacy-first web becomes a reality. + +You can support privacy today by signing [the petition](https://www.globalencryption.org/2024/05/joint-statement-on-the-dangers-of-the-may-2024-council-of-the-eu-compromise-proposal-on-eu-csam/) prepared by Global Encryption Coalition in support of communication privacy. You can also write to your elected representatives, explaining them how data privacy and encrypted communications protect children safety and reduce crime. diff --git a/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md b/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md new file mode 100644 index 0000000000..e81bf5516a --- /dev/null +++ b/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md @@ -0,0 +1,247 @@ +--- +layout: layouts/article.html +title: "SimpleX network: the investment from Jack Dorsey and Asymmetric, v6.0 released with the new user experience and private message routing." +date: 2024-08-14 +image: images/20240814-reachable.png +previewBody: blog_previews/20240814.html +permalink: "/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.html" +--- + +# SimpleX network: the investment from Jack Dorsey and Asymmetric, v6.0 released with the new user experience and private message routing. + +**Published:** Aug 14, 2024 + +[SimpleX Chat: vision and funding 2.0](#simplex-chat-vision-and-funding-20): +- [The past](#the-past-investment-from-village-global): investment from Village Global. +- [The present](#the-present-announcing-the-investment-from-jack-dorsey-and-asymmetric): announcing the investment from Jack Dorsey and Asymmetric Capital Partners. +- [The future](#the-future-faster-development-and-transition-to-non-profit-governance): faster development and the path to non-profit governance. + +[What's new in v6.0](#whats-new-in-v60): +- Private message routing — now enabled by default. +- [New chat experience](#new-chat-experience): + - connect to your friends faster. + - [new reachable interface](#new-reachable-interface). + - archive contacts to chat later. + - new way to start chat. + - [moderate like a pro](#moderate-like-a-pro): delete many messages at once. + - new chat themes* + - increase font size**. +- [New media options](#new-media-options): + - play from the chat list. + - blur for better privacy. + - [share from other apps](#share-from-other-apps)*. +- [Improved networking and reduced battery usage](#improved-networking-and-reduced-battery-usage) + +\* New for iOS app. + +\*\* Android and desktop apps. + +## SimpleX Chat: vision and funding 2.0 + +### The past: investment from Village Global + +Last year [we announced](https://simplex.chat/blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.html#how-is-it-funded-and-what-is-the-business-model) pre-seed funding from several angel investors and Village Global. Some of our users were very excited that we have funds to continue developing SimpleX network. But as some of Village Global LPs (Limited Partners) are [the founders of very large technology companies](https://www.villageglobal.vc), some of our users were worried about any negative influence this investment might have on the project. + +[Ben Casnocha](https://www.villageglobal.vc/team/ben-casnocha), the founder and general partner of Village Global, commented on their investment: + +> I believe in SimpleX Chat vision and team’s ability to execute it. The growing number of Internet users who demand privacy of their data and contacts will make SimpleX Chat profitable, which is critically important for any sustainable organization. +> +> We are fortunate to have LPs who founded many iconic Internet ventures. But they don’t have any influence on the 400+ companies we invested in. They are financial investors in our fund and exert no control or influence on any of the underlying portfolio companies. +> +> What's more, we believe that founders should lead their ventures, as it yields better results – our investment in SimpleX Chat has no control provisions. We are happy to help, but we don’t control any decisions nor have a board seat. Evgeny runs the company independently. + +Ben, thank you for believing in our vision – without it SimpleX Chat would simply not exist, as most other investors at the time did not believe that privacy could ever escape the niche of privacy enthusiasts – and we already see the first signs of it happening. + +### The present: announcing the investment from Jack Dorsey and Asymmetric + +The Android app recently hit [100,000 downloads on Google Play Store](https://play.google.com/store/apps/details?id=chat.simplex.app), and our users naturally ask for improved reliability, privacy, security, better user experience and design – all at the same time, and as soon as possible. This requires more funding. + +We are very happy to announce that we now have funds to move faster – we raised a $1.3 million pre-seed round led by [Jack Dorsey](https://en.wikipedia.org/wiki/Jack_Dorsey), with participation of [Asymmetric Capital Partners](https://www.acp.vc) (ACP) VC fund. + +When Jack discovered SimpleX Chat last year, he [posted on Twitter](https://x.com/jack/status/1661681076983529479): + +> Better than Signal? Looks promising. +> A few bugs and UX issues but great foundation. Love that it’s public domain. + +And [on Nostr](https://primal.net/e/note1txz9xmmc456kwcg7zrsrtqrhn7as29ptuz0qulu452k8n85hsshqq6uh6q): + +> A full day with @SimpleX Chat. Solid overall. TestFlight is not recommended. There are some scaling issues today. And not the most intuitive onboarding for everyone. Name still reminds everyone of herpes. All fixable. It’s fast and doesn’t require a phone number or email and I do believe people will eventually see the value of that. Finally, some competition for Signal, and in a permissionless way. And def a solid path so apps don’t have to build their own DM experiences. + +Jack, we are super lucky to have your support and investment – thank you for believing in our ability to build a better messaging network! It is a hard work, and we’ve made a lot of progress since your note was written, and a lot of work is ongoing! + +The ACP investment is strategically important – it is a fund that only invests in B2B startups, and SimpleX Chat currently is mostly used by individual users. Making a private communication network sustainable requires its adoption by businesses, and we already see a growing usage by the small teams. + +[Rob Biederman](https://www.acp.vc/team/rob-biederman) and [Sam Clayman](https://www.acp.vc/team/sam), the partners of ACP, commented: + +> We believe that SimpleX Chat network can grow into a de facto Internet standard for private and secure communications for both businesses and individual users, unifying instant and email-like messaging into a single product. +> +> Emails no longer provide privacy and security that businesses require, particularly given the emerging threat of AI-led phishing and social engineering attacks. We look forward to SimpleX network providing a secure alternative. + +I was lucky to have met Rob, Sam and the ACP team when I was presenting SimpleX Chat in London – thank you all for your support and believing that the future of communication requires a single product, both for businesses and individual users. + +### The future: faster development and the path to non-profit governance + +Jack Dorsey and ACP support enable us to make huge product improvements, thanks to a bigger team, and provide us with medium-term funding to get to the next stage of product and business evolution. Like with Village Global, this is a financial investment, without control or board seat provisions – so the users can be certain that SimpleX remains true to our vision of privacy first communication network. + +We already added two great engineers to the team and are about to hire a UX/UI designer. + +[Trail of Bits](https://www.trailofbits.com/about/) has just completed the protocols design security review and will be doing implementation security review in the end of the year. We will publish the first report soon. + +This year we will launch group improvements that we presented in the [live-stream last year](https://www.youtube.com/watch?v=7yjQFmhAftE). While the main problem explained in this video was solved with the current design, the issue of group scalability remains – to send a message to a group your client needs to send it to each member, creating substantial traffic. + +We will also launch long-form email-like messaging over SimpleX network this year, together with optional short public addresses that show profile you are connecting to before the connection – this is important for any public users and businesses. + +The last but not the least, we started the work with [Heather Meeker](https://www.techlawpartners.com/heather), a great legal expert on intellectual property matters and one of the earliest advocates of the open-source software development in businesses, to setup open-source governance model, to some extent similar to how Matrix did it. We believe, and our investors agree, that it would both increase the company value and also create more value for the users community. + +## What's new in v6.0 + +v6.0 is one of our biggest releases ever, with a lot of focus on UX and stability improvements, and the new features the users asked for. + +The private message routing [we announced before](./20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md) is now enabled for all users by default – it protects users IP addresses and sessions from the destination servers. + +### New chat experience + +#### Connect to your friends faster + +This version includes messaging protocol improvements that reduce twice the number of messages required for two users to connect. Not only it means connecting faster and using less traffic, this change allows to start sending messages sooner, so you would see "connecting" in the list of the chats for a much shorter time than before. + +It will be improved further in the next version: you will be able to send messages straight after using the invitation link, without waiting for your contact to be online. + +#### New reachable interface + + + +Like with the most innovative mobile browsers (e.g., Safari and Firefox), SimpleX Chat users now can use the app with one hand by moving the toolbar and search bar to the bottom of the screen, and ordering the chats with the most recent conversations in the bottom too, where they can be more easily reached on a mobile screen. + +This layout is enabled by default, and you can disable it right from the list of chats when you install the new version if you prefer to use conventional UI. + +Give it a try – our experience is that that after less than a day of using it, it starts feeling as the only right way. You can always toggle it in the Appearance settings. + +#### Archive contacts to chat later + +   + +Now you have two new options when deleting a conversation: +- only delete conversation, and archive contact. We will add archiving conversation without clearing it in the next version, as some users of our beta version asked. +- delete contact but keep the conversation. + +Also, deleting a contact now requires double confirmation, so you are less likely to delete the contact accidentally. This deletion is irreversible, and the only way to re-connect would be using a new link. + +#### New way to start chat + + + +When you tap pencil button, you will see a large *New message* sheet, that adds new functions to the options you had before. + +Old options: +- *Add contact* to create a new 1-time invitation link, +- *Scan / paste link*: to use the link you received. It can be 1-time invitation, a public SimpleX address, or a link to join the group. +- *Create group* + +New options: +- Open archived chats. +- Accept pending contact requests. +- Connect to preset public addresses (we will add an option to add your own addresses here too). +- Search for your contacts. + +#### New chat themes + +We released the new themes [for Android and desktop apps](./20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md) in the previous version, and now they are available for iOS too. + +You can set different themes for different chat profiles you have, and for different conversations – it can help avoid mistakes about which conversation you are in. + +Also, these themes are compatible between platforms, so you can import the theme created on Android into iOS app and vice versa. + +#### Moderate like a pro + + + +As much as we disagree with the attacks on the freedom of speech on the society level – all people must be able to express their opinions – we also believe that the small community owners should have full control over which content is allowed and which is not. But as communities grow, bad actors begin to join in order to disrupt, subvert and troll the conversations. So, the moderation tools are critical for small public communities to thrive. + +SimpleX Chat already has several moderation tools available for community owners: +- Moderate individual messages. +- Set the default role of the new members to "observer" — they won't be able to send messages until you allow it. In addition to that, by enabling default messages for admins and owners only you can reach out to the new members and ask some questions before allowing to send messages. +- Block messages of a member for yourself only. +- Block a member for all other members — only admins and group owners can do that. + +With this version you can now select multiple messages at once and delete or moderate them, depending on your role in the community. The current version limits the number of messages that can be deleted to 20 — this limit will be increased to 200 messages in the next version. + +Also, this version makes profile images of the blocked members blurred, to prevent the abuse via inappropriate profile images. + +#### Increase font size + +Android and desktop apps now allow to increase font size inside the app, without changing the system settings. Desktop app also allows to zoom the whole screen — it can be helpful on some systems with a limited support of high density displays. + +These settings can be changed via Appearance settings. + +### New media options + +#### Play from the chat list + + + +Now you can interact with the media directly from the list of the chats. + +This is very convenient – when somebody sends you a voice message or a video, they can be played directly from the list of chats, without opening a conversation. Similarly, an image can be opened, a file can be saved, and the link with preview can be opened in the browser. + +And, in some circumstances, this is also more private, as you can interact with the media, without opening the whole conversation. + +We will add the option to return missed calls from the chat list in the next version. + +#### Blur for better privacy + +You can set all images and videos to blur in your app, and unblur them on tap (or on hover in desktop app). The blur level can be set in Privacy and security settings. + +#### Share from other apps + + + +Not much to brag about, as most iOS messaging apps allow it, and users expected it to be possible since the beginning. + +But iOS makes it much harder to develop the capability to share into the app than Android, so it's only in this version you can share images, videos, files and links into SimpleX Chat from other apps. + +### Improved networking and reduced battery usage + +This version includes the statistics of how your app communicates with all servers when sending and receiving messages and files. This information also includes the status of connection to all servers from which you receive messages — whether the connection is authorized to push messages from server to your device, and the share of these active connections. + +Please note, that when you send a message to a group, your app has to send it to each member separately, so sent message statistics account for that — it may seem to be quite a large number if you actively participate in some large groups. Also, message counts not only include visible messages you receive and send, but also any service messages, reactions, message updates, message deletions, etc. — this is the correct reflection of how much traffic your app uses. + +This information is only available to your device, we do NOT collect this information, even in the aggregate form. + +While the main reason we added this information is to reduce traffic and battery usage, to be able to identify any cases of high traffic, this version already reduced a lot battery and traffic usage, as reported by several beta-version users. + +## SimpleX network + +Some links to answer the most common questions: + +[How can SimpleX deliver messages without user identifiers](./20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers). + +[What are the risks to have identifiers assigned to the users](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md#why-having-users-identifiers-is-bad-for-the-users). + +[Technical details and limitations](https://github.com/simplex-chat/simplex-chat#privacy-technical-details-and-limitations). + +[Frequently asked questions](../docs/FAQ.md). + +Please also see our [website](https://simplex.chat). + +## Please support us with your donations + +Huge thank you to everybody who donated to SimpleX Chat! + +You might ask: *Why do you need donations if you've just raised the investment?* + +Prioritizing users privacy and security, and also raising the investment, would have been impossible without your support and donations. + +Also, funding the work to transition the protocols to non-profit governance model would not have been possible without the donations we received from the users. + +Our pledge to our users is that SimpleX protocols are and will remain open, and in public domain, so anybody can build the future implementations of the clients and the servers. We are building SimpleX platform based on the same principles as email and web, but much more private and secure. + +Your donations help us raise more funds — any amount, even the price of the cup of coffee, makes a big difference for us. + +See [this section](https://github.com/simplex-chat/simplex-chat/tree/master#help-us-with-donations) for the ways to donate. + +Thank you, + +Evgeny + +SimpleX Chat founder diff --git a/blog/README.md b/blog/README.md index 2f3b5aeeb2..03afc15f8f 100644 --- a/blog/README.md +++ b/blog/README.md @@ -1,5 +1,61 @@ # Blog +Aug 14, 2024 [SimpleX network: the investment from Jack Dorsey and Asymmetric, v6.0 released with the new user experience and private message routing](./20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.md) + +[SimpleX Chat: vision and funding 2.0](#simplex-chat-vision-and-funding-20): past, present, future. + +Announcing the investment from Jack Dorsey and Asymmetric. + +What's new in v6.0: +- Private message routing - now enabled by default +- New chat experience: + - connect to your friends faster. + - new reachable interface. + - and much more! +- Improved networking and reduced battery usage + +--- + +Jul 4, 2024 [The Future of Privacy: Enforcing Privacy Standards](./20240704-future-of-privacy-enforcing-privacy-standards.md) + +It's time we shift the focus: privacy should be a non-negotiable duty of technology providers, not just a right users must fight to protect, and not something that users can be asked to consent away as a condition of access to a service. + +--- + +Jun 4, 2024 [SimpleX network: private message routing, v5.8 released with IP address protection and chat themes](./20240604-simplex-chat-v5.8-private-message-routing-chat-themes.md) + +What's new in v5.8: +- private message routing. +- server transparency. +- protect IP address when downloading files & media. +- chat themes for better conversation privacy - in Android and desktop apps. +- group improvements - reduced traffic and additional preferences. +- improved networking, message and file delivery. + +Also, we added Persian interface language to the Android and desktop apps, thanks to our users and Weblate. + +--- + +Jun 1, 2024 [Children's Safety Requires End-to-End Encryption](./20240601-children-safety-requires-e2e-encryption.md) + +As lawmakers grapple with the serious issue of child exploitation online, some proposed solutions would fuel the very problem they aim to solve. + +--- + +May 16, 2024 [SimpleX: Redefining Privacy by Making Hard Choices](./20240516-simplex-redefining-privacy-hard-choices.md) + +When it comes to open source privacy tools, the status quo often dictates the limitations of existing protocols and structures. However, these norms need to be challenged to radically shift how we approach genuinely private communication. This requires doing some uncomfortable things, like making hard choices as it relates to funding, alternative decentralization models, doubling down on privacy over convenience, and more. + +In this post we explain a bit more about why SimpleX operates and makes decisions the way it does: + +- No user accounts. +- Privacy over convenience. +- Network decentralization. +- Funding and profitability. +- Company jurisdiction. + +--- + Apr 26, 2024 [SimpleX network: legally binding transparency, v5.7 released with better calls and messages](./20240426-simplex-legally-binding-transparency-v5-7-better-user-experience.md) We published Transparency Reports, Security Policy, and Frequently Asked Questions, and updated Privacy Policy. diff --git a/blog/images/20240314-comparison.jpg b/blog/images/20240314-comparison.jpg index 579b2fd73c..5ff22be005 100644 Binary files a/blog/images/20240314-comparison.jpg and b/blog/images/20240314-comparison.jpg differ diff --git a/blog/images/20240516-parliament.jpg b/blog/images/20240516-parliament.jpg new file mode 100644 index 0000000000..53e8013249 Binary files /dev/null and b/blog/images/20240516-parliament.jpg differ diff --git a/blog/images/20240601-eu-privacy.png b/blog/images/20240601-eu-privacy.png new file mode 100644 index 0000000000..4ae1a17e30 Binary files /dev/null and b/blog/images/20240601-eu-privacy.png differ diff --git a/blog/images/20240604-routing.png b/blog/images/20240604-routing.png new file mode 100644 index 0000000000..8fb0c821c4 Binary files /dev/null and b/blog/images/20240604-routing.png differ diff --git a/blog/images/20240604-server.png b/blog/images/20240604-server.png new file mode 100644 index 0000000000..4ab610f3b9 Binary files /dev/null and b/blog/images/20240604-server.png differ diff --git a/blog/images/20240604-theme1.png b/blog/images/20240604-theme1.png new file mode 100644 index 0000000000..e9a1422a71 Binary files /dev/null and b/blog/images/20240604-theme1.png differ diff --git a/blog/images/20240604-theme2.png b/blog/images/20240604-theme2.png new file mode 100644 index 0000000000..e7972f6e05 Binary files /dev/null and b/blog/images/20240604-theme2.png differ diff --git a/blog/images/20240704-privacy.jpg b/blog/images/20240704-privacy.jpg new file mode 100644 index 0000000000..bfcbf11d3f Binary files /dev/null and b/blog/images/20240704-privacy.jpg differ diff --git a/blog/images/20240814-delete-contact-1.png b/blog/images/20240814-delete-contact-1.png new file mode 100644 index 0000000000..7f35664502 Binary files /dev/null and b/blog/images/20240814-delete-contact-1.png differ diff --git a/blog/images/20240814-delete-contact-2.png b/blog/images/20240814-delete-contact-2.png new file mode 100644 index 0000000000..766ddd677b Binary files /dev/null and b/blog/images/20240814-delete-contact-2.png differ diff --git a/blog/images/20240814-delete-messages.png b/blog/images/20240814-delete-messages.png new file mode 100644 index 0000000000..b2c65f28ad Binary files /dev/null and b/blog/images/20240814-delete-messages.png differ diff --git a/blog/images/20240814-new-message.png b/blog/images/20240814-new-message.png new file mode 100644 index 0000000000..69930499e7 Binary files /dev/null and b/blog/images/20240814-new-message.png differ diff --git a/blog/images/20240814-play.png b/blog/images/20240814-play.png new file mode 100644 index 0000000000..3e719df642 Binary files /dev/null and b/blog/images/20240814-play.png differ diff --git a/blog/images/20240814-reachable.png b/blog/images/20240814-reachable.png new file mode 100644 index 0000000000..dde3c747f9 Binary files /dev/null and b/blog/images/20240814-reachable.png differ diff --git a/blog/images/20240814-share.png b/blog/images/20240814-share.png new file mode 100644 index 0000000000..b486fe2356 Binary files /dev/null and b/blog/images/20240814-share.png differ diff --git a/blog/images/simplex-explained.png b/blog/images/simplex-explained.png new file mode 100644 index 0000000000..bf5c738d85 Binary files /dev/null and b/blog/images/simplex-explained.png differ diff --git a/blog/images/simplex-explained.svg b/blog/images/simplex-explained.svg new file mode 100644 index 0000000000..ac57f491ef --- /dev/null +++ b/blog/images/simplex-explained.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/blog/lang/fr-fr/README_fr.md b/blog/lang/fr-fr/README_fr.md index bebc42d7bc..471f3ce046 100644 --- a/blog/lang/fr-fr/README_fr.md +++ b/blog/lang/fr-fr/README_fr.md @@ -1,6 +1,6 @@ # Blog -4 févr. 2023 [SimpleX Chat v4.5 publié](./20230103-simplex-chat-v4.4-disappearing-messages.md) +4 févr. 2023 [SimpleX Chat v4.5 publié](../../20230103-simplex-chat-v4.4-disappearing-messages.md) - profils de chat multiples. - brouillon de message. @@ -10,7 +10,7 @@ Nous avons également ajouté [l'interface en italien](#french-language-interface), grâce à nos utilisateurs et à Weblate ! -3 janv. 2023 [SimpleX Chat v4.4 publié](./20230103-simplex-chat-v4.4-disappearing-messages.md) +3 janv. 2023 [SimpleX Chat v4.4 publié](../../20230103-simplex-chat-v4.4-disappearing-messages.md) - messages éphèméres. - messages "en direct" (dynamique). @@ -19,7 +19,7 @@ Nous avons également ajouté [l'interface en italien](#french-language-interfac Nous avons également ajouté [l'interface en français](#french-language-interface), grâce à nos utilisateurs et à Weblate ! -6 déc. 2022 [SimpleX Chat : révision et sortie de la v4.3](./20221206-simplex-chat-v4.3-voice-messages.md) +6 déc. 2022 [SimpleX Chat : révision et sortie de la v4.3](../../20221206-simplex-chat-v4.3-voice-messages.md) Critiques de novembre : @@ -35,7 +35,7 @@ Sortie de la v4.3 : - amélioration de la configuration du serveur SMP et du support des mots de passe du serveur - améliorations de la confidentialité et de la sécurité : protection de l'écran de l'application, sécurité des liens SimpleX, etc. -8 nov. 2022 [Audit de sécurité par Trail of Bits, nouveau site web et sortie de la v4.2](./20221108-simplex-chat-v4.2-security-audit-new-website.md) +8 nov. 2022 [Audit de sécurité par Trail of Bits, nouveau site web et sortie de la v4.2](../../20221108-simplex-chat-v4.2-security-audit-new-website.md) _"Avez-vous été audité ou devons-nous simplement vous ignorer ?"_ @@ -51,7 +51,7 @@ Sortie de la v4.2 : - changer manuellement de contact ou de membre vers une autre adresse / serveur (BETA) - recevoir des fichiers plus rapidement (BETA) -28 sept. 2022 [v4 : chiffrement de la base de données locale](./20220928-simplex-chat-v4-encrypted-database.md) +28 sept. 2022 [v4 : chiffrement de la base de données locale](../../20220928-simplex-chat-v4-encrypted-database.md) - base de données locale de chat chiffrée - si vous utilisez déjà l'application, vous pouvez chiffrer la base de données dans les paramètres de l'application. - support pour les serveurs WebRTC ICE auto-hébergés @@ -61,7 +61,7 @@ Sortie de la v4.2 : - support des images animées dans l'application Android - Interface utilisateur en allemand pour les applications mobiles -1 sept. 2022 [v3.2 : Mode Incognito](./20220901-simplex-chat-v3.2-incognito-mode.md) +1 sept. 2022 [v3.2 : Mode Incognito](../../20220901-simplex-chat-v3.2-incognito-mode.md) - Mode Incognito - utiliser un nouveau nom de profil aléatoire pour chaque contact - utiliser des adresses de serveur .onion avec Tor @@ -71,7 +71,7 @@ Sortie de la v4.2 : L'audit d'implémentation est prévu pour Octobre ! -8 août 2022 [v3.1 : groupes de discussion](./20220808-simplex-chat-v3.1-chat-groups.md) +8 août 2022 [v3.1 : groupes de discussion](../../20220808-simplex-chat-v3.1-chat-groups.md) - enfin, des groupes de chat secrets - personne d'autre que les membres ne sait qu'ils existent ! - accès aux serveurs de messagerie via Tor sur toutes les plateformes @@ -79,37 +79,37 @@ L'audit d'implémentation est prévu pour Octobre ! - protocole de chat publié - nouvelles icônes d'application -23 juil. 2022 [v3.1-beta : accès aux serveurs via Tor](./20220723-simplex-chat-v3.1-tor-groups-efficiency.md) +23 juil. 2022 [v3.1-beta : accès aux serveurs via Tor](../../20220723-simplex-chat-v3.1-tor-groups-efficiency.md) - application terminale : accès aux serveurs de messagerie via un proxy SOCKS5 (par exemple, Tor). - applications mobiles : rejoindre et quitter des groupes de discussion. - utilisation optimisée de la batterie et du trafic - réduction jusqu'à 90x ! - deux configurations docker pour les serveurs SMP auto-hébergés. -11 juil. 2022 [v3 : notifications push instantanées pour iOS et appels audio/vidéo](./20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md) : +11 juil. 2022 [v3 : notifications push instantanées pour iOS et appels audio/vidéo](../../20220711-simplex-chat-v3-released-ios-notifications-audio-video-calls-database-export-import-protocol-improvements.md) : - exportation et importation de la base de données de chat - appels audio/vidéo chiffrés de bout en bout - amélioration de la confidentialité du protocole et des performances -4 juin 2022 [v2.2 : nouveaux paramètres de confidentialité et de sécurité](./20220604-simplex-chat-new-privacy-security-settings.md) +4 juin 2022 [v2.2 : nouveaux paramètres de confidentialité et de sécurité](../../20220604-simplex-chat-new-privacy-security-settings.md) -24 mai 2022 [v2.1 : effacement des messages pour une meilleure confidentialité des conversations](./20220524-simplex-chat-better-privacy.md) +24 mai 2022 [v2.1 : effacement des messages pour une meilleure confidentialité des conversations](../../20220524-simplex-chat-better-privacy.md) -11 mai 2022 [Publication de la v2.0 - envoi d'images et de fichiers dans les applications mobiles](./20220511-simplex-chat-v2-images-files.md) +11 mai 2022 [Publication de la v2.0 - envoi d'images et de fichiers dans les applications mobiles](../../20220511-simplex-chat-v2-images-files.md) -04 avr. 2022 [Notifications instantanées pour les applications mobiles SimpleX Chat](./20220404-simplex-chat-instant-notifications.md) +04 avr. 2022 [Notifications instantanées pour les applications mobiles SimpleX Chat](../../20220404-simplex-chat-instant-notifications.md) -08 mars 2022 [Applications mobiles pour iOS et Android](./20220308-simplex-chat-mobile-apps.md) +08 mars 2022 [Applications mobiles pour iOS et Android](../../20220308-simplex-chat-mobile-apps.md) -14 févr. 2022. [SimpleX Chat : rejoignez notre version bêta publique pour iOS](./20220214-simplex-chat-ios-public-beta.md) +14 févr. 2022. [SimpleX Chat : rejoignez notre version bêta publique pour iOS](../../20220214-simplex-chat-ios-public-beta.md) -12 janv. 2022. [SimpleX Chat v1 : la plateforme de chat et d'application la plus privée et la plus sécurisée](./20220112-simplex-chat-v1-released.md) +12 janv. 2022. [SimpleX Chat v1 : la plateforme de chat et d'application la plus privée et la plus sécurisée](../../20220112-simplex-chat-v1-released.md) -08 déc. 2021. [Sortie de SimpleX Chat v0.5 : la première plateforme de chat 100% privée par définition - aucun accès à votre graphe de connexions](./20211208-simplex-chat-v0.5-released.md) +08 déc. 2021. [Sortie de SimpleX Chat v0.5 : la première plateforme de chat 100% privée par définition - aucun accès à votre graphe de connexions](../../20211208-simplex-chat-v0.5-released.md) -14 septembre 2021. [SimpleX Chat v0.4 publié : chat open-source qui utilise un protocole de routage de messages préservant la confidentialité](./20210914-simplex-chat-v0.4-released.md) +14 septembre 2021. [SimpleX Chat v0.4 publié : chat open-source qui utilise un protocole de routage de messages préservant la confidentialité](../../20210914-simplex-chat-v0.4-released.md) -12 mai 2021. [Prototype de chat SimpleX](./20210512-simplex-chat-terminal-ui.md) +12 mai 2021. [Prototype de chat SimpleX](../../20210512-simplex-chat-terminal-ui.md) -22 oct. 2020. [SimpleX Chat](./20201022-simplex-chat.md) +22 oct. 2020. [SimpleX Chat](../../20201022-simplex-chat.md) diff --git a/cabal.project b/cabal.project index 9e8e0e1dac..9bf39c2841 100644 --- a/cabal.project +++ b/cabal.project @@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd source-repository-package type: git location: https://github.com/simplex-chat/simplexmq.git - tag: 0b5ab3a374ff0ed9719a77fdda03a296e49eb78b + tag: 56986f82c89b04beae84a61208db8b55eb0098e3 source-repository-package type: git diff --git a/docs/CLI.md b/docs/CLI.md index d4f799c7af..abc09b0e7c 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -98,7 +98,7 @@ git checkout stable DOCKER_BUILDKIT=1 docker build --output ~/.local/bin . ``` -> **Please note:** If you encounter `` version `GLIBC_2.28' not found `` error, rebuild it with `haskell:8.10.7-stretch` base image (change it in your local [Dockerfile](Dockerfile)). +> **Please note:** If you encounter `` version `GLIBC_2.28' not found `` error, rebuild it with `haskell:8.10.7-stretch` base image (change it in your local [Dockerfile](/Dockerfile)). #### In any OS diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index bc013cd7eb..493496fd3d 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -34,48 +34,49 @@ You will have to add `/opt/homebrew/opt/openssl@1.1/bin` to your PATH in order t - `stable-android` - used to build stable Android core library with Nix (GHC 8.10.7) - only for Android armv7a. -- `stable-ios` - used to build stable iOS core library with Nix (GHC 8.10.7) – this branch should be the same as `stable-android` except Nix configuration files. Deprecated. - -- `master` - branch for beta version releases (GHC 9.6.3). - -- `master-ghc8107` - branch for beta version releases (GHC 8.10.7). Deprecated. +- `master` - branch for beta version releases (compatible with both GHC 9.6.3 and 8.10.7). - `master-android` - used to build beta Android core library with Nix (GHC 8.10.7) - only for Android armv7a. -- `master-ios` - used to build beta iOS core library with Nix (GHC 8.10.7). Deprecated. - -- `windows-ghc8107` - branch for windows core library build (GHC 8.10.7). Deprecated? - -`master-ios` and `windows-ghc8107` branches should be the same as `master-ghc8107` except Nix configuration files. - **In simplexmq repo** -- `master` - uses GHC 9.6.3 its commit should be used in `master` branch of simplex-chat repo. - -- `master-ghc8107` - its commit should be used in `master-android` (and `master-ios`) branch of simplex-chat repo. Deprecated. +- `master` - compatible with both GHC 9.6.3 and 8.10.7. ## Development & release process 1. Make PRs to `master` branch _only_ for both simplex-chat and simplexmq repos. -2. If simplexmq repo was changed, to build mobile core libraries you need to merge its `master` branch into `master-ghc8107` branch. - -3. To build core libraries for Android, iOS and windows: +2. To build core libraries for Android, iOS and windows: - merge `master` branch to `master-android` branch. -- update code to be compatible with GHC 8.10.7 (see below). - push to GitHub. -4. All libraries should be built from `master` branch, Android armv7a - from `master-android` branch. +3. All libraries should be built from `master` branch, Android armv7a - from `master-android` branch. -5. To build Desktop and CLI apps, make tag in `master` branch, APK files should be attached to the release. +4. To build Desktop and CLI apps, make tag in `master` branch, APK files should be attached to the release. -6. After the public release to App Store and Play Store, merge: +5. After the public release to App Store and Play Store, merge: - `master` to `stable` - `master` to `master-android` (and compile/update code) - `master-android` to `stable-android` -7. Independently, `master` branch of simplexmq repo should be merged to `stable` branch on stable releases. +6. Independently, `master` branch of simplexmq repo should be merged to `stable` branch on stable releases. +## Branches and PRs + +Use change scope (or comma separated scopes) as the first word in the PR names, followed by the colon. Commit name itself should be lowercase, in present tense. + +The PR names in simplex-chat repo are used in release notes, they should describe the solved problem and not the change. Possible PR scopes: +- ios +- android +- desktop +- core +- docs +- website +- ci + +We squash PRs, do not rewrite branch history after the review. + +For some complex features we create feature branches that will be merged once ready - do not make commits directly to them, make PRs to feature branches. ## Differences between GHC 8.10.7 and GHC 9.6.3 diff --git a/docs/DOWNLOADS.md b/docs/DOWNLOADS.md index 0432b0f92e..06850e76d2 100644 --- a/docs/DOWNLOADS.md +++ b/docs/DOWNLOADS.md @@ -1,13 +1,13 @@ --- title: Download SimpleX apps permalink: /downloads/index.html -revision: 11.02.2024 +revision: 03.07.2024 --- -| Updated 23.03.2024 | Languages: EN | +| Updated 03.07.2024 | Languages: EN | # Download SimpleX apps -The latest stable version is v5.6. +The latest stable version is v5.8. You can get the latest beta releases from [GitHub](https://github.com/simplex-chat/simplex-chat/releases). @@ -21,7 +21,7 @@ You can get the latest beta releases from [GitHub](https://github.com/simplex-ch You can link your mobile device with desktop to use the same profile remotely, but this is only possible when both devices are connected to the same local network. -**Linux**: [AppImage](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-x86_64.AppImage) (most Linux distros), [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-ubuntu-20_04-x86_64.deb) (and Debian-based distros), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-ubuntu-22_04-x86_64.deb). +**Linux**: [AppImage](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-x86_64.AppImage) or [Flatpak](https://flathub.org/apps/chat.simplex.simplex) (most Linux distros), [Ubuntu 20.04](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-ubuntu-20_04-x86_64.deb) (and Debian-based distros), [Ubuntu 22.04](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-ubuntu-22_04-x86_64.deb). **Mac**: [aarch64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-macos-aarch64.dmg) (Apple Silicon), [x86_64](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex-desktop-macos-x86_64.dmg) (Intel). diff --git a/docs/JOIN_TEAM.md b/docs/JOIN_TEAM.md index 3379f9ae04..26502a05af 100644 --- a/docs/JOIN_TEAM.md +++ b/docs/JOIN_TEAM.md @@ -15,29 +15,32 @@ We want to add up to 3 people to the team. ## Who we are looking for +### Product/UI designer + +You will be designing the user experience and the interface of both the app and the website in collaboration with the team. + +The current focus of the app is privacy and security, but we hope to have the design that would support the feeling of psychological safety, enabling people to achieve the results in the smallest amount of time. + +You are an experienced and innovative product designer with: +- 8+ years of user experience and visual design. +- Expertise in typography and high sensitivity to colors. +- Exceptional precision and attention to details. +- Strong opinions (weakly held). +- A strong empathy. + ### Application Haskell engineer +You will work with the Haskell core of the client applications and with the network servers. + You are an expert in language models, databases and Haskell: - expert knowledge of SQL. -- exception handling, concurrency, STM. -- type systems - we use ad hoc dependent types a lot. -- experience integrating open-source language models. -- experience developing community-centric applications. -- interested to build the next generation of messaging network. - -You will be focussed mostly on our client applications, and will also contribute to the servers also written in Haskell. - -### iOS / Mac engineer - -You are an expert in Apple platforms, including: -- iOS and Mac platform architecture. -- Swift and Objective-C. -- SwiftUI and UIKit. -- extensions, including notification service extension and sharing extension. -- low level inter-process communication primitives for concurrency. -- interested about creating the next generation of UX for a communication/social network. - -Knowledge of Android and Kotlin Multiplatform would be a bonus - we use Kotlin Jetpack Compose for our Android and desktop apps. +- Haskell exception handling, concurrency, STM, type systems. +- 8y+ of software engineering experience in complex projects, +- deep understanding of the common programming principles: + - data structures, bits and bytes, text encoding. + - software design and algorithms. + - concurrency. + - networking. ## About you @@ -53,20 +56,10 @@ Knowledge of Android and Kotlin Multiplatform would be a bonus - we use Kotlin J - focus on solving only today's problems and resist engineering for the future (aka over-engineering) – see [The Duct Tape Programmer](https://www.joelonsoftware.com/2009/09/23/the-duct-tape-programmer/) and [Why I Hate Frameworks](https://medium.com/@johnfliu/why-i-hate-frameworks-6af8cbadba42). - do not suffer from "not invented here" syndrome, at the same time interested to design and implement protocols and systems from the ground up when appropriate. -- **Love software engineering**: - - have 5y+ of software engineering experience in complex projects, - - great understanding of the common principles: - - data structures, bits and byte manipulation - - text encoding and manipulation - - software design and algorithms - - concurrency - - networking - - **Want to join a very early stage startup**: - high pace and intensity, longer hours. - a substantial part of the compensation is stock options. - - full transparency – we believe that too much [autonomy](https://twitter.com/KentBeck/status/851459129830850561) hurts learning and slows down progress. - + - full transparency - we believe that too much [autonomy](https://twitter.com/KentBeck/status/851459129830850561) hurts learning and slows down progress. ## How to join the team @@ -75,3 +68,5 @@ Knowledge of Android and Kotlin Multiplatform would be a bonus - we use Kotlin J 2. Also look through [GitHub issues](https://github.com/simplex-chat/simplex-chat/issues) submitted by the users to see what would you want to contribute as a test. 3. [Connect to us](https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FKBCmxJ3-lEjpWLPPkI6OWPk-YJneU5uY%23%2F%3Fv%3D1%26dh%3DMCowBQYDK2VuAyEAtixHJWDXvYWcoe-77vIfjvI6XWEuzUsapMS9nVHP_Go%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion) via SimpleX Chat to chat about what you want to contribute and about joining the team. + +4. You can also email [jobs@simplex.chat](mailto:jobs@simplex.chat?subject=Join%20SimpleX%20Chat%20team) diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 77f588ec26..0885e31725 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -8,7 +8,7 @@ revision: 23.04.2024 While great care is taken to ensure the highest level of security and privacy in SimpleX network servers and clients, all software can have flaws, and we believe it is a critical part of an organization's social responsibility to minimize the impact of these flaws through continual vulnerability discovery efforts, defense in depth design, and prompt remediation and notification. -The security assessment of SimpleX cryptography and networking was done by Trail of Bits in [November 2022](https://simplex.chat/blog/20221108-simplex-chat-v4.2-security-audit-new-website.html). +The security assessment of SimpleX cryptography and networking was done by Trail of Bits in [November 2022](../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). We are planning design review of SimpleX protocols in July 2024 and implementation review in December 2024/January 2025. diff --git a/docs/SERVER.md b/docs/SERVER.md index c29a805452..c2cb486375 100644 --- a/docs/SERVER.md +++ b/docs/SERVER.md @@ -1,9 +1,33 @@ --- title: Hosting your own SMP Server -revision: 31.07.2023 +revision: 03.06.2024 --- -| Updated 05.06.2023 | Languages: EN, [FR](/docs/lang/fr/SERVER.md), [CZ](/docs/lang/cs/SERVER.md), [PL](/docs/lang/pl/SERVER.md) | +| Updated 28.05.2024 | Languages: EN, [FR](/docs/lang/fr/SERVER.md), [CZ](/docs/lang/cs/SERVER.md), [PL](/docs/lang/pl/SERVER.md) | + +### Table of Contents + +- [Hosting your own SMP server](#hosting-your-own-smp-server) + - [Overview](#overview) + - [Installation](#installation) + - [Configuration](#configuration) + - [Interactively](#interactively) + - [Via command line options](#via-command-line-options) + - [Further configuration](#further-configuration) + - [Server security](#server-security) + - [Initialization](#initialization) + - [Private keys](#private-keys) + - [Online certificate rotation](#online-certificate-rotation) + - [Tor: installation and configuration](#tor-installation-and-configuration) + - [Installation for onion address](#installation-for-onion-address) + - [SOCKS port for SMP PROXY](#socks-port-for-smp-proxy) + - [Server information page](#server-information-page) + - [Documentation](#documentation) + - [SMP server address](#smp-server-address) + - [Systemd commands](#systemd-commands) + - [Monitoring](#monitoring) + - [Updating your SMP server](#updating-your-smp-server) + - [Configuring the app to use the server](#configuring-the-app-to-use-the-server) # Hosting your own SMP Server @@ -13,7 +37,7 @@ SMP server is the relay server used to pass messages in SimpleX network. SimpleX SimpleX clients only determine which server is used to receive the messages, separately for each contact (or group connection with a group member), and these servers are only temporary, as the delivery address can change. -_Please note_: when you change the servers in the app configuration, it only affects which server will be used for the new contacts, the existing contacts will not automatically move to the new servers, but you can move them manually using ["Change receiving address"](../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md#change-your-delivery-address-beta) button in contact/member information pages – it will be automated soon. +_Please note_: when you change the servers in the app configuration, it only affects which servers will be used for the new contacts, the existing contacts will not automatically move to the new servers, but you can move them manually using ["Change receiving address"](../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md#change-your-delivery-address-beta) button in contact/member information pages – it will be automated in the future. ## Installation @@ -22,7 +46,7 @@ _Please note_: when you change the servers in the app configuration, it only aff - Manual deployment (see below) - Semi-automatic deployment: - - [Offical installation script](https://github.com/simplex-chat/simplexmq#using-installation-script) + - [Installation script](https://github.com/simplex-chat/simplexmq#using-installation-script) - [Docker container](https://github.com/simplex-chat/simplexmq#using-docker) - [Linode Marketplace](https://www.linode.com/marketplace/apps/simplex-chat/simplex-chat/) @@ -30,7 +54,7 @@ Manual installation requires some preliminary actions: 1. Install binary: - - Using offical binaries: + - Using pre-compiled binaries: ```sh curl -L https://github.com/simplex-chat/simplexmq/releases/latest/download/smp-server-ubuntu-20_04-x86-64 -o /usr/local/bin/smp-server && chmod +x /usr/local/bin/smp-server @@ -85,86 +109,6 @@ Manual installation requires some preliminary actions: And execute `sudo systemctl daemon-reload`. -## Tor installation - -smp-server can also be deployed to serve from [tor](https://www.torproject.org) network. Run the following commands as `root` user. - -1. Install tor: - - We're assuming you're using Ubuntu/Debian based distributions. If not, please refer to [offical tor documentation](https://community.torproject.org/onion-services/setup/install/) or your distribution guide. - - - Configure offical Tor PPA repository: - - ```sh - CODENAME="$(lsb_release -c | awk '{print $2}')" - echo "deb [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org ${CODENAME} main - deb-src [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org ${CODENAME} main" > /etc/apt/sources.list.d/tor.list - ``` - - - Import repository key: - - ```sh - curl --proto '=https' --tlsv1.2 -sSf https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --dearmor | tee /usr/share/keyrings/tor-archive-keyring.gpg >/dev/null - ``` - - - Update repository index: - - ```sh - apt update - ``` - - - Install `tor` package: - - ```sh - apt install -y tor deb.torproject.org-keyring - ``` - -2. Configure tor: - - - File configuration: - - Open tor configuration with your editor of choice (`nano`,`vim`,`emacs`,etc.): - - ```sh - vim /etc/tor/torrc - ``` - - And insert the following lines to the bottom of configuration. Please note lines starting with `#`: this is comments about each individual options. - - ```sh - # Enable log (otherwise, tor doesn't seemd to deploy onion address) - Log notice file /var/log/tor/notices.log - # Enable single hop routing (2 options below are dependencies of third). Will reduce latency in exchange of anonimity (since tor runs alongside smp-server and onion address will be displayed in clients, this is totally fine) - SOCKSPort 0 - HiddenServiceNonAnonymousMode 1 - HiddenServiceSingleHopMode 1 - # smp-server hidden service host directory and port mappings - HiddenServiceDir /var/lib/tor/simplex-smp/ - HiddenServicePort 5223 localhost:5223 - ``` - - - Create directories: - - ```sh - mkdir /var/lib/tor/simplex-smp/ && chown debian-tor:debian-tor /var/lib/tor/simplex-smp/ && chmod 700 /var/lib/tor/simplex-smp/ - ``` - -3. Start tor: - - Enable `systemd` service and start tor. Offical `tor` is a bit flunky on the first start and may not create onion host address, so we're restarting it just in case. - - ```sh - systemctl enable tor && systemctl start tor && systemctl restart tor - ``` - -4. Display onion host: - - Execute the following command to display your onion host address: - - ```sh - cat /var/lib/tor/simplex-smp/hostname - ``` - ## Configuration To see which options are available, execute `smp-server` without flags: @@ -205,11 +149,11 @@ There are several options to consider: Enter `y` to enable logging statistics in CSV format, e.g. they can be used to show aggregate usage charts in `Grafana`. -These statistics include daily counts of created, secured and deleted queues, sent and received messages, and also daily, weekly, and monthly counts of active queues (that is, the queues that were used for any messages). We believe that this information does not include anything that would allow correlating different queues as belonging to the same users, but please let us know, confidentially, if you believe that this can be exploited in any way. +These statistics include daily counts of created, secured and deleted queues, sent and received messages, and also daily, weekly, and monthly counts of active queues (that is, the queues that were used for any messages). We believe that this information does not include anything that would allow correlating different queues as belonging to the same users, but please [let us know](./SECURITY.md), confidentially, if you believe that this can be exploited in any way. - `Require a password to create new messaging queues?` - Enter `r` or your arbitrary password to password-protect `smp-server`, or `n` to disable password protection. + Press `Enter` or enter your arbitrary password to password-protect `smp-server`, or `n` to disable password protection. - `Enter server FQDN or IP address for certificate (127.0.0.1):` @@ -277,7 +221,485 @@ Fingerprint: d5fcsc7hhtPpexYUbI2XPxDbyU2d3WsVmROimcL90ss= Server address: smp://d5fcsc7hhtPpexYUbI2XPxDbyU2d3WsVmROimcL90ss=:V8ONoJ6ICwnrZnTC_QuSHfCEYq53uLaJKQ_oIC6-ve8=@ ``` -The server address above should be used in your client configuration and if you added server password it should only be shared with the other people when you want to allow them to use your server to receive the messages (all your contacts will be able to send messages, as it does not require a password). If you passed IP address or hostnames during the initialisation, they will be printed as part of server address, otherwise replace `` with the actual server addresses. +The server address above should be used in your client configuration, and if you added server password it should only be shared with the other people who you want to allow using your server to receive the messages (all your contacts will be able to send messages - it does not require a password). If you passed IP address or hostnames during the initialisation, they will be printed as part of server address, otherwise replace `` with the actual server hostnames. + +## Further configuration + +All generated configuration, along with a description for each parameter, is available inside configuration file in `/etc/opt/simplex/smp-server.ini` for further customization. Depending on the smp-server version, the configuration file looks something like this: + +```ini +[INFORMATION] +# AGPLv3 license requires that you make any source code modifications +# available to the end users of the server. +# LICENSE: https://github.com/simplex-chat/simplexmq/blob/stable/LICENSE +# Include correct source code URI in case the server source code is modified in any way. +# If any other information fields are present, source code property also MUST be present. + +source_code: https://github.com/simplex-chat/simplexmq + +# Declaring all below information is optional, any of these fields can be omitted. + +# Server usage conditions and amendments. +# It is recommended to use standard conditions with any amendments in a separate document. +# usage_conditions: https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md +# condition_amendments: link + +# Server location and operator. +server_country: +operator: +operator_country: +website: + +# Administrative contacts. +#admin_simplex: SimpleX address +admin_email: +# admin_pgp: +# admin_pgp_fingerprint: + +# Contacts for complaints and feedback. +# complaints_simplex: SimpleX address +complaints_email: +# complaints_pgp: +# complaints_pgp_fingerprint: + +# Hosting provider. +hosting: +hosting_country: + +[STORE_LOG] +# The server uses STM memory for persistence, +# that will be lost on restart (e.g., as with redis). +# This option enables saving memory to append only log, +# and restoring it when the server is started. +# Log is compacted on start (deleted objects are removed). +enable: on + +# Undelivered messages are optionally saved and restored when the server restarts, +# they are preserved in the .bak file until the next restart. +restore_messages: on +expire_messages_days: 21 + +# Log daily server statistics to CSV file +log_stats: on + +[AUTH] +# Set new_queues option to off to completely prohibit creating new messaging queues. +# This can be useful when you want to decommission the server, but not all connections are switched yet. +new_queues: on + +# Use create_password option to enable basic auth to create new messaging queues. +# The password should be used as part of server address in client configuration: +# smp://fingerprint:password@host1,host2 +# The password will not be shared with the connecting contacts, you must share it only +# with the users who you want to allow creating messaging queues on your server. +# create_password: password to create new queues (any printable ASCII characters without whitespace, '@', ':' and '/') + +[TRANSPORT] +# host is only used to print server address on start +host: +port: 5223 +log_tls_errors: off +websockets: off +# control_port: 5224 + +[PROXY] +# Network configuration for SMP proxy client. +# `host_mode` can be 'public' (default) or 'onion'. +# It defines prefferred hostname for destination servers with multiple hostnames. +# host_mode: public +# required_host_mode: off + +# The domain suffixes of the relays you operate (space-separated) to count as separate proxy statistics. +# own_server_domains: + +# SOCKS proxy port for forwarding messages to destination servers. +# You may need a separate instance of SOCKS proxy for incoming single-hop requests. +# socks_proxy: localhost:9050 + +# `socks_mode` can be 'onion' for SOCKS proxy to be used for .onion destination hosts only (default) +# or 'always' to be used for all destination hosts (can be used if it is an .onion server). +# socks_mode: onion + +# Limit number of threads a client can spawn to process proxy commands in parrallel. +# client_concurrency: 32 + +[INACTIVE_CLIENTS] +# TTL and interval to check inactive clients +disconnect: off +# ttl: 43200 +# check_interval: 3600 + +[WEB] +# Set path to generate static mini-site for server information and qr codes/links +static_path: /var/opt/simplex/www + +# Run an embedded server on this port +# Onion sites can use any port and register it in the hidden service config. +# Running on a port 80 may require setting process capabilities. +# http: 8000 + +# You can run an embedded TLS web server too if you provide port and cert and key files. +# Not required for running relay on onion address. +# https: 443 +# cert: /etc/opt/simplex/web.cert +# key: /etc/opt/simplex/web.key +``` + +## Server security + +### Initialization + +Although it's convenient to initialize smp-server configuration directly on the server, operators **ARE ADVISED** to initialize smp-server fully offline to protect your SMP server CA private key. + +Follow the steps to quickly initialize the server offline: + +1. Install Docker on your system. + +2. Deploy [smp-server](https://github.com/simplex-chat/simplexmq#using-docker) locally. + +3. Destroy the container. All relevant configuration files and keys will be available at `$HOME/simplex/smp/config`. + +4. Move your `CA` private key (`ca.key`) to the safe place. For further explanation, see the next section: [Server security: Private keys](#private-keys). + +5. Copy all other configuration files **except** the CA key to the server: + + ```sh + rsync -hzasP $HOME/simplex/smp/config/ @:/etc/opt/simplex/ + ``` + +### Private keys + +Connection to the smp server occurs via a TLS connection. During the TLS handshake, the client verifies smp-server CA and server certificates by comparing its fingerprint with the one included in server address. If server TLS credential is compromised, this key can be used to sign a new one, keeping the same server identity and established connections. In order to protect your smp-server from bad actors, operators **ARE ADVISED** to move CA private key to a safe place. That could be: + +- [Tails](https://tails.net/) live usb drive with [persistent and encrypted storage](https://tails.net/doc/persistent_storage/create/index.en.html). +- Offline Linux laptop. +- Bitwarden. +- Any other safe storage that satisfy your security requirements. + +Follow the steps to secure your CA keys: + +1. Login to your server via SSH. + +2. Copy the CA key to a safe place from this file: + + ```sh + /etc/opt/simplex/ca.key + ``` + +3. Delete the CA key from the server. **Please make sure you've saved you CA key somewhere safe. Otherwise, you would lose the ability to [rotate the online certificate](#online-certificate-rotation)**: + + ```sh + rm /etc/opt/simplex/ca.key + ``` + +### Online certificate rotation + +Operators of smp servers **ARE ADVISED** to rotate online certificate regularly (e.g., every 3 months). In order to do this, follow the steps: + +1. Create relevant folders: + + ```sh + mkdir -p $HOME/simplex/smp/config + ``` + +1. Copy the configuration files from the server to the local machine (if not yet): + + ```sh + rsync -hzasP @:/etc/opt/simplex/ $HOME/simplex/smp/config/ + ``` + +2. **Copy** your CA private key from a safe place to the local machine and name it `ca.key`. + +3. Download latest `smp-server` binary [from Github releases](https://github.com/simplex-chat/simplexmq/releases): + + ```sh + curl -L 'https://github.com/simplex-chat/simplexmq/releases/latest/download/smp-server-ubuntu-20_04-x86-64' -o smp-server + ``` + +4. Put the `smp-server` binary to your `$PATH` and make it executable: + + ```sh + sudo mv smp-server /usr/local/bin/ && chmod +x /usr/local/bin/smp-server + ``` + +5. Export a variable to configure your path to smp-server configuration: + + ```sh + export SMP_SERVER_CFG_PATH=$HOME/simplex/smp/config + ``` + +6. Execute the following command: + + ```sh + smp-server cert + ``` + + This command should print: + + ```sh + Certificate request self-signature ok + subject=CN = + Generated new server credentials + ---------- + You should store CA private key securely and delete it from the server. + If server TLS credential is compromised this key can be used to sign a new one, keeping the same server identity and established connections. + CA private key location: + $HOME/simplex/smp/config/ca.key + ---------- + ``` + +7. Remove the CA key from the config folder (make sure you have a backup!): + + ```sh + rm $HOME/simplex/smp/config/ca.key + ``` + +8. Upload new certificates to the server: + + ```sh + rsync -hzasP $HOME/simplex/smp/config/ @:/etc/opt/simplex/ + ``` + +9. Connect to the server via SSH and restart the service: + + ```sh + ssh @ "systemctl restart smp-server" + ``` + +10. Done! + +## Tor: installation and configuration + +### Installation for onion address + +SMP-server can also be deployed to be available via [Tor](https://www.torproject.org) network. Run the following commands as `root` user. + +1. Install tor: + + We're assuming you're using Ubuntu/Debian based distributions. If not, please refer to [offical tor documentation](https://community.torproject.org/onion-services/setup/install/) or your distribution guide. + + - Configure offical Tor PPA repository: + + ```sh + CODENAME="$(lsb_release -c | awk '{print $2}')" + echo "deb [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org ${CODENAME} main + deb-src [signed-by=/usr/share/keyrings/tor-archive-keyring.gpg] https://deb.torproject.org/torproject.org ${CODENAME} main" > /etc/apt/sources.list.d/tor.list + ``` + + - Import repository key: + + ```sh + curl --proto '=https' --tlsv1.2 -sSf https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc | gpg --dearmor | tee /usr/share/keyrings/tor-archive-keyring.gpg >/dev/null + ``` + + - Update repository index: + + ```sh + apt update + ``` + + - Install `tor` package: + + ```sh + apt install -y tor deb.torproject.org-keyring + ``` + +2. Configure tor: + + - File configuration: + + Open tor configuration with your editor of choice (`nano`,`vim`,`emacs`,etc.): + + ```sh + vim /etc/tor/torrc + ``` + + And insert the following lines to the bottom of configuration. Please note lines starting with `#`: this is comments about each individual options. + + ```sh + # Enable log (otherwise, tor doesn't seem to deploy onion address) + Log notice file /var/log/tor/notices.log + # Enable single hop routing (2 options below are dependencies of the third) - It will reduce the latency at the cost of lower anonimity of the server - as SMP-server onion address is used in the clients together with public address, this is ok. If you deploy SMP-server with onion-only address, you may want to keep standard configuration instead. + SOCKSPort 0 + HiddenServiceNonAnonymousMode 1 + HiddenServiceSingleHopMode 1 + # smp-server hidden service host directory and port mappings + HiddenServiceDir /var/lib/tor/simplex-smp/ + HiddenServicePort 5223 localhost:5223 + ``` + + - Create directories: + + ```sh + mkdir /var/lib/tor/simplex-smp/ && chown debian-tor:debian-tor /var/lib/tor/simplex-smp/ && chmod 700 /var/lib/tor/simplex-smp/ + ``` + +3. Start tor: + + Enable `systemd` service and start tor. Offical `tor` is a bit flaky on the first start and may not create onion host address, so we're restarting it just in case. + + ```sh + systemctl enable --now tor && systemctl restart tor + ``` + +4. Display onion host: + + Execute the following command to display your onion host address: + + ```sh + cat /var/lib/tor/simplex-smp/hostname + ``` + +### SOCKS port for SMP PROXY + +SMP-server versions starting from `v5.8.0-beta.0` can be configured to PROXY smp servers available exclusively through [Tor](https://www.torproject.org) network to be accessible to the clients that do not use Tor. Run the following commands as `root` user. + +1. Install tor as described in the [previous section](#installation-for-onion-address). + +2. Execute the following command to creatae a new Tor daemon instance: + + ```sh + tor-instance-create tor2 + ``` + +3. Open the `tor2` configuration and replace its content with the following lines: + + ```sh + vim /etc/tor/instances/tor2/torrc + ``` + + ```sh + # Log tor to systemd daemon + Log notice syslog + # Listen to local 9050 port for socks proxy + SocksPort 9050 + ``` + +3. Enable service at startup and start the daemon: + + ```sh + systemctl enable --now tor@tor2 + ``` + + You can check `tor2` logs with the following command: + + ```sh + journalctl -u tor@tor2 + ``` + +4. After [server initialization](#configuration), configure the `PROXY` section like so: + + ```ini + ... + [PROXY] + socks_proxy: 127.0.0.1:9050 + own_server_domains: + ... + ``` + +## Server information page + +SMP-server versions starting from `v5.8.0` can be configured to serve Web page with server information that can include admin info, server info, provider info, etc. Run the following commands as `root` user. + +1. Add the following to your smp-server configuration (please modify fields in [INFORMATION] section to include relevant information): + + ```sh + vim /etc/opt/simplex/smp-server.ini + ``` + + ```ini + [WEB] + static_path: /var/opt/simplex/www + + [INFORMATION] + # AGPLv3 license requires that you make any source code modifications + # available to the end users of the server. + # LICENSE: https://github.com/simplex-chat/simplexmq/blob/stable/LICENSE + # Include correct source code URI in case the server source code is modified in any way. + # If any other information fields are present, source code property also MUST be present. + + source_code: https://github.com/simplex-chat/simplexmq + + # Declaring all below information is optional, any of these fields can be omitted. + + # Server usage conditions and amendments. + # It is recommended to use standard conditions with any amendments in a separate document. + # usage_conditions: https://github.com/simplex-chat/simplex-chat/blob/stable/PRIVACY.md + # condition_amendments: link + + # Server location and operator. + server_country: + operator: + operator_country: + website: + + # Administrative contacts. + #admin_simplex: SimpleX address + admin_email: + # admin_pgp: + # admin_pgp_fingerprint: + + # Contacts for complaints and feedback. + # complaints_simplex: SimpleX address + complaints_email: + # complaints_pgp: + # complaints_pgp_fingerprint: + + # Hosting provider. + hosting: + hosting_country: + ``` + +2. Install the webserver. For easy deployment we'll describe the installtion process of [Caddy](https://caddyserver.com) webserver on Ubuntu server: + + 1. Install the packages: + + ```sh + sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl + ``` + + 2. Install caddy gpg key for repository: + + ```sh + curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg + ``` + + 3. Install Caddy repository: + + ```sh + curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list + ``` + + 4. Install Caddy: + + ```sh + sudo apt update && sudo apt install caddy + ``` + + [Full Caddy instllation instructions](https://caddyserver.com/docs/install) + +3. Replace Caddy configuration with the following (don't forget to replace ``): + + ```sh + vim /etc/caddy/Caddyfile + ``` + + ```caddy + { + root * /var/opt/simplex/www + file_server + } + ``` + +4. Enable and start Caddy service: + + ```sh + systemctl enable --now caddy + ``` + +5. Upgrade your smp-server to latest version - [Updating your smp server](#updating-your-smp-server) + +6. Access the webpage you've deployed from your browser. You should see the smp-server information that you've provided in your ini file. ## Documentation @@ -371,26 +793,69 @@ You can enable `smp-server` statistics for `Grafana` dashboard by setting value Logs will be stored as `csv` file in `/var/opt/simplex/smp-server-stats.daily.log`. Fields for the `csv` file are: ```sh -fromTime,qCreated,qSecured,qDeleted,msgSent,msgRecv,dayMsgQueues,weekMsgQueues,monthMsgQueues +fromTime,qCreated,qSecured,qDeleted,msgSent,msgRecv,dayMsgQueues,weekMsgQueues,monthMsgQueues,msgSentNtf,msgRecvNtf,dayCountNtf,weekCountNtf,monthCountNtf,qCount,msgCount,msgExpired,qDeletedNew,qDeletedSecured,pRelays_pRequests,pRelays_pSuccesses,pRelays_pErrorsConnect,pRelays_pErrorsCompat,pRelays_pErrorsOther,pRelaysOwn_pRequests,pRelaysOwn_pSuccesses,pRelaysOwn_pErrorsConnect,pRelaysOwn_pErrorsCompat,pRelaysOwn_pErrorsOther,pMsgFwds_pRequests,pMsgFwds_pSuccesses,pMsgFwds_pErrorsConnect,pMsgFwds_pErrorsCompat,pMsgFwds_pErrorsOther,pMsgFwdsOwn_pRequests,pMsgFwdsOwn_pSuccesses,pMsgFwdsOwn_pErrorsConnect,pMsgFwdsOwn_pErrorsCompat,pMsgFwdsOwn_pErrorsOther,pMsgFwdsRecv,qSub,qSubAuth,qSubDuplicate,qSubProhibited,msgSentAuth,msgSentQuota,msgSentLarge ``` -- `fromTime` - timestamp; date and time of event - -- `qCreated` - int; created queues - -- `qSecured` - int; established queues - -- `qDeleted` - int; deleted queues - -- `msgSent` - int; sent messages - -- `msgRecv` - int; received messages - -- `dayMsgQueues` - int; active queues in a day - -- `weekMsgQueues` - int; active queues in a week - -- `monthMsgQueues` - int; active queues in a month +| Field number | Field name | Field Description | +| ------------- | ---------------------------- | -------------------------- | +| 1 | `fromTime` | Date of statistics | +| Messaging queue: | +| 2 | `qCreated` | Created | +| 3 | `qSecured` | Established | +| 4 | `qDeleted` | Deleted | +| Messages: | +| 5 | `msgSent` | Sent | +| 6 | `msgRecv` | Received | +| 7 | `dayMsgQueues` | Active queues in a day | +| 8 | `weekMsgQueues` | Active queues in a week | +| 9 | `monthMsgQueues` | Active queues in a month | +| Messages with "notification" flag | +| 10 | `msgSentNtf` | Sent | +| 11 | `msgRecvNtf` | Received | +| 12 | `dayCountNtf` | Active queues in a day | +| 13 | `weekCountNtf` | Active queues in a week | +| 14 | `monthCountNtf` | Active queues in a month | +| Additional statistics: | +| 15 | `qCount` | Stored queues | +| 16 | `msgCount` | Stored messages | +| 17 | `msgExpired` | Expired messages | +| 18 | `qDeletedNew` | New deleted queues | +| 19 | `qDeletedSecured` | Secured deleted queues | +| Requested sessions with all relays: | +| 20 | `pRelays_pRequests` | - requests | +| 21 | `pRelays_pSuccesses` | - successes | +| 22 | `pRelays_pErrorsConnect` | - connection errors | +| 23 | `pRelays_pErrorsCompat` | - compatability errors | +| 24 | `pRelays_pErrorsOther` | - other errors | +| Requested sessions with own relays: | +| 25 | `pRelaysOwn_pRequests` | - requests | +| 26 | `pRelaysOwn_pSuccesses` | - successes | +| 27 | `pRelaysOwn_pErrorsConnect` | - connection errors | +| 28 | `pRelaysOwn_pErrorsCompat` | - compatability errors | +| 29 | `pRelaysOwn_pErrorsOther` | - other errors | +| Message forwards to all relays: | +| 30 | `pMsgFwds_pRequests` | - requests | +| 31 | `pMsgFwds_pSuccesses` | - successes | +| 32 | `pMsgFwds_pErrorsConnect` | - connection errors | +| 33 | `pMsgFwds_pErrorsCompat` | - compatability errors | +| 34 | `pMsgFwds_pErrorsOther` | - other errors | +| Message forward to own relays: | +| 35 | `pMsgFwdsOwn_pRequests` | - requests | +| 36 | `pMsgFwdsOwn_pSuccesses` | - successes | +| 37 | `pMsgFwdsOwn_pErrorsConnect` | - connection errors | +| 38 | `pMsgFwdsOwn_pErrorsCompat` | - compatability errors | +| 39 | `pMsgFwdsOwn_pErrorsOther` | - other errors | +| Received message forwards: | +| 40 | `pMsgFwdsRecv` | | +| Message queue subscribtion errors: | +| 41 | `qSub` | All | +| 42 | `qSubAuth` | Authentication erorrs | +| 43 | `qSubDuplicate` | Duplicate SUB errors | +| 44 | `qSubProhibited` | Prohibited SUB errors | +| Message errors: | +| 45 | `msgSentAuth` | Authentication errors | +| 46 | `msgSentQuota` | Quota errors | +| 47 | `msgSentLarge` | Large message errors | To import `csv` to `Grafana` one should: @@ -417,7 +882,7 @@ To import `csv` to `Grafana` one should: For further documentation, see: [CSV Data Source for Grafana - Documentation](https://grafana.github.io/grafana-csv-datasource/) -# Updating your SMP server +## Updating your SMP server To update your smp-server to latest version, choose your installation method and follow the steps: @@ -474,7 +939,7 @@ To update your smp-server to latest version, choose your installation method and docker image prune ``` -### Configuring the app to use the server +## Configuring the app to use the server To configure the app to use your messaging server copy it's full address, including password, and add it to the app. You have an option to use your server together with preset servers or without them - you can remove or disable them. diff --git a/docs/TRANSPARENCY.md b/docs/TRANSPARENCY.md index 547660861b..43fdd12ac5 100644 --- a/docs/TRANSPARENCY.md +++ b/docs/TRANSPARENCY.md @@ -1,12 +1,12 @@ --- title: Transparency Reports permalink: /transparency/index.html -revision: 26.04.2024 +revision: 16.07.2024 --- # Transparency Reports -**Updated**: Apr 26, 2024 +**Updated**: Jul 16, 2024 SimpleX Chat Ltd. is a company registered in the UK – it develops communication software enabling users to operate and communicate via SimpleX network, without user profile identifiers of any kind, and without having their data hosted by any network infrastructure operators. diff --git a/docs/XFTP-SERVER.md b/docs/XFTP-SERVER.md index a0ad0e0cf7..a2eb9816e5 100644 --- a/docs/XFTP-SERVER.md +++ b/docs/XFTP-SERVER.md @@ -75,7 +75,7 @@ Manual installation requires some preliminary actions: Group=xftp Type=simple ExecStart=/usr/local/bin/xftp-server start +RTS -N -RTS - ExecStopPost=/usr/bin/env sh -c '[ -e "/var/opt/simplex-xftp/file-server-store.log" ] && cp "/var/opt/simplex-xftp/file-server-store.log" "/var/opt/simplex-xftp/file-server-store.log.$(date +'%FT%T')"' + ExecStopPost=/usr/bin/env sh -c '[ -e "/var/opt/simplex-xftp/file-server-store.log" ] && cp "/var/opt/simplex-xftp/file-server-store.log" "/var/opt/simplex-xftp/file-server-store.log.$(date +'%%FT%%T')"' LimitNOFILE=65535 KillSignal=SIGINT TimeoutStopSec=infinity diff --git a/docs/lang/cs/CLI.md b/docs/lang/cs/CLI.md index 9cbce8e6fe..338e48e57e 100644 --- a/docs/lang/cs/CLI.md +++ b/docs/lang/cs/CLI.md @@ -97,7 +97,7 @@ git checkout stable DOCKER_BUILDKIT=1 docker build --output ~/.local/bin . ``` -> **Upozornění:** Pokud narazíte na chybu `` verze `GLIBC_2.28' nenalezena ``, obnovte jej pomocí základního obrazu `haskell:8.10.7-stretch` (změňte jej ve svém lokálním [Dockerfile](Dockerfile)). +> **Upozornění:** Pokud narazíte na chybu `` verze `GLIBC_2.28' nenalezena ``, obnovte jej pomocí základního obrazu `haskell:8.10.7-stretch` (změňte jej ve svém lokálním [Dockerfile](/Dockerfile)). #### V libovolném operačním systému diff --git a/docs/lang/cs/README.md b/docs/lang/cs/README.md index f536cb1aa6..7eab61395e 100644 --- a/docs/lang/cs/README.md +++ b/docs/lang/cs/README.md @@ -26,7 +26,7 @@ - 🚀 [TestFlight preview for iOS](https://testflight.apple.com/join/DWuT2LQu) s novými funkcemi o 1-2 týdny dříve - **omezeno na 10 000 uživatelů**! - 🖥 K dispozici jako terminálová (konzolová) [aplikace / CLI](#zap-quick-installation-of-a-terminal-app) v systémech Linux, MacOS, Windows. -**NOVINKA**: Bezpečnostní audit od [Trail of Bits](https://www.trailofbits.com/about), [nové webové stránky](https://simplex.chat) a vydána verze 4.2! [Viz oznámení](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). +**NOVINKA**: Bezpečnostní audit od [Trail of Bits](https://www.trailofbits.com/about), [nové webové stránky](https://simplex.chat) a vydána verze 4.2! [Viz oznámení](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). ## Obsah @@ -62,23 +62,23 @@ Nestačí používat end-to-end šifrovaný messenger, všichni bychom měli pou ### Úplné soukromí vaší identity, profilu, kontaktů a metadat. -**Na rozdíl od všech ostatních existujících platforem pro zasílání zpráv nemá SimpleX přiřazeny žádné identifikátory uživatelů** - dokonce ani náhodná čísla. To chrání soukromí toho, s kým komunikujete, a skrývá to před servery platformy SimpleX i před jakýmikoli pozorovateli. [Více informací](./docs/SIMPLEX.md#full-privacy-of-your-identity-profile-contacts-and-metadata). +**Na rozdíl od všech ostatních existujících platforem pro zasílání zpráv nemá SimpleX přiřazeny žádné identifikátory uživatelů** - dokonce ani náhodná čísla. To chrání soukromí toho, s kým komunikujete, a skrývá to před servery platformy SimpleX i před jakýmikoli pozorovateli. [Více informací](./SIMPLEX.md#full-privacy-of-your-identity-profile-contacts-and-metadata). ### Nejlepší ochrana proti spamu a zneužití -Protože na platformě SimpleX nemáte žádný identifikátor, nelze vás kontaktovat, pokud nesdílíte odkaz na jednorázovou pozvánku nebo volitelnou dočasnou uživatelskou adresu. [Více informací](./docs/SIMPLEX.md#nejlepší-ochrana-před-spamem-a-zneužitím). +Protože na platformě SimpleX nemáte žádný identifikátor, nelze vás kontaktovat, pokud nesdílíte odkaz na jednorázovou pozvánku nebo volitelnou dočasnou uživatelskou adresu. [Více informací](./SIMPLEX.md#nejlepší-ochrana-proti-spamu-a-zneužití). ### Úplné vlastnictví, kontrola a zabezpečení vašich dat -SimpleX ukládá všechna uživatelská data na klientských zařízeních, zprávy jsou pouze dočasně uchovávány na relay serverech SimpleX, dokud nejsou přijaty. [Více informací](./docs/SIMPLEX.md#complete-ownership-control-and-security-of-your-data). +SimpleX ukládá všechna uživatelská data na klientských zařízeních, zprávy jsou pouze dočasně uchovávány na relay serverech SimpleX, dokud nejsou přijaty. [Více informací](./SIMPLEX.md#complete-ownership-control-and-security-of-your-data). ### Uživatelé vlastní síť SimpleX -Můžete používat SimpleX s vlastními servery a přitom komunikovat s lidmi, kteří používají servery předkonfigurované v aplikacích nebo jakékoli jiné servery SimpleX. [Více informací](./docs/SIMPLEX.md#users-own-simplex-network). +Můžete používat SimpleX s vlastními servery a přitom komunikovat s lidmi, kteří používají servery předkonfigurované v aplikacích nebo jakékoli jiné servery SimpleX. [Více informací](./SIMPLEX.md#users-own-simplex-network). ## Často kladené otázky -1. _Jak může SimpleX doručovat zprávy bez identifikátorů uživatelů?_ Viz [oznámení o vydání v2](./blog/20220511-simplex-chat-v2-images-files.md#prvni-platforma-zasilani-zpráv-bez-identifikátoru-uživatele), kde je vysvětleno, jak SimpleX funguje. +1. _Jak může SimpleX doručovat zprávy bez identifikátorů uživatelů?_ Viz [oznámení o vydání v2](../../../blog/20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers), kde je vysvětleno, jak SimpleX funguje. 2. _Proč bych neměl používat jen Signal?_ Signal je centralizovaná platforma, která k identifikaci svých uživatelů a jejich kontaktů používá telefonní čísla. To znamená, že zatímco obsah vašich zpráv na službě Signal je chráněn robustním šifrováním end-to-end, pro službu Signal je viditelné velké množství metadat - s kým a kdy hovoříte. @@ -88,17 +88,17 @@ Můžete používat SimpleX s vlastními servery a přitom komunikovat s lidmi, Poslední aktualizace: V současné době je k dispozici několik nových aplikací, např: -[Vydání verze 4.5 - s více uživatelskými profily, návrhem zpráv, izolací transportu a italským rozhraním](./blog/20230204-simplex-chat-v4-5-user-chat-profiles.md). +[Vydání verze 4.5 - s více uživatelskými profily, návrhem zpráv, izolací transportu a italským rozhraním](../../../blog/20230204-simplex-chat-v4-5-user-chat-profiles.md). -[03. 01. 2023. v4.4 vydána - s mizejícími zprávami, "živými" zprávami, bezpečnostním ověřováním spojení, GIFy a nálepkami a s francouzským jazykem rozhraní](./blog/20230103-simplex-chat-v4.4-disappearing-messages.md). +[03. 01. 2023. v4.4 vydána - s mizejícími zprávami, "živými" zprávami, bezpečnostním ověřováním spojení, GIFy a nálepkami a s francouzským jazykem rozhraní](../../../blog/20230103-simplex-chat-v4.4-disappearing-messages.md). -[prosinec 06, 2022. Listopadové recenze a vydána verze 4.3 - s okamžitými hlasovými zprávami, nevratným mazáním odeslaných zpráv a vylepšenou konfigurací serveru](./blog/20221206-simplex-chat-v4.3-hlasove-zpravy.md). +[prosinec 06, 2022. Listopadové recenze a vydána verze 4.3 - s okamžitými hlasovými zprávami, nevratným mazáním odeslaných zpráv a vylepšenou konfigurací serveru](../../../blog/20221206-simplex-chat-v4.3-voice-messages.md). -[Nov 08, 2022. Bezpečnostní audit Trail of Bits, vydány nové webové stránky a verze 4.2](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). +[Nov 08, 2022. Bezpečnostní audit Trail of Bits, vydány nové webové stránky a verze 4.2](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). -[28. 9. 2022. v4.0: šifrovaná lokální databáze chatu a mnoho dalších změn](./blog/20220928-simplex-chat-v4-encrypted-database.md). +[28. 9. 2022. v4.0: šifrovaná lokální databáze chatu a mnoho dalších změn](../../../blog/20220928-simplex-chat-v4-encrypted-database.md). -[Všechny aktualizace](./blog) +[Všechny aktualizace](../../../blog) ## Vytvoření soukromého připojení @@ -118,13 +118,13 @@ Po instalaci chatovacího klienta jednoduše spusťte `simplex-chat` z terminál ![simplex-chat](./images/connection.gif) -Více informací o [instalaci a používání terminálové aplikace](./docs/CLI.md). +Více informací o [instalaci a používání terminálové aplikace](./CLI.md). ## Návrh platformy SimpleX SimpleX je síť klient-server s unikátní topologií sítě, která využívá redundantní, jednorázové uzly pro předávání zpráv (relay nodes) k asynchronnímu předávání zpráv prostřednictvím jednosměrných (simplexních) front zpráv, což zajišťuje anonymitu příjemce i odesílatele. -Na rozdíl od sítí P2P jsou všechny zprávy předávány přes jeden nebo několik serverových uzlů, které ani nemusí mít perzistenci. Současná implementace [SMP serveru](https://github.com/simplex-chat/simplexmq#smp-server) ve skutečnosti používá ukládání zpráv v paměti a uchovává pouze záznamy o frontách. SimpleX poskytuje lepší ochranu metadat než návrhy P2P, protože k doručování zpráv se nepoužívají globální identifikátory účastníků, a vyhýbá se [problémům sítí P2P](./docs/SIMPLEX.md#comparison-with-p2p-messaging-protocols). +Na rozdíl od sítí P2P jsou všechny zprávy předávány přes jeden nebo několik serverových uzlů, které ani nemusí mít perzistenci. Současná implementace [SMP serveru](https://github.com/simplex-chat/simplexmq#smp-server) ve skutečnosti používá ukládání zpráv v paměti a uchovává pouze záznamy o frontách. SimpleX poskytuje lepší ochranu metadat než návrhy P2P, protože k doručování zpráv se nepoužívají globální identifikátory účastníků, a vyhýbá se [problémům sítí P2P](./SIMPLEX.md#comparison-with-p2p-messaging-protocols). Na rozdíl od federativních sítí nemají uzly serveru **záznamy o uživatelích**, **nekomunikují mezi sebou** a **neukládají zprávy** po jejich doručení příjemcům. Neexistuje způsob, jak zjistit úplný seznam serverů účastnících se sítě SimpleX. Tato konstrukce se vyhýbá problému viditelnosti metadat, který mají všechny federované sítě, a lépe chrání před útoky na celou síť. @@ -132,7 +132,7 @@ Informace o uživatelích, jejich kontaktech a skupinách mají pouze klientská Další informace o cílech a technickém návrhu platformy naleznete v dokumentu [SimpleX whitepaper](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md). -Formát zpráv zasílaných mezi klienty chatu prostřednictvím [SimpleX Messaging Protocol](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/simplex-messaging.md) viz [SimpleX Chat Protocol](./docs/protocol/simplex-chat.md). +Formát zpráv zasílaných mezi klienty chatu prostřednictvím [SimpleX Messaging Protocol](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/simplex-messaging.md) viz [SimpleX Chat Protocol](../../protocol/simplex-chat.md). ## Soukromí: technické podrobnosti a omezení @@ -149,7 +149,7 @@ Co je již implementováno: 6. Počínaje verzí v2 protokolu SMP (současná verze je v4) jsou všechna metadata zprávy včetně času, kdy byla zpráva přijata serverem (zaokrouhleno na sekundy), odesílána příjemcům uvnitř šifrované obálky, takže ani v případě kompromitace TLS je nelze pozorovat. 7. Pro spojení klient-server je povoleno pouze TLS 1.2/1.3, omezené na kryptografické algoritmy: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448. 8. Na ochranu proti útokům typu replay vyžadují servery SimpleX [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) jako ID relace v každém klientském příkazu podepsaném efemérním klíčem per-queue. -9. Pro ochranu vaší IP adresy podporují všichni klienti SimpleX Chat přístup k serverům pro zasílání zpráv přes Tor - více informací najdete v [oznámení o vydání v3.1](./blog/20220808-simplex-chat-v3.1-chat-groups.md). +9. Pro ochranu vaší IP adresy podporují všichni klienti SimpleX Chat přístup k serverům pro zasílání zpráv přes Tor - více informací najdete v [oznámení o vydání v3.1](../../../blog/20220808-simplex-chat-v3.1-chat-groups.md). 10. Šifrování místní databáze s přístupovou frází - kontakty, skupiny a všechny odeslané a přijaté zprávy jsou uloženy šifrovaně. Pokud jste používali SimpleX Chat před verzí 4.0, musíte šifrování povolit prostřednictvím nastavení aplikace. 11. Izolace transportu - pro provoz různých uživatelských profilů se používají různá spojení TCP a okruhy Tor, volitelně - pro různá spojení kontaktů a členů skupin. @@ -166,7 +166,7 @@ Můžete: - použít knihovnu SimpleX Chat k integraci funkcí chatu do svých mobilních aplikací. - vytvářet chatovací boty a služby v jazyce Haskell - viz [simple](./apps/simplex-bot/) a více [advanced chat bot example](./apps/simplex-bot-advanced/). - vytvářet chatovací boty a služby v libovolném jazyce se spuštěným terminálem SimpleX Chat CLI jako lokálním serverem WebSocket. Viz [TypeScript SimpleX Chat client](./packages/simplex-chat-client/) a [JavaScript chat bot example](./packages/simplex-chat-client/typescript/examples/squaring-bot.js). -- spustit [simplex-chat terminal CLI](./docs/CLI.md) pro provádění jednotlivých příkazů chatu, např. pro odesílání zpráv v rámci provádění shellových skriptů. +- spustit [simplex-chat terminal CLI](./CLI.md) pro provádění jednotlivých příkazů chatu, např. pro odesílání zpráv v rámci provádění shellových skriptů. Pokud uvažujete o vývoji s platformou SimpleX, obraťte se na nás pro případné rady a podporu. @@ -253,7 +253,7 @@ Aktuální jazyky rozhraní: - Italština: [@unbranched](https://github.com/unbranched) - Ruština: projektový tým -Jazyky ve vývoji: Čínština, hindština, čeština, japonština, holandština a [mnoho dalších](https://hosted.weblate.org/projects/simplex-chat/#languages). Další jazyky budeme přidávat, jakmile budou některé z již přidaných jazyků dokončeny - navrhněte prosím nové jazyky, projděte si [průvodce překladem](./docs/TRANSLATIONS.md) a kontaktujte nás! +Jazyky ve vývoji: Čínština, hindština, čeština, japonština, holandština a [mnoho dalších](https://hosted.weblate.org/projects/simplex-chat/#languages). Další jazyky budeme přidávat, jakmile budou některé z již přidaných jazyků dokončeny - navrhněte prosím nové jazyky, projděte si [průvodce překladem](./TRANSLATIONS.md) a kontaktujte nás! ## Přispívejte @@ -294,7 +294,7 @@ Zakladatel SimpleX Chat Protokoly a bezpečnostní model [SimpleX](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) byly revidovány a ve verzi 1.0.0 došlo k mnoha zlomovým změnám a vylepšením. -Bezpečnostní audit provedla v říjnu 2022 společnost [Trail of Bits](https://www.trailofbits.com/about) a většina oprav byla vydána ve verzi 4.2.0 - viz [oznámení](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). +Bezpečnostní audit provedla v říjnu 2022 společnost [Trail of Bits](https://www.trailofbits.com/about) a většina oprav byla vydána ve verzi 4.2.0 - viz [oznámení](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). SimpleX Chat je stále relativně ranou fází platformy (mobilní aplikace byly vydány v březnu 2022), takže můžete objevit některé chyby a chybějící funkce. Velmi oceníme, pokud nám dáte vědět o všem, co je třeba opravit nebo vylepšit. diff --git a/docs/lang/cs/SERVER.md b/docs/lang/cs/SERVER.md index 3dd2f3780c..f75adeb8cf 100644 --- a/docs/lang/cs/SERVER.md +++ b/docs/lang/cs/SERVER.md @@ -12,7 +12,7 @@ SMP server je relay server používaný k předávání zpráv v síti SimpleX. Klienti SimpleX pouze určují, který server bude použit pro příjem zpráv, a to pro každý kontakt (nebo spojení skupiny s členem skupiny) zvlášť, přičemž tyto servery jsou pouze dočasné, protože adresa pro doručování se může změnit. -_Upozornění_: když změníte servery v konfiguraci aplikace, ovlivní to pouze to, který server bude použit pro nové kontakty, stávající kontakty se na nové servery automaticky nepřesunou, ale můžete je přesunout ručně pomocí tlačítka ["Změnit adresu příjmu"](../blog/20221108-simplex-chat-v4.2-bezpecnostni-audit-novy-website.md#zmeny-dorucovani-adresy-beta) na stránkách s informacemi o kontaktech/členech - brzy bude automatizováno. +_Upozornění_: když změníte servery v konfiguraci aplikace, ovlivní to pouze to, který server bude použit pro nové kontakty, stávající kontakty se na nové servery automaticky nepřesunou, ale můžete je přesunout ručně pomocí tlačítka ["Změnit adresu příjmu"](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md#change-your-delivery-address-beta) na stránkách s informacemi o kontaktech/členech - brzy bude automatizováno. ## Instalace diff --git a/docs/lang/fr/CLI.md b/docs/lang/fr/CLI.md index bb596491f1..e5093f20c0 100644 --- a/docs/lang/fr/CLI.md +++ b/docs/lang/fr/CLI.md @@ -97,7 +97,7 @@ git checkout stable DOCKER_BUILDKIT=1 docker build --output ~/.local/bin . ``` -> **Veuillez noter** : Si vous rencontrez l'erreur ``version `GLIBC_2.28' non trouvée``, reconstruisez-le avec l'image de base `haskell:8.10.7-stretch`(changez-la dans votre [Dockerfile](Dockerfile) local). +> **Veuillez noter** : Si vous rencontrez l'erreur ``version `GLIBC_2.28' non trouvée``, reconstruisez-le avec l'image de base `haskell:8.10.7-stretch`(changez-la dans votre [Dockerfile](/Dockerfile) local). #### Utiliser Haskell stack diff --git a/docs/lang/pl/CLI.md b/docs/lang/pl/CLI.md index 585eca3e31..0a72b163bb 100644 --- a/docs/lang/pl/CLI.md +++ b/docs/lang/pl/CLI.md @@ -97,7 +97,7 @@ git checkout stable DOCKER_BUILDKIT=1 docker build --output ~/.local/bin . ``` -> **Uwaga:** Jeśli napotkasz błąd `` version `GLIBC_2.28' not found ``, przebuduj go z obrazem bazowym `haskell:8.10.7-stretch` (zmień go w Twoim lokalnym pliku [Dockerfile](Dockerfile)). +> **Uwaga:** Jeśli napotkasz błąd `` version `GLIBC_2.28' not found ``, przebuduj go z obrazem bazowym `haskell:8.10.7-stretch` (zmień go w Twoim lokalnym pliku [Dockerfile](/Dockerfile)). #### Używając Haskella na dowolnym systemie operacyjnym @@ -200,7 +200,7 @@ Po uruchomieniu czatu zostaniesz poproszony o podanie swojej "nazwy wyświetlane Poniższy schemat przedstawia sposób łączenia się z kontaktem i wysyłania do niego wiadomości:
- +
Gdy już skonfigurujesz swój profil lokalny, wpisz `/c` (oznaczające `/connect`), aby utworzyć nowe połączenie i wygenerować zaproszenie. Wyślij to zaproszenie do swojego kontaktu za pośrednictwem dowolnego innego kanału komunikacji. @@ -219,7 +219,7 @@ Użyj `/help` na czacie, by uzyskać listę pozostałych dostępnych komend. Aby utworzyć grupę, użyj `/g `, a następnie dodaj do niej kontakty za pomocą `/a `. Możesz wysyłać wiadomości do grupy wpisując `# `. Użyj `/help groups`, by uzyskać listę pozostałych dostępnych komend. -![simplex-chat](../images/groups.gif) +![simplex-chat](/images/groups.gif) > **Uwaga**: informacje o grupach nie są przechowywane na żadnym serwerze, są one zapisywane jako lista członków w bazie danych aplikacji klientów, do których będą wysyłane wiadomości. @@ -227,7 +227,7 @@ Aby utworzyć grupę, użyj `/g `, a następnie dodaj do niej konta Możesz wysłać plik do kontaktu za pomocą `/f @ <ścieżka_do_pliku>` - odbiorca będzie musiał go zaakceptować przed rozpoczęciem wysyłania. Użyj `/help files`, by uzyskać listę pozostałych dostępnych komend. -![simplex-chat](../images/files.gif) +![simplex-chat](/images/files.gif) Możesz wysyłać pliki do grupy za pomocą `/f # <ścieżka_do_pliku>`. @@ -241,4 +241,4 @@ Prośby o kontakt możesz przyjąć za pomocą komendy `/ac ` oraz odrzuc Użyj `/help address`, by uzyskać listę pozostałych dostępnych komend. -![simplex-chat](../images/user-addresses.gif) +![simplex-chat](/images/user-addresses.gif) diff --git a/docs/lang/pl/README.md b/docs/lang/pl/README.md index ba40a42d74..23ca00c3e6 100644 --- a/docs/lang/pl/README.md +++ b/docs/lang/pl/README.md @@ -50,7 +50,7 @@ Możesz połączyć się z naszym zespołem za pośrednictwem aplikacji, korzyst Odpowiadamy na pytania manualnie, więc nie jest to natychmiastowe - może to potrwać do 24 godzin. -Jeśli jesteś zainteresowany pomocą w integracji otwartoźródłowych modeli językowych i [dołączeniem do naszego zespołu](./docs/lang/pl/JOIN_TEAM.md), skontaktuj się z nami. +Jeśli jesteś zainteresowany pomocą w integracji otwartoźródłowych modeli językowych i [dołączeniem do naszego zespołu](../../JOIN_TEAM.md), skontaktuj się z nami. ## Dołącz do grup użytkowników @@ -62,7 +62,7 @@ Możesz również: - krytykować aplikację i dokonywać porównań z innymi komunikatorami. - udostępniać nowe komunikatory, które Twoim zdaniem mogą być interesujące z punktu widzenia prywatności, o ile nie spamujesz. - udostępniać niektóre publikacje związane z prywatnością, raczej dość rzadko. -- po wstępnym zatwierdzeniu przez administratora w prywatnej wiadomości, udostępnić link do utworzonej grupy, ale tylko raz. Gdy grupa ma więcej niż 10 członków, może zostać przesłana do [SimpleX Directory Service](./docs/DIRECTORY.md), gdzie nowi użytkownicy będą mogli ją odkryć. +- po wstępnym zatwierdzeniu przez administratora w prywatnej wiadomości, udostępnić link do utworzonej grupy, ale tylko raz. Gdy grupa ma więcej niż 10 członków, może zostać przesłana do [SimpleX Directory Service](../../DIRECTORY.md), gdzie nowi użytkownicy będą mogli ją odkryć. Musisz: - być uprzejmym wobec innych użytkowników. @@ -104,11 +104,11 @@ Kanał, za pośrednictwem którego udostępniasz link, nie musi być bezpieczny Wykonaj prywatne połączenie Conversation Połączenie wideo -Po wykonaniu połączenia możesz [zweryfikować kod bezpieczeństwa połączenia](./blog/20230103-simplex-chat-v4.4-disappearing-messages.md#connection-security-verification). +Po wykonaniu połączenia możesz [zweryfikować kod bezpieczeństwa połączenia](../../../blog/20230103-simplex-chat-v4.4-disappearing-messages.md#connection-security-verification). ## Poradnik dla użytkownika (NOWE) -Przeczytaj o funkcjach i ustawieniach aplikacji w nowym [Przewodniku użytkownika](./docs/guide/README.md). +Przeczytaj o funkcjach i ustawieniach aplikacji w nowym [Przewodniku użytkownika](../../guide/README.md). ## Pomóż nam przetłumaczyć SimpleX Chat @@ -139,13 +139,13 @@ Dołącz do naszych tłumaczy, aby pomóc SimpleX w rozwoju! |🇺🇦 uk|Українська| |[![android app](https://hosted.weblate.org/widgets/simplex-chat/uk/android/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/android/uk/)
[![ios app](https://hosted.weblate.org/widgets/simplex-chat/uk/ios/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/ios/uk/)|[![website](https://hosted.weblate.org/widgets/simplex-chat/uk/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/uk/)|| |🇨🇳 zh-CHS|简体中文|[sith-on-mars](https://github.com/sith-on-mars)

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

[![website](https://hosted.weblate.org/widgets/simplex-chat/zh_Hans/website/svg-badge.svg)](https://hosted.weblate.org/projects/simplex-chat/website/zh_Hans/)|| -Trwają prace nad wersjami językowymi: Arabski, japoński, koreański, portugalski i [inne](https://hosted.weblate.org/projects/simplex-chat/#languages). Będziemy dodawać kolejne języki, gdy niektóre z już dodanych zostaną ukończone - zasugeruj nowe języki, przejrzyj [przewodnik po tłumaczeniach](./docs/lang/pl/TRANSLATIONS.md) i skontaktuj się z nami! +Trwają prace nad wersjami językowymi: Arabski, japoński, koreański, portugalski i [inne](https://hosted.weblate.org/projects/simplex-chat/#languages). Będziemy dodawać kolejne języki, gdy niektóre z już dodanych zostaną ukończone - zasugeruj nowe języki, przejrzyj [przewodnik po tłumaczeniach](./TRANSLATIONS.md) i skontaktuj się z nami! ## Kontrybuuj Chcielibyśmy, abyś przyczynił się do naszego rozwoju! Możesz nam pomóc: -- [dzieląc się motywem kolorystycznym](./docs/THEMES.md), którego używasz w aplikacji na Androida! +- [dzieląc się motywem kolorystycznym](../../THEMES.md), którego używasz w aplikacji na Androida! - pisząc samouczki lub poradniki, które dotyczą hostowania serwerów, automatyzacji czatbotów itp. - współtworząc bazy wiedzy SimpleX Chat. - rozwijając funkcje - skontaktuj się z nami za pośrednictwem czatu, abyśmy mogli pomóc Ci zacząć. @@ -208,23 +208,23 @@ Używanie szyfrowanego komunikatora end-to-end nie jest wystarczające. Powinni ### Kompletna prywatność Twojej tożsamości, profilu, kontaktów i metadanych. -**W przeciwieństwie do innych komunikatorów, SimpleX nie posiada żadnych identyfikatorów przypisanych do użytkowników**. Nie posiada nawet numerów generowanych losowo. Zapewnia to prywatność tego, z kim się komunikujesz, ukrywając jego tożsamość oraz fakt komunikacji przed serwerami platformy SimpleX i wszelkimi obserwatorami [Czytaj więcej](./docs/lang/pl/SIMPLEX.md#full-privacy-of-your-identity-profile-contacts-and-metadata). +**W przeciwieństwie do innych komunikatorów, SimpleX nie posiada żadnych identyfikatorów przypisanych do użytkowników**. Nie posiada nawet numerów generowanych losowo. Zapewnia to prywatność tego, z kim się komunikujesz, ukrywając jego tożsamość oraz fakt komunikacji przed serwerami platformy SimpleX i wszelkimi obserwatorami [Czytaj więcej](./SIMPLEX.md#full-privacy-of-your-identity-profile-contacts-and-metadata). ### Najlepsza ochrona przed spamem i nadużyciami -Ponieważ na platformie SimpleX nie masz identyfikatora ani stałego adresu, nikt nie może się z Tobą skontaktować, chyba że udostępnisz jednorazowy lub tymczasowy adres użytkownika, w postaci kodu QR lub linku. [Czytaj więcej](./docs/lang/pl/SIMPLEX.md#the-best-protection-against-spam-and-abuse). +Ponieważ na platformie SimpleX nie masz identyfikatora ani stałego adresu, nikt nie może się z Tobą skontaktować, chyba że udostępnisz jednorazowy lub tymczasowy adres użytkownika, w postaci kodu QR lub linku. [Czytaj więcej](./SIMPLEX.md#the-best-protection-against-spam-and-abuse). ### Pełna kontrola i bezpieczeństwo Twoich danych -SimpleX przechowuje wszystkie dane użytkownika na urządzeniach klienckich, wiadomości są przechowywane tymczasowo na serwerach przekaźnikowych SimpleX do momentu ich odebrania, po czym są trwale usuwane. [Czytaj więcej](./docs/lang/pl/SIMPLEX.md#complete-ownership-control-and-security-of-your-data). +SimpleX przechowuje wszystkie dane użytkownika na urządzeniach klienckich, wiadomości są przechowywane tymczasowo na serwerach przekaźnikowych SimpleX do momentu ich odebrania, po czym są trwale usuwane. [Czytaj więcej](./SIMPLEX.md#complete-ownership-control-and-security-of-your-data). ### Użytkownicy są właścicielami sieci SimpleX -Możesz używać SimpleX na własnych serwerach i nadal komunikować się z ludźmi za pomocą serwerów, które są wstępnie skonfigurowane w aplikacjach lub z dowolnymi innymi serwerami SimpleX. [Czytaj więcej](./docs/lang/pl/SIMPLEX.md#users-own-simplex-network). +Możesz używać SimpleX na własnych serwerach i nadal komunikować się z ludźmi za pomocą serwerów, które są wstępnie skonfigurowane w aplikacjach lub z dowolnymi innymi serwerami SimpleX. [Czytaj więcej](./SIMPLEX.md#users-own-simplex-network). ## Często zadawane pytania -1. _W jaki sposób SimpleX może dostarczać wiadomości bez jakichkolwiek identyfikatorów użytkownika?_ Zobacz [ogłoszenie wydania v2](./blog/20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers) wyjaśniające jak SimpleX działa. +1. _W jaki sposób SimpleX może dostarczać wiadomości bez jakichkolwiek identyfikatorów użytkownika?_ Zobacz [ogłoszenie wydania v2](../../../blog/20220511-simplex-chat-v2-images-files.md#the-first-messaging-platform-without-user-identifiers) wyjaśniające jak SimpleX działa. 2. _Dlaczego po prostu nie mogę używać Signal?_ Signal to scentralizowana platforma, która wykorzystuje numery telefonów do identyfikacji użytkowników i ich kontaktów. Oznacza to, że podczas gdy treść wiadomości w Signal jest chroniona solidnym szyfrowaniem end-to-end, istnieje duża ilość metadanych widocznych dla Signal - to, z kim rozmawiasz i kiedy. @@ -234,29 +234,29 @@ Możesz używać SimpleX na własnych serwerach i nadal komunikować się z lud Najnowsze i ważne wiadomości: -[Mar 23, 2024. SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration.](./blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md) +[Mar 23, 2024. SimpleX network: real privacy and stable profits, non-profits for protocols, v5.6 released with quantum resistant e2e encryption and simple profile migration.](../../../blog/20240323-simplex-network-privacy-non-profit-v5-6-quantum-resistant-e2e-encryption-simple-migration.md) -[Mar 14, 2024. SimpleX Chat v5.6 beta: adding quantum resistance to Signal double ratchet algorithm.](./blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md) +[Mar 14, 2024. SimpleX Chat v5.6 beta: adding quantum resistance to Signal double ratchet algorithm.](../../../blog/20240314-simplex-chat-v5-6-quantum-resistance-signal-double-ratchet-algorithm.md) -[Jan 24, 2024. SimpleX Chat: free infrastructure from Linode, v5.5 released with private notes, group history and a simpler UX to connect.](./blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md) +[Jan 24, 2024. SimpleX Chat: free infrastructure from Linode, v5.5 released with private notes, group history and a simpler UX to connect.](../../../blog/20240124-simplex-chat-infrastructure-costs-v5-5-simplex-ux-private-notes-group-history.md) -[Nov 25, 2023. SimpleX Chat v5.4 released: link mobile and desktop apps via quantum resistant protocol, and much better groups](./blog/20231125-simplex-chat-v5-4-link-mobile-desktop-quantum-resistant-better-groups.md). +[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). +[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). +[Jul 22, 2023. SimpleX Chat: v5.2 released with message delivery receipts](../../../blog/20230722-simplex-chat-v5-2-message-delivery-receipts.md). -[May 23, 2023. SimpleX Chat: v5.1 released with message reactions and self-destruct passcode](./blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md). +[May 23, 2023. SimpleX Chat: v5.1 released with message reactions and self-destruct passcode](../../../blog/20230523-simplex-chat-v5-1-message-reactions-self-destruct-passcode.md). -[Apr 22, 2023. SimpleX Chat: vision and funding, v5.0 released with videos and files up to 1gb](./blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md). +[Apr 22, 2023. SimpleX Chat: vision and funding, v5.0 released with videos and files up to 1gb](../../../blog/20230422-simplex-chat-vision-funding-v5-videos-files-passcode.md). -[Mar 1, 2023. SimpleX File Transfer Protocol – send large files efficiently, privately and securely, soon to be integrated into SimpleX Chat apps.](./blog/20230301-simplex-file-transfer-protocol.md). +[Mar 1, 2023. SimpleX File Transfer Protocol – send large files efficiently, privately and securely, soon to be integrated into SimpleX Chat apps.](../../../blog/20230301-simplex-file-transfer-protocol.md). -[Nov 8, 2022. Security audit by Trail of Bits, the new website and v4.2 released](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). +[Nov 8, 2022. Security audit by Trail of Bits, the new website and v4.2 released](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). -[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](./blog/20220928-simplex-chat-v4-encrypted-database.md). +[Sep 28, 2022. v4.0: encrypted local chat database and many other changes](../../../blog/20220928-simplex-chat-v4-encrypted-database.md). -[All updates](./blog) +[All updates](../../../blog) ## :zap: Szybka instalacja terminalowej wersji aplikacji @@ -268,13 +268,13 @@ Po pobraniu klienta czatu można go uruchomić za pomocą polecenia `simplex-cha ![simplex-chat](./images/connection.gif) -Przeczytaj więcej o [instalowaniu i używaniu terminalowej wersji czatu](./docs/lang/pl/CLI.md). +Przeczytaj więcej o [instalowaniu i używaniu terminalowej wersji czatu](./CLI.md). ## Budowa Platformy SimpleX SimpleX to sieć typu klient-serwer z unikatową topologią sieciową, która wykorzystuje redundantne, jednorazowe węzły przekazywania wiadomości do asynchronicznego przekazywania wiadomości za pośrednictwem jednokierunkowych (simpleksowych) kolejek wiadomości, zapewniając anonimowość odbiorcy i nadawcy. -W przeciwieństwie do sieci P2P, wszystkie wiadomości są przekazywane przez jeden lub kilka węzłów serwera, które nawet nie muszą być trwałe. Obecna implementacja [serwera SMP](https://github.com/simplex-chat/simplexmq#smp-server) wykorzystuje przechowywanie wiadomości w pamięci, utrzymując jedynie rejestr kolejki. SimpleX zapewnia lepszą ochronę metadanych niż projekty P2P, ponieważ żadne globalne identyfikatory uczestników nie są używane do dostarczania wiadomości i pozwala to uniknąć [różnych problemów związanych z sieciami P2P](./docs/lang/pl/SIMPLEX.md#comparison-with-p2p-messaging-protocols). +W przeciwieństwie do sieci P2P, wszystkie wiadomości są przekazywane przez jeden lub kilka węzłów serwera, które nawet nie muszą być trwałe. Obecna implementacja [serwera SMP](https://github.com/simplex-chat/simplexmq#smp-server) wykorzystuje przechowywanie wiadomości w pamięci, utrzymując jedynie rejestr kolejki. SimpleX zapewnia lepszą ochronę metadanych niż projekty P2P, ponieważ żadne globalne identyfikatory uczestników nie są używane do dostarczania wiadomości i pozwala to uniknąć [różnych problemów związanych z sieciami P2P](./SIMPLEX.md#comparison-with-p2p-messaging-protocols). W przeciwieństwie do sieci sfederowanych, węzły serwera **nie posiadają danych użytkowników**, **nie komunikują się ze sobą** i **nie przechowują wiadomości** po ich dostarczeniu do odbiorców. Nie ma możliwości na odkrycie pełnej listy serwerów działających w sieci SimpleX. Taka konstrukcja pozwala uniknąć problemu związanego z widocznością metadanych, z którym borykają się wszystkie sieci sfederowane i pozwala ona na lepszą ochronę przed atakami obejmującymi całą sieć. @@ -282,29 +282,29 @@ Informacje o użytkownikach, ich kontaktach i grupach znajdują się wyłącznie Przeczytaj [whitepaper SimpleX](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/overview-tjr.md) po więcej informacji o zadaniach platformy oraz by dowiedzieć się jak wygląda koncepcja techniczna modelu. -Zobacz [Protokół Czatu SimpleX](./docs/protocol/simplex-chat.md) by dowiedzieć się o formacie wiadomości wysyłanych między klientem czatu za pośrednictwem [Protokołu Wiadomości SimpleX](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/simplex-messaging.md). +Zobacz [Protokół Czatu SimpleX](../../protocol/simplex-chat.md) by dowiedzieć się o formacie wiadomości wysyłanych między klientem czatu za pośrednictwem [Protokołu Wiadomości SimpleX](https://github.com/simplex-chat/simplexmq/blob/stable/protocol/simplex-messaging.md). ## Prywatność i bezpieczeństwo: szczegóły techniczne i ograniczenia Prace nad SimpleX Chat wciąż trwają - udostępniamy nowe ulepszenia, gdy tylko będą gotowe. To Ty musisz zdecydować, czy obecny stan jest wystarczająco dobry dla Twojego przypadku zastosowania. -Stworzyliśmy [słownik pojęć](./docs/GLOSSARY.md) używany do opisu systemów komunikacyjnych, aby pomóc zrozumieć niektóre z poniższych pojęć oraz aby pomóc Ci w porównaniu zalet i wad różnych systemów komunikacyjnych. +Stworzyliśmy [słownik pojęć](../../GLOSSARY.md) używany do opisu systemów komunikacyjnych, aby pomóc zrozumieć niektóre z poniższych pojęć oraz aby pomóc Ci w porównaniu zalet i wad różnych systemów komunikacyjnych. Co zostało już wprowadzone: -1. Zamiast identyfikatorów użytkownika używanych przez wszystkie inne platformy, nawet te najbardziej prywatne, SimpleX używa [pairwise per-queue identifiers](./docs/GLOSSARY.md#pairwise-pseudonymous-identifier) (2 adresy dla każdej jednokierunkowej kolejki wiadomości, z opcjonalnym trzecim adresem dla powiadomień push na iOS, 2 kolejki w każdym połączeniu między użytkownikami). Sprawia to, że trudniej jest w ten sposób obserwować przebieg połączeń sieciowych na poziomie aplikacji, ponieważ dla `n` użytkowników może istnieć do `n * (n-1)` kolejek wiadomości. -2. [Szyfrowanie end-to-end](./docs/GLOSSARY.md#end-to-end-encryption) w każdej kolejce wiadomości używając [cryptoboxa NaCl](https://nacl.cr.yp.to/box.html). Zostało to dodane, aby umożliwić redundancję w przyszłości (przekazywanie każdej wiadomości przez kilka serwerów), aby uniknąć posiadania tego samego ciphertext w różnych kolejkach (które byłyby widoczne tylko dla atakującego, w przypadku przejęcia TLS). Klucze szyfrujące używane do tego szyfrowania nie są rotowane, zamiast tego planujemy rotować kolejki. Do negocjacji kluczy używane są klucze Curve25519. -3. Szyfrowanie end-to-end [double ratchet](./docs/GLOSSARY.md#double-ratchet-algorithm) w każdej rozmowie między dwoma użytkownikami (lub członkami grupy). Jest to ten sam algorytm, który jest używany w Signal i wielu innych komunikatorach; zapewnia on komunikację OTR z [forward secrecy](./docs/GLOSSARY.md#forward-secrecy) (każda wiadomość jest szyfrowana własnym kluczem efemerycznym) i [break-in recovery](./docs/GLOSSARY.md#post-compromise-security) (klucze są często renegocjowane w ramach wymiany wiadomości). Dwie pary kluczy Curve448 są używane do początkowego [key agreement](./docs/GLOSSARY.md#key-agreement-protocol), strona inicjująca przekazuje te klucze przez link połączenia, a strona akceptująca - w nagłówku wiadomości potwierdzającej. +1. Zamiast identyfikatorów użytkownika używanych przez wszystkie inne platformy, nawet te najbardziej prywatne, SimpleX używa [pairwise per-queue identifiers](../../GLOSSARY.md#pairwise-pseudonymous-identifier) (2 adresy dla każdej jednokierunkowej kolejki wiadomości, z opcjonalnym trzecim adresem dla powiadomień push na iOS, 2 kolejki w każdym połączeniu między użytkownikami). Sprawia to, że trudniej jest w ten sposób obserwować przebieg połączeń sieciowych na poziomie aplikacji, ponieważ dla `n` użytkowników może istnieć do `n * (n-1)` kolejek wiadomości. +2. [Szyfrowanie end-to-end](../../GLOSSARY.md#end-to-end-encryption) w każdej kolejce wiadomości używając [cryptoboxa NaCl](https://nacl.cr.yp.to/box.html). Zostało to dodane, aby umożliwić redundancję w przyszłości (przekazywanie każdej wiadomości przez kilka serwerów), aby uniknąć posiadania tego samego ciphertext w różnych kolejkach (które byłyby widoczne tylko dla atakującego, w przypadku przejęcia TLS). Klucze szyfrujące używane do tego szyfrowania nie są rotowane, zamiast tego planujemy rotować kolejki. Do negocjacji kluczy używane są klucze Curve25519. +3. Szyfrowanie end-to-end [double ratchet](../../GLOSSARY.md#double-ratchet-algorithm) w każdej rozmowie między dwoma użytkownikami (lub członkami grupy). Jest to ten sam algorytm, który jest używany w Signal i wielu innych komunikatorach; zapewnia on komunikację OTR z [forward secrecy](../../GLOSSARY.md#forward-secrecy) (każda wiadomość jest szyfrowana własnym kluczem efemerycznym) i [break-in recovery](../../GLOSSARY.md#post-compromise-security) (klucze są często renegocjowane w ramach wymiany wiadomości). Dwie pary kluczy Curve448 są używane do początkowego [key agreement](../../GLOSSARY.md#key-agreement-protocol), strona inicjująca przekazuje te klucze przez link połączenia, a strona akceptująca - w nagłówku wiadomości potwierdzającej. 4. Dodatkowa warstwa szyfrowania przy użyciu NaCL cryptobox dla wiadomości dostarczanych z serwera do odbiorcy. Warstwa ta pozwala uniknąć wspólnego szyfrogramu między wysyłanym i odbieranym ruchem serwera wewnątrz TLS (i nie ma też wspólnych identyfikatorów). -5. Kilka poziomów [content padding](./docs/GLOSSARY.md#message-padding) w celu utrudnienia ataków na rozmiar wiadomości. +5. Kilka poziomów [content padding](../../GLOSSARY.md#message-padding) w celu utrudnienia ataków na rozmiar wiadomości. 6. Wszystkie metadane wiadomości, w tym czas odebrania wiadomości przez serwer (zaokrąglony do sekundy), są wysyłane do odbiorców w zaszyfrowanej postaci, więc nawet jeśli TLS zostanie przejęty, nie można ich zobaczyć. 7. Dozwolone są tylko TLS 1.2/1.3 dla połączeń klient-serwer, z ograniczeniem do algorytmów kryptograficznych: CHACHA20POLY1305_SHA256, Ed25519/Ed448, Curve25519/Curve448. 8. Aby zapobiec atakom typu replay, serwery SimpleX wymagają [tlsunique channel binding](https://www.rfc-editor.org/rfc/rfc5929.html) jako identyfikatora sesji w każdym poleceniu klienta podpisanym kluczem efemerycznym dla każdej kolejki. -9. Aby ochronić swój adres IP, wszystkie klienty SimpleX Chat obsługują dostęp do serwerów komunikacyjnych za pośrednictwem Tora - zobacz [v3.1 release announcement](./blog/20220808-simplex-chat-v3.1-chat-groups.md) po więcej szczegółów. +9. Aby ochronić swój adres IP, wszystkie klienty SimpleX Chat obsługują dostęp do serwerów komunikacyjnych za pośrednictwem Tora - zobacz [v3.1 release announcement](../../../blog/20220808-simplex-chat-v3.1-chat-groups.md) po więcej szczegółów. 10. Lokalne szyfrowanie bazy danych z hasłem - kontakty, grupy oraz wszystkie wysłane i odebrane wiadomości są przechowywane w postaci zaszyfrowanej. Jeśli korzystałeś z SimpleX Chat przed wersją v4.0, musisz włączyć szyfrowanie w ustawieniach aplikacji. 11. Izolacja transportu - różne połączenia TCP i obwody Tor używane są dla ruchu różnych profili użytkowników, opcjonalnie - dla różnych kontaktów i połączeń członków grupy. 12. Ręczne obracanie kolejki wiadomości w celu przeniesienia konwersacji do innego przekaźnika SMP. -13. Wysyłanie zaszyfrowanych plików end-to-end przy użyciu [protokołu XFTP](https://simplex.chat/blog/20230301-simplex-file-transfer-protocol.html). +13. Wysyłanie zaszyfrowanych plików end-to-end przy użyciu [protokołu XFTP](../../../blog/20230301-simplex-file-transfer-protocol.md). 14. Szyfrowanie plików lokalnych. Planujemy dodać: @@ -322,7 +322,7 @@ Możesz: - korzystać z biblioteki SimpleX Chat w celu zintegrowania funkcji czatu z aplikacjami mobilnymi. - tworzyć boty i usługi czatu w języku Haskell - zobacz [prosty](./apps/simplex-bot/) i bardziej [zaawansowany przykład bota czatu](./apps/simplex-bot-advanced/). - tworzenie chat botów i usług w dowolnym języku z wykorzystaniem terminala CLI SimpleX Chat jako lokalnego serwera WebSocket. Zobacz [TypeScript SimpleX Chat client](./packages/simplex-chat-client/) i [JavaScript chat bot example](./packages/simplex-chat-client/typescript/examples/squaring-bot.js). -- uruchomić [simplex-chat w terminal ](./docs/lang/pl/CLI.md), aby wykonywać poszczególne polecenia czatu, np. wysyłać wiadomości w ramach wykonywania skryptu powłoki. +- uruchomić [simplex-chat w terminal ](./CLI.md), aby wykonywać poszczególne polecenia czatu, np. wysyłać wiadomości w ramach wykonywania skryptu powłoki. Jeśli chcesz rozwijać platformę SimpleX, skontaktuj się z nami, aby uzyskać porady i wsparcie. @@ -365,7 +365,7 @@ Dołącz również do grupy [#simplex-devs](https://simplex.chat/contact#/?v=1-2 - ✅ Ulepszone połączenia audio i wideo. - ✅ Obsługa starszego systemu operacyjnego Android i 32-bitowych procesorów. - ✅ Ukryte profile czatu. -- ✅ Wysyłanie i odbieranie dużych plików przez [protokół XFTP](./blog/20230301-simplex-file-transfer-protocol.md). +- ✅ Wysyłanie i odbieranie dużych plików przez [protokół XFTP](../../../blog/20230301-simplex-file-transfer-protocol.md). - ✅ Wiadomości wideo. - ✅ Kod dostępu do aplikacji. - ✅ Ulepszenie interfejsu Androidowej aplikacji. @@ -402,7 +402,7 @@ Dołącz również do grupy [#simplex-devs](https://simplex.chat/contact#/?v=1-2 [Protokoły i model bezpieczeństwa SimpleX](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md) zostały poddane przeglądowi i zawierały wiele istotnych zmian i ulepszeń w wersji v1.0.0. -Audyt bezpieczeństwa został przeprowadzony w październiku 2022 r. przez [Trail of Bits](https://www.trailofbits.com/about), a większość poprawek została wydana w wersji 4.2.0 - zobacz [ogłoszenie](./blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). +Audyt bezpieczeństwa został przeprowadzony w październiku 2022 r. przez [Trail of Bits](https://www.trailofbits.com/about), a większość poprawek została wydana w wersji 4.2.0 - zobacz [ogłoszenie](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md). SimpleX Chat jest nadal na stosunkowo wczesnym etapie rozwoju (aplikacje mobilne zostały wydane w marcu 2022 r.), więc możesz odkryć pewne błędy i brakujące funkcje. Będziemy bardzo wdzięczni za poinformowanie nas o wszystkim, co wymaga naprawy lub ulepszenia. diff --git a/docs/lang/pl/SERVER.md b/docs/lang/pl/SERVER.md index 72cb51a4bf..f4bffc1bcc 100644 --- a/docs/lang/pl/SERVER.md +++ b/docs/lang/pl/SERVER.md @@ -13,7 +13,7 @@ Serwer SMP to serwer przekaźnikowy używany do przekazywania wiadomości w siec Klienty SimpleX określają tylko, który serwer jest używany do odbierania wiadomości, oddzielnie dla każdego kontaktu (lub połączenia grupowego z członkiem grupy), a serwery te są tylko tymczasowe, ponieważ adres dostawy może ulec zmianie. -_Uwaga_: gdy zmienisz serwery w ustawieniach aplikacji, wpłynie to tylko na to, który serwer będzie używany dla nowych kontaktów, istniejące kontakty nie zostaną automatycznie przeniesione na nowe serwery, ale możesz przenieść je ręcznie za pomocą przycisku ["Zmień adres odbiorczy"](../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md#change-your-delivery-address-beta) na stronie z informacjami kontaktu/członka - wkrótce zostanie to zautomatyzowane. +_Uwaga_: gdy zmienisz serwery w ustawieniach aplikacji, wpłynie to tylko na to, który serwer będzie używany dla nowych kontaktów, istniejące kontakty nie zostaną automatycznie przeniesione na nowe serwery, ale możesz przenieść je ręcznie za pomocą przycisku ["Zmień adres odbiorczy"](../../../blog/20221108-simplex-chat-v4.2-security-audit-new-website.md#change-your-delivery-address-beta) na stronie z informacjami kontaktu/członka - wkrótce zostanie to zautomatyzowane. ## Instalacja diff --git a/docs/lang/pl/SIMPLEX.md b/docs/lang/pl/SIMPLEX.md index ff7106d84c..a1ceb8c5e4 100644 --- a/docs/lang/pl/SIMPLEX.md +++ b/docs/lang/pl/SIMPLEX.md @@ -11,7 +11,7 @@ revision: 07.02.2023 Istniejące komunikatory oraz protokoły borykają się ze wszystkimi lub kilkoma podanymi problemami: - Brak zachowania prywatności profilu i kontaktów użytkownika (zachowanie poufności metadanych). -- Brak ochrony (lub jedynie opcjonalna ochrona) przed atakami MITM przez dostawcę usług przy użyciu szyfrowania [end to end](1) +- Brak ochrony (lub jedynie opcjonalna ochrona) przed atakami MITM przez dostawcę usług przy użyciu szyfrowania [end to end][1] - Niechciane wiadomości (spam i nadużycia). - Brak własności danych i ich ochrony. - Dla nietechnicznych użytkowników używanie niescentralizowanych protokołów jest skomplikowane. diff --git a/docs/protocol/diagrams/group.mmd b/docs/protocol/diagrams/group.mmd index 18d392caa5..6a9bd0c786 100644 --- a/docs/protocol/diagrams/group.mmd +++ b/docs/protocol/diagrams/group.mmd @@ -4,9 +4,16 @@ sequenceDiagram participant B as Bob participant C as Existing
contact - note over A, B: 1. send and accept group invitation - A ->> B: x.grp.inv
invite Bob to group
(via contact connection) - B ->> A: x.grp.acpt
accept invitation
(via member connection)
establish group member connection + alt invite contact + note over A, B: 1a. send and accept group invitation + A ->> B: x.grp.inv
invite Bob to group
(via contact connection) + B ->> A: x.grp.acpt
accept invitation
(via member connection)
establish group member connection + else join via group link + note over A, B: 1b. join via group link and accept request + B ->> A: join via group link
SimpleX contact address + A ->> B: x.grp.link.inv in SMP confirmation
accept joining member request,
sending group profile, etc.
establish group member connection + A ->> B: x.grp.link.mem
send inviting member profile + end note over M, B: 2. introduce new member Bob to all existing members A ->> M: x.grp.mem.new
"announce" Bob
to existing members
(via member connections) @@ -20,14 +27,25 @@ sequenceDiagram end A ->> M: x.grp.mem.fwd
forward "invitations" and
Bob's chat protocol version
to all members
(via member connections) + note over M, B: group message forwarding
(while connections between members are being established) + M -->> B: messages between members and Bob are forwarded by Alice + B -->> M: + note over M, B: 3. establish direct and group member connections M ->> B: establish group member connection opt chat protocol compatible version < 2 M ->> B: establish direct connection - note over M, C: 4. deduplicate new contact + note over M, C: 3*. deduplicate new contact B ->> M: x.info.probe
"probe" is sent to all new members B ->> C: x.info.probe.check
"probe" hash,
in case contact and
member profiles match C ->> B: x.info.probe.ok
original "probe",
in case contact and member
are the same user note over B: merge existing and new contacts if received and sent probe hashes match end + + note over M, B: 4. notify inviting member that connection is established + M ->> A: x.grp.mem.con + B ->> A: x.grp.mem.con + note over A: stops forwarding messages + M -->> B: messages are sent via group connection without forwarding + B -->> M: diff --git a/docs/protocol/diagrams/group.svg b/docs/protocol/diagrams/group.svg index 8c1b65dee2..f3c9aa8a26 100644 --- a/docs/protocol/diagrams/group.svg +++ b/docs/protocol/diagrams/group.svg @@ -1 +1,3 @@ -ExistingcontactBobAliceN existingmembersExistingcontactBobAliceN existingmembers1. send and accept group invitation2. introduce new member Bob to all existing membersprepare group member connectionsprepare direct connectionsopt[chat protocolcompatible version< 2]loop[batched]3. establish direct and group member connections4. deduplicate new contactmerge existing and new contacts if received and sent probe hashes matchopt[chat protocol compatible version < 2]x.grp.invinvite Bob to group(via contact connection)x.grp.acptaccept invitation(via member connection)establish group member connectionx.grp.mem.new"announce" Bobto existing members(via member connections)x.grp.mem.intro * N"introduce" members andtheir chat protocol versions(via member connection)x.grp.mem.inv * N"invitations" to connectfor all members(via member connection)x.grp.mem.fwdforward "invitations" andBob's chat protocol versionto all members(via member connections)establish group member connectionestablish direct connectionx.info.probe"probe" is sent to all new membersx.info.probe.check"probe" hash,in case contact andmember profiles matchx.info.probe.ok original "probe", in case contact and memberare the same user \ No newline at end of file + + +ExistingcontactBobAliceN existingmembersExistingcontactBobAliceN existingmembers1a. send and accept group invitation1b. join via group link and accept requestalt[invite contact][join via group link]2. introduce new member Bob to all existing membersprepare group member connectionsprepare direct connectionsopt[chat protocolcompatible version< 2]loop[batched]group message forwarding(while connections between members are being established)3. establish direct and group member connections3*. deduplicate new contactmerge existing and new contacts if received and sent probe hashes matchopt[chat protocol compatible version < 2]4. notify inviting member that connection is establishedstops forwarding messagesx.grp.invinvite Bob to group(via contact connection)x.grp.acptaccept invitation(via member connection)establish group member connectionjoin via group linkSimpleX contact addressx.grp.link.inv in SMP confirmationaccept joining member request,sending group profile, etc.establish group member connectionx.grp.link.memsend inviting member profilex.grp.mem.new"announce" Bobto existing members(via member connections)x.grp.mem.intro * N"introduce" members andtheir chat protocol versions(via member connection)x.grp.mem.inv * N"invitations" to connectfor all members(via member connection)x.grp.mem.fwdforward "invitations" andBob's chat protocol versionto all members(via member connections)messages between members and Bob are forwarded by Aliceestablish group member connectionestablish direct connectionx.info.probe"probe" is sent to all new membersx.info.probe.check"probe" hash,in case contact andmember profiles matchx.info.probe.ok original "probe", in case contact and memberare the same userx.grp.mem.conx.grp.mem.conmessages are sent via group connection without forwarding \ No newline at end of file diff --git a/docs/protocol/simplex-chat.md b/docs/protocol/simplex-chat.md index e3d8e88ae0..4b5f87821b 100644 --- a/docs/protocol/simplex-chat.md +++ b/docs/protocol/simplex-chat.md @@ -2,7 +2,7 @@ title: SimpleX Chat Protocol revision: 08.08.2022 --- -DRAFT Revision 0.1, 2022-08-08 +Revision 2, 2024-06-24 Evgeny Poberezkin @@ -17,18 +17,23 @@ SimpleX Chat Protocol is a protocol used by SimpleX Chat clients to exchange mes The scope of SimpleX Chat Protocol is application level messages, both for chat functionality, related to the conversations between the clients, and extensible for any other application functions. Currently supported chat functions: - direct and group messages, -- message replies (quoting), forwarded messages and message deletions, -- message attachments: images and files, +- message replies (quoting), message editing, forwarded messages and message deletions, +- message attachments: images, videos, voice messages and files, - creating and managing chat groups, - invitation and signalling for audio/video WebRTC calls. ## General message format -SimpleX Chat protocol supports two message formats: +SimpleX Chat protocol supports these message formats: - JSON-based format for chat and application messages. +- compressed format for adapting larger messages to reduced size of message envelope, caused by addition of PQ encryption keys to SMP agent message envelope. - binary format for sending files or any other binary data. +JSON-based message format supports batching inside a single container message, by encoding list of messages as JSON array. + +Current implementation of chat protocol in SimpleX Chat uses SimpleX File Transfer Protocol (XFTP) for file transfer, with passing file description as chat protocol messages, instead passing files in binary format via SMP connections. + ### JSON format for chat and application messages This document uses JTD schemas [RFC 8927](https://www.rfc-editor.org/rfc/rfc8927.html) to define the properties of chat messages, with some additional restrictions on message properties included in metadata member of JTD schemas. In case of any contradiction between JSON examples and JTD schema the latter MUST be considered correct. @@ -77,8 +82,22 @@ For example, this message defines a simple text message `"hello!"`: `params` property includes message data, depending on `event`, as defined below and in [JTD schema](./simplex-chat.schema.json). +### Compressed format + +The syntax of compressed message is defined by the following ABNF notation: + +```abnf +compressedMessage = %s"X" 1*15780 OCTET; compressed message data +``` + +Compressed message is required to fit into 13388 bytes, accounting for agent overhead (see Protocol's maxCompressedMsgLength). + +The actual JSON message is required to fit into 15610 bytes, accounting for group message forwarding (x.grp.msg.forward) overhead (see Protocol's maxEncodedMsgLength). + ### Binary format for sending files +> Note: Planned to be deprecated. No longer used for file transfer in SimpleX Chat implementation of chat protocol. + SimpleX Chat clients use separate connections to send files using a binary format. File chunk size send in each message MUST NOT be bigger than 15,780 bytes to fit into 16kb (16384 bytes) transport block. The syntax of each message used to send files is defined by the following ABNF notation: @@ -117,7 +136,9 @@ SimpleX Chat Protocol supports the following message types passed in `event` pro - `x.contact` - contact profile and additional data sent as part of contact request to a long-term contact address. - `x.info*` - messages to send, update and de-duplicate contact profiles. - `x.msg.*` - messages to create, update and delete content chat items. +- `x.msg.file.descr` - message to transfer XFTP file description. - `x.file.*` - messages to accept and cancel sending files (see files sub-protocol). +- `x.direct.del` - message to notify about contact deletion. - `x.grp.*` - messages used to manage groups and group members (see group sub-protocol). - `x.call.*` - messages to invite to WebRTC calls and send signalling messages. - `x.ok` - message sent during connection handshake. @@ -136,7 +157,7 @@ This message is sent by both sides of the connection during the connection hands ### Probing for duplicate contacts -As there are no globally unique user identitifiers, when the contact a user is already connected to is added to the group by some other group member, this contact will be added to user's list of contacts as a new contact. To allow merging such contacts, "a probe" (random base64url-encoded 32 bytes) SHOULD be sent to all new members as part of `x.info.probe` message and, in case there is a contact with the same profile, the hash of the probe MAY be sent to it as part of `x.info.probe.check` message. In case both the new member and the existing contact are the same user (they would receive both the probe and its hash), the contact would send back the original probe as part of `x.info.probe.ok` message via the previously existing contact connection – proving to the sender that this new member and the existing contact are the same user, in which case the sender SHOULD merge these two contacts. +As there are no globally unique user identifiers, when the contact a user is already connected to is added to the group by some other group member, this contact will be added to user's list of contacts as a new contact. To allow merging such contacts, "a probe" (random base64url-encoded 32 bytes) SHOULD be sent to all new members as part of `x.info.probe` message and, in case there is a contact with the same profile, the hash of the probe MAY be sent to it as part of `x.info.probe.check` message. In case both the new member and the existing contact are the same user (they would receive both the probe and its hash), the contact would send back the original probe as part of `x.info.probe.ok` message via the previously existing contact connection – proving to the sender that this new member and the existing contact are the same user, in which case the sender SHOULD merge these two contacts. Sending clients MAY disable this functionality, and receiving clients MAY ignore probe messages. @@ -155,6 +176,8 @@ Message content can be one of four types: - `text` - no file attachment is expected for this format, `text` property MUST be non-empty. - `file` - attached file is required, `text` property MAY be empty. - `image` - attached file is required, `text` property MAY be empty. +- `video` - attached file is required, `text` property MAY be empty. +- `voice` - attached file is required, `text` property MAY be empty. - `link` - no file attachment is expected, `text` property MUST be non-empty. `preview` property contains information about link preview. See `/definition/msgContent` in [JTD schema](./simplex-chat.schema.json) for message container format. @@ -181,25 +204,29 @@ File attachment can optionally include connection address to receive the file - `x.file.cancel` message is sent to notify the recipient that sending of the file was cancelled. It is sent in response to accepting the file with `x.file.acpt.inv` message. It is sent in the same connection where the file was offered. +`x.msg.file.descr` message is used to send XFTP file description. File descriptions that don't fit into a single chat protocol message are sent in parts, with messages including part number (`fileDescrPartNo`) and description completion marker (`fileDescrComplete`). Recipient client accumulates description parts and starts file download upon completing file description. + ## Sub-protocol for chat groups ### Decentralized design for chat groups -SimpleX Chat groups are fully decentralized and do not have any globally unique group identifiers - they are only defined on client devices as a group profile and a set of bi-directional SimpleX connections with other group members. When a new member accepts group invitation, the inviting member introduces a new member to all existing members and forwards the connection addresses so that they can establish direct and group member connections. +SimpleX Chat groups are fully decentralized and do not have any globally unique group identifiers - they are only defined on client devices as a group profile and a set of bi-directional SimpleX connections with other group members. When a new member accepts group invitation or joins via group link, the inviting member introduces a new member to all existing members and forwards the connection addresses so that they can establish direct and group member connections. There is a possibility of the attack here: as the introducing member forwards the addresses, they can substitute them with other addresses, performing MITM attack on the communication between existing and introduced members - this is similar to the communication operator being able to perform MITM on any connection between the users. To mitigate this attack this group sub-protocol will be extended to allow validating security of the connection by sending connection verification out-of-band. -Clients are RECOMMENDED to indicate in the UI whether the connection to a group member or contact was made directly or via annother user. +Clients are RECOMMENDED to indicate in the UI whether the connection to a group member or contact was made directly or via another user. Each member in the group is identified by a group-wide unique identifier used by all members in the group. This is to allow referencing members in the messages and to allow group message integrity validation. The diagram below shows the sequence of messages sent between the users' clients to add the new member to the group. +While introduced members establish connection inside group, inviting member forwards messages between them by sending `x.grp.msg.forward` messages. When introduced members finalize connection, they notify inviting member to stop forwarding via `x.grp.mem.con` message. + ![Adding member to the group](./diagrams/group.svg) ### Member roles -Currently members can have one of three roles - `owner`, `admin` and `member`. The user that created the group is self-assigned owner role, the new members are assigned role by the member who adds them - only `owner` and `admin` members can add new members; only `owner` members can add members with `owner` role. +Currently members can have one of three roles - `owner`, `admin`, `member` and `observer`. The user that created the group is self-assigned owner role, the new members are assigned role by the member who adds them - only `owner` and `admin` members can add new members; only `owner` members can add members with `owner` role. `Observer` members only receive messages and aren't allowed to send messages. ### Messages to manage groups and add members @@ -207,6 +234,10 @@ Currently members can have one of three roles - `owner`, `admin` and `member`. T `x.grp.acpt` message is sent as part of group member connection handshake, only to the inviting user. +`x.grp.link.inv` message is sent as part of connection handshake to member joining via group link, and contains group profile and initial information about inviting and joining member. + +`x.grp.link.mem` message is sent as part of connection handshake to member joining via group link, and contains remaining information about inviting member. + `x.grp.mem.new` message is sent by the inviting user to all connected members (and scheduled as pending to all announced but not yet connected members) to announce a new member to the existing members. This message MUST only be sent by members with `admin` or `owner` role. Receiving clients MUST ignore this message if it is received from member with `member` role. `x.grp.mem.intro` messages are sent by the inviting user to the invited member, via their group member connection, one message for each existing member. When this message is sent by any other member than the one who invited the recipient it MUST be ignored. @@ -219,6 +250,10 @@ Currently members can have one of three roles - `owner`, `admin` and `member`. T `x.grp.mem.role` message is sent to update group member role - it is sent to all members by the member who updated the role of the member referenced in this message. This message MUST only be sent by members with `admin` or `owner` role. Receiving clients MUST ignore this message if it is received from member with role less than `admin`. +`x.grp.mem.restrict` message is sent to group members to communicate group member restrictions, such as member being blocked for sending messages. + +`x.grp.mem.con` message is sent by members connecting inside group to inviting member, to notify the inviting member they have completed the connection and no longer require forwarding messages between them. + `x.grp.mem.del` message is sent to delete a member - it is sent to all members by the member who deletes the member referenced in this message. This message MUST only be sent by members with `admin` or `owner` role. Receiving clients MUST ignore this message if it is received from member with `member` role. `x.grp.leave` message is sent to all members by the member leaving the group. If the only group `owner` leaves the group, it will not be possible to delete it with `x.grp.del` message - but all members can still leave the group with `x.grp.leave` message and then delete a local copy of the group. @@ -227,6 +262,10 @@ Currently members can have one of three roles - `owner`, `admin` and `member`. T `x.grp.info` message is sent to all members by the member who updated group profile. Only group owners can update group profiles. Clients MAY implement some conflict resolution strategy - it is currently not implemented by SimpleX Chat client. This message MUST only be sent by members with `owner` role. Receiving clients MUST ignore this message if it is received from member other than with `owner` role. +`x.grp.direct.inv` message is sent to a group member to propose establishing a direct connection between members, thus creating a contact with another member. + +`x.grp.msg.forward` message is sent by inviting member to forward messages between introduced members, while they are connecting. + ## Sub-protocol for WebRTC audio/video calls This sub-protocol is used to send call invitations and to negotiate end-to-end encryption keys and pass WebRTC signalling information. @@ -240,3 +279,66 @@ These message are used for WebRTC calls: 3. `x.call.answer`: to continue with call connection the initiating clients must reply with `x.call.answer` message. This message contains WebRTC answer and collected ICE candidates. Additional ICE candidates can be sent in `x.call.extra` message. 4. `x.call.end` message is sent to notify the other party that the call is terminated. + +## Threat model + +This threat model compliments SMP, XFTP, push notifications and XRCP protocols threat models: + +- [SimpleX Messaging Protocol threat model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/overview-tjr.md#threat-model); +- [SimpleX File Transfer Protocol threat model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/xftp.md#threat-model); +- [Push notifications threat model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/push-notifications.md#threat-model); +- [SimpleX Remote Control Protocol threat model](https://github.com/simplex-chat/simplexmq/blob/master/protocol/xrcp.md#threat-model). + +#### A user's contact + +*can:* + +- send messages prohibited by user's preferences or otherwise act non-compliantly with user's preferences (for example, if message with updated preferences was lost or failed to be processed, or with modified client), in which case user client should treat such messages and actions as prohibited. + +- by exchanging special messages with user's client, match user's contact with existing group members and/or contacts that have identical user profile (see [Probing for duplicate contacts](#probing-for-duplicate-contacts)). + +- identify that and when a user is using SimpleX, in case user has delivery receipts enabled, or based on other automated client responses. + +*cannot:* + +- match user's contact with existing group members and/or contacts with different or with incognito profiles. + +- match user's contact without communicating with the user's client. + +#### A group member + +*can:* + +- send messages prohibited by group's preferences and member restrictions or otherwise act non-compliantly with preferences and restrictions (for example, if decentralized group state diverged, or with modified client), in which case user client should treat such messages and actions as prohibited. + +- create a direct contact with a user if group permissions allow it. + +- by exchanging special messages with user's client, match user's group member record with the existing group members and/or contacts that have identical user profile. + +- undetectably send different messages to different group members, or selectively send messages to some members and not send to others. + +- identify that and when a user is using SimpleX, in case user has delivery receipts enabled, or based on other automated client responses. + +- join the same group several times, from the same or from different user profile, and pretend to be different members. + +*cannot:* + +- match user's contact with existing group members and/or contacts with different or with incognito profiles. + +- match user's group member record with existing group members and/or contacts without communication of user's client. + +- determine whether two group members with different or with incognito profiles are the same user. + +#### A group admin + +*can:* + +- carry out MITM attack between user and other group member(s) when forwarding invitations for group connections (user can detect such attack by verifying connection security codes out-of-band). + +- undetectably forward different messages to different group members, selectively adding, modifying, and dropping forwarded messages. + +- disrupt decentralized group state by sending different messages that change group state (such as adding or removing members, member role changes, etc.) to different group members, or sending such messages selectively. + +*cannot:* + +- prove that two group members with incognito profiles is the same user. diff --git a/docs/protocol/simplex-chat.schema.json b/docs/protocol/simplex-chat.schema.json index a9738190bd..2e94a4f2c2 100644 --- a/docs/protocol/simplex-chat.schema.json +++ b/docs/protocol/simplex-chat.schema.json @@ -8,7 +8,7 @@ "displayName": { "type": "string", "metadata": { - "format": "non-empty string without spaces, the first character must not be # or @" + "format": "non-empty string, the first character must not be # or @" } }, "fullName": {"type": "string"} @@ -19,6 +19,39 @@ "metadata": { "format": "data URI format for base64 encoded image" } + }, + "contactLink": {"ref": "connReqUri"}, + "preferences": { + "type": "string", + "metadata": { + "format": "JSON encoded user preferences" + } + } + }, + "additionalProperties": true + }, + "groupProfile": { + "properties": { + "displayName": { + "type": "string", + "metadata": { + "format": "non-empty string, the first character must not be # or @" + } + }, + "fullName": {"type": "string"} + }, + "optionalProperties": { + "image": { + "type": "string", + "metadata": { + "format": "data URI format for base64 encoded image" + } + }, + "groupPreferences": { + "type": "string", + "metadata": { + "format": "JSON encoded user preferences" + } } }, "additionalProperties": true @@ -29,6 +62,8 @@ }, "optionalProperties": { "file": {"ref": "fileInvitation"}, + "ttl": {"type": "integer"}, + "live": {"type": "boolean"}, "quote": { "properties": { "msgRef": {"ref": "msgRef"}, @@ -56,17 +91,47 @@ } }, "image": { - "text": {"type": "string", "metadata": {"comment": "can be empty"}}, - "image": {"ref": "base64url"} + "properties": { + "text": {"type": "string", "metadata": {"comment": "can be empty"}}, + "image": {"ref": "base64url"} + } + }, + "video": { + "properties": { + "text": {"type": "string", "metadata": {"comment": "can be empty"}}, + "image": {"ref": "base64url"}, + "duration": {"type": "integer"} + } + }, + "voice": { + "properties": { + "text": {"type": "string", "metadata": {"comment": "can be empty"}}, + "duration": {"type": "integer"} + } }, "file": { - "text": {"type": "string", "metadata": {"comment": "can be empty"}} + "properties": { + "text": {"type": "string", "metadata": {"comment": "can be empty"}} + } } }, "metadata": { "comment": "it is RECOMMENDED that the clients support other values in `type` properties showing them as text messages in case `text` property is present" } }, + "msgReaction" : { + "discriminator": "type", + "mapping": { + "emoji": { + "properties": { + "emoji": { + "type": "string", + "metadata": {"comment": "emoji character"} + } + } + } + } + }, "msgRef": { "properties": { "msgId": {"ref": "base64url"}, @@ -91,7 +156,31 @@ "fileSize": {"type": "uint32"} }, "optionalProperties": { - "fileConnReq": {"ref": "connReqUri"} + "fileDigest": {"ref": "base64url"}, + "fileConnReq": {"ref": "connReqUri"}, + "fileDescr": {"ref": "fileDescription"} + } + }, + "fileDescription": { + "properties": { + "fileDescrText": { + "type": "string", + "metadata": { + "format": "XFTP file description part text" + } + }, + "fileDescrPartNo": { + "type": "integer", + "metadata": { + "format": "XFTP file description part number" + } + }, + "fileDescrComplete": { + "type": "boolean", + "metadata": { + "format": "XFTP file description completion marker" + } + } } }, "linkPreview": { @@ -100,6 +189,21 @@ "title": {"type": "string"}, "description": {"type": "string"}, "image": {"ref": "base64url"} + }, + "optionalProperties": { + "content": {"ref": "linkContent"} + } + }, + "linkContent": { + "discriminator": "type", + "mapping": { + "page": {}, + "image": {}, + "video": { + "optionalProperties": { + "duration": {"type": "integer"} + } + } } }, "groupInvitation": { @@ -107,15 +211,27 @@ "fromMember": {"ref": "memberIdRole"}, "invitedMember": {"ref": "memberIdRole"}, "connRequest": {"ref": "connReqUri"}, - "groupProfile": {"ref": "profile"} + "groupProfile": {"ref": "groupProfile"} }, "optionalProperties": { "groupLinkId": {"ref": "base64url"}, + "groupSize": {"type": "integer"}, "metadata": { - "comment": "used to identify invitation via group link" + "comment": "groupLinkId is used to identify invitation via group link" } } }, + "groupLinkInvitation": { + "properties": { + "fromMember": {"ref": "memberIdRole"}, + "fromMemberName": {"type": "string"}, + "invitedMember": {"ref": "memberIdRole"}, + "groupProfile": {"ref": "groupProfile"} + }, + "optionalProperties": { + "groupSize": {"type": "integer"} + } + }, "memberIdRole": { "properties": { "memberId": {"ref": "base64url"}, @@ -127,16 +243,35 @@ "memberId": {"ref": "base64url"}, "memberRole": {"ref": "groupMemberRole"}, "profile": {"ref": "profile"} + }, + "optionalProperties": { + "v": {"ref": "chatVersionRange"} + } + }, + "memberRestrictions": { + "properties": { + "restriction": {"ref": "memberRestrictionStatus"} + } + }, + "memberRestrictionStatus": { + "enum": ["blocked", "unrestricted"] + }, + "chatVersionRange": { + "type": "string", + "metadata": { + "format": "chat version range string encoded as `-`, or as `` if min = max" } }, "introInvitation": { "properties": { - "groupConnReq": {"ref": "connReqUri"}, + "groupConnReq": {"ref": "connReqUri"} + }, + "optionalProperties": { "directConnReq": {"ref": "connReqUri"} } }, "groupMemberRole": { - "enum": ["author", "member", "admin", "owner"] + "enum": ["observer", "author", "member", "admin", "owner"] }, "callInvitation": { "properties": { @@ -257,6 +392,17 @@ "params": {"ref": "msgContainer"} } }, + "x.msg.file.descr": { + "properties": { + "msgId": {"ref": "base64url"}, + "params": { + "properties": { + "msgId": {"ref": "base64url"}, + "fileDescr": {"ref": "fileDescription"} + } + } + } + }, "x.msg.update": { "properties": { "msgId": {"ref": "base64url"}, @@ -264,6 +410,10 @@ "properties": { "msgId": {"ref": "base64url"}, "content": {"ref": "msgContent"} + }, + "optionalProperties": { + "ttl": {"type": "integer"}, + "live": {"type": "boolean"} } } } @@ -274,6 +424,24 @@ "params": { "properties": { "msgId": {"ref": "base64url"} + }, + "optionalProperties": { + "memberId": {"ref": "base64url"} + } + } + } + }, + "x.msg.react": { + "properties": { + "msgId": {"ref": "base64url"}, + "params": { + "properties": { + "msgId": {"ref": "base64url"}, + "reaction": {"ref": "msgReaction"}, + "add": {"type": "boolean"} + }, + "optionalProperties": { + "memberId": {"ref": "base64url"} } } } @@ -294,8 +462,10 @@ "params": { "properties": { "msgId": {"ref": "base64url"}, - "fileConnReq": {"ref": "connReqUri"}, "fileName": {"type": "string"} + }, + "optionalProperties": { + "fileConnReq": {"ref": "connReqUri"} } } } @@ -310,6 +480,14 @@ } } }, + "x.direct.del": { + "properties": { + "msgId": {"ref": "base64url"}, + "params": { + "properties": {} + } + } + }, "x.grp.inv": { "properties": { "msgId": {"ref": "base64url"}, @@ -330,6 +508,26 @@ } } }, + "x.grp.link.inv": { + "properties": { + "msgId": {"ref": "base64url"}, + "params": { + "properties": { + "groupLinkInvitation": {"ref": "groupLinkInvitation"} + } + } + } + }, + "x.grp.link.mem": { + "properties": { + "msgId": {"ref": "base64url"}, + "params": { + "properties": { + "profile": {"ref": "profile"} + } + } + } + }, "x.grp.mem.new": { "properties": { "msgId": {"ref": "base64url"}, @@ -346,6 +544,9 @@ "params": { "properties": { "memberInfo": {"ref": "memberInfo"} + }, + "optionalProperties": { + "memberRestrictions": {"ref": "memberRestrictions"} } } } @@ -394,6 +595,27 @@ } } }, + "x.grp.mem.restrict": { + "properties": { + "msgId": {"ref": "base64url"}, + "params": { + "properties": { + "memberId": {"ref": "base64url"}, + "memberRestrictions": {"ref": "memberRestrictions"} + } + } + } + }, + "x.grp.mem.con": { + "properties": { + "msgId": {"ref": "base64url"}, + "params": { + "properties": { + "memberId": {"ref": "base64url"} + } + } + } + }, "x.grp.mem.del": { "properties": { "msgId": {"ref": "base64url"}, @@ -425,7 +647,42 @@ "msgId": {"ref": "base64url"}, "params": { "properties": { - "groupProfile": {"ref": "profile"} + "groupProfile": {"ref": "groupProfile"} + } + } + } + }, + "x.grp.direct.inv": { + "properties": { + "msgId": {"ref": "base64url"}, + "params": { + "properties": { + "connReq": {"ref": "connReqUri"} + }, + "optionalProperties": { + "content": {"ref": "msgContent"} + } + } + } + }, + "x.grp.msg.forward": { + "properties": { + "msgId": {"ref": "base64url"}, + "params": { + "properties": { + "memberId": {"ref": "base64url"}, + "msg": { + "type": "string", + "metadata": { + "format": "JSON encoded chat message" + } + }, + "msgTs": { + "type": "string", + "metadata": { + "format": "ISO8601 UTC time of the message" + } + } } } } @@ -436,7 +693,7 @@ "params": { "properties": { "callId": {"ref": "base64url"}, - "invitation": {} + "invitation": {"ref": "callInvitation"} } } } diff --git a/docs/rfcs/2024-05-17-flexible-user-records.md b/docs/rfcs/2024-05-17-flexible-user-records.md new file mode 100644 index 0000000000..da56fe75b6 --- /dev/null +++ b/docs/rfcs/2024-05-17-flexible-user-records.md @@ -0,0 +1,158 @@ +# Flexible user records + +## Problem + +Currently user records work as rigid containers for conversations. New conversations can be created only for an active user. Users want to be able to select a user record for a new conversation right at the connection screen (i.e. when scanning QR code or pasting a link). A similar problem was previously solved regarding Incognito mode - initially it required changing a global setting, then it was moved as a toggle to the connection screen, then it was reworked to be offered after scanning the link. + +## Solution + +## UI + +Connection UI would offer to join connections as other users. + +Current options when joining: + +``` +- "Use current profile" +- "Use new incognito profile" +``` + +Will change to: + +If there're only 2 users: + +``` +- "Use current profile" +- "Use new incognito profile" +- "Use profile" +``` + +If there's more than 2 users: + +``` +- "Use current profile" +- "Use new incognito profile" +- "Use other profile" (opens sheet with list of users) +``` + +Things to consider: + +- hidden users should be excluded from this selection +- choosing different user should make it active and open chat list for this user, then create pending connection there +- should connection plan api take into account all users? + +## Other ideas + +### Incognito chats in a separate user "profile" + +Having incognito conversations interleaved with "main profile" conversations is another point of confusion, as incognito profile is offered as an alternative to main profile, but conversations are still "attached" to it and inherit some of its settings (e.g. servers). We could unite all incognito chats under a new dummy "incognito" user profile. It would have a special representation in UI, not as a regular user profile, but as an incognito mode. It would allow customizing a specific theme, servers, preferences and other settings for all incognito chats. + +``` haskell +-- Types + +data User = User + { ... + incognitoUser :: Bool, + ... + } + +-- Controller + +APIConnect UserId IncognitoEnabled (Maybe AConnectionRequestUri) +-- -> changed to -> +APIConnect UserId (Maybe AConnectionRequestUri) +-- since conversation being incognito would be defined by user +``` + +Considerations / problems: + +- migration of existing incognito conversations is non trivial, as it requires migrating both agent and chat connection records to a new user record, in addition to migrating other chat entities. + - some programmatic one-time migration on chat start would be required, e.g.: + - create new user in agent; + - create new user in chat with incognito_user set to true; + - for each (non hidden, see below) user read chat list, update user_id for all chat entities: + - contacts, + - groups, + - group_members, + - contact_profiles, + - chat_items, etc. + - this new user servers would include all servers from users that had incognito conversations (?). + - this seems quite complex error-prone. +- it may be more pragmatic to not migrate old conversations to new user record, but instead filter them out in their respective user chat lists, and filter them in incognito user profile. + - in this case "legacy" incognito conversations would be marked by their user record (avatar/name inside chat list; note inside chat view saying that such and such settings are inherited from user x). + - we could still make a hack to apply same incognito user theme for "legacy" incognito conversations. +- on the other hand the second approach requires loading all chats for the incognito user (this may be related to "All chats view", see below). +- when creating an incognito conversation for a hidden user, it should still be attached to that user. + - or we could create an "incognito hidden user". +- considering complexities, this all seems quite a rabbit hole and may be not worth it.. +- MVP may be to do nothing for legacy incognito contacts and just explain it in app. A-la "New incognito conversations will appear here, previously created incognito conversations will stay attached to user profiles they were created in". + +### Forward messages between users + +Should be somewhat easy in backend: + +``` haskell +APIForwardChatItem {toChatRef :: ChatRef, fromChatRef :: ChatRef, chatItemId :: ChatItemId, ttl :: Maybe Int} +-- -> changed to -> +APIForwardChatItem {toUserId :: UserId, toChatRef :: ChatRef, fromUserId :: UserId, fromChatRef :: ChatRef, chatItemId :: ChatItemId, ttl :: Maybe Int} +-- or include UserId into ChatRef +``` + +More complex in UI - requires "knowing" conversations for other / all users: +- either have all conversations for all users in model. +- or have other users expand in forward list, and request their chat lists at that point. + +### Per user network settings + +Requires changes in agent and in backend. + +In agent requires storing network settings in UserId to settings maps, similar to servers: + +``` haskell +data AgentClient = AgentClient + { ... + useNetworkConfig :: TVar (NetworkConfig, NetworkConfig), + -- -> changed to -> + useNetworkConfig :: TMap UserId (NetworkConfig, NetworkConfig), -- slow/fast per user + } +``` + +Chat APIs: + +``` haskell +APISetNetworkConfig NetworkConfig +APIGetNetworkConfig +-- -> changed to -> +APISetNetworkConfig UserId NetworkConfig +APIGetNetworkConfig UserId +``` + +### All chats in united list + +We could add a view where chats for all users could be viewed in a single list / filtered by users. + +We could: +- either always load all chats for all users (see Incognito user, Forward between users above) and have a single api, then filter conversations by user in UI +- or modify/duplicate APIGetChats api and queries. +- in any case may require some rework of pagination queries, as indexes might become inefficient. + +``` haskell +APIGetChats {userId :: UserId, pendingConnections :: Bool, pagination :: PaginationByTime, query :: ChatListQuery} +-- -> changed to -> +APIGetChats {pendingConnections :: Bool, pagination :: PaginationByTime, query :: ChatListQuery} +-- or +APIGetChats {userId :: Maybe UserId, pendingConnections :: Bool, pagination :: PaginationByTime, query :: ChatListQuery} +-- with Nothing meaning all +-- or +APIGetChats {userIds :: [UserId], pendingConnections :: Bool, pagination :: PaginationByTime, query :: ChatListQuery} +-- to filter for multiple users? or only filter in UI? +``` + +### Move chats between user profiles + +This would further deepen the illusion of user record being a conversation tag rather than a rigid container for conversations. + +There are some of the same issues as described in migration of incognito conversation settings. + +"Moved" conversation would still be using servers that were configured for the previous user. +Perhaps it makes more sense to implement after automated queue rotation. diff --git a/docs/rfcs/2024-06-17-agent-stats-persistence.md b/docs/rfcs/2024-06-17-agent-stats-persistence.md new file mode 100644 index 0000000000..2f5d641769 --- /dev/null +++ b/docs/rfcs/2024-06-17-agent-stats-persistence.md @@ -0,0 +1,87 @@ +# Agent stats persistence + +## Problem + +State/state tracked in agent are lost on app restart, which makes it difficult to debug user bugs. + +## Solution + +Persist stats between sessions. + +App terminal signals may vary per platform / be absent (?) -> persist stats periodically. + +Stats would have `` key, so we don't want to store them in a plaintext file to not leak used servers locally -> persist in encrypted db. + +There's couple of orthogonal design decision to be made: +- persist in chat or in agent db + - pros for chat: + - possibly less contention for db than agent + - pros for agent: + - no unnecessary back and forth, especially if agent starts accumulating from past sessions and has to be parameterized with past stats (see below) +- agent to start accumulating from past sessions stats, or keep past separately and only accumulate for current session from zeros + - pros for accumulating from past sessions: + - easier to maintain stats - e.g. user deletion has to remove keys, which is more convoluted if past stats are not stored in memory + - simpler UI - overall stats, no differentiation for past/current session (or less logic in backend preparing presentation data) + - pros for accumulating from zeros: + - simpler start logic - no need to restore stats from agent db / pass initial stats from chat db + - can differentiate between past sessions and current session stats in UI + +### Option 1 - Persist in chat db, agent to track only current session + +- Chat stores stats in such table: + +```sql +CREATE TABLE agent_stats( + agent_stats_id INTEGER PRIMARY KEY, -- dummy id, there will only be one record + past_stats TEXT, -- accumulated from previous sessions + session_stats TEXT, -- current session + past_started_at TEXT NOT NULL DEFAULT(datetime('now')), -- starting point of tracking stats, reset on stats reset + session_started_at TEXT NOT NULL DEFAULT(datetime('now')), -- starting point of current session + session_updated_at TEXT NOT NULL DEFAULT(datetime('now')) -- last update of current session stats (periodic, frequent updates) +); +``` + +- Chat periodically calls getAgentServersStats api and updates `session_stats`. + - interval? should be short to not lose too much data, 5-30 seconds? +- On start `session_stats` are accumulated into `past_stats` and set to null. +- On user deletion, agent updates current session stats in memory (removes keys), chat has to do same for both stats fields in db. + - other cases where stats have to be manipulated in similar way? + +### Option 2 - Persist in chat db, agent to accumulate stats from past sessions + +- Table is only used for persistence of overall stats: + +```sql +CREATE TABLE agent_stats( + agent_stats_id INTEGER PRIMARY KEY, -- dummy id, there will only be one record + agent_stats TEXT, -- overall stats - past and session + started_tracking_at TEXT NOT NULL DEFAULT(datetime('now')), -- starting point of tracking stats, reset on stats reset + updated_at TEXT NOT NULL DEFAULT(datetime('now')) +); +``` + +- Chat to parameterize creation of agent client with initial stats. + +### Option 3 - Persist in agent db, agent to differentiate past stats and session stats + +- Table in agent db similar to option 1. +- Agent is responsible for periodic updates in session, as well as accumulating into "past" and resetting session stats on start. +- Agent only communicates stats to chat on request. +- On user deletion agent is fully responsible for maintaining both in-memory session stats, and updating db records. + +### Option 4 - Persist in agent db, agent to accumulate stats from past sessions + +- Table in agent db similar to option 2. +- On start agent restores initial stats into memory by itself. +- Since all stats are in memory, on user deletion it's enough to update in memory without updating db. + - there is a race possible where agent crashes after updating stats (removing user keys) in memory before database stats have been overwritten by a periodic update, so it may be better to immediately overwrite and not wait for periodic update. + - still at least there's at least no additional logic to update past stats. + +### Other considerations + +Why is it important to timely remove user keys from past stats? +- stats not being saved for past users: + - important both privacy-wise and to not cause confusion when showing "All" stats (e.g. user summing up across users stats would have smaller total than total stats). + - to avoid accidentally mixing up with newer users. + - though we do have an AUTOINCREMENT user_id in agent so probably it wouldn't be a problem. +- on the other hand maybe we don't want to "forget" stats on user deletion so that stats would reflect networking more accurately? diff --git a/docs/rfcs/2024-07-09-group-snd-status.md b/docs/rfcs/2024-07-09-group-snd-status.md new file mode 100644 index 0000000000..43f491880d --- /dev/null +++ b/docs/rfcs/2024-07-09-group-snd-status.md @@ -0,0 +1,47 @@ +# Group messages sending status + +## Problem + +Currently in UI chat item info: +- There's no differentiation between sent messages and pending messages. +- There's no differentiation between pending messages reasons (establishing connection or member inactivity). + - Since the former is usually not a case due to group forwarding, this can be ignored. +- Messages to be forwarded by admin are not accounted. + +## Solution + +Differentiate new statuses for group sending in chat item info: +- forwarded +- inactive / pending + +Option 1 is to add statuses to CIStatus / ACIStatus types. + +Pros: +- simple. + +Cons: +- further muddies type of statuses for chat item with impossible states / different dimension, as it's not applicable directly to chat item but a technicality of group sending process. + +Option 2 is to create a new type, GroupSndStatus. + +```haskell +data GroupSndStatus + = GSSNew + | GSSForwarded + | GSSInactive + | GSSSent + | GSSRcvd {msgRcptStatus :: MsgReceiptStatus} + | GSSError {agentError :: SndError} + | GSSWarning {agentError :: SndError} + | GSSInvalid {text :: Text} +``` + +Most statuses repeat CIStatus sending statuses, with addition of forwarded and inactive for group sending process. + +Pros: +- separates concerns of chat item presentation from group sending process. +- allows future extension without further muddying CIStatus types. + +Cons: +- more work. +- requires backwards compatible decoding with ACIStatus to read previous data from db. diff --git a/flake.nix b/flake.nix index 24a5062be3..64b624e674 100644 --- a/flake.nix +++ b/flake.nix @@ -309,7 +309,7 @@ packages.direct-sqlcipher.flags.commoncrypto = true; packages.entropy.flags.DoNotGetEntropy = true; packages.simplexmq.components.library.libs = pkgs.lib.mkForce [ - (pkgs.openssl.override { static = true; }) + ((pkgs.openssl.override { static = true; }).overrideDerivation (old: { CFLAGS = "-mcpu=apple-a7 -march=armv8-a+norcpc" ;})) ]; }]; }).simplex-chat.components.library.override ( diff --git a/package.yaml b/package.yaml index 96439b1633..b0732e17ee 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 5.7.5.0 +version: 6.0.3.0 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme @@ -45,7 +45,7 @@ dependencies: - stm == 2.5.* - terminal == 0.2.* - time == 1.12.* - - tls >= 1.7.0 && < 1.8 + - tls >= 1.9.0 && < 1.10 - unliftio == 0.2.* - unliftio-core == 0.2.* - zip == 2.0.* @@ -152,12 +152,31 @@ tests: ghc-options: # - -haddock - -O2 - - -Wall + - -Weverything + - -Wno-missing-exported-signatures + - -Wno-missing-import-lists + - -Wno-missed-specialisations + - -Wno-all-missed-specialisations + - -Wno-unsafe + - -Wno-safe + - -Wno-missing-local-signatures + - -Wno-missing-kind-signatures + - -Wno-missing-deriving-strategies + - -Wno-monomorphism-restriction + - -Wno-prepositive-qualified-module + - -Wno-unused-packages + - -Wno-implicit-prelude + - -Wno-missing-safe-haskell-mode + - -Wno-missing-export-lists + - -Wno-partial-fields - -Wcompat + - -Werror=incomplete-record-updates - -Werror=incomplete-patterns + - -Werror=missing-methods + - -Werror=incomplete-uni-patterns + - -Werror=tabs - -Wredundant-constraints - -Wincomplete-record-updates - - -Wincomplete-uni-patterns - -Wunused-type-patterns default-extensions: diff --git a/scripts/android/compress-and-sign-apk.sh b/scripts/android/compress-and-sign-apk.sh index e46b8a54f1..74d59203c4 100755 --- a/scripts/android/compress-and-sign-apk.sh +++ b/scripts/android/compress-and-sign-apk.sh @@ -47,7 +47,7 @@ for ORIG_NAME in "${ORIG_NAMES[@]}"; do #(cd apk && 7z a -r -mx=0 -tzip ../$ORIG_NAME resources.arsc) ALL_TOOLS=("$sdk_dir"/build-tools/*/) - BIN_DIR="${ALL_TOOLS[1]}" + BIN_DIR="${ALL_TOOLS[${#ALL_TOOLS[@]}-1]}" "$BIN_DIR"/zipalign -p -f 4 "$ORIG_NAME" "$ORIG_NAME"-2 diff --git a/scripts/desktop/build-lib-linux.sh b/scripts/desktop/build-lib-linux.sh index c8b2079bb7..3aaa9c4d17 100755 --- a/scripts/desktop/build-lib-linux.sh +++ b/scripts/desktop/build-lib-linux.sh @@ -29,6 +29,11 @@ cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN' --ghc-optio cd $BUILD_DIR/build #patchelf --add-needed libHSrts_thr-ghc${GHC_VERSION}.so libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so #patchelf --add-rpath '$ORIGIN' libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so + +# GitHub's Ubuntu 20.04 runner started to set libffi.so.7 as a dependency while Ubuntu 20.04 on user's devices may not have it +# but libffi.so.8 is shipped as an external library with other libs +patchelf --replace-needed "libffi.so.7" "libffi.so.8" libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so + mkdir deps 2> /dev/null || true ldd libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so | grep "ghc" | cut -d' ' -f 3 | xargs -I {} cp {} ./deps/ diff --git a/scripts/desktop/make-appimage-linux.sh b/scripts/desktop/make-appimage-linux.sh index 3bbdd65b49..5084a0276d 100755 --- a/scripts/desktop/make-appimage-linux.sh +++ b/scripts/desktop/make-appimage-linux.sh @@ -40,6 +40,10 @@ if [ ! -f ../appimagetool-x86_64.AppImage ]; then wget --secure-protocol=TLSv1_3 https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage -O ../appimagetool-x86_64.AppImage chmod +x ../appimagetool-x86_64.AppImage fi -../appimagetool-x86_64.AppImage . +if [ ! -f ../runtime-fuse3-x86_64 ]; then + wget --secure-protocol=TLSv1_3 https://github.com/AppImage/type2-runtime/releases/download/old/runtime-fuse3-x86_64 -O ../runtime-fuse3-x86_64 + chmod +x ../runtime-fuse3-x86_64 +fi +../appimagetool-x86_64.AppImage --runtime-file ../runtime-fuse3-x86_64 . mv *imple*.AppImage ../../ diff --git a/scripts/desktop/prepare-openssl-windows.sh b/scripts/desktop/prepare-openssl-windows.sh index 942646853f..d65d4b8e31 100644 --- a/scripts/desktop/prepare-openssl-windows.sh +++ b/scripts/desktop/prepare-openssl-windows.sh @@ -12,7 +12,7 @@ cd $root_dir if [ ! -f dist-newstyle/openssl-1.1.1w/libcrypto-1_1-x64.dll ]; then mkdir dist-newstyle 2>/dev/null || true cd dist-newstyle - curl --tlsv1.2 https://www.openssl.org/source/openssl-1.1.1w.tar.gz -o openssl.tar.gz + curl --tlsv1.2 https://www.openssl.org/source/openssl-1.1.1w.tar.gz -L -o openssl.tar.gz $WINDIR\\System32\\tar.exe -xvzf openssl.tar.gz cd openssl-1.1.1w ./Configure mingw64 diff --git a/scripts/flatpak/chat.simplex.simplex.desktop b/scripts/flatpak/chat.simplex.simplex.desktop new file mode 100644 index 0000000000..4f4cd4eece --- /dev/null +++ b/scripts/flatpak/chat.simplex.simplex.desktop @@ -0,0 +1,10 @@ +[Desktop Entry] +Type=Application +Terminal=false +Name=SimpleX Chat +Comment=A private & encrypted open-source messenger without any user IDs (not even random ones)! +Keywords=chat;message;private;secure;simplex; +Categories=Utility;Chat;InstantMessaging; +Exec=simplex %U +Icon=chat.simplex.simplex +StartupWMClass=simplex-chat diff --git a/scripts/flatpak/chat.simplex.simplex.metainfo.xml b/scripts/flatpak/chat.simplex.simplex.metainfo.xml new file mode 100644 index 0000000000..a74a7b86a0 --- /dev/null +++ b/scripts/flatpak/chat.simplex.simplex.metainfo.xml @@ -0,0 +1,118 @@ + + + chat.simplex.simplex + + SimpleX Chat + A private & encrypted open-source messenger without any user IDs (not even random)! + + + SimpleX Chat + + + MIT + AGPL-3.0-or-later + + +

SimpleX - the first messaging platform that has no user identifiers, not even random numbers!

+

Security assessment was done by Trail of Bits in November 2022.

+

SimpleX Chat features:

+
    +
  1. end-to-end encrypted messages, with editing, replies and deletion of messages.
  2. +
  3. sending end-to-end encrypted images and files.
  4. +
  5. single-use and long-term user addresses.
  6. +
  7. secret chat groups - only group members know it exists and who is the member.
  8. +
  9. end-to-end encrypted audio and video calls.
  10. +
  11. private instant notifications.
  12. +
  13. portable chat profile - you can transfer your chat contacts and history to another device (terminal or mobile).
  14. +
+

SimpleX Chat advantages:

+
    +
  1. Full privacy of your identity, profile, contacts and metadata: unlike any other existing messaging platform, SimpleX uses no phone numbers or any other identifiers assigned to the users - not even random numbers. This protects the privacy of who you are communicating with, hiding it from SimpleX platform servers and from any observers.
  2. +
  3. Complete protection against spam and abuse: as you have no identifier on SimpleX platform, you cannot be contacted unless you share a one-time invitation link or an optional temporary user address.
  4. +
  5. Full ownership, control and security of your data: SimpleX stores all user data on client devices, the messages are only held temporarily on SimpleX relay servers until they are received.
  6. +
  7. Decentralized network: you can use SimpleX with your own servers and still communicate with people using the servers that are pre-configured in the apps or any other SimpleX servers.
  8. +
+

You can connect to anybody you know via link or scan QR code (in the video call or in person) and start sending messages instantly - no emails, phone numbers or passwords needed.

+

Your profile and contacts are only stored in the app on your device - our servers do not have access to this information.

+

All messages are end-to-end encrypted using open-source double-ratchet protocol; the messages are routed via our servers using open-source SimpleX Messaging Protocol.

+
+ + + + https://github.com/simplex-chat/simplex-chat/releases/tag/v6.0.0 + +

New chat experience:

+
    +
  1. connect to your friends faster.
  2. +
  3. archive contacts to chat later.
  4. +
  5. delete up to 20 messages at once.
  6. +
  7. increase font size.
  8. +
+

New media options:

+
    +
  1. play from the chat list.
  2. +
  3. blur for better privacy.
  4. +
+

Private routing:

+
    +
  1. it protects your IP address and connections and is now enabled by default.
  2. +
+

Connection and servers information:

+
    +
  1. to control your network status and usage.
  2. +
+
+
+ + https://github.com/simplex-chat/simplex-chat/releases/tag/v5.8.2 + +

General:

+
    +
  1. missed call notification.
  2. +
  3. remove notifications of hidden/removed user profiles.
  4. +
  5. support for faster connection with the new contacts (disabled in this version).
  6. +
  7. general fixes.
  8. +
+
+
+ + +

General:

+
    +
  1. fixes in sending/receiving files.
  2. +
  3. better error reporting when connecting to desktop app.
  4. +
  5. prevent forwarding to conversations where conversation preferences do not allow message.
  6. +
+

Android and desktop apps:

+
    +
  1. support transparent theme colors for chat message bubbles.
  2. +
  3. do not reset changed network settings when switching SOCKS proxy on/off
  4. +
  5. fix swipe to reply when animation is disabled.
  6. +
  7. fix bug when duplicate group shown in the UI.
  8. +
+
+
+
+ + + + https://simplex.chat/ + https://github.com/simplex-chat/simplex-chat/issues + https://opencollective.com/simplex-chat + https://simplex.chat/docs/translations + https://simplex.chat/faq + https://github.com/simplex-chat/simplex-chat + + chat.simplex.simplex.desktop + + + #a5f0ff + #110e26 + + + + + https://simplex.chat/blog/images/simplex-desktop-light.png + + +
diff --git a/scripts/ios/import-localizations.sh b/scripts/ios/import-localizations.sh index e323865f2d..0f04cd0cde 100755 --- a/scripts/ios/import-localizations.sh +++ b/scripts/ios/import-localizations.sh @@ -12,7 +12,6 @@ for lang in "${langs[@]}"; do xcodebuild -importLocalizations \ -project ./apps/ios/SimpleX.xcodeproj \ -localizationPath ./apps/ios/SimpleX\ Localizations/$lang.xcloc \ - -disableAutomaticPackageResolution \ -skipPackageUpdates sleep 10 done diff --git a/scripts/nix/sha256map.nix b/scripts/nix/sha256map.nix index 2eca78943d..0569199515 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."0b5ab3a374ff0ed9719a77fdda03a296e49eb78b" = "1p4pqv9i1isjws037ll8nkb0lhrkqkm8ck3kvjb7akxaq43v3nl4"; + "https://github.com/simplex-chat/simplexmq.git"."56986f82c89b04beae84a61208db8b55eb0098e3" = "0vqvdnm560xrfq7kjsghdbpk67vn4hcdpp58dfqgh9l2c9f79bin"; "https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38"; "https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d"; "https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl"; diff --git a/simplex-chat.cabal b/simplex-chat.cabal index 5b1e1c8675..fed3a884ce 100644 --- a/simplex-chat.cabal +++ b/simplex-chat.cabal @@ -5,7 +5,7 @@ cabal-version: 1.12 -- see: https://github.com/sol/hpack name: simplex-chat -version: 5.7.5.0 +version: 6.0.3.0 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat @@ -142,6 +142,10 @@ library Simplex.Chat.Migrations.M20240324_custom_data Simplex.Chat.Migrations.M20240402_item_forwarded Simplex.Chat.Migrations.M20240430_ui_theme + Simplex.Chat.Migrations.M20240501_chat_deleted + Simplex.Chat.Migrations.M20240510_chat_items_via_proxy + Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays + Simplex.Chat.Migrations.M20240528_quota_err_counter Simplex.Chat.Mobile Simplex.Chat.Mobile.File Simplex.Chat.Mobile.Shared @@ -156,6 +160,7 @@ library Simplex.Chat.Remote.RevHTTP Simplex.Chat.Remote.Transport Simplex.Chat.Remote.Types + Simplex.Chat.Stats Simplex.Chat.Store Simplex.Chat.Store.AppSettings Simplex.Chat.Store.Connections @@ -187,7 +192,7 @@ library src default-extensions: StrictData - ghc-options: -O2 -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns + ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns build-depends: aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -221,7 +226,7 @@ library , stm ==2.5.* , terminal ==0.2.* , time ==1.12.* - , tls >=1.7.0 && <1.8 + , tls >=1.9.0 && <1.10 , unliftio ==0.2.* , unliftio-core ==0.2.* , zip ==2.0.* @@ -249,7 +254,7 @@ executable simplex-bot apps/simplex-bot default-extensions: StrictData - ghc-options: -O2 -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded + ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded build-depends: aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -284,7 +289,7 @@ executable simplex-bot , stm ==2.5.* , terminal ==0.2.* , time ==1.12.* - , tls >=1.7.0 && <1.8 + , tls >=1.9.0 && <1.10 , unliftio ==0.2.* , unliftio-core ==0.2.* , zip ==2.0.* @@ -312,7 +317,7 @@ executable simplex-bot-advanced apps/simplex-bot-advanced default-extensions: StrictData - ghc-options: -O2 -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded + ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded build-depends: aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -347,7 +352,7 @@ executable simplex-bot-advanced , stm ==2.5.* , terminal ==0.2.* , time ==1.12.* - , tls >=1.7.0 && <1.8 + , tls >=1.9.0 && <1.10 , unliftio ==0.2.* , unliftio-core ==0.2.* , zip ==2.0.* @@ -378,7 +383,7 @@ executable simplex-broadcast-bot Broadcast.Bot Broadcast.Options Paths_simplex_chat - ghc-options: -O2 -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded + ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded build-depends: aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -413,7 +418,7 @@ executable simplex-broadcast-bot , stm ==2.5.* , terminal ==0.2.* , time ==1.12.* - , tls >=1.7.0 && <1.8 + , tls >=1.9.0 && <1.10 , unliftio ==0.2.* , unliftio-core ==0.2.* , zip ==2.0.* @@ -442,7 +447,7 @@ executable simplex-chat apps/simplex-chat default-extensions: StrictData - ghc-options: -O2 -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded + ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded build-depends: aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -477,7 +482,7 @@ executable simplex-chat , stm ==2.5.* , terminal ==0.2.* , time ==1.12.* - , tls >=1.7.0 && <1.8 + , tls >=1.9.0 && <1.10 , unliftio ==0.2.* , unliftio-core ==0.2.* , websockets ==0.12.* @@ -512,7 +517,7 @@ executable simplex-directory-service Directory.Service Directory.Store Paths_simplex_chat - ghc-options: -O2 -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded + ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded build-depends: aeson ==2.2.* , ansi-terminal >=0.10 && <0.12 @@ -547,7 +552,7 @@ executable simplex-directory-service , stm ==2.5.* , terminal ==0.2.* , time ==1.12.* - , tls >=1.7.0 && <1.8 + , tls >=1.9.0 && <1.10 , unliftio ==0.2.* , unliftio-core ==0.2.* , zip ==2.0.* @@ -588,6 +593,7 @@ test-suite simplex-chat-test MessageBatching MobileTests ProtocolTests + RandomServers RemoteTests SchemaDump ValidNames @@ -607,7 +613,7 @@ test-suite simplex-chat-test apps/simplex-directory-service/src default-extensions: StrictData - ghc-options: -O2 -Wall -Wcompat -Werror=incomplete-patterns -Wredundant-constraints -Wincomplete-record-updates -Wincomplete-uni-patterns -Wunused-type-patterns -threaded + ghc-options: -O2 -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-missing-kind-signatures -Wno-missing-deriving-strategies -Wno-monomorphism-restriction -Wno-prepositive-qualified-module -Wno-unused-packages -Wno-implicit-prelude -Wno-missing-safe-haskell-mode -Wno-missing-export-lists -Wno-partial-fields -Wcompat -Werror=incomplete-record-updates -Werror=incomplete-patterns -Werror=missing-methods -Werror=incomplete-uni-patterns -Werror=tabs -Wredundant-constraints -Wincomplete-record-updates -Wunused-type-patterns -threaded build-depends: QuickCheck ==2.14.* , aeson ==2.2.* @@ -646,7 +652,7 @@ test-suite simplex-chat-test , stm ==2.5.* , terminal ==0.2.* , time ==1.12.* - , tls >=1.7.0 && <1.8 + , tls >=1.9.0 && <1.10 , unliftio ==0.2.* , unliftio-core ==0.2.* , zip ==2.0.* diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 55eecd499f..5899be6445 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -41,7 +41,7 @@ import Data.Fixed (div') import Data.Functor (($>)) import Data.Functor.Identity import Data.Int (Int64) -import Data.List (find, foldl', isSuffixOf, partition, sortOn) +import Data.List (find, foldl', isSuffixOf, mapAccumL, partition, sortOn) import Data.List.NonEmpty (NonEmpty (..), nonEmpty, toList, (<|)) import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) @@ -70,6 +70,7 @@ import Simplex.Chat.ProfileGenerator (generateRandomProfile) import Simplex.Chat.Protocol import Simplex.Chat.Remote import Simplex.Chat.Remote.Types +import Simplex.Chat.Stats import Simplex.Chat.Store import Simplex.Chat.Store.AppSettings import Simplex.Chat.Store.Connections @@ -83,7 +84,6 @@ import Simplex.Chat.Store.Shared import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared -import Simplex.Chat.Types.Util import Simplex.Chat.Util (encryptFile, liftIOEither, shuffle) import qualified Simplex.Chat.Util as U import Simplex.FileTransfer.Client.Main (maxFileSize, maxFileSizeHard) @@ -91,9 +91,11 @@ import Simplex.FileTransfer.Client.Presets (defaultXFTPServers) import Simplex.FileTransfer.Description (FileDescriptionURI (..), ValidFileDescription) import qualified Simplex.FileTransfer.Description as FD import Simplex.FileTransfer.Protocol (FileParty (..), FilePartyI) +import qualified Simplex.FileTransfer.Transport as XFTP +import Simplex.FileTransfer.Types (FileErrorType (..), RcvFileId, SndFileId) import Simplex.Messaging.Agent as Agent -import Simplex.Messaging.Agent.Client (AgentStatsKey (..), SubInfo (..), agentClientStore, getAgentWorkersDetails, getAgentWorkersSummary, temporaryAgentError, withLockMap) -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig) +import Simplex.Messaging.Agent.Client (SubInfo (..), agentClientStore, getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, getFastNetworkConfig, ipAddressProtected, withLockMap) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), ServerCfg (..), createAgentStore, defaultAgentConfig, enabledServerCfg, presetServerCfg) import Simplex.Messaging.Agent.Lock (withLock) import Simplex.Messaging.Agent.Protocol import qualified Simplex.Messaging.Agent.Protocol as AP (AgentErrorType (..)) @@ -101,7 +103,7 @@ import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), Migrati import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations -import Simplex.Messaging.Client (defaultNetworkConfig) +import Simplex.Messaging.Client (NetworkConfig (..), ProxyClientError (..), SocksMode (SMAlways), defaultNetworkConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.File as CF @@ -110,10 +112,11 @@ import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (base64P) -import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth, ProtocolTypeI, SProtocolType (..), SubscriptionMode (..), UserProtocol, userProtocol) +import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, XFTPServer, userProtocol) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.ServiceScheme (ServiceScheme (..)) import qualified Simplex.Messaging.TMap as TM +import Simplex.Messaging.Transport (TransportError (..)) import Simplex.Messaging.Transport.Client (defaultSocksProxy) import Simplex.Messaging.Util import Simplex.Messaging.Version @@ -121,6 +124,7 @@ import Simplex.RemoteControl.Invitation (RCInvitation (..), RCSignedInvitation ( import Simplex.RemoteControl.Types (RCCtrlAddress (..)) import System.Exit (ExitCode, exitSuccess) import System.FilePath (takeFileName, ()) +import qualified System.FilePath as FP import System.IO (Handle, IOMode (..), SeekMode (..), hFlush) import System.Random (randomRIO) import Text.Read (readMaybe) @@ -144,8 +148,10 @@ defaultChatConfig = defaultServers = DefaultAgentServers { smp = _defaultSMPServers, + useSMP = 4, ntf = _defaultNtfServers, - xftp = defaultXFTPServers, + xftp = L.map (presetServerCfg True) defaultXFTPServers, + useXFTP = L.length defaultXFTPServers, netCfg = defaultNetworkConfig }, tbqSize = 1024, @@ -169,16 +175,29 @@ defaultChatConfig = chatHooks = defaultChatHooks } -_defaultSMPServers :: NonEmpty SMPServerWithAuth +_defaultSMPServers :: NonEmpty (ServerCfg 'PSMP) _defaultSMPServers = - L.fromList - [ "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion", - "smp://SkIkI6EPd2D63F4xFKfHk7I1UGZVNn6k1QWZ5rcyr6w=@smp9.simplex.im,jssqzccmrcws6bhmn77vgmhfjmhwlyr3u7puw4erkyoosywgl67slqqd.onion", - "smp://6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE=@smp10.simplex.im,rb2pbttocvnbrngnwziclp2f4ckjq65kebafws6g4hy22cdaiv5dwjqd.onion", - "smp://1OwYGt-yqOfe2IyVHhxz3ohqo3aCCMjtB-8wn4X_aoY=@smp11.simplex.im,6ioorbm6i3yxmuoezrhjk6f6qgkc4syabh7m3so74xunb5nzr4pwgfqd.onion", - "smp://UkMFNAXLXeAAe0beCa4w6X_zp18PwxSaSjY17BKUGXQ=@smp12.simplex.im,ie42b5weq7zdkghocs3mgxdjeuycheeqqmksntj57rmejagmg4eor5yd.onion", - "smp://enEkec4hlR3UtKx2NMpOUK_K4ZuDxjWBO1d9Y4YXVaA=@smp14.simplex.im,aspkyu2sopsnizbyfabtsicikr2s4r3ti35jogbcekhm3fsoeyjvgrid.onion" - ] + L.fromList $ + map + (presetServerCfg True) + [ "smp://0YuTwO05YJWS8rkjn9eLJDjQhFKvIYd8d4xG8X1blIU=@smp8.simplex.im,beccx4yfxxbvyhqypaavemqurytl6hozr47wfc7uuecacjqdvwpw2xid.onion", + "smp://SkIkI6EPd2D63F4xFKfHk7I1UGZVNn6k1QWZ5rcyr6w=@smp9.simplex.im,jssqzccmrcws6bhmn77vgmhfjmhwlyr3u7puw4erkyoosywgl67slqqd.onion", + "smp://6iIcWT_dF2zN_w5xzZEY7HI2Prbh3ldP07YTyDexPjE=@smp10.simplex.im,rb2pbttocvnbrngnwziclp2f4ckjq65kebafws6g4hy22cdaiv5dwjqd.onion", + "smp://1OwYGt-yqOfe2IyVHhxz3ohqo3aCCMjtB-8wn4X_aoY=@smp11.simplex.im,6ioorbm6i3yxmuoezrhjk6f6qgkc4syabh7m3so74xunb5nzr4pwgfqd.onion", + "smp://UkMFNAXLXeAAe0beCa4w6X_zp18PwxSaSjY17BKUGXQ=@smp12.simplex.im,ie42b5weq7zdkghocs3mgxdjeuycheeqqmksntj57rmejagmg4eor5yd.onion", + "smp://enEkec4hlR3UtKx2NMpOUK_K4ZuDxjWBO1d9Y4YXVaA=@smp14.simplex.im,aspkyu2sopsnizbyfabtsicikr2s4r3ti35jogbcekhm3fsoeyjvgrid.onion", + "smp://h--vW7ZSkXPeOUpfxlFGgauQmXNFOzGoizak7Ult7cw=@smp15.simplex.im,oauu4bgijybyhczbnxtlggo6hiubahmeutaqineuyy23aojpih3dajad.onion", + "smp://hejn2gVIqNU6xjtGM3OwQeuk8ZEbDXVJXAlnSBJBWUA=@smp16.simplex.im,p3ktngodzi6qrf7w64mmde3syuzrv57y55hxabqcq3l5p6oi7yzze6qd.onion", + "smp://ZKe4uxF4Z_aLJJOEsC-Y6hSkXgQS5-oc442JQGkyP8M=@smp17.simplex.im,ogtwfxyi3h2h5weftjjpjmxclhb5ugufa5rcyrmg7j4xlch7qsr5nuqd.onion", + "smp://PtsqghzQKU83kYTlQ1VKg996dW4Cw4x_bvpKmiv8uns=@smp18.simplex.im,lyqpnwbs2zqfr45jqkncwpywpbtq7jrhxnib5qddtr6npjyezuwd3nqd.onion", + "smp://N_McQS3F9TGoh4ER0QstUf55kGnNSd-wXfNPZ7HukcM=@smp19.simplex.im,i53bbtoqhlc365k6kxzwdp5w3cdt433s7bwh3y32rcbml2vztiyyz5id.onion" + ] + <> map + (presetServerCfg False) + [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion", + "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion", + "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion" + ] _defaultNtfServers :: [NtfServer] _defaultNtfServers = @@ -214,11 +233,12 @@ newChatController :: ChatDatabase -> Maybe User -> ChatConfig -> ChatOpts -> Boo newChatController ChatDatabase {chatStore, agentStore} user - cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, deviceNameForRemote} - ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, networkConfig, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize} + cfg@ChatConfig {agentConfig = aCfg, defaultServers, inlineFiles, deviceNameForRemote, confirmMigrations} + ChatOpts {coreOptions = CoreChatOpts {smpServers, xftpServers, simpleNetCfg, logLevel, logConnections, logServerHosts, logFile, tbqSize, highlyAvailable, yesToUpMigrations}, deviceName, optFilesFolder, optTempDirectory, showReactions, allowInstantFiles, autoAcceptFileSize} backgroundMode = do let inlineFiles' = if allowInstantFiles || autoAcceptFileSize > 0 then inlineFiles else inlineFiles {sendChunks = 0, receiveInstant = False} - config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = configServers, inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable} + confirmMigrations' = if confirmMigrations == MCConsole && yesToUpMigrations then MCYesUp else confirmMigrations + config = cfg {logLevel, showReactions, tbqSize, subscriptionEvents = logConnections, hostEvents = logServerHosts, defaultServers = configServers, inlineFiles = inlineFiles', autoAcceptFileSize, highlyAvailable, confirmMigrations = confirmMigrations'} firstTime = dbNew chatStore currentUser <- newTVarIO user currentRemoteHost <- newTVarIO Nothing @@ -226,27 +246,28 @@ newChatController smpAgent <- getSMPAgentClient aCfg {tbqSize} servers agentStore backgroundMode agentAsync <- newTVarIO Nothing random <- liftIO C.newRandom + eventSeq <- newTVarIO 0 inputQ <- newTBQueueIO tbqSize outputQ <- newTBQueueIO tbqSize - connNetworkStatuses <- atomically TM.empty + connNetworkStatuses <- TM.emptyIO subscriptionMode <- newTVarIO SMSubscribe chatLock <- newEmptyTMVarIO - entityLocks <- atomically TM.empty + entityLocks <- TM.emptyIO sndFiles <- newTVarIO M.empty rcvFiles <- newTVarIO M.empty - currentCalls <- atomically TM.empty + currentCalls <- TM.emptyIO localDeviceName <- newTVarIO $ fromMaybe deviceNameForRemote deviceName multicastSubscribers <- newTMVarIO 0 remoteSessionSeq <- newTVarIO 0 - remoteHostSessions <- atomically TM.empty + remoteHostSessions <- TM.emptyIO remoteHostsFolder <- newTVarIO Nothing remoteCtrlSession <- newTVarIO Nothing filesFolder <- newTVarIO optFilesFolder chatStoreChanged <- newTVarIO False - expireCIThreads <- newTVarIO M.empty - expireCIFlags <- newTVarIO M.empty + expireCIThreads <- TM.emptyIO + expireCIFlags <- TM.emptyIO cleanupManagerAsync <- newTVarIO Nothing - timedItemThreads <- atomically TM.empty + timedItemThreads <- TM.emptyIO chatActivated <- newTVarIO True showLiveItems <- newTVarIO False encryptLocalFiles <- newTVarIO False @@ -263,6 +284,7 @@ newChatController chatStore, chatStoreChanged, random, + eventSeq, inputQ, outputQ, connNetworkStatuses, @@ -295,10 +317,10 @@ newChatController where configServers :: DefaultAgentServers configServers = - let DefaultAgentServers {smp = defSmp, xftp = defXftp} = defaultServers - smp' = fromMaybe defSmp (nonEmpty smpServers) - xftp' = fromMaybe defXftp (nonEmpty xftpServers) - in defaultServers {smp = smp', xftp = xftp', netCfg = networkConfig} + let DefaultAgentServers {smp = defSmp, xftp = defXftp, netCfg} = defaultServers + smp' = maybe defSmp (L.map enabledServerCfg) (nonEmpty smpServers) + xftp' = maybe defXftp (L.map enabledServerCfg) (nonEmpty xftpServers) + in defaultServers {smp = smp', xftp = xftp', netCfg = updateNetworkConfig netCfg simpleNetCfg} agentServers :: ChatConfig -> IO InitialAgentServers agentServers config@ChatConfig {defaultServers = defServers@DefaultAgentServers {ntf, netCfg}} = do users <- withTransaction chatStore getUsers @@ -306,15 +328,22 @@ newChatController xftp' <- getUserServers users SPXFTP pure InitialAgentServers {smp = smp', xftp = xftp', ntf, netCfg} where - getUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => [User] -> SProtocolType p -> IO (Map UserId (NonEmpty (ProtoServerWithAuth p))) + getUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => [User] -> SProtocolType p -> IO (Map UserId (NonEmpty (ServerCfg p))) getUserServers users protocol = case users of [] -> pure $ M.fromList [(1, cfgServers protocol defServers)] _ -> M.fromList <$> initialServers where - initialServers :: IO [(UserId, NonEmpty (ProtoServerWithAuth p))] + initialServers :: IO [(UserId, NonEmpty (ServerCfg p))] initialServers = mapM (\u -> (aUserId u,) <$> userServers u) users - userServers :: User -> IO (NonEmpty (ProtoServerWithAuth p)) - userServers user' = activeAgentServers config protocol <$> withTransaction chatStore (`getProtocolServers` user') + userServers :: User -> IO (NonEmpty (ServerCfg p)) + userServers user' = useServers config protocol <$> withTransaction chatStore (`getProtocolServers` user') + +updateNetworkConfig :: NetworkConfig -> SimpleNetCfg -> NetworkConfig +updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, smpProxyMode_, smpProxyFallback_, tcpTimeout_, logTLSErrors} = + let cfg1 = maybe cfg (\smpProxyMode -> cfg {smpProxyMode}) smpProxyMode_ + cfg2 = maybe cfg1 (\smpProxyFallback -> cfg1 {smpProxyFallback}) smpProxyFallback_ + cfg3 = maybe cfg2 (\tcpTimeout -> cfg2 {tcpTimeout, tcpConnectTimeout = (tcpTimeout * 3) `div` 2}) tcpTimeout_ + in cfg3 {socksProxy, socksMode, logTLSErrors} withChatLock :: String -> CM a -> CM a withChatLock name action = asks chatLock >>= \l -> withLock l name action @@ -350,23 +379,40 @@ withFileLock :: String -> Int64 -> CM a -> CM a withFileLock name = withEntityLock name . CLFile {-# INLINE withFileLock #-} -activeAgentServers :: UserProtocol p => ChatConfig -> SProtocolType p -> [ServerCfg p] -> NonEmpty (ProtoServerWithAuth p) -activeAgentServers ChatConfig {defaultServers} p = - fromMaybe (cfgServers p defaultServers) - . nonEmpty - . map (\ServerCfg {server} -> server) - . filter (\ServerCfg {enabled} -> enabled) +useServers :: UserProtocol p => ChatConfig -> SProtocolType p -> [ServerCfg p] -> NonEmpty (ServerCfg p) +useServers ChatConfig {defaultServers} p = fromMaybe (cfgServers p defaultServers) . nonEmpty -cfgServers :: UserProtocol p => SProtocolType p -> (DefaultAgentServers -> NonEmpty (ProtoServerWithAuth p)) +randomServers :: forall p. UserProtocol p => SProtocolType p -> ChatConfig -> IO (NonEmpty (ServerCfg p), [ServerCfg p]) +randomServers p ChatConfig {defaultServers} = do + let srvs = cfgServers p defaultServers + (enbldSrvs, dsbldSrvs) = L.partition (\ServerCfg {enabled} -> enabled) srvs + toUse = cfgServersToUse p defaultServers + if length enbldSrvs <= toUse + then pure (srvs, []) + else do + (enbldSrvs', srvsToDisable) <- splitAt toUse <$> shuffle enbldSrvs + let dsbldSrvs' = map (\srv -> (srv :: ServerCfg p) {enabled = False}) srvsToDisable + srvs' = sortOn server' $ enbldSrvs' <> dsbldSrvs' <> dsbldSrvs + pure (fromMaybe srvs $ L.nonEmpty srvs', srvs') + where + server' ServerCfg {server = ProtoServerWithAuth srv _} = srv + +cfgServers :: UserProtocol p => SProtocolType p -> DefaultAgentServers -> NonEmpty (ServerCfg p) cfgServers p DefaultAgentServers {smp, xftp} = case p of SPSMP -> smp SPXFTP -> xftp -startChatController :: Bool -> CM' (Async ()) -startChatController mainApp = do +cfgServersToUse :: UserProtocol p => SProtocolType p -> DefaultAgentServers -> Int +cfgServersToUse p DefaultAgentServers {useSMP, useXFTP} = case p of + SPSMP -> useSMP + SPXFTP -> useXFTP + +-- enableSndFiles has no effect when mainApp is True +startChatController :: Bool -> Bool -> CM' (Async ()) +startChatController mainApp enableSndFiles = do asks smpAgent >>= liftIO . resumeAgentClient unless mainApp $ chatWriteVar' subscriptionMode SMOnlyCreate - users <- fromRight [] <$> runExceptT (withStore' getUsers) + users <- fromRight [] <$> runExceptT (withFastStore' getUsers) restoreCalls s <- asks agentAsync readTVarIO s >>= maybe (start s users) (pure . fst) @@ -378,15 +424,17 @@ startChatController mainApp = do then Just <$> async (subscribeUsers False users) else pure Nothing atomically . writeTVar s $ Just (a1, a2) - when mainApp $ do - startXFTP - void $ forkIO $ startFilesToReceive users - startCleanupManager - startExpireCIs users + if mainApp + then do + startXFTP xftpStartWorkers + void $ forkIO $ startFilesToReceive users + startCleanupManager + void $ forkIO $ startExpireCIs users + else when enableSndFiles $ startXFTP xftpStartSndWorkers pure a1 - startXFTP = do + startXFTP startWorkers = do tmp <- readTVarIO =<< asks tempDirectory - runExceptT (withAgent $ \a -> xftpStartWorkers a tmp) >>= \case + runExceptT (withAgent $ \a -> startWorkers a tmp) >>= \case Left e -> liftIO $ print $ "Error starting XFTP workers: " <> show e Right _ -> pure () startCleanupManager = do @@ -427,11 +475,11 @@ startReceiveUserFiles user = do filesToReceive <- withStore' (`getRcvFilesToReceive` user) forM_ filesToReceive $ \ft -> flip catchChatError (toView . CRChatError (Just user)) $ - toView =<< receiveFile' user ft Nothing Nothing + toView =<< receiveFile' user ft False Nothing Nothing restoreCalls :: CM' () restoreCalls = do - savedCalls <- fromRight [] <$> runExceptT (withStore' getCalls) + savedCalls <- fromRight [] <$> runExceptT (withFastStore' getCalls) let callsMap = M.fromList $ map (\call@Call {contactId} -> (contactId, call)) savedCalls calls <- asks currentCalls atomically $ writeTVar calls callsMap @@ -497,68 +545,70 @@ processChatCommand cmd = processChatCommand' :: VersionRangeChat -> ChatCommand -> CM ChatResponse processChatCommand' vr = \case ShowActiveUser -> withUser' $ pure . CRActiveUser - CreateActiveUser NewUser {profile, sameServers, pastTimestamp} -> do + CreateActiveUser NewUser {profile, pastTimestamp} -> do forM_ profile $ \Profile {displayName} -> checkValidName displayName p@Profile {displayName} <- liftIO $ maybe generateRandomProfile pure profile u <- asks currentUser (smp, smpServers) <- chooseServers SPSMP (xftp, xftpServers) <- chooseServers SPXFTP - users <- withStore' getUsers + users <- withFastStore' getUsers forM_ users $ \User {localDisplayName = n, activeUser, viewPwdHash} -> when (n == displayName) . throwChatError $ if activeUser || isNothing viewPwdHash then CEUserExists displayName else CEInvalidDisplayName {displayName, validName = ""} auId <- withAgent (\a -> createUser a smp xftp) ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure - user <- withStore $ \db -> createUserRecordAt db (AgentUserId auId) p True ts - when (null users) $ withStore (\db -> createContact db user simplexContactProfile) `catchChatError` \_ -> pure () - withStore $ \db -> createNoteFolder db user + user <- withFastStore $ \db -> createUserRecordAt db (AgentUserId auId) p True ts + createPresetContactCards user `catchChatError` \_ -> pure () + withFastStore $ \db -> createNoteFolder db user storeServers user smpServers storeServers user xftpServers atomically . writeTVar u $ Just user pure $ CRActiveUser user where - chooseServers :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> CM (NonEmpty (ProtoServerWithAuth p), [ServerCfg p]) - chooseServers protocol - | sameServers = - asks currentUser >>= readTVarIO >>= \case - Nothing -> throwChatError CENoActiveUser - Just user -> do - servers <- withStore' (`getProtocolServers` user) - cfg <- asks config - pure (activeAgentServers cfg protocol servers, servers) - | otherwise = do - defServers <- asks $ defaultServers . config - pure (cfgServers protocol defServers, []) + createPresetContactCards :: User -> CM () + createPresetContactCards user = + withFastStore $ \db -> do + createContact db user simplexStatusContactProfile + createContact db user simplexTeamContactProfile + chooseServers :: (ProtocolTypeI p, UserProtocol p) => SProtocolType p -> CM (NonEmpty (ServerCfg p), [ServerCfg p]) + chooseServers protocol = + asks currentUser >>= readTVarIO >>= \case + Nothing -> asks config >>= liftIO . randomServers protocol + Just user -> chosenServers =<< withFastStore' (`getProtocolServers` user) + where + chosenServers servers = do + cfg <- asks config + pure (useServers cfg protocol servers, servers) storeServers user servers = - unless (null servers) . withStore $ + unless (null servers) . withFastStore $ \db -> overwriteProtocolServers db user servers coupleDaysAgo t = (`addUTCTime` t) . fromInteger . negate . (+ (2 * day)) <$> randomRIO (0, day) day = 86400 - ListUsers -> CRUsersList <$> withStore' getUsersInfo + ListUsers -> CRUsersList <$> withFastStore' getUsersInfo APISetActiveUser userId' viewPwd_ -> do unlessM (lift chatStarted) $ throwChatError CEChatNotStarted user_ <- chatReadVar currentUser user' <- privateGetUser userId' validateUserPassword_ user_ user' viewPwd_ - withStore' (`setActiveUser` userId') + withFastStore' (`setActiveUser` userId') let user'' = user' {activeUser = True} chatWriteVar currentUser $ Just user'' pure $ CRActiveUser user'' SetActiveUser uName viewPwd_ -> do - tryChatError (withStore (`getUserIdByName` uName)) >>= \case + tryChatError (withFastStore (`getUserIdByName` uName)) >>= \case Left _ -> throwChatError CEUserUnknown Right userId -> processChatCommand $ APISetActiveUser userId viewPwd_ - SetAllContactReceipts onOff -> withUser $ \_ -> withStore' (`updateAllContactReceipts` onOff) >> ok_ + SetAllContactReceipts onOff -> withUser $ \_ -> withFastStore' (`updateAllContactReceipts` onOff) >> ok_ APISetUserContactReceipts userId' settings -> withUser $ \user -> do user' <- privateGetUser userId' validateUserPassword user user' Nothing - withStore' $ \db -> updateUserContactReceipts db user' settings + withFastStore' $ \db -> updateUserContactReceipts db user' settings ok user SetUserContactReceipts settings -> withUser $ \User {userId} -> processChatCommand $ APISetUserContactReceipts userId settings APISetUserGroupReceipts userId' settings -> withUser $ \user -> do user' <- privateGetUser userId' validateUserPassword user user' Nothing - withStore' $ \db -> updateUserGroupReceipts db user' settings + withFastStore' $ \db -> updateUserGroupReceipts db user' settings ok user SetUserGroupReceipts settings -> withUser $ \User {userId} -> processChatCommand $ APISetUserGroupReceipts userId settings APIHideUser userId' (UserPwd viewPwd) -> withUser $ \user -> do @@ -567,7 +617,7 @@ processChatCommand' vr = \case Just _ -> throwChatError $ CEUserAlreadyHidden userId' _ -> do when (T.null viewPwd) $ throwChatError $ CEEmptyUserPassword userId' - users <- withStore' getUsers + users <- withFastStore' getUsers unless (length (filter (isNothing . viewPwdHash) users) > 1) $ throwChatError $ CECantHideLastUser userId' viewPwdHash' <- hashPassword setUserPrivacy user user' {viewPwdHash = viewPwdHash', showNtfs = False} @@ -596,10 +646,11 @@ processChatCommand' vr = \case checkDeleteChatUser user' withChatLock "deleteUser" . procCmd $ deleteChatUser user' delSMPQueues DeleteUser uName delSMPQueues viewPwd_ -> withUserName uName $ \userId -> APIDeleteUser userId delSMPQueues viewPwd_ - StartChat mainApp -> withUser' $ \_ -> + StartChat {mainApp, enableSndFiles} -> withUser' $ \_ -> asks agentAsync >>= readTVarIO >>= \case Just _ -> pure CRChatRunning - _ -> checkStoreNotChanged . lift $ startChatController mainApp $> CRChatStarted + _ -> checkStoreNotChanged . lift $ startChatController mainApp enableSndFiles $> CRChatStarted + CheckChatRunning -> maybe CRChatStopped (const CRChatRunning) <$> chatReadVar agentAsync APIStopChat -> do ask >>= liftIO . stopChatController pure CRChatStopped @@ -608,7 +659,7 @@ processChatCommand' vr = \case lift $ withAgent' foregroundAgent chatWriteVar chatActivated True when restoreChat $ do - users <- withStore' getUsers + users <- withFastStore' getUsers lift $ do void . forkIO $ subscribeUsers True users void . forkIO $ startFilesToReceive users @@ -647,7 +698,7 @@ processChatCommand' vr = \case chatWriteVar sel $ Just f APISetEncryptLocalFiles on -> chatWriteVar encryptLocalFiles on >> ok_ SetContactMergeEnabled onOff -> chatWriteVar contactMergeEnabled onOff >> ok_ - APIExportArchive cfg -> checkChatStopped $ lift (exportArchive cfg) >> ok_ + APIExportArchive cfg -> checkChatStopped $ CRArchiveExported <$> lift (exportArchive cfg) ExportArchive -> do ts <- liftIO getCurrentTime let filePath = "simplex-chat." <> formatTime defaultTimeLocale "%FT%H%M%SZ" ts <> ".zip" @@ -656,8 +707,8 @@ processChatCommand' vr = \case fileErrs <- lift $ importArchive cfg setStoreChanged pure $ CRArchiveImported fileErrs - APISaveAppSettings as -> withStore' (`saveAppSettings` as) >> ok_ - APIGetAppSettings platformDefaults -> CRAppSettings <$> withStore' (`getAppSettings` platformDefaults) + APISaveAppSettings as -> withFastStore' (`saveAppSettings` as) >> ok_ + APIGetAppSettings platformDefaults -> CRAppSettings <$> withFastStore' (`getAppSettings` platformDefaults) APIDeleteStorage -> withStoreChanged deleteStorage APIStorageEncryption cfg -> withStoreChanged $ sqlCipherExport cfg TestStorageEncryption key -> sqlCipherTestKey key >> ok_ @@ -676,34 +727,31 @@ processChatCommand' vr = \case . M.assocs <$> withConnection st (readTVarIO . DB.slow) APIGetChats {userId, pendingConnections, pagination, query} -> withUserId' userId $ \user -> do - (errs, previews) <- partitionEithers <$> withStore' (\db -> getChatPreviews db vr user pendingConnections pagination query) + (errs, previews) <- partitionEithers <$> withFastStore' (\db -> getChatPreviews db vr user pendingConnections pagination query) unless (null errs) $ toView $ CRChatErrors (Just user) (map ChatErrorStore errs) pure $ CRApiChats user previews APIGetChat (ChatRef cType cId) pagination search -> withUser $ \user -> case cType of -- TODO optimize queries calculating ChatStats, currently they're disabled CTDirect -> do - directChat <- withStore (\db -> getDirectChat db vr user cId pagination search) + directChat <- withFastStore (\db -> getDirectChat db vr user cId pagination search) pure $ CRApiChat user (AChat SCTDirect directChat) CTGroup -> do - groupChat <- withStore (\db -> getGroupChat db vr user cId pagination search) + groupChat <- withFastStore (\db -> getGroupChat db vr user cId pagination search) pure $ CRApiChat user (AChat SCTGroup groupChat) CTLocal -> do - localChat <- withStore (\db -> getLocalChat db user cId pagination search) + localChat <- withFastStore (\db -> getLocalChat db user cId pagination search) pure $ CRApiChat user (AChat SCTLocal localChat) CTContactRequest -> pure $ chatCmdError (Just user) "not implemented" CTContactConnection -> pure $ chatCmdError (Just user) "not supported" APIGetChatItems pagination search -> withUser $ \user -> do - chatItems <- withStore $ \db -> getAllChatItems db vr user pagination search + chatItems <- withFastStore $ \db -> getAllChatItems db vr user pagination search pure $ CRChatItems user Nothing chatItems APIGetChatItemInfo chatRef itemId -> withUser $ \user -> do - (aci@(AChatItem cType dir _ ci), versions) <- withStore $ \db -> + (aci@(AChatItem cType dir _ ci), versions) <- withFastStore $ \db -> (,) <$> getAChatItem db vr user chatRef itemId <*> liftIO (getChatItemVersions db itemId) let itemVersions = if null versions then maybeToList $ mkItemVersion ci else versions memberDeliveryStatuses <- case (cType, dir) of - (SCTGroup, SMDSnd) -> do - withStore' (`getGroupSndStatuses` itemId) >>= \case - [] -> pure Nothing - memStatuses -> pure $ Just $ map (uncurry MemberDeliveryStatus) memStatuses + (SCTGroup, SMDSnd) -> L.nonEmpty <$> withFastStore' (`getGroupSndStatuses` itemId) _ -> pure Nothing forwardedFromChatItem <- getForwardedFromItem user ci pure $ CRChatItemInfo user aci ChatItemInfo {itemVersions, memberDeliveryStatuses, forwardedFromChatItem} @@ -711,9 +759,9 @@ processChatCommand' vr = \case getForwardedFromItem :: User -> ChatItem c d -> CM (Maybe AChatItem) getForwardedFromItem user ChatItem {meta = CIMeta {itemForwarded}} = case itemForwarded of Just (CIFFContact _ _ (Just ctId) (Just fwdItemId)) -> - Just <$> withStore (\db -> getAChatItem db vr user (ChatRef CTDirect ctId) fwdItemId) + Just <$> withFastStore (\db -> getAChatItem db vr user (ChatRef CTDirect ctId) fwdItemId) Just (CIFFGroup _ _ (Just gId) (Just fwdItemId)) -> - Just <$> withStore (\db -> getAChatItem db vr user (ChatRef CTGroup gId) fwdItemId) + Just <$> withFastStore (\db -> getAChatItem db vr user (ChatRef CTGroup gId) fwdItemId) _ -> pure Nothing APISendMessage (ChatRef cType chatId) live itemTTL cm -> withUser $ \user -> case cType of CTDirect -> @@ -729,9 +777,9 @@ processChatCommand' vr = \case createNoteFolderContentItem user folderId cm Nothing APIUpdateChatItem (ChatRef cType chatId) itemId live mc -> withUser $ \user -> case cType of CTDirect -> withContactLock "updateChatItem" chatId $ do - ct@Contact {contactId} <- withStore $ \db -> getContact db vr user chatId + ct@Contact {contactId} <- withFastStore $ \db -> getContact db vr user chatId assertDirectAllowed user MDSnd ct XMsgUpdate_ - cci <- withStore $ \db -> getDirectCIWithReactions db user ct itemId + cci <- withFastStore $ \db -> getDirectCIWithReactions db user ct itemId case cci of CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable}, content = ciContent} -> do case (ciContent, itemSharedMsgId, editable) of @@ -740,7 +788,7 @@ processChatCommand' vr = \case if changed || fromMaybe False itemLive then do (SndMessage {msgId}, _) <- sendDirectContactMessage user ct (XMsgUpdate itemSharedMId mc (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive)) - ci' <- withStore' $ \db -> do + ci' <- withFastStore' $ \db -> do currentTs <- liftIO getCurrentTime when changed $ addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc) @@ -752,34 +800,37 @@ processChatCommand' vr = \case _ -> throwChatError CEInvalidChatItemUpdate CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate CTGroup -> withGroupLock "updateChatItem" chatId $ do - Group gInfo@GroupInfo {groupId} ms <- withStore $ \db -> getGroup db vr user chatId + Group gInfo@GroupInfo {groupId, membership} ms <- withFastStore $ \db -> getGroup db vr user chatId assertUserGroupRole gInfo GRAuthor - cci <- withStore $ \db -> getGroupCIWithReactions db user gInfo itemId - case cci of - CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable}, content = ciContent} -> do - case (ciContent, itemSharedMsgId, editable) of - (CISndMsgContent oldMC, Just itemSharedMId, True) -> do - let changed = mc /= oldMC - if changed || fromMaybe False itemLive - then do - (SndMessage {msgId}, _) <- sendGroupMessage user gInfo ms (XMsgUpdate itemSharedMId mc (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive)) - ci' <- withStore' $ \db -> do - currentTs <- liftIO getCurrentTime - when changed $ - addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc) - let edited = itemLive /= Just True - updateGroupChatItem db user groupId ci (CISndMsgContent mc) edited live $ Just msgId - startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci' - pure $ CRChatItemUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci') - else pure $ CRChatItemNotChanged user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) - _ -> throwChatError CEInvalidChatItemUpdate - CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate + if prohibitedSimplexLinks gInfo membership mc + then pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (groupFeatureNameText GFSimplexLinks)) + else do + cci <- withFastStore $ \db -> getGroupCIWithReactions db user gInfo itemId + case cci of + CChatItem SMDSnd ci@ChatItem {meta = CIMeta {itemSharedMsgId, itemTimed, itemLive, editable}, content = ciContent} -> do + case (ciContent, itemSharedMsgId, editable) of + (CISndMsgContent oldMC, Just itemSharedMId, True) -> do + let changed = mc /= oldMC + if changed || fromMaybe False itemLive + then do + (SndMessage {msgId}, _) <- sendGroupMessage user gInfo ms (XMsgUpdate itemSharedMId mc (ttl' <$> itemTimed) (justTrue . (live &&) =<< itemLive)) + ci' <- withFastStore' $ \db -> do + currentTs <- liftIO getCurrentTime + when changed $ + addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc) + let edited = itemLive /= Just True + updateGroupChatItem db user groupId ci (CISndMsgContent mc) edited live $ Just msgId + startUpdatedTimedItemThread user (ChatRef CTGroup groupId) ci ci' + pure $ CRChatItemUpdated user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci') + else pure $ CRChatItemNotChanged user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) + _ -> throwChatError CEInvalidChatItemUpdate + CChatItem SMDRcv _ -> throwChatError CEInvalidChatItemUpdate CTLocal -> do - (nf@NoteFolder {noteFolderId}, cci) <- withStore $ \db -> (,) <$> getNoteFolder db user chatId <*> getLocalChatItem db user chatId itemId + (nf@NoteFolder {noteFolderId}, cci) <- withFastStore $ \db -> (,) <$> getNoteFolder db user chatId <*> getLocalChatItem db user chatId itemId case cci of CChatItem SMDSnd ci@ChatItem {content = CISndMsgContent oldMC} | mc == oldMC -> pure $ CRChatItemNotChanged user (AChatItem SCTLocal SMDSnd (LocalChat nf) ci) - | otherwise -> withStore' $ \db -> do + | otherwise -> withFastStore' $ \db -> do currentTs <- getCurrentTime addInitialAndNewCIVersions db itemId (chatItemTs' ci, oldMC) (currentTs, mc) ci' <- updateLocalChatItem' db user noteFolderId ci (CISndMsgContent mc) True @@ -787,57 +838,110 @@ processChatCommand' vr = \case _ -> throwChatError CEInvalidChatItemUpdate CTContactRequest -> pure $ chatCmdError (Just user) "not supported" CTContactConnection -> pure $ chatCmdError (Just user) "not supported" - APIDeleteChatItem (ChatRef cType chatId) itemId mode -> withUser $ \user -> case cType of + APIDeleteChatItem (ChatRef cType chatId) itemIds mode -> withUser $ \user -> case cType of CTDirect -> withContactLock "deleteChatItem" chatId $ do - (ct, CChatItem msgDir ci@ChatItem {meta = CIMeta {itemSharedMsgId, deletable}}) <- withStore $ \db -> (,) <$> getContact db vr user chatId <*> getDirectChatItem db user chatId itemId - case (mode, msgDir, itemSharedMsgId, deletable) of - (CIDMInternal, _, _, _) -> deleteDirectCI user ct ci True False - (CIDMBroadcast, SMDSnd, Just itemSharedMId, True) -> do + ct <- withStore $ \db -> getContact db vr user chatId + (errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getDirectCI db) (L.toList itemIds)) + unless (null errs) $ toView $ CRChatErrors (Just user) errs + case mode of + CIDMInternal -> deleteDirectCIs user ct items True False + CIDMBroadcast -> do + assertDeletable items assertDirectAllowed user MDSnd ct XMsgDel_ - (SndMessage {msgId}, _) <- sendDirectContactMessage user ct (XMsgDel itemSharedMId Nothing) + let msgIds = itemsMsgIds items + events = map (\msgId -> XMsgDel msgId Nothing) msgIds + forM_ (L.nonEmpty events) $ \events' -> + sendDirectContactMessages user ct events' if featureAllowed SCFFullDelete forUser ct - then deleteDirectCI user ct ci True False - else markDirectCIDeleted user ct ci msgId True =<< liftIO getCurrentTime - (CIDMBroadcast, _, _, _) -> throwChatError CEInvalidChatItemDelete + then deleteDirectCIs user ct items True False + else markDirectCIsDeleted user ct items True =<< liftIO getCurrentTime + where + getDirectCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTDirect)) + getDirectCI db itemId = runExceptT . withExceptT ChatErrorStore $ getDirectChatItem db user chatId itemId CTGroup -> withGroupLock "deleteChatItem" chatId $ do Group gInfo ms <- withStore $ \db -> getGroup db vr user chatId - CChatItem msgDir ci@ChatItem {meta = CIMeta {itemSharedMsgId, deletable}} <- withStore $ \db -> getGroupChatItem db user chatId itemId - case (mode, msgDir, itemSharedMsgId, deletable) of - (CIDMInternal, _, _, _) -> deleteGroupCI user gInfo ci True False Nothing =<< liftIO getCurrentTime - (CIDMBroadcast, SMDSnd, Just itemSharedMId, True) -> do + (errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getGroupCI db) (L.toList itemIds)) + unless (null errs) $ toView $ CRChatErrors (Just user) errs + case mode of + CIDMInternal -> deleteGroupCIs user gInfo items True False Nothing =<< liftIO getCurrentTime + CIDMBroadcast -> do + assertDeletable items assertUserGroupRole gInfo GRObserver -- can still delete messages sent earlier - (SndMessage {msgId}, _) <- sendGroupMessage user gInfo ms $ XMsgDel itemSharedMId Nothing - delGroupChatItem user gInfo ci msgId Nothing - (CIDMBroadcast, _, _, _) -> throwChatError CEInvalidChatItemDelete + let msgIds = itemsMsgIds items + events = L.nonEmpty $ map (`XMsgDel` Nothing) msgIds + mapM_ (sendGroupMessages user gInfo ms) events + delGroupChatItems user gInfo items Nothing + where + getGroupCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTGroup)) + getGroupCI db itemId = runExceptT . withExceptT ChatErrorStore $ getGroupChatItem db user chatId itemId CTLocal -> do - (nf, CChatItem _ ci) <- withStore $ \db -> (,) <$> getNoteFolder db user chatId <*> getLocalChatItem db user chatId itemId - deleteLocalCI user nf ci True False + nf <- withStore $ \db -> getNoteFolder db user chatId + (errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getLocalCI db) (L.toList itemIds)) + unless (null errs) $ toView $ CRChatErrors (Just user) errs + deleteLocalCIs user nf items True False + where + getLocalCI :: DB.Connection -> ChatItemId -> IO (Either ChatError (CChatItem 'CTLocal)) + getLocalCI db itemId = runExceptT . withExceptT ChatErrorStore $ getLocalChatItem db user chatId itemId CTContactRequest -> pure $ chatCmdError (Just user) "not supported" CTContactConnection -> pure $ chatCmdError (Just user) "not supported" - APIDeleteMemberChatItem gId mId itemId -> withUser $ \user -> withGroupLock "deleteChatItem" gId $ do + where + assertDeletable :: forall c. ChatTypeI c => [CChatItem c] -> CM () + assertDeletable items = do + currentTs <- liftIO getCurrentTime + unless (all (itemDeletable currentTs) items) $ throwChatError CEInvalidChatItemDelete + where + itemDeletable :: UTCTime -> CChatItem c -> Bool + itemDeletable currentTs (CChatItem msgDir ChatItem {meta = CIMeta {itemSharedMsgId, itemTs, itemDeleted}, content}) = + case msgDir of + -- We check with a 6 hour margin compared to CIMeta deletable to account for deletion on the border + SMDSnd -> isJust itemSharedMsgId && deletable' content itemDeleted itemTs (nominalDay + 6 * 3600) currentTs + SMDRcv -> False + itemsMsgIds :: [CChatItem c] -> [SharedMsgId] + itemsMsgIds = mapMaybe (\(CChatItem _ ChatItem {meta = CIMeta {itemSharedMsgId}}) -> itemSharedMsgId) + APIDeleteMemberChatItem gId itemIds -> withUser $ \user -> withGroupLock "deleteChatItem" gId $ do Group gInfo@GroupInfo {membership} ms <- withStore $ \db -> getGroup db vr user gId - CChatItem _ ci@ChatItem {chatDir, meta = CIMeta {itemSharedMsgId}} <- withStore $ \db -> getGroupChatItem db user gId itemId - case (chatDir, itemSharedMsgId) of - (CIGroupRcv GroupMember {groupMemberId, memberRole, memberId}, Just itemSharedMId) -> do - when (groupMemberId /= mId) $ throwChatError CEInvalidChatItemDelete - assertUserGroupRole gInfo $ max GRAdmin memberRole - (SndMessage {msgId}, _) <- sendGroupMessage user gInfo ms $ XMsgDel itemSharedMId $ Just memberId - delGroupChatItem user gInfo ci msgId (Just membership) - (_, _) -> throwChatError CEInvalidChatItemDelete + (errs, items) <- lift $ partitionEithers <$> withStoreBatch (\db -> map (getGroupCI db user) (L.toList itemIds)) + unless (null errs) $ toView $ CRChatErrors (Just user) errs + assertDeletable gInfo items + assertUserGroupRole gInfo GRAdmin + let msgMemIds = itemsMsgMemIds gInfo items + events = L.nonEmpty $ map (\(msgId, memId) -> XMsgDel msgId (Just memId)) msgMemIds + mapM_ (sendGroupMessages user gInfo ms) events + delGroupChatItems user gInfo items (Just membership) + where + getGroupCI :: DB.Connection -> User -> ChatItemId -> IO (Either ChatError (CChatItem 'CTGroup)) + getGroupCI db user itemId = runExceptT . withExceptT ChatErrorStore $ getGroupChatItem db user gId itemId + assertDeletable :: GroupInfo -> [CChatItem 'CTGroup] -> CM () + assertDeletable GroupInfo {membership = GroupMember {memberRole = membershipMemRole}} items = + unless (all itemDeletable items) $ throwChatError CEInvalidChatItemDelete + where + itemDeletable :: CChatItem 'CTGroup -> Bool + itemDeletable (CChatItem _ ChatItem {chatDir, meta = CIMeta {itemSharedMsgId}}) = + case chatDir of + CIGroupRcv GroupMember {memberRole} -> membershipMemRole >= memberRole && isJust itemSharedMsgId + CIGroupSnd -> isJust itemSharedMsgId + itemsMsgMemIds :: GroupInfo -> [CChatItem 'CTGroup] -> [(SharedMsgId, MemberId)] + itemsMsgMemIds GroupInfo {membership = GroupMember {memberId = membershipMemId}} = mapMaybe itemMsgMemIds + where + itemMsgMemIds :: CChatItem 'CTGroup -> Maybe (SharedMsgId, MemberId) + itemMsgMemIds (CChatItem _ ChatItem {chatDir, meta = CIMeta {itemSharedMsgId}}) = + join <$> forM itemSharedMsgId $ \msgId -> Just $ case chatDir of + CIGroupRcv GroupMember {memberId} -> (msgId, memberId) + CIGroupSnd -> (msgId, membershipMemId) APIChatItemReaction (ChatRef cType chatId) itemId add reaction -> withUser $ \user -> case cType of CTDirect -> withContactLock "chatItemReaction" chatId $ - withStore (\db -> (,) <$> getContact db vr user chatId <*> getDirectChatItem db user chatId itemId) >>= \case + withFastStore (\db -> (,) <$> getContact db vr user chatId <*> getDirectChatItem db user chatId itemId) >>= \case (ct, CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}}) -> do unless (featureAllowed SCFReactions forUser ct) $ throwChatError (CECommandError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFReactions)) unless (ciReactionAllowed ci) $ throwChatError (CECommandError "reaction not allowed - chat item has no content") - rs <- withStore' $ \db -> getDirectReactions db ct itemSharedMId True + rs <- withFastStore' $ \db -> getDirectReactions db ct itemSharedMId True checkReactionAllowed rs (SndMessage {msgId}, _) <- sendDirectContactMessage user ct $ XMsgReact itemSharedMId Nothing reaction add createdAt <- liftIO getCurrentTime - reactions <- withStore' $ \db -> do + reactions <- withFastStore' $ \db -> do setDirectReaction db ct itemSharedMId True reaction add msgId createdAt liftIO $ getDirectCIReactions db ct itemSharedMId let ci' = CChatItem md ci {reactions} @@ -846,18 +950,18 @@ processChatCommand' vr = \case _ -> throwChatError $ CECommandError "reaction not possible - no shared item ID" CTGroup -> withGroupLock "chatItemReaction" chatId $ - withStore (\db -> (,) <$> getGroup db vr user chatId <*> getGroupChatItem db user chatId itemId) >>= \case + withFastStore (\db -> (,) <$> getGroup db vr user chatId <*> getGroupChatItem db user chatId itemId) >>= \case (Group g@GroupInfo {membership} ms, CChatItem md ci@ChatItem {meta = CIMeta {itemSharedMsgId = Just itemSharedMId}}) -> do unless (groupFeatureAllowed SGFReactions g) $ throwChatError (CECommandError $ "feature not allowed " <> T.unpack (chatFeatureNameText CFReactions)) unless (ciReactionAllowed ci) $ throwChatError (CECommandError "reaction not allowed - chat item has no content") let GroupMember {memberId = itemMemberId} = chatItemMember g ci - rs <- withStore' $ \db -> getGroupReactions db g membership itemMemberId itemSharedMId True + rs <- withFastStore' $ \db -> getGroupReactions db g membership itemMemberId itemSharedMId True checkReactionAllowed rs (SndMessage {msgId}, _) <- sendGroupMessage user g ms (XMsgReact itemSharedMId (Just itemMemberId) reaction add) createdAt <- liftIO getCurrentTime - reactions <- withStore' $ \db -> do + reactions <- withFastStore' $ \db -> do setGroupReaction db g membership itemMemberId itemSharedMId True reaction add msgId createdAt liftIO $ getGroupCIReactions db g itemMemberId itemSharedMId let ci' = CChatItem md ci {reactions} @@ -873,15 +977,15 @@ processChatCommand' vr = \case throwChatError (CECommandError $ "reaction already " <> if add then "added" else "removed") when (add && length rs >= maxMsgReactions) $ throwChatError (CECommandError "too many reactions") - APIForwardChatItem (ChatRef toCType toChatId) (ChatRef fromCType fromChatId) itemId -> withUser $ \user -> case toCType of + APIForwardChatItem (ChatRef toCType toChatId) (ChatRef fromCType fromChatId) itemId itemTTL -> withUser $ \user -> case toCType of CTDirect -> do (cm, ciff) <- prepareForward user withContactLock "forwardChatItem, to contact" toChatId $ - sendContactContentMessage user toChatId False Nothing cm ciff + sendContactContentMessage user toChatId False itemTTL cm ciff CTGroup -> do (cm, ciff) <- prepareForward user withGroupLock "forwardChatItem, to group" toChatId $ - sendGroupContentMessage user toChatId False Nothing cm ciff + sendGroupContentMessage user toChatId False itemTTL cm ciff CTLocal -> do (cm, ciff) <- prepareForward user createNoteFolderContentItem user toChatId cm ciff @@ -891,7 +995,7 @@ processChatCommand' vr = \case prepareForward :: User -> CM (ComposedMessage, Maybe CIForwardedFrom) prepareForward user = case fromCType of CTDirect -> withContactLock "forwardChatItem, from contact" fromChatId $ do - (ct, CChatItem _ ci) <- withStore $ \db -> do + (ct, CChatItem _ ci) <- withFastStore $ \db -> do ct <- getContact db vr user fromChatId cci <- getDirectChatItem db user fromChatId itemId pure (ct, cci) @@ -905,7 +1009,7 @@ processChatCommand' vr = \case | localAlias /= "" = localAlias | otherwise = displayName CTGroup -> withGroupLock "forwardChatItem, from group" fromChatId $ do - (gInfo, CChatItem _ ci) <- withStore $ \db -> do + (gInfo, CChatItem _ ci) <- withFastStore $ \db -> do gInfo <- getGroupInfo db vr user fromChatId cci <- getGroupChatItem db user fromChatId itemId pure (gInfo, cci) @@ -917,7 +1021,7 @@ processChatCommand' vr = \case forwardName :: GroupInfo -> ContactName forwardName GroupInfo {groupProfile = GroupProfile {displayName}} = displayName CTLocal -> do - (CChatItem _ ci) <- withStore $ \db -> getLocalChatItem db user fromChatId itemId + (CChatItem _ ci) <- withFastStore $ \db -> getLocalChatItem db user fromChatId itemId (mc, _) <- forwardMC ci file <- forwardCryptoFile ci let ciff = forwardCIFF ci Nothing @@ -978,72 +1082,91 @@ processChatCommand' vr = \case when (B.length ch /= chSize') $ throwError $ CF.FTCEFileIOError "encrypting file: unexpected EOF" liftIO . CF.hPut w $ LB.fromStrict ch when (size' > 0) $ copyChunks r w size' - APIUserRead userId -> withUserId userId $ \user -> withStore' (`setUserChatsRead` user) >> ok user + APIUserRead userId -> withUserId userId $ \user -> withFastStore' (`setUserChatsRead` user) >> ok user UserRead -> withUser $ \User {userId} -> processChatCommand $ APIUserRead userId APIChatRead (ChatRef cType chatId) fromToIds -> withUser $ \_ -> case cType of CTDirect -> do - user <- withStore $ \db -> getUserByContactId db chatId - timedItems <- withStore' $ \db -> getDirectUnreadTimedItems db user chatId fromToIds + user <- withFastStore $ \db -> getUserByContactId db chatId + timedItems <- withFastStore' $ \db -> getDirectUnreadTimedItems db user chatId fromToIds ts <- liftIO getCurrentTime forM_ timedItems $ \(itemId, ttl) -> do let deleteAt = addUTCTime (realToFrac ttl) ts - withStore' $ \db -> setDirectChatItemDeleteAt db user chatId itemId deleteAt + withFastStore' $ \db -> setDirectChatItemDeleteAt db user chatId itemId deleteAt startProximateTimedItemThread user (ChatRef CTDirect chatId, itemId) deleteAt - withStore' $ \db -> updateDirectChatItemsRead db user chatId fromToIds + withFastStore' $ \db -> updateDirectChatItemsRead db user chatId fromToIds ok user CTGroup -> do - user@User {userId} <- withStore $ \db -> getUserByGroupId db chatId - timedItems <- withStore' $ \db -> getGroupUnreadTimedItems db user chatId fromToIds + user@User {userId} <- withFastStore $ \db -> getUserByGroupId db chatId + timedItems <- withFastStore' $ \db -> getGroupUnreadTimedItems db user chatId fromToIds ts <- liftIO getCurrentTime forM_ timedItems $ \(itemId, ttl) -> do let deleteAt = addUTCTime (realToFrac ttl) ts - withStore' $ \db -> setGroupChatItemDeleteAt db user chatId itemId deleteAt + withFastStore' $ \db -> setGroupChatItemDeleteAt db user chatId itemId deleteAt startProximateTimedItemThread user (ChatRef CTGroup chatId, itemId) deleteAt - withStore' $ \db -> updateGroupChatItemsRead db userId chatId fromToIds + withFastStore' $ \db -> updateGroupChatItemsRead db userId chatId fromToIds ok user CTLocal -> do - user <- withStore $ \db -> getUserByNoteFolderId db chatId - withStore' $ \db -> updateLocalChatItemsRead db user chatId fromToIds + user <- withFastStore $ \db -> getUserByNoteFolderId db chatId + withFastStore' $ \db -> updateLocalChatItemsRead db user chatId fromToIds ok user CTContactRequest -> pure $ chatCmdError Nothing "not supported" CTContactConnection -> pure $ chatCmdError Nothing "not supported" APIChatUnread (ChatRef cType chatId) unreadChat -> withUser $ \user -> case cType of CTDirect -> do - withStore $ \db -> do + withFastStore $ \db -> do ct <- getContact db vr user chatId liftIO $ updateContactUnreadChat db user ct unreadChat ok user CTGroup -> do - withStore $ \db -> do + withFastStore $ \db -> do Group {groupInfo} <- getGroup db vr user chatId liftIO $ updateGroupUnreadChat db user groupInfo unreadChat ok user CTLocal -> do - withStore $ \db -> do + withFastStore $ \db -> do nf <- getNoteFolder db user chatId liftIO $ updateNoteFolderUnreadChat db user nf unreadChat ok user _ -> pure $ chatCmdError (Just user) "not supported" - APIDeleteChat (ChatRef cType chatId) notify -> withUser $ \user@User {userId} -> case cType of + APIDeleteChat cRef@(ChatRef cType chatId) cdm -> withUser $ \user@User {userId} -> case cType of CTDirect -> do - ct <- withStore $ \db -> getContact db vr user chatId - filesInfo <- withStore' $ \db -> getContactFileInfo db user ct - withContactLock "deleteChat direct" chatId . procCmd $ do - cancelFilesInProgress user filesInfo - deleteFilesLocally filesInfo - let doSendDel = contactReady ct && contactActive ct && notify - when doSendDel $ void (sendDirectContactMessage user ct XDirectDel) `catchChatError` const (pure ()) - contactConnIds <- map aConnId <$> withStore' (\db -> getContactConnections db vr userId ct) - deleteAgentConnectionsAsync' user contactConnIds doSendDel - -- functions below are called in separate transactions to prevent crashes on android - -- (possibly, race condition on integrity check?) - withStore' $ \db -> deleteContactConnectionsAndFiles db userId ct - withStore $ \db -> deleteContact db user ct - pure $ CRContactDeleted user ct + ct <- withFastStore $ \db -> getContact db vr user chatId + filesInfo <- withFastStore' $ \db -> getContactFileInfo db user ct + withContactLock "deleteChat direct" chatId . procCmd $ + case cdm of + CDMFull notify -> do + cancelFilesInProgress user filesInfo + deleteFilesLocally filesInfo + sendDelDeleteConns ct notify + -- functions below are called in separate transactions to prevent crashes on android + -- (possibly, race condition on integrity check?) + withFastStore' $ \db -> do + deleteContactConnections db user ct + deleteContactFiles db user ct + withFastStore $ \db -> deleteContact db user ct + pure $ CRContactDeleted user ct + CDMEntity notify -> do + cancelFilesInProgress user filesInfo + sendDelDeleteConns ct notify + ct' <- withFastStore $ \db -> do + liftIO $ deleteContactConnections db user ct + liftIO $ void $ updateContactStatus db user ct CSDeletedByUser + getContact db vr user chatId + pure $ CRContactDeleted user ct' + CDMMessages -> do + void $ processChatCommand $ APIClearChat cRef + withFastStore' $ \db -> setContactChatDeleted db user ct True + pure $ CRContactDeleted user ct {chatDeleted = True} + where + sendDelDeleteConns ct notify = do + let doSendDel = contactReady ct && contactActive ct && notify + when doSendDel $ void (sendDirectContactMessage user ct XDirectDel) `catchChatError` const (pure ()) + contactConnIds <- map aConnId <$> withFastStore' (\db -> getContactConnections db vr userId ct) + deleteAgentConnectionsAsync' user contactConnIds doSendDel CTContactConnection -> withConnectionLock "deleteChat contactConnection" chatId . procCmd $ do - conn@PendingContactConnection {pccAgentConnId = AgentConnId acId} <- withStore $ \db -> getPendingContactConnection db userId chatId + conn@PendingContactConnection {pccAgentConnId = AgentConnId acId} <- withFastStore $ \db -> getPendingContactConnection db userId chatId deleteAgentConnectionAsync user acId - withStore' $ \db -> deletePendingContactConnection db userId chatId + withFastStore' $ \db -> deletePendingContactConnection db userId chatId pure $ CRContactConnectionDeleted user conn CTGroup -> do Group gInfo@GroupInfo {membership} members <- withStore $ \db -> getGroup db vr user chatId @@ -1089,34 +1212,34 @@ processChatCommand' vr = \case CTContactRequest -> pure $ chatCmdError (Just user) "not supported" APIClearChat (ChatRef cType chatId) -> withUser $ \user@User {userId} -> case cType of CTDirect -> do - ct <- withStore $ \db -> getContact db vr user chatId - filesInfo <- withStore' $ \db -> getContactFileInfo db user ct + ct <- withFastStore $ \db -> getContact db vr user chatId + filesInfo <- withFastStore' $ \db -> getContactFileInfo db user ct cancelFilesInProgress user filesInfo deleteFilesLocally filesInfo - withStore' $ \db -> deleteContactCIs db user ct + withFastStore' $ \db -> deleteContactCIs db user ct pure $ CRChatCleared user (AChatInfo SCTDirect $ DirectChat ct) CTGroup -> do - gInfo <- withStore $ \db -> getGroupInfo db vr user chatId - filesInfo <- withStore' $ \db -> getGroupFileInfo db user gInfo + gInfo <- withFastStore $ \db -> getGroupInfo db vr user chatId + filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo cancelFilesInProgress user filesInfo deleteFilesLocally filesInfo - withStore' $ \db -> deleteGroupCIs db user gInfo - membersToDelete <- withStore' $ \db -> getGroupMembersForExpiration db vr user gInfo - forM_ membersToDelete $ \m -> withStore' $ \db -> deleteGroupMember db user m + withFastStore' $ \db -> deleteGroupChatItemsMessages db user gInfo + membersToDelete <- withFastStore' $ \db -> getGroupMembersForExpiration db vr user gInfo + forM_ membersToDelete $ \m -> withFastStore' $ \db -> deleteGroupMember db user m pure $ CRChatCleared user (AChatInfo SCTGroup $ GroupChat gInfo) CTLocal -> do - nf <- withStore $ \db -> getNoteFolder db user chatId - filesInfo <- withStore' $ \db -> getNoteFolderFileInfo db user nf + nf <- withFastStore $ \db -> getNoteFolder db user chatId + filesInfo <- withFastStore' $ \db -> getNoteFolderFileInfo db user nf deleteFilesLocally filesInfo - withStore' $ \db -> deleteNoteFolderFiles db userId nf - withStore' $ \db -> deleteNoteFolderCIs db user nf + withFastStore' $ \db -> deleteNoteFolderFiles db userId nf + withFastStore' $ \db -> deleteNoteFolderCIs db user nf pure $ CRChatCleared user (AChatInfo SCTLocal $ LocalChat nf) CTContactConnection -> pure $ chatCmdError (Just user) "not supported" CTContactRequest -> pure $ chatCmdError (Just user) "not supported" APIAcceptContact incognito connReqId -> withUser $ \_ -> do - (user@User {userId}, cReq@UserContactRequest {userContactLinkId}) <- withStore $ \db -> getContactRequest' db connReqId + (user@User {userId}, cReq@UserContactRequest {userContactLinkId}) <- withFastStore $ \db -> getContactRequest' db connReqId withUserContactLock "acceptContact" userContactLinkId $ do - ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId + ucl <- withFastStore $ \db -> getUserContactLinkById db userId userContactLinkId let contactUsed = (\(_, groupId_, _) -> isNothing groupId_) ucl -- [incognito] generate profile to send, create connection with incognito profile incognitoProfile <- if incognito then Just . NewIncognito <$> liftIO generateRandomProfile else pure Nothing @@ -1124,7 +1247,7 @@ processChatCommand' vr = \case pure $ CRAcceptingContactRequest user ct APIRejectContact connReqId -> withUser $ \user -> do cReq@UserContactRequest {userContactLinkId, agentContactConnId = AgentConnId connId, agentInvitationId = AgentInvId invId} <- - withStore $ \db -> + withFastStore $ \db -> getContactRequest db user connReqId `storeFinally` liftIO (deleteContactRequest db user connReqId) withUserContactLock "rejectContact" userContactLinkId $ do @@ -1132,7 +1255,7 @@ processChatCommand' vr = \case pure $ CRContactRequestRejected user cReq APISendCallInvitation contactId callType -> withUser $ \user -> do -- party initiating call - ct <- withStore $ \db -> getContact db vr user contactId + ct <- withFastStore $ \db -> getContact db vr user contactId assertDirectAllowed user MDSnd ct XCallInv_ if featureAllowed SCFCalls forUser ct then do @@ -1152,14 +1275,14 @@ processChatCommand' vr = \case ok user else pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (chatFeatureNameText CFCalls)) SendCallInvitation cName callType -> withUser $ \user -> do - contactId <- withStore $ \db -> getContactIdByName db user cName + contactId <- withFastStore $ \db -> getContactIdByName db user cName processChatCommand $ APISendCallInvitation contactId callType APIRejectCall contactId -> -- party accepting call withCurrentCall contactId $ \user ct Call {chatItemId, callState} -> case callState of CallInvitationReceived {} -> do let aciContent = ACIContent SMDRcv $ CIRcvCall CISCallRejected 0 - withStore' $ \db -> updateDirectChatItemsRead db user contactId $ Just (chatItemId, chatItemId) + withFastStore' $ \db -> updateDirectChatItemsRead db user contactId $ Just (chatItemId, chatItemId) timed_ <- contactCITimed ct updateDirectChatItemView user ct chatItemId aciContent False False timed_ Nothing forM_ (timed_ >>= timedDeleteAt') $ @@ -1175,7 +1298,7 @@ processChatCommand' vr = \case callState' = CallOfferSent {localCallType = callType, peerCallType, localCallSession = rtcSession, sharedKey} aciContent = ACIContent SMDRcv $ CIRcvCall CISCallAccepted 0 (SndMessage {msgId}, _) <- sendDirectContactMessage user ct (XCallOffer callId offer) - withStore' $ \db -> updateDirectChatItemsRead db user contactId $ Just (chatItemId, chatItemId) + withFastStore' $ \db -> updateDirectChatItemsRead db user contactId $ Just (chatItemId, chatItemId) updateDirectChatItemView user ct chatItemId aciContent False False Nothing $ Just msgId pure $ Just call {callState = callState'} _ -> throwChatError . CECallState $ callStateTag callState @@ -1209,7 +1332,7 @@ processChatCommand' vr = \case (SndMessage {msgId}, _) <- sendDirectContactMessage user ct (XCallEnd callId) updateCallItemStatus user ct call WCSDisconnected $ Just msgId pure Nothing - APIGetCallInvitations -> withUser $ \_ -> lift $ do + APIGetCallInvitations -> withUser' $ \_ -> lift $ do calls <- asks currentCalls >>= readTVarIO let invs = mapMaybe callInvitation $ M.elems calls rcvCallInvitations <- rights <$> mapM rcvCallInvitation invs @@ -1218,7 +1341,7 @@ processChatCommand' vr = \case callInvitation Call {contactId, callState, callTs} = case callState of CallInvitationReceived {peerCallType, sharedKey} -> Just (contactId, callTs, peerCallType, sharedKey) _ -> Nothing - rcvCallInvitation (contactId, callTs, peerCallType, sharedKey) = runExceptT . withStore $ \db -> do + rcvCallInvitation (contactId, callTs, peerCallType, sharedKey) = runExceptT . withFastStore $ \db -> do user <- getUserByContactId db contactId contact <- getContact db vr user contactId pure RcvCallInvitation {user, contact, callType = peerCallType, sharedKey, callTs} @@ -1229,20 +1352,20 @@ processChatCommand' vr = \case updateCallItemStatus user ct call receivedStatus Nothing $> Just call APIUpdateProfile userId profile -> withUserId userId (`updateProfile` profile) APISetContactPrefs contactId prefs' -> withUser $ \user -> do - ct <- withStore $ \db -> getContact db vr user contactId + ct <- withFastStore $ \db -> getContact db vr user contactId updateContactPrefs user ct prefs' APISetContactAlias contactId localAlias -> withUser $ \user@User {userId} -> do - ct' <- withStore $ \db -> do + ct' <- withFastStore $ \db -> do ct <- getContact db vr user contactId liftIO $ updateContactAlias db userId ct localAlias pure $ CRContactAliasUpdated user ct' APISetConnectionAlias connId localAlias -> withUser $ \user@User {userId} -> do - conn' <- withStore $ \db -> do + conn' <- withFastStore $ \db -> do conn <- getPendingContactConnection db userId connId liftIO $ updateContactConnectionAlias db userId conn localAlias pure $ CRConnectionAliasUpdated user conn' APISetUserUIThemes uId uiThemes -> withUser $ \user@User {userId} -> do - user'@User {userId = uId'} <- withStore $ \db -> do + user'@User {userId = uId'} <- withFastStore $ \db -> do user' <- getUser db uId liftIO $ setUserUIThemes db user uiThemes pure user' @@ -1250,46 +1373,44 @@ processChatCommand' vr = \case ok user' APISetChatUIThemes (ChatRef cType chatId) uiThemes -> withUser $ \user -> case cType of CTDirect -> do - withStore $ \db -> do + withFastStore $ \db -> do ct <- getContact db vr user chatId liftIO $ setContactUIThemes db user ct uiThemes ok user CTGroup -> do - withStore $ \db -> do + withFastStore $ \db -> do g <- getGroupInfo db vr user chatId liftIO $ setGroupUIThemes db user g uiThemes ok user _ -> pure $ chatCmdError (Just user) "not supported" APIParseMarkdown text -> pure . CRApiParsedMarkdown $ parseMaybeMarkdownList text - APIGetNtfToken -> withUser $ \_ -> crNtfToken <$> withAgent getNtfToken + APIGetNtfToken -> withUser' $ \_ -> crNtfToken <$> withAgent getNtfToken APIRegisterToken token mode -> withUser $ \_ -> CRNtfTokenStatus <$> withAgent (\a -> registerNtfToken a token mode) APIVerifyToken token nonce code -> withUser $ \_ -> withAgent (\a -> verifyNtfToken a token nonce code) >> ok_ APIDeleteToken token -> withUser $ \_ -> withAgent (`deleteNtfToken` token) >> ok_ APIGetNtfMessage nonce encNtfInfo -> withUser $ \_ -> do - (NotificationInfo {ntfConnId, ntfMsgMeta}, msgs) <- withAgent $ \a -> getNotificationMessage a nonce encNtfInfo + (NotificationInfo {ntfConnId, ntfMsgMeta}, msg) <- withAgent $ \a -> getNotificationMessage a nonce encNtfInfo let msgTs' = systemToUTCTime . (\SMP.NMsgMeta {msgTs} -> msgTs) <$> ntfMsgMeta agentConnId = AgentConnId ntfConnId user_ <- withStore' (`getUserByAConnId` agentConnId) connEntity_ <- pure user_ $>>= \user -> withStore (\db -> Just <$> getConnectionEntity db vr user agentConnId) `catchChatError` (\e -> toView (CRChatError (Just user) e) $> Nothing) - pure CRNtfMessages {user_, connEntity_, msgTs = msgTs', ntfMessages = map ntfMsgInfo msgs} + pure CRNtfMessages {user_, connEntity_, msgTs = msgTs', ntfMessage_ = ntfMsgInfo <$> msg} APIGetUserProtoServers userId (AProtocolType p) -> withUserId userId $ \user -> withServerProtocol p $ do - ChatConfig {defaultServers} <- asks config - servers <- withStore' (`getProtocolServers` user) - let defServers = cfgServers p defaultServers - servers' = fromMaybe (L.map toServerCfg defServers) $ nonEmpty servers - pure $ CRUserProtoServers user $ AUPS $ UserProtoServers p servers' defServers - where - toServerCfg server = ServerCfg {server, preset = True, tested = Nothing, enabled = True} + cfg@ChatConfig {defaultServers} <- asks config + servers <- withFastStore' (`getProtocolServers` user) + pure $ CRUserProtoServers user $ AUPS $ UserProtoServers p (useServers cfg p servers) (cfgServers p defaultServers) GetUserProtoServers aProtocol -> withUser $ \User {userId} -> processChatCommand $ APIGetUserProtoServers userId aProtocol - APISetUserProtoServers userId (APSC p (ProtoServersConfig servers)) -> withUserId userId $ \user -> withServerProtocol p $ do - withStore $ \db -> overwriteProtocolServers db user servers - cfg <- asks config - lift $ withAgent' $ \a -> setProtocolServers a (aUserId user) $ activeAgentServers cfg p servers - ok user + APISetUserProtoServers userId (APSC p (ProtoServersConfig servers)) + | null servers || any (\ServerCfg {enabled} -> enabled) servers -> withUserId userId $ \user -> withServerProtocol p $ do + withFastStore $ \db -> overwriteProtocolServers db user servers + cfg <- asks config + lift $ withAgent' $ \a -> setProtocolServers a (aUserId user) $ useServers cfg p servers + ok user + | otherwise -> withUserId userId $ \user -> pure $ chatCmdError (Just user) "all servers are disabled" SetUserProtoServers serversConfig -> withUser $ \User {userId} -> processChatCommand $ APISetUserProtoServers userId serversConfig APITestProtoServer userId srv@(AProtoServerWithAuth _ server) -> withUserId userId $ \user -> @@ -1301,32 +1422,39 @@ processChatCommand' vr = \case withChatLock "setChatItemTTL" $ do case newTTL_ of Nothing -> do - withStore' $ \db -> setChatItemTTL db user newTTL_ + withFastStore' $ \db -> setChatItemTTL db user newTTL_ lift $ setExpireCIFlag user False Just newTTL -> do - oldTTL <- withStore' (`getChatItemTTL` user) + oldTTL <- withFastStore' (`getChatItemTTL` user) when (maybe True (newTTL <) oldTTL) $ do lift $ setExpireCIFlag user False expireChatItems user newTTL True - withStore' $ \db -> setChatItemTTL db user newTTL_ + withFastStore' $ \db -> setChatItemTTL db user newTTL_ lift $ startExpireCIThread user lift . whenM chatStarted $ setExpireCIFlag user True ok user SetChatItemTTL newTTL_ -> withUser' $ \User {userId} -> do processChatCommand $ APISetChatItemTTL userId newTTL_ APIGetChatItemTTL userId -> withUserId' userId $ \user -> do - ttl <- withStore' (`getChatItemTTL` user) + ttl <- withFastStore' (`getChatItemTTL` user) pure $ CRChatItemTTL user ttl GetChatItemTTL -> withUser' $ \User {userId} -> do processChatCommand $ APIGetChatItemTTL userId APISetNetworkConfig cfg -> withUser' $ \_ -> lift (withAgent' (`setNetworkConfig` cfg)) >> ok_ APIGetNetworkConfig -> withUser' $ \_ -> - lift $ CRNetworkConfig <$> withAgent' getNetworkConfig + CRNetworkConfig <$> lift getNetworkConfig + SetNetworkConfig simpleNetCfg -> do + cfg <- (`updateNetworkConfig` simpleNetCfg) <$> lift getNetworkConfig + void . processChatCommand $ APISetNetworkConfig cfg + pure $ CRNetworkConfig cfg APISetNetworkInfo info -> lift (withAgent' (`setUserNetworkInfo` info)) >> ok_ ReconnectAllServers -> withUser' $ \_ -> lift (withAgent' reconnectAllServers) >> ok_ + ReconnectServer userId srv -> withUserId userId $ \user -> do + lift (withAgent' $ \a -> reconnectSMPServer a (aUserId user) srv) + ok_ APISetChatSettings (ChatRef cType chatId) chatSettings -> withUser $ \user -> case cType of CTDirect -> do - ct <- withStore $ \db -> do + ct <- withFastStore $ \db -> do ct <- getContact db vr user chatId liftIO $ updateContactSettings db user chatId chatSettings pure ct @@ -1334,7 +1462,7 @@ processChatCommand' vr = \case withAgent $ \a -> toggleConnectionNtfs a connId (chatHasNtfs chatSettings) ok user CTGroup -> do - ms <- withStore $ \db -> do + ms <- withFastStore $ \db -> do Group _ ms <- getGroup db vr user chatId liftIO $ updateGroupSettings db user chatId chatSettings pure ms @@ -1343,7 +1471,7 @@ processChatCommand' vr = \case ok user _ -> pure $ chatCmdError (Just user) "not supported" APISetMemberSettings gId gMemberId settings -> withUser $ \user -> do - m <- withStore $ \db -> do + m <- withFastStore $ \db -> do liftIO $ updateGroupMemberSettings db user gId gMemberId settings getGroupMember db vr user gId gMemberId let ntfOn = showMessages $ memberSettings m @@ -1351,50 +1479,60 @@ processChatCommand' vr = \case ok user APIContactInfo contactId -> withUser $ \user@User {userId} -> do -- [incognito] print user's incognito profile for this contact - ct@Contact {activeConn} <- withStore $ \db -> getContact db vr user contactId + ct@Contact {activeConn} <- withFastStore $ \db -> getContact db vr user contactId incognitoProfile <- case activeConn of Nothing -> pure Nothing Just Connection {customUserProfileId} -> - forM customUserProfileId $ \profileId -> withStore (\db -> getProfileById db userId profileId) + forM customUserProfileId $ \profileId -> withFastStore (\db -> getProfileById db userId profileId) connectionStats <- mapM (withAgent . flip getConnectionServers) (contactConnId ct) pure $ CRContactInfo user ct connectionStats (fmap fromLocalProfile incognitoProfile) + APIContactQueueInfo contactId -> withUser $ \user -> do + ct@Contact {activeConn} <- withFastStore $ \db -> getContact db vr user contactId + case activeConn of + Just conn -> getConnQueueInfo user conn + Nothing -> throwChatError $ CEContactNotActive ct APIGroupInfo gId -> withUser $ \user -> do - (g, s) <- withStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> liftIO (getGroupSummary db user gId) + (g, s) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> liftIO (getGroupSummary db user gId) pure $ CRGroupInfo user g s APIGroupMemberInfo gId gMemberId -> withUser $ \user -> do - (g, m) <- withStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId + (g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId connectionStats <- mapM (withAgent . flip getConnectionServers) (memberConnId m) pure $ CRGroupMemberInfo user g m connectionStats + APIGroupMemberQueueInfo gId gMemberId -> withUser $ \user -> do + GroupMember {activeConn} <- withFastStore $ \db -> getGroupMember db vr user gId gMemberId + case activeConn of + Just conn -> getConnQueueInfo user conn + Nothing -> throwChatError CEGroupMemberNotActive APISwitchContact contactId -> withUser $ \user -> do - ct <- withStore $ \db -> getContact db vr user contactId + ct <- withFastStore $ \db -> getContact db vr user contactId case contactConnId ct of Just connId -> do connectionStats <- withAgent $ \a -> switchConnectionAsync a "" connId pure $ CRContactSwitchStarted user ct connectionStats Nothing -> throwChatError $ CEContactNotActive ct APISwitchGroupMember gId gMemberId -> withUser $ \user -> do - (g, m) <- withStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId + (g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId case memberConnId m of Just connId -> do connectionStats <- withAgent (\a -> switchConnectionAsync a "" connId) pure $ CRGroupMemberSwitchStarted user g m connectionStats _ -> throwChatError CEGroupMemberNotActive APIAbortSwitchContact contactId -> withUser $ \user -> do - ct <- withStore $ \db -> getContact db vr user contactId + ct <- withFastStore $ \db -> getContact db vr user contactId case contactConnId ct of Just connId -> do connectionStats <- withAgent $ \a -> abortConnectionSwitch a connId pure $ CRContactSwitchAborted user ct connectionStats Nothing -> throwChatError $ CEContactNotActive ct APIAbortSwitchGroupMember gId gMemberId -> withUser $ \user -> do - (g, m) <- withStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId + (g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId case memberConnId m of Just connId -> do connectionStats <- withAgent $ \a -> abortConnectionSwitch a connId pure $ CRGroupMemberSwitchAborted user g m connectionStats _ -> throwChatError CEGroupMemberNotActive APISyncContactRatchet contactId force -> withUser $ \user -> withContactLock "syncContactRatchet" contactId $ do - ct <- withStore $ \db -> getContact db vr user contactId + ct <- withFastStore $ \db -> getContact db vr user contactId case contactConn ct of Just conn@Connection {pqSupport} -> do cStats@ConnectionStats {ratchetSyncState = rss} <- withAgent $ \a -> synchronizeRatchet a (aConnId conn) pqSupport force @@ -1402,7 +1540,7 @@ processChatCommand' vr = \case pure $ CRContactRatchetSyncStarted user ct cStats Nothing -> throwChatError $ CEContactNotActive ct APISyncGroupMemberRatchet gId gMemberId force -> withUser $ \user -> withGroupLock "syncGroupMemberRatchet" gId $ do - (g, m) <- withStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId + (g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId case memberConnId m of Just connId -> do cStats@ConnectionStats {ratchetSyncState = rss} <- withAgent $ \a -> synchronizeRatchet a connId PQSupportOff force @@ -1410,7 +1548,7 @@ processChatCommand' vr = \case pure $ CRGroupMemberRatchetSyncStarted user g m cStats _ -> throwChatError CEGroupMemberNotActive APIGetContactCode contactId -> withUser $ \user -> do - ct@Contact {activeConn} <- withStore $ \db -> getContact db vr user contactId + ct@Contact {activeConn} <- withFastStore $ \db -> getContact db vr user contactId case activeConn of Just conn@Connection {connId} -> do code <- getConnectionCode $ aConnId conn @@ -1418,13 +1556,13 @@ processChatCommand' vr = \case Just SecurityCode {securityCode} | sameVerificationCode code securityCode -> pure ct | otherwise -> do - withStore' $ \db -> setConnectionVerified db user connId Nothing + withFastStore' $ \db -> setConnectionVerified db user connId Nothing pure (ct :: Contact) {activeConn = Just $ (conn :: Connection) {connectionCode = Nothing}} _ -> pure ct pure $ CRContactCode user ct' code Nothing -> throwChatError $ CEContactNotActive ct APIGetGroupMemberCode gId gMemberId -> withUser $ \user -> do - (g, m@GroupMember {activeConn}) <- withStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId + (g, m@GroupMember {activeConn}) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId case activeConn of Just conn@Connection {connId} -> do code <- getConnectionCode $ aConnId conn @@ -1432,50 +1570,52 @@ processChatCommand' vr = \case Just SecurityCode {securityCode} | sameVerificationCode code securityCode -> pure m | otherwise -> do - withStore' $ \db -> setConnectionVerified db user connId Nothing + withFastStore' $ \db -> setConnectionVerified db user connId Nothing pure (m :: GroupMember) {activeConn = Just $ (conn :: Connection) {connectionCode = Nothing}} _ -> pure m pure $ CRGroupMemberCode user g m' code _ -> throwChatError CEGroupMemberNotActive APIVerifyContact contactId code -> withUser $ \user -> do - ct@Contact {activeConn} <- withStore $ \db -> getContact db vr user contactId + ct@Contact {activeConn} <- withFastStore $ \db -> getContact db vr user contactId case activeConn of Just conn -> verifyConnectionCode user conn code Nothing -> throwChatError $ CEContactNotActive ct APIVerifyGroupMember gId gMemberId code -> withUser $ \user -> do - GroupMember {activeConn} <- withStore $ \db -> getGroupMember db vr user gId gMemberId + GroupMember {activeConn} <- withFastStore $ \db -> getGroupMember db vr user gId gMemberId case activeConn of Just conn -> verifyConnectionCode user conn code _ -> throwChatError CEGroupMemberNotActive APIEnableContact contactId -> withUser $ \user -> do - ct@Contact {activeConn} <- withStore $ \db -> getContact db vr user contactId + ct@Contact {activeConn} <- withFastStore $ \db -> getContact db vr user contactId case activeConn of Just conn -> do - withStore' $ \db -> setConnectionAuthErrCounter db user conn 0 + withFastStore' $ \db -> setAuthErrCounter db user conn 0 ok user Nothing -> throwChatError $ CEContactNotActive ct APIEnableGroupMember gId gMemberId -> withUser $ \user -> do - GroupMember {activeConn} <- withStore $ \db -> getGroupMember db vr user gId gMemberId + GroupMember {activeConn} <- withFastStore $ \db -> getGroupMember db vr user gId gMemberId case activeConn of Just conn -> do - withStore' $ \db -> setConnectionAuthErrCounter db user conn 0 + withFastStore' $ \db -> setAuthErrCounter db user conn 0 ok user _ -> throwChatError CEGroupMemberNotActive SetShowMessages cName ntfOn -> updateChatSettings cName (\cs -> cs {enableNtfs = ntfOn}) SetSendReceipts cName rcptsOn_ -> updateChatSettings cName (\cs -> cs {sendRcpts = rcptsOn_}) SetShowMemberMessages gName mName showMessages -> withUser $ \user -> do (gId, mId) <- getGroupAndMemberId user gName mName - gInfo <- withStore $ \db -> getGroupInfo db vr user gId - m <- withStore $ \db -> getGroupMember db vr user gId mId + gInfo <- withFastStore $ \db -> getGroupInfo db vr user gId + m <- withFastStore $ \db -> getGroupMember db vr user gId mId let GroupInfo {membership = GroupMember {memberRole = membershipRole}} = gInfo when (membershipRole >= GRAdmin) $ throwChatError $ CECantBlockMemberForSelf gInfo m showMessages let settings = (memberSettings m) {showMessages} processChatCommand $ APISetMemberSettings gId mId settings ContactInfo cName -> withContactName cName APIContactInfo ShowGroupInfo gName -> withUser $ \user -> do - groupId <- withStore $ \db -> getGroupIdByName db user gName + groupId <- withFastStore $ \db -> getGroupIdByName db user gName processChatCommand $ APIGroupInfo groupId GroupMemberInfo gName mName -> withMemberName gName mName APIGroupMemberInfo + ContactQueueInfo cName -> withContactName cName APIContactQueueInfo + GroupMemberQueueInfo gName mName -> withMemberName gName mName APIGroupMemberQueueInfo SwitchContact cName -> withContactName cName APISwitchContact SwitchGroupMember gName mName -> withMemberName gName mName APISwitchGroupMember AbortSwitchContact cName -> withContactName cName APIAbortSwitchContact @@ -1496,12 +1636,12 @@ processChatCommand' vr = \case subMode <- chatReadVar subscriptionMode (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing IKPQOn subMode -- TODO PQ pass minVersion from the current range - conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnNew incognitoProfile subMode initialChatVersion PQSupportOn + conn <- withFastStore' $ \db -> createDirectConnection db user connId cReq ConnNew incognitoProfile subMode initialChatVersion PQSupportOn pure $ CRInvitation user cReq conn AddContact incognito -> withUser $ \User {userId} -> processChatCommand $ APIAddContact userId incognito APISetConnectionIncognito connId incognito -> withUser $ \user@User {userId} -> do - conn'_ <- withStore $ \db -> do + conn'_ <- withFastStore $ \db -> do conn@PendingContactConnection {pccConnStatus, customUserProfileId} <- getPendingContactConnection db userId connId case (pccConnStatus, customUserProfileId, incognito) of (ConnNew, Nothing, True) -> liftIO $ do @@ -1515,6 +1655,40 @@ processChatCommand' vr = \case case conn'_ of Just conn' -> pure $ CRConnectionIncognitoUpdated user conn' Nothing -> throwChatError CEConnectionIncognitoChangeProhibited + APIChangeConnectionUser connId newUserId -> withUser $ \user@User {userId} -> do + conn <- withFastStore $ \db -> getPendingContactConnection db userId connId + let PendingContactConnection {pccConnStatus, connReqInv} = conn + case (pccConnStatus, connReqInv) of + (ConnNew, Just cReqInv) -> do + newUser <- privateGetUser newUserId + conn' <- ifM (canKeepLink cReqInv newUser) (updateConnRecord user conn newUser) (recreateConn user conn newUser) + pure $ CRConnectionUserChanged user conn conn' newUser + _ -> throwChatError CEConnectionUserChangeProhibited + where + canKeepLink :: ConnReqInvitation -> User -> CM Bool + canKeepLink (CRInvitationUri crData _) newUser = do + let ConnReqUriData {crSmpQueues = q :| _} = crData + SMPQueueUri {queueAddress = SMPQueueAddress {smpServer}} = q + cfg <- asks config + newUserServers <- L.map (\ServerCfg {server} -> protoServer server) . useServers cfg SPSMP <$> withFastStore' (`getProtocolServers` newUser) + pure $ smpServer `elem` newUserServers + updateConnRecord user@User {userId} conn@PendingContactConnection {customUserProfileId} newUser = do + withAgent $ \a -> changeConnectionUser a (aUserId user) (aConnId' conn) (aUserId newUser) + withFastStore' $ \db -> do + conn' <- updatePCCUser db userId conn newUserId + forM_ customUserProfileId $ \profileId -> + deletePCCIncognitoProfile db user profileId + pure conn' + recreateConn user conn@PendingContactConnection {customUserProfileId} newUser = do + subMode <- chatReadVar subscriptionMode + (agConnId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing IKPQOn subMode + conn' <- withFastStore' $ \db -> do + deleteConnectionRecord db user connId + forM_ customUserProfileId $ \profileId -> + deletePCCIncognitoProfile db user profileId + createDirectConnection db newUser agConnId cReq ConnNew Nothing subMode initialChatVersion PQSupportOn + deleteAgentConnectionAsync user (aConnId' conn) + pure conn' APIConnectPlan userId cReqUri -> withUserId userId $ \user -> CRConnectionPlan user <$> connectPlan user cReqUri APIConnect userId incognito (Just (ACR SCMInvitation cReq)) -> withUserId userId $ \user -> withInvitationLock "connect" (strEncode cReq) . procCmd $ do @@ -1529,8 +1703,8 @@ processChatCommand' vr = \case let chatV = agentToChatVersion agentV dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup' - conn <- withStore' $ \db -> createDirectConnection db user connId cReq ConnJoined (incognitoProfile $> profileToSend) subMode chatV pqSup' - void . withAgent $ \a -> joinConnection a (aUserId user) (Just connId) True cReq dm pqSup' subMode + conn@PendingContactConnection {pccConnId} <- withFastStore' $ \db -> createDirectConnection db user connId cReq ConnJoined (incognitoProfile $> profileToSend) subMode chatV pqSup' + joinPreparedAgentConnection user pccConnId connId cReq dm pqSup' subMode pure $ CRSentConfirmation user conn APIConnect userId incognito (Just (ACR SCMContact cReq)) -> withUserId userId $ \user -> connectViaContact user incognito cReq APIConnect _ _ Nothing -> throwChatError CEInvalidConnReq @@ -1543,7 +1717,7 @@ processChatCommand' vr = \case _ -> processChatCommand $ APIConnect userId incognito aCReqUri Connect _ Nothing -> throwChatError CEInvalidConnReq APIConnectContactViaAddress userId incognito contactId -> withUserId userId $ \user -> do - ct@Contact {activeConn, profile = LocalProfile {contactLink}} <- withStore $ \db -> getContact db vr user contactId + ct@Contact {activeConn, profile = LocalProfile {contactLink}} <- withFastStore $ \db -> getContact db vr user contactId when (isJust activeConn) $ throwChatError (CECommandError "contact already has connection") case contactLink of Just cReq -> connectContactViaAddress user incognito ct cReq @@ -1556,26 +1730,26 @@ processChatCommand' vr = \case CPContactAddress (CAPContactViaAddress Contact {contactId}) -> processChatCommand $ APIConnectContactViaAddress userId incognito contactId _ -> processChatCommand $ APIConnect userId incognito (Just cReqUri) - DeleteContact cName -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId) True + DeleteContact cName cdm -> withContactName cName $ \ctId -> APIDeleteChat (ChatRef CTDirect ctId) cdm ClearContact cName -> withContactName cName $ APIClearChat . ChatRef CTDirect APIListContacts userId -> withUserId userId $ \user -> - CRContactsList user <$> withStore' (\db -> getUserContacts db vr user) + CRContactsList user <$> withFastStore' (\db -> getUserContacts db vr user) ListContacts -> withUser $ \User {userId} -> processChatCommand $ APIListContacts userId APICreateMyAddress userId -> withUserId userId $ \user -> procCmd $ do subMode <- chatReadVar subscriptionMode (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact Nothing IKPQOn subMode - withStore $ \db -> createUserContactLink db user connId cReq subMode + withFastStore $ \db -> createUserContactLink db user connId cReq subMode pure $ CRUserContactLinkCreated user cReq CreateMyAddress -> withUser $ \User {userId} -> processChatCommand $ APICreateMyAddress userId APIDeleteMyAddress userId -> withUserId userId $ \user@User {profile = p} -> do - conns <- withStore $ \db -> getUserAddressConnections db vr user + conns <- withFastStore $ \db -> getUserAddressConnections db vr user withChatLock "deleteMyAddress" $ do deleteAgentConnectionsAsync user $ map aConnId conns - withStore' (`deleteUserAddress` user) + withFastStore' (`deleteUserAddress` user) let p' = (fromLocalProfile p :: Profile) {contactLink = Nothing} - r <- updateProfile_ user p' $ withStore' $ \db -> setUserProfileContactLink db user Nothing + r <- updateProfile_ user p' $ withFastStore' $ \db -> setUserProfileContactLink db user Nothing let user' = case r of CRUserProfileUpdated u' _ _ _ -> u' _ -> user @@ -1583,54 +1757,54 @@ processChatCommand' vr = \case DeleteMyAddress -> withUser $ \User {userId} -> processChatCommand $ APIDeleteMyAddress userId APIShowMyAddress userId -> withUserId' userId $ \user -> - CRUserContactLink user <$> withStore (`getUserAddress` user) + CRUserContactLink user <$> withFastStore (`getUserAddress` user) ShowMyAddress -> withUser' $ \User {userId} -> processChatCommand $ APIShowMyAddress userId APISetProfileAddress userId False -> withUserId userId $ \user@User {profile = p} -> do let p' = (fromLocalProfile p :: Profile) {contactLink = Nothing} - updateProfile_ user p' $ withStore' $ \db -> setUserProfileContactLink db user Nothing + updateProfile_ user p' $ withFastStore' $ \db -> setUserProfileContactLink db user Nothing APISetProfileAddress userId True -> withUserId userId $ \user@User {profile = p} -> do - ucl@UserContactLink {connReqContact} <- withStore (`getUserAddress` user) + ucl@UserContactLink {connReqContact} <- withFastStore (`getUserAddress` user) let p' = (fromLocalProfile p :: Profile) {contactLink = Just connReqContact} - updateProfile_ user p' $ withStore' $ \db -> setUserProfileContactLink db user $ Just ucl + updateProfile_ user p' $ withFastStore' $ \db -> setUserProfileContactLink db user $ Just ucl SetProfileAddress onOff -> withUser $ \User {userId} -> processChatCommand $ APISetProfileAddress userId onOff APIAddressAutoAccept userId autoAccept_ -> withUserId userId $ \user -> do - contactLink <- withStore (\db -> updateUserAddressAutoAccept db user autoAccept_) + contactLink <- withFastStore (\db -> updateUserAddressAutoAccept db user autoAccept_) pure $ CRUserContactLinkUpdated user contactLink AddressAutoAccept autoAccept_ -> withUser $ \User {userId} -> processChatCommand $ APIAddressAutoAccept userId autoAccept_ AcceptContact incognito cName -> withUser $ \User {userId} -> do - connReqId <- withStore $ \db -> getContactRequestIdByName db userId cName + connReqId <- withFastStore $ \db -> getContactRequestIdByName db userId cName processChatCommand $ APIAcceptContact incognito connReqId RejectContact cName -> withUser $ \User {userId} -> do - connReqId <- withStore $ \db -> getContactRequestIdByName db userId cName + connReqId <- withFastStore $ \db -> getContactRequestIdByName db userId cName processChatCommand $ APIRejectContact connReqId ForwardMessage toChatName fromContactName forwardedMsg -> withUser $ \user -> do - contactId <- withStore $ \db -> getContactIdByName db user fromContactName - forwardedItemId <- withStore $ \db -> getDirectChatItemIdByText' db user contactId forwardedMsg + contactId <- withFastStore $ \db -> getContactIdByName db user fromContactName + forwardedItemId <- withFastStore $ \db -> getDirectChatItemIdByText' db user contactId forwardedMsg toChatRef <- getChatRef user toChatName - processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTDirect contactId) forwardedItemId + processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTDirect contactId) forwardedItemId Nothing ForwardGroupMessage toChatName fromGroupName fromMemberName_ forwardedMsg -> withUser $ \user -> do - groupId <- withStore $ \db -> getGroupIdByName db user fromGroupName - forwardedItemId <- withStore $ \db -> getGroupChatItemIdByText db user groupId fromMemberName_ forwardedMsg + groupId <- withFastStore $ \db -> getGroupIdByName db user fromGroupName + forwardedItemId <- withFastStore $ \db -> getGroupChatItemIdByText db user groupId fromMemberName_ forwardedMsg toChatRef <- getChatRef user toChatName - processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTGroup groupId) forwardedItemId + processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTGroup groupId) forwardedItemId Nothing ForwardLocalMessage toChatName forwardedMsg -> withUser $ \user -> do - folderId <- withStore (`getUserNoteFolderId` user) - forwardedItemId <- withStore $ \db -> getLocalChatItemIdByText' db user folderId forwardedMsg + folderId <- withFastStore (`getUserNoteFolderId` user) + forwardedItemId <- withFastStore $ \db -> getLocalChatItemIdByText' db user folderId forwardedMsg toChatRef <- getChatRef user toChatName - processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTLocal folderId) forwardedItemId + processChatCommand $ APIForwardChatItem toChatRef (ChatRef CTLocal folderId) forwardedItemId Nothing SendMessage (ChatName cType name) msg -> withUser $ \user -> do let mc = MCText msg case cType of CTDirect -> - withStore' (\db -> runExceptT $ getContactIdByName db user name) >>= \case + withFastStore' (\db -> runExceptT $ getContactIdByName db user name) >>= \case Right ctId -> do let chatRef = ChatRef CTDirect ctId processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage Nothing Nothing mc Left _ -> - withStore' (\db -> runExceptT $ getActiveMembersByName db vr user name) >>= \case + withFastStore' (\db -> runExceptT $ getActiveMembersByName db vr user name) >>= \case Right [(gInfo, member)] -> do let GroupInfo {localDisplayName = gName} = gInfo GroupMember {localDisplayName = mName} = member @@ -1640,22 +1814,22 @@ processChatCommand' vr = \case _ -> throwChatError $ CEContactNotFound name Nothing CTGroup -> do - gId <- withStore $ \db -> getGroupIdByName db user name + gId <- withFastStore $ \db -> getGroupIdByName db user name let chatRef = ChatRef CTGroup gId processChatCommand . APISendMessage chatRef False Nothing $ ComposedMessage Nothing Nothing mc CTLocal | name == "" -> do - folderId <- withStore (`getUserNoteFolderId` user) + folderId <- withFastStore (`getUserNoteFolderId` user) processChatCommand . APICreateChatItem folderId $ ComposedMessage Nothing Nothing mc | otherwise -> throwChatError $ CECommandError "not supported" _ -> throwChatError $ CECommandError "not supported" SendMemberContactMessage gName mName msg -> withUser $ \user -> do (gId, mId) <- getGroupAndMemberId user gName mName - m <- withStore $ \db -> getGroupMember db vr user gId mId + m <- withFastStore $ \db -> getGroupMember db vr user gId mId let mc = MCText msg case memberContactId m of Nothing -> do - g <- withStore $ \db -> getGroupInfo db vr user gId + g <- withFastStore $ \db -> getGroupInfo db vr user gId unless (groupFeatureMemberAllowed SGFDirectMessages (membership g) g) $ throwChatError $ CECommandError "direct messages not allowed" toView $ CRNoMemberContactCreating user g m processChatCommand (APICreateMemberContact gId mId) >>= \case @@ -1671,7 +1845,7 @@ processChatCommand' vr = \case let mc = MCText msg processChatCommand . APISendMessage chatRef True Nothing $ ComposedMessage Nothing Nothing mc SendMessageBroadcast msg -> withUser $ \user -> do - contacts <- withStore' $ \db -> getUserContacts db vr user + contacts <- withFastStore' $ \db -> getUserContacts db vr user withChatLock "sendMessageBroadcast" . procCmd $ do let ctConns_ = L.nonEmpty $ foldr addContactConn [] contacts case ctConns_ of @@ -1681,7 +1855,7 @@ processChatCommand' vr = \case Just (ctConns :: NonEmpty (Contact, Connection)) -> do let idsEvts = L.map ctSndEvent ctConns sndMsgs <- lift $ createSndMessages idsEvts - let msgReqs_ :: NonEmpty (Either ChatError MsgReq) = L.zipWith (fmap . ctMsgReq) ctConns sndMsgs + let msgReqs_ :: NonEmpty (Either ChatError ChatMsgReq) = L.zipWith (fmap . ctMsgReq) ctConns sndMsgs (errs, ctSndMsgs :: [(Contact, SndMessage)]) <- partitionEithers . L.toList . zipWith3' combineResults ctConns sndMsgs <$> deliverMessagesB msgReqs_ timestamp <- liftIO getCurrentTime @@ -1695,11 +1869,11 @@ processChatCommand' vr = \case _ -> ctConns ctSndEvent :: (Contact, Connection) -> (ConnOrGroupId, ChatMsgEvent 'Json) ctSndEvent (_, Connection {connId}) = (ConnectionId connId, XMsgNew $ MCSimple (extMsgContent mc Nothing)) - ctMsgReq :: (Contact, Connection) -> SndMessage -> MsgReq - ctMsgReq (_, conn) SndMessage {msgId, msgBody} = (conn, MsgFlags {notification = hasNotification XMsgNew_}, msgBody, msgId) + ctMsgReq :: (Contact, Connection) -> SndMessage -> ChatMsgReq + ctMsgReq (_, conn) SndMessage {msgId, msgBody} = (conn, MsgFlags {notification = hasNotification XMsgNew_}, msgBody, [msgId]) zipWith3' :: (a -> b -> c -> d) -> NonEmpty a -> NonEmpty b -> NonEmpty c -> NonEmpty d zipWith3' f ~(x :| xs) ~(y :| ys) ~(z :| zs) = f x y z :| zipWith3 f xs ys zs - combineResults :: (Contact, Connection) -> Either ChatError SndMessage -> Either ChatError (Int64, PQEncryption) -> Either ChatError (Contact, SndMessage) + combineResults :: (Contact, Connection) -> Either ChatError SndMessage -> Either ChatError ([Int64], PQEncryption) -> Either ChatError (Contact, SndMessage) combineResults (ct, _) (Right msg') (Right _) = Right (ct, msg') combineResults _ (Left e) _ = Left e combineResults _ _ (Left e) = Left e @@ -1707,18 +1881,18 @@ processChatCommand' vr = \case createCI db user createdAt (ct, sndMsg) = void $ createNewSndChatItem db user (CDDirectSnd ct) sndMsg (CISndMsgContent mc) Nothing Nothing Nothing False createdAt SendMessageQuote cName (AMsgDirection msgDir) quotedMsg msg -> withUser $ \user@User {userId} -> do - contactId <- withStore $ \db -> getContactIdByName db user cName - quotedItemId <- withStore $ \db -> getDirectChatItemIdByText db userId contactId msgDir quotedMsg + contactId <- withFastStore $ \db -> getContactIdByName db user cName + quotedItemId <- withFastStore $ \db -> getDirectChatItemIdByText db userId contactId msgDir quotedMsg let mc = MCText msg processChatCommand . APISendMessage (ChatRef CTDirect contactId) False Nothing $ ComposedMessage Nothing (Just quotedItemId) mc DeleteMessage chatName deletedMsg -> withUser $ \user -> do chatRef <- getChatRef user chatName deletedItemId <- getSentChatItemIdByText user chatRef deletedMsg - processChatCommand $ APIDeleteChatItem chatRef deletedItemId CIDMBroadcast + processChatCommand $ APIDeleteChatItem chatRef (deletedItemId :| []) CIDMBroadcast DeleteMemberMessage gName mName deletedMsg -> withUser $ \user -> do - (gId, mId) <- getGroupAndMemberId user gName mName - deletedItemId <- withStore $ \db -> getGroupChatItemIdByText db user gId (Just mName) deletedMsg - processChatCommand $ APIDeleteMemberChatItem gId mId deletedItemId + gId <- withFastStore $ \db -> getGroupIdByName db user gName + deletedItemId <- withFastStore $ \db -> getGroupChatItemIdByText db user gId (Just mName) deletedMsg + processChatCommand $ APIDeleteMemberChatItem gId (deletedItemId :| []) EditMessage chatName editedMsg msg -> withUser $ \user -> do chatRef <- getChatRef user chatName editedItemId <- getSentChatItemIdByText user chatRef editedMsg @@ -1737,14 +1911,14 @@ processChatCommand' vr = \case gVar <- asks random -- [incognito] generate incognito profile for group membership incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing - groupInfo <- withStore $ \db -> createNewGroup db vr gVar user gProfile incognitoProfile + groupInfo <- withFastStore $ \db -> createNewGroup db vr gVar user gProfile incognitoProfile createInternalChatItem user (CDGroupSnd groupInfo) (CISndGroupE2EEInfo $ E2EInfo {pqEnabled = PQEncOff}) Nothing pure $ CRGroupCreated user groupInfo NewGroup incognito gProfile -> withUser $ \User {userId} -> processChatCommand $ APINewGroup userId incognito gProfile APIAddMember groupId contactId memRole -> withUser $ \user -> withGroupLock "addMember" groupId $ do -- TODO for large groups: no need to load all members to determine if contact is a member - (group, contact) <- withStore $ \db -> (,) <$> getGroup db vr user groupId <*> getContact db vr user contactId + (group, contact) <- withFastStore $ \db -> (,) <$> getGroup db vr user groupId <*> getContact db vr user contactId assertDirectAllowed user MDSnd contact XGrpInv_ let Group gInfo members = group Contact {localDisplayName = cName} = contact @@ -1759,13 +1933,13 @@ processChatCommand' vr = \case gVar <- asks random subMode <- chatReadVar subscriptionMode (agentConnId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing IKPQOff subMode - member <- withStore $ \db -> createNewContactMember db gVar user gInfo contact memRole agentConnId cReq subMode + member <- withFastStore $ \db -> createNewContactMember db gVar user gInfo contact memRole agentConnId cReq subMode sendInvitation member cReq pure $ CRSentGroupInvitation user gInfo contact member Just member@GroupMember {groupMemberId, memberStatus, memberRole = mRole} | memberStatus == GSMemInvited -> do - unless (mRole == memRole) $ withStore' $ \db -> updateGroupMemberRole db user member memRole - withStore' (\db -> getMemberInvitation db user groupMemberId) >>= \case + unless (mRole == memRole) $ withFastStore' $ \db -> updateGroupMemberRole db user member memRole + withFastStore' (\db -> getMemberInvitation db user groupMemberId) >>= \case Just cReq -> do sendInvitation member {memberRole = memRole} cReq pure $ CRSentGroupInvitation user gInfo contact member {memberRole = memRole} @@ -1773,7 +1947,7 @@ processChatCommand' vr = \case | otherwise -> throwChatError $ CEGroupDuplicateMember cName APIJoinGroup groupId -> withUser $ \user@User {userId} -> do withGroupLock "joinGroup" groupId . procCmd $ do - (invitation, ct) <- withStore $ \db -> do + (invitation, ct) <- withFastStore $ \db -> do inv@ReceivedGroupInvitation {fromMember} <- getGroupInvitation db vr user groupId (inv,) <$> getContactViaMember db vr user fromMember let ReceivedGroupInvitation {fromMember, connRequest, groupInfo = g@GroupInfo {membership}} = invitation @@ -1785,16 +1959,24 @@ processChatCommand' vr = \case dm <- encodeConnInfo $ XGrpAcpt membershipMemId agentConnId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True connRequest PQSupportOff let chatV = vr `peerConnChatVersion` peerChatVRange - withStore' $ \db -> do - createMemberConnection db userId fromMember agentConnId chatV peerChatVRange subMode + cId <- withFastStore' $ \db -> do + Connection {connId = cId} <- createMemberConnection db userId fromMember agentConnId chatV peerChatVRange subMode updateGroupMemberStatus db userId fromMember GSMemAccepted updateGroupMemberStatus db userId membership GSMemAccepted - void . withAgent $ \a -> joinConnection a (aUserId user) (Just agentConnId) True connRequest dm PQSupportOff subMode - updateCIGroupInvitationStatus user g CIGISAccepted `catchChatError` \_ -> pure () + pure cId + void (withAgent $ \a -> joinConnection a (aUserId user) (Just agentConnId) True connRequest dm PQSupportOff subMode) + `catchChatError` \e -> do + withFastStore' $ \db -> do + deleteConnectionRecord db user cId + updateGroupMemberStatus db userId fromMember GSMemInvited + updateGroupMemberStatus db userId membership GSMemInvited + withAgent $ \a -> deleteConnectionAsync a False agentConnId + throwError e + updateCIGroupInvitationStatus user g CIGISAccepted `catchChatError` (toView . CRChatError (Just user)) pure $ CRUserAcceptedGroupSent user g {membership = membership {memberStatus = GSMemAccepted}} Nothing Nothing -> throwChatError $ CEContactNotActive ct APIMemberRole groupId memberId memRole -> withUser $ \user -> do - Group gInfo@GroupInfo {membership} members <- withStore $ \db -> getGroup db vr user groupId + Group gInfo@GroupInfo {membership} members <- withFastStore $ \db -> getGroup db vr user groupId if memberId == groupMemberId' membership then changeMemberRole user gInfo members membership $ SGEUserRole memRole else case find ((== memberId) . groupMemberId') members of @@ -1806,10 +1988,10 @@ processChatCommand' vr = \case assertUserGroupRole gInfo $ maximum [GRAdmin, mRole, memRole] withGroupLock "memberRole" groupId . procCmd $ do unless (mRole == memRole) $ do - withStore' $ \db -> updateGroupMemberRole db user m memRole + withFastStore' $ \db -> updateGroupMemberRole db user m memRole case mStatus of GSMemInvited -> do - withStore (\db -> (,) <$> mapM (getContact db vr user) memberContactId <*> liftIO (getMemberInvitation db user $ groupMemberId' m)) >>= \case + withFastStore (\db -> (,) <$> mapM (getContact db vr user) memberContactId <*> liftIO (getMemberInvitation db user $ groupMemberId' m)) >>= \case (Just ct, Just cReq) -> sendGrpInvitation user ct gInfo (m :: GroupMember) {memberRole = memRole} cReq _ -> throwChatError $ CEGroupCantResendInvitation gInfo cName _ -> do @@ -1818,7 +2000,7 @@ processChatCommand' vr = \case toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) pure CRMemberRoleUser {user, groupInfo = gInfo, member = m {memberRole = memRole}, fromRole = mRole, toRole = memRole} APIBlockMemberForAll groupId memberId blocked -> withUser $ \user -> do - Group gInfo@GroupInfo {membership} members <- withStore $ \db -> getGroup db vr user groupId + Group gInfo@GroupInfo {membership} members <- withFastStore $ \db -> getGroup db vr user groupId when (memberId == groupMemberId' membership) $ throwChatError $ CECommandError "can't block/unblock self" case splitMember memberId members of Nothing -> throwChatError $ CEException "expected to find a single blocked member" @@ -1829,11 +2011,11 @@ processChatCommand' vr = \case withGroupLock "blockForAll" groupId . procCmd $ do let mrs = if blocked then MRSBlocked else MRSUnrestricted event = XGrpMemRestrict bmMemberId MemberRestrictions {restriction = mrs} - (msg, _) <- sendGroupMessage' user gInfo remainingMembers event + msg <- sendGroupMessage' user gInfo remainingMembers event let ciContent = CISndGroupEvent $ SGEMemberBlocked memberId (fromLocalProfile bmp) blocked ci <- saveSndChatItem user (CDGroupSnd gInfo) msg ciContent toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) - bm' <- withStore $ \db -> do + bm' <- withFastStore $ \db -> do liftIO $ updateGroupMemberBlocked db user groupId memberId mrs getGroupMember db vr user groupId memberId toggleNtf user bm' (not blocked) @@ -1843,7 +2025,7 @@ processChatCommand' vr = \case (_, []) -> Nothing (ms1, bm : ms2) -> Just (bm, ms1 <> ms2) APIRemoveMember groupId memberId -> withUser $ \user -> do - Group gInfo members <- withStore $ \db -> getGroup db vr user groupId + Group gInfo members <- withFastStore $ \db -> getGroup db vr user groupId case find ((== memberId) . groupMemberId') members of Nothing -> throwChatError CEGroupMemberNotFound Just m@GroupMember {memberId = mId, memberRole = mRole, memberStatus = mStatus, memberProfile} -> do @@ -1852,7 +2034,7 @@ processChatCommand' vr = \case case mStatus of GSMemInvited -> do deleteMemberConnection user m - withStore' $ \db -> deleteGroupMember db user m + withFastStore' $ \db -> deleteGroupMember db user m _ -> do (msg, _) <- sendGroupMessage user gInfo members $ XGrpMemDel mId ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent $ SGEMemberDeleted memberId (fromLocalProfile memberProfile)) @@ -1862,85 +2044,85 @@ processChatCommand' vr = \case deleteOrUpdateMemberRecord user m pure $ CRUserDeletedMember user gInfo m {memberStatus = GSMemRemoved} APILeaveGroup groupId -> withUser $ \user@User {userId} -> do - Group gInfo@GroupInfo {membership} members <- withStore $ \db -> getGroup db vr user groupId - filesInfo <- withStore' $ \db -> getGroupFileInfo db user gInfo + Group gInfo@GroupInfo {membership} members <- withFastStore $ \db -> getGroup db vr user groupId + filesInfo <- withFastStore' $ \db -> getGroupFileInfo db user gInfo withGroupLock "leaveGroup" groupId . procCmd $ do cancelFilesInProgress user filesInfo - (msg, _) <- sendGroupMessage' user gInfo members XGrpLeave + msg <- sendGroupMessage' user gInfo members XGrpLeave ci <- saveSndChatItem user (CDGroupSnd gInfo) msg (CISndGroupEvent SGEUserLeft) toView $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) -- TODO delete direct connections that were unused deleteGroupLinkIfExists user gInfo -- member records are not deleted to keep history deleteMembersConnections' user members True - withStore' $ \db -> updateGroupMemberStatus db userId membership GSMemLeft + withFastStore' $ \db -> updateGroupMemberStatus db userId membership GSMemLeft pure $ CRLeftMemberUser user gInfo {membership = membership {memberStatus = GSMemLeft}} APIListMembers groupId -> withUser $ \user -> - CRGroupMembers user <$> withStore (\db -> getGroup db vr user groupId) + CRGroupMembers user <$> withFastStore (\db -> getGroup db vr user groupId) AddMember gName cName memRole -> withUser $ \user -> do - (groupId, contactId) <- withStore $ \db -> (,) <$> getGroupIdByName db user gName <*> getContactIdByName db user cName + (groupId, contactId) <- withFastStore $ \db -> (,) <$> getGroupIdByName db user gName <*> getContactIdByName db user cName processChatCommand $ APIAddMember groupId contactId memRole JoinGroup gName -> withUser $ \user -> do - groupId <- withStore $ \db -> getGroupIdByName db user gName + groupId <- withFastStore $ \db -> getGroupIdByName db user gName processChatCommand $ APIJoinGroup groupId MemberRole gName gMemberName memRole -> withMemberName gName gMemberName $ \gId gMemberId -> APIMemberRole gId gMemberId memRole BlockForAll gName gMemberName blocked -> withMemberName gName gMemberName $ \gId gMemberId -> APIBlockMemberForAll gId gMemberId blocked RemoveMember gName gMemberName -> withMemberName gName gMemberName APIRemoveMember LeaveGroup gName -> withUser $ \user -> do - groupId <- withStore $ \db -> getGroupIdByName db user gName + groupId <- withFastStore $ \db -> getGroupIdByName db user gName processChatCommand $ APILeaveGroup groupId DeleteGroup gName -> withUser $ \user -> do - groupId <- withStore $ \db -> getGroupIdByName db user gName - processChatCommand $ APIDeleteChat (ChatRef CTGroup groupId) True + groupId <- withFastStore $ \db -> getGroupIdByName db user gName + processChatCommand $ APIDeleteChat (ChatRef CTGroup groupId) (CDMFull True) ClearGroup gName -> withUser $ \user -> do - groupId <- withStore $ \db -> getGroupIdByName db user gName + groupId <- withFastStore $ \db -> getGroupIdByName db user gName processChatCommand $ APIClearChat (ChatRef CTGroup groupId) ListMembers gName -> withUser $ \user -> do - groupId <- withStore $ \db -> getGroupIdByName db user gName + groupId <- withFastStore $ \db -> getGroupIdByName db user gName processChatCommand $ APIListMembers groupId APIListGroups userId contactId_ search_ -> withUserId userId $ \user -> - CRGroupsList user <$> withStore' (\db -> getUserGroupsWithSummary db vr user contactId_ search_) + CRGroupsList user <$> withFastStore' (\db -> getUserGroupsWithSummary db vr user contactId_ search_) ListGroups cName_ search_ -> withUser $ \user@User {userId} -> do - ct_ <- forM cName_ $ \cName -> withStore $ \db -> getContactByName db vr user cName + ct_ <- forM cName_ $ \cName -> withFastStore $ \db -> getContactByName db vr user cName processChatCommand $ APIListGroups userId (contactId' <$> ct_) search_ APIUpdateGroupProfile groupId p' -> withUser $ \user -> do - g <- withStore $ \db -> getGroup db vr user groupId + g <- withFastStore $ \db -> getGroup db vr user groupId runUpdateGroupProfile user g p' UpdateGroupNames gName GroupProfile {displayName, fullName} -> updateGroupProfileByName gName $ \p -> p {displayName, fullName} ShowGroupProfile gName -> withUser $ \user -> - CRGroupProfile user <$> withStore (\db -> getGroupInfoByName db vr user gName) + CRGroupProfile user <$> withFastStore (\db -> getGroupInfoByName db vr user gName) UpdateGroupDescription gName description -> updateGroupProfileByName gName $ \p -> p {description} ShowGroupDescription gName -> withUser $ \user -> - CRGroupDescription user <$> withStore (\db -> getGroupInfoByName db vr user gName) + CRGroupDescription user <$> withFastStore (\db -> getGroupInfoByName db vr user gName) APICreateGroupLink groupId mRole -> withUser $ \user -> withGroupLock "createGroupLink" groupId $ do - gInfo <- withStore $ \db -> getGroupInfo db vr user groupId + gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId assertUserGroupRole gInfo GRAdmin when (mRole > GRMember) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole groupLinkId <- GroupLinkId <$> drgRandomBytes 16 subMode <- chatReadVar subscriptionMode let crClientData = encodeJSON $ CRDataGroup groupLinkId (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMContact (Just crClientData) IKPQOff subMode - withStore $ \db -> createGroupLink db user gInfo connId cReq groupLinkId mRole subMode + withFastStore $ \db -> createGroupLink db user gInfo connId cReq groupLinkId mRole subMode pure $ CRGroupLinkCreated user gInfo cReq mRole APIGroupLinkMemberRole groupId mRole' -> withUser $ \user -> withGroupLock "groupLinkMemberRole" groupId $ do - gInfo <- withStore $ \db -> getGroupInfo db vr user groupId - (groupLinkId, groupLink, mRole) <- withStore $ \db -> getGroupLink db user gInfo + gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId + (groupLinkId, groupLink, mRole) <- withFastStore $ \db -> getGroupLink db user gInfo assertUserGroupRole gInfo GRAdmin when (mRole' > GRMember) $ throwChatError $ CEGroupMemberInitialRole gInfo mRole' - when (mRole' /= mRole) $ withStore' $ \db -> setGroupLinkMemberRole db user groupLinkId mRole' + when (mRole' /= mRole) $ withFastStore' $ \db -> setGroupLinkMemberRole db user groupLinkId mRole' pure $ CRGroupLink user gInfo groupLink mRole' APIDeleteGroupLink groupId -> withUser $ \user -> withGroupLock "deleteGroupLink" groupId $ do - gInfo <- withStore $ \db -> getGroupInfo db vr user groupId + gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId deleteGroupLink' user gInfo pure $ CRGroupLinkDeleted user gInfo APIGetGroupLink groupId -> withUser $ \user -> do - gInfo <- withStore $ \db -> getGroupInfo db vr user groupId - (_, groupLink, mRole) <- withStore $ \db -> getGroupLink db user gInfo + gInfo <- withFastStore $ \db -> getGroupInfo db vr user groupId + (_, groupLink, mRole) <- withFastStore $ \db -> getGroupLink db user gInfo pure $ CRGroupLink user gInfo groupLink mRole APICreateMemberContact gId gMemberId -> withUser $ \user -> do - (g, m) <- withStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId + (g, m) <- withFastStore $ \db -> (,) <$> getGroupInfo db vr user gId <*> getGroupMember db vr user gId gMemberId assertUserGroupRole g GRAuthor unless (groupFeatureMemberAllowed SGFDirectMessages (membership g) g) $ throwChatError $ CECommandError "direct messages not allowed" case memberConn m of @@ -1951,19 +2133,19 @@ processChatCommand' vr = \case -- TODO PQ should negotitate contact connection with PQSupportOn? (connId, cReq) <- withAgent $ \a -> createConnection a (aUserId user) True SCMInvitation Nothing IKPQOff subMode -- [incognito] reuse membership incognito profile - ct <- withStore' $ \db -> createMemberContact db user connId cReq g m mConn subMode + ct <- withFastStore' $ \db -> createMemberContact db user connId cReq g m mConn subMode -- TODO not sure it is correct to set connections status here? lift $ setContactNetworkStatus ct NSConnected pure $ CRNewMemberContact user ct g m _ -> throwChatError CEGroupMemberNotActive APISendMemberContactInvitation contactId msgContent_ -> withUser $ \user -> do - (g@GroupInfo {groupId}, m, ct, cReq) <- withStore $ \db -> getMemberContact db vr user contactId + (g@GroupInfo {groupId}, m, ct, cReq) <- withFastStore $ \db -> getMemberContact db vr user contactId when (contactGrpInvSent ct) $ throwChatError $ CECommandError "x.grp.direct.inv already sent" case memberConn m of Just mConn -> do let msg = XGrpDirectInv cReq msgContent_ (sndMsg, _, _) <- sendDirectMemberMessage mConn msg groupId - withStore' $ \db -> setContactGrpInvSent db ct True + withFastStore' $ \db -> setContactGrpInvSent db ct True let ct' = ct {contactGrpInvSent = True} forM_ msgContent_ $ \mc -> do ci <- saveSndChatItem user (CDDirectSnd ct') sndMsg (CISndMsgContent mc) @@ -1971,28 +2153,28 @@ processChatCommand' vr = \case pure $ CRNewMemberContactSentInv user ct' g m _ -> throwChatError CEGroupMemberNotActive CreateGroupLink gName mRole -> withUser $ \user -> do - groupId <- withStore $ \db -> getGroupIdByName db user gName + groupId <- withFastStore $ \db -> getGroupIdByName db user gName processChatCommand $ APICreateGroupLink groupId mRole GroupLinkMemberRole gName mRole -> withUser $ \user -> do - groupId <- withStore $ \db -> getGroupIdByName db user gName + groupId <- withFastStore $ \db -> getGroupIdByName db user gName processChatCommand $ APIGroupLinkMemberRole groupId mRole DeleteGroupLink gName -> withUser $ \user -> do - groupId <- withStore $ \db -> getGroupIdByName db user gName + groupId <- withFastStore $ \db -> getGroupIdByName db user gName processChatCommand $ APIDeleteGroupLink groupId ShowGroupLink gName -> withUser $ \user -> do - groupId <- withStore $ \db -> getGroupIdByName db user gName + groupId <- withFastStore $ \db -> getGroupIdByName db user gName processChatCommand $ APIGetGroupLink groupId SendGroupMessageQuote gName cName quotedMsg msg -> withUser $ \user -> do - groupId <- withStore $ \db -> getGroupIdByName db user gName - quotedItemId <- withStore $ \db -> getGroupChatItemIdByText db user groupId cName quotedMsg + groupId <- withFastStore $ \db -> getGroupIdByName db user gName + quotedItemId <- withFastStore $ \db -> getGroupChatItemIdByText db user groupId cName quotedMsg let mc = MCText msg processChatCommand . APISendMessage (ChatRef CTGroup groupId) False Nothing $ ComposedMessage Nothing (Just quotedItemId) mc ClearNoteFolder -> withUser $ \user -> do - folderId <- withStore (`getUserNoteFolderId` user) + folderId <- withFastStore (`getUserNoteFolderId` user) processChatCommand $ APIClearChat (ChatRef CTLocal folderId) LastChats count_ -> withUser' $ \user -> do let count = fromMaybe 5000 count_ - (errs, previews) <- partitionEithers <$> withStore' (\db -> getChatPreviews db vr user False (PTLast count) clqNoFilters) + (errs, previews) <- partitionEithers <$> withFastStore' (\db -> getChatPreviews db vr user False (PTLast count) clqNoFilters) unless (null errs) $ toView $ CRChatErrors (Just user) (map ChatErrorStore errs) pure $ CRChats previews LastMessages (Just chatName) count search -> withUser $ \user -> do @@ -2000,22 +2182,22 @@ processChatCommand' vr = \case chatResp <- processChatCommand $ APIGetChat chatRef (CPLast count) search pure $ CRChatItems user (Just chatName) (aChatItems . chat $ chatResp) LastMessages Nothing count search -> withUser $ \user -> do - chatItems <- withStore $ \db -> getAllChatItems db vr user (CPLast count) search + chatItems <- withFastStore $ \db -> getAllChatItems db vr user (CPLast count) search pure $ CRChatItems user Nothing chatItems LastChatItemId (Just chatName) index -> withUser $ \user -> do chatRef <- getChatRef user chatName chatResp <- processChatCommand (APIGetChat chatRef (CPLast $ index + 1) Nothing) pure $ CRChatItemId user (fmap aChatItemId . listToMaybe . aChatItems . chat $ chatResp) LastChatItemId Nothing index -> withUser $ \user -> do - chatItems <- withStore $ \db -> getAllChatItems db vr user (CPLast $ index + 1) Nothing + chatItems <- withFastStore $ \db -> getAllChatItems db vr user (CPLast $ index + 1) Nothing pure $ CRChatItemId user (fmap aChatItemId . listToMaybe $ chatItems) ShowChatItem (Just itemId) -> withUser $ \user -> do - chatItem <- withStore $ \db -> do + chatItem <- withFastStore $ \db -> do chatRef <- getChatRefViaItemId db user itemId getAChatItem db vr user chatRef itemId pure $ CRChatItems user Nothing ((: []) chatItem) ShowChatItem Nothing -> withUser $ \user -> do - chatItems <- withStore $ \db -> getAllChatItems db vr user (CPLast 1) Nothing + chatItems <- withFastStore $ \db -> getAllChatItems db vr user (CPLast 1) Nothing pure $ CRChatItems user Nothing chatItems ShowChatItemInfo chatName msg -> withUser $ \user -> do chatRef <- getChatRef user chatName @@ -2039,21 +2221,22 @@ processChatCommand' vr = \case ForwardFile chatName fileId -> forwardFile chatName fileId SendFile ForwardImage chatName fileId -> forwardFile chatName fileId SendImage SendFileDescription _chatName _f -> pure $ chatCmdError Nothing "TODO" - ReceiveFile fileId encrypted_ rcvInline_ filePath_ -> withUser $ \_ -> + -- TODO to use priority transactions we need a parameter that differentiates manual and automatic acceptance + ReceiveFile fileId userApprovedRelays encrypted_ rcvInline_ filePath_ -> withUser $ \_ -> withFileLock "receiveFile" fileId . procCmd $ do (user, ft@RcvFileTransfer {fileStatus}) <- withStore (`getRcvFileTransferById` fileId) encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles ft' <- (if encrypt && fileStatus == RFSNew then setFileToEncrypt else pure) ft - receiveFile' user ft' rcvInline_ filePath_ - SetFileToReceive fileId encrypted_ -> withUser $ \_ -> do + receiveFile' user ft' userApprovedRelays rcvInline_ filePath_ + SetFileToReceive fileId userApprovedRelays encrypted_ -> withUser $ \_ -> do withFileLock "setFileToReceive" fileId . procCmd $ do encrypt <- (`fromMaybe` encrypted_) <$> chatReadVar encryptLocalFiles cfArgs <- if encrypt then Just <$> (atomically . CF.randomArgs =<< asks random) else pure Nothing - withStore' $ \db -> setRcvFileToReceive db fileId cfArgs + withStore' $ \db -> setRcvFileToReceive db fileId userApprovedRelays cfArgs ok_ CancelFile fileId -> withUser $ \user@User {userId} -> withFileLock "cancelFile" fileId . procCmd $ - withStore (\db -> getFileTransfer db user fileId) >>= \case + withFastStore (\db -> getFileTransfer db user fileId) >>= \case FTSnd ftm@FileTransferMeta {xftpSndFile, cancelled} fts | cancelled -> throwChatError $ CEFileCancel fileId "file already cancelled" | not (null fts) && all fileCancelledOrCompleteSMP fts -> @@ -2061,16 +2244,16 @@ processChatCommand' vr = \case | otherwise -> do fileAgentConnIds <- cancelSndFile user ftm fts True deleteAgentConnectionsAsync user fileAgentConnIds - withStore (\db -> liftIO $ lookupChatRefByFileId db user fileId) >>= \case + withFastStore (\db -> liftIO $ lookupChatRefByFileId db user fileId) >>= \case Nothing -> pure () Just (ChatRef CTDirect contactId) -> do - (contact, sharedMsgId) <- withStore $ \db -> (,) <$> getContact db vr user contactId <*> getSharedMsgIdByFileId db userId fileId + (contact, sharedMsgId) <- withFastStore $ \db -> (,) <$> getContact db vr user contactId <*> getSharedMsgIdByFileId db userId fileId void . sendDirectContactMessage user contact $ XFileCancel sharedMsgId Just (ChatRef CTGroup groupId) -> do - (Group gInfo ms, sharedMsgId) <- withStore $ \db -> (,) <$> getGroup db vr user groupId <*> getSharedMsgIdByFileId db userId fileId + (Group gInfo ms, sharedMsgId) <- withFastStore $ \db -> (,) <$> getGroup db vr user groupId <*> getSharedMsgIdByFileId db userId fileId void . sendGroupMessage user gInfo ms $ XFileCancel sharedMsgId Just _ -> throwChatError $ CEFileInternal "invalid chat ref for file transfer" - ci <- withStore $ \db -> lookupChatItemByFileId db vr user fileId + ci <- withFastStore $ \db -> lookupChatItemByFileId db vr user fileId pure $ CRSndFileCancelled user ci ftm fts where fileCancelledOrCompleteSMP SndFileTransfer {fileStatus = s} = @@ -2081,7 +2264,7 @@ processChatCommand' vr = \case | otherwise -> case xftpRcvFile of Nothing -> do cancelRcvFileTransfer user ftr >>= mapM_ (deleteAgentConnectionAsync user) - ci <- withStore $ \db -> lookupChatItemByFileId db vr user fileId + ci <- withFastStore $ \db -> lookupChatItemByFileId db vr user fileId pure $ CRRcvFileCancelled user ci ftr Just XFTPRcvFile {agentRcvFileId} -> do forM_ (liveRcvFileTransferPath ftr) $ \filePath -> do @@ -2089,17 +2272,12 @@ processChatCommand' vr = \case liftIO $ removeFile fsFilePath `catchAll_` pure () lift . forM_ agentRcvFileId $ \(AgentRcvFileId aFileId) -> withAgent' (`xftpDeleteRcvFile` aFileId) - ci <- withStore $ \db -> do - liftIO $ do - updateCIFileStatus db user fileId CIFSRcvInvitation - updateRcvFileStatus db fileId FSNew - updateRcvFileAgentId db fileId Nothing - lookupChatItemByFileId db vr user fileId - pure $ CRRcvFileCancelled user ci ftr + aci_ <- resetRcvCIFileStatus user fileId CIFSRcvInvitation + pure $ CRRcvFileCancelled user aci_ ftr FileStatus fileId -> withUser $ \user -> do - withStore (\db -> lookupChatItemByFileId db vr user fileId) >>= \case + withFastStore (\db -> lookupChatItemByFileId db vr user fileId) >>= \case Nothing -> do - fileStatus <- withStore $ \db -> getFileTransferProgress db user fileId + fileStatus <- withFastStore $ \db -> getFileTransferProgress db user fileId pure $ CRFileTransferStatus user fileStatus Just ci@(AChatItem _ _ _ ChatItem {file}) -> case file of Just CIFile {fileProtocol = FPLocal} -> @@ -2107,7 +2285,7 @@ processChatCommand' vr = \case Just CIFile {fileProtocol = FPXFTP} -> pure $ CRFileTransferStatusXFTP user ci _ -> do - fileStatus <- withStore $ \db -> getFileTransferProgress db user fileId + fileStatus <- withFastStore $ \db -> getFileTransferProgress db user fileId pure $ CRFileTransferStatus user fileStatus ShowProfile -> withUser $ \user@User {profile} -> pure $ CRUserProfile user (fromLocalProfile profile) UpdateProfile displayName fullName -> withUser $ \user@User {profile} -> do @@ -2121,7 +2299,7 @@ processChatCommand' vr = \case let p = (fromLocalProfile profile :: Profile) {preferences = Just . setPreference f (Just allowed) $ preferences' user} updateProfile user p SetContactFeature (ACF f) cName allowed_ -> withUser $ \user -> do - ct@Contact {userPreferences} <- withStore $ \db -> getContactByName db vr user cName + ct@Contact {userPreferences} <- withFastStore $ \db -> getContactByName db vr user cName let prefs' = setPreference f allowed_ $ Just userPreferences updateContactPrefs user ct prefs' SetGroupFeature (AGFNR f) gName enabled -> @@ -2136,7 +2314,7 @@ processChatCommand' vr = \case p = (fromLocalProfile profile :: Profile) {preferences = Just . setPreference' SCFTimedMessages (Just pref) $ preferences' user} updateProfile user p SetContactTimedMessages cName timedMessagesEnabled_ -> withUser $ \user -> do - ct@Contact {userPreferences = userPreferences@Preferences {timedMessages}} <- withStore $ \db -> getContactByName db vr user cName + ct@Contact {userPreferences = userPreferences@Preferences {timedMessages}} <- withFastStore $ \db -> getContactByName db vr user cName let currentTTL = timedMessages >>= \TimedMessagesPreference {ttl} -> ttl pref_ = tmeToPref currentTTL <$> timedMessagesEnabled_ prefs' = setPreference' SCFTimedMessages pref_ $ Just userPreferences @@ -2180,7 +2358,7 @@ processChatCommand' vr = \case ShowVersion -> do -- simplexmqCommitQ makes iOS builds crash m( let versionInfo = coreVersionInfo "" - chatMigrations <- map upMigration <$> withStore' (Migrations.getCurrent . DB.conn) + chatMigrations <- map upMigration <$> withFastStore' (Migrations.getCurrent . DB.conn) agentMigrations <- withAgent getAgentMigrations pure $ CRVersionInfo {versionInfo, chatMigrations, agentMigrations} DebugLocks -> lift $ do @@ -2198,13 +2376,24 @@ processChatCommand' vr = \case CLUserContact ucId -> "UserContact " <> show ucId CLFile fId -> "File " <> show fId DebugEvent event -> toView event >> ok_ + GetAgentSubsTotal userId -> withUserId userId $ \user -> do + users <- withStore' $ \db -> getUsers db + let userIds = map aUserId $ filter (\u -> isNothing (viewPwdHash u) || aUserId u == aUserId user) users + (subsTotal, hasSession) <- lift $ withAgent' $ \a -> getAgentSubsTotal a userIds + pure $ CRAgentSubsTotal user subsTotal hasSession + GetAgentServersSummary userId -> withUserId userId $ \user -> do + agentServersSummary <- lift $ withAgent' getAgentServersSummary + cfg <- asks config + (users, smpServers, xftpServers) <- + withStore' $ \db -> (,,) <$> getUsers db <*> getServers db cfg user SPSMP <*> getServers db cfg user SPXFTP + let presentedServersSummary = toPresentedServersSummary agentServersSummary users user smpServers xftpServers _defaultNtfServers + pure $ CRAgentServersSummary user presentedServersSummary + where + getServers :: (ProtocolTypeI p, UserProtocol p) => DB.Connection -> ChatConfig -> User -> SProtocolType p -> IO (NonEmpty (ProtocolServer p)) + getServers db cfg user p = L.map (\ServerCfg {server} -> protoServer server) . useServers cfg p <$> getProtocolServers db user + ResetAgentServersStats -> withAgent resetAgentServersStats >> ok_ GetAgentWorkers -> lift $ CRAgentWorkersSummary <$> withAgent' getAgentWorkersSummary GetAgentWorkersDetails -> lift $ CRAgentWorkersDetails <$> withAgent' getAgentWorkersDetails - GetAgentStats -> lift $ CRAgentStats . map stat <$> withAgent' getAgentStats - where - stat (AgentStatsKey {host, clientTs, cmd, res}, count) = - map B.unpack [host, clientTs, cmd, res, bshow count] - ResetAgentStats -> lift (withAgent' resetAgentStats) >> ok_ GetAgentSubs -> lift $ summary <$> withAgent' getAgentSubscriptions where summary SubscriptionsInfo {activeSubscriptions, pendingSubscriptions, removedSubscriptions} = @@ -2219,6 +2408,7 @@ processChatCommand' vr = \case SubInfo {server, subError = Just e} -> M.alter (Just . maybe [e] (e :)) server m _ -> m GetAgentSubsDetails -> lift $ CRAgentSubsDetails <$> withAgent' getAgentSubscriptions + GetAgentQueuesInfo -> lift $ CRAgentQueuesInfo <$> withAgent' getAgentQueuesInfo -- CustomChatCommand is unsupported, it can be processed in preCmdHook -- in a modified CLI app or core - the hook should return Either ChatResponse ChatCommand CustomChatCommand _cmd -> withUser $ \user -> pure $ chatCmdError (Just user) "not supported" @@ -2241,10 +2431,10 @@ processChatCommand' vr = \case getChatRef :: User -> ChatName -> CM ChatRef getChatRef user (ChatName cType name) = ChatRef cType <$> case cType of - CTDirect -> withStore $ \db -> getContactIdByName db user name - CTGroup -> withStore $ \db -> getGroupIdByName db user name + CTDirect -> withFastStore $ \db -> getContactIdByName db user name + CTGroup -> withFastStore $ \db -> getGroupIdByName db user name CTLocal - | name == "" -> withStore (`getUserNoteFolderId` user) + | name == "" -> withFastStore (`getUserNoteFolderId` user) | otherwise -> throwChatError $ CECommandError "not supported" _ -> throwChatError $ CECommandError "not supported" checkChatStopped :: CM ChatResponse -> CM ChatResponse @@ -2256,10 +2446,10 @@ processChatCommand' vr = \case checkStoreNotChanged :: CM ChatResponse -> CM ChatResponse checkStoreNotChanged = ifM (asks chatStoreChanged >>= readTVarIO) (throwChatError CEChatStoreChanged) withUserName :: UserName -> (UserId -> ChatCommand) -> CM ChatResponse - withUserName uName cmd = withStore (`getUserIdByName` uName) >>= processChatCommand . cmd + withUserName uName cmd = withFastStore (`getUserIdByName` uName) >>= processChatCommand . cmd withContactName :: ContactName -> (ContactId -> ChatCommand) -> CM ChatResponse withContactName cName cmd = withUser $ \user -> - withStore (\db -> getContactIdByName db user cName) >>= processChatCommand . cmd + withFastStore (\db -> getContactIdByName db user cName) >>= processChatCommand . cmd withMemberName :: GroupName -> ContactName -> (GroupId -> GroupMemberId -> ChatCommand) -> CM ChatResponse withMemberName gName mName cmd = withUser $ \user -> getGroupAndMemberId user gName mName >>= processChatCommand . uncurry cmd @@ -2269,23 +2459,23 @@ processChatCommand' vr = \case verifyConnectionCode user conn@Connection {connId} (Just code) = do code' <- getConnectionCode $ aConnId conn let verified = sameVerificationCode code code' - when verified . withStore' $ \db -> setConnectionVerified db user connId $ Just code' + when verified . withFastStore' $ \db -> setConnectionVerified db user connId $ Just code' pure $ CRConnectionVerified user verified code' verifyConnectionCode user conn@Connection {connId} _ = do code' <- getConnectionCode $ aConnId conn - withStore' $ \db -> setConnectionVerified db user connId Nothing + withFastStore' $ \db -> setConnectionVerified db user connId Nothing pure $ CRConnectionVerified user False code' getSentChatItemIdByText :: User -> ChatRef -> Text -> CM Int64 getSentChatItemIdByText user@User {userId, localDisplayName} (ChatRef cType cId) msg = case cType of - CTDirect -> withStore $ \db -> getDirectChatItemIdByText db userId cId SMDSnd msg - CTGroup -> withStore $ \db -> getGroupChatItemIdByText db user cId (Just localDisplayName) msg - CTLocal -> withStore $ \db -> getLocalChatItemIdByText db user cId SMDSnd msg + CTDirect -> withFastStore $ \db -> getDirectChatItemIdByText db userId cId SMDSnd msg + CTGroup -> withFastStore $ \db -> getGroupChatItemIdByText db user cId (Just localDisplayName) msg + CTLocal -> withFastStore $ \db -> getLocalChatItemIdByText db user cId SMDSnd msg _ -> throwChatError $ CECommandError "not supported" getChatItemIdByText :: User -> ChatRef -> Text -> CM Int64 getChatItemIdByText user (ChatRef cType cId) msg = case cType of - CTDirect -> withStore $ \db -> getDirectChatItemIdByText' db user cId msg - CTGroup -> withStore $ \db -> getGroupChatItemIdByText' db user cId msg - CTLocal -> withStore $ \db -> getLocalChatItemIdByText' db user cId msg + CTDirect -> withFastStore $ \db -> getDirectChatItemIdByText' db user cId msg + CTGroup -> withFastStore $ \db -> getGroupChatItemIdByText' db user cId msg + CTLocal -> withFastStore $ \db -> getLocalChatItemIdByText' db user cId msg _ -> throwChatError $ CECommandError "not supported" connectViaContact :: User -> IncognitoEnabled -> ConnectionRequestUri 'CMContact -> CM ChatResponse connectViaContact user@User {userId} incognito cReq@(CRContactUri ConnReqUriData {crClientData}) = withInvitationLock "connectViaContact" (strEncode cReq) $ do @@ -2294,7 +2484,7 @@ processChatCommand' vr = \case case groupLinkId of -- contact address Nothing -> - withStore' (\db -> getConnReqContactXContactId db vr user cReqHash) >>= \case + withFastStore' (\db -> getConnReqContactXContactId db vr user cReqHash) >>= \case (Just contact, _) -> pure $ CRContactAlreadyExists user contact (_, xContactId_) -> procCmd $ do let randomXContactId = XContactId <$> drgRandomBytes 16 @@ -2302,7 +2492,7 @@ processChatCommand' vr = \case connect' Nothing cReqHash xContactId False -- group link Just gLinkId -> - withStore' (\db -> getConnReqContactXContactId db vr user cReqHash) >>= \case + withFastStore' (\db -> getConnReqContactXContactId db vr user cReqHash) >>= \case (Just _contact, _) -> procCmd $ do -- allow repeat contact request newXContactId <- XContactId <$> drgRandomBytes 16 @@ -2318,8 +2508,8 @@ processChatCommand' vr = \case -- [incognito] generate profile to send incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing subMode <- chatReadVar subscriptionMode - conn <- withStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId subMode chatV pqSup - joinContact user connId cReq incognitoProfile xContactId inGroup pqSup chatV + conn@PendingContactConnection {pccConnId} <- withFastStore' $ \db -> createConnReqConnection db userId connId cReqHash xContactId incognitoProfile groupLinkId subMode chatV pqSup + joinContact user pccConnId connId cReq incognitoProfile xContactId inGroup pqSup chatV pure $ CRSentInvitation user conn incognitoProfile connectContactViaAddress :: User -> IncognitoEnabled -> Contact -> ConnectionRequestUri 'CMContact -> CM ChatResponse connectContactViaAddress user incognito ct cReq = @@ -2331,8 +2521,8 @@ processChatCommand' vr = \case -- [incognito] generate profile to send incognitoProfile <- if incognito then Just <$> liftIO generateRandomProfile else pure Nothing subMode <- chatReadVar subscriptionMode - ct' <- withStore $ \db -> createAddressContactConnection db vr user ct connId cReqHash newXContactId incognitoProfile subMode chatV pqSup - joinContact user connId cReq incognitoProfile newXContactId False pqSup chatV + (pccConnId, ct') <- withFastStore $ \db -> createAddressContactConnection db vr user ct connId cReqHash newXContactId incognitoProfile subMode chatV pqSup + joinContact user pccConnId connId cReq incognitoProfile newXContactId False pqSup chatV pure $ CRSentInvitationToContact user ct' incognitoProfile prepareContact :: User -> ConnectionRequestUri 'CMContact -> PQSupport -> CM (ConnId, VersionChat) prepareContact user cReq pqSup = do @@ -2345,12 +2535,19 @@ processChatCommand' vr = \case let chatV = agentToChatVersion agentV connId <- withAgent $ \a -> prepareConnectionToJoin a (aUserId user) True cReq pqSup pure (connId, chatV) - joinContact :: User -> ConnId -> ConnectionRequestUri 'CMContact -> Maybe Profile -> XContactId -> Bool -> PQSupport -> VersionChat -> CM () - joinContact user connId cReq incognitoProfile xContactId inGroup pqSup chatV = do + joinContact :: User -> Int64 -> ConnId -> ConnectionRequestUri 'CMContact -> Maybe Profile -> XContactId -> Bool -> PQSupport -> VersionChat -> CM () + joinContact user pccConnId connId cReq incognitoProfile xContactId inGroup pqSup chatV = do let profileToSend = userProfileToSend user incognitoProfile Nothing inGroup dm <- encodeConnInfoPQ pqSup chatV (XContact profileToSend $ Just xContactId) subMode <- chatReadVar subscriptionMode - void . withAgent $ \a -> joinConnection a (aUserId user) (Just connId) True cReq dm pqSup subMode + joinPreparedAgentConnection user pccConnId connId cReq dm pqSup subMode + joinPreparedAgentConnection :: User -> Int64 -> ConnId -> ConnectionRequestUri m -> ByteString -> PQSupport -> SubscriptionMode -> CM () + joinPreparedAgentConnection user pccConnId connId cReq connInfo pqSup subMode = do + void (withAgent $ \a -> joinConnection a (aUserId user) (Just connId) True cReq connInfo pqSup subMode) + `catchChatError` \e -> do + withFastStore' $ \db -> deleteConnectionRecord db user pccConnId + withAgent $ \a -> deleteConnectionAsync a False connId + throwError e contactMember :: Contact -> [GroupMember] -> Maybe GroupMember contactMember Contact {contactId} = find $ \GroupMember {memberContactId = cId, memberStatus = s} -> @@ -2363,14 +2560,14 @@ processChatCommand' vr = \case when (fromInteger fileSize > maxFileSize) $ throwChatError $ CEFileSize f pure fileSize updateProfile :: User -> Profile -> CM ChatResponse - updateProfile user p' = updateProfile_ user p' $ withStore $ \db -> updateUserProfile db user p' + updateProfile user p' = updateProfile_ user p' $ withFastStore $ \db -> updateUserProfile db user p' updateProfile_ :: User -> Profile -> CM User -> CM ChatResponse updateProfile_ user@User {profile = p@LocalProfile {displayName = n}} p'@Profile {displayName = n'} updateUser | p' == fromLocalProfile p = pure $ CRUserProfileNoChange user | otherwise = do when (n /= n') $ checkValidName n' -- read contacts before user update to correctly merge preferences - contacts <- withStore' $ \db -> getUserContacts db vr user + contacts <- withFastStore' $ \db -> getUserContacts db vr user user' <- updateUser asks currentUser >>= atomically . (`writeTVar` Just user') withChatLock "updateProfile" . procCmd $ do @@ -2405,10 +2602,10 @@ processChatCommand' vr = \case mergedProfile' = userProfileToSend user' Nothing (Just ct') False ctSndEvent :: ChangedProfileContact -> (ConnOrGroupId, ChatMsgEvent 'Json) ctSndEvent ChangedProfileContact {mergedProfile', conn = Connection {connId}} = (ConnectionId connId, XInfo mergedProfile') - ctMsgReq :: ChangedProfileContact -> Either ChatError SndMessage -> Either ChatError MsgReq + ctMsgReq :: ChangedProfileContact -> Either ChatError SndMessage -> Either ChatError ChatMsgReq ctMsgReq ChangedProfileContact {conn} = fmap $ \SndMessage {msgId, msgBody} -> - (conn, MsgFlags {notification = hasNotification XInfo_}, msgBody, msgId) + (conn, MsgFlags {notification = hasNotification XInfo_}, msgBody, [msgId]) updateContactPrefs :: User -> Contact -> Preferences -> CM ChatResponse updateContactPrefs _ ct@Contact {activeConn = Nothing} _ = throwChatError $ CEContactNotActive ct updateContactPrefs user@User {userId} ct@Contact {activeConn = Just Connection {customUserProfileId}, userPreferences = contactUserPrefs} contactUserPrefs' @@ -2448,12 +2645,12 @@ processChatCommand' vr = \case when (memberStatus membership == GSMemInvited) $ throwChatError (CEGroupNotJoined g) when (memberRemoved membership) $ throwChatError CEGroupMemberUserRemoved unless (memberActive membership) $ throwChatError CEGroupMemberNotActive - delGroupChatItem :: MsgDirectionI d => User -> GroupInfo -> ChatItem 'CTGroup d -> MessageId -> Maybe GroupMember -> CM ChatResponse - delGroupChatItem user gInfo ci msgId byGroupMember = do + delGroupChatItems :: User -> GroupInfo -> [CChatItem 'CTGroup] -> Maybe GroupMember -> CM ChatResponse + delGroupChatItems user gInfo items byGroupMember = do deletedTs <- liftIO getCurrentTime if groupFeatureAllowed SGFFullDelete gInfo - then deleteGroupCI user gInfo ci True False byGroupMember deletedTs - else markGroupCIDeleted user gInfo ci msgId True byGroupMember deletedTs + then deleteGroupCIs user gInfo items True False byGroupMember deletedTs + else markGroupCIsDeleted user gInfo items True byGroupMember deletedTs updateGroupProfileByName :: GroupName -> (GroupProfile -> GroupProfile) -> CM ChatResponse updateGroupProfileByName gName update = withUser $ \user -> do g@(Group GroupInfo {groupProfile = p} _) <- withStore $ \db -> @@ -2548,34 +2745,34 @@ processChatCommand' vr = \case setUserPrivacy user@User {userId} user'@User {userId = userId'} | userId == userId' = do asks currentUser >>= atomically . (`writeTVar` Just user') - withStore' (`updateUserPrivacy` user') + withFastStore' (`updateUserPrivacy` user') pure $ CRUserPrivacy {user = user', updatedUser = user'} | otherwise = do - withStore' (`updateUserPrivacy` user') + withFastStore' (`updateUserPrivacy` user') pure $ CRUserPrivacy {user, updatedUser = user'} checkDeleteChatUser :: User -> CM () checkDeleteChatUser user@User {userId} = do - users <- withStore' getUsers + users <- withFastStore' getUsers let otherVisible = filter (\User {userId = userId', viewPwdHash} -> userId /= userId' && isNothing viewPwdHash) users when (activeUser user && length otherVisible > 0) $ throwChatError (CECantDeleteActiveUser userId) deleteChatUser :: User -> Bool -> CM ChatResponse deleteChatUser user delSMPQueues = do - filesInfo <- withStore' (`getUserFileInfo` user) + filesInfo <- withFastStore' (`getUserFileInfo` user) cancelFilesInProgress user filesInfo deleteFilesLocally filesInfo withAgent $ \a -> deleteUser a (aUserId user) delSMPQueues - withStore' (`deleteUserRecord` user) + withFastStore' (`deleteUserRecord` user) when (activeUser user) $ chatWriteVar currentUser Nothing ok_ updateChatSettings :: ChatName -> (ChatSettings -> ChatSettings) -> CM ChatResponse updateChatSettings (ChatName cType name) updateSettings = withUser $ \user -> do (chatId, chatSettings) <- case cType of - CTDirect -> withStore $ \db -> do + CTDirect -> withFastStore $ \db -> do ctId <- getContactIdByName db user name Contact {chatSettings} <- getContact db vr user ctId pure (ctId, chatSettings) CTGroup -> - withStore $ \db -> do + withFastStore $ \db -> do gId <- getGroupIdByName db user name GroupInfo {chatSettings} <- getGroupInfo db vr user gId pure (gId, chatSettings) @@ -2583,7 +2780,7 @@ processChatCommand' vr = \case processChatCommand $ APISetChatSettings (ChatRef cType chatId) $ updateSettings chatSettings connectPlan :: User -> AConnectionRequestUri -> CM ConnectionPlan connectPlan user (ACR SCMInvitation (CRInvitationUri crData e2e)) = do - withStore' (\db -> getConnectionEntityByConnReq db vr user cReqSchemas) >>= \case + withFastStore' (\db -> getConnectionEntityByConnReq db vr user cReqSchemas) >>= \case Nothing -> pure $ CPInvitationLink ILPOk Just (RcvDirectMsgConnection conn ct_) -> do let Connection {connStatus, contactConnInitiated} = conn @@ -2608,12 +2805,12 @@ processChatCommand' vr = \case case groupLinkId of -- contact address Nothing -> - withStore' (\db -> getUserContactLinkByConnReq db user cReqSchemas) >>= \case + withFastStore' (\db -> getUserContactLinkByConnReq db user cReqSchemas) >>= \case Just _ -> pure $ CPContactAddress CAPOwnLink Nothing -> - withStore' (\db -> getContactConnEntityByConnReqHash db vr user cReqHashes) >>= \case + withFastStore' (\db -> getContactConnEntityByConnReqHash db vr user cReqHashes) >>= \case Nothing -> - withStore' (\db -> getContactWithoutConnViaAddress db vr user cReqSchemas) >>= \case + withFastStore' (\db -> getContactWithoutConnViaAddress db vr user cReqSchemas) >>= \case Nothing -> pure $ CPContactAddress CAPOk Just ct -> pure $ CPContactAddress (CAPContactViaAddress ct) Just (RcvDirectMsgConnection _conn Nothing) -> pure $ CPContactAddress CAPConnectingConfirmReconnect @@ -2624,11 +2821,11 @@ processChatCommand' vr = \case Just _ -> throwChatError $ CECommandError "found connection entity is not RcvDirectMsgConnection" -- group link Just _ -> - withStore' (\db -> getGroupInfoByUserContactLinkConnReq db vr user cReqSchemas) >>= \case + withFastStore' (\db -> getGroupInfoByUserContactLinkConnReq db vr user cReqSchemas) >>= \case Just g -> pure $ CPGroupLink (GLPOwnLink g) Nothing -> do - connEnt_ <- withStore' $ \db -> getContactConnEntityByConnReqHash db vr user cReqHashes - gInfo_ <- withStore' $ \db -> getGroupInfoByGroupLinkHash db vr user cReqHashes + connEnt_ <- withFastStore' $ \db -> getContactConnEntityByConnReqHash db vr user cReqHashes + gInfo_ <- withFastStore' $ \db -> getGroupInfoByGroupLinkHash db vr user cReqHashes case (gInfo_, connEnt_) of (Nothing, Nothing) -> pure $ CPGroupLink GLPOk (Nothing, Just (RcvDirectMsgConnection _conn Nothing)) -> pure $ CPGroupLink GLPConnectingConfirmReconnect @@ -2651,7 +2848,7 @@ processChatCommand' vr = \case cReqHashes = bimap hash hash cReqSchemas hash = ConnReqUriHash . C.sha256Hash . strEncode updateCIGroupInvitationStatus user GroupInfo {groupId} newStatus = do - AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withStore $ \db -> getChatItemByGroupId db vr user groupId + AChatItem _ _ cInfo ChatItem {content, meta = CIMeta {itemId}} <- withFastStore $ \db -> getChatItemByGroupId db vr user groupId case (cInfo, content) of (DirectChat ct@Contact {contactId}, CIRcvGroupInvitation ciGroupInv@CIGroupInvitation {status} memRole) | status == CIGISPending -> do @@ -2663,9 +2860,9 @@ processChatCommand' vr = \case _ -> pure () -- prohibited sendContactContentMessage :: User -> ContactId -> Bool -> Maybe Int -> ComposedMessage -> Maybe CIForwardedFrom -> CM ChatResponse sendContactContentMessage user contactId live itemTTL (ComposedMessage file_ quotedItemId_ mc) itemForwarded = do - ct@Contact {contactUsed} <- withStore $ \db -> getContact db vr user contactId + ct@Contact {contactUsed} <- withFastStore $ \db -> getContact db vr user contactId assertDirectAllowed user MDSnd ct XMsgNew_ - unless contactUsed $ withStore' $ \db -> updateContactUsed db user ct + unless contactUsed $ withFastStore' $ \db -> updateContactUsed db user ct if isVoice mc && not (featureAllowed SCFVoice forUser ct) then pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (chatFeatureNameText CFVoice)) else do @@ -2688,7 +2885,7 @@ processChatCommand' vr = \case (Nothing, Just _) -> pure (MCForward (ExtMsgContent mc fInv_ (ttl' <$> timed_) (justTrue live)), Nothing) (Just quotedItemId, Nothing) -> do CChatItem _ qci@ChatItem {meta = CIMeta {itemTs, itemSharedMsgId}, formattedText, file} <- - withStore $ \db -> getDirectChatItem db user contactId quotedItemId + withFastStore $ \db -> getDirectChatItem db user contactId quotedItemId (origQmc, qd, sent) <- quoteData qci let msgRef = MsgRef {msgId = itemSharedMsgId, sentAt = itemTs, sent, memberId = Nothing} qmc = quoteContent mc origQmc file @@ -2703,7 +2900,7 @@ processChatCommand' vr = \case quoteData _ = throwChatError CEInvalidQuote sendGroupContentMessage :: User -> GroupId -> Bool -> Maybe Int -> ComposedMessage -> Maybe CIForwardedFrom -> CM ChatResponse sendGroupContentMessage user groupId live itemTTL (ComposedMessage file_ quotedItemId_ mc) itemForwarded = do - g@(Group gInfo _) <- withStore $ \db -> getGroup db vr user groupId + g@(Group gInfo _) <- withFastStore $ \db -> getGroup db vr user groupId assertUserGroupRole gInfo GRAuthor send g where @@ -2714,14 +2911,19 @@ processChatCommand' vr = \case (fInv_, ciFile_) <- L.unzip <$> setupSndFileTransfer g (length $ filter memberCurrent ms) timed_ <- sndGroupCITimed live gInfo itemTTL (msgContainer, quotedItem_) <- prepareGroupMsg user gInfo mc quotedItemId_ itemForwarded fInv_ timed_ live - (msg, sentToMembers) <- sendGroupMessage user gInfo ms (XMsgNew msgContainer) + (msg, r) <- sendGroupMessage user gInfo ms (XMsgNew msgContainer) ci <- saveSndChatItem' user (CDGroupSnd gInfo) msg (CISndMsgContent mc) ciFile_ quotedItem_ itemForwarded timed_ live - withStore' $ \db -> - forM_ sentToMembers $ \GroupMember {groupMemberId} -> - createGroupSndStatus db (chatItemId' ci) groupMemberId CISSndNew + withFastStore' $ \db -> do + let GroupSndResult {sentTo, pending, forwarded} = mkGroupSndResult r + createMemberSndStatuses db ci sentTo GSSNew + createMemberSndStatuses db ci forwarded GSSForwarded + createMemberSndStatuses db ci pending GSSInactive forM_ (timed_ >>= timedDeleteAt') $ startProximateTimedItemThread user (ChatRef CTGroup groupId, chatItemId' ci) pure $ CRNewChatItem user (AChatItem SCTGroup SMDSnd (GroupChat gInfo) ci) + where + createMemberSndStatuses db ci ms' gss = + forM_ ms' $ \GroupMember {groupMemberId} -> createGroupSndStatus db (chatItemId' ci) groupMemberId gss notAllowedError f = pure $ chatCmdError (Just user) ("feature not allowed " <> T.unpack (groupFeatureNameText f)) setupSndFileTransfer :: Group -> Int -> CM (Maybe (FileInvitation, CIFile 'MDSnd)) setupSndFileTransfer g n = forM file_ $ \file -> do @@ -2732,20 +2934,20 @@ processChatCommand' vr = \case (fInv, ciFile, ft) <- xftpSndFileTransfer_ user file fileSize n $ Just contactOrGroup case contactOrGroup of CGContact Contact {activeConn} -> forM_ activeConn $ \conn -> - withStore' $ \db -> createSndFTDescrXFTP db user Nothing conn ft dummyFileDescr + withFastStore' $ \db -> createSndFTDescrXFTP db user Nothing conn ft dummyFileDescr CGGroup (Group _ ms) -> forM_ ms $ \m -> saveMemberFD m `catchChatError` (toView . CRChatError (Just user)) where -- we are not sending files to pending members, same as with inline files saveMemberFD m@GroupMember {activeConn = Just conn@Connection {connStatus}} = when ((connStatus == ConnReady || connStatus == ConnSndReady) && not (connDisabled conn)) $ - withStore' $ + withFastStore' $ \db -> createSndFTDescrXFTP db user (Just m) conn ft dummyFileDescr saveMemberFD _ = pure () pure (fInv, ciFile) createNoteFolderContentItem :: User -> NoteFolderId -> ComposedMessage -> Maybe CIForwardedFrom -> CM ChatResponse createNoteFolderContentItem user folderId (ComposedMessage file_ quotedItemId_ mc) itemForwarded = do forM_ quotedItemId_ $ \_ -> throwError $ ChatError $ CECommandError "not supported" - nf <- withStore $ \db -> getNoteFolder db user folderId + nf <- withFastStore $ \db -> getNoteFolder db user folderId createdAt <- liftIO getCurrentTime let content = CISndMsgContent mc let cd = CDLocalSnd nf @@ -2754,11 +2956,14 @@ processChatCommand' vr = \case fsFilePath <- lift $ toFSFilePath filePath fileSize <- liftIO $ CF.getFileContentsSize $ CryptoFile fsFilePath cryptoArgs chunkSize <- asks $ fileChunkSize . config - withStore' $ \db -> do + withFastStore' $ \db -> do fileId <- createLocalFile CIFSSndStored db user nf ciId createdAt cf fileSize chunkSize pure CIFile {fileId, fileName = takeFileName filePath, fileSize, fileSource = Just cf, fileStatus = CIFSSndStored, fileProtocol = FPLocal} let ci = mkChatItem cd ciId content ciFile_ Nothing Nothing itemForwarded Nothing False createdAt Nothing createdAt pure . CRNewChatItem user $ AChatItem SCTLocal SMDSnd (LocalChat nf) ci + getConnQueueInfo user Connection {connId, agentConnId = AgentConnId acId} = do + msgInfo <- withFastStore' (`getLastRcvMsgInfo` connId) + CRQueueInfo user msgInfo <$> withAgent (`getConnectionQueueInfo` acId) contactCITimed :: Contact -> CM (Maybe CITimed) contactCITimed ct = sndContactCITimed False ct Nothing @@ -2855,9 +3060,17 @@ prohibitedGroupContent :: GroupInfo -> GroupMember -> MsgContent -> Maybe f -> M prohibitedGroupContent gInfo m mc file_ | isVoice mc && not (groupFeatureMemberAllowed SGFVoice m gInfo) = Just GFVoice | not (isVoice mc) && isJust file_ && not (groupFeatureMemberAllowed SGFFiles m gInfo) = Just GFFiles - | not (groupFeatureMemberAllowed SGFSimplexLinks m gInfo) && containsFormat isSimplexLink (parseMarkdown $ msgContentText mc) = Just GFSimplexLinks + | prohibitedSimplexLinks gInfo m mc = Just GFSimplexLinks | otherwise = Nothing +prohibitedSimplexLinks :: GroupInfo -> GroupMember -> MsgContent -> Bool +prohibitedSimplexLinks gInfo m mc = + not (groupFeatureMemberAllowed SGFSimplexLinks m gInfo) + && maybe False (any ftIsSimplexLink) (parseMaybeMarkdownList $ msgContentText mc) + where + ftIsSimplexLink :: FormattedText -> Bool + ftIsSimplexLink FormattedText {format} = maybe False isSimplexLink format + roundedFDCount :: Int -> Int roundedFDCount n | n <= 0 = 4 @@ -3032,18 +3245,18 @@ setFileToEncrypt ft@RcvFileTransfer {fileId} = do withStore' $ \db -> setFileCryptoArgs db fileId cfArgs pure (ft :: RcvFileTransfer) {cryptoArgs = Just cfArgs} -receiveFile' :: User -> RcvFileTransfer -> Maybe Bool -> Maybe FilePath -> CM ChatResponse -receiveFile' user ft rcvInline_ filePath_ = do - (CRRcvFileAccepted user <$> acceptFileReceive user ft rcvInline_ filePath_) `catchChatError` processError +receiveFile' :: User -> RcvFileTransfer -> Bool -> Maybe Bool -> Maybe FilePath -> CM ChatResponse +receiveFile' user ft userApprovedRelays rcvInline_ filePath_ = do + (CRRcvFileAccepted user <$> acceptFileReceive user ft userApprovedRelays rcvInline_ filePath_) `catchChatError` processError where processError = \case -- TODO AChatItem in Cancelled events - ChatErrorAgent (SMP SMP.AUTH) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft + ChatErrorAgent (SMP _ SMP.AUTH) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft ChatErrorAgent (CONN DUPLICATE) _ -> pure $ CRRcvFileAcceptedSndCancelled user ft e -> throwError e -acceptFileReceive :: User -> RcvFileTransfer -> Maybe Bool -> Maybe FilePath -> CM AChatItem -acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = FileInvitation {fileName = fName, fileConnReq, fileInline, fileSize}, fileStatus, grpMemberId, cryptoArgs} rcvInline_ filePath_ = do +acceptFileReceive :: User -> RcvFileTransfer -> Bool -> Maybe Bool -> Maybe FilePath -> CM AChatItem +acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileInvitation = FileInvitation {fileName = fName, fileConnReq, fileInline, fileSize}, fileStatus, grpMemberId, cryptoArgs} userApprovedRelays rcvInline_ filePath_ = do unless (fileStatus == RFSNew) $ case fileStatus of RFSCancelled _ -> throwChatError $ CEFileCancelled fName _ -> throwChatError $ CEFileAlreadyReceiving fName @@ -3057,15 +3270,16 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI filePath <- getRcvFilePath fileId filePath_ fName True withStore $ \db -> acceptRcvFileTransfer db vr user fileId connIds ConnJoined filePath subMode -- XFTP - (Just XFTPRcvFile {}, _) -> do + (Just XFTPRcvFile {userApprovedRelays = approvedBeforeReady}, _) -> do + let userApproved = approvedBeforeReady || userApprovedRelays filePath <- getRcvFilePath fileId filePath_ fName False (ci, rfd) <- withStore $ \db -> do -- marking file as accepted and reading description in the same transaction -- to prevent race condition with appending description - ci <- xftpAcceptRcvFT db vr user fileId filePath + ci <- xftpAcceptRcvFT db vr user fileId filePath userApproved rfd <- getRcvFileDescrByRcvFileId db fileId pure (ci, rfd) - receiveViaCompleteFD user fileId rfd cryptoArgs + receiveViaCompleteFD user fileId rfd userApproved cryptoArgs pure ci -- group & direct file protocol _ -> do @@ -3110,18 +3324,62 @@ acceptFileReceive user@User {userId} RcvFileTransfer {fileId, xftpRcvFile, fileI || (rcvInline_ == Just True && fileSize <= fileChunkSize * offerChunks) ) -receiveViaCompleteFD :: User -> FileTransferId -> RcvFileDescr -> Maybe CryptoFileArgs -> CM () -receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} cfArgs = +receiveViaCompleteFD :: User -> FileTransferId -> RcvFileDescr -> Bool -> Maybe CryptoFileArgs -> CM () +receiveViaCompleteFD user fileId RcvFileDescr {fileDescrText, fileDescrComplete} userApprovedRelays cfArgs = when fileDescrComplete $ do rd <- parseFileDescription fileDescrText - aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd cfArgs - startReceivingFile user fileId - withStore' $ \db -> updateRcvFileAgentId db fileId (Just $ AgentRcvFileId aFileId) + if userApprovedRelays + then receive' rd True + else do + let srvs = fileServers rd + unknownSrvs <- getUnknownSrvs srvs + let approved = null unknownSrvs + ifM + ((approved ||) <$> ipProtectedForSrvs srvs) + (receive' rd approved) + (relaysNotApproved unknownSrvs) + where + receive' :: ValidFileDescription 'FRecipient -> Bool -> CM () + receive' rd approved = do + aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) rd cfArgs approved + startReceivingFile user fileId + withStore' $ \db -> updateRcvFileAgentId db fileId (Just $ AgentRcvFileId aFileId) + fileServers :: ValidFileDescription 'FRecipient -> [XFTPServer] + fileServers (FD.ValidFileDescription FD.FileDescription {chunks}) = + S.toList $ S.fromList $ concatMap (\FD.FileChunk {replicas} -> map (\FD.FileChunkReplica {server} -> server) replicas) chunks + getUnknownSrvs :: [XFTPServer] -> CM [XFTPServer] + getUnknownSrvs srvs = do + cfg <- asks config + knownSrvs <- L.map (\ServerCfg {server} -> protoServer server) . useServers cfg SPXFTP <$> withStore' (`getProtocolServers` user) + pure $ filter (`notElem` knownSrvs) srvs + ipProtectedForSrvs :: [XFTPServer] -> CM Bool + ipProtectedForSrvs srvs = do + netCfg <- lift getNetworkConfig + pure $ all (ipAddressProtected netCfg) srvs + relaysNotApproved :: [XFTPServer] -> CM () + relaysNotApproved unknownSrvs = do + aci_ <- resetRcvCIFileStatus user fileId CIFSRcvInvitation + forM_ aci_ $ \aci -> toView $ CRChatItemUpdated user aci + throwChatError $ CEFileNotApproved fileId unknownSrvs + +getNetworkConfig :: CM' NetworkConfig +getNetworkConfig = withAgent' $ liftIO . getFastNetworkConfig + +resetRcvCIFileStatus :: User -> FileTransferId -> CIFileStatus 'MDRcv -> CM (Maybe AChatItem) +resetRcvCIFileStatus user fileId ciFileStatus = do + vr <- chatVersionRange + withStore $ \db -> do + liftIO $ do + updateCIFileStatus db user fileId ciFileStatus + updateRcvFileStatus db fileId FSNew + updateRcvFileAgentId db fileId Nothing + lookupChatItemByFileId db vr user fileId receiveViaURI :: User -> FileDescriptionURI -> CryptoFile -> CM RcvFileTransfer receiveViaURI user@User {userId} FileDescriptionURI {description} cf@CryptoFile {cryptoArgs} = do fileId <- withStore $ \db -> createRcvStandaloneFileTransfer db userId cf fileSize chunkSize - aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) description cryptoArgs + -- currently the only use case is user migrating via their configured servers, so we pass approvedRelays = True + aFileId <- withAgent $ \a -> xftpReceiveFile a (aUserId user) description cryptoArgs True withStore $ \db -> do liftIO $ do updateRcvFileStatus db fileId FSConnected @@ -3184,8 +3442,9 @@ acceptContactRequest user UserContactRequest {agentInvitationId = AgentInvId inv chatV = vr `peerConnChatVersion` cReqChatVRange pqSup' = pqSup `CR.pqSupportAnd` pqSupport dm <- encodeConnInfoPQ pqSup' chatV $ XInfo profileToSend - acId <- withAgent $ \a -> acceptContact a True invId dm pqSup' subMode - withStore' $ \db -> createAcceptedContact db user acId chatV cReqChatVRange cName profileId cp userContactLinkId xContactId incognitoProfile subMode pqSup' contactUsed + (acId, sqSecured) <- withAgent $ \a -> acceptContact a True invId dm pqSup' subMode + let connStatus = if sqSecured then ConnSndReady else ConnNew + withStore' $ \db -> createAcceptedContact db user acId connStatus chatV cReqChatVRange cName profileId cp userContactLinkId xContactId incognitoProfile subMode pqSup' contactUsed acceptContactRequestAsync :: User -> UserContactRequest -> Maybe IncognitoProfile -> Bool -> PQSupport -> CM Contact acceptContactRequestAsync user UserContactRequest {agentInvitationId = AgentInvId invId, cReqChatVRange, localDisplayName = cName, profileId, profile = p, userContactLinkId, xContactId} incognitoProfile contactUsed pqSup = do @@ -3195,7 +3454,7 @@ acceptContactRequestAsync user UserContactRequest {agentInvitationId = AgentInvI let chatV = vr `peerConnChatVersion` cReqChatVRange (cmdId, acId) <- agentAcceptContactAsync user True invId (XInfo profileToSend) subMode pqSup chatV withStore' $ \db -> do - ct@Contact {activeConn} <- createAcceptedContact db user acId chatV cReqChatVRange cName profileId p userContactLinkId xContactId incognitoProfile subMode pqSup contactUsed + ct@Contact {activeConn} <- createAcceptedContact db user acId ConnNew chatV cReqChatVRange cName profileId p userContactLinkId xContactId incognitoProfile subMode pqSup contactUsed forM_ activeConn $ \Connection {connId} -> setCommandConnId db user cmdId connId pure ct @@ -3255,10 +3514,13 @@ deleteGroupLink_ user gInfo conn = do agentSubscriber :: CM' () agentSubscriber = do q <- asks $ subQ . smpAgent - forever $ atomically (readTBQueue q) >>= process + forever (atomically (readTBQueue q) >>= process) + `E.catchAny` \e -> do + toView' $ CRChatError Nothing $ ChatErrorAgent (CRITICAL True $ "Message reception stopped: " <> show e) Nothing + E.throwIO e where - process :: (ACorrId, EntityId, APartyCmd 'Agent) -> CM' () - process (corrId, entId, APC e msg) = run $ case e of + process :: (ACorrId, EntityId, AEvt) -> CM' () + process (corrId, entId, AEvt e msg) = run $ case e of SAENone -> processAgentMessageNoConn msg SAEConn -> processAgentMessage corrId entId msg SAERcvFile -> processAgentMsgRcvFile corrId entId msg @@ -3375,7 +3637,7 @@ subscribeUserConnections vr onlyNeeded agentBatchSubscribe user = do errorNetworkStatus :: ChatError -> String errorNetworkStatus = \case ChatErrorAgent (BROKER _ NETWORK) _ -> "network" - ChatErrorAgent (SMP SMP.AUTH) _ -> "contact deleted" + ChatErrorAgent (SMP _ SMP.AUTH) _ -> "contact deleted" e -> show e -- TODO possibly below could be replaced with less noisy events for API contactLinkSubsToView :: Map ConnId (Either AgentErrorType ()) -> Map ConnId UserContact -> CM () @@ -3513,12 +3775,12 @@ deleteTimedItem user (ChatRef cType chatId, itemId) deleteAt = do vr <- chatVersionRange case cType of CTDirect -> do - (ct, CChatItem _ ci) <- withStore $ \db -> (,) <$> getContact db vr user chatId <*> getDirectChatItem db user chatId itemId - deleteDirectCI user ct ci True True >>= toView + (ct, ci) <- withStore $ \db -> (,) <$> getContact db vr user chatId <*> getDirectChatItem db user chatId itemId + deleteDirectCIs user ct [ci] True True >>= toView CTGroup -> do - (gInfo, CChatItem _ ci) <- withStore $ \db -> (,) <$> getGroupInfo db vr user chatId <*> getGroupChatItem db user chatId itemId + (gInfo, ci) <- withStore $ \db -> (,) <$> getGroupInfo db vr user chatId <*> getGroupChatItem db user chatId itemId deletedTs <- liftIO getCurrentTime - deleteGroupCI user gInfo ci True True Nothing deletedTs >>= toView + deleteGroupCIs user gInfo [ci] True True Nothing deletedTs >>= toView _ -> toView . CRChatError (Just user) . ChatError $ CEInternalError "bad deleteTimedItem cType" startUpdatedTimedItemThread :: User -> ChatRef -> ChatItem c d -> ChatItem c d -> CM () @@ -3572,7 +3834,7 @@ expireChatItems user@User {userId} ttl sync = do membersToDelete <- withStore' $ \db -> getGroupMembersForExpiration db vr user gInfo forM_ membersToDelete $ \m -> withStore' $ \db -> deleteGroupMember db user m -processAgentMessage :: ACorrId -> ConnId -> ACommand 'Agent 'AEConn -> CM () +processAgentMessage :: ACorrId -> ConnId -> AEvent 'AEConn -> CM () processAgentMessage _ connId (DEL_RCVQ srv qId err_) = toView $ CRAgentRcvQueueDeleted (AgentConnId connId) srv (AgentQueueId qId) err_ processAgentMessage _ connId DEL_CONN = @@ -3601,7 +3863,7 @@ critical a = ChatErrorStore SEDBBusyError {message} -> throwError $ ChatErrorAgent (CRITICAL True message) Nothing e -> throwError e -processAgentMessageNoConn :: ACommand 'Agent 'AENone -> CM () +processAgentMessageNoConn :: AEvent 'AENone -> CM () processAgentMessageNoConn = \case CONNECT p h -> hostEvent $ CRHostConnected p h DISCONNECT p h -> hostEvent $ CRHostDisconnected p h @@ -3622,7 +3884,7 @@ processAgentMessageNoConn = \case cs <- withStore' (`getConnectionsContacts` conns) toView $ event srv cs -processAgentMsgSndFile :: ACorrId -> SndFileId -> ACommand 'Agent 'AESndFile -> CM () +processAgentMsgSndFile :: ACorrId -> SndFileId -> AEvent 'AESndFile -> CM () processAgentMsgSndFile _corrId aFileId msg = do (cRef_, fileId) <- withStore (`getXFTPSndFileDBIds` AgentSndFileId aFileId) withEntityLock_ cRef_ . withFileLock "processAgentMsgSndFile" fileId $ @@ -3656,11 +3918,11 @@ processAgentMsgSndFile _corrId aFileId msg = do lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) withStore' $ \db -> createExtraSndFTDescrs db user fileId (map fileDescrText rfds) case rfds of - [] -> sendFileError "no receiver descriptions" vr ft + [] -> sendFileError (FileErrOther "no receiver descriptions") "no receiver descriptions" vr ft rfd : _ -> case [fd | fd@(FD.ValidFileDescription FD.FileDescription {chunks = [_]}) <- rfds] of [] -> case xftpRedirectFor of Nothing -> xftpSndFileRedirect user fileId rfd >>= toView . CRSndFileRedirectStartXFTP user ft - Just _ -> sendFileError "Prohibit chaining redirects" vr ft + Just _ -> sendFileError (FileErrOther "chaining redirects") "Prohibit chaining redirects" vr ft rfds' -> do -- we have 1 chunk - use it as URI whether it is redirect or not ft' <- maybe (pure ft) (\fId -> withStore $ \db -> getFileTransferMeta db user fId) xftpRedirectFor @@ -3674,15 +3936,22 @@ processAgentMsgSndFile _corrId aFileId msg = do case (rfds, sfts, d, cInfo) of (rfd : extraRFDs, sft : _, SMDSnd, DirectChat ct) -> do withStore' $ \db -> createExtraSndFTDescrs db user fileId (map fileDescrText extraRFDs) - msgDeliveryId <- sendFileDescription sft rfd sharedMsgId $ sendDirectContactMessage user ct - withStore' $ \db -> updateSndFTDeliveryXFTP db sft msgDeliveryId + conn@Connection {connId} <- liftEither $ contactSendConn_ ct + sendFileDescriptions (ConnectionId connId) ((conn, sft, fileDescrText rfd) :| []) sharedMsgId >>= \case + Just rs -> case L.last rs of + Right ([msgDeliveryId], _) -> + withStore' $ \db -> updateSndFTDeliveryXFTP db sft msgDeliveryId + Right (deliveryIds, _) -> toView $ CRChatError (Just user) $ ChatError $ CEInternalError $ "SFDONE, sendFileDescriptions: expected 1 delivery id, got " <> show (length deliveryIds) + Left e -> toView $ CRChatError (Just user) e + Nothing -> toView $ CRChatError (Just user) $ ChatError $ CEInternalError "SFDONE, sendFileDescriptions: expected at least 1 result" lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) (_, _, SMDSnd, GroupChat g@GroupInfo {groupId}) -> do ms <- withStore' $ \db -> getGroupMembers db vr user g - let rfdsMemberFTs = zip rfds $ memberFTs ms + let rfdsMemberFTs = zipWith (\rfd (conn, sft) -> (conn, sft, fileDescrText rfd)) rfds (memberFTs ms) extraRFDs = drop (length rfdsMemberFTs) rfds withStore' $ \db -> createExtraSndFTDescrs db user fileId (map fileDescrText extraRFDs) - forM_ rfdsMemberFTs $ \mt -> sendToMember mt `catchChatError` (toView . CRChatError (Just user)) + forM_ (L.nonEmpty rfdsMemberFTs) $ \rfdsMemberFTs' -> + sendFileDescriptions (GroupId groupId) rfdsMemberFTs' sharedMsgId ci' <- withStore $ \db -> do liftIO $ updateCIFileStatus db user fileId CIFSSndComplete getChatItemByFileId db vr user fileId @@ -3694,62 +3963,80 @@ processAgentMsgSndFile _corrId aFileId msg = do where mConns' = mapMaybe useMember ms sfts' = mapMaybe (\sft@SndFileTransfer {groupMemberId} -> (,sft) <$> groupMemberId) sfts + -- Should match memberSendAction logic useMember GroupMember {groupMemberId, activeConn = Just conn@Connection {connStatus}} - | (connStatus == ConnReady || connStatus == ConnSndReady) && not (connDisabled conn) = Just (groupMemberId, conn) + | (connStatus == ConnReady || connStatus == ConnSndReady) && not (connDisabled conn) && not (connInactive conn) = + Just (groupMemberId, conn) | otherwise = Nothing useMember _ = Nothing - sendToMember :: (ValidFileDescription 'FRecipient, (Connection, SndFileTransfer)) -> CM () - sendToMember (rfd, (conn, sft)) = - void $ sendFileDescription sft rfd sharedMsgId $ \msg' -> do - (sndMsg, msgDeliveryId, _) <- sendDirectMemberMessage conn msg' groupId - pure (sndMsg, msgDeliveryId) _ -> pure () _ -> pure () -- TODO error? - SFERR e - | temporaryAgentError e -> - throwChatError $ CEXFTPSndFile fileId (AgentSndFileId aFileId) e - | otherwise -> - sendFileError (tshow e) vr ft + SFWARN e -> do + let err = tshow e + logWarn $ "Sent file warning: " <> err + ci <- withStore $ \db -> do + liftIO $ updateCIFileStatus db user fileId (CIFSSndWarning $ agentFileError e) + lookupChatItemByFileId db vr user fileId + toView $ CRSndFileWarning user ci ft err + SFERR e -> + sendFileError (agentFileError e) (tshow e) vr ft where fileDescrText :: FilePartyI p => ValidFileDescription p -> T.Text fileDescrText = safeDecodeUtf8 . strEncode - sendFileDescription :: SndFileTransfer -> ValidFileDescription 'FRecipient -> SharedMsgId -> (ChatMsgEvent 'Json -> CM (SndMessage, Int64)) -> CM Int64 - sendFileDescription sft rfd msgId sendMsg = do - let rfdText = fileDescrText rfd - withStore' $ \db -> updateSndFTDescrXFTP db user sft rfdText - parts <- splitFileDescr rfdText - loopSend parts + sendFileDescriptions :: ConnOrGroupId -> NonEmpty (Connection, SndFileTransfer, RcvFileDescrText) -> SharedMsgId -> CM (Maybe (NonEmpty (Either ChatError ([Int64], PQEncryption)))) + sendFileDescriptions connOrGroupId connsTransfersDescrs sharedMsgId = do + lift . void . withStoreBatch' $ \db -> L.map (\(_, sft, rfdText) -> updateSndFTDescrXFTP db user sft rfdText) connsTransfersDescrs + partSize <- asks $ xftpDescrPartSize . config + let connsIdsEvts = connDescrEvents partSize + sndMsgs_ <- lift $ createSndMessages $ L.map snd connsIdsEvts + let (errs, msgReqs) = partitionEithers . L.toList $ L.zipWith (fmap . toMsgReq) connsIdsEvts sndMsgs_ + delivered <- mapM deliverMessages (L.nonEmpty msgReqs) + let errs' = errs <> maybe [] (lefts . L.toList) delivered + unless (null errs') $ toView $ CRChatErrors (Just user) errs' + pure delivered where - -- returns msgDeliveryId of the last file description message - loopSend :: NonEmpty FileDescr -> CM Int64 - loopSend (fileDescr :| fds) = do - (_, msgDeliveryId) <- sendMsg $ XMsgFileDescr {msgId, fileDescr} - case L.nonEmpty fds of - Just fds' -> loopSend fds' - Nothing -> pure msgDeliveryId - sendFileError :: Text -> VersionRangeChat -> FileTransferMeta -> CM () - sendFileError err vr ft = do + connDescrEvents :: Int -> NonEmpty (Connection, (ConnOrGroupId, ChatMsgEvent 'Json)) + connDescrEvents partSize = L.fromList $ concatMap splitText (L.toList connsTransfersDescrs) + where + splitText :: (Connection, SndFileTransfer, RcvFileDescrText) -> [(Connection, (ConnOrGroupId, ChatMsgEvent 'Json))] + splitText (conn, _, rfdText) = + map (\fileDescr -> (conn, (connOrGroupId, XMsgFileDescr {msgId = sharedMsgId, fileDescr}))) (L.toList $ splitFileDescr partSize rfdText) + toMsgReq :: (Connection, (ConnOrGroupId, ChatMsgEvent 'Json)) -> SndMessage -> ChatMsgReq + toMsgReq (conn, _) SndMessage {msgId, msgBody} = + (conn, MsgFlags {notification = hasNotification XMsgFileDescr_}, msgBody, [msgId]) + sendFileError :: FileError -> Text -> VersionRangeChat -> FileTransferMeta -> CM () + sendFileError ferr err vr ft = do logError $ "Sent file error: " <> err ci <- withStore $ \db -> do - liftIO $ updateFileCancelled db user fileId CIFSSndError + liftIO $ updateFileCancelled db user fileId (CIFSSndError ferr) lookupChatItemByFileId db vr user fileId lift $ withAgent' (`xftpDeleteSndFileInternal` aFileId) toView $ CRSndFileError user ci ft err -splitFileDescr :: RcvFileDescrText -> CM (NonEmpty FileDescr) -splitFileDescr rfdText = do - partSize <- asks $ xftpDescrPartSize . config - pure $ splitParts 1 partSize rfdText +agentFileError :: AgentErrorType -> FileError +agentFileError = \case + XFTP _ XFTP.AUTH -> FileErrAuth + FILE NO_FILE -> FileErrNoFile + BROKER _ e -> brokerError FileErrRelay e + e -> FileErrOther $ tshow e where - splitParts partNo partSize remText = + brokerError srvErr = \case + HOST -> srvErr SrvErrHost + SMP.TRANSPORT TEVersion -> srvErr SrvErrVersion + e -> srvErr . SrvErrOther $ tshow e + +splitFileDescr :: Int -> RcvFileDescrText -> NonEmpty FileDescr +splitFileDescr partSize rfdText = splitParts 1 rfdText + where + splitParts partNo remText = let (part, rest) = T.splitAt partSize remText complete = T.null rest fileDescr = FileDescr {fileDescrText = part, fileDescrPartNo = partNo, fileDescrComplete = complete} in if complete then fileDescr :| [] - else fileDescr <| splitParts (partNo + 1) partSize rest + else fileDescr <| splitParts (partNo + 1) rest -processAgentMsgRcvFile :: ACorrId -> RcvFileId -> ACommand 'Agent 'AERcvFile -> CM () +processAgentMsgRcvFile :: ACorrId -> RcvFileId -> AEvent 'AERcvFile -> CM () processAgentMsgRcvFile _corrId aFileId msg = do (cRef_, fileId) <- withStore (`getXFTPRcvFileDBIds` AgentRcvFileId aFileId) withEntityLock_ cRef_ . withFileLock "processAgentMsgRcvFile" fileId $ @@ -3788,17 +4075,24 @@ processAgentMsgRcvFile _corrId aFileId msg = do lookupChatItemByFileId db vr user fileId agentXFTPDeleteRcvFile aFileId fileId toView $ maybe (CRRcvStandaloneFileComplete user fsTargetPath ft) (CRRcvFileComplete user) ci_ + RFWARN e -> do + ci <- withStore $ \db -> do + liftIO $ updateCIFileStatus db user fileId (CIFSRcvWarning $ agentFileError e) + lookupChatItemByFileId db vr user fileId + toView $ CRRcvFileWarning user ci e ft RFERR e - | temporaryAgentError e -> - throwChatError $ CEXFTPRcvFile fileId (AgentRcvFileId aFileId) e + | e == FILE NOT_APPROVED -> do + aci_ <- resetRcvCIFileStatus user fileId CIFSRcvAborted + agentXFTPDeleteRcvFile aFileId fileId + forM_ aci_ $ \aci -> toView $ CRChatItemUpdated user aci | otherwise -> do ci <- withStore $ \db -> do - liftIO $ updateFileCancelled db user fileId CIFSRcvError + liftIO $ updateFileCancelled db user fileId (CIFSRcvError $ agentFileError e) lookupChatItemByFileId db vr user fileId agentXFTPDeleteRcvFile aFileId fileId toView $ CRRcvFileError user ci e ft -processAgentMessageConn :: VersionRangeChat -> User -> ACorrId -> ConnId -> ACommand 'Agent 'AEConn -> CM () +processAgentMessageConn :: VersionRangeChat -> User -> ACorrId -> ConnId -> AEvent 'AEConn -> CM () processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = do -- Missing connection/entity errors here will be sent to the view but not shown as CRITICAL alert, -- as in this case no need to ACK message - we can't process messages for this connection anyway. @@ -3830,8 +4124,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = pure $ updateEntityConnStatus acEntity connStatus Nothing -> pure acEntity - agentMsgConnStatus :: ACommand 'Agent e -> Maybe ConnStatus + agentMsgConnStatus :: AEvent e -> Maybe ConnStatus agentMsgConnStatus = \case + JOINED True -> Just ConnSndReady CONF {} -> Just ConnRequested INFO {} -> Just ConnSndReady CON _ -> Just ConnReady @@ -3852,7 +4147,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = processINFOpqSupport Connection {pqSupport = pq} pq' = when (pq /= pq') $ messageWarning "processINFOpqSupport: unexpected pqSupport change" - processDirectMessage :: ACommand 'Agent e -> ConnectionEntity -> Connection -> Maybe Contact -> CM () + processDirectMessage :: AEvent e -> ConnectionEntity -> Connection -> Maybe Contact -> CM () processDirectMessage agentMsg connEntity conn@Connection {connId, connChatVersion, peerChatVRange, viaUserContactLink, customUserProfileId, connectionCode} = \case Nothing -> case agentMsg of CONF confId pqSupport _ connInfo -> do @@ -3867,21 +4162,26 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = processINFOpqSupport conn pqSupport _conn' <- saveConnInfo conn connInfo pure () - MSG meta _msgFlags msgBody -> - -- TODO only acknowledge without saving message? - -- probably this branch is never executed, so there should be no reason - -- to save message if contact hasn't been created yet - chat item isn't created anyway - withAckMessage' agentConnId meta $ - void $ - saveDirectRcvMSG conn meta msgBody - SENT msgId -> + MSG meta _msgFlags _msgBody -> + -- We are not saving message (saveDirectRcvMSG) as contact hasn't been created yet, + -- chat item is also not created here + withAckMessage' "new contact msg" agentConnId meta $ pure () + SENT msgId _proxy -> do + void $ continueSending connEntity conn sentMsgDeliveryEvent conn msgId OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + JOINED _ -> + -- [async agent commands] continuation on receiving JOINED + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + QCONT -> + void $ continueSending connEntity conn + MWARN _ err -> + processConnMWARN connEntity conn err MERR _ err -> do toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) - incAuthErrCounter connEntity conn err + processConnMERR connEntity conn err MERRS _ err -> do -- error cannot be AUTH error here toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) @@ -3902,41 +4202,61 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = forM_ contData $ \(hostConnId, xGrpMemIntroCont) -> sendXGrpMemInv hostConnId (Just directConnReq) xGrpMemIntroCont CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type" - MSG msgMeta _msgFlags msgBody -> - withAckMessage agentConnId msgMeta True $ do + MSG msgMeta _msgFlags msgBody -> do + tags <- newTVarIO [] + withAckMessage "contact msg" agentConnId msgMeta True (Just tags) $ \eInfo -> do let MsgMeta {pqEncryption} = msgMeta (ct', conn') <- updateContactPQRcv user ct conn pqEncryption checkIntegrityCreateItem (CDDirectRcv ct') msgMeta `catchChatError` \_ -> pure () - (conn'', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveDirectRcvMSG conn' msgMeta msgBody - let ct'' = ct' {activeConn = Just conn''} :: Contact - assertDirectAllowed user MDRcv ct'' $ toCMEventTag event - case event of - XMsgNew mc -> newContentMessage ct'' mc msg msgMeta - XMsgFileDescr sharedMsgId fileDescr -> messageFileDescription ct'' sharedMsgId fileDescr - XMsgUpdate sharedMsgId mContent ttl live -> messageUpdate ct'' sharedMsgId mContent msg msgMeta ttl live - XMsgDel sharedMsgId _ -> messageDelete ct'' sharedMsgId msg msgMeta - XMsgReact sharedMsgId _ reaction add -> directMsgReaction ct'' sharedMsgId reaction add msg msgMeta - -- TODO discontinue XFile - XFile fInv -> processFileInvitation' ct'' fInv msg msgMeta - XFileCancel sharedMsgId -> xFileCancel ct'' sharedMsgId - XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct'' sharedMsgId fileConnReq_ fName - XInfo p -> xInfo ct'' p - XDirectDel -> xDirectDel ct'' msg msgMeta - XGrpInv gInv -> processGroupInvitation ct'' gInv msg msgMeta - XInfoProbe probe -> xInfoProbe (COMContact ct'') probe - XInfoProbeCheck probeHash -> xInfoProbeCheck (COMContact ct'') probeHash - XInfoProbeOk probe -> xInfoProbeOk (COMContact ct'') probe - XCallInv callId invitation -> xCallInv ct'' callId invitation msg msgMeta - XCallOffer callId offer -> xCallOffer ct'' callId offer msg - XCallAnswer callId answer -> xCallAnswer ct'' callId answer msg - XCallExtra callId extraInfo -> xCallExtra ct'' callId extraInfo msg - XCallEnd callId -> xCallEnd ct'' callId msg - BFileChunk sharedMsgId chunk -> bFileChunk ct'' sharedMsgId chunk msgMeta - _ -> messageError $ "unsupported message: " <> T.pack (show event) - let Contact {chatSettings = ChatSettings {sendRcpts}} = ct'' - pure $ fromMaybe (sendRcptsContacts user) sendRcpts && hasDeliveryReceipt (toCMEventTag event) + forM_ aChatMsgs $ \case + Right (ACMsg _ chatMsg) -> + processEvent ct' conn' tags eInfo chatMsg `catchChatError` \e -> toView $ CRChatError (Just user) e + Left e -> do + atomically $ modifyTVar' tags ("error" :) + logInfo $ "contact msg=error " <> eInfo <> " " <> tshow e + toView $ CRChatError (Just user) (ChatError . CEException $ "error parsing chat message: " <> e) + checkSendRcpt ct' $ rights aChatMsgs -- not crucial to use ct'' from processEvent + where + aChatMsgs = parseChatMessages msgBody + processEvent :: Contact -> Connection -> TVar [Text] -> Text -> MsgEncodingI e => ChatMessage e -> CM () + processEvent ct' conn' tags eInfo chatMsg@ChatMessage {chatMsgEvent} = do + let tag = toCMEventTag chatMsgEvent + atomically $ modifyTVar' tags (tshow tag :) + logInfo $ "contact msg=" <> tshow tag <> " " <> eInfo + (conn'', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveDirectRcvMSG conn' msgMeta msgBody chatMsg + let ct'' = ct' {activeConn = Just conn''} :: Contact + case event of + XMsgNew mc -> newContentMessage ct'' mc msg msgMeta + XMsgFileDescr sharedMsgId fileDescr -> messageFileDescription ct'' sharedMsgId fileDescr + XMsgUpdate sharedMsgId mContent ttl live -> messageUpdate ct'' sharedMsgId mContent msg msgMeta ttl live + XMsgDel sharedMsgId _ -> messageDelete ct'' sharedMsgId msg msgMeta + XMsgReact sharedMsgId _ reaction add -> directMsgReaction ct'' sharedMsgId reaction add msg msgMeta + -- TODO discontinue XFile + XFile fInv -> processFileInvitation' ct'' fInv msg msgMeta + XFileCancel sharedMsgId -> xFileCancel ct'' sharedMsgId + XFileAcptInv sharedMsgId fileConnReq_ fName -> xFileAcptInv ct'' sharedMsgId fileConnReq_ fName + XInfo p -> xInfo ct'' p + XDirectDel -> xDirectDel ct'' msg msgMeta + XGrpInv gInv -> processGroupInvitation ct'' gInv msg msgMeta + XInfoProbe probe -> xInfoProbe (COMContact ct'') probe + XInfoProbeCheck probeHash -> xInfoProbeCheck (COMContact ct'') probeHash + XInfoProbeOk probe -> xInfoProbeOk (COMContact ct'') probe + XCallInv callId invitation -> xCallInv ct'' callId invitation msg msgMeta + XCallOffer callId offer -> xCallOffer ct'' callId offer msg + XCallAnswer callId answer -> xCallAnswer ct'' callId answer msg + XCallExtra callId extraInfo -> xCallExtra ct'' callId extraInfo msg + XCallEnd callId -> xCallEnd ct'' callId msg + BFileChunk sharedMsgId chunk -> bFileChunk ct'' sharedMsgId chunk msgMeta + _ -> messageError $ "unsupported message: " <> T.pack (show event) + checkSendRcpt :: Contact -> [AChatMessage] -> CM Bool + checkSendRcpt ct' aMsgs = do + let Contact {chatSettings = ChatSettings {sendRcpts}} = ct' + pure $ fromMaybe (sendRcptsContacts user) sendRcpts && any aChatMsgHasReceipt aMsgs + where + aChatMsgHasReceipt (ACMsg _ ChatMessage {chatMsgEvent}) = + hasDeliveryReceipt (toCMEventTag chatMsgEvent) RCVD msgMeta msgRcpt -> - withAckMessage' agentConnId msgMeta $ + withAckMessage' "contact rcvd" agentConnId msgMeta $ directMsgReceived ct conn msgMeta msgRcpt CONF confId pqSupport _ connInfo -> do conn' <- processCONFpqSupport conn pqSupport @@ -3991,11 +4311,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = forM_ viaUserContactLink $ \userContactLinkId -> do ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId let (UserContactLink {autoAccept}, groupId_, gLinkMemRole) = ucl - forM_ autoAccept $ \(AutoAccept {autoReply = mc_}) -> - forM_ mc_ $ \mc -> do - (msg, _) <- sendDirectContactMessage user ct' (XMsgNew $ MCSimple (extMsgContent mc Nothing)) - ci <- saveSndChatItem user (CDDirectSnd ct') msg (CISndMsgContent mc) - toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct') ci) + when (connChatVersion < batchSend2Version) $ sendAutoReply ct' autoAccept forM_ groupId_ $ \groupId -> do groupInfo <- withStore $ \db -> getGroupInfo db vr user groupId subMode <- chatReadVar subscriptionMode @@ -4007,10 +4323,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = notifyMemberConnected gInfo m $ Just ct let connectedIncognito = contactConnIncognito ct || incognitoMembership gInfo when (memberCategory m == GCPreMember) $ probeMatchingContactsAndMembers ct connectedIncognito True - SENT msgId -> do + SENT msgId proxy -> do + void $ continueSending connEntity conn sentMsgDeliveryEvent conn msgId checkSndInlineFTComplete conn msgId - updateDirectItemStatus ct conn msgId $ CISSndSent SSPComplete + ci_ <- withStore $ \db -> do + ci_ <- updateDirectItemStatus' db ct conn msgId (CISSndSent SSPComplete) + forM ci_ $ \ci -> liftIO $ setDirectSndChatItemViaProxy db user ct ci (isJust proxy) + forM_ ci_ $ \ci -> toView $ CRChatItemStatusUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) SWITCH qd phase cStats -> do toView $ CRContactSwitch user ct (SwitchProgress qd phase cStats) when (phase `elem` [SPStarted, SPCompleted]) $ case qd of @@ -4045,21 +4365,43 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + JOINED sqSecured -> + -- [async agent commands] continuation on receiving JOINED + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> + when (directOrUsed ct && sqSecured) $ do + lift $ setContactNetworkStatus ct NSConnected + toView $ CRContactSndReady user ct + forM_ viaUserContactLink $ \userContactLinkId -> do + ucl <- withStore $ \db -> getUserContactLinkById db userId userContactLinkId + let (UserContactLink {autoAccept}, _, _) = ucl + when (connChatVersion >= batchSend2Version) $ sendAutoReply ct autoAccept + QCONT -> + void $ continueSending connEntity conn + MWARN msgId err -> do + updateDirectItemStatus ct conn msgId (CISSndWarning $ agentSndError err) + processConnMWARN connEntity conn err MERR msgId err -> do - updateDirectItemStatus ct conn msgId $ agentErrToItemStatus err + updateDirectItemStatus ct conn msgId (CISSndError $ agentSndError err) toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) - incAuthErrCounter connEntity conn err + processConnMERR connEntity conn err MERRS msgIds err -> do -- error cannot be AUTH error here - updateDirectItemsStatus ct conn (L.toList msgIds) $ agentErrToItemStatus err + updateDirectItemsStatus ct conn (L.toList msgIds) (CISSndError $ agentSndError err) toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) ERR err -> do toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () -- TODO add debugging output _ -> pure () + where + sendAutoReply ct = \case + Just AutoAccept {autoReply = Just mc} -> do + (msg, _) <- sendDirectContactMessage user ct (XMsgNew $ MCSimple (extMsgContent mc Nothing)) + ci <- saveSndChatItem user (CDDirectSnd ct) msg (CISndMsgContent mc) + toView $ CRNewChatItem user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) + _ -> pure () - processGroupMessage :: ACommand 'Agent e -> ConnectionEntity -> Connection -> GroupInfo -> GroupMember -> CM () + processGroupMessage :: AEvent e -> ConnectionEntity -> Connection -> GroupInfo -> GroupMember -> CM () processGroupMessage agentMsg connEntity conn@Connection {connId, connectionCode} gInfo@GroupInfo {groupId, groupProfile, membership, chatSettings} m = case agentMsg of INV (ACR _ cReq) -> withCompletedCommand conn agentMsg $ \CommandData {cmdFunction} -> @@ -4274,7 +4616,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = xMsgNewChatMsg = ChatMessage {chatVRange = senderVRange, msgId = itemSharedMsgId, chatMsgEvent = XMsgNew msgContainer} fileDescrEvents <- case (snd <$> fInvDescr_, itemSharedMsgId) of (Just fileDescrText, Just msgId) -> do - parts <- splitFileDescr fileDescrText + partSize <- asks $ xftpDescrPartSize . config + let parts = splitFileDescr partSize fileDescrText pure . toList $ L.map (XMsgFileDescr msgId) parts _ -> pure [] let fileDescrChatMsgs = map (ChatMessage senderVRange Nothing) fileDescrEvents @@ -4310,19 +4653,26 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = void $ sendDirectMemberMessage imConn (XGrpMemCon memberId) groupId _ -> messageWarning "sendXGrpMemCon: member category GCPreMember or GCPostMember is expected" MSG msgMeta _msgFlags msgBody -> do - withAckMessage agentConnId msgMeta True $ do + tags <- newTVarIO [] + withAckMessage "group msg" agentConnId msgMeta True (Just tags) $ \eInfo -> do checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta `catchChatError` \_ -> pure () forM_ aChatMsgs $ \case Right (ACMsg _ chatMsg) -> - processEvent chatMsg `catchChatError` \e -> toView $ CRChatError (Just user) e - Left e -> toView $ CRChatError (Just user) (ChatError . CEException $ "error parsing chat message: " <> e) - forwardMsg_ `catchChatError` \_ -> pure () + processEvent tags eInfo chatMsg `catchChatError` \e -> toView $ CRChatError (Just user) e + Left e -> do + atomically $ modifyTVar' tags ("error" :) + logInfo $ "group msg=error " <> eInfo <> " " <> tshow e + toView $ CRChatError (Just user) (ChatError . CEException $ "error parsing chat message: " <> e) + forwardMsgs (rights aChatMsgs) `catchChatError` (toView . CRChatError (Just user)) checkSendRcpt $ rights aChatMsgs where aChatMsgs = parseChatMessages msgBody brokerTs = metaBrokerTs msgMeta - processEvent :: MsgEncodingI e => ChatMessage e -> CM () - processEvent chatMsg = do + processEvent :: TVar [Text] -> Text -> MsgEncodingI e => ChatMessage e -> CM () + processEvent tags eInfo chatMsg@ChatMessage {chatMsgEvent} = do + let tag = toCMEventTag chatMsgEvent + atomically $ modifyTVar' tags (tshow tag :) + logInfo $ "group msg=" <> tshow tag <> " " <> eInfo (m', conn', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveGroupRcvMsg user groupId m conn msgMeta msgBody chatMsg case event of XMsgNew mc -> memberCanSend m' $ newGroupContentMessage gInfo m' mc msg brokerTs False @@ -4353,7 +4703,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = XInfoProbeCheck probeHash -> xInfoProbeCheck (COMGroupMember m') probeHash XInfoProbeOk probe -> xInfoProbeOk (COMGroupMember m') probe BFileChunk sharedMsgId chunk -> bFileChunkGroup gInfo sharedMsgId chunk msgMeta - _ -> messageError $ "unsupported message: " <> T.pack (show event) + _ -> messageError $ "unsupported message: " <> tshow event checkSendRcpt :: [AChatMessage] -> CM Bool checkSendRcpt aMsgs = do currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo @@ -4365,34 +4715,33 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = where aChatMsgHasReceipt (ACMsg _ ChatMessage {chatMsgEvent}) = hasDeliveryReceipt (toCMEventTag chatMsgEvent) - forwardMsg_ :: CM () - forwardMsg_ = do + forwardMsgs :: [AChatMessage] -> CM () + forwardMsgs aMsgs = do let GroupMember {memberRole = membershipMemRole} = membership - when (membershipMemRole >= GRAdmin && not (blockedByAdmin m)) $ case aChatMsgs of - -- currently only a single message is forwarded - [Right (ACMsg _ chatMsg)] -> - forM_ (forwardedGroupMsg chatMsg) $ \chatMsg' -> do - ChatConfig {highlyAvailable} <- asks config - -- members introduced to this invited member - introducedMembers <- - if memberCategory m == GCInviteeMember - then withStore' $ \db -> getForwardIntroducedMembers db vr user m highlyAvailable - else pure [] - -- invited members to which this member was introduced - invitedMembers <- withStore' $ \db -> getForwardInvitedMembers db vr user m highlyAvailable - let GroupMember {memberId} = m - ms = forwardedToGroupMembers (introducedMembers <> invitedMembers) chatMsg' - msg = XGrpMsgForward memberId chatMsg' brokerTs - unless (null ms) . void $ - sendGroupMessage' user gInfo ms msg - _ -> pure () + when (membershipMemRole >= GRAdmin && not (blockedByAdmin m)) $ do + let forwardedMsgs = mapMaybe (\(ACMsg _ chatMsg) -> forwardedGroupMsg chatMsg) aMsgs + forM_ (L.nonEmpty forwardedMsgs) $ \forwardedMsgs' -> do + ChatConfig {highlyAvailable} <- asks config + -- members introduced to this invited member + introducedMembers <- + if memberCategory m == GCInviteeMember + then withStore' $ \db -> getForwardIntroducedMembers db vr user m highlyAvailable + else pure [] + -- invited members to which this member was introduced + invitedMembers <- withStore' $ \db -> getForwardInvitedMembers db vr user m highlyAvailable + let GroupMember {memberId} = m + ms = forwardedToGroupMembers (introducedMembers <> invitedMembers) forwardedMsgs' + events = L.map (\cm -> XGrpMsgForward memberId cm brokerTs) forwardedMsgs' + unless (null ms) $ sendGroupMessages user gInfo ms events RCVD msgMeta msgRcpt -> - withAckMessage' agentConnId msgMeta $ + withAckMessage' "group rcvd" agentConnId msgMeta $ groupMsgReceived gInfo m conn msgMeta msgRcpt - SENT msgId -> do + SENT msgId proxy -> do + continued <- continueSending connEntity conn sentMsgDeliveryEvent conn msgId checkSndInlineFTComplete conn msgId - updateGroupItemStatus gInfo m conn msgId $ CISSndSent SSPComplete + updateGroupItemStatus gInfo m conn msgId GSSSent (Just $ isJust proxy) + when continued $ sendPendingGroupMessages user m conn SWITCH qd phase cStats -> do toView $ CRGroupMemberSwitch user gInfo m (SwitchProgress qd phase cStats) when (phase `elem` [SPStarted, SPCompleted]) $ case qd of @@ -4428,13 +4777,22 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + JOINED _ -> + -- [async agent commands] continuation on receiving JOINED + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + QCONT -> do + continued <- continueSending connEntity conn + when continued $ sendPendingGroupMessages user m conn + MWARN msgId err -> do + withStore' $ \db -> updateGroupItemErrorStatus db msgId (groupMemberId' m) (GSSWarning $ agentSndError err) + processConnMWARN connEntity conn err MERR msgId err -> do - withStore' $ \db -> updateGroupItemErrorStatus db msgId (groupMemberId' m) $ agentErrToItemStatus err + withStore' $ \db -> updateGroupItemErrorStatus db msgId (groupMemberId' m) (GSSError $ agentSndError err) -- group errors are silenced to reduce load on UI event log -- toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) - incAuthErrCounter connEntity conn err + processConnMERR connEntity conn err MERRS msgIds err -> do - let newStatus = agentErrToItemStatus err + let newStatus = GSSError $ agentSndError err -- error cannot be AUTH error here withStore' $ \db -> forM_ msgIds $ \msgId -> updateGroupItemErrorStatus db msgId (groupMemberId' m) newStatus `catchAll_` pure () @@ -4445,7 +4803,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = -- TODO add debugging output _ -> pure () where - updateGroupItemErrorStatus :: DB.Connection -> AgentMsgId -> GroupMemberId -> CIStatus 'MDSnd -> IO () + updateGroupItemErrorStatus :: DB.Connection -> AgentMsgId -> GroupMemberId -> GroupSndStatus -> IO () updateGroupItemErrorStatus db msgId groupMemberId newStatus = do chatItemId_ <- getChatItemIdByAgentMsgId db connId msgId forM_ chatItemId_ $ \itemId -> updateGroupMemSndStatus' db itemId groupMemberId newStatus @@ -4472,7 +4830,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = r n'' = Just (ci, CIRcvDecryptionError mde n'') mdeUpdatedCI _ _ = Nothing - processSndFileConn :: ACommand 'Agent e -> ConnectionEntity -> Connection -> SndFileTransfer -> CM () + processSndFileConn :: AEvent e -> ConnectionEntity -> Connection -> SndFileTransfer -> CM () processSndFileConn agentMsg connEntity conn ft@SndFileTransfer {fileId, fileName, fileStatus} = case agentMsg of -- SMP CONF for SndFileConnection happens for direct file protocol @@ -4495,13 +4853,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = updateDirectCIFileStatus db vr user fileId $ CIFSSndTransfer 0 1 toView $ CRSndFileStart user ci ft sendFileChunk user ft - SENT msgId -> do + SENT msgId _proxy -> do withStore' $ \db -> updateSndFileChunkSent db ft msgId unless (fileStatus == FSCancelled) $ sendFileChunk user ft MERR _ err -> do cancelSndFileTransfer user ft True >>= mapM_ (deleteAgentConnectionAsync user) case err of - SMP SMP.AUTH -> unless (fileStatus == FSCancelled) $ do + SMP _ SMP.AUTH -> unless (fileStatus == FSCancelled) $ do ci <- withStore $ \db -> do liftIO (lookupChatRefByFileId db user fileId) >>= \case Just (ChatRef CTDirect _) -> liftIO $ updateFileCancelled db user fileId CIFSSndCancelled @@ -4509,17 +4867,21 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = lookupChatItemByFileId db vr user fileId toView $ CRSndFileRcvCancelled user ci ft _ -> throwChatError $ CEFileSend fileId err - MSG meta _ _ -> withAckMessage' agentConnId meta $ pure () + MSG meta _ _ -> + withAckMessage' "file msg" agentConnId meta $ pure () OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + JOINED _ -> + -- [async agent commands] continuation on receiving JOINED + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () ERR err -> do toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () -- TODO add debugging output _ -> pure () - processRcvFileConn :: ACommand 'Agent e -> ConnectionEntity -> Connection -> RcvFileTransfer -> CM () + processRcvFileConn :: AEvent e -> ConnectionEntity -> Connection -> RcvFileTransfer -> CM () processRcvFileConn agentMsg connEntity conn ft@RcvFileTransfer {fileId, fileInvitation = FileInvitation {fileName}, grpMemberId} = case agentMsg of INV (ACR _ cReq) -> @@ -4559,9 +4921,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = OK -> -- [async agent commands] continuation on receiving OK when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () + JOINED _ -> + -- [async agent commands] continuation on receiving JOINED + when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () MERR _ err -> do toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) - incAuthErrCounter connEntity conn err + processConnMERR connEntity conn err ERR err -> do toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () @@ -4585,7 +4950,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = RcvChunkOk -> if B.length chunk /= fromInteger chunkSize then badRcvFileChunk ft "incorrect chunk size" - else withAckMessage' agentConnId meta $ appendFileChunk ft chunkNo chunk False + else withAckMessage' "file msg" agentConnId meta $ appendFileChunk ft chunkNo chunk False RcvChunkFinal -> if B.length chunk > fromInteger chunkSize then badRcvFileChunk ft "incorrect chunk size" @@ -4599,10 +4964,10 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = getChatItemByFileId db vr user fileId toView $ CRRcvFileComplete user ci forM_ conn_ $ \conn -> deleteAgentConnectionAsync user (aConnId conn) - RcvChunkDuplicate -> withAckMessage' agentConnId meta $ pure () + RcvChunkDuplicate -> withAckMessage' "file msg" agentConnId meta $ pure () RcvChunkError -> badRcvFileChunk ft $ "incorrect chunk number " <> show chunkNo - processUserContactRequest :: ACommand 'Agent e -> ConnectionEntity -> Connection -> UserContact -> CM () + processUserContactRequest :: AEvent e -> ConnectionEntity -> Connection -> UserContact -> CM () processUserContactRequest agentMsg connEntity conn UserContact {userContactLinkId} = case agentMsg of REQ invId pqSupport _ connInfo -> do ChatMessage {chatVRange, chatMsgEvent} <- parseChatMessage conn connInfo @@ -4613,7 +4978,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = _ -> pure () MERR _ err -> do toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) - incAuthErrCounter connEntity conn err + processConnMERR connEntity conn err ERR err -> do toView $ CRChatError (Just user) (ChatErrorAgent err $ Just connEntity) when (corrId /= "") $ withCompletedCommand conn agentMsg $ \_cmdData -> pure () @@ -4653,24 +5018,50 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | memberRole <= GRObserver = messageError "member is not allowed to send messages" | otherwise = a - incAuthErrCounter :: ConnectionEntity -> Connection -> AgentErrorType -> CM () - incAuthErrCounter connEntity conn err = do + processConnMERR :: ConnectionEntity -> Connection -> AgentErrorType -> CM () + processConnMERR connEntity conn err = do case err of - SMP SMP.AUTH -> do - authErrCounter' <- withStore' $ \db -> incConnectionAuthErrCounter db user conn - when (authErrCounter' >= authErrDisableCount) $ do - toView $ CRConnectionDisabled connEntity + SMP _ SMP.AUTH -> do + authErrCounter' <- withStore' $ \db -> incAuthErrCounter db user conn + when (authErrCounter' >= authErrDisableCount) $ case connEntity of + RcvDirectMsgConnection ctConn (Just ct) -> do + toView $ CRContactDisabled user ct {activeConn = Just ctConn {authErrCounter = authErrCounter'}} + _ -> toView $ CRConnectionDisabled connEntity + SMP _ SMP.QUOTA -> + unless (connInactive conn) $ do + withStore' $ \db -> setQuotaErrCounter db user conn quotaErrSetOnMERR + toView $ CRConnectionInactive connEntity True _ -> pure () + processConnMWARN :: ConnectionEntity -> Connection -> AgentErrorType -> CM () + processConnMWARN connEntity conn err = do + case err of + SMP _ SMP.QUOTA -> + unless (connInactive conn) $ do + quotaErrCounter' <- withStore' $ \db -> incQuotaErrCounter db user conn + when (quotaErrCounter' >= quotaErrInactiveCount) $ + toView $ + CRConnectionInactive connEntity True + _ -> pure () + + continueSending :: ConnectionEntity -> Connection -> CM Bool + continueSending connEntity conn = + if connInactive conn + then do + withStore' $ \db -> setQuotaErrCounter db user conn 0 + toView $ CRConnectionInactive connEntity False + pure True + else pure False + -- TODO v5.7 / v6.0 - together with deprecating old group protocol establishing direct connections? -- we could save command records only for agent APIs we process continuations for (INV) - withCompletedCommand :: forall e. AEntityI e => Connection -> ACommand 'Agent e -> (CommandData -> CM ()) -> CM () + withCompletedCommand :: forall e. AEntityI e => Connection -> AEvent e -> (CommandData -> CM ()) -> CM () withCompletedCommand Connection {connId} agentMsg action = do - let agentMsgTag = APCT (sAEntity @e) $ aCommandTag agentMsg + let agentMsgTag = AEvtTag (sAEntity @e) $ aEventTag agentMsg cmdData_ <- withStore' $ \db -> getCommandDataByCorrId db user corrId case cmdData_ of Just cmdData@CommandData {cmdId, cmdConnId = Just cmdConnId', cmdFunction} - | connId == cmdConnId' && (agentMsgTag == commandExpectedResponse cmdFunction || agentMsgTag == APCT SAEConn ERR_) -> do + | connId == cmdConnId' && (agentMsgTag == commandExpectedResponse cmdFunction || agentMsgTag == AEvtTag SAEConn ERR_) -> do withStore' $ \db -> deleteCommand db user cmdId action cmdData | otherwise -> err cmdId $ "not matching connection id or unexpected response, corrId = " <> show corrId @@ -4681,25 +5072,45 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withStore' $ \db -> updateCommandStatus db user cmdId CSError throwChatError . CEAgentCommandError $ msg - withAckMessage' :: ConnId -> MsgMeta -> CM () -> CM () - withAckMessage' cId msgMeta action = do - withAckMessage cId msgMeta False $ action $> False + withAckMessage' :: Text -> ConnId -> MsgMeta -> CM () -> CM () + withAckMessage' label cId msgMeta action = do + withAckMessage label cId msgMeta False Nothing $ \_ -> action $> False - withAckMessage :: ConnId -> MsgMeta -> Bool -> CM Bool -> CM () - withAckMessage cId msgMeta showCritical action = + withAckMessage :: Text -> ConnId -> MsgMeta -> Bool -> Maybe (TVar [Text]) -> (Text -> CM Bool) -> CM () + withAckMessage label cId msgMeta showCritical tags action = do -- [async agent commands] command should be asynchronous -- TODO catching error and sending ACK after an error, particularly if it is a database error, will result in the message not processed (and no notification to the user). -- Possible solutions are: -- 1) retry processing several times -- 2) stabilize database -- 3) show screen of death to the user asking to restart - tryChatError action >>= \case - Right withRcpt -> ackMsg msgMeta $ if withRcpt then Just "" else Nothing + eInfo <- eventInfo + logInfo $ label <> ": " <> eInfo + tryChatError (action eInfo) >>= \case + Right withRcpt -> + withLog (eInfo <> " ok") $ ackMsg msgMeta $ if withRcpt then Just "" else Nothing -- If showCritical is True, then these errors don't result in ACK and show user visible alert -- This prevents losing the message that failed to be processed. Left (ChatErrorStore SEDBBusyError {message}) | showCritical -> throwError $ ChatErrorAgent (CRITICAL True message) Nothing - Left e -> ackMsg msgMeta Nothing >> throwError e + Left e -> do + withLog (eInfo <> " error: " <> tshow e) $ ackMsg msgMeta Nothing + throwError e where + eventInfo = do + v <- asks eventSeq + eId <- atomically $ stateTVar v $ \i -> (i + 1, i + 1) + pure $ "conn_id=" <> tshow cId <> " event_id=" <> tshow eId + withLog eInfo' ack = do + ts <- showTags + logInfo $ T.unwords [label, "ack:", ts, eInfo'] + ack + logInfo $ T.unwords [label, "ack=success:", ts, eInfo'] + showTags = do + ts <- maybe (pure []) readTVarIO tags + pure $ case ts of + [] -> "no_chat_messages" + [t] -> "chat_message=" <> t + _ -> "chat_message_batch=" <> T.intercalate "," (reverse ts) ackMsg :: MsgMeta -> Maybe MsgReceiptInfo -> CM () ackMsg MsgMeta {recipient = (msgId, _)} rcpt = withAgent $ \a -> ackMessageAsync a "" cId msgId rcpt @@ -4707,9 +5118,21 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = sentMsgDeliveryEvent Connection {connId} msgId = withStore' $ \db -> updateSndMsgDeliveryStatus db connId msgId MDSSndSent - agentErrToItemStatus :: AgentErrorType -> CIStatus 'MDSnd - agentErrToItemStatus (SMP AUTH) = CISSndErrorAuth - agentErrToItemStatus err = CISSndError . T.unpack . safeDecodeUtf8 $ strEncode err + agentSndError :: AgentErrorType -> SndError + agentSndError = \case + SMP _ AUTH -> SndErrAuth + SMP _ QUOTA -> SndErrQuota + BROKER _ e -> brokerError SndErrRelay e + SMP proxySrv (SMP.PROXY (SMP.BROKER e)) -> brokerError (SndErrProxy proxySrv) e + AP.PROXY proxySrv _ (ProxyProtocolError (SMP.PROXY (SMP.BROKER e))) -> brokerError (SndErrProxyRelay proxySrv) e + e -> SndErrOther $ tshow e + where + brokerError srvErr = \case + NETWORK -> SndErrExpired + TIMEOUT -> SndErrExpired + HOST -> srvErr SrvErrHost + SMP.TRANSPORT TEVersion -> srvErr SrvErrVersion + e -> srvErr . SrvErrOther $ tshow e badRcvFileChunk :: RcvFileTransfer -> String -> CM () badRcvFileChunk ft err = @@ -4821,8 +5244,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = autoAcceptFile :: Maybe (RcvFileTransfer, CIFile 'MDRcv) -> CM () autoAcceptFile = mapM_ $ \(ft, CIFile {fileSize}) -> do + -- ! autoAcceptFileSize is only used in tests ChatConfig {autoAcceptFileSize = sz} <- asks config - when (sz > fileSize) $ receiveFile' user ft Nothing Nothing >>= toView + when (sz > fileSize) $ receiveFile' user ft False Nothing Nothing >>= toView messageFileDescription :: Contact -> SharedMsgId -> FileDescr -> CM () messageFileDescription ct@Contact {contactId} sharedMsgId fileDescr = do @@ -4848,12 +5272,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = ci <- withStore $ \db -> getAChatItemBySharedMsgId db user cd sharedMsgId toView $ CRRcvFileDescrReady user ci ft' rfd case (fileStatus, xftpRcvFile) of - (RFSAccepted _, Just XFTPRcvFile {}) -> receiveViaCompleteFD user fileId rfd cryptoArgs + (RFSAccepted _, Just XFTPRcvFile {userApprovedRelays}) -> receiveViaCompleteFD user fileId rfd userApprovedRelays cryptoArgs _ -> pure () processFileInvitation :: Maybe FileInvitation -> MsgContent -> (DB.Connection -> FileInvitation -> Maybe InlineFileMode -> Integer -> ExceptT StoreError IO RcvFileTransfer) -> CM (Maybe (RcvFileTransfer, CIFile 'MDRcv)) - processFileInvitation fInv_ mc createRcvFT = forM fInv_ $ \fInv@FileInvitation {fileName, fileSize} -> do + processFileInvitation fInv_ mc createRcvFT = forM fInv_ $ \fInv' -> do ChatConfig {fileChunkSize} <- asks config + let fInv@FileInvitation {fileName, fileSize} = mkValidFileInvitation fInv' inline <- receiveInlineMode fInv (Just mc) fileChunkSize ft@RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFT db fInv inline fileChunkSize let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP @@ -4869,6 +5294,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = fileSource = (`CryptoFile` cryptoArgs) <$> filePath pure (ft', CIFile {fileId, fileName, fileSize, fileSource, fileStatus, fileProtocol}) + mkValidFileInvitation :: FileInvitation -> FileInvitation + mkValidFileInvitation fInv@FileInvitation {fileName} = fInv {fileName = FP.makeValid $ FP.takeFileName fileName} + messageUpdate :: Contact -> SharedMsgId -> MsgContent -> RcvMessage -> MsgMeta -> Maybe Int -> Maybe Bool -> CM () messageUpdate ct@Contact {contactId} sharedMsgId mc msg@RcvMessage {msgId} msgMeta ttl live_ = do updateRcvChatItem `catchCINotFound` \_ -> do @@ -4905,19 +5333,26 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = _ -> messageError "x.msg.update: contact attempted invalid message update" messageDelete :: Contact -> SharedMsgId -> RcvMessage -> MsgMeta -> CM () - messageDelete ct@Contact {contactId} sharedMsgId RcvMessage {msgId} msgMeta = do + messageDelete ct@Contact {contactId} sharedMsgId _rcvMessage msgMeta = do deleteRcvChatItem `catchCINotFound` (toView . CRChatItemDeletedNotFound user ct) where brokerTs = metaBrokerTs msgMeta deleteRcvChatItem = do - CChatItem msgDir ci <- withStore $ \db -> getDirectChatItemBySharedMsgId db user contactId sharedMsgId + cci@(CChatItem msgDir ci) <- withStore $ \db -> getDirectChatItemBySharedMsgId db user contactId sharedMsgId case msgDir of - SMDRcv -> - if featureAllowed SCFFullDelete forContact ct - then deleteDirectCI user ct ci False False >>= toView - else markDirectCIDeleted user ct ci msgId False brokerTs >>= toView + SMDRcv + | rcvItemDeletable ci brokerTs -> + if featureAllowed SCFFullDelete forContact ct + then deleteDirectCIs user ct [cci] False False >>= toView + else markDirectCIsDeleted user ct [cci] False brokerTs >>= toView + | otherwise -> messageError "x.msg.del: contact attempted invalid message delete" SMDSnd -> messageError "x.msg.del: contact attempted invalid message delete" + rcvItemDeletable :: ChatItem c d -> UTCTime -> Bool + rcvItemDeletable ChatItem {meta = CIMeta {itemTs, itemDeleted}} brokerTs = + -- 78 hours margin to account for possible sending delay + diffUTCTime brokerTs itemTs < (78 * 3600) && isNothing itemDeleted + directMsgReaction :: Contact -> SharedMsgId -> MsgReaction -> Bool -> RcvMessage -> MsgMeta -> CM () directMsgReaction ct sharedMsgId reaction add RcvMessage {msgId} MsgMeta {broker = (_, brokerTs)} = do when (featureAllowed SCFReactions forContact ct) $ do @@ -4995,7 +5430,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = ci <- createNonLive file_ ci' <- withStore' $ \db -> markGroupCIBlockedByAdmin db user gInfo ci groupMsgToView gInfo ci' - applyModeration CIModeration {moderatorMember = moderator@GroupMember {memberRole = moderatorRole}, createdByMsgId, moderatedAt} + applyModeration CIModeration {moderatorMember = moderator@GroupMember {memberRole = moderatorRole}, moderatedAt} | moderatorRole < GRAdmin || moderatorRole < memberRole = createContentItem | groupFeatureAllowed SGFFullDelete gInfo = do @@ -5005,7 +5440,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | otherwise = do file_ <- processFileInv ci <- createNonLive file_ - toView =<< markGroupCIDeleted user gInfo ci createdByMsgId False (Just moderator) moderatedAt + toView =<< markGroupCIsDeleted user gInfo [CChatItem SMDRcv ci] False (Just moderator) moderatedAt createNonLive file_ = saveRcvChatItem' user (CDGroupRcv gInfo m) msg sharedMsgId_ brokerTs (CIRcvMsgContent content) (snd <$> file_) timed' False createContentItem = do @@ -5021,18 +5456,21 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = groupMsgToView gInfo ci' {reactions} groupMessageUpdate :: GroupInfo -> GroupMember -> SharedMsgId -> MsgContent -> RcvMessage -> UTCTime -> Maybe Int -> Maybe Bool -> CM () - groupMessageUpdate gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId, memberId} sharedMsgId mc msg@RcvMessage {msgId} brokerTs ttl_ live_ = - updateRcvChatItem `catchCINotFound` \_ -> do - -- This patches initial sharedMsgId into chat item when locally deleted chat item - -- received an update from the sender, so that it can be referenced later (e.g. by broadcast delete). - -- Chat item and update message which created it will have different sharedMsgId in this case... - let timed_ = rcvGroupCITimed gInfo ttl_ - ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg (Just sharedMsgId) brokerTs content Nothing timed_ live - ci' <- withStore' $ \db -> do - createChatItemVersion db (chatItemId' ci) brokerTs mc - ci' <- updateGroupChatItem db user groupId ci content True live Nothing - blockedMember m ci' $ markGroupChatItemBlocked db user gInfo ci' - toView $ CRChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci') + groupMessageUpdate gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId, memberId} sharedMsgId mc msg@RcvMessage {msgId} brokerTs ttl_ live_ + | prohibitedSimplexLinks gInfo m mc = + messageWarning $ "x.msg.update ignored: feature not allowed " <> groupFeatureNameText GFSimplexLinks + | otherwise = do + updateRcvChatItem `catchCINotFound` \_ -> do + -- This patches initial sharedMsgId into chat item when locally deleted chat item + -- received an update from the sender, so that it can be referenced later (e.g. by broadcast delete). + -- Chat item and update message which created it will have different sharedMsgId in this case... + let timed_ = rcvGroupCITimed gInfo ttl_ + ci <- saveRcvChatItem' user (CDGroupRcv gInfo m) msg (Just sharedMsgId) brokerTs content Nothing timed_ live + ci' <- withStore' $ \db -> do + createChatItemVersion db (chatItemId' ci) brokerTs mc + ci' <- updateGroupChatItem db user groupId ci content True live Nothing + blockedMember m ci' $ markGroupChatItemBlocked db user gInfo ci' + toView $ CRChatItemUpdated user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci') where content = CIRcvMsgContent mc live = fromMaybe False live_ @@ -5061,35 +5499,46 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = groupMessageDelete gInfo@GroupInfo {groupId, membership} m@GroupMember {memberId, memberRole = senderRole} sharedMsgId sndMemberId_ RcvMessage {msgId} brokerTs = do let msgMemberId = fromMaybe memberId sndMemberId_ withStore' (\db -> runExceptT $ getGroupMemberCIBySharedMsgId db user groupId msgMemberId sharedMsgId) >>= \case - Right (CChatItem _ ci@ChatItem {chatDir}) -> case chatDir of - CIGroupRcv mem - | sameMemberId memberId mem && msgMemberId == memberId -> delete ci Nothing >>= toView - | otherwise -> deleteMsg mem ci - CIGroupSnd -> deleteMsg membership ci + Right cci@(CChatItem _ ci@ChatItem {chatDir}) -> case chatDir of + CIGroupRcv mem -> case sndMemberId_ of + -- regular deletion + Nothing + | sameMemberId memberId mem && msgMemberId == memberId && rcvItemDeletable ci brokerTs -> + delete cci Nothing >>= toView + | otherwise -> + messageError "x.msg.del: member attempted invalid message delete" + -- moderation (not limited by time) + Just _ + | sameMemberId memberId mem && msgMemberId == memberId -> + delete cci (Just m) >>= toView + | otherwise -> + moderate mem cci + CIGroupSnd -> moderate membership cci Left e | msgMemberId == memberId -> messageError $ "x.msg.del: message not found, " <> tshow e | senderRole < GRAdmin -> messageError $ "x.msg.del: message not found, message of another member with insufficient member permissions, " <> tshow e | otherwise -> withStore' $ \db -> createCIModeration db gInfo m msgMemberId sharedMsgId msgId brokerTs where - deleteMsg :: MsgDirectionI d => GroupMember -> ChatItem 'CTGroup d -> CM () - deleteMsg mem ci = case sndMemberId_ of + moderate :: GroupMember -> CChatItem 'CTGroup -> CM () + moderate mem cci = case sndMemberId_ of Just sndMemberId - | sameMemberId sndMemberId mem -> checkRole mem $ delete ci (Just m) >>= toView + | sameMemberId sndMemberId mem -> checkRole mem $ delete cci (Just m) >>= toView | otherwise -> messageError "x.msg.del: message of another member with incorrect memberId" _ -> messageError "x.msg.del: message of another member without memberId" checkRole GroupMember {memberRole} a | senderRole < GRAdmin || senderRole < memberRole = messageError "x.msg.del: message of another member with insufficient member permissions" | otherwise = a - delete :: MsgDirectionI d => ChatItem 'CTGroup d -> Maybe GroupMember -> CM ChatResponse - delete ci byGroupMember - | groupFeatureAllowed SGFFullDelete gInfo = deleteGroupCI user gInfo ci False False byGroupMember brokerTs - | otherwise = markGroupCIDeleted user gInfo ci msgId False byGroupMember brokerTs + delete :: CChatItem 'CTGroup -> Maybe GroupMember -> CM ChatResponse + delete cci byGroupMember + | groupFeatureAllowed SGFFullDelete gInfo = deleteGroupCIs user gInfo [cci] False False byGroupMember brokerTs + | otherwise = markGroupCIsDeleted user gInfo [cci] False byGroupMember brokerTs -- TODO remove once XFile is discontinued processFileInvitation' :: Contact -> FileInvitation -> RcvMessage -> MsgMeta -> CM () - processFileInvitation' ct fInv@FileInvitation {fileName, fileSize} msg@RcvMessage {sharedMsgId_} msgMeta = do + processFileInvitation' ct fInv' msg@RcvMessage {sharedMsgId_} msgMeta = do ChatConfig {fileChunkSize} <- asks config + let fInv@FileInvitation {fileName, fileSize} = mkValidFileInvitation fInv' inline <- receiveInlineMode fInv Nothing fileChunkSize RcvFileTransfer {fileId, xftpRcvFile} <- withStore $ \db -> createRcvFileTransfer db userId ct fInv inline fileChunkSize let fileProtocol = if isJust xftpRcvFile then FPXFTP else FPSMP @@ -5260,9 +5709,9 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = _ -> messageError "x.file.acpt.inv: member connection is not active" else messageError "x.file.acpt.inv: fileName is different from expected" - groupMsgToView :: GroupInfo -> ChatItem 'CTGroup 'MDRcv -> CM () + groupMsgToView :: forall d. MsgDirectionI d => GroupInfo -> ChatItem 'CTGroup d -> CM () groupMsgToView gInfo ci = - toView $ CRNewChatItem user (AChatItem SCTGroup SMDRcv (GroupChat gInfo) ci) + toView $ CRNewChatItem user (AChatItem SCTGroup (msgDirection @d) (GroupChat gInfo) ci) processGroupInvitation :: Contact -> GroupInvitation -> RcvMessage -> MsgMeta -> CM () processGroupInvitation ct inv msg msgMeta = do @@ -5736,14 +6185,14 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = withStore' $ \db -> updateGroupMemberStatusById db userId groupMemberId GSMemIntroInvited xGrpMemInv :: GroupInfo -> GroupMember -> MemberId -> IntroInvitation -> CM () - xGrpMemInv gInfo@GroupInfo {groupId} m memId introInv = do + xGrpMemInv gInfo m memId introInv = do case memberCategory m of GCInviteeMember -> withStore' (\db -> runExceptT $ getGroupMemberByMemberId db vr user gInfo memId) >>= \case Left _ -> messageError "x.grp.mem.inv error: referenced member does not exist" Right reMember -> do GroupMemberIntro {introId} <- withStore $ \db -> saveIntroInvitation db reMember m introInv - sendGroupMemberMessage user reMember (XGrpMemFwd (memberInfo m) introInv) groupId (Just introId) $ + sendGroupMemberMessage user gInfo reMember (XGrpMemFwd (memberInfo m) introInv) (Just introId) $ withStore' $ \db -> updateIntroStatus db introId GMIntroInvForwarded _ -> messageError "x.grp.mem.inv can be only sent by invitee member" @@ -6038,13 +6487,13 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta `catchChatError` \_ -> pure () forM_ msgRcpts $ \MsgReceipt {agentMsgId, msgRcptStatus} -> do withStore' $ \db -> updateSndMsgDeliveryStatus db connId agentMsgId $ MDSSndRcvd msgRcptStatus - updateGroupItemStatus gInfo m conn agentMsgId $ CISSndRcvd msgRcptStatus SSPComplete + updateGroupItemStatus gInfo m conn agentMsgId (GSSRcvd msgRcptStatus) Nothing updateDirectItemsStatus :: Contact -> Connection -> [AgentMsgId] -> CIStatus 'MDSnd -> CM () updateDirectItemsStatus ct conn msgIds newStatus = do cis_ <- withStore' $ \db -> forM msgIds $ \msgId -> runExceptT $ updateDirectItemStatus' db ct conn msgId newStatus -- only send the last expired item event to view - case catMaybes $ rights $ reverse cis_ of + case reverse $ catMaybes $ rights cis_ of ci : _ -> toView $ CRChatItemStatusUpdated user (AChatItem SCTDirect SMDSnd (DirectChat ct) ci) _ -> pure () @@ -6062,24 +6511,25 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = | otherwise -> Just <$> updateDirectChatItemStatus db user ct itemId newStatus _ -> pure Nothing - updateGroupMemSndStatus :: ChatItemId -> GroupMemberId -> CIStatus 'MDSnd -> CM Bool + updateGroupMemSndStatus :: ChatItemId -> GroupMemberId -> GroupSndStatus -> CM Bool updateGroupMemSndStatus itemId groupMemberId newStatus = withStore' $ \db -> updateGroupMemSndStatus' db itemId groupMemberId newStatus - updateGroupMemSndStatus' :: DB.Connection -> ChatItemId -> GroupMemberId -> CIStatus 'MDSnd -> IO Bool + updateGroupMemSndStatus' :: DB.Connection -> ChatItemId -> GroupMemberId -> GroupSndStatus -> IO Bool updateGroupMemSndStatus' db itemId groupMemberId newStatus = runExceptT (getGroupSndStatus db itemId groupMemberId) >>= \case - Right (CISSndRcvd _ _) -> pure False + Right (GSSRcvd _) -> pure False Right memStatus | memStatus == newStatus -> pure False | otherwise -> updateGroupSndStatus db itemId groupMemberId newStatus $> True _ -> pure False - updateGroupItemStatus :: GroupInfo -> GroupMember -> Connection -> AgentMsgId -> CIStatus 'MDSnd -> CM () - updateGroupItemStatus gInfo@GroupInfo {groupId} GroupMember {groupMemberId} Connection {connId} msgId newMemStatus = + updateGroupItemStatus :: GroupInfo -> GroupMember -> Connection -> AgentMsgId -> GroupSndStatus -> Maybe Bool -> CM () + updateGroupItemStatus gInfo@GroupInfo {groupId} GroupMember {groupMemberId} Connection {connId} msgId newMemStatus viaProxy_ = withStore' (\db -> getGroupChatItemByAgentMsgId db user groupId connId msgId) >>= \case Just (CChatItem SMDSnd ChatItem {meta = CIMeta {itemStatus = CISSndRcvd _ SSPComplete}}) -> pure () Just (CChatItem SMDSnd ChatItem {meta = CIMeta {itemId, itemStatus}}) -> do + forM_ viaProxy_ $ \viaProxy -> withStore' $ \db -> setGroupSndViaProxy db itemId groupMemberId viaProxy memStatusChanged <- updateGroupMemSndStatus itemId groupMemberId newMemStatus when memStatusChanged $ do memStatusCounts <- withStore' (`getGroupSndStatusCounts` itemId) @@ -6363,6 +6813,22 @@ deleteOrUpdateMemberRecord user@User {userId} member = Just _ -> updateGroupMemberStatus db userId member GSMemRemoved Nothing -> deleteGroupMember db user member +sendDirectContactMessages :: MsgEncodingI e => User -> Contact -> NonEmpty (ChatMsgEvent e) -> CM () +sendDirectContactMessages user ct events = do + Connection {connChatVersion = v} <- liftEither $ contactSendConn_ ct + if v >= batchSend2Version + then sendDirectContactMessages' user ct events + else mapM_ (void . sendDirectContactMessage user ct) events + +sendDirectContactMessages' :: MsgEncodingI e => User -> Contact -> NonEmpty (ChatMsgEvent e) -> CM () +sendDirectContactMessages' user ct events = do + conn@Connection {connId} <- liftEither $ contactSendConn_ ct + let idsEvts = L.map (ConnectionId connId,) events + msgFlags = MsgFlags {notification = any (hasNotification . toCMEventTag) events} + (errs, msgs) <- lift $ partitionEithers . L.toList <$> createSndMessages idsEvts + unless (null errs) $ toView $ CRChatErrors (Just user) errs + mapM_ (batchSendConnMessages user conn msgFlags) (L.nonEmpty msgs) + sendDirectContactMessage :: MsgEncodingI e => User -> Contact -> ChatMsgEvent e -> CM (SndMessage, Int64) sendDirectContactMessage user ct chatMsgEvent = do conn@Connection {connId} <- liftEither $ contactSendConn_ ct @@ -6418,35 +6884,24 @@ sendGroupMemberMessages user conn events groupId = do let idsEvts = L.map (GroupId groupId,) events (errs, msgs) <- lift $ partitionEithers . L.toList <$> createSndMessages idsEvts unless (null errs) $ toView $ CRChatErrors (Just user) errs - forM_ (L.nonEmpty msgs) $ \msgs' -> do - -- TODO v5.7 based on version (?) - -- let shouldCompress = False - -- let batched = if shouldCompress then batchSndMessagesBinary msgs' else batchSndMessagesJSON msgs' - let batched = batchSndMessagesJSON msgs' - let (errs', msgBatches) = partitionEithers batched - -- shouldn't happen, as large messages would have caused createNewSndMessage to throw SELargeMsg - unless (null errs') $ toView $ CRChatErrors (Just user) errs' - forM_ msgBatches $ \batch -> - processSndMessageBatch conn batch `catchChatError` (toView . CRChatError (Just user)) + forM_ (L.nonEmpty msgs) $ \msgs' -> + batchSendConnMessages user conn MsgFlags {notification = True} msgs' -processSndMessageBatch :: Connection -> MsgBatch -> CM () -processSndMessageBatch conn@Connection {connId} (MsgBatch batchBody sndMsgs) = do - (agentMsgId, _pqEnc) <- withAgent $ \a -> sendMessage a (aConnId conn) PQEncOff MsgFlags {notification = True} batchBody - let sndMsgDelivery = SndMsgDelivery {connId, agentMsgId} - lift . void . withStoreBatch' $ \db -> map (\SndMessage {msgId} -> createSndMsgDelivery db sndMsgDelivery msgId) sndMsgs +batchSendConnMessages :: User -> Connection -> MsgFlags -> NonEmpty SndMessage -> CM () +batchSendConnMessages user conn msgFlags msgs = do + let batched = batchSndMessagesJSON msgs + let (errs', msgBatches) = partitionEithers batched + -- shouldn't happen, as large messages would have caused createNewSndMessage to throw SELargeMsg + unless (null errs') $ toView $ CRChatErrors (Just user) errs' + forM_ (L.nonEmpty msgBatches) $ \msgBatches' -> do + let msgReq = L.map (msgBatchReq conn msgFlags) msgBatches' + void $ deliverMessages msgReq --- TODO v5.7 update batching for groups batchSndMessagesJSON :: NonEmpty SndMessage -> [Either ChatError MsgBatch] batchSndMessagesJSON = batchMessages maxEncodedMsgLength . L.toList --- batchSndMessagesBinary :: NonEmpty SndMessage -> [Either ChatError MsgBatch] --- batchSndMessagesBinary msgs = map toMsgBatch . SMP.batchTransmissions_ (maxEncodedMsgLength) $ L.zip (map compress1 msgs) msgs --- where --- toMsgBatch :: SMP.TransportBatch SndMessage -> Either ChatError MsgBatch --- toMsgBatch = \case --- SMP.TBTransmissions combined _n sms -> Right $ MsgBatch (markCompressedBatch combined) sms --- SMP.TBError tbe SndMessage {msgId} -> Left . ChatError $ CEInternalError (show tbe <> " " <> show msgId) --- SMP.TBTransmission {} -> Left . ChatError $ CEInternalError "batchTransmissions_ didn't produce a batch" +msgBatchReq :: Connection -> MsgFlags -> MsgBatch -> ChatMsgReq +msgBatchReq conn msgFlags (MsgBatch batchBody sndMsgs) = (conn, msgFlags, batchBody, map (\SndMessage {msgId} -> msgId) sndMsgs) encodeConnInfo :: MsgEncodingI e => ChatMsgEvent e -> CM ByteString encodeConnInfo chatMsgEvent = do @@ -6473,19 +6928,23 @@ deliverMessage conn cmEventTag msgBody msgId = do deliverMessage' :: Connection -> MsgFlags -> MsgBody -> MessageId -> CM (Int64, PQEncryption) deliverMessage' conn msgFlags msgBody msgId = - deliverMessages ((conn, msgFlags, msgBody, msgId) :| []) >>= \case - r :| [] -> liftEither r + deliverMessages ((conn, msgFlags, msgBody, [msgId]) :| []) >>= \case + r :| [] -> case r of + Right ([deliveryId], pqEnc) -> pure (deliveryId, pqEnc) + Right (deliveryIds, _) -> throwChatError $ CEInternalError $ "deliverMessage: expected 1 delivery id, got " <> show (length deliveryIds) + Left e -> throwError e rs -> throwChatError $ CEInternalError $ "deliverMessage: expected 1 result, got " <> show (length rs) -type MsgReq = (Connection, MsgFlags, MsgBody, MessageId) +-- [MessageId] - SndMessage ids inside MsgBatch, or single message id +type ChatMsgReq = (Connection, MsgFlags, MsgBody, [MessageId]) -deliverMessages :: NonEmpty MsgReq -> CM (NonEmpty (Either ChatError (Int64, PQEncryption))) +deliverMessages :: NonEmpty ChatMsgReq -> CM (NonEmpty (Either ChatError ([Int64], PQEncryption))) deliverMessages msgs = deliverMessagesB $ L.map Right msgs -deliverMessagesB :: NonEmpty (Either ChatError MsgReq) -> CM (NonEmpty (Either ChatError (Int64, PQEncryption))) +deliverMessagesB :: NonEmpty (Either ChatError ChatMsgReq) -> CM (NonEmpty (Either ChatError ([Int64], PQEncryption))) deliverMessagesB msgReqs = do msgReqs' <- liftIO compressBodies - sent <- L.zipWith prepareBatch msgReqs' <$> withAgent (`sendMessagesB` L.map toAgent msgReqs') + sent <- L.zipWith prepareBatch msgReqs' <$> withAgent (`sendMessagesB` snd (mapAccumL toAgent Nothing msgReqs')) lift . void $ withStoreBatch' $ \db -> map (updatePQSndEnabled db) (rights . L.toList $ sent) lift . withStoreBatch $ \db -> L.map (bindRight $ createDelivery db) sent where @@ -6501,16 +6960,20 @@ deliverMessagesB msgReqs = do when (B.length msgBody' > maxCompressedMsgLength) $ throwError $ ChatError $ CEException "large compressed message" pure (conn, msgFlags, msgBody', msgId) _ -> pure mr - toAgent = \case - Right (conn@Connection {pqEncryption}, msgFlags, msgBody, _msgId) -> Right (aConnId conn, pqEncryption, msgFlags, msgBody) - Left _ce -> Left (AP.INTERNAL "ChatError, skip") -- as long as it is Left, the agent batchers should just step over it + toAgent prev = \case + Right (conn@Connection {connId, pqEncryption}, msgFlags, msgBody, _msgIds) -> + let cId = case prev of + Just prevId | prevId == connId -> "" + _ -> aConnId conn + in (Just connId, Right (cId, pqEncryption, msgFlags, msgBody)) + Left _ce -> (prev, Left (AP.INTERNAL "ChatError, skip")) -- as long as it is Left, the agent batchers should just step over it prepareBatch (Right req) (Right ar) = Right (req, ar) prepareBatch (Left ce) _ = Left ce -- restore original ChatError prepareBatch _ (Left ae) = Left $ ChatErrorAgent ae Nothing - createDelivery :: DB.Connection -> (MsgReq, (AgentMsgId, PQEncryption)) -> IO (Either ChatError (Int64, PQEncryption)) - createDelivery db ((Connection {connId}, _, _, msgId), (agentMsgId, pqEnc')) = - Right . (,pqEnc') <$> createSndMsgDelivery db (SndMsgDelivery {connId, agentMsgId}) msgId - updatePQSndEnabled :: DB.Connection -> (MsgReq, (AgentMsgId, PQEncryption)) -> IO () + createDelivery :: DB.Connection -> (ChatMsgReq, (AgentMsgId, PQEncryption)) -> IO (Either ChatError ([Int64], PQEncryption)) + createDelivery db ((Connection {connId}, _, _, msgIds), (agentMsgId, pqEnc')) = do + Right . (,pqEnc') <$> mapM (createSndMsgDelivery db (SndMsgDelivery {connId, agentMsgId})) msgIds + updatePQSndEnabled :: DB.Connection -> (ChatMsgReq, (AgentMsgId, PQEncryption)) -> IO () updatePQSndEnabled db ((Connection {connId, pqSndEnabled}, _, _, _), (_, pqSndEnabled')) = case (pqSndEnabled, pqSndEnabled') of (Just b, b') | b' /= b -> updatePQ @@ -6519,11 +6982,13 @@ deliverMessagesB msgReqs = do where updatePQ = updateConnPQSndEnabled db connId pqSndEnabled' -sendGroupMessage :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> CM (SndMessage, [GroupMember]) +-- TODO combine profile update and message into one batch +-- Take into account that it may not fit, and that we currently don't support sending multiple messages to the same connection in one call. +sendGroupMessage :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> CM (SndMessage, GroupSndResultData) sendGroupMessage user gInfo members chatMsgEvent = do when shouldSendProfileUpdate $ - sendProfileUpdate `catchChatError` (\e -> toView (CRChatError (Just user) e)) - sendGroupMessage' user gInfo members chatMsgEvent + sendProfileUpdate `catchChatError` (toView . CRChatError (Just user)) + sendGroupMessage_ user gInfo members chatMsgEvent where User {profile = p, userMemberProfileUpdatedAt} = user GroupInfo {userMemberProfileSentAt} = gInfo @@ -6541,21 +7006,59 @@ sendGroupMessage user gInfo members chatMsgEvent = do currentTs <- liftIO getCurrentTime withStore' $ \db -> updateUserMemberProfileSentAt db user gInfo currentTs -sendGroupMessage' :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> CM (SndMessage, [GroupMember]) -sendGroupMessage' user GroupInfo {groupId} members chatMsgEvent = do - msg@SndMessage {msgId, msgBody} <- createSndMessage chatMsgEvent (GroupId groupId) - recipientMembers <- liftIO $ shuffleMembers (filter memberCurrent members) - let msgFlags = MsgFlags {notification = hasNotification $ toCMEventTag chatMsgEvent} - (toSend, pending, _, dups) = foldr addMember ([], [], S.empty, 0 :: Int) recipientMembers +type GroupSndResultData = (([Either ChatError ([Int64], PQEncryption)], [(GroupMember, Connection)]), ([Either ChatError ()], [GroupMember]), [GroupMember]) + +data GroupSndResult = GroupSndResult + { sentTo :: [GroupMember], + pending :: [GroupMember], + forwarded :: [GroupMember] + } + +sendGroupMessage' :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> CM SndMessage +sendGroupMessage' user gInfo members chatMsgEvent = fst <$> sendGroupMessage_ user gInfo members chatMsgEvent + +sendGroupMessage_ :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> CM (SndMessage, GroupSndResultData) +sendGroupMessage_ user gInfo members chatMsgEvent = + sendGroupMessages_ user gInfo members (chatMsgEvent :| []) >>= \case + (msg :| [], r) -> pure (msg, r) + _ -> throwChatError $ CEInternalError "sendGroupMessage': expected 1 message" + +sendGroupMessages :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> NonEmpty (ChatMsgEvent e) -> CM () +sendGroupMessages user gInfo members events = void $ sendGroupMessages_ user gInfo members events + +mkGroupSndResult :: GroupSndResultData -> GroupSndResult +mkGroupSndResult ((delivered, sentTo), (stored, pending), forwarded) = + GroupSndResult + { sentTo = filterSent' delivered sentTo fst, + pending = filterSent' stored pending id, + forwarded + } + where + -- TODO in theory this could deduplicate members and keep results only when ... some sent? or all sent? + -- This is not important, as it is not used in batch calls + filterSent' :: [Either ChatError a] -> [mem] -> (mem -> GroupMember) -> [GroupMember] + filterSent' rs ms mem = [mem m | (Right _, m) <- zip rs ms] + +sendGroupMessages_ :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> NonEmpty (ChatMsgEvent e) -> CM (NonEmpty SndMessage, GroupSndResultData) +sendGroupMessages_ user gInfo@GroupInfo {groupId} members events = do + let idsEvts = L.map (GroupId groupId,) events + (errs, msgs) <- lift $ partitionEithers . L.toList <$> createSndMessages idsEvts + unless (null errs) $ toView $ CRChatErrors (Just user) errs + case L.nonEmpty msgs of + Nothing -> throwChatError $ CEInternalError "sendGroupMessages: no messages created" + Just msgs' -> do + recipientMembers <- liftIO $ shuffleMembers (filter memberCurrent members) + let msgFlags = MsgFlags {notification = any (hasNotification . toCMEventTag) events} + (toSendSeparate, toSendBatched, pending, forwarded, _, dups) = + foldr addMember ([], [], [], [], S.empty, 0 :: Int) recipientMembers + when (dups /= 0) $ logError $ "sendGroupMessage: " <> tshow dups <> " duplicate members" -- TODO PQ either somehow ensure that group members connections cannot have pqSupport/pqEncryption or pass Off's here - msgReqs = map (\(_, conn) -> (conn, msgFlags, msgBody, msgId)) toSend - when (dups /= 0) $ logError $ "sendGroupMessage: " <> tshow dups <> " duplicate members" - delivered <- maybe (pure []) (fmap L.toList . deliverMessages) $ L.nonEmpty msgReqs - let errors = lefts delivered - unless (null errors) $ toView $ CRChatErrors (Just user) errors - stored <- lift . withStoreBatch' $ \db -> map (\m -> createPendingGroupMessage db (groupMemberId' m) msgId Nothing) pending - let sentToMembers = filterSent delivered toSend fst <> filterSent stored pending id - pure (msg, sentToMembers) + let msgReqs = prepareMsgReqs msgFlags msgs' toSendSeparate toSendBatched + delivered <- maybe (pure []) (fmap L.toList . deliverMessages) $ L.nonEmpty msgReqs + let errors = lefts delivered + unless (null errors) $ toView $ CRChatErrors (Just user) errors + stored <- lift . withStoreBatch' $ \db -> map (\m -> createPendingMsgs db m msgs') pending + pure (msgs', ((delivered, toSendSeparate <> toSendBatched), (stored, pending), forwarded)) where shuffleMembers :: [GroupMember] -> IO [GroupMember] shuffleMembers ms = do @@ -6563,85 +7066,112 @@ sendGroupMessage' user GroupInfo {groupId} members chatMsgEvent = do liftM2 (<>) (shuffle adminMs) (shuffle otherMs) where isAdmin GroupMember {memberRole} = memberRole >= GRAdmin - addMember m acc@(toSend, pending, !mIds, !dups) = case memberSendAction chatMsgEvent members m of - Just a - | mId `S.member` mIds -> (toSend, pending, mIds, dups + 1) - | otherwise -> case a of - MSASend conn -> ((m, conn) : toSend, pending, mIds', dups) - MSAPending -> (toSend, m : pending, mIds', dups) - Nothing -> acc + addMember m acc@(toSendSeparate, toSendBatched, pending, forwarded, !mIds, !dups) = + case memberSendAction gInfo events members m of + Just a + | mId `S.member` mIds -> (toSendSeparate, toSendBatched, pending, forwarded, mIds, dups + 1) + | otherwise -> case a of + MSASend conn -> ((m, conn) : toSendSeparate, toSendBatched, pending, forwarded, mIds', dups) + MSASendBatched conn -> (toSendSeparate, (m, conn) : toSendBatched, pending, forwarded, mIds', dups) + MSAPending -> (toSendSeparate, toSendBatched, m : pending, forwarded, mIds', dups) + MSAForwarded -> (toSendSeparate, toSendBatched, pending, m : forwarded, mIds', dups) + Nothing -> acc where mId = groupMemberId' m mIds' = S.insert mId mIds - filterSent :: [Either ChatError a] -> [mem] -> (mem -> GroupMember) -> [GroupMember] - filterSent rs ms mem = [mem m | (Right _, m) <- zip rs ms] + prepareMsgReqs :: MsgFlags -> NonEmpty SndMessage -> [(GroupMember, Connection)] -> [(GroupMember, Connection)] -> [ChatMsgReq] + prepareMsgReqs msgFlags msgs toSendSeparate toSendBatched = do + let msgReqsSeparate = foldr (\(_, conn) reqs -> foldr (\msg -> (sndMessageReq conn msg :)) reqs msgs) [] toSendSeparate + batched = batchSndMessagesJSON msgs + -- _errs shouldn't happen, as large messages would have caused createNewSndMessage to throw SELargeMsg + (_errs, msgBatches) = partitionEithers batched + case L.nonEmpty msgBatches of + Just msgBatches' -> do + let msgReqsBatched = foldr (\(_, conn) reqs -> foldr (\batch -> (msgBatchReq conn msgFlags batch :)) reqs msgBatches') [] toSendBatched + msgReqsSeparate <> msgReqsBatched + Nothing -> msgReqsSeparate + where + sndMessageReq :: Connection -> SndMessage -> ChatMsgReq + sndMessageReq conn SndMessage {msgId, msgBody} = (conn, msgFlags, msgBody, [msgId]) + createPendingMsgs :: DB.Connection -> GroupMember -> NonEmpty SndMessage -> IO () + createPendingMsgs db m = mapM_ (\SndMessage {msgId} -> createPendingGroupMessage db (groupMemberId' m) msgId Nothing) -data MemberSendAction = MSASend Connection | MSAPending +data MemberSendAction = MSASend Connection | MSASendBatched Connection | MSAPending | MSAForwarded -memberSendAction :: ChatMsgEvent e -> [GroupMember] -> GroupMember -> Maybe MemberSendAction -memberSendAction chatMsgEvent members m@GroupMember {invitedByGroupMemberId} = case memberConn m of +memberSendAction :: GroupInfo -> NonEmpty (ChatMsgEvent e) -> [GroupMember] -> GroupMember -> Maybe MemberSendAction +memberSendAction gInfo events members m@GroupMember {memberRole} = case memberConn m of Nothing -> pendingOrForwarded Just conn@Connection {connStatus} | connDisabled conn || connStatus == ConnDeleted -> Nothing - | connStatus == ConnSndReady || connStatus == ConnReady -> Just (MSASend conn) + | connInactive conn -> Just MSAPending + | connStatus == ConnSndReady || connStatus == ConnReady -> sendBatchedOrSeparate conn | otherwise -> pendingOrForwarded where - pendingOrForwarded - | forwardSupported && isForwardedGroupMsg chatMsgEvent = Nothing - | isXGrpMsgForward chatMsgEvent = Nothing - | otherwise = Just MSAPending + sendBatchedOrSeparate conn + -- admin doesn't support batch forwarding - send messages separately so that admin can forward one by one + | memberRole >= GRAdmin && not (m `supportsVersion` batchSend2Version) = Just (MSASend conn) + -- either member is not admin, or admin supports batched forwarding + | otherwise = Just (MSASendBatched conn) + pendingOrForwarded = case memberCategory m of + GCUserMember -> Nothing -- shouldn't happen + GCInviteeMember -> Just MSAPending + GCHostMember -> Just MSAPending + GCPreMember -> forwardSupportedOrPending (invitedByGroupMemberId $ membership gInfo) + GCPostMember -> forwardSupportedOrPending (invitedByGroupMemberId m) where - forwardSupported = m `supportsVersion` groupForwardVersion && invitingMemberSupportsForward - invitingMemberSupportsForward = case invitedByGroupMemberId of - Just invMemberId -> - -- can be optimized for large groups by replacing [GroupMember] with Map GroupMemberId GroupMember - case find (\m' -> groupMemberId' m' == invMemberId) members of - Just invitingMember -> invitingMember `supportsVersion` groupForwardVersion + forwardSupportedOrPending invitingMemberId_ + | membersSupport && all isForwardedGroupMsg events = Just MSAForwarded + | any isXGrpMsgForward events = Nothing + | otherwise = Just MSAPending + where + membersSupport = + m `supportsVersion` groupForwardVersion && invitingMemberSupportsForward + invitingMemberSupportsForward = case invitingMemberId_ of + Just invMemberId -> + -- can be optimized for large groups by replacing [GroupMember] with Map GroupMemberId GroupMember + case find (\m' -> groupMemberId' m' == invMemberId) members of + Just invitingMember -> invitingMember `supportsVersion` groupForwardVersion + Nothing -> False Nothing -> False - Nothing -> False - isXGrpMsgForward ev = case ev of - XGrpMsgForward {} -> True - _ -> False + isXGrpMsgForward event = case event of + XGrpMsgForward {} -> True + _ -> False -sendGroupMemberMessage :: MsgEncodingI e => User -> GroupMember -> ChatMsgEvent e -> Int64 -> Maybe Int64 -> CM () -> CM () -sendGroupMemberMessage user m@GroupMember {groupMemberId} chatMsgEvent groupId introId_ postDeliver = do +sendGroupMemberMessage :: MsgEncodingI e => User -> GroupInfo -> GroupMember -> ChatMsgEvent e -> Maybe Int64 -> CM () -> CM () +sendGroupMemberMessage user gInfo@GroupInfo {groupId} m@GroupMember {groupMemberId} chatMsgEvent introId_ postDeliver = do msg <- createSndMessage chatMsgEvent (GroupId groupId) - messageMember msg `catchChatError` (\e -> toView (CRChatError (Just user) e)) + messageMember msg `catchChatError` (toView . CRChatError (Just user)) where messageMember :: SndMessage -> CM () - messageMember SndMessage {msgId, msgBody} = forM_ (memberSendAction chatMsgEvent [m] m) $ \case + messageMember SndMessage {msgId, msgBody} = forM_ (memberSendAction gInfo (chatMsgEvent :| []) [m] m) $ \case MSASend conn -> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId >> postDeliver + MSASendBatched conn -> deliverMessage conn (toCMEventTag chatMsgEvent) msgBody msgId >> postDeliver MSAPending -> withStore' $ \db -> createPendingGroupMessage db groupMemberId msgId introId_ + MSAForwarded -> pure () +-- TODO ensure order - pending messages interleave with user input messages sendPendingGroupMessages :: User -> GroupMember -> Connection -> CM () -sendPendingGroupMessages user GroupMember {groupMemberId, localDisplayName} conn = do - pendingMessages <- withStore' $ \db -> getPendingGroupMessages db groupMemberId - -- TODO ensure order - pending messages interleave with user input messages - forM_ pendingMessages $ \pgm -> - processPendingMessage pgm `catchChatError` (toView . CRChatError (Just user)) +sendPendingGroupMessages user GroupMember {groupMemberId} conn = do + pgms <- withStore' $ \db -> getPendingGroupMessages db groupMemberId + forM_ (L.nonEmpty pgms) $ \pgms' -> do + let msgs = L.map (\(sndMsg, _, _) -> sndMsg) pgms' + batchSendConnMessages user conn MsgFlags {notification = True} msgs + lift . void . withStoreBatch' $ \db -> L.map (\SndMessage {msgId} -> deletePendingGroupMessage db groupMemberId msgId) msgs + lift . void . withStoreBatch' $ \db -> L.map (\(_, tag, introId_) -> updateIntro_ db tag introId_) pgms' where - processPendingMessage PendingGroupMessage {msgId, cmEventTag = ACMEventTag _ tag, msgBody, introId_} = do - void $ deliverMessage conn tag msgBody msgId - withStore' $ \db -> deletePendingGroupMessage db groupMemberId msgId - case tag of - XGrpMemFwd_ -> case introId_ of - Just introId -> withStore' $ \db -> updateIntroStatus db introId GMIntroInvForwarded - _ -> throwChatError $ CEGroupMemberIntroNotFound localDisplayName - _ -> pure () + updateIntro_ :: DB.Connection -> ACMEventTag -> Maybe Int64 -> IO () + updateIntro_ db tag introId_ = case (tag, introId_) of + (ACMEventTag _ XGrpMemFwd_, Just introId) -> updateIntroStatus db introId GMIntroInvForwarded + _ -> pure () --- TODO [batch send] refactor direct message processing same as groups (e.g. checkIntegrity before processing) -saveDirectRcvMSG :: Connection -> MsgMeta -> MsgBody -> CM (Connection, RcvMessage) -saveDirectRcvMSG conn@Connection {connId} agentMsgMeta msgBody = - case parseChatMessages msgBody of - [Right (ACMsg _ ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent})] -> do - conn' <- updatePeerChatVRange conn chatVRange - let agentMsgId = fst $ recipient agentMsgMeta - newMsg = NewRcvMessage {chatMsgEvent, msgBody} - rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta} - msg <- withStore $ \db -> createNewMessageAndRcvMsgDelivery db (ConnectionId connId) newMsg sharedMsgId_ rcvMsgDelivery Nothing - pure (conn', msg) - [Left e] -> error $ "saveDirectRcvMSG: error parsing chat message: " <> e - _ -> error "saveDirectRcvMSG: batching not supported" +saveDirectRcvMSG :: MsgEncodingI e => Connection -> MsgMeta -> MsgBody -> ChatMessage e -> CM (Connection, RcvMessage) +saveDirectRcvMSG conn@Connection {connId} agentMsgMeta msgBody ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent} = do + conn' <- updatePeerChatVRange conn chatVRange + let agentMsgId = fst $ recipient agentMsgMeta + newMsg = NewRcvMessage {chatMsgEvent, msgBody} + rcvMsgDelivery = RcvMsgDelivery {connId, agentMsgId, agentMsgMeta} + msg <- withStore $ \db -> createNewMessageAndRcvMsgDelivery db (ConnectionId connId) newMsg sharedMsgId_ rcvMsgDelivery Nothing + pure (conn', msg) saveGroupRcvMsg :: MsgEncodingI e => User -> GroupId -> GroupMember -> Connection -> MsgMeta -> MsgBody -> ChatMessage e -> CM (GroupMember, Connection, RcvMessage) saveGroupRcvMsg user groupId authorMember conn@Connection {connId} agentMsgMeta msgBody ChatMessage {chatVRange, msgId = sharedMsgId_, chatMsgEvent} = do @@ -6685,7 +7215,7 @@ saveSndChatItem' :: ChatTypeI c => User -> ChatDirection c 'MDSnd -> SndMessage saveSndChatItem' user cd msg@SndMessage {sharedMsgId} content ciFile quotedItem itemForwarded itemTimed live = do createdAt <- liftIO getCurrentTime ciId <- withStore' $ \db -> do - when (ciRequiresAttention content) $ updateChatTs db user cd createdAt + when (ciRequiresAttention content || contactChatDeleted cd) $ updateChatTs db user cd createdAt ciId <- createNewSndChatItem db user cd msg content quotedItem itemForwarded itemTimed live createdAt forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt pure ciId @@ -6699,7 +7229,7 @@ saveRcvChatItem' :: (ChatTypeI c, ChatTypeQuotable c) => User -> ChatDirection c saveRcvChatItem' user cd msg@RcvMessage {forwardedByMember} sharedMsgId_ brokerTs content ciFile itemTimed live = do createdAt <- liftIO getCurrentTime (ciId, quotedItem, itemForwarded) <- withStore' $ \db -> do - when (ciRequiresAttention content) $ updateChatTs db user cd createdAt + when (ciRequiresAttention content || contactChatDeleted cd) $ updateChatTs db user cd createdAt r@(ciId, _, _) <- createNewRcvChatItem db user cd msg sharedMsgId_ content itemTimed live brokerTs createdAt forM_ ciFile $ \CIFile {fileId} -> updateFileTransferChatItemId db fileId ciId createdAt pure r @@ -6709,62 +7239,88 @@ mkChatItem :: (ChatTypeI c, MsgDirectionI d) => ChatDirection c d -> ChatItemId mkChatItem cd ciId content file quotedItem sharedMsgId itemForwarded itemTimed live itemTs forwardedByMember currentTs = let itemText = ciContentToText content itemStatus = ciCreateStatus content - meta = mkCIMeta ciId content itemText itemStatus sharedMsgId itemForwarded Nothing False itemTimed (justTrue live) currentTs itemTs forwardedByMember currentTs currentTs + meta = mkCIMeta ciId content itemText itemStatus Nothing sharedMsgId itemForwarded Nothing False itemTimed (justTrue live) currentTs itemTs forwardedByMember currentTs currentTs in ChatItem {chatDir = toCIDirection cd, meta, content, formattedText = parseMaybeMarkdownList itemText, quotedItem, reactions = [], file} -deleteDirectCI :: MsgDirectionI d => User -> Contact -> ChatItem 'CTDirect d -> Bool -> Bool -> CM ChatResponse -deleteDirectCI user ct ci@ChatItem {file} byUser timed = do - deleteCIFile user file - withStore' $ \db -> deleteDirectChatItem db user ct ci - pure $ CRChatItemDeleted user (AChatItem SCTDirect msgDirection (DirectChat ct) ci) Nothing byUser timed - -deleteGroupCI :: MsgDirectionI d => User -> GroupInfo -> ChatItem 'CTGroup d -> Bool -> Bool -> Maybe GroupMember -> UTCTime -> CM ChatResponse -deleteGroupCI user gInfo ci@ChatItem {file} byUser timed byGroupMember_ deletedTs = do - deleteCIFile user file - toCi <- withStore' $ \db -> - case byGroupMember_ of - Nothing -> deleteGroupChatItem db user gInfo ci $> Nothing - Just m -> Just <$> updateGroupChatItemModerated db user gInfo ci m deletedTs - pure $ CRChatItemDeleted user (gItem ci) (gItem <$> toCi) byUser timed +deleteDirectCIs :: User -> Contact -> [CChatItem 'CTDirect] -> Bool -> Bool -> CM ChatResponse +deleteDirectCIs user ct items byUser timed = do + let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items + deleteCIFiles user ciFilesInfo + (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (deleteItem db) items) + unless (null errs) $ toView $ CRChatErrors (Just user) errs + pure $ CRChatItemsDeleted user deletions byUser timed where - gItem = AChatItem SCTGroup msgDirection (GroupChat gInfo) + deleteItem db (CChatItem md ci) = do + deleteDirectChatItem db user ct ci + pure $ contactDeletion md ct ci Nothing -deleteLocalCI :: MsgDirectionI d => User -> NoteFolder -> ChatItem 'CTLocal d -> Bool -> Bool -> CM ChatResponse -deleteLocalCI user nf ci@ChatItem {file = file_} byUser timed = do - forM_ file_ $ \file -> do - let filesInfo = [mkCIFileInfo file] - deleteFilesLocally filesInfo - withStore' $ \db -> deleteLocalChatItem db user nf ci - pure $ CRChatItemDeleted user (AChatItem SCTLocal msgDirection (LocalChat nf) ci) Nothing byUser timed - -deleteCIFile :: MsgDirectionI d => User -> Maybe (CIFile d) -> CM () -deleteCIFile user file_ = - forM_ file_ $ \file -> do - let filesInfo = [mkCIFileInfo file] - cancelFilesInProgress user filesInfo - deleteFilesLocally filesInfo - -markDirectCIDeleted :: MsgDirectionI d => User -> Contact -> ChatItem 'CTDirect d -> MessageId -> Bool -> UTCTime -> CM ChatResponse -markDirectCIDeleted user ct ci@ChatItem {file} msgId byUser deletedTs = do - cancelCIFile user file - ci' <- withStore' $ \db -> markDirectChatItemDeleted db user ct ci msgId deletedTs - pure $ CRChatItemDeleted user (ctItem ci) (Just $ ctItem ci') byUser False +deleteGroupCIs :: User -> GroupInfo -> [CChatItem 'CTGroup] -> Bool -> Bool -> Maybe GroupMember -> UTCTime -> CM ChatResponse +deleteGroupCIs user gInfo items byUser timed byGroupMember_ deletedTs = do + let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items + deleteCIFiles user ciFilesInfo + (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (deleteItem db) items) + unless (null errs) $ toView $ CRChatErrors (Just user) errs + pure $ CRChatItemsDeleted user deletions byUser timed where - ctItem = AChatItem SCTDirect msgDirection (DirectChat ct) + deleteItem :: DB.Connection -> CChatItem 'CTGroup -> IO ChatItemDeletion + deleteItem db (CChatItem md ci) = do + ci' <- case byGroupMember_ of + Just m -> Just <$> updateGroupChatItemModerated db user gInfo ci m deletedTs + Nothing -> Nothing <$ deleteGroupChatItem db user gInfo ci + pure $ groupDeletion md gInfo ci ci' -markGroupCIDeleted :: MsgDirectionI d => User -> GroupInfo -> ChatItem 'CTGroup d -> MessageId -> Bool -> Maybe GroupMember -> UTCTime -> CM ChatResponse -markGroupCIDeleted user gInfo ci@ChatItem {file} msgId byUser byGroupMember_ deletedTs = do - cancelCIFile user file - ci' <- withStore' $ \db -> markGroupChatItemDeleted db user gInfo ci msgId byGroupMember_ deletedTs - pure $ CRChatItemDeleted user (gItem ci) (Just $ gItem ci') byUser False +deleteLocalCIs :: User -> NoteFolder -> [CChatItem 'CTLocal] -> Bool -> Bool -> CM ChatResponse +deleteLocalCIs user nf items byUser timed = do + let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items + deleteFilesLocally ciFilesInfo + (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (deleteItem db) items) + unless (null errs) $ toView $ CRChatErrors (Just user) errs + pure $ CRChatItemsDeleted user deletions byUser timed where - gItem = AChatItem SCTGroup msgDirection (GroupChat gInfo) + deleteItem db (CChatItem md ci) = do + deleteLocalChatItem db user nf ci + pure $ ChatItemDeletion (nfItem md ci) Nothing + nfItem :: MsgDirectionI d => SMsgDirection d -> ChatItem 'CTLocal d -> AChatItem + nfItem md = AChatItem SCTLocal md (LocalChat nf) -cancelCIFile :: MsgDirectionI d => User -> Maybe (CIFile d) -> CM () -cancelCIFile user file_ = - forM_ file_ $ \file -> do - let filesInfo = [mkCIFileInfo file] - cancelFilesInProgress user filesInfo +deleteCIFiles :: User -> [CIFileInfo] -> CM () +deleteCIFiles user filesInfo = do + cancelFilesInProgress user filesInfo + deleteFilesLocally filesInfo + +markDirectCIsDeleted :: User -> Contact -> [CChatItem 'CTDirect] -> Bool -> UTCTime -> CM ChatResponse +markDirectCIsDeleted user ct items byUser deletedTs = do + let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items + cancelFilesInProgress user ciFilesInfo + (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (markDeleted db) items) + unless (null errs) $ toView $ CRChatErrors (Just user) errs + pure $ CRChatItemsDeleted user deletions byUser False + where + markDeleted db (CChatItem md ci) = do + ci' <- markDirectChatItemDeleted db user ct ci deletedTs + pure $ contactDeletion md ct ci (Just ci') + +markGroupCIsDeleted :: User -> GroupInfo -> [CChatItem 'CTGroup] -> Bool -> Maybe GroupMember -> UTCTime -> CM ChatResponse +markGroupCIsDeleted user gInfo items byUser byGroupMember_ deletedTs = do + let ciFilesInfo = mapMaybe (\(CChatItem _ ChatItem {file}) -> mkCIFileInfo <$> file) items + cancelFilesInProgress user ciFilesInfo + (errs, deletions) <- lift $ partitionEithers <$> withStoreBatch' (\db -> map (markDeleted db) items) + unless (null errs) $ toView $ CRChatErrors (Just user) errs + pure $ CRChatItemsDeleted user deletions byUser False + where + markDeleted db (CChatItem md ci) = do + ci' <- markGroupChatItemDeleted db user gInfo ci byGroupMember_ deletedTs + pure $ groupDeletion md gInfo ci (Just ci') + +groupDeletion :: MsgDirectionI d => SMsgDirection d -> GroupInfo -> ChatItem 'CTGroup d -> Maybe (ChatItem 'CTGroup d) -> ChatItemDeletion +groupDeletion md g ci ci' = ChatItemDeletion (gItem ci) (gItem <$> ci') + where + gItem = AChatItem SCTGroup md (GroupChat g) + +contactDeletion :: MsgDirectionI d => SMsgDirection d -> Contact -> ChatItem 'CTDirect d -> Maybe (ChatItem 'CTDirect d) -> ChatItemDeletion +contactDeletion md ct ci ci' = ChatItemDeletion (ctItem ci) (ctItem <$> ci') + where + ctItem = AChatItem SCTDirect md (DirectChat ct) createAgentConnectionAsync :: ConnectionModeI c => User -> CommandFunction -> Bool -> SConnectionMode c -> SubscriptionMode -> CM (CommandId, ConnId) createAgentConnectionAsync user cmdFunction enableNtfs cMode subMode = do @@ -6833,22 +7389,30 @@ agentXFTPDeleteSndFilesRemote user sndFiles = do let redirects' = mapMaybe mapRedirectMeta $ concat redirects sndFilesAll = redirects' <> sndFiles sndFilesAll' = filter (not . agentSndFileDeleted . fst) sndFilesAll - sndFilesAll'' <- catMaybes <$> mapM sndFileDescr sndFilesAll' - let sfs = map (\(XFTPSndFile {agentSndFileId = AgentSndFileId aFileId}, sfd, _) -> (aFileId, sfd)) sndFilesAll'' - withAgent' $ \a -> xftpDeleteSndFilesRemote a (aUserId user) sfs - void . withStoreBatch' $ \db -> map (setSndFTAgentDeleted db user . (\(_, _, fId) -> fId)) sndFilesAll'' + -- while file is being prepared and uploaded, it would not have description available; + -- this partitions files into those with and without descriptions - + -- files with description are deleted remotely, files without description are deleted internally + (sfsNoDescr, sfsWithDescr) <- partitionSndDescr sndFilesAll' [] [] + withAgent' $ \a -> xftpDeleteSndFilesInternal a sfsNoDescr + withAgent' $ \a -> xftpDeleteSndFilesRemote a (aUserId user) sfsWithDescr + void . withStoreBatch' $ \db -> map (setSndFTAgentDeleted db user . snd) sndFilesAll' where mapRedirectMeta :: FileTransferMeta -> Maybe (XFTPSndFile, FileTransferId) mapRedirectMeta FileTransferMeta {fileId = fileId, xftpSndFile = Just sndFileRedirect} = Just (sndFileRedirect, fileId) mapRedirectMeta _ = Nothing - sndFileDescr :: (XFTPSndFile, FileTransferId) -> CM' (Maybe (XFTPSndFile, ValidFileDescription 'FSender, FileTransferId)) - sndFileDescr (xsf@XFTPSndFile {privateSndFileDescr}, fileId) = - join <$> forM privateSndFileDescr parseSndDescr - where - parseSndDescr sfdText = + partitionSndDescr :: + [(XFTPSndFile, FileTransferId)] -> + [SndFileId] -> + [(SndFileId, ValidFileDescription 'FSender)] -> + CM' ([SndFileId], [(SndFileId, ValidFileDescription 'FSender)]) + partitionSndDescr [] filesWithoutDescr filesWithDescr = pure (filesWithoutDescr, filesWithDescr) + partitionSndDescr ((XFTPSndFile {agentSndFileId = AgentSndFileId aFileId, privateSndFileDescr}, _) : xsfs) filesWithoutDescr filesWithDescr = + case privateSndFileDescr of + Nothing -> partitionSndDescr xsfs (aFileId : filesWithoutDescr) filesWithDescr + Just sfdText -> tryChatError' (parseFileDescription sfdText) >>= \case - Left _ -> pure Nothing - Right sd -> pure $ Just (xsf, sd, fileId) + Left _ -> partitionSndDescr xsfs (aFileId : filesWithoutDescr) filesWithDescr + Right sfd -> partitionSndDescr xsfs filesWithoutDescr ((aFileId, sfd) : filesWithDescr) userProfileToSend :: User -> Maybe Profile -> Maybe Contact -> Bool -> Profile userProfileToSend user@User {profile = p} incognitoProfile ct inGroup = do @@ -6965,7 +7529,7 @@ createInternalItemsForChats user itemTs_ dirsCIContents = do where updateChat :: DB.Connection -> UTCTime -> ChatDirection c d -> [CIContent d] -> IO () updateChat db createdAt cd contents - | any ciRequiresAttention contents = updateChatTs db user cd createdAt + | any ciRequiresAttention contents || contactChatDeleted cd = updateChatTs db user cd createdAt | otherwise = pure () createACIs :: DB.Connection -> UTCTime -> UTCTime -> ChatDirection c d -> [CIContent d] -> [IO AChatItem] createACIs db itemTs createdAt cd = map $ \content -> do @@ -7062,8 +7626,12 @@ chatCommandP = "/_delete user " *> (APIDeleteUser <$> A.decimal <* " del_smp=" <*> onOffP <*> optional (A.space *> jsonP)), "/delete user " *> (DeleteUser <$> displayName <*> pure True <*> optional (A.space *> pwdP)), ("/user" <|> "/u") $> ShowActiveUser, - "/_start main=" *> (StartChat <$> onOffP), - "/_start" $> StartChat True, + "/_start " *> do + mainApp <- "main=" *> onOffP + enableSndFiles <- " snd_files=" *> onOffP <|> pure mainApp + pure StartChat {mainApp, enableSndFiles}, + "/_start" $> StartChat True True, + "/_check running" $> CheckChatRunning, "/_stop" $> APIStopChat, "/_app activate restore=" *> (APIActivateChat <$> onOffP), "/_app activate" $> APIActivateChat True, @@ -7105,15 +7673,15 @@ chatCommandP = "/_send " *> (APISendMessage <$> chatRefP <*> liveMessageP <*> sendMessageTTLP <*> (" json " *> jsonP <|> " text " *> (ComposedMessage Nothing Nothing <$> mcTextP))), "/_create *" *> (APICreateChatItem <$> A.decimal <*> (" json " *> jsonP <|> " text " *> (ComposedMessage Nothing Nothing <$> mcTextP))), "/_update item " *> (APIUpdateChatItem <$> chatRefP <* A.space <*> A.decimal <*> liveMessageP <* A.space <*> msgContentP), - "/_delete item " *> (APIDeleteChatItem <$> chatRefP <* A.space <*> A.decimal <* A.space <*> ciDeleteMode), - "/_delete member item #" *> (APIDeleteMemberChatItem <$> A.decimal <* A.space <*> A.decimal <* A.space <*> A.decimal), + "/_delete item " *> (APIDeleteChatItem <$> chatRefP <*> _strP <* A.space <*> ciDeleteMode), + "/_delete member item #" *> (APIDeleteMemberChatItem <$> A.decimal <*> _strP), "/_reaction " *> (APIChatItemReaction <$> chatRefP <* A.space <*> A.decimal <* A.space <*> onOffP <* A.space <*> jsonP), - "/_forward " *> (APIForwardChatItem <$> chatRefP <* A.space <*> chatRefP <* A.space <*> A.decimal), + "/_forward " *> (APIForwardChatItem <$> chatRefP <* A.space <*> chatRefP <* A.space <*> A.decimal <*> sendMessageTTLP), "/_read user " *> (APIUserRead <$> A.decimal), "/read user" $> UserRead, "/_read chat " *> (APIChatRead <$> chatRefP <*> optional (A.space *> ((,) <$> ("from=" *> A.decimal) <* A.space <*> ("to=" *> A.decimal)))), "/_unread chat " *> (APIChatUnread <$> chatRefP <* A.space <*> onOffP), - "/_delete " *> (APIDeleteChat <$> chatRefP <*> (A.space *> "notify=" *> onOffP <|> pure True)), + "/_delete " *> (APIDeleteChat <$> chatRefP <*> chatDeleteMode), "/_clear chat " *> (APIClearChat <$> chatRefP), "/_accept" *> (APIAcceptContact <$> incognitoOnOffP <* A.space <*> A.decimal), "/_reject " *> (APIRejectContact <$> A.decimal), @@ -7151,9 +7719,9 @@ chatCommandP = "/xftp test " *> (TestProtoServer . AProtoServerWithAuth SPXFTP <$> strP), "/ntf test " *> (TestProtoServer . AProtoServerWithAuth SPNTF <$> strP), "/_servers " *> (APISetUserProtoServers <$> A.decimal <* A.space <*> srvCfgP), - "/smp " *> (SetUserProtoServers . APSC SPSMP . ProtoServersConfig . map toServerCfg <$> protocolServersP), + "/smp " *> (SetUserProtoServers . APSC SPSMP . ProtoServersConfig . map enabledServerCfg <$> protocolServersP), "/smp default" $> SetUserProtoServers (APSC SPSMP $ ProtoServersConfig []), - "/xftp " *> (SetUserProtoServers . APSC SPXFTP . ProtoServersConfig . map toServerCfg <$> protocolServersP), + "/xftp " *> (SetUserProtoServers . APSC SPXFTP . ProtoServersConfig . map enabledServerCfg <$> protocolServersP), "/xftp default" $> SetUserProtoServers (APSC SPXFTP $ ProtoServersConfig []), "/_servers " *> (APIGetUserProtoServers <$> A.decimal <* A.space <*> strP), "/smp" $> GetUserProtoServers (AProtocolType SPSMP), @@ -7164,8 +7732,9 @@ chatCommandP = "/ttl" $> GetChatItemTTL, "/_network info " *> (APISetNetworkInfo <$> jsonP), "/_network " *> (APISetNetworkConfig <$> jsonP), - ("/network " <|> "/net ") *> (APISetNetworkConfig <$> netCfgP), + ("/network " <|> "/net ") *> (SetNetworkConfig <$> netCfgP), ("/network" <|> "/net") $> APIGetNetworkConfig, + "/reconnect " *> (ReconnectServer <$> A.decimal <* A.space <*> strP), "/reconnect" $> ReconnectAllServers, "/_settings " *> (APISetChatSettings <$> chatRefP <* A.space <*> jsonP), "/_member settings #" *> (APISetMemberSettings <$> A.decimal <* A.space <*> A.decimal <* A.space <*> jsonP), @@ -7175,6 +7744,10 @@ chatCommandP = ("/info #" <|> "/i #") *> (GroupMemberInfo <$> displayName <* A.space <* char_ '@' <*> displayName), ("/info #" <|> "/i #") *> (ShowGroupInfo <$> displayName), ("/info " <|> "/i ") *> char_ '@' *> (ContactInfo <$> displayName), + "/_queue info #" *> (APIGroupMemberQueueInfo <$> A.decimal <* A.space <*> A.decimal), + "/_queue info @" *> (APIContactQueueInfo <$> A.decimal), + ("/queue info #" <|> "/qi #") *> (GroupMemberQueueInfo <$> displayName <* A.space <* char_ '@' <*> displayName), + ("/queue info " <|> "/qi ") *> char_ '@' *> (ContactQueueInfo <$> displayName), "/_switch #" *> (APISwitchGroupMember <$> A.decimal <* A.space <*> A.decimal), "/_switch @" *> (APISwitchContact <$> A.decimal), "/_abort switch #" *> (APIAbortSwitchGroupMember <$> A.decimal <* A.space <*> A.decimal), @@ -7219,7 +7792,7 @@ chatCommandP = ("/remove " <|> "/rm ") *> char_ '#' *> (RemoveMember <$> displayName <* A.space <* char_ '@' <*> displayName), ("/leave " <|> "/l ") *> char_ '#' *> (LeaveGroup <$> displayName), ("/delete #" <|> "/d #") *> (DeleteGroup <$> displayName), - ("/delete " <|> "/d ") *> char_ '@' *> (DeleteContact <$> displayName), + ("/delete " <|> "/d ") *> char_ '@' *> (DeleteContact <$> displayName <*> chatDeleteMode), "/clear *" $> ClearNoteFolder, "/clear #" *> (ClearGroup <$> displayName), "/clear " *> char_ '@' *> (ClearContact <$> displayName), @@ -7251,6 +7824,7 @@ chatCommandP = "/_connect " *> (APIConnect <$> A.decimal <*> incognitoOnOffP <* A.space <*> ((Just <$> strP) <|> A.takeByteString $> Nothing)), "/_connect " *> (APIAddContact <$> A.decimal <*> incognitoOnOffP), "/_set incognito :" *> (APISetConnectionIncognito <$> A.decimal <* A.space <*> onOffP), + "/_set conn user :" *> (APIChangeConnectionUser <$> A.decimal <* A.space <*> A.decimal), ("/connect" <|> "/c") *> (Connect <$> incognitoP <* A.space <*> ((Just <$> strP) <|> A.takeTill isSpace $> Nothing)), ("/connect" <|> "/c") *> (AddContact <$> incognitoP), ForwardMessage <$> chatNameP <* " <- @" <*> displayName <* A.space <*> msgTextP, @@ -7280,8 +7854,8 @@ chatCommandP = ("/fforward " <|> "/ff ") *> (ForwardFile <$> chatNameP' <* A.space <*> A.decimal), ("/image_forward " <|> "/imgf ") *> (ForwardImage <$> chatNameP' <* A.space <*> A.decimal), ("/fdescription " <|> "/fd") *> (SendFileDescription <$> chatNameP' <* A.space <*> filePath), - ("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> optional (" encrypt=" *> onOffP) <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)), - "/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal <*> optional (" encrypt=" *> onOffP)), + ("/freceive " <|> "/fr ") *> (ReceiveFile <$> A.decimal <*> (" approved_relays=" *> onOffP <|> pure False) <*> optional (" encrypt=" *> onOffP) <*> optional (" inline=" *> onOffP) <*> optional (A.space *> filePath)), + "/_set_file_to_receive " *> (SetFileToReceive <$> A.decimal <*> (" approved_relays=" *> onOffP <|> pure False) <*> optional (" encrypt=" *> onOffP)), ("/fcancel " <|> "/fc ") *> (CancelFile <$> A.decimal), ("/fstatus " <|> "/fs ") *> (FileStatus <$> A.decimal), "/_connect contact " *> (APIConnectContactViaAddress <$> A.decimal <*> incognitoOnOffP <* A.space <*> A.decimal), @@ -7344,12 +7918,14 @@ chatCommandP = ("/version" <|> "/v") $> ShowVersion, "/debug locks" $> DebugLocks, "/debug event " *> (DebugEvent <$> jsonP), - "/get stats" $> GetAgentStats, - "/reset stats" $> ResetAgentStats, + "/get subs total " *> (GetAgentSubsTotal <$> A.decimal), + "/get servers summary " *> (GetAgentServersSummary <$> A.decimal), + "/reset servers stats" $> ResetAgentServersStats, "/get subs" $> GetAgentSubs, "/get subs details" $> GetAgentSubsDetails, "/get workers" $> GetAgentWorkers, "/get workers details" $> GetAgentWorkersDetails, + "/get queues" $> GetAgentQueuesInfo, "//" *> (CustomChatCommand <$> A.takeByteString) ] where @@ -7370,6 +7946,15 @@ chatCommandP = mcTextP = MCText . safeDecodeUtf8 <$> A.takeByteString msgContentP = "text " *> mcTextP <|> "json " *> jsonP ciDeleteMode = "broadcast" $> CIDMBroadcast <|> "internal" $> CIDMInternal + chatDeleteMode = + A.choice + [ " full" *> (CDMFull <$> notifyP), + " entity" *> (CDMEntity <$> notifyP), + " messages" $> CDMMessages, + CDMFull <$> notifyP -- backwards compatible + ] + where + notifyP = " notify=" *> onOffP <|> pure True displayName = safeDecodeUtf8 <$> (quoted "'" <|> takeNameTill isSpace) where takeNameTill p = @@ -7398,10 +7983,9 @@ chatCommandP = onOffP = ("on" $> True) <|> ("off" $> False) profileNames = (,) <$> displayName <*> fullNameP newUserP = do - sameServers <- "same_servers=" *> onOffP <* A.space <|> pure False (cName, fullName) <- profileNames let profile = Just Profile {displayName = cName, fullName, image = Nothing, contactLink = Nothing, preferences = Nothing} - pure NewUser {profile, sameServers, pastTimestamp = False} + pure NewUser {profile, pastTimestamp = False} jsonP :: J.FromJSON a => Parser a jsonP = J.eitherDecodeStrict' <$?> A.takeByteString groupProfile = do @@ -7462,10 +8046,13 @@ chatCommandP = <|> ("no" $> TMEDisableKeepTTL) netCfgP = do socksProxy <- "socks=" *> ("off" $> Nothing <|> "on" $> Just defaultSocksProxy <|> Just <$> strP) + socksMode <- " socks-mode=" *> strP <|> pure SMAlways + smpProxyMode_ <- optional $ " smp-proxy=" *> strP + smpProxyFallback_ <- optional $ " smp-proxy-fallback=" *> strP t_ <- optional $ " timeout=" *> A.decimal - logErrors <- " log=" *> onOffP <|> pure False - let tcpTimeout = 1000000 * fromMaybe (maybe 5 (const 10) socksProxy) t_ - pure $ fullNetworkConfig socksProxy tcpTimeout logErrors + logTLSErrors <- " log=" *> onOffP <|> pure False + let tcpTimeout_ = (1000000 *) <$> t_ + pure $ SimpleNetCfg {socksProxy, socksMode, smpProxyMode_, smpProxyFallback_, tcpTimeout_, logTLSErrors} dbKeyP = nonEmptyKey <$?> strP nonEmptyKey k@(DBEncryptionKey s) = if BA.null s then Left "empty key" else Right k dbEncryptionConfig currentKey newKey = DBEncryptionConfig {currentKey, newKey, keepKey = Just False} @@ -7475,7 +8062,6 @@ chatCommandP = (Just <$> (AutoAccept <$> (" incognito=" *> onOffP <|> pure False) <*> optional (A.space *> msgContentP))) (pure Nothing) srvCfgP = strP >>= \case AProtocolType p -> APSC p <$> (A.space *> jsonP) - toServerCfg server = ServerCfg {server, preset = False, tested = Nothing, enabled = True} rcCtrlAddressP = RCCtrlAddress <$> ("addr=" *> strP) <*> (" iface=" *> (jsonP <|> text1P)) text1P = safeDecodeUtf8 <$> A.takeTill (== ' ') char_ = optional . A.char @@ -7484,8 +8070,8 @@ adminContactReq :: ConnReqContact adminContactReq = either error id $ strDecode "simplex:/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D" -simplexContactProfile :: Profile -simplexContactProfile = +simplexTeamContactProfile :: Profile +simplexTeamContactProfile = Profile { displayName = "SimpleX Chat team", fullName = "", @@ -7494,6 +8080,16 @@ simplexContactProfile = preferences = Nothing } +simplexStatusContactProfile :: Profile +simplexStatusContactProfile = + Profile + { displayName = "SimpleX-Status", + fullName = "", + image = Just (ImageData "data:image/jpg;base64,/9j/4AAQSkZJRgABAQAASABIAAD/4QBYRXhpZgAATU0AKgAAAAgAAgESAAMAAAABAAEAAIdpAAQAAAABAAAAJgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAAr6ADAAQAAAABAAAArwAAAAD/7QA4UGhvdG9zaG9wIDMuMAA4QklNBAQAAAAAAAA4QklNBCUAAAAAABDUHYzZjwCyBOmACZjs+EJ+/8AAEQgArwCvAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAQEBAQEBAgEBAgMCAgIDBAMDAwMEBgQEBAQEBgcGBgYGBgYHBwcHBwcHBwgICAgICAkJCQkJCwsLCwsLCwsLC//bAEMBAgICAwMDBQMDBQsIBggLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLC//dAAQAC//aAAwDAQACEQMRAD8A/v4ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/Q/v4ooooAKKKKACiiigAoorE8R+ItF8J6Jc+IvEVwlrZ2iGSWWQ4CgVUISlJRirtmdatTo05VaslGMU223ZJLVtvokbdFfl3of/BRbS734rtpup2Ig8LSsIYrjnzkOcea3bafTqBX6cafqFjq1jFqemSrPbzqHjkQ5VlPIINetm2Q43LXD65T5eZXX+XquqPiuC/Efh/itYh5HiVUdGTjJWaflJJ6uEvsy2fqXKKKK8c+5Ciq17e2mnWkl/fyLDDCpd3c4VVHJJJr8c/2kf8Ago34q8M3mpTfByG3fT7CGSJZrlC3nStwJF5GFU8gd69LA5VicXTrVaMfdpxcpPokk397toj4LjvxKyLhGjRqZxValVkowhFc05O9m0tPdjfV7dN2kfq346+J3w9+GWlPrXxA1m00i1QZL3Uqxj8Mnn8K/Mj4tf8ABYD4DeEJ5dM+Gmn3niq4TIE0YEFtn/ffBI+imv51vHfxA8b/ABR1+bxT8RNUuNXvp3LtJcOWCk84VeigdgBXI18LXzupLSkrL72fzrxH9IXNsTKVPKKMaMOkpe/P8fdXpaXqfqvrf/BYH9p6+1w3+iafo1jZA8WrRPKSPeTcpz9BX1l8J/8Ags34PvxDp/xn8M3OmSnAe709hcQfUoSHA/A1/PtSE4/GuKGZ4mLvz39T4TL/ABe4swlZ1ljpTvvGaUo/dbT/ALdsf2rfCX9pT4HfHGzF18M/EdnqTYBaFXCzJn+9G2GH5V7nX8IOm6hqGkX8eraLcy2d3EcpPbuY5FPsykGv6gf+CWf7QPxB+OPwX1Ky+JF22pX3h69+yJdyf62WJlDrvPdlzjPevdwGae3l7OcbP8D+i/DTxm/1ixkcqx2H5K7TalF3jLlV2rPWLtqtWvM/T2iiivYP3c//0f7+KKKKACiiigAooooAK/Fv/goX8Qvi2fFcXgfWrRtP8NDEls0bZS7YfxORxlT0Xt1r9pK8u+L/AMI/Cfxp8F3HgvxbFujlGYpgB5kMg6Op9R+tfR8K5vQy3MYYnE01KK0843+0vNf8NZn5f4wcFZhxTwziMpy3FOjVeqSdo1Lf8u5u11GXk97Xuro/mBFyDX3t+yL+2Be/CW+h8B+OHafw7cyALIxJa0Ldx6p6jt1FfMvx/wDgR4w/Z+8YN4d8RoZrSbLWd4owk6D+TDuK8KF0K/pLFYHA51geWVp0pq6a/Brs1/wH2P8ALvJsz4h4D4h9tR5qGLoS5ZRls11jJbSjJferSi9mf1uafqFlqtlFqWmyrPBOoeORDlWU8gg069vrPTbSS/v5FhghUu7ucKqjqSa/CH9j79sm++EuoQ/D/wAeSNceHbmRVjlZstZk9x6p6jt2q3+15+2fffFS8n8AfD2V7bw9CxWWZThrwj+Se3evxB+G2Zf2n9TX8Lf2nTl/+S/u/PbU/v2P0nuGv9Vf7cf+9/D9Xv73tLd/+ffXn7afF7pqftbfth3nxUu5vAXgGR7fw/A5WWUHDXZX19E9B361+Z/xKm3eCL9R3UfzFbQul6Cn+I/A3ivxR8LPEXivSbVn07RoVkurg8Iu5gAue7HPSv1HOsrwmVcN4uhRSjBUp6vq3Fq7fVt/5I/gTNeI884x4kjmeYOVWtKSdop2hCPvWjFbQjFNv5ybbuz4Toqa0ge9uoLOIhWnkSNSxwAXIUEnsBnmv0+/aK/4Jg+O/gj8Hoviz4b1n/hJFt40l1G2ig2NDG4yZEIJ3KvfgHHNfxVTw9SpGUoK6W5+xZVw1mWZYfEYrA0XOFBKU2raJ31te72b0T0R+XRIAyegr+gr/glx+yZoHhjwBc/tKfFywiafUY2OmpeIGS3sVGWmIbgF+TkjhR71+YP7DX7Lt9+1H8ZLfR75WTw5pBS61ScDKsoIKwg+snf0Ffqd/wAFSv2o4Phf4Ltv2WvhmVtrjUbRBfvA2Ps1kOFhAHQyAc9ML9a9HL6UacHi6q0W3mz9Q8M8owuV4KvxpnEL0aN40Yv/AJeVXpp5LZPo7v7J+M/7U/jX4e/EL4/+JfFXwrsI9P0Ke5K26RKESTZw0oUcAOeQBX7J/wDBFU5+HPjYf9RWH/0SK/nqACgKOgr+hT/giouPh143b11SH/0SKWVzc8YpPrf8jHwexk8XxzSxVRJSn7WTSVknKMnoui7H7a0UUV9cf3Mf/9L+/iiiigAoorzX4wfGD4afAP4bav8AF74v6xbaD4d0K3e6vb26cJHHGgyevUnoAOSeBTjFyajFXYHpVFf55Xxt/wCDu34nj9vzS/G3wX0Qz/ArQ2ksLnSp1CXurQyMA15uPMTqBmJD2+914/uU/Y//AGxfgH+3P8ENL+P37OutxazoWpoNwHyzW02PmhmjPKSKeCD9RxXqY/JcXg4QqV4WUvw8n2ZnCrGTaTPqGiiivKNDy/4u/CLwd8afBtx4N8ZW4kilBMUoH7yGTs6HsR+tfzjftA/AXxl+z54yfw34jQzWkuXs7xF/dzR/0YdxX9OPiDxBofhPQ7vxN4mu4rDT7CF57m4ncJHFFGMszMcAAAZJNf53n/Bav/g5W1H4ufGjTvg5+xB5F14E8JX4l1HVriIE6xNE2GjhLDKQdRuGC55HHX9L8Os+x2ExP1eKcsO/iX8vmvPy6/ifg3jZ4NYDjDBPFUEqeYU17k/50vsT8n0lvF+V0fq0LhTUgnA4r4y/ZG/bJ+FX7YXw9HjDwBP5N/ahV1LTZeJrSUjoR3U/wsOK+sRdL/n/APXX9G0nCrBTpu6Z/mVmuSYvLcXUwOPpOnWg7SjJWaf9ap7NarQ+pf2dP2evGH7Q3i4aLogNvp1uQ15esMpEnoPVj2Ffrd+1V8GvDnw5/YU8X+APh/Z7IrewEjYGXlZGUs7nqSQM18C/sO/ti6b8F7o/Dnx6qpoN9LvS6RRvglbjL45ZT69vpX7wX1poHjjwxNYzbL3TdUt2jbaQySRSrg4PoQa/nnxXxGaTxLwmIjy4e3uW2lpu33Xbp87v+7Po58I8L4nhfFVMuqKeY1oTp1nJe9S5k0oxWtoPfmXxve1uVfwqKA0YHYiv6Ev+CZ37bVv490eP9mb4zXAn1GKJo9Murg5F3bgYMLk9XUcD+8tflR+1/wDsn+Nv2XfiNdadqFs8vh28md9Mv1GY3iJyEY9nXoQa+UrC/v8ASr+DVdJnktbq2dZYZomKvG6nIZSOhFfztQrVMJW1Xqu5+Z8PZ5mvBWeSc4NSg+WrTeinHqv1jL56ptP+s7xHZ/A//gnR8EfE/jTwra+RHqF5JdxWpbLTXcwwkSnrsGPwXNfyrfEDx54l+J/jXU/iB4wna51LVZ3nmdj3Y8KPQKOAPQV2vxX/AGhvjT8corC3+K2vz6vFpq7beNgERT3YqvBY92NeNVeOxirNRpq0Fsju8RePKWfTo4TLqPscFRXuU9F7z+KTSuvJK7srvqwr+ir/AIIuaVd2/wAH/FesSIRDd6uFjb+8Y41Dfka/BX4YfCzx78ZfGVr4C+G+nyajqV22Aqj5I17u7dFUdya/r+/ZV+Aenfs2fBLSPhbZyC4ntVaW7nAx5tzKd0jfTJwPYV1ZLQk63tbaI+w8AOHcXiM8ebcjVClGS5ujlJWUV3sm27baX3R9FUUUV9Uf2gf/0/7+KKKKACv4If8Ag8QT9vN9W8IsVk/4Z+WJedOL7f7Xyd32/HGNu3yc/LnPev73q84+Lnwj+G/x3+HGr/CT4uaRba74d123e1vbK6QPHJG4weD0I6gjkHkV6WUY9YLFQxDgpJdP8vMipDmi0f4W1frt/wAEhP8Agrt8af8AglD8b38V+Fo21zwPr7xp4i0B3KpcRoeJoTyEnjBO04+boeK+m/8AguZ/wQz+I3/BMD4kyfEn4Ww3fiD4Oa5KzWWolC76XKx4tbphwOuI3PDAc81/PdX7LCeFzHC3VpU5f18mjympU5eZ/t9fsk/tb/Av9tv4G6N+0F+z3rUWs6BrEQYFCPNt5cfPDMnVJEPDKf5V794h8Q6F4T0O78TeJ7uGw06wiae4uZ3EcUUaDLMzHAAA6k1/j9f8EiP+Cunxv/4JTfHAeKPCZfWfAuuyRx+IvD8jkRTxg486Lsk8YJ2n+Loa/V7/AILy/wDBxZd/t2eHl/Zc/Y6mu9I+Gl1DDNrWoSBoLvUpGAY2+OqQoeH/AL5GOlfneI4OxCxio0taT+12Xn59u53xxMeW73ND/g4M/wCDgzVP2yNV1H9jz9j3UZrD4ZWE7waxrEDlH110ONiEYItgQe/7z6V/I6AAMDgCgAKNo6Cv0j/4Jkf8Ex/j/wD8FOvj/Y/Cj4UWE9voFvNGdf18xk2um2pPzEt0MhGdiZyTX6FhsNhctwvLH3YR1bfXzfn/AEjhlKVSR77/AMEMf2Rf2v8A9qr9tPRrb9mNpdL0fSp438UaxKjNYW+nk/PHKOA7uoIjTrnniv7Lfj98CvG37PPjiXwj4uiLxNl7S7UYjuIuzD39R1Ffvt+wn+wd+z5/wTy+A+n/AAF/Z70pbKyt1V728cA3V/c4w0079WYnoOijgV7V8cPgb4G+Pngqfwb41twwYEwXCgebBJ2ZT/MdDXi5N4mTwmYWqRvhXpb7S/vL9V28z8c8YfBXC8XYL61hbQx9Ne7LpNfyT8v5ZfZfkfyXi5r9Lf2Jv24bn4S3UHwz+JkzT+HZ5AsNy5LNZlu3vHn8q+KPj38CPHf7PPjabwn4yt2ELMxtLsD91cRg8Mp6Z9R2rxAXAPANfuePyzL89y/2c7TpTV1JdOzT6Nf8Bn8C5FnGfcEZ79Yw96OJpPlnCS0a6xkusX/k4u9mf2IeK/B/w++Mngt9C8U2ltrWi6lEGCuA6OrDhlPY+hHNfztftw/8E4tN+AGlTfE34ba3HJo0koVdMvGC3CFv4Ym/5aAenBArvf2PP2+9R+CGmv4B+JSy6joEUbtaOp3TQOBkRj1Rjx7V8uftEftH+Nf2i/G7+KPEzmG0hyllZqT5cEef1Y9zX4LT8GMTisynhsY7UI6qot5J7Jefe+i87o/prxI8YuEM/wCF6WM+rc2ZSXKo6qVJrdykvih/Ktebsmnb4DkilicxyqVYdQRzXUaN4R1HVMSzjyIf7zDk/QV6dIlpJIJ5Y1Z16MRk1+qf7DX7Ed58ULmH4p/Fe2kt/D8Dq9paSDabwjncf+mf/oX0rKXg3lOR+0zDPMW6lCL92EVyufZN3vfyjbvdI/AeFsJnHFOPp5TktD97L4pP4YLrJu2iXnq3ok20es/8Erv2f/G/gf8AtD4ozj7Bo2pwiFIpY/3t2VOQ4J5VFzx659q/aKq9paWthax2VlGsUMShERBtVVHAAA6AVYr4LNcdTxWIdSjRjSpqyjGKslFber7t6tn+k3APB1LhjJaOUUqsqjjdylJ/FKTvJpfZV9orbzd2yiiivNPsj//U/v4ooooAKKKKAPO/iz8Jvh18c/h1q/wm+LGk2+ueHtdt3tb2yukDxyxuMEEHoR1B6g81/lm/8Fy/+CFfxG/4Jh/ENvid8J4bzxF8Htdmke1vliaRtHctxbXTAEBecRyHAbGDzX+q54j8R6B4Q0C88U+KbyHT9N0+F7i5ubhxHFFFGMszMcAADqa/zM/+Dhb/AIL06p+3f4rvP2Tf2Xr6S0+Eui3DR397GcHXriM8N7W6EfIP4jz6V9fwfPGLFctD+H9q+3/D9jmxKjy+9ufyq0UAY4or9ZPMP0v/AOCX3/BLf9oT/gqP8d4Phf8ACa0lsvDtjLG3iDxDJGTa6bbse56NKwB8uPOSfav9ZX9hD9hT4Df8E8v2fdK/Z7+AenLbWNkoe8vHUfab+6I+eeZhyWY9B0UcCv8AKC/4JUf8FV/j1/wSu+PCfEf4aSHUvC+rPHH4i0CViIL63U43D+7MgJKN+B4r/Wd/Yy/bM+BH7eHwH0j9oL9n7Vo9S0fU4182LI8+0nx88MydVdTxz16ivzbjZ43nipfwOlu/n59uh6GE5Labn1ZRRRXwB2Hi3x3+BPgj9oHwJceCPGcIIYFre4UfvYJezKf5jvX8vH7QvwB8d/s4eOZfB/jKEtDIS9neKP3VxFngqfX1Hav6gvj58e/An7PHgK48ceN7gLtBW2twf3txL2RR/M9hX8rX7Qn7Rnjz9o3x5L418ZyhUXKWlqh/dW8WeFUevqe5r988G4Zu3Ut/ueu/839z/wBu6fM/jj6UdPhlwo8y/wCFTS3Lb+H/ANPf/bPtf9unlQuAec077SPWueFznrTxc1+/eyP4udE/XX9g79h24+K8tv8AF74qQvD4fgkDWdo64N4V53H/AKZg/wDfX0r+ge0tLWwtY7KyjWKGJQiIgwqqOAAOwFfzc/sIft2XnwO1KH4ZfEeVp/Ct5L8k7Es9k7YHH/TMnkjt1r+kDTNT07WtOg1fSJ0ubW5QSRSxncjowyCCOoNfyr4q0s3jmreYfwtfZW+Hl/8Akv5r6/Kx/or9HSXDX+rqhkqtidPb81vac/d/3P5Lab/auXqKKK/Lz+gwooooA//V/v4ooooAKxfEniTQPB2gXnirxVew6dpunQvcXV1cOI4oYoxlndjgAADJJrar/PV/4Ozf+CiX7Xlr8Yrf9hCx0u98GfDaS0iv5L1GZT4iZs5HmKceTERgx9d3LcYr08py2eOxMaEXbu/L9SKk1CN2fIX/AAcD/wDBfrXv27vFF1+yx+ylqFzpnwl0id476+icxSa/MhwGOMEWykHYv8fU9hX8qoAAwOAKUAAYFfqj/wAEnf8AglH8cv8Agqp8ek+Hvw/R9M8I6NJFJ4k19lzHZW7k/ImeGmcAhF/E8V+xUKGFyzC2Xuwju/1fds8tuVSXmM/4JQ/8Epfjr/wVU+Pcfw5+HiPpXhPSXjl8ReIZEJhsoGP3E7PO4B2J+J4r7o/4Li/8EC/H3/BL/UYPjH8Hp7vxV8JNQMcL3sy7rnTLkgDbcFRjZI3KPwATg9q/0rP2MP2MPgL+wZ8BdI/Z5/Z60hNM0bS4x5kpANxeTn7887gAvI55JPToOK9y+J/ww8AfGfwBqvwu+KOlW+t6Brdu9re2V0gkilicYIIP6HqDXwVbjSu8YqlNfulpy9139e3Y7VhY8tnuf4VdfqD/AMErP+Cpvx1/4Jb/ALQNn8S/h7cS6j4VvpUj8QeH2kIt723zgsB0WVRyjetffn/BeH/ghJ4x/wCCZvjlvjP8EYbvXPg5rk7GKcqZJdGmc5FvOwH+rOcRyH0wea/nCr9ApVcNmOGuvehL+vk0cLUqcvM/24v2Mf20PgH+3l8CdK/aA/Z61iPVNI1FF86LI+0Wc+PnhnTqjqeOevUcV3nx/wD2gfh/+zp4CuPHHjq5CBQVtrZT+9uJeyIP5noBX+Ud/wAEL/25f2t/2NP2u7A/s7xPrPhzW5Yk8T6LOzCyls1PzTE9I5UXJRupPHIr+p39o79pXx/+0v8AEGbxv42l2RrlLO0QnyreLPCqPX1PUmvM4b8KauYZg5VJWwkdW/tP+6vPu+i8z8r8VvF3D8L4P6vhbTx017sekF/PL/21fafkjV/aF/aN8e/tHePZ/GvjOc+XuK2lopPlW8WeFUevqe9eFfasDmsL7UB1r9kv+Cen/BPuX4mPa/Gv41Wrw6HE4k0/T5FwbsjkO4PPl56D+L6V/QWbZjlnDmW+1q2hSgrRit2+kYrq/wDh2fw9kXDmdcZ526NK9SvUfNOctorrKT6JdF6JIh/Yq/4JyXXxq8MSfEn4wtPpukXkLLp1vH8s0hYcTHPRR1Ud6+KP2nP2bvHX7MXj+Twl4pUz2U+Xsb5QRHcRZ/Rh/Etf2D2trbWNtHZ2caxRRKEREGFVRwAAOgFeSfHL4G+Af2gvAVz4A8f2wmt5huimUDzYJB0dD2I/Wv5/yrxgx0c3niMcr4abtyL7C6OPdrr/ADeWlv604g+jdlFTh6ngsrfLjaauqj/5eS6xn2i/s2+Hz1v/ABi+d3r9O/2DP28r/wCBGpRfDT4lSvdeFL2UBJmYs9izcZX1j7kduor48/ah/Zr8bfsu/EWTwZ4pHn2c4MtheqMJcQ5IB9mHRhXzd9oAFf0Djsuy3iHLeSdqlGorpr8Gn0a/4DW6P5DyrMc74Mzz2tG9LE0XaUXs11jJdYv/ACaezP7pdK1bTNd02DWdGnS6tLlBJFLEwZHRuQQR1FaFfix/wSG1n47X3hPVLHXUL+BoT/oEtxneLjPzLD6pjr2B6d6/aev424nyP+yMyrZf7RT5Huvv17NdV0Z/pTwPxP8A6w5Lh82dGVJ1FrGXdaNp9YveL6oKKKK8A+sP/9b+/iiiigAr4E/4KI/8E4f2b/8AgpZ8DLr4M/H7SklljV5NJ1aJQLzTblhxLC/Uc43L0YcGvvuitKNadKaqU3aS2Ymk1Zn+Vt8Nf+DZH9vDxJ/wUEn/AGQfGti+m+DdMkF5eeNlTNjLpRb5Xgz964cfL5XVWyTx1/0lv2L/ANif9nv9gn4H6b8Bv2dNDh0jSrFF8+YKDcXs4GGmuJOskjHPJ6dBxX1lgZz3pa9bNc+xWPjGFV2iui6vu/60M6dKMNgooorxTU4T4m/DHwB8ZfAeqfDH4paRba7oGtQPbXtjeRiWGaJxghlII/wr/M//AOCw/wDwbq/En9kb9o7Ttc/ZhQ6h8KvGl4VgknkUyaJIxy0UmTueMDmNgCexr/SN/aA/aA+Hf7N3w6u/iL8RbtYYIFIggBHm3Ev8Mca9yfyA5NfyB/tTftZfEX9qv4gSeL/GEv2exgLJYWEZPlW8WeOO7H+Ju9fsXhRwnmOZYl4hNwwi+Jv7T/lj5930Xnofj3iv4nYThrCPD0bTxs17kekV/PPy7L7T8rn58fs1fs1/Df8AZg8Dp4U8CwB7qYK19fuAZrmQDkseyjsvQV9GfaWrAWcjvUnnt6mv62w+Cp0KapUo2itkfwFmOLxWPxNTGYyo51Zu8pN6t/1stktEftx/wTa/YHsfi6sHx2+L8aT6BFJnT7DcGFy6dWlAzhQf4T171/SBaWltY20dlZRrFDEoREQYVVHAAA6AV/Hv+xJ+3N4y/ZO8Wi0ui+oeE9QkX7dYk5KdjLFzw49Ohr+tj4c/Efwb8WPB1l498A30eoaZqEYkiljOevVWHZh0IPIr+TPGXLs6p5p9Zxz5sO9KbXwxX8rXSXd/a3Wmi/t76P8AmHD08l+qZZDkxUdaydueT/mT0vDsl8Oz1d33FFFFfjR/QB4x8dPgN8O/2hvA1x4F+Idms8MgJhmAxLbydnjbqCP1r8RPg3/wSV8Z/wDC9r7T/izMreDNIlEkM8TYfUVPKpgcoAPv+/Ar+iKivrsh43zbKMLWweCq2hUXXXlf80eza0/HdJnwPFHhpkHEGOw+YZlQ5qlJ7rTnXSM/5op6/hs2jD8NeGdA8HaHbeGvC9nFYWFmgjhghUIiKOwArcoor5Oc5Tk5Sd292fd06cacVCCtFaJLZLsgoooqSz//1/7+KKKKACiiigAooooAK8J/aK/aG+H37M/wzvPiX8QrgRwwDbb26kebczH7saDuSep7DmvdW3bTt69s1/Hj/wAFS9c/acu/2hbiw+Psf2fTYWf+w47bd9ha2zw0ZPWQj7+eQfav0Dw44PpcRZssLXqqFOK5pK9pSS6RXfu+i1PzvxN4zrcN5PLF4ei51JPli7XjFv7U327Lq9Dwr9qv9rn4lftZ+Pv+Ev8AG8i29na7ksNPiJ8m2iJ7Ak5Y/wATHrXy/wDacDJNYfn45PFftR/wTX/4Ju6j8aryz+OXxttpLXwtbSrJY2Mi7W1Bl53MD0hB/wC+vpX9jZpmGU8LZT7WolTo01aMVu30jFdW/wDNvqz+HcryTOeLs4dODdSvUd5Tlsl1lJ9Eui9Elsix/wAE8/8Agmpc/Hq3HxZ+OcFxY+F8f6Daj93Jen++eMiMdum76V88ft4fsM+LP2RvGH9p6MJtS8G6gxNnfMMmFj/yxmIAAYfwnuPev7DbGxs9Ms4tP0+JYIIFCRxoNqqq8AADoBXL+P8AwB4R+KHhG+8C+OrGPUNM1CMxTQyjIIPcehHUEdDX8x4PxqzWOdvH11fDS0dJbKPRp/zrdvrtta39V47wCyWeQRy7D6YqOqrPeUuqkv5Hsl9ndXd7/wACwuGHevvT9iL9u7x1+yP4n+wMDqXhPUJVN/YMTlOxlh/uuB+BqH9vD9hXxl+yD4v/ALS03zNT8HajIfsV8VyYSf8AljNjgMOx/iHvX59C6bHav6fjDKeJsqurVcPVX9ecZRfzTP5LdLOeE850vRxNJ/15SjJfJo/v3+GnxJ8HfF3wRp/xC8BXiX2l6lEJYZEPr1Vh2YdCDyDXd1/PD/wRa8KftJW8moeKfPNp8N7kMBBdKT9ouR/Hbgn5QP4m6Gv6Hq/iHjXh6lkmb1svoVlUjF6Nbq/2ZdOZdbfhsf6AcC8SVs9yahmWIoOlOS1T2dvtR68r3V/x3ZRRRXyh9eFFFFABRRRQB//Q/v4ooooAKKKKACiiigAr5u/aj/Zg+HX7VvwyuPh14+i2N/rLO8jA861mHR0Pp2YdCOK+kaK6sDjq+DxEMVhZuFSDumt00cmOwOHxuHnhcVBTpzVpJ7NM/nF/ZW/4I2eINL+MV9rH7Rk0Vz4d0G5H2GCA8anjlXfuiDjcvJJ46V/RfY2FlpdlFpumxJBbwII444wFVEUYAAHAAFW6K9/injHM+Ia8a+Y1L8qsorSK7tLu3q3+iSPn+E+C8q4dw86GW07czvJvWT7Jvstkv1bYUUUV8sfVnEfEb4c+Dvix4Mv/AAB49sY9Q0vUYjFNDIMjB7j0YdQRyDX4HeH/APgiNJB+0LKNe1vzvhzARcxBeLyUEn/R27ADu46jtmv6KKK+r4d42zjI6Vajl1ZxjUVmt7P+aN9pW0uv0R8lxJwNk2e1aFfMqCnKk7p7XX8srbxvrZ/qzn/CnhXw/wCCPDll4R8K2sdlp2nQrBbwRDCoiDAAFdBRRXy05ynJzm7t6tvqfVwhGEVCCsloktkgoooqSgooooAKKKKAP//R/v4ooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/Z"), + contactLink = Just (either error id $ strDecode "simplex:/contact/#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2FShQuD-rPokbDvkyotKx5NwM8P3oUXHxA%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEA6fSx1k9zrOmF0BJpCaTarZvnZpMTAVQhd3RkDQ35KT0%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion"), + preferences = Nothing + } + timeItToView :: String -> CM' a -> CM' a timeItToView s action = do t1 <- liftIO getCurrentTime diff --git a/src/Simplex/Chat/AppSettings.hs b/src/Simplex/Chat/AppSettings.hs index 6996cc1d87..6d23a19ba1 100644 --- a/src/Simplex/Chat/AppSettings.hs +++ b/src/Simplex/Chat/AppSettings.hs @@ -9,6 +9,7 @@ import Control.Applicative ((<|>)) import Data.Aeson (FromJSON (..), (.:?)) import qualified Data.Aeson as J import qualified Data.Aeson.TH as JQ +import Data.Map.Strict (Map) import Data.Maybe (fromMaybe) import Data.Text (Text) import Simplex.Chat.Types.UITheme @@ -28,11 +29,13 @@ data AppSettings = AppSettings { appPlatform :: Maybe AppPlatform, networkConfig :: Maybe NetworkConfig, privacyEncryptLocalFiles :: Maybe Bool, + privacyAskToApproveRelays :: Maybe Bool, privacyAcceptImages :: Maybe Bool, privacyLinkPreviews :: Maybe Bool, privacyShowChatPreviews :: Maybe Bool, privacySaveLastDraft :: Maybe Bool, privacyProtectScreen :: Maybe Bool, + privacyMediaBlurRadius :: Maybe Int, notificationMode :: Maybe NotificationMode, notificationPreviewMode :: Maybe NotificationPreviewMode, webrtcPolicyRelay :: Maybe Bool, @@ -48,7 +51,9 @@ data AppSettings = AppSettings uiProfileImageCornerRadius :: Maybe Double, uiColorScheme :: Maybe UIColorScheme, uiDarkColorScheme :: Maybe DarkColorScheme, - uiThemes :: Maybe UIThemes + uiCurrentThemeIds :: Maybe (Map ThemeColorScheme Text), + uiThemes :: Maybe [UITheme], + oneHandUI :: Maybe Bool } deriving (Show) @@ -58,11 +63,13 @@ defaultAppSettings = { appPlatform = Nothing, networkConfig = Just defaultNetworkConfig, privacyEncryptLocalFiles = Just True, + privacyAskToApproveRelays = Just True, privacyAcceptImages = Just True, privacyLinkPreviews = Just True, privacyShowChatPreviews = Just True, privacySaveLastDraft = Just True, privacyProtectScreen = Just False, + privacyMediaBlurRadius = Just 0, notificationMode = Just NMInstant, notificationPreviewMode = Just NPMMessage, webrtcPolicyRelay = Just True, @@ -78,7 +85,9 @@ defaultAppSettings = uiProfileImageCornerRadius = Just 22.5, uiColorScheme = Just UCSSystem, uiDarkColorScheme = Just DCSSimplex, - uiThemes = Nothing + uiCurrentThemeIds = Nothing, + uiThemes = Nothing, + oneHandUI = Just True } defaultParseAppSettings :: AppSettings @@ -87,11 +96,13 @@ defaultParseAppSettings = { appPlatform = Nothing, networkConfig = Nothing, privacyEncryptLocalFiles = Nothing, + privacyAskToApproveRelays = Nothing, privacyAcceptImages = Nothing, privacyLinkPreviews = Nothing, privacyShowChatPreviews = Nothing, privacySaveLastDraft = Nothing, privacyProtectScreen = Nothing, + privacyMediaBlurRadius = Nothing, notificationMode = Nothing, notificationPreviewMode = Nothing, webrtcPolicyRelay = Nothing, @@ -107,7 +118,9 @@ defaultParseAppSettings = uiProfileImageCornerRadius = Nothing, uiColorScheme = Nothing, uiDarkColorScheme = Nothing, - uiThemes = Nothing + uiCurrentThemeIds = Nothing, + uiThemes = Nothing, + oneHandUI = Nothing } combineAppSettings :: AppSettings -> AppSettings -> AppSettings @@ -116,11 +129,13 @@ combineAppSettings platformDefaults storedSettings = { appPlatform = p appPlatform, networkConfig = p networkConfig, privacyEncryptLocalFiles = p privacyEncryptLocalFiles, + privacyAskToApproveRelays = p privacyAskToApproveRelays, privacyAcceptImages = p privacyAcceptImages, privacyLinkPreviews = p privacyLinkPreviews, privacyShowChatPreviews = p privacyShowChatPreviews, privacySaveLastDraft = p privacySaveLastDraft, privacyProtectScreen = p privacyProtectScreen, + privacyMediaBlurRadius = p privacyMediaBlurRadius, notificationMode = p notificationMode, notificationPreviewMode = p notificationPreviewMode, webrtcPolicyRelay = p webrtcPolicyRelay, @@ -136,7 +151,9 @@ combineAppSettings platformDefaults storedSettings = uiProfileImageCornerRadius = p uiProfileImageCornerRadius, uiColorScheme = p uiColorScheme, uiDarkColorScheme = p uiDarkColorScheme, - uiThemes = p uiThemes + uiCurrentThemeIds = p uiCurrentThemeIds, + uiThemes = p uiThemes, + oneHandUI = p oneHandUI } where p :: (AppSettings -> Maybe a) -> Maybe a @@ -157,11 +174,13 @@ instance FromJSON AppSettings where appPlatform <- p "appPlatform" networkConfig <- p "networkConfig" privacyEncryptLocalFiles <- p "privacyEncryptLocalFiles" + privacyAskToApproveRelays <- p "privacyAskToApproveRelays" privacyAcceptImages <- p "privacyAcceptImages" privacyLinkPreviews <- p "privacyLinkPreviews" privacyShowChatPreviews <- p "privacyShowChatPreviews" privacySaveLastDraft <- p "privacySaveLastDraft" privacyProtectScreen <- p "privacyProtectScreen" + privacyMediaBlurRadius <- p "privacyMediaBlurRadius" notificationMode <- p "notificationMode" notificationPreviewMode <- p "notificationPreviewMode" webrtcPolicyRelay <- p "webrtcPolicyRelay" @@ -177,17 +196,21 @@ instance FromJSON AppSettings where uiProfileImageCornerRadius <- p "uiProfileImageCornerRadius" uiColorScheme <- p "uiColorScheme" uiDarkColorScheme <- p "uiDarkColorScheme" + uiCurrentThemeIds <- p "uiCurrentThemeIds" uiThemes <- p "uiThemes" + oneHandUI <- p "oneHandUI" pure AppSettings { appPlatform, networkConfig, privacyEncryptLocalFiles, + privacyAskToApproveRelays, privacyAcceptImages, privacyLinkPreviews, privacyShowChatPreviews, privacySaveLastDraft, privacyProtectScreen, + privacyMediaBlurRadius, notificationMode, notificationPreviewMode, webrtcPolicyRelay, @@ -203,7 +226,9 @@ instance FromJSON AppSettings where uiProfileImageCornerRadius, uiColorScheme, uiDarkColorScheme, - uiThemes + uiCurrentThemeIds, + uiThemes, + oneHandUI } where p key = v .:? key <|> pure Nothing diff --git a/src/Simplex/Chat/Archive.hs b/src/Simplex/Chat/Archive.hs index 01897de791..218d1e1f2e 100644 --- a/src/Simplex/Chat/Archive.hs +++ b/src/Simplex/Chat/Archive.hs @@ -52,18 +52,22 @@ archiveAssetsFolder = "simplex_v1_assets" wallpapersFolder :: String wallpapersFolder = "wallpapers" -exportArchive :: ArchiveConfig -> CM' () +exportArchive :: ArchiveConfig -> CM' [ArchiveError] exportArchive cfg@ArchiveConfig {archivePath, disableCompression} = withTempDir cfg "simplex-chat." $ \dir -> do StorageFiles {chatStore, agentStore, filesPath, assetsPath} <- storageFiles copyFile (dbFilePath chatStore) $ dir archiveChatDbFile copyFile (dbFilePath agentStore) $ dir archiveAgentDbFile - forM_ filesPath $ \fp -> - copyDirectoryFiles fp $ dir archiveFilesFolder + errs <- + forM filesPath $ \fp -> + copyValidDirectoryFiles entrySelectorError fp $ dir archiveFilesFolder forM_ assetsPath $ \fp -> copyDirectoryFiles (fp wallpapersFolder) $ dir archiveAssetsFolder wallpapersFolder let method = if disableCompression == Just True then Z.Store else Z.Deflate Z.createArchive archivePath $ Z.packDirRecur method Z.mkEntrySelector dir + pure $ fromMaybe [] errs + where + entrySelectorError f = (Z.mkEntrySelector f $> Nothing) `E.catchAny` (pure . Just . show) importArchive :: ArchiveConfig -> CM' [ArchiveError] importArchive cfg@ArchiveConfig {archivePath} = @@ -85,7 +89,7 @@ importArchive cfg@ArchiveConfig {archivePath} = (doesDirectoryExist fromDir) (copyDirectoryFiles fromDir fp) (pure []) - `E.catch` \(e :: E.SomeException) -> pure [AEImport . ChatError . CEException $ show e] + `E.catch` \(e :: E.SomeException) -> pure [AEImport $ show e] _ -> pure [] withTempDir :: ArchiveConfig -> (String -> (FilePath -> CM' a) -> CM' a) @@ -94,14 +98,22 @@ withTempDir cfg = case parentTempDirectory (cfg :: ArchiveConfig) of _ -> withSystemTempDirectory copyDirectoryFiles :: FilePath -> FilePath -> CM' [ArchiveError] -copyDirectoryFiles fromDir toDir = do +copyDirectoryFiles fromDir toDir = copyValidDirectoryFiles (\_ -> pure Nothing) fromDir toDir + +copyValidDirectoryFiles :: (FilePath -> IO (Maybe String)) -> FilePath -> FilePath -> CM' [ArchiveError] +copyValidDirectoryFiles isFileError fromDir toDir = do createDirectoryIfMissing True toDir fs <- listDirectory fromDir foldM copyFileCatchError [] fs where copyFileCatchError fileErrs f = - (copyDirectoryFile f $> fileErrs) - `E.catch` \(e :: E.SomeException) -> pure (AEImportFile f (ChatError . CEException $ show e) : fileErrs) + liftIO (isFileError f) >>= \case + Nothing -> + (copyDirectoryFile f $> fileErrs) + `E.catch` \(e :: E.SomeException) -> addErr $ show e + Just e -> addErr e + where + addErr e = pure $ AEFileError f e : fileErrs copyDirectoryFile f = do let fn = takeFileName f f' = fromDir fn diff --git a/src/Simplex/Chat/Bot.hs b/src/Simplex/Chat/Bot.hs index c5c5ff7eed..f3de92e1f2 100644 --- a/src/Simplex/Chat/Bot.hs +++ b/src/Simplex/Chat/Bot.hs @@ -3,6 +3,7 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedLists #-} module Simplex.Chat.Bot where @@ -73,9 +74,9 @@ sendComposedMessage' cc ctId quotedItemId msgContent = do deleteMessage :: ChatController -> Contact -> ChatItemId -> IO () deleteMessage cc ct chatItemId = do - let cmd = APIDeleteChatItem (contactRef ct) chatItemId CIDMInternal + let cmd = APIDeleteChatItem (contactRef ct) [chatItemId] CIDMInternal sendChatCmd cc cmd >>= \case - CRChatItemDeleted {} -> printLog cc CLLInfo $ "deleted message from " <> contactInfo ct + CRChatItemsDeleted {} -> printLog cc CLLInfo $ "deleted message(s) from " <> contactInfo ct r -> putStrLn $ "unexpected delete message response: " <> show r contactRef :: Contact -> ChatRef diff --git a/src/Simplex/Chat/Call.hs b/src/Simplex/Chat/Call.hs index 115cd839e4..9968d170aa 100644 --- a/src/Simplex/Chat/Call.hs +++ b/src/Simplex/Chat/Call.hs @@ -21,10 +21,10 @@ import Data.Time.Clock (UTCTime) import Database.SQLite.Simple.FromField (FromField (..)) import Database.SQLite.Simple.ToField (ToField (..)) import Simplex.Chat.Types (Contact, ContactId, User) -import Simplex.Chat.Types.Util (decodeJSON, encodeJSON) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, fstToLower, singleFieldJSON) +import Simplex.Messaging.Util (decodeJSON, encodeJSON) data Call = Call { contactId :: ContactId, diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 26ace9c772..c4f056c778 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -13,6 +13,7 @@ {-# LANGUAGE StrictData #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -fno-warn-implicit-lift #-} module Simplex.Chat.Controller where @@ -59,6 +60,7 @@ import Simplex.Chat.Messages.CIContent import Simplex.Chat.Protocol import Simplex.Chat.Remote.AppVersion import Simplex.Chat.Remote.Types +import Simplex.Chat.Stats (PresentedServersSummary) import Simplex.Chat.Store (AutoAccept, ChatLockEntity, StoreError (..), UserContactLink, UserMsgReceiptSettings) import Simplex.Chat.Types import Simplex.Chat.Types.Preferences @@ -67,13 +69,14 @@ import Simplex.Chat.Types.UITheme import Simplex.Chat.Util (liftIOEither) import Simplex.FileTransfer.Description (FileDescriptionURI) import Simplex.Messaging.Agent (AgentClient, SubscriptionsInfo) -import Simplex.Messaging.Agent.Client (AgentLocks, AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, UserNetworkInfo) -import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig) +import Simplex.Messaging.Agent.Client (AgentLocks, AgentQueuesInfo (..), AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, SMPServerSubs, ServerQueueInfo, UserNetworkInfo) +import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig, ServerCfg) import Simplex.Messaging.Agent.Lock import Simplex.Messaging.Agent.Protocol -import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration, withTransaction) +import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration, withTransaction, withTransactionPriority) import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import Simplex.Messaging.Client (SMPProxyFallback (..), SMPProxyMode (..), SocksMode (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..)) import qualified Simplex.Messaging.Crypto.File as CF @@ -81,10 +84,10 @@ import Simplex.Messaging.Crypto.Ratchet (PQEncryption) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), NtfTknStatus) import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, parseString, sumTypeJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, NtfServer, ProtoServerWithAuth, ProtocolTypeI, QueueId, SMPMsgMeta (..), SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServerWithAuth, userProtocol) +import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, NtfServer, ProtocolType (..), ProtocolTypeI, QueueId, SMPMsgMeta (..), SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServer, userProtocol) import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.Transport (TLS, simplexMQVersion) -import Simplex.Messaging.Transport.Client (TransportHost) +import Simplex.Messaging.Transport.Client (SocksProxy, TransportHost) import Simplex.Messaging.Util (allFinally, catchAllErrors, catchAllErrors', tryAllErrors, tryAllErrors', (<$$>)) import Simplex.RemoteControl.Client import Simplex.RemoteControl.Invitation (RCSignedInvitation, RCVerifiedInvitation) @@ -170,9 +173,11 @@ defaultChatHooks = } data DefaultAgentServers = DefaultAgentServers - { smp :: NonEmpty SMPServerWithAuth, + { smp :: NonEmpty (ServerCfg 'PSMP), + useSMP :: Int, ntf :: [NtfServer], - xftp :: NonEmpty XFTPServerWithAuth, + xftp :: NonEmpty (ServerCfg 'PXFTP), + useXFTP :: Int, netCfg :: NetworkConfig } @@ -205,6 +210,7 @@ data ChatController = ChatController chatStore :: SQLiteStore, chatStoreChanged :: TVar Bool, -- if True, chat should be fully restarted random :: TVar ChaChaDRG, + eventSeq :: TVar Int, inputQ :: TBQueue String, outputQ :: TBQueue (Maybe CorrId, Maybe RemoteHostId, ChatResponse), connNetworkStatuses :: TMap AgentConnId NetworkStatus, @@ -259,7 +265,8 @@ data ChatCommand | UnmuteUser | APIDeleteUser UserId Bool (Maybe UserPwd) | DeleteUser UserName Bool (Maybe UserPwd) - | StartChat {mainApp :: Bool} + | StartChat {mainApp :: Bool, enableSndFiles :: Bool} -- enableSndFiles has no effect when mainApp is True + | CheckChatRunning | APIStopChat | APIActivateChat {restoreChat :: Bool} | APISuspendChat {suspendTimeout :: Int} @@ -288,15 +295,15 @@ data ChatCommand | APISendMessage {chatRef :: ChatRef, liveMessage :: Bool, ttl :: Maybe Int, composedMessage :: ComposedMessage} | APICreateChatItem {noteFolderId :: NoteFolderId, composedMessage :: ComposedMessage} | APIUpdateChatItem {chatRef :: ChatRef, chatItemId :: ChatItemId, liveMessage :: Bool, msgContent :: MsgContent} - | APIDeleteChatItem ChatRef ChatItemId CIDeleteMode - | APIDeleteMemberChatItem GroupId GroupMemberId ChatItemId + | APIDeleteChatItem ChatRef (NonEmpty ChatItemId) CIDeleteMode + | APIDeleteMemberChatItem GroupId (NonEmpty ChatItemId) | APIChatItemReaction {chatRef :: ChatRef, chatItemId :: ChatItemId, add :: Bool, reaction :: MsgReaction} - | APIForwardChatItem {toChatRef :: ChatRef, fromChatRef :: ChatRef, chatItemId :: ChatItemId} + | APIForwardChatItem {toChatRef :: ChatRef, fromChatRef :: ChatRef, chatItemId :: ChatItemId, ttl :: Maybe Int} | APIUserRead UserId | UserRead | APIChatRead ChatRef (Maybe (ChatItemId, ChatItemId)) | APIChatUnread ChatRef Bool - | APIDeleteChat ChatRef Bool -- `notify` flag is only applied to direct chats + | APIDeleteChat ChatRef ChatDeleteMode -- currently delete mode settings are only applied to direct chats | APIClearChat ChatRef | APIAcceptContact IncognitoEnabled Int64 | APIRejectContact Int64 @@ -348,13 +355,17 @@ data ChatCommand | GetChatItemTTL | APISetNetworkConfig NetworkConfig | APIGetNetworkConfig + | SetNetworkConfig SimpleNetCfg | APISetNetworkInfo UserNetworkInfo | ReconnectAllServers + | ReconnectServer UserId SMPServer | APISetChatSettings ChatRef ChatSettings | APISetMemberSettings GroupId GroupMemberId GroupMemberSettings | APIContactInfo ContactId | APIGroupInfo GroupId | APIGroupMemberInfo GroupId GroupMemberId + | APIContactQueueInfo ContactId + | APIGroupMemberQueueInfo GroupId GroupMemberId | APISwitchContact ContactId | APISwitchGroupMember GroupId GroupMemberId | APIAbortSwitchContact ContactId @@ -373,6 +384,8 @@ data ChatCommand | ContactInfo ContactName | ShowGroupInfo GroupName | GroupMemberInfo GroupName ContactName + | ContactQueueInfo ContactName + | GroupMemberQueueInfo GroupName ContactName | SwitchContact ContactName | SwitchGroupMember GroupName ContactName | AbortSwitchContact ContactName @@ -390,12 +403,13 @@ data ChatCommand | APIAddContact UserId IncognitoEnabled | AddContact IncognitoEnabled | APISetConnectionIncognito Int64 IncognitoEnabled + | APIChangeConnectionUser Int64 UserId -- new user id to switch connection to | APIConnectPlan UserId AConnectionRequestUri | APIConnect UserId IncognitoEnabled (Maybe AConnectionRequestUri) | Connect IncognitoEnabled (Maybe AConnectionRequestUri) | APIConnectContactViaAddress UserId IncognitoEnabled ContactId | ConnectSimplex IncognitoEnabled -- UserId (not used in UI) - | DeleteContact ContactName + | DeleteContact ContactName ChatDeleteMode | ClearContact ContactName | APIListContacts UserId | ListContacts @@ -458,8 +472,8 @@ data ChatCommand | ForwardFile ChatName FileTransferId | ForwardImage ChatName FileTransferId | SendFileDescription ChatName FilePath - | ReceiveFile {fileId :: FileTransferId, storeEncrypted :: Maybe Bool, fileInline :: Maybe Bool, filePath :: Maybe FilePath} - | SetFileToReceive {fileId :: FileTransferId, storeEncrypted :: Maybe Bool} + | ReceiveFile {fileId :: FileTransferId, userApprovedRelays :: Bool, storeEncrypted :: Maybe Bool, fileInline :: Maybe Bool, filePath :: Maybe FilePath} + | SetFileToReceive {fileId :: FileTransferId, userApprovedRelays :: Bool, storeEncrypted :: Maybe Bool} | CancelFile FileTransferId | FileStatus FileTransferId | ShowProfile -- UserId (not used in UI) @@ -495,12 +509,14 @@ data ChatCommand | ShowVersion | DebugLocks | DebugEvent ChatResponse - | GetAgentStats - | ResetAgentStats + | GetAgentSubsTotal UserId + | GetAgentServersSummary UserId + | ResetAgentServersStats | GetAgentSubs | GetAgentSubsDetails | GetAgentWorkers | GetAgentWorkersDetails + | GetAgentQueuesInfo | -- The parser will return this command for strings that start from "//". -- This command should be processed in preCmdHook CustomChatCommand ByteString @@ -565,6 +581,7 @@ data ChatResponse | CRContactInfo {user :: User, contact :: Contact, connectionStats_ :: Maybe ConnectionStats, customUserProfile :: Maybe Profile} | CRGroupInfo {user :: User, groupInfo :: GroupInfo, groupSummary :: GroupSummary} | CRGroupMemberInfo {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats_ :: Maybe ConnectionStats} + | CRQueueInfo {user :: User, rcvMsgInfo :: Maybe RcvMsgInfo, queueInfo :: ServerQueueInfo} | CRContactSwitchStarted {user :: User, contact :: Contact, connectionStats :: ConnectionStats} | CRGroupMemberSwitchStarted {user :: User, groupInfo :: GroupInfo, member :: GroupMember, connectionStats :: ConnectionStats} | CRContactSwitchAborted {user :: User, contact :: Contact, connectionStats :: ConnectionStats} @@ -585,7 +602,7 @@ data ChatResponse | CRChatItemUpdated {user :: User, chatItem :: AChatItem} | CRChatItemNotChanged {user :: User, chatItem :: AChatItem} | CRChatItemReaction {user :: User, added :: Bool, reaction :: ACIReaction} - | CRChatItemDeleted {user :: User, deletedChatItem :: AChatItem, toChatItem :: Maybe AChatItem, byUser :: Bool, timed :: Bool} + | CRChatItemsDeleted {user :: User, chatItemDeletions :: [ChatItemDeletion], byUser :: Bool, timed :: Bool} | CRChatItemDeletedNotFound {user :: User, contact :: Contact, sharedMsgId :: SharedMsgId} | CRBroadcastSent {user :: User, msgContent :: MsgContent, successes :: Int, failures :: Int, timestamp :: UTCTime} | CRMsgIntegrityError {user :: User, msgError :: MsgErrorType} @@ -612,6 +629,7 @@ data ChatResponse | CRVersionInfo {versionInfo :: CoreVersionInfo, chatMigrations :: [UpMigration], agentMigrations :: [UpMigration]} | CRInvitation {user :: User, connReqInvitation :: ConnReqInvitation, connection :: PendingContactConnection} | CRConnectionIncognitoUpdated {user :: User, toConnection :: PendingContactConnection} + | CRConnectionUserChanged {user :: User, fromConnection :: PendingContactConnection, toConnection :: PendingContactConnection, newUser :: User} | CRConnectionPlan {user :: User, connectionPlan :: ConnectionPlan} | CRSentConfirmation {user :: User, connection :: PendingContactConnection} | CRSentInvitation {user :: User, connection :: PendingContactConnection, customUserProfile :: Maybe Profile} @@ -642,6 +660,7 @@ data ChatResponse | CRRcvFileCancelled {user :: User, chatItem_ :: Maybe AChatItem, rcvFileTransfer :: RcvFileTransfer} | CRRcvFileSndCancelled {user :: User, chatItem :: AChatItem, rcvFileTransfer :: RcvFileTransfer} | CRRcvFileError {user :: User, chatItem_ :: Maybe AChatItem, agentError :: AgentErrorType, rcvFileTransfer :: RcvFileTransfer} + | CRRcvFileWarning {user :: User, chatItem_ :: Maybe AChatItem, agentError :: AgentErrorType, rcvFileTransfer :: RcvFileTransfer} | CRSndFileStart {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} | CRSndFileComplete {user :: User, chatItem :: AChatItem, sndFileTransfer :: SndFileTransfer} | CRSndFileRcvCancelled {user :: User, chatItem_ :: Maybe AChatItem, sndFileTransfer :: SndFileTransfer} @@ -654,6 +673,7 @@ data ChatResponse | CRSndStandaloneFileComplete {user :: User, fileTransferMeta :: FileTransferMeta, rcvURIs :: [Text]} | CRSndFileCancelledXFTP {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta} | CRSndFileError {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta, errorMessage :: Text} + | CRSndFileWarning {user :: User, chatItem_ :: Maybe AChatItem, fileTransferMeta :: FileTransferMeta, errorMessage :: Text} | CRUserProfileUpdated {user :: User, fromProfile :: Profile, toProfile :: Profile, updateSummary :: UserProfileUpdateSummary} | CRUserProfileImage {user :: User, profile :: Profile} | CRContactAliasUpdated {user :: User, toContact :: Contact} @@ -661,6 +681,7 @@ data ChatResponse | CRContactPrefsUpdated {user :: User, fromContact :: Contact, toContact :: Contact} | CRContactConnecting {user :: User, contact :: Contact} | CRContactConnected {user :: User, contact :: Contact, userCustomProfile :: Maybe Profile} + | CRContactSndReady {user :: User, contact :: Contact} | CRContactAnotherClient {user :: User, contact :: Contact} | CRSubscriptionEnd {user :: User, connectionEntity :: ConnectionEntity} | CRContactsDisconnected {server :: SMPServer, contactRefs :: [ContactRef]} @@ -720,7 +741,7 @@ data ChatResponse | CRUserContactLinkSubError {chatError :: ChatError} -- TODO delete | CRNtfTokenStatus {status :: NtfTknStatus} | CRNtfToken {token :: DeviceToken, status :: NtfTknStatus, ntfMode :: NotificationsMode, ntfServer :: NtfServer} - | CRNtfMessages {user_ :: Maybe User, connEntity_ :: Maybe ConnectionEntity, msgTs :: Maybe UTCTime, ntfMessages :: [NtfMsgInfo]} + | CRNtfMessages {user_ :: Maybe User, connEntity_ :: Maybe ConnectionEntity, msgTs :: Maybe UTCTime, ntfMessage_ :: Maybe NtfMsgInfo} | CRNtfMessage {user :: User, connEntity :: ConnectionEntity, ntfMessage :: NtfMsgInfo} | CRContactConnectionDeleted {user :: User, connection :: PendingContactConnection} | CRRemoteHostList {remoteHosts :: [RemoteHostInfo]} @@ -741,12 +762,16 @@ data ChatResponse | CRSQLResult {rows :: [Text]} | CRSlowSQLQueries {chatQueries :: [SlowSQLQuery], agentQueries :: [SlowSQLQuery]} | CRDebugLocks {chatLockName :: Maybe String, chatEntityLocks :: Map String String, agentLocks :: AgentLocks} - | CRAgentStats {agentStats :: [[String]]} + | CRAgentSubsTotal {user :: User, subsTotal :: SMPServerSubs, hasSession :: Bool} + | CRAgentServersSummary {user :: User, serversSummary :: PresentedServersSummary} | CRAgentWorkersDetails {agentWorkersDetails :: AgentWorkersDetails} | CRAgentWorkersSummary {agentWorkersSummary :: AgentWorkersSummary} | CRAgentSubs {activeSubs :: Map Text Int, pendingSubs :: Map Text Int, removedSubs :: Map Text [String]} | CRAgentSubsDetails {agentSubs :: SubscriptionsInfo} + | CRAgentQueuesInfo {agentQueuesInfo :: AgentQueuesInfo} + | CRContactDisabled {user :: User, contact :: Contact} | CRConnectionDisabled {connectionEntity :: ConnectionEntity} + | CRConnectionInactive {connectionEntity :: ConnectionEntity, inactive :: Bool} | CRAgentRcvQueueDeleted {agentConnId :: AgentConnId, server :: SMPServer, agentQueueId :: AgentQueueId, agentError_ :: Maybe AgentErrorType} | CRAgentConnDeleted {agentConnId :: AgentConnId} | CRAgentUserDeleted {agentUserId :: Int64} @@ -754,6 +779,7 @@ data ChatResponse | CRChatCmdError {user_ :: Maybe User, chatError :: ChatError} | CRChatError {user_ :: Maybe User, chatError :: ChatError} | CRChatErrors {user_ :: Maybe User, chatErrors :: [ChatError]} + | CRArchiveExported {archiveErrors :: [ArchiveError]} | CRArchiveImported {archiveErrors :: [ArchiveError]} | CRAppSettings {appSettings :: AppSettings} | CRTimedAction {action :: String, durationMilliseconds :: Int64} @@ -824,6 +850,12 @@ data ChatListQuery clqNoFilters :: ChatListQuery clqNoFilters = CLQFilters {favorite = False, unread = False} +data ChatDeleteMode + = CDMFull {notify :: Bool} -- delete both contact and conversation + | CDMEntity {notify :: Bool} -- delete contact (connection), keep conversation + | CDMMessages -- delete conversation, keep contact - can be re-opened from Contacts view + deriving (Show) + data ConnectionPlan = CPInvitationLink {invitationLinkPlan :: InvitationLinkPlan} | CPContactAddress {contactAddressPlan :: ContactAddressPlan} @@ -907,7 +939,7 @@ deriving instance Show AProtoServersConfig data UserProtoServers p = UserProtoServers { serverProtocol :: SProtocolType p, protoServers :: NonEmpty (ServerCfg p), - presetServers :: NonEmpty (ProtoServerWithAuth p) + presetServers :: NonEmpty (ServerCfg p) } deriving (Show) @@ -941,6 +973,19 @@ data AppFilePathsConfig = AppFilePathsConfig } deriving (Show) +data SimpleNetCfg = SimpleNetCfg + { socksProxy :: Maybe SocksProxy, + socksMode :: SocksMode, + smpProxyMode_ :: Maybe SMPProxyMode, + smpProxyFallback_ :: Maybe SMPProxyFallback, + tcpTimeout_ :: Maybe Int, + logTLSErrors :: Bool + } + deriving (Show) + +defaultSimpleNetCfg :: SimpleNetCfg +defaultSimpleNetCfg = SimpleNetCfg Nothing SMAlways Nothing Nothing Nothing False + data ContactSubStatus = ContactSubStatus { contact :: Contact, contactError :: Maybe ChatError @@ -1041,6 +1086,12 @@ tmeToPref currentTTL tme = uncurry TimedMessagesPreference $ case tme of TMEEnableKeepTTL -> (FAYes, currentTTL) TMEDisableKeepTTL -> (FANo, currentTTL) +data ChatItemDeletion = ChatItemDeletion + { deletedChatItem :: AChatItem, + toChatItem :: Maybe AChatItem + } + deriving (Show) + data ChatLogLevel = CLLDebug | CLLInfo | CLLWarning | CLLError | CLLImportant deriving (Eq, Ord, Show) @@ -1106,7 +1157,6 @@ data ChatErrorType | CECantBlockMemberForSelf {groupInfo :: GroupInfo, member :: GroupMember, setShowMessages :: Bool} | CEGroupMemberUserRemoved | CEGroupMemberNotFound - | CEGroupMemberIntroNotFound {contactName :: ContactName} | CEGroupCantResendInvitation {groupInfo :: GroupInfo, contactName :: ContactName} | CEGroupInternal {message :: String} | CEFileNotFound {message :: String} @@ -1123,8 +1173,7 @@ data ChatErrorType | CEFileImageType {filePath :: FilePath} | CEFileImageSize {filePath :: FilePath} | CEFileNotReceived {fileId :: FileTransferId} - | CEXFTPRcvFile {fileId :: FileTransferId, agentRcvFileId :: AgentRcvFileId, agentError :: AgentErrorType} - | CEXFTPSndFile {fileId :: FileTransferId, agentSndFileId :: AgentSndFileId, agentError :: AgentErrorType} + | CEFileNotApproved {fileId :: FileTransferId, unknownServers :: [XFTPServer]} | CEFallbackToSMPProhibited {fileId :: FileTransferId} | CEInlineFileProhibited {fileId :: FileTransferId} | CEInvalidQuote @@ -1144,6 +1193,7 @@ data ChatErrorType | CEAgentCommandError {message :: String} | CEInvalidFileDescription {message :: String} | CEConnectionIncognitoChangeProhibited + | CEConnectionUserChangeProhibited | CEPeerChatVRangeIncompatible | CEInternalError {message :: String} | CEException {message :: String} @@ -1157,7 +1207,7 @@ data DatabaseError | DBErrorOpen {sqliteError :: SQLiteError} deriving (Show, Exception) -data SQLiteError = SQLiteErrorNotADatabase | SQLiteError String +data SQLiteError = SQLiteErrorNotADatabase | SQLiteError {dbError :: String} deriving (Show, Exception) throwDBError :: DatabaseError -> CM () @@ -1206,8 +1256,8 @@ data RemoteCtrlStopReason deriving (Show, Exception) data ArchiveError - = AEImport {chatError :: ChatError} - | AEImportFile {file :: String, chatError :: ChatError} + = AEImport {importError :: String} + | AEFileError {file :: String, fileError :: String} deriving (Show, Exception) -- | Host (mobile) side of transport to process remote commands and forward notifications @@ -1356,11 +1406,24 @@ toView' ev = do withStore' :: (DB.Connection -> IO a) -> CM a withStore' action = withStore $ liftIO . action +{-# INLINE withStore' #-} + +withFastStore' :: (DB.Connection -> IO a) -> CM a +withFastStore' action = withFastStore $ liftIO . action +{-# INLINE withFastStore' #-} withStore :: (DB.Connection -> ExceptT StoreError IO a) -> CM a -withStore action = do +withStore = withStorePriority False +{-# INLINE withStore #-} + +withFastStore :: (DB.Connection -> ExceptT StoreError IO a) -> CM a +withFastStore = withStorePriority True +{-# INLINE withFastStore #-} + +withStorePriority :: Bool -> (DB.Connection -> ExceptT StoreError IO a) -> CM a +withStorePriority priority action = do ChatController {chatStore} <- ask - liftIOEither $ withTransaction chatStore (runExceptT . withExceptT ChatErrorStore . action) `E.catches` handleDBErrors + liftIOEither $ withTransactionPriority chatStore priority (runExceptT . withExceptT ChatErrorStore . action) `E.catches` handleDBErrors withStoreBatch :: Traversable t => (DB.Connection -> t (IO (Either ChatError a))) -> CM' (t (Either ChatError a)) withStoreBatch actions = do @@ -1436,6 +1499,8 @@ $(JQ.deriveJSON defaultJSON ''ServerAddress) $(JQ.deriveJSON defaultJSON ''ParsedServerAddress) +$(JQ.deriveJSON defaultJSON ''ChatItemDeletion) + $(JQ.deriveJSON defaultJSON ''CoreVersionInfo) $(JQ.deriveJSON defaultJSON ''SlowSQLQuery) diff --git a/src/Simplex/Chat/Core.hs b/src/Simplex/Chat/Core.hs index a8580746d1..a07968654d 100644 --- a/src/Simplex/Chat/Core.hs +++ b/src/Simplex/Chat/Core.hs @@ -54,7 +54,7 @@ runSimplexChat :: ChatOpts -> User -> ChatController -> (User -> ChatController runSimplexChat ChatOpts {maintenance} u cc chat | maintenance = wait =<< async (chat u cc) | otherwise = do - a1 <- runReaderT (startChatController True) cc + a1 <- runReaderT (startChatController True True) cc a2 <- async $ chat u cc waitEither_ a1 a2 @@ -105,7 +105,7 @@ createActiveUser cc = do loop = do displayName <- T.pack <$> getWithPrompt "display name" let profile = Just Profile {displayName, fullName = "", image = Nothing, contactLink = Nothing, preferences = Nothing} - execChatCommand' (CreateActiveUser NewUser {profile, sameServers = False, pastTimestamp = False}) `runReaderT` cc >>= \case + execChatCommand' (CreateActiveUser NewUser {profile, pastTimestamp = False}) `runReaderT` cc >>= \case CRActiveUser user -> pure user r -> do ts <- getCurrentTime diff --git a/src/Simplex/Chat/Markdown.hs b/src/Simplex/Chat/Markdown.hs index d3b9ea52f1..8e82bfe727 100644 --- a/src/Simplex/Chat/Markdown.hs +++ b/src/Simplex/Chat/Markdown.hs @@ -29,13 +29,12 @@ import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import Simplex.Chat.Types -import Simplex.Chat.Types.Util import Simplex.Messaging.Agent.Protocol (AConnectionRequestUri (..), ConnReqUriData (..), ConnectionRequestUri (..), SMPQueue (..)) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fstToLower, sumTypeJSON) import Simplex.Messaging.Protocol (ProtocolServer (..)) import Simplex.Messaging.ServiceScheme (ServiceScheme (..)) -import Simplex.Messaging.Util (safeDecodeUtf8) +import Simplex.Messaging.Util (decodeJSON, safeDecodeUtf8) import System.Console.ANSI.Types import qualified Text.Email.Validate as Email @@ -144,13 +143,9 @@ markdownToList (m1 :|: m2) = markdownToList m1 <> markdownToList m2 parseMarkdown :: Text -> Markdown parseMarkdown s = fromRight (unmarked s) $ A.parseOnly (markdownP <* A.endOfInput) s -containsFormat :: (Format -> Bool) -> Markdown -> Bool -containsFormat p (Markdown f _) = maybe False p f -containsFormat p (m1 :|: m2) = containsFormat p m1 || containsFormat p m2 - isSimplexLink :: Format -> Bool isSimplexLink = \case - SimplexLink {} -> True; + SimplexLink {} -> True _ -> False markdownP :: Parser Markdown @@ -227,11 +222,15 @@ markdownP = mconcat <$> A.many' fragmentP wordMD s | T.null s = unmarked s | isUri s = - let t = T.takeWhileEnd isPunctuation s - uri = uriMarkdown $ T.dropWhileEnd isPunctuation s + let t = T.takeWhileEnd isPunctuation' s + uri = uriMarkdown $ T.dropWhileEnd isPunctuation' s in if T.null t then uri else uri :|: unmarked t | isEmail s = markdown Email s | otherwise = unmarked s + isPunctuation' = \case + '/' -> False + ')' -> False + c -> isPunctuation c uriMarkdown s = case strDecode $ encodeUtf8 s of Right cReq -> markdown (simplexUriFormat cReq) s _ -> markdown Uri s diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index a6d5761b5f..78e3b4c640 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -14,6 +14,7 @@ {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} +{-# OPTIONS_GHC -fno-warn-operator-whitespace #-} module Simplex.Chat.Messages where @@ -29,11 +30,12 @@ import qualified Data.ByteString.Lazy.Char8 as LB import Data.Char (isSpace) import Data.Int (Int64) import Data.Kind (Constraint) +import Data.List.NonEmpty (NonEmpty) import Data.Maybe (fromMaybe, isJust, isNothing) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding (decodeLatin1, encodeUtf8) -import Data.Time.Clock (UTCTime, diffUTCTime, nominalDay) +import Data.Time.Clock (UTCTime, diffUTCTime, nominalDay, NominalDiffTime) import Data.Type.Equality import Data.Typeable (Typeable) import Database.SQLite.Simple.FromField (FromField (..)) @@ -45,6 +47,7 @@ import Simplex.Chat.Messages.CIContent import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Chat.Types.Preferences +import Simplex.Chat.Types.Util (textParseJSON) import Simplex.Messaging.Agent.Protocol (AgentMsgId, MsgMeta (..), MsgReceiptStatus (..)) import Simplex.Messaging.Crypto.File (CryptoFile (..)) import qualified Simplex.Messaging.Crypto.File as CF @@ -277,6 +280,12 @@ toChatInfo = \case CDLocalSnd l -> LocalChat l CDLocalRcv l -> LocalChat l +contactChatDeleted :: ChatDirection c d -> Bool +contactChatDeleted = \case + CDDirectSnd Contact {chatDeleted} -> chatDeleted + CDDirectRcv Contact {chatDeleted} -> chatDeleted + _ -> False + data NewChatItem d = NewChatItem { createdByMsgId :: Maybe MessageId, itemSent :: SMsgDirection d, @@ -338,6 +347,7 @@ data CIMeta (c :: ChatType) (d :: MsgDirection) = CIMeta itemTs :: ChatItemTs, itemText :: Text, itemStatus :: CIStatus d, + sentViaProxy :: Maybe Bool, itemSharedMsgId :: Maybe SharedMsgId, itemForwarded :: Maybe CIForwardedFrom, itemDeleted :: Maybe (CIDeleted c), @@ -352,16 +362,20 @@ data CIMeta (c :: ChatType) (d :: MsgDirection) = CIMeta } deriving (Show) -mkCIMeta :: forall c d. ChatTypeI c => ChatItemId -> CIContent d -> Text -> CIStatus d -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe (CIDeleted c) -> Bool -> Maybe CITimed -> Maybe Bool -> UTCTime -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> UTCTime -> CIMeta c d -mkCIMeta itemId itemContent itemText itemStatus itemSharedMsgId itemForwarded itemDeleted itemEdited itemTimed itemLive currentTs itemTs forwardedByMember createdAt updatedAt = - let deletable = case itemContent of - CISndMsgContent _ -> - case chatTypeI @c of - SCTLocal -> isNothing itemDeleted - _ -> diffUTCTime currentTs itemTs < nominalDay && isNothing itemDeleted - _ -> False +mkCIMeta :: forall c d. ChatTypeI c => ChatItemId -> CIContent d -> Text -> CIStatus d -> Maybe Bool -> Maybe SharedMsgId -> Maybe CIForwardedFrom -> Maybe (CIDeleted c) -> Bool -> Maybe CITimed -> Maybe Bool -> UTCTime -> ChatItemTs -> Maybe GroupMemberId -> UTCTime -> UTCTime -> CIMeta c d +mkCIMeta itemId itemContent itemText itemStatus sentViaProxy itemSharedMsgId itemForwarded itemDeleted itemEdited itemTimed itemLive currentTs itemTs forwardedByMember createdAt updatedAt = + let deletable = deletable' itemContent itemDeleted itemTs nominalDay currentTs editable = deletable && isNothing itemForwarded - in CIMeta {itemId, itemTs, itemText, itemStatus, itemSharedMsgId, itemForwarded, itemDeleted, itemEdited, itemTimed, itemLive, deletable, editable, forwardedByMember, createdAt, updatedAt} + in CIMeta {itemId, itemTs, itemText, itemStatus, sentViaProxy, itemSharedMsgId, itemForwarded, itemDeleted, itemEdited, itemTimed, itemLive, deletable, editable, forwardedByMember, createdAt, updatedAt} + +deletable' :: forall c d. ChatTypeI c => CIContent d -> Maybe (CIDeleted c) -> UTCTime -> NominalDiffTime -> UTCTime -> Bool +deletable' itemContent itemDeleted itemTs allowedInterval currentTs = + case itemContent of + CISndMsgContent _ -> + case chatTypeI @c of + SCTLocal -> isNothing itemDeleted + _ -> diffUTCTime currentTs itemTs < allowedInterval && isNothing itemDeleted + _ -> False dummyMeta :: ChatItemId -> UTCTime -> Text -> CIMeta c 'MDSnd dummyMeta itemId ts itemText = @@ -370,6 +384,7 @@ dummyMeta itemId ts itemText = itemTs = ts, itemText, itemStatus = CISSndNew, + sentViaProxy = Nothing, itemSharedMsgId = Nothing, itemForwarded = Nothing, itemDeleted = Nothing, @@ -445,10 +460,10 @@ deriving instance Show ACIReaction data JSONCIReaction c d = JSONCIReaction {chatInfo :: ChatInfo c, chatReaction :: CIReaction c d} type family ChatTypeQuotable (a :: ChatType) :: Constraint where - ChatTypeQuotable CTDirect = () - ChatTypeQuotable CTGroup = () + ChatTypeQuotable 'CTDirect = () + ChatTypeQuotable 'CTGroup = () ChatTypeQuotable a = - (Int ~ Bool, TypeError (Type.Text "ChatType " :<>: ShowType a :<>: Type.Text " cannot be quoted")) + (Int ~ Bool, TypeError ('Type.Text "ChatType " ':<>: 'ShowType a ':<>: 'Type.Text " cannot be quoted")) data CIQDirection (c :: ChatType) where CIQDirectSnd :: CIQDirection 'CTDirect @@ -525,13 +540,16 @@ data CIFileStatus (d :: MsgDirection) where CIFSSndTransfer :: {sndProgress :: Int64, sndTotal :: Int64} -> CIFileStatus 'MDSnd CIFSSndCancelled :: CIFileStatus 'MDSnd CIFSSndComplete :: CIFileStatus 'MDSnd - CIFSSndError :: CIFileStatus 'MDSnd + CIFSSndError :: {sndFileError :: FileError} -> CIFileStatus 'MDSnd + CIFSSndWarning :: {sndFileError :: FileError} -> CIFileStatus 'MDSnd CIFSRcvInvitation :: CIFileStatus 'MDRcv CIFSRcvAccepted :: CIFileStatus 'MDRcv CIFSRcvTransfer :: {rcvProgress :: Int64, rcvTotal :: Int64} -> CIFileStatus 'MDRcv + CIFSRcvAborted :: CIFileStatus 'MDRcv CIFSRcvComplete :: CIFileStatus 'MDRcv CIFSRcvCancelled :: CIFileStatus 'MDRcv - CIFSRcvError :: CIFileStatus 'MDRcv + CIFSRcvError :: {rcvFileError :: FileError} -> CIFileStatus 'MDRcv + CIFSRcvWarning :: {rcvFileError :: FileError} -> CIFileStatus 'MDRcv CIFSInvalid :: {text :: Text} -> CIFileStatus 'MDSnd deriving instance Eq (CIFileStatus d) @@ -544,13 +562,16 @@ ciFileEnded = \case CIFSSndTransfer {} -> False CIFSSndCancelled -> True CIFSSndComplete -> True - CIFSSndError -> True + CIFSSndError {} -> True + CIFSSndWarning {} -> False CIFSRcvInvitation -> False CIFSRcvAccepted -> False CIFSRcvTransfer {} -> False + CIFSRcvAborted -> True CIFSRcvCancelled -> True CIFSRcvComplete -> True - CIFSRcvError -> True + CIFSRcvError {} -> True + CIFSRcvWarning {} -> False CIFSInvalid {} -> True ciFileLoaded :: CIFileStatus d -> Bool @@ -559,13 +580,16 @@ ciFileLoaded = \case CIFSSndTransfer {} -> True CIFSSndComplete -> True CIFSSndCancelled -> True - CIFSSndError -> True + CIFSSndError {} -> True + CIFSSndWarning {} -> True CIFSRcvInvitation -> False CIFSRcvAccepted -> False CIFSRcvTransfer {} -> False + CIFSRcvAborted -> False CIFSRcvCancelled -> False CIFSRcvComplete -> True - CIFSRcvError -> False + CIFSRcvError {} -> False + CIFSRcvWarning {} -> False CIFSInvalid {} -> False data ACIFileStatus = forall d. MsgDirectionI d => AFS (SMsgDirection d) (CIFileStatus d) @@ -578,13 +602,16 @@ instance MsgDirectionI d => StrEncoding (CIFileStatus d) where CIFSSndTransfer sent total -> strEncode (Str "snd_transfer", sent, total) CIFSSndCancelled -> "snd_cancelled" CIFSSndComplete -> "snd_complete" - CIFSSndError -> "snd_error" + CIFSSndError sndFileErr -> "snd_error " <> strEncode sndFileErr + CIFSSndWarning sndFileErr -> "snd_warning " <> strEncode sndFileErr CIFSRcvInvitation -> "rcv_invitation" CIFSRcvAccepted -> "rcv_accepted" CIFSRcvTransfer rcvd total -> strEncode (Str "rcv_transfer", rcvd, total) + CIFSRcvAborted -> "rcv_aborted" CIFSRcvComplete -> "rcv_complete" CIFSRcvCancelled -> "rcv_cancelled" - CIFSRcvError -> "rcv_error" + CIFSRcvError rcvFileErr -> "rcv_error " <> strEncode rcvFileErr + CIFSRcvWarning rcvFileErr -> "rcv_warning " <> strEncode rcvFileErr CIFSInvalid {} -> "invalid" strP = (\(AFS _ st) -> checkDirection st) <$?> strP @@ -600,13 +627,16 @@ instance StrEncoding ACIFileStatus where "snd_transfer" -> AFS SMDSnd <$> progress CIFSSndTransfer "snd_cancelled" -> pure $ AFS SMDSnd CIFSSndCancelled "snd_complete" -> pure $ AFS SMDSnd CIFSSndComplete - "snd_error" -> pure $ AFS SMDSnd CIFSSndError + "snd_error" -> AFS SMDSnd . CIFSSndError <$> ((A.space *> strP) <|> pure (FileErrOther "")) -- alternative for backwards compatibility + "snd_warning" -> AFS SMDSnd . CIFSSndWarning <$> (A.space *> strP) "rcv_invitation" -> pure $ AFS SMDRcv CIFSRcvInvitation "rcv_accepted" -> pure $ AFS SMDRcv CIFSRcvAccepted "rcv_transfer" -> AFS SMDRcv <$> progress CIFSRcvTransfer + "rcv_aborted" -> pure $ AFS SMDRcv CIFSRcvAborted "rcv_complete" -> pure $ AFS SMDRcv CIFSRcvComplete "rcv_cancelled" -> pure $ AFS SMDRcv CIFSRcvCancelled - "rcv_error" -> pure $ AFS SMDRcv CIFSRcvError + "rcv_error" -> AFS SMDRcv . CIFSRcvError <$> ((A.space *> strP) <|> pure (FileErrOther "")) -- alternative for backwards compatibility + "rcv_warning" -> AFS SMDRcv . CIFSRcvWarning <$> (A.space *> strP) _ -> fail "bad file status" progress :: (Int64 -> Int64 -> a) -> A.Parser a progress f = f <$> num <*> num <|> pure (f 0 1) @@ -617,13 +647,16 @@ data JSONCIFileStatus | JCIFSSndTransfer {sndProgress :: Int64, sndTotal :: Int64} | JCIFSSndCancelled | JCIFSSndComplete - | JCIFSSndError + | JCIFSSndError {sndFileError :: FileError} + | JCIFSSndWarning {sndFileError :: FileError} | JCIFSRcvInvitation | JCIFSRcvAccepted | JCIFSRcvTransfer {rcvProgress :: Int64, rcvTotal :: Int64} + | JCIFSRcvAborted | JCIFSRcvComplete | JCIFSRcvCancelled - | JCIFSRcvError + | JCIFSRcvError {rcvFileError :: FileError} + | JCIFSRcvWarning {rcvFileError :: FileError} | JCIFSInvalid {text :: Text} jsonCIFileStatus :: CIFileStatus d -> JSONCIFileStatus @@ -632,13 +665,16 @@ jsonCIFileStatus = \case CIFSSndTransfer sent total -> JCIFSSndTransfer sent total CIFSSndCancelled -> JCIFSSndCancelled CIFSSndComplete -> JCIFSSndComplete - CIFSSndError -> JCIFSSndError + CIFSSndError sndFileErr -> JCIFSSndError sndFileErr + CIFSSndWarning sndFileErr -> JCIFSSndWarning sndFileErr CIFSRcvInvitation -> JCIFSRcvInvitation CIFSRcvAccepted -> JCIFSRcvAccepted CIFSRcvTransfer rcvd total -> JCIFSRcvTransfer rcvd total + CIFSRcvAborted -> JCIFSRcvAborted CIFSRcvComplete -> JCIFSRcvComplete CIFSRcvCancelled -> JCIFSRcvCancelled - CIFSRcvError -> JCIFSRcvError + CIFSRcvError rcvFileErr -> JCIFSRcvError rcvFileErr + CIFSRcvWarning rcvFileErr -> JCIFSRcvWarning rcvFileErr CIFSInvalid text -> JCIFSInvalid text aciFileStatusJSON :: JSONCIFileStatus -> ACIFileStatus @@ -647,15 +683,39 @@ aciFileStatusJSON = \case JCIFSSndTransfer sent total -> AFS SMDSnd $ CIFSSndTransfer sent total JCIFSSndCancelled -> AFS SMDSnd CIFSSndCancelled JCIFSSndComplete -> AFS SMDSnd CIFSSndComplete - JCIFSSndError -> AFS SMDSnd CIFSSndError + JCIFSSndError sndFileErr -> AFS SMDSnd (CIFSSndError sndFileErr) + JCIFSSndWarning sndFileErr -> AFS SMDSnd (CIFSSndWarning sndFileErr) JCIFSRcvInvitation -> AFS SMDRcv CIFSRcvInvitation JCIFSRcvAccepted -> AFS SMDRcv CIFSRcvAccepted JCIFSRcvTransfer rcvd total -> AFS SMDRcv $ CIFSRcvTransfer rcvd total + JCIFSRcvAborted -> AFS SMDRcv CIFSRcvAborted JCIFSRcvComplete -> AFS SMDRcv CIFSRcvComplete JCIFSRcvCancelled -> AFS SMDRcv CIFSRcvCancelled - JCIFSRcvError -> AFS SMDRcv CIFSRcvError + JCIFSRcvError rcvFileErr -> AFS SMDRcv (CIFSRcvError rcvFileErr) + JCIFSRcvWarning rcvFileErr -> AFS SMDRcv (CIFSRcvWarning rcvFileErr) JCIFSInvalid text -> AFS SMDSnd $ CIFSInvalid text +data FileError + = FileErrAuth + | FileErrNoFile + | FileErrRelay {srvError :: SrvError} + | FileErrOther {fileError :: Text} + deriving (Eq, Show) + +instance StrEncoding FileError where + strEncode = \case + FileErrAuth -> "auth" + FileErrNoFile -> "no_file" + FileErrRelay srvErr -> "relay " <> strEncode srvErr + FileErrOther e -> "other " <> encodeUtf8 e + strP = + A.takeWhile1 (/= ' ') >>= \case + "auth" -> pure FileErrAuth + "no_file" -> pure FileErrNoFile + "relay" -> FileErrRelay <$> (A.space *> strP) + "other" -> FileErrOther . safeDecodeUtf8 <$> (A.space *> A.takeByteString) + s -> FileErrOther . safeDecodeUtf8 . (s <>) <$> A.takeByteString + -- to conveniently read file data from db data CIFileInfo = CIFileInfo { fileId :: Int64, @@ -676,8 +736,9 @@ data CIStatus (d :: MsgDirection) where CISSndNew :: CIStatus 'MDSnd CISSndSent :: SndCIStatusProgress -> CIStatus 'MDSnd CISSndRcvd :: MsgReceiptStatus -> SndCIStatusProgress -> CIStatus 'MDSnd - CISSndErrorAuth :: CIStatus 'MDSnd - CISSndError :: String -> CIStatus 'MDSnd + CISSndErrorAuth :: CIStatus 'MDSnd -- deprecated + CISSndError :: SndError -> CIStatus 'MDSnd + CISSndWarning :: SndError -> CIStatus 'MDSnd CISRcvNew :: CIStatus 'MDRcv CISRcvRead :: CIStatus 'MDRcv CISInvalid :: Text -> CIStatus 'MDSnd @@ -696,7 +757,8 @@ instance MsgDirectionI d => StrEncoding (CIStatus d) where CISSndSent sndProgress -> "snd_sent " <> strEncode sndProgress CISSndRcvd msgRcptStatus sndProgress -> "snd_rcvd " <> strEncode msgRcptStatus <> " " <> strEncode sndProgress CISSndErrorAuth -> "snd_error_auth" - CISSndError e -> "snd_error " <> encodeUtf8 (T.pack e) + CISSndError sndErr -> "snd_error " <> strEncode sndErr + CISSndWarning sndErr -> "snd_warning " <> strEncode sndErr CISRcvNew -> "rcv_new" CISRcvRead -> "rcv_read" CISInvalid {} -> "invalid" @@ -714,17 +776,68 @@ instance StrEncoding ACIStatus where "snd_sent" -> ACIStatus SMDSnd . CISSndSent <$> ((A.space *> strP) <|> pure SSPComplete) "snd_rcvd" -> ACIStatus SMDSnd <$> (CISSndRcvd <$> (A.space *> strP) <*> ((A.space *> strP) <|> pure SSPComplete)) "snd_error_auth" -> pure $ ACIStatus SMDSnd CISSndErrorAuth - "snd_error" -> ACIStatus SMDSnd . CISSndError . T.unpack . safeDecodeUtf8 <$> (A.space *> A.takeByteString) + "snd_error" -> ACIStatus SMDSnd . CISSndError <$> (A.space *> strP) + "snd_warning" -> ACIStatus SMDSnd . CISSndWarning <$> (A.space *> strP) "rcv_new" -> pure $ ACIStatus SMDRcv CISRcvNew "rcv_read" -> pure $ ACIStatus SMDRcv CISRcvRead _ -> fail "bad status" +-- see serverHostError in agent +data SndError + = SndErrAuth + | SndErrQuota + | SndErrExpired -- TIMEOUT/NETWORK errors + | SndErrRelay {srvError :: SrvError} -- BROKER errors (other than TIMEOUT/NETWORK) + | SndErrProxy {proxyServer :: String, srvError :: SrvError} -- SMP PROXY errors + | SndErrProxyRelay {proxyServer :: String, srvError :: SrvError} -- PROXY BROKER errors + | SndErrOther {sndError :: Text} -- other errors + deriving (Eq, Show) + +data SrvError + = SrvErrHost + | SrvErrVersion + | SrvErrOther {srvError :: Text} + deriving (Eq, Show) + +instance StrEncoding SndError where + strEncode = \case + SndErrAuth -> "auth" + SndErrQuota -> "quota" + SndErrExpired -> "expired" + SndErrRelay srvErr -> "relay " <> strEncode srvErr + SndErrProxy proxy srvErr -> "proxy " <> encodeUtf8 (T.pack proxy) <> " " <> strEncode srvErr + SndErrProxyRelay proxy srvErr -> "proxy_relay " <> encodeUtf8 (T.pack proxy) <> " " <> strEncode srvErr + SndErrOther e -> "other " <> encodeUtf8 e + strP = + A.takeWhile1 (/= ' ') >>= \case + "auth" -> pure SndErrAuth + "quota" -> pure SndErrQuota + "expired" -> pure SndErrExpired + "relay" -> SndErrRelay <$> (A.space *> strP) + "proxy" -> SndErrProxy . T.unpack . safeDecodeUtf8 <$> (A.space *> A.takeWhile1 (/= ' ') <* A.space) <*> strP + "proxy_relay" -> SndErrProxyRelay . T.unpack . safeDecodeUtf8 <$> (A.space *> A.takeWhile1 (/= ' ') <* A.space) <*> strP + "other" -> SndErrOther . safeDecodeUtf8 <$> (A.space *> A.takeByteString) + s -> SndErrOther . safeDecodeUtf8 . (s <>) <$> A.takeByteString -- for backward compatibility with `CISSndError String` + +instance StrEncoding SrvError where + strEncode = \case + SrvErrHost -> "host" + SrvErrVersion -> "version" + SrvErrOther e -> "other " <> encodeUtf8 e + strP = + A.takeWhile1 (/= ' ') >>= \case + "host" -> pure SrvErrHost + "version" -> pure SrvErrVersion + "other" -> SrvErrOther . safeDecodeUtf8 <$> (A.space *> A.takeByteString) + _ -> fail "bad SrvError" + data JSONCIStatus = JCISSndNew | JCISSndSent {sndProgress :: SndCIStatusProgress} | JCISSndRcvd {msgRcptStatus :: MsgReceiptStatus, sndProgress :: SndCIStatusProgress} - | JCISSndErrorAuth - | JCISSndError {agentError :: String} + | JCISSndErrorAuth -- deprecated + | JCISSndError {agentError :: SndError} + | JCISSndWarning {agentError :: SndError} | JCISRcvNew | JCISRcvRead | JCISInvalid {text :: Text} @@ -736,7 +849,8 @@ jsonCIStatus = \case CISSndSent sndProgress -> JCISSndSent sndProgress CISSndRcvd msgRcptStatus sndProgress -> JCISSndRcvd msgRcptStatus sndProgress CISSndErrorAuth -> JCISSndErrorAuth - CISSndError e -> JCISSndError e + CISSndError sndErr -> JCISSndError sndErr + CISSndWarning sndErr -> JCISSndWarning sndErr CISRcvNew -> JCISRcvNew CISRcvRead -> JCISRcvRead CISInvalid text -> JCISInvalid text @@ -747,7 +861,8 @@ jsonACIStatus = \case JCISSndSent sndProgress -> ACIStatus SMDSnd $ CISSndSent sndProgress JCISSndRcvd msgRcptStatus sndProgress -> ACIStatus SMDSnd $ CISSndRcvd msgRcptStatus sndProgress JCISSndErrorAuth -> ACIStatus SMDSnd CISSndErrorAuth - JCISSndError e -> ACIStatus SMDSnd $ CISSndError e + JCISSndError sndErr -> ACIStatus SMDSnd $ CISSndError sndErr + JCISSndWarning sndErr -> ACIStatus SMDSnd $ CISSndWarning sndErr JCISRcvNew -> ACIStatus SMDRcv CISRcvNew JCISRcvRead -> ACIStatus SMDRcv CISRcvRead JCISInvalid text -> ACIStatus SMDSnd $ CISInvalid text @@ -762,7 +877,7 @@ ciCreateStatus content = case msgDirection @d of SMDSnd -> ciStatusNew SMDRcv -> if ciRequiresAttention content then ciStatusNew else CISRcvRead -membersGroupItemStatus :: [(CIStatus 'MDSnd, Int)] -> CIStatus 'MDSnd +membersGroupItemStatus :: [(GroupSndStatus, Int)] -> CIStatus 'MDSnd membersGroupItemStatus memStatusCounts | rcvdOk == total = CISSndRcvd MROk SSPComplete | rcvdOk + rcvdBad == total = CISSndRcvd MRBadMsgHash SSPComplete @@ -773,9 +888,9 @@ membersGroupItemStatus memStatusCounts | otherwise = CISSndNew where total = sum $ map snd memStatusCounts - rcvdOk = fromMaybe 0 $ lookup (CISSndRcvd MROk SSPComplete) memStatusCounts - rcvdBad = fromMaybe 0 $ lookup (CISSndRcvd MRBadMsgHash SSPComplete) memStatusCounts - sent = fromMaybe 0 $ lookup (CISSndSent SSPComplete) memStatusCounts + rcvdOk = fromMaybe 0 $ lookup (GSSRcvd MROk) memStatusCounts + rcvdBad = fromMaybe 0 $ lookup (GSSRcvd MRBadMsgHash) memStatusCounts + sent = fromMaybe 0 $ lookup GSSSent memStatusCounts data SndCIStatusProgress = SSPPartial @@ -792,6 +907,47 @@ instance StrEncoding SndCIStatusProgress where "complete" -> pure SSPComplete _ -> fail "bad SndCIStatusProgress" +data GroupSndStatus + = GSSNew + | GSSForwarded + | GSSInactive + | GSSSent + | GSSRcvd {msgRcptStatus :: MsgReceiptStatus} + | GSSError {agentError :: SndError} + | GSSWarning {agentError :: SndError} + | GSSInvalid {text :: Text} + +deriving instance Eq GroupSndStatus + +deriving instance Show GroupSndStatus + +-- Preserve CIStatus encoding for backwards compatibility +instance StrEncoding GroupSndStatus where + strEncode = \case + GSSNew -> "snd_new" + GSSForwarded -> "snd_forwarded" + GSSInactive -> "snd_inactive" + GSSSent -> "snd_sent complete" + GSSRcvd msgRcptStatus -> "snd_rcvd " <> strEncode msgRcptStatus <> " complete" + GSSError sndErr -> "snd_error " <> strEncode sndErr + GSSWarning sndErr -> "snd_warning " <> strEncode sndErr + GSSInvalid {} -> "invalid" + strP = + (statusP <* A.endOfInput) -- see ACIStatus decoding + <|> (GSSInvalid . safeDecodeUtf8 <$> A.takeByteString) + where + statusP = + A.takeTill (== ' ') >>= \case + "snd_new" -> pure GSSNew + "snd_forwarded" -> pure GSSForwarded + "snd_inactive" -> pure GSSInactive + "snd_sent" -> GSSSent <$ " complete" + "snd_rcvd" -> GSSRcvd <$> (_strP <* " complete") + "snd_error_auth" -> pure $ GSSError SndErrAuth + "snd_error" -> GSSError <$> (A.space *> strP) + "snd_warning" -> GSSWarning <$> (A.space *> strP) + _ -> fail "bad status" + type ChatItemId = Int64 type ChatItemTs = UTCTime @@ -871,13 +1027,6 @@ data RcvMessage = RcvMessage forwardedByMember :: Maybe GroupMemberId } -data PendingGroupMessage = PendingGroupMessage - { msgId :: MessageId, - cmEventTag :: ACMEventTag, - msgBody :: MsgBody, - introId_ :: Maybe Int64 - } - type MessageId = Int64 data ConnOrGroupId = ConnectionId Int64 | GroupId Int64 @@ -895,6 +1044,15 @@ data RcvMsgDelivery = RcvMsgDelivery } deriving (Show) +data RcvMsgInfo = RcvMsgInfo + { msgId :: Int64, + msgDeliveryId :: Int64, + msgDeliveryStatus :: Text, + agentMsgId :: AgentMsgId, + agentMsgMeta :: Text + } + deriving (Show) + data MsgMetaJSON = MsgMetaJSON { integrity :: Text, rcvId :: Int64, @@ -1034,7 +1192,7 @@ instance TextEncoding CIForwardedFromTag where data ChatItemInfo = ChatItemInfo { itemVersions :: [ChatItemVersion], - memberDeliveryStatuses :: Maybe [MemberDeliveryStatus], + memberDeliveryStatuses :: Maybe (NonEmpty MemberDeliveryStatus), forwardedFromChatItem :: Maybe AChatItem } deriving (Show) @@ -1063,7 +1221,8 @@ mkItemVersion ChatItem {content, meta} = version <$> ciMsgContent content data MemberDeliveryStatus = MemberDeliveryStatus { groupMemberId :: GroupMemberId, - memberDeliveryStatus :: CIStatus 'MDSnd + memberDeliveryStatus :: GroupSndStatus, + sentViaProxy :: Maybe Bool } deriving (Eq, Show) @@ -1101,6 +1260,10 @@ $(JQ.deriveJSON defaultJSON ''CITimed) $(JQ.deriveJSON (enumJSON $ dropPrefix "SSP") ''SndCIStatusProgress) +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "SrvErr") ''SrvError) + +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "SndErr") ''SndError) + $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "JCIS") ''JSONCIStatus) instance MsgDirectionI d => FromJSON (CIStatus d) where @@ -1116,6 +1279,12 @@ instance (Typeable d, MsgDirectionI d) => FromField (CIStatus d) where fromField instance FromField ACIStatus where fromField = fromTextField_ $ eitherToMaybe . strDecode . encodeUtf8 +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "GSS") ''GroupSndStatus) + +instance ToField GroupSndStatus where toField = toField . decodeLatin1 . strEncode + +instance FromField GroupSndStatus where fromField = fromTextField_ $ eitherToMaybe . strDecode . encodeUtf8 + $(JQ.deriveJSON defaultJSON ''MemberDeliveryStatus) $(JQ.deriveJSON defaultJSON ''ChatItemVersion) @@ -1127,6 +1296,8 @@ instance ChatTypeI c => ToJSON (CIMeta c d) where toJSON = $(JQ.mkToJSON defaultJSON ''CIMeta) toEncoding = $(JQ.mkToEncoding defaultJSON ''CIMeta) +$(JQ.deriveJSON (sumTypeJSON $ dropPrefix "FileErr") ''FileError) + $(JQ.deriveJSON (sumTypeJSON $ dropPrefix "JCIFS") ''JSONCIFileStatus) instance MsgDirectionI d => FromJSON (CIFileStatus d) where @@ -1260,3 +1431,5 @@ $(JQ.deriveJSON defaultJSON ''MsgMetaJSON) msgMetaJson :: MsgMeta -> Text msgMetaJson = decodeLatin1 . LB.toStrict . J.encode . msgMetaToJson + +$(JQ.deriveJSON defaultJSON ''RcvMsgInfo) diff --git a/src/Simplex/Chat/Messages/CIContent.hs b/src/Simplex/Chat/Messages/CIContent.hs index 3d71047d57..e198183b06 100644 --- a/src/Simplex/Chat/Messages/CIContent.hs +++ b/src/Simplex/Chat/Messages/CIContent.hs @@ -29,12 +29,11 @@ import Simplex.Chat.Protocol import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared -import Simplex.Chat.Types.Util import Simplex.Messaging.Agent.Protocol (MsgErrorType (..), RatchetSyncState (..), SwitchPhase (..)) -import Simplex.Messaging.Crypto.Ratchet (PQEncryption, pattern PQEncOn, pattern PQEncOff) +import Simplex.Messaging.Crypto.Ratchet (PQEncryption, pattern PQEncOff, pattern PQEncOn) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fstToLower, singleFieldJSON, sumTypeJSON) -import Simplex.Messaging.Util (safeDecodeUtf8, tshow, (<$?>)) +import Simplex.Messaging.Util (encodeJSON, safeDecodeUtf8, tshow, (<$?>)) data MsgDirection = MDRcv | MDSnd deriving (Eq, Show) diff --git a/src/Simplex/Chat/Migrations/M20240501_chat_deleted.hs b/src/Simplex/Chat/Migrations/M20240501_chat_deleted.hs new file mode 100644 index 0000000000..a7faf33472 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20240501_chat_deleted.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20240501_chat_deleted where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240501_chat_deleted :: Query +m20240501_chat_deleted = + [sql| +ALTER TABLE contacts ADD COLUMN chat_deleted INTEGER NOT NULL DEFAULT 0; +|] + +down_m20240501_chat_deleted :: Query +down_m20240501_chat_deleted = + [sql| +ALTER TABLE contacts DROP COLUMN chat_deleted; +|] diff --git a/src/Simplex/Chat/Migrations/M20240510_chat_items_via_proxy.hs b/src/Simplex/Chat/Migrations/M20240510_chat_items_via_proxy.hs new file mode 100644 index 0000000000..3c32034344 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20240510_chat_items_via_proxy.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20240510_chat_items_via_proxy where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240510_chat_items_via_proxy :: Query +m20240510_chat_items_via_proxy = + [sql| +ALTER TABLE chat_items ADD COLUMN via_proxy INTEGER; +ALTER TABLE group_snd_item_statuses ADD COLUMN via_proxy INTEGER; +|] + +down_m20240510_chat_items_via_proxy :: Query +down_m20240510_chat_items_via_proxy = + [sql| +ALTER TABLE chat_items DROP COLUMN via_proxy; +ALTER TABLE group_snd_item_statuses DROP COLUMN via_proxy; +|] diff --git a/src/Simplex/Chat/Migrations/M20240515_rcv_files_user_approved_relays.hs b/src/Simplex/Chat/Migrations/M20240515_rcv_files_user_approved_relays.hs new file mode 100644 index 0000000000..cd4f647685 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20240515_rcv_files_user_approved_relays.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240515_rcv_files_user_approved_relays :: Query +m20240515_rcv_files_user_approved_relays = + [sql| +ALTER TABLE rcv_files ADD COLUMN user_approved_relays INTEGER NOT NULL DEFAULT 0; +|] + +down_m20240515_rcv_files_user_approved_relays :: Query +down_m20240515_rcv_files_user_approved_relays = + [sql| +ALTER TABLE rcv_files DROP COLUMN user_approved_relays; +|] diff --git a/src/Simplex/Chat/Migrations/M20240528_quota_err_counter.hs b/src/Simplex/Chat/Migrations/M20240528_quota_err_counter.hs new file mode 100644 index 0000000000..ea1f3a78e7 --- /dev/null +++ b/src/Simplex/Chat/Migrations/M20240528_quota_err_counter.hs @@ -0,0 +1,18 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Simplex.Chat.Migrations.M20240528_quota_err_counter where + +import Database.SQLite.Simple (Query) +import Database.SQLite.Simple.QQ (sql) + +m20240528_quota_err_counter :: Query +m20240528_quota_err_counter = + [sql| +ALTER TABLE connections ADD COLUMN quota_err_counter INTEGER NOT NULL DEFAULT 0; +|] + +down_m20240528_quota_err_counter :: Query +down_m20240528_quota_err_counter = + [sql| +ALTER TABLE connections DROP COLUMN quota_err_counter; +|] diff --git a/src/Simplex/Chat/Migrations/chat_schema.sql b/src/Simplex/Chat/Migrations/chat_schema.sql index f2f94d019c..fdbc44a9c3 100644 --- a/src/Simplex/Chat/Migrations/chat_schema.sql +++ b/src/Simplex/Chat/Migrations/chat_schema.sql @@ -76,6 +76,7 @@ CREATE TABLE contacts( contact_status TEXT NOT NULL DEFAULT 'active', custom_data BLOB, ui_themes TEXT, + chat_deleted INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(user_id, local_display_name) REFERENCES display_names(user_id, local_display_name) ON DELETE CASCADE @@ -228,7 +229,8 @@ CREATE TABLE rcv_files( REFERENCES xftp_file_descriptions ON DELETE SET NULL, agent_rcv_file_id BLOB NULL, agent_rcv_file_deleted INTEGER DEFAULT 0 CHECK(agent_rcv_file_deleted NOT NULL), - to_receive INTEGER + to_receive INTEGER, + user_approved_relays INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE snd_file_chunks( file_id INTEGER NOT NULL, @@ -287,6 +289,7 @@ CREATE TABLE connections( pq_encryption INTEGER NOT NULL DEFAULT 0, pq_snd_enabled INTEGER, pq_rcv_enabled INTEGER, + quota_err_counter INTEGER NOT NULL DEFAULT 0, FOREIGN KEY(snd_file_id, connection_id) REFERENCES snd_files(file_id, connection_id) ON DELETE CASCADE @@ -391,7 +394,8 @@ CREATE TABLE chat_items( fwd_from_msg_dir INTEGER, fwd_from_contact_id INTEGER REFERENCES contacts ON DELETE SET NULL, fwd_from_group_id INTEGER REFERENCES groups ON DELETE SET NULL, - fwd_from_chat_item_id INTEGER REFERENCES chat_items ON DELETE SET NULL + fwd_from_chat_item_id INTEGER REFERENCES chat_items ON DELETE SET NULL, + via_proxy INTEGER ); CREATE TABLE chat_item_messages( chat_item_id INTEGER NOT NULL REFERENCES chat_items ON DELETE CASCADE, @@ -502,6 +506,8 @@ CREATE TABLE group_snd_item_statuses( group_snd_item_status TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT(datetime('now')), updated_at TEXT NOT NULL DEFAULT(datetime('now')) + , + via_proxy INTEGER ); CREATE TABLE IF NOT EXISTS "sent_probes"( sent_probe_id INTEGER PRIMARY KEY, diff --git a/src/Simplex/Chat/Mobile.hs b/src/Simplex/Chat/Mobile.hs index 5883c6042c..57b0ee6c17 100644 --- a/src/Simplex/Chat/Mobile.hs +++ b/src/Simplex/Chat/Mobile.hs @@ -48,7 +48,6 @@ import Simplex.Chat.Types import Simplex.Messaging.Agent.Client (agentClientStore) import Simplex.Messaging.Agent.Env.SQLite (createAgentStore) import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), MigrationError, closeSQLiteStore, reopenSQLiteStore) -import Simplex.Messaging.Client (defaultNetworkConfig) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, sumTypeJSON) @@ -192,14 +191,15 @@ mobileChatOpts dbFilePrefix = dbKey = "", -- for API database is already opened, and the key in options is not used smpServers = [], xftpServers = [], - networkConfig = defaultNetworkConfig, + simpleNetCfg = defaultSimpleNetCfg, logLevel = CLLImportant, logConnections = False, logServerHosts = True, logAgent = Nothing, logFile = Nothing, tbqSize = 1024, - highlyAvailable = False + highlyAvailable = False, + yesToUpMigrations = False }, deviceName = Nothing, chatCmd = "", diff --git a/src/Simplex/Chat/Options.hs b/src/Simplex/Chat/Options.hs index 871e3358ec..0b5961b042 100644 --- a/src/Simplex/Chat/Options.hs +++ b/src/Simplex/Chat/Options.hs @@ -13,7 +13,6 @@ module Simplex.Chat.Options coreChatOptsP, getChatOpts, protocolServersP, - fullNetworkConfig, ) where @@ -22,15 +21,17 @@ import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteArray (ScrubbedBytes) import qualified Data.ByteString.Char8 as B import Data.Text (Text) +import qualified Data.Text as T +import Data.Text.Encoding (encodeUtf8) import Numeric.Natural (Natural) import Options.Applicative -import Simplex.Chat.Controller (ChatLogLevel (..), updateStr, versionNumber, versionString) +import Simplex.Chat.Controller (ChatLogLevel (..), SimpleNetCfg (..), updateStr, versionNumber, versionString) import Simplex.FileTransfer.Description (mb) -import Simplex.Messaging.Client (NetworkConfig (..), defaultNetworkConfig) +import Simplex.Messaging.Client (SocksMode (..)) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (parseAll) import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI, SMPServerWithAuth, XFTPServerWithAuth) -import Simplex.Messaging.Transport.Client (SocksProxy, defaultSocksProxy) +import Simplex.Messaging.Transport.Client (defaultSocksProxy) import System.FilePath (combine) data ChatOpts = ChatOpts @@ -55,14 +56,15 @@ data CoreChatOpts = CoreChatOpts dbKey :: ScrubbedBytes, smpServers :: [SMPServerWithAuth], xftpServers :: [XFTPServerWithAuth], - networkConfig :: NetworkConfig, + simpleNetCfg :: SimpleNetCfg, logLevel :: ChatLogLevel, logConnections :: Bool, logServerHosts :: Bool, logAgent :: Maybe LogLevel, logFile :: Maybe FilePath, tbqSize :: Natural, - highlyAvailable :: Bool + highlyAvailable :: Bool, + yesToUpMigrations :: Bool } data ChatCmdLog = CCLAll | CCLMessages | CCLNone @@ -123,18 +125,42 @@ coreChatOptsP appDir defaultDbFileName = do socksProxy <- flag' (Just defaultSocksProxy) (short 'x' <> help "Use local SOCKS5 proxy at :9050") <|> option - parseSocksProxy + strParse ( long "socks-proxy" <> metavar "SOCKS5" <> help "Use SOCKS5 proxy at `ipv4:port` or `:port`" <> value Nothing ) + socksMode <- + option + strParse + ( long "socks-mode" + <> metavar "SOCKS_MODE" + <> help "Use SOCKS5 proxy: always (default), onion (with onion-only relays)" + <> value SMAlways + ) + smpProxyMode_ <- + optional $ + option + strParse + ( long "smp-proxy" + <> metavar "SMP_PROXY_MODE" + <> help "Use private message routing: always, unknown (default), unprotected, never" + ) + smpProxyFallback_ <- + optional $ + option + strParse + ( long "smp-proxy-fallback" + <> metavar "SMP_PROXY_FALLBACK_MODE" + <> help "Allow downgrade and connect directly: no, [when IP address is] protected (default), yes" + ) t <- option auto ( long "tcp-timeout" <> metavar "TIMEOUT" - <> help "TCP timeout, seconds (default: 5/10 without/with SOCKS5 proxy)" + <> help "TCP timeout, seconds (default: 7/15 without/with SOCKS5 proxy)" <> value 0 ) logLevel <- @@ -149,7 +175,7 @@ coreChatOptsP appDir defaultDbFileName = do logTLSErrors <- switch ( long "log-tls-errors" - <> help "Log TLS errors (also enabled with `-l debug`)" + <> help "Log TLS errors" ) logConnections <- switch @@ -188,23 +214,30 @@ coreChatOptsP appDir defaultDbFileName = do ( long "ha" <> help "Run as a highly available client (this may increase traffic in groups)" ) + yesToUpMigrations <- + switch + ( long "--yes-migrate" + <> short 'y' + <> help "Automatically confirm \"up\" database migrations" + ) pure CoreChatOpts { dbFilePrefix, dbKey, smpServers, xftpServers, - networkConfig = fullNetworkConfig socksProxy (useTcpTimeout socksProxy t) (logTLSErrors || logLevel == CLLDebug), + simpleNetCfg = SimpleNetCfg {socksProxy, socksMode, smpProxyMode_, smpProxyFallback_, tcpTimeout_ = Just $ useTcpTimeout socksProxy t, logTLSErrors}, logLevel, logConnections = logConnections || logLevel <= CLLInfo, logServerHosts = logServerHosts || logLevel <= CLLInfo, logAgent = if logAgent || logLevel == CLLDebug then Just $ agentLogLevel logLevel else Nothing, logFile, tbqSize, - highlyAvailable + highlyAvailable, + yesToUpMigrations } where - useTcpTimeout p t = 1000000 * if t > 0 then t else maybe 5 (const 10) p + useTcpTimeout p t = 1000000 * if t > 0 then t else maybe 7 (const 15) p defaultDbFilePath = combine appDir defaultDbFileName chatOptsP :: FilePath -> FilePath -> Parser ChatOpts @@ -321,16 +354,11 @@ chatOptsP appDir defaultDbFileName = do maintenance } -fullNetworkConfig :: Maybe SocksProxy -> Int -> Bool -> NetworkConfig -fullNetworkConfig socksProxy tcpTimeout logTLSErrors = - let tcpConnectTimeout = (tcpTimeout * 3) `div` 2 - in defaultNetworkConfig {socksProxy, tcpTimeout, tcpConnectTimeout, logTLSErrors} - parseProtocolServers :: ProtocolTypeI p => ReadM [ProtoServerWithAuth p] parseProtocolServers = eitherReader $ parseAll protocolServersP . B.pack -parseSocksProxy :: ReadM (Maybe SocksProxy) -parseSocksProxy = eitherReader $ parseAll strP . B.pack +strParse :: StrEncoding a => ReadM a +strParse = eitherReader $ parseAll strP . encodeUtf8 . T.pack parseServerPort :: ReadM (Maybe String) parseServerPort = eitherReader $ parseAll serverPortP . B.pack diff --git a/src/Simplex/Chat/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 8c5a9e1905..6d4b2eda68 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -31,8 +31,9 @@ import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import Data.ByteString.Internal (c2w, w2c) import qualified Data.ByteString.Lazy.Char8 as LB +import Data.List.NonEmpty (NonEmpty (..)) import qualified Data.List.NonEmpty as L -import Data.Maybe (fromMaybe) +import Data.Maybe (fromMaybe, mapMaybe) import Data.String import Data.Text (Text) import qualified Data.Text as T @@ -46,14 +47,13 @@ import Database.SQLite.Simple.ToField (ToField (..)) import Simplex.Chat.Call import Simplex.Chat.Types import Simplex.Chat.Types.Shared -import Simplex.Chat.Types.Util import Simplex.Messaging.Agent.Protocol (VersionSMPA, pqdrSMPAgentVersion) import Simplex.Messaging.Compression (Compressed, compress1, decompress1) import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, fromTextField_, fstToLower, parseAll, sumTypeJSON, taggedObjectJSON) import Simplex.Messaging.Protocol (MsgBody) -import Simplex.Messaging.Util (eitherToMaybe, safeDecodeUtf8, (<$?>)) +import Simplex.Messaging.Util (decodeJSON, eitherToMaybe, encodeJSON, safeDecodeUtf8, (<$?>)) import Simplex.Messaging.Version hiding (version) -- Chat version history: @@ -64,12 +64,14 @@ import Simplex.Messaging.Version hiding (version) -- 5 - batch sending messages (12/23/2023) -- 6 - send group welcome message after history (12/29/2023) -- 7 - update member profiles (1/15/2024) +-- 8 - compress messages and PQ e2e encryption (2024-03-08) +-- 9 - batch sending in direct connections (2024-07-24) -- This should not be used directly in code, instead use `maxVersion chatVRange` from ChatConfig. -- This indirection is needed for backward/forward compatibility testing. -- Testing with real app versions is still needed, as tests use the current code with different version ranges, not the old code. currentChatVersion :: VersionChat -currentChatVersion = VersionChat 8 +currentChatVersion = VersionChat 9 -- This should not be used directly in code, instead use `chatVRange` from ChatConfig (see comment above) supportedChatVRange :: VersionRangeChat @@ -104,6 +106,10 @@ memberProfileUpdateVersion = VersionChat 7 pqEncryptionCompressionVersion :: VersionChat pqEncryptionCompressionVersion = VersionChat 8 +-- version range that supports batch sending in direct connections, and forwarding batched messages in groups +batchSend2Version :: VersionChat +batchSend2Version = VersionChat 9 + agentToChatVersion :: VersionSMPA -> VersionChat agentToChatVersion v | v < pqdrSMPAgentVersion = initialChatVersion @@ -324,13 +330,20 @@ forwardedGroupMsg msg@ChatMessage {chatMsgEvent} = case encoding @e of SJson | isForwardedGroupMsg chatMsgEvent -> Just msg _ -> Nothing --- applied after checking forwardedGroupMsg and building list of group members to forward to, see Chat -forwardedToGroupMembers :: forall e. MsgEncodingI e => [GroupMember] -> ChatMessage e -> [GroupMember] -forwardedToGroupMembers ms ChatMessage {chatMsgEvent} = case encoding @e of - SJson -> case chatMsgEvent of - XGrpMemRestrict mId _ -> filter (\GroupMember {memberId} -> memberId /= mId) ms - _ -> ms - _ -> [] +-- applied after checking forwardedGroupMsg and building list of group members to forward to, see Chat; +-- this filters out members if any of forwarded events in batch is an XGrpMemRestrict event referring to them, +-- but practically XGrpMemRestrict is not batched with other events so it wouldn't prevent forwarding of other events +-- to these members +forwardedToGroupMembers :: forall e. MsgEncodingI e => [GroupMember] -> NonEmpty (ChatMessage e) -> [GroupMember] +forwardedToGroupMembers ms forwardedMsgs = + filter (\GroupMember {memberId} -> memberId `notElem` restrictMemberIds) ms + where + restrictMemberIds = mapMaybe restrictMemberId $ L.toList forwardedMsgs + restrictMemberId ChatMessage {chatMsgEvent} = case encoding @e of + SJson -> case chatMsgEvent of + XGrpMemRestrict mId _ -> Just mId + _ -> Nothing + _ -> Nothing data MsgReaction = MREmoji {emoji :: MREmojiChar} | MRUnknown {tag :: Text, json :: J.Object} deriving (Eq, Show) diff --git a/src/Simplex/Chat/Remote.hs b/src/Simplex/Chat/Remote.hs index e8d13402ef..276baf56e8 100644 --- a/src/Simplex/Chat/Remote.hs +++ b/src/Simplex/Chat/Remote.hs @@ -72,11 +72,11 @@ import UnliftIO.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExis -- when acting as host minRemoteCtrlVersion :: AppVersion -minRemoteCtrlVersion = AppVersion [5, 7, 0, 3] +minRemoteCtrlVersion = AppVersion [6, 0, 0, 4] -- when acting as controller minRemoteHostVersion :: AppVersion -minRemoteHostVersion = AppVersion [5, 7, 0, 3] +minRemoteHostVersion = AppVersion [6, 0, 0, 4] currentAppVersion :: AppVersion currentAppVersion = AppVersion SC.version diff --git a/src/Simplex/Chat/Stats.hs b/src/Simplex/Chat/Stats.hs new file mode 100644 index 0000000000..6dd5c79ab1 --- /dev/null +++ b/src/Simplex/Chat/Stats.hs @@ -0,0 +1,354 @@ +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE TemplateHaskell #-} +{-# OPTIONS_GHC -fno-warn-ambiguous-fields #-} + +module Simplex.Chat.Stats where + +import qualified Data.Aeson.TH as J +import Data.List (partition) +import Data.List.NonEmpty (NonEmpty) +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as M +import Data.Maybe (fromMaybe, isJust) +import Data.Time.Clock (UTCTime) +import Simplex.Chat.Types +import Simplex.Messaging.Agent.Client +import Simplex.Messaging.Agent.Protocol (UserId) +import Simplex.Messaging.Agent.Stats +import Simplex.Messaging.Parsers (defaultJSON) +import Simplex.Messaging.Protocol + +data PresentedServersSummary = PresentedServersSummary + { statsStartedAt :: UTCTime, + allUsersSMP :: SMPServersSummary, + allUsersXFTP :: XFTPServersSummary, + allUsersNtf :: NtfServersSummary, + currentUserSMP :: SMPServersSummary, + currentUserXFTP :: XFTPServersSummary, + currentUserNtf :: NtfServersSummary + } + deriving (Show) + +-- Presentation of servers will be split into separate categories, +-- so users can differentiate currently used (connected) servers, +-- previously connected servers that were in use in previous sessions, +-- and servers that are only proxied (not connected directly). +data SMPServersSummary = SMPServersSummary + { -- SMP totals are calculated from all accounted SMP server summaries + smpTotals :: SMPTotals, + -- currently used SMP servers are those with Just in sessions and/or subs in SMPServerSummary; + -- all other servers would fall either into previously used or only proxied servers category + currentlyUsedSMPServers :: [SMPServerSummary], + -- previously used SMP servers are those with Nothing in sessions and subs, + -- and have any of sentDirect, sentProxied, recvMsgs, etc. > 0 in server stats (see toPresentedServersSummary); + -- remaining servers would fall into only proxied servers category + previouslyUsedSMPServers :: [SMPServerSummary], + -- only proxied SMP servers are those that aren't (according to current state - sessions and subs) + -- and weren't (according to stats) connected directly; they would have Nothing in sessions and subs, + -- and have all of sentDirect, sentProxied, recvMsgs, etc. = 0 in server stats + onlyProxiedSMPServers :: [SMPServerSummary] + } + deriving (Show) + +data SMPTotals = SMPTotals + { sessions :: ServerSessions, + subs :: SMPServerSubs, + stats :: AgentSMPServerStatsData + } + deriving (Show) + +data SMPServerSummary = SMPServerSummary + { smpServer :: SMPServer, + -- known: + -- for simplicity always Nothing in totalServersSummary - allows us to load configured servers only for current user, + -- and also unnecessary unless we want to add navigation to other users servers settings; + -- always Just in currentUserServers - True if server is in list of user servers, otherwise False; + -- True - allows to navigate to server settings, False - allows to add server to configured as known (SEKnown) + known :: Maybe Bool, + sessions :: Maybe ServerSessions, + subs :: Maybe SMPServerSubs, + -- stats: + -- even if sessions and subs are Nothing, stats can be Just - server could be used earlier in session, + -- or in previous sessions and stats for it were restored; server would fall into a category of + -- previously used or only proxied servers - see ServersSummary above + stats :: Maybe AgentSMPServerStatsData + } + deriving (Show) + +data XFTPServersSummary = XFTPServersSummary + { -- XFTP totals are calculated from all accounted XFTP server summaries + xftpTotals :: XFTPTotals, + -- currently used XFTP servers are those with Just in sessions in XFTPServerSummary, + -- and/or have upload/download/deletion in progress; + -- all other servers would fall into previously used servers category + currentlyUsedXFTPServers :: [XFTPServerSummary], + -- previously used XFTP servers are those with Nothing in sessions and don't have any process in progress + previouslyUsedXFTPServers :: [XFTPServerSummary] + } + deriving (Show) + +data XFTPTotals = XFTPTotals + { sessions :: ServerSessions, + stats :: AgentXFTPServerStatsData + } + deriving (Show) + +data XFTPServerSummary = XFTPServerSummary + { xftpServer :: XFTPServer, + known :: Maybe Bool, -- same as for SMPServerSummary + sessions :: Maybe ServerSessions, + stats :: Maybe AgentXFTPServerStatsData, + rcvInProgress :: Bool, + sndInProgress :: Bool, + delInProgress :: Bool + } + deriving (Show) + +data NtfServersSummary = NtfServersSummary + { ntfTotals :: NtfTotals, + currentlyUsedNtfServers :: [NtfServerSummary], + previouslyUsedNtfServers :: [NtfServerSummary] + } + deriving (Show) + +data NtfTotals = NtfTotals + { sessions :: ServerSessions, + stats :: AgentNtfServerStatsData + } + deriving (Show) + +data NtfServerSummary = NtfServerSummary + { ntfServer :: NtfServer, + known :: Maybe Bool, + sessions :: Maybe ServerSessions, + stats :: Maybe AgentNtfServerStatsData + } + deriving (Show) + +-- Maps AgentServersSummary to PresentedServersSummary: +-- - currentUserServers is for currentUser; +-- - users are passed to exclude hidden users from totalServersSummary; +-- - if currentUser is hidden, it should be accounted in totalServersSummary; +-- - known is set only in user level summaries based on passed userSMPSrvs and userXFTPSrvs +toPresentedServersSummary :: AgentServersSummary -> [User] -> User -> NonEmpty SMPServer -> NonEmpty XFTPServer -> [NtfServer] -> PresentedServersSummary +toPresentedServersSummary agentSummary users currentUser userSMPSrvs userXFTPSrvs userNtfSrvs = do + let (userSMPSrvsSumms, allSMPSrvsSumms) = accSMPSrvsSummaries + (userSMPCurr, userSMPPrev, userSMPProx) = smpSummsIntoCategories userSMPSrvsSumms + (allSMPCurr, allSMPPrev, allSMPProx) = smpSummsIntoCategories allSMPSrvsSumms + let (userXFTPSrvsSumms, allXFTPSrvsSumms) = accXFTPSrvsSummaries + (userXFTPCurr, userXFTPPrev) = xftpSummsIntoCategories userXFTPSrvsSumms + (allXFTPCurr, allXFTPPrev) = xftpSummsIntoCategories allXFTPSrvsSumms + let (userNtfSrvsSumms, allNtfSrvsSumms) = accNtfSrvsSummaries + (userNtfCurr, userNtfPrev) = ntfSummsIntoCategories userNtfSrvsSumms + (allNtfCurr, allNtfPrev) = ntfSummsIntoCategories allNtfSrvsSumms + PresentedServersSummary + { statsStartedAt, + allUsersSMP = + SMPServersSummary + { smpTotals = accSMPTotals allSMPSrvsSumms, + currentlyUsedSMPServers = allSMPCurr, + previouslyUsedSMPServers = allSMPPrev, + onlyProxiedSMPServers = allSMPProx + }, + allUsersXFTP = + XFTPServersSummary + { xftpTotals = accXFTPTotals allXFTPSrvsSumms, + currentlyUsedXFTPServers = allXFTPCurr, + previouslyUsedXFTPServers = allXFTPPrev + }, + allUsersNtf = + NtfServersSummary + { ntfTotals = accNtfTotals allNtfSrvsSumms, + currentlyUsedNtfServers = allNtfCurr, + previouslyUsedNtfServers = allNtfPrev + }, + currentUserSMP = + SMPServersSummary + { smpTotals = accSMPTotals userSMPSrvsSumms, + currentlyUsedSMPServers = userSMPCurr, + previouslyUsedSMPServers = userSMPPrev, + onlyProxiedSMPServers = userSMPProx + }, + currentUserXFTP = + XFTPServersSummary + { xftpTotals = accXFTPTotals userXFTPSrvsSumms, + currentlyUsedXFTPServers = userXFTPCurr, + previouslyUsedXFTPServers = userXFTPPrev + }, + currentUserNtf = + NtfServersSummary + { ntfTotals = accNtfTotals userNtfSrvsSumms, + currentlyUsedNtfServers = userNtfCurr, + previouslyUsedNtfServers = userNtfPrev + } + } + where + AgentServersSummary {statsStartedAt, smpServersSessions, smpServersSubs, smpServersStats, xftpServersSessions, xftpServersStats, xftpRcvInProgress, xftpSndInProgress, xftpDelInProgress, ntfServersSessions, ntfServersStats} = agentSummary + countUserInAll auId = countUserInAllStats (AgentUserId auId) currentUser users + accSMPTotals :: Map SMPServer SMPServerSummary -> SMPTotals + accSMPTotals = M.foldr' addTotals initialTotals + where + initialTotals = SMPTotals {sessions = ServerSessions 0 0 0, subs = SMPServerSubs 0 0, stats = newAgentSMPServerStatsData} + addTotals SMPServerSummary {sessions, subs, stats} SMPTotals {sessions = accSess, subs = accSubs, stats = accStats} = + SMPTotals + { sessions = maybe accSess (accSess `addServerSessions`) sessions, + subs = maybe accSubs (accSubs `addSMPSubs`) subs, + stats = maybe accStats (accStats `addSMPStatsData`) stats + } + accXFTPTotals :: Map XFTPServer XFTPServerSummary -> XFTPTotals + accXFTPTotals = M.foldr' addTotals initialTotals + where + initialTotals = XFTPTotals {sessions = ServerSessions 0 0 0, stats = newAgentXFTPServerStatsData} + addTotals XFTPServerSummary {sessions, stats} XFTPTotals {sessions = accSess, stats = accStats} = + XFTPTotals + { sessions = maybe accSess (accSess `addServerSessions`) sessions, + stats = maybe accStats (accStats `addXFTPStatsData`) stats + } + accNtfTotals :: Map NtfServer NtfServerSummary -> NtfTotals + accNtfTotals = M.foldr' addTotals initialTotals + where + initialTotals = NtfTotals {sessions = ServerSessions 0 0 0, stats = newAgentNtfServerStatsData} + addTotals NtfServerSummary {sessions, stats} NtfTotals {sessions = accSess, stats = accStats} = + NtfTotals + { sessions = maybe accSess (accSess `addServerSessions`) sessions, + stats = maybe accStats (accStats `addNtfStatsData`) stats + } + smpSummsIntoCategories :: Map SMPServer SMPServerSummary -> ([SMPServerSummary], [SMPServerSummary], [SMPServerSummary]) + smpSummsIntoCategories = M.foldr' addSummary ([], [], []) + where + addSummary srvSumm (curr, prev, prox) + | isCurrentlyUsed srvSumm = (srvSumm : curr, prev, prox) + | isPreviouslyUsed srvSumm = (curr, srvSumm : prev, prox) + | otherwise = (curr, prev, srvSumm : prox) + isCurrentlyUsed SMPServerSummary {sessions, subs} = isJust sessions || isJust subs + isPreviouslyUsed SMPServerSummary {stats} = case stats of + Nothing -> False + -- add connCompleted, connDeleted? + -- check: should connCompleted be counted for proxy? is it? + Just AgentSMPServerStatsData {_sentDirect, _sentProxied, _sentDirectAttempts, _sentProxiedAttempts, _recvMsgs, _connCreated, _connSecured, _connSubscribed, _connSubAttempts} -> + _sentDirect > 0 || _sentProxied > 0 || _sentDirectAttempts > 0 || _sentProxiedAttempts > 0 || _recvMsgs > 0 || _connCreated > 0 || _connSecured > 0 || _connSubscribed > 0 || _connSubAttempts > 0 + xftpSummsIntoCategories :: Map XFTPServer XFTPServerSummary -> ([XFTPServerSummary], [XFTPServerSummary]) + xftpSummsIntoCategories = partition isCurrentlyUsed . M.elems + where + isCurrentlyUsed XFTPServerSummary {sessions, rcvInProgress, sndInProgress, delInProgress} = + isJust sessions || rcvInProgress || sndInProgress || delInProgress + ntfSummsIntoCategories :: Map NtfServer NtfServerSummary -> ([NtfServerSummary], [NtfServerSummary]) + ntfSummsIntoCategories = partition isCurrentlyUsed . M.elems + where + isCurrentlyUsed NtfServerSummary {sessions} = isJust sessions + accSMPSrvsSummaries :: (Map SMPServer SMPServerSummary, Map SMPServer SMPServerSummary) + accSMPSrvsSummaries = M.foldrWithKey' (addServerData addStats) summs2 smpServersStats + where + summs1 = M.foldrWithKey' (addServerData addSessions) (M.empty, M.empty) smpServersSessions + summs2 = M.foldrWithKey' (addServerData addSubs) summs1 smpServersSubs + addServerData = addServerData_ newSummary newUserSummary + newUserSummary srv = (newSummary srv :: SMPServerSummary) {known = Just $ srv `elem` userSMPSrvs} + newSummary srv = + SMPServerSummary + { smpServer = srv, + known = Nothing, + sessions = Nothing, + subs = Nothing, + stats = Nothing + } + addSessions :: ServerSessions -> SMPServerSummary -> SMPServerSummary + addSessions s summ@SMPServerSummary {sessions} = summ {sessions = Just $ maybe s (s `addServerSessions`) sessions} + addSubs :: SMPServerSubs -> SMPServerSummary -> SMPServerSummary + addSubs s summ@SMPServerSummary {subs} = summ {subs = Just $ maybe s (s `addSMPSubs`) subs} + addStats :: AgentSMPServerStatsData -> SMPServerSummary -> SMPServerSummary + addStats s summ@SMPServerSummary {stats} = summ {stats = Just $ maybe s (s `addSMPStatsData`) stats} + accXFTPSrvsSummaries :: (Map XFTPServer XFTPServerSummary, Map XFTPServer XFTPServerSummary) + accXFTPSrvsSummaries = M.foldrWithKey' (addServerData addStats) summs1 xftpServersStats + where + summs1 = M.foldrWithKey' (addServerData addSessions) (M.empty, M.empty) xftpServersSessions + addServerData = addServerData_ newSummary newUserSummary + addSessions :: ServerSessions -> XFTPServerSummary -> XFTPServerSummary + addSessions s summ@XFTPServerSummary {sessions} = summ {sessions = Just $ maybe s (s `addServerSessions`) sessions} + addStats :: AgentXFTPServerStatsData -> XFTPServerSummary -> XFTPServerSummary + addStats s summ@XFTPServerSummary {stats} = summ {stats = Just $ maybe s (s `addXFTPStatsData`) stats} + newUserSummary srv = (newSummary srv :: XFTPServerSummary) {known = Just $ srv `elem` userXFTPSrvs} + newSummary srv = + XFTPServerSummary + { xftpServer = srv, + known = Nothing, + sessions = Nothing, + stats = Nothing, + rcvInProgress = srv `elem` xftpRcvInProgress, + sndInProgress = srv `elem` xftpSndInProgress, + delInProgress = srv `elem` xftpDelInProgress + } + accNtfSrvsSummaries :: (Map NtfServer NtfServerSummary, Map NtfServer NtfServerSummary) + accNtfSrvsSummaries = M.foldrWithKey' (addServerData addStats) summs1 ntfServersStats + where + summs1 = M.foldrWithKey' (addServerData addSessions) (M.empty, M.empty) ntfServersSessions + addServerData = addServerData_ newSummary newUserSummary + addSessions :: ServerSessions -> NtfServerSummary -> NtfServerSummary + addSessions s summ@NtfServerSummary {sessions} = summ {sessions = Just $ maybe s (s `addServerSessions`) sessions} + addStats :: AgentNtfServerStatsData -> NtfServerSummary -> NtfServerSummary + addStats s summ@NtfServerSummary {stats} = summ {stats = Just $ maybe s (s `addNtfStatsData`) stats} + newUserSummary srv = (newSummary srv :: NtfServerSummary) {known = Just $ srv `elem` userNtfSrvs} + newSummary srv = + NtfServerSummary + { ntfServer = srv, + known = Nothing, + sessions = Nothing, + stats = Nothing + } + addServerData_ :: + (ProtocolServer p -> s) -> + (ProtocolServer p -> s) -> + (a -> s -> s) -> + (UserId, ProtocolServer p) -> + a -> + (Map (ProtocolServer p) s, Map (ProtocolServer p) s) -> + (Map (ProtocolServer p) s, Map (ProtocolServer p) s) + addServerData_ newSummary newUserSummary addData (userId, srv) d (userSumms, allUsersSumms) = (userSumms', allUsersSumms') + where + userSumms' + | userId == aUserId currentUser = alterSumms (newUserSummary srv) userSumms + | otherwise = userSumms + allUsersSumms' + | countUserInAll userId = alterSumms (newSummary srv) allUsersSumms + | otherwise = allUsersSumms + alterSumms n = M.alter (Just . addData d . fromMaybe n) srv + addServerSessions :: ServerSessions -> ServerSessions -> ServerSessions + addServerSessions ss1 ss2 = + ServerSessions + { ssConnected = ssConnected ss1 + ssConnected ss2, + ssErrors = ssErrors ss1 + ssErrors ss2, + ssConnecting = ssConnecting ss1 + ssConnecting ss2 + } + +countUserInAllStats :: AgentUserId -> User -> [User] -> Bool +countUserInAllStats (AgentUserId auId) currentUser users = + auId == aUserId currentUser || auId `notElem` hiddenUserIds + where + hiddenUserIds = map aUserId $ filter (isJust . viewPwdHash) users + +addSMPSubs :: SMPServerSubs -> SMPServerSubs -> SMPServerSubs +addSMPSubs ss1 ss2 = + SMPServerSubs + { ssActive = ssActive ss1 + ssActive ss2, + ssPending = ssPending ss1 + ssPending ss2 + } + +$(J.deriveJSON defaultJSON ''SMPTotals) + +$(J.deriveJSON defaultJSON ''SMPServerSummary) + +$(J.deriveJSON defaultJSON ''SMPServersSummary) + +$(J.deriveJSON defaultJSON ''XFTPTotals) + +$(J.deriveJSON defaultJSON ''XFTPServerSummary) + +$(J.deriveJSON defaultJSON ''XFTPServersSummary) + +$(J.deriveJSON defaultJSON ''NtfTotals) + +$(J.deriveJSON defaultJSON ''NtfServerSummary) + +$(J.deriveJSON defaultJSON ''NtfServersSummary) + +$(J.deriveJSON defaultJSON ''PresentedServersSummary) diff --git a/src/Simplex/Chat/Store/Connections.hs b/src/Simplex/Chat/Store/Connections.hs index bae9d00bfd..2c7543f08a 100644 --- a/src/Simplex/Chat/Store/Connections.hs +++ b/src/Simplex/Chat/Store/Connections.hs @@ -14,6 +14,7 @@ module Simplex.Chat.Store.Connections getContactConnEntityByConnReqHash, getConnectionsToSubscribe, unsetConnectionToSubscribe, + deleteConnectionRecord, ) where @@ -84,7 +85,7 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do [sql| SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, - created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter, + created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter, quota_err_counter, conn_chat_version, peer_chat_min_version, peer_chat_max_version FROM connections WHERE user_id = ? AND agent_conn_id = ? @@ -98,19 +99,19 @@ getConnectionEntity db vr user@User {userId, userContactId} agentConnId = do [sql| SELECT c.contact_profile_id, c.local_display_name, c.via_group, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, c.contact_used, c.contact_status, c.enable_ntfs, c.send_rcpts, c.favorite, - p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.contact_group_member_id, c.contact_grp_inv_sent, c.ui_themes, c.custom_data + p.preferences, c.user_preferences, c.created_at, c.updated_at, c.chat_ts, c.contact_group_member_id, c.contact_grp_inv_sent, c.ui_themes, c.chat_deleted, c.custom_data FROM contacts c JOIN contact_profiles p ON c.contact_profile_id = p.contact_profile_id WHERE c.user_id = ? AND c.contact_id = ? AND c.deleted = 0 |] (userId, contactId) toContact' :: Int64 -> Connection -> [ContactRow'] -> Either StoreError Contact - toContact' contactId conn [(profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, uiThemes, customData)] = + toContact' contactId conn [(profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. (contactGroupMemberId, contactGrpInvSent, uiThemes, chatDeleted, customData)] = let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite} mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn activeConn = Just conn - in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, uiThemes, customData} + in Right Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, uiThemes, chatDeleted, customData} toContact' _ _ _ = Left $ SEInternalError "referenced contact not found" getGroupAndMember_ :: Int64 -> Connection -> ExceptT StoreError IO (GroupInfo, GroupMember) getGroupAndMember_ groupMemberId c = ExceptT $ do @@ -225,3 +226,7 @@ getConnectionsToSubscribe db vr = do unsetConnectionToSubscribe :: DB.Connection -> IO () unsetConnectionToSubscribe db = DB.execute_ db "UPDATE connections SET to_subscribe = 0 WHERE to_subscribe = 1" + +deleteConnectionRecord :: DB.Connection -> User -> Int64 -> IO () +deleteConnectionRecord db User {userId} cId = do + DB.execute db "DELETE FROM connections WHERE user_id = ? AND connection_id = ?" (userId, cId) diff --git a/src/Simplex/Chat/Store/Direct.hs b/src/Simplex/Chat/Store/Direct.hs index deb0f9fc4c..c0f007b6ac 100644 --- a/src/Simplex/Chat/Store/Direct.hs +++ b/src/Simplex/Chat/Store/Direct.hs @@ -30,7 +30,8 @@ module Simplex.Chat.Store.Direct getConnReqContactXContactId, getContactByConnReqHash, createDirectContact, - deleteContactConnectionsAndFiles, + deleteContactConnections, + deleteContactFiles, deleteContact, deleteContactWithoutGroups, setContactDeleted, @@ -50,8 +51,10 @@ module Simplex.Chat.Store.Direct updateContactStatus, updateGroupUnreadChat, setConnectionVerified, - incConnectionAuthErrCounter, - setConnectionAuthErrCounter, + incAuthErrCounter, + setAuthErrCounter, + incQuotaErrCounter, + setQuotaErrCounter, getUserContacts, createOrUpdateContactRequest, getContactRequest', @@ -61,6 +64,7 @@ module Simplex.Chat.Store.Direct createAcceptedContact, getUserByContactRequestId, getPendingContactConnections, + updatePCCUser, getContactConnections, getConnectionById, getConnectionsContacts, @@ -70,6 +74,7 @@ module Simplex.Chat.Store.Direct resetContactConnInitiated, setContactCustomData, setContactUIThemes, + setContactChatDeleted, ) where @@ -128,11 +133,11 @@ deletePendingContactConnection db userId connId = |] (userId, connId, ConnContact) -createAddressContactConnection :: DB.Connection -> VersionRangeChat -> User -> Contact -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> ExceptT StoreError IO Contact +createAddressContactConnection :: DB.Connection -> VersionRangeChat -> User -> Contact -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> SubscriptionMode -> VersionChat -> PQSupport -> ExceptT StoreError IO (Int64, Contact) createAddressContactConnection db vr user@User {userId} Contact {contactId} acId cReqHash xContactId incognitoProfile subMode chatV pqSup = do PendingContactConnection {pccConnId} <- liftIO $ createConnReqConnection db userId acId cReqHash xContactId incognitoProfile Nothing subMode chatV pqSup liftIO $ DB.execute db "UPDATE connections SET contact_id = ? WHERE connection_id = ?" (contactId, pccConnId) - getContact db vr user contactId + (pccConnId,) <$> getContact db vr user contactId createConnReqConnection :: DB.Connection -> UserId -> ConnId -> ConnReqUriHash -> XContactId -> Maybe Profile -> Maybe GroupLinkId -> SubscriptionMode -> VersionChat -> PQSupport -> IO PendingContactConnection createConnReqConnection db userId acId cReqHash xContactId incognitoProfile groupLinkId subMode chatV pqSup = do @@ -178,10 +183,10 @@ getContactByConnReqHash db vr user@User {userId} cReqHash = SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.custom_data, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id @@ -241,12 +246,13 @@ createDirectContact db user@User {userId} conn@Connection {connId, localAlias} p chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False, - customData = Nothing, - uiThemes = Nothing + uiThemes = Nothing, + chatDeleted = False, + customData = Nothing } -deleteContactConnectionsAndFiles :: DB.Connection -> UserId -> Contact -> IO () -deleteContactConnectionsAndFiles db userId Contact {contactId} = do +deleteContactConnections :: DB.Connection -> User -> Contact -> IO () +deleteContactConnections db User {userId} Contact {contactId} = do DB.execute db [sql| @@ -258,6 +264,9 @@ deleteContactConnectionsAndFiles db userId Contact {contactId} = do ) |] (userId, contactId) + +deleteContactFiles :: DB.Connection -> User -> Contact -> IO () +deleteContactFiles db User {userId} Contact {contactId} = do DB.execute db "DELETE FROM files WHERE user_id = ? AND contact_id = ?" (userId, contactId) deleteContact :: DB.Connection -> User -> Contact -> ExceptT StoreError IO () @@ -416,6 +425,19 @@ updatePCCIncognito db User {userId} conn customUserProfileId = do (customUserProfileId, updatedAt, userId, pccConnId conn) pure (conn :: PendingContactConnection) {customUserProfileId, updatedAt} +updatePCCUser :: DB.Connection -> UserId -> PendingContactConnection -> UserId -> IO PendingContactConnection +updatePCCUser db userId conn newUserId = do + updatedAt <- getCurrentTime + DB.execute + db + [sql| + UPDATE connections + SET user_id = ?, custom_user_profile_id = NULL, updated_at = ? + WHERE user_id = ? AND connection_id = ? + |] + (newUserId, updatedAt, userId, pccConnId conn) + pure (conn :: PendingContactConnection) {customUserProfileId = Nothing, updatedAt} + deletePCCIncognitoProfile :: DB.Connection -> User -> ProfileId -> IO () deletePCCIncognitoProfile db User {userId} profileId = DB.execute @@ -467,19 +489,32 @@ setConnectionVerified db User {userId} connId code = do updatedAt <- getCurrentTime DB.execute db "UPDATE connections SET security_code = ?, security_code_verified_at = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (code, code $> updatedAt, updatedAt, userId, connId) -incConnectionAuthErrCounter :: DB.Connection -> User -> Connection -> IO Int -incConnectionAuthErrCounter db User {userId} Connection {connId, authErrCounter} = do +incAuthErrCounter :: DB.Connection -> User -> Connection -> IO Int +incAuthErrCounter db User {userId} Connection {connId, authErrCounter} = do updatedAt <- getCurrentTime (counter_ :: Maybe Int) <- maybeFirstRow fromOnly $ DB.query db "SELECT auth_err_counter FROM connections WHERE user_id = ? AND connection_id = ?" (userId, connId) let counter' = fromMaybe authErrCounter counter_ + 1 DB.execute db "UPDATE connections SET auth_err_counter = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (counter', updatedAt, userId, connId) pure counter' -setConnectionAuthErrCounter :: DB.Connection -> User -> Connection -> Int -> IO () -setConnectionAuthErrCounter db User {userId} Connection {connId} counter = do +setAuthErrCounter :: DB.Connection -> User -> Connection -> Int -> IO () +setAuthErrCounter db User {userId} Connection {connId} counter = do updatedAt <- getCurrentTime DB.execute db "UPDATE connections SET auth_err_counter = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (counter, updatedAt, userId, connId) +incQuotaErrCounter :: DB.Connection -> User -> Connection -> IO Int +incQuotaErrCounter db User {userId} Connection {connId, quotaErrCounter} = do + updatedAt <- getCurrentTime + (counter_ :: Maybe Int) <- maybeFirstRow fromOnly $ DB.query db "SELECT quota_err_counter FROM connections WHERE user_id = ? AND connection_id = ?" (userId, connId) + let counter' = fromMaybe quotaErrCounter counter_ + 1 + DB.execute db "UPDATE connections SET quota_err_counter = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (counter', updatedAt, userId, connId) + pure counter' + +setQuotaErrCounter :: DB.Connection -> User -> Connection -> Int -> IO () +setQuotaErrCounter db User {userId} Connection {connId} counter = do + updatedAt <- getCurrentTime + DB.execute db "UPDATE connections SET quota_err_counter = ?, updated_at = ? WHERE user_id = ? AND connection_id = ?" (counter, updatedAt, userId, connId) + updateContactProfile_ :: DB.Connection -> UserId -> ProfileId -> Profile -> IO () updateContactProfile_ db userId profileId profile = do currentTs <- getCurrentTime @@ -600,10 +635,10 @@ createOrUpdateContactRequest db vr user@User {userId} userContactLinkId invId (V SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.custom_data, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id @@ -731,8 +766,8 @@ deleteContactRequest db User {userId} contactRequestId = do (userId, userId, contactRequestId, userId) DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND contact_request_id = ?" (userId, contactRequestId) -createAcceptedContact :: DB.Connection -> User -> ConnId -> VersionChat -> VersionRangeChat -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> PQSupport -> Bool -> IO Contact -createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId connChatVersion cReqChatVRange localDisplayName profileId profile userContactLinkId xContactId incognitoProfile subMode pqSup contactUsed = do +createAcceptedContact :: DB.Connection -> User -> ConnId -> ConnStatus -> VersionChat -> VersionRangeChat -> ContactName -> ProfileId -> Profile -> Int64 -> Maybe XContactId -> Maybe IncognitoProfile -> SubscriptionMode -> PQSupport -> Bool -> IO Contact +createAcceptedContact db user@User {userId, profile = LocalProfile {preferences}} agentConnId connStatus connChatVersion cReqChatVRange localDisplayName profileId profile userContactLinkId xContactId incognitoProfile subMode pqSup contactUsed = do DB.execute db "DELETE FROM contact_requests WHERE user_id = ? AND local_display_name = ?" (userId, localDisplayName) createdAt <- getCurrentTime customUserProfileId <- forM incognitoProfile $ \case @@ -744,7 +779,7 @@ createAcceptedContact db user@User {userId, profile = LocalProfile {preferences} "INSERT INTO contacts (user_id, local_display_name, contact_profile_id, enable_ntfs, user_preferences, created_at, updated_at, chat_ts, xcontact_id, contact_used) VALUES (?,?,?,?,?,?,?,?,?,?)" (userId, localDisplayName, profileId, True, userPreferences, createdAt, createdAt, createdAt, xContactId, contactUsed) contactId <- insertedRowId db - conn <- createConnection_ db userId ConnContact (Just contactId) agentConnId connChatVersion cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode pqSup + conn <- createConnection_ db userId ConnContact (Just contactId) agentConnId connStatus connChatVersion cReqChatVRange Nothing (Just userContactLinkId) customUserProfileId 0 createdAt subMode pqSup let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito conn pure $ Contact @@ -764,6 +799,7 @@ createAcceptedContact db user@User {userId, profile = LocalProfile {preferences} contactGroupMemberId = Nothing, contactGrpInvSent = False, uiThemes = Nothing, + chatDeleted = False, customData = Nothing } @@ -784,10 +820,10 @@ getContact_ db vr user@User {userId} contactId deleted = SELECT -- Contact ct.contact_id, ct.contact_profile_id, ct.local_display_name, ct.via_group, cp.display_name, cp.full_name, cp.image, cp.contact_link, cp.local_alias, ct.contact_used, ct.contact_status, ct.enable_ntfs, ct.send_rcpts, ct.favorite, - cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.custom_data, + cp.preferences, ct.user_preferences, ct.created_at, ct.updated_at, ct.chat_ts, ct.contact_group_member_id, ct.contact_grp_inv_sent, ct.ui_themes, ct.chat_deleted, ct.custom_data, -- Connection c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, - c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, + c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM contacts ct JOIN contact_profiles cp ON ct.contact_profile_id = cp.contact_profile_id @@ -841,7 +877,7 @@ getContactConnections db vr userId Contact {contactId} = [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, - c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM connections c JOIN contacts ct ON ct.contact_id = c.contact_id @@ -859,7 +895,7 @@ getConnectionById db vr User {userId} connId = ExceptT $ do [sql| SELECT connection_id, agent_conn_id, conn_level, via_contact, via_user_contact_link, via_group_link, group_link_id, custom_user_profile_id, conn_status, conn_type, contact_conn_initiated, local_alias, contact_id, group_member_id, snd_file_id, rcv_file_id, user_contact_link_id, - created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter, + created_at, security_code, security_code_verified_at, pq_support, pq_encryption, pq_snd_enabled, pq_rcv_enabled, auth_err_counter, quota_err_counter, conn_chat_version, peer_chat_min_version, peer_chat_max_version FROM connections WHERE user_id = ? AND connection_id = ? @@ -934,3 +970,8 @@ setContactUIThemes :: DB.Connection -> User -> Contact -> Maybe UIThemeEntityOve setContactUIThemes db User {userId} Contact {contactId} uiThemes = do updatedAt <- getCurrentTime DB.execute db "UPDATE contacts SET ui_themes = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" (uiThemes, updatedAt, userId, contactId) + +setContactChatDeleted :: DB.Connection -> User -> Contact -> Bool -> IO () +setContactChatDeleted db User {userId} Contact {contactId} chatDeleted = do + updatedAt <- getCurrentTime + DB.execute db "UPDATE contacts SET chat_deleted = ?, updated_at = ? WHERE user_id = ? AND contact_id = ?" (chatDeleted, updatedAt, userId, contactId) diff --git a/src/Simplex/Chat/Store/Files.hs b/src/Simplex/Chat/Store/Files.hs index 528290aa59..d1da081cee 100644 --- a/src/Simplex/Chat/Store/Files.hs +++ b/src/Simplex/Chat/Store/Files.hs @@ -455,7 +455,7 @@ lookupChatRefByFileId db User {userId} fileId = createSndFileConnection_ :: DB.Connection -> VersionRangeChat -> UserId -> Int64 -> ConnId -> SubscriptionMode -> IO Connection createSndFileConnection_ db vr userId fileId agentConnId subMode = do currentTs <- getCurrentTime - createConnection_ db userId ConnSndFile (Just fileId) agentConnId (minVersion vr) chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff + createConnection_ db userId ConnSndFile (Just fileId) agentConnId ConnNew (minVersion vr) chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff updateSndFileStatus :: DB.Connection -> SndFileTransfer -> FileStatus -> IO () updateSndFileStatus db SndFileTransfer {fileId, connId} status = do @@ -514,7 +514,7 @@ createRcvFileTransfer db userId Contact {contactId, localDisplayName = c} f@File rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr let rfdId = (\RcvFileDescr {fileDescrId} -> fileDescrId) <$> rfd_ -- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it - xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_ + xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False, userApprovedRelays = False}) <$> rfd_ fileProtocol = if isJust rfd_ then FPXFTP else FPSMP fileId <- liftIO $ do DB.execute @@ -535,7 +535,7 @@ createRcvGroupFileTransfer db userId GroupMember {groupId, groupMemberId, localD rfd_ <- mapM (createRcvFD_ db userId currentTs) fileDescr let rfdId = (\RcvFileDescr {fileDescrId} -> fileDescrId) <$> rfd_ -- cryptoArgs = Nothing here, the decision to encrypt is made when receiving it - xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False}) <$> rfd_ + xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId = Nothing, agentRcvFileDeleted = False, userApprovedRelays = False}) <$> rfd_ fileProtocol = if isJust rfd_ then FPXFTP else FPSMP fileId <- liftIO $ do DB.execute @@ -676,7 +676,9 @@ getRcvFileTransfer_ db userId fileId = do [sql| SELECT r.file_status, r.file_queue_info, r.group_member_id, f.file_name, f.file_size, f.chunk_size, f.cancelled, cs.local_display_name, m.local_display_name, - f.file_path, f.file_crypto_key, f.file_crypto_nonce, r.file_inline, r.rcv_file_inline, r.agent_rcv_file_id, r.agent_rcv_file_deleted, c.connection_id, c.agent_conn_id + f.file_path, f.file_crypto_key, f.file_crypto_nonce, r.file_inline, r.rcv_file_inline, + r.agent_rcv_file_id, r.agent_rcv_file_deleted, r.user_approved_relays, + c.connection_id, c.agent_conn_id FROM rcv_files r JOIN files f USING (file_id) LEFT JOIN connections c ON r.file_id = c.rcv_file_id @@ -690,9 +692,9 @@ getRcvFileTransfer_ db userId fileId = do where rcvFileTransfer :: Maybe RcvFileDescr -> - (FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe Bool) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe InlineFileMode, Maybe AgentRcvFileId, Bool) :. (Maybe Int64, Maybe AgentConnId) -> + (FileStatus, Maybe ConnReqInvitation, Maybe Int64, String, Integer, Integer, Maybe Bool) :. (Maybe ContactName, Maybe ContactName, Maybe FilePath, Maybe C.SbKey, Maybe C.CbNonce, Maybe InlineFileMode, Maybe InlineFileMode, Maybe AgentRcvFileId, Bool, Bool) :. (Maybe Int64, Maybe AgentConnId) -> ExceptT StoreError IO RcvFileTransfer - rcvFileTransfer rfd_ ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileKey, fileNonce, fileInline, rcvFileInline, agentRcvFileId, agentRcvFileDeleted) :. (connId_, agentConnId_)) = + rcvFileTransfer rfd_ ((fileStatus', fileConnReq, grpMemberId, fileName, fileSize, chunkSize, cancelled_) :. (contactName_, memberName_, filePath_, fileKey, fileNonce, fileInline, rcvFileInline, agentRcvFileId, agentRcvFileDeleted, userApprovedRelays) :. (connId_, agentConnId_)) = case contactName_ <|> memberName_ <|> standaloneName_ of Nothing -> throwError $ SERcvFileInvalid fileId Just name -> @@ -709,7 +711,7 @@ getRcvFileTransfer_ db userId fileId = do ft senderDisplayName fileStatus = let fileInvitation = FileInvitation {fileName, fileSize, fileDigest = Nothing, fileConnReq, fileInline, fileDescr = Nothing} cryptoArgs = CFArgs <$> fileKey <*> fileNonce - xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId, agentRcvFileDeleted}) <$> rfd_ + xftpRcvFile = (\rfd -> XFTPRcvFile {rcvFileDescription = rfd, agentRcvFileId, agentRcvFileDeleted, userApprovedRelays}) <$> rfd_ in RcvFileTransfer {fileId, xftpRcvFile, fileInvitation, fileStatus, rcvFileInline, senderDisplayName, chunkSize, cancelled, grpMemberId, cryptoArgs} rfi = maybe (throwError $ SERcvFileInvalid fileId) pure =<< rfi_ rfi_ = case (filePath_, connId_, agentConnId_) of @@ -720,7 +722,7 @@ getRcvFileTransfer_ db userId fileId = do acceptRcvFileTransfer :: DB.Connection -> VersionRangeChat -> User -> Int64 -> (CommandId, ConnId) -> ConnStatus -> FilePath -> SubscriptionMode -> ExceptT StoreError IO AChatItem acceptRcvFileTransfer db vr user@User {userId} fileId (cmdId, acId) connStatus filePath subMode = ExceptT $ do currentTs <- getCurrentTime - acceptRcvFT_ db user fileId filePath Nothing currentTs + acceptRcvFT_ db user fileId filePath False Nothing currentTs DB.execute db "INSERT INTO connections (agent_conn_id, conn_status, conn_type, rcv_file_id, user_id, created_at, updated_at, to_subscribe) VALUES (?,?,?,?,?,?,?,?)" @@ -740,33 +742,40 @@ getContactByFileId db vr user@User {userId} fileId = do acceptRcvInlineFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem acceptRcvInlineFT db vr user fileId filePath = do - liftIO $ acceptRcvFT_ db user fileId filePath (Just IFMOffer) =<< getCurrentTime + liftIO $ acceptRcvFT_ db user fileId filePath False (Just IFMOffer) =<< getCurrentTime getChatItemByFileId db vr user fileId startRcvInlineFT :: DB.Connection -> User -> RcvFileTransfer -> FilePath -> Maybe InlineFileMode -> IO () startRcvInlineFT db user RcvFileTransfer {fileId} filePath rcvFileInline = - acceptRcvFT_ db user fileId filePath rcvFileInline =<< getCurrentTime + acceptRcvFT_ db user fileId filePath False rcvFileInline =<< getCurrentTime -xftpAcceptRcvFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> ExceptT StoreError IO AChatItem -xftpAcceptRcvFT db vr user fileId filePath = do - liftIO $ acceptRcvFT_ db user fileId filePath Nothing =<< getCurrentTime +xftpAcceptRcvFT :: DB.Connection -> VersionRangeChat -> User -> FileTransferId -> FilePath -> Bool -> ExceptT StoreError IO AChatItem +xftpAcceptRcvFT db vr user fileId filePath userApprovedRelays = do + liftIO $ acceptRcvFT_ db user fileId filePath userApprovedRelays Nothing =<< getCurrentTime getChatItemByFileId db vr user fileId -acceptRcvFT_ :: DB.Connection -> User -> FileTransferId -> FilePath -> Maybe InlineFileMode -> UTCTime -> IO () -acceptRcvFT_ db User {userId} fileId filePath rcvFileInline currentTs = do +acceptRcvFT_ :: DB.Connection -> User -> FileTransferId -> FilePath -> Bool -> Maybe InlineFileMode -> UTCTime -> IO () +acceptRcvFT_ db User {userId} fileId filePath userApprovedRelays rcvFileInline currentTs = do DB.execute db "UPDATE files SET file_path = ?, ci_file_status = ?, updated_at = ? WHERE user_id = ? AND file_id = ?" (filePath, CIFSRcvAccepted, currentTs, userId, fileId) DB.execute db - "UPDATE rcv_files SET rcv_file_inline = ?, file_status = ?, updated_at = ? WHERE file_id = ?" - (rcvFileInline, FSAccepted, currentTs, fileId) + "UPDATE rcv_files SET user_approved_relays = ?, rcv_file_inline = ?, file_status = ?, updated_at = ? WHERE file_id = ?" + (userApprovedRelays, rcvFileInline, FSAccepted, currentTs, fileId) -setRcvFileToReceive :: DB.Connection -> FileTransferId -> Maybe CryptoFileArgs -> IO () -setRcvFileToReceive db fileId cfArgs_ = do +setRcvFileToReceive :: DB.Connection -> FileTransferId -> Bool -> Maybe CryptoFileArgs -> IO () +setRcvFileToReceive db fileId userApprovedRelays cfArgs_ = do currentTs <- getCurrentTime - DB.execute db "UPDATE rcv_files SET to_receive = 1, updated_at = ? WHERE file_id = ?" (currentTs, fileId) + DB.execute + db + [sql| + UPDATE rcv_files + SET to_receive = 1, user_approved_relays = ?, updated_at = ? + WHERE file_id = ? + |] + (userApprovedRelays, currentTs, fileId) forM_ cfArgs_ $ \cfArgs -> setFileCryptoArgs_ db fileId cfArgs currentTs setFileCryptoArgs :: DB.Connection -> FileTransferId -> CryptoFileArgs -> IO () @@ -950,7 +959,7 @@ getFileTransferMeta_ db userId fileId = fileTransferMeta (fileName, fileSize, chunkSize, filePath, fileKey, fileNonce, fileInline, aSndFileId_, agentSndFileDeleted, privateSndFileDescr, cancelled_, xftpRedirectFor) = let cryptoArgs = CFArgs <$> fileKey <*> fileNonce xftpSndFile = (\fId -> XFTPSndFile {agentSndFileId = fId, privateSndFileDescr, agentSndFileDeleted, cryptoArgs}) <$> aSndFileId_ - in FileTransferMeta {fileId, xftpSndFile, xftpRedirectFor, fileName, fileSize, chunkSize, filePath, fileInline, cancelled = fromMaybe False cancelled_} + in FileTransferMeta {fileId, xftpSndFile, xftpRedirectFor, fileName, fileSize, chunkSize, filePath, fileInline, cancelled = fromMaybe False cancelled_} lookupFileTransferRedirectMeta :: DB.Connection -> User -> Int64 -> IO [FileTransferMeta] lookupFileTransferRedirectMeta db User {userId} fileId = do diff --git a/src/Simplex/Chat/Store/Groups.hs b/src/Simplex/Chat/Store/Groups.hs index d8655c818b..55847114ca 100644 --- a/src/Simplex/Chat/Store/Groups.hs +++ b/src/Simplex/Chat/Store/Groups.hs @@ -191,7 +191,7 @@ createGroupLink db User {userId} groupInfo@GroupInfo {groupId, localDisplayName} "INSERT INTO user_contact_links (user_id, group_id, group_link_id, local_display_name, conn_req_contact, group_link_member_role, auto_accept, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)" (userId, groupId, groupLinkId, "group_link_" <> localDisplayName, cReq, memberRole, True, currentTs, currentTs) userContactLinkId <- insertedRowId db - void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode PQSupportOff + void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId ConnNew initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode PQSupportOff getGroupLinkConnection :: DB.Connection -> VersionRangeChat -> User -> GroupInfo -> ExceptT StoreError IO Connection getGroupLinkConnection db vr User {userId} groupInfo@GroupInfo {groupId} = @@ -201,7 +201,7 @@ getGroupLinkConnection db vr User {userId} groupInfo@GroupInfo {groupId} = [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, - c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM connections c JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id @@ -287,7 +287,7 @@ getGroupAndMember db User {userId, userContactId} groupMemberId vr = m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, - c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) @@ -705,7 +705,7 @@ groupMemberQuery = m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, - c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM group_members m JOIN contact_profiles p ON p.contact_profile_id = COALESCE(m.member_profile_id, m.contact_profile_id) @@ -914,7 +914,7 @@ createAcceptedMemberConnection groupMemberId subMode = do createdAt <- liftIO getCurrentTime - Connection {connId} <- createConnection_ db userId ConnMember (Just groupMemberId) agentConnId chatV cReqChatVRange Nothing (Just userContactLinkId) Nothing 0 createdAt subMode PQSupportOff + Connection {connId} <- createConnection_ db userId ConnMember (Just groupMemberId) agentConnId ConnNew chatV cReqChatVRange Nothing (Just userContactLinkId) Nothing 0 createdAt subMode PQSupportOff setCommandConnId db user cmdId connId getContactViaMember :: DB.Connection -> VersionRangeChat -> User -> GroupMember -> ExceptT StoreError IO Contact @@ -944,10 +944,10 @@ getMemberInvitation db User {userId} groupMemberId = fmap join . maybeFirstRow fromOnly $ DB.query db "SELECT sent_inv_queue_info FROM group_members WHERE group_member_id = ? AND user_id = ?" (groupMemberId, userId) -createMemberConnection :: DB.Connection -> UserId -> GroupMember -> ConnId -> VersionChat -> VersionRangeChat -> SubscriptionMode -> IO () +createMemberConnection :: DB.Connection -> UserId -> GroupMember -> ConnId -> VersionChat -> VersionRangeChat -> SubscriptionMode -> IO Connection createMemberConnection db userId GroupMember {groupMemberId} agentConnId chatV peerChatVRange subMode = do currentTs <- getCurrentTime - void $ createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange Nothing 0 currentTs subMode + createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange Nothing 0 currentTs subMode createMemberConnectionAsync :: DB.Connection -> User -> GroupMemberId -> (CommandId, ConnId) -> VersionChat -> VersionRangeChat -> SubscriptionMode -> IO () createMemberConnectionAsync db user@User {userId} groupMemberId (cmdId, agentConnId) chatV peerChatVRange subMode = do @@ -1090,20 +1090,33 @@ createIntroductions db chatV members toMember = do then pure [] else do currentTs <- getCurrentTime - mapM (insertIntro_ currentTs) reMembers + catMaybes <$> mapM (createIntro_ currentTs) reMembers where - insertIntro_ :: UTCTime -> GroupMember -> IO GroupMemberIntro - insertIntro_ ts reMember = do - DB.execute - db - [sql| - INSERT INTO group_member_intros - (re_group_member_id, to_group_member_id, intro_status, intro_chat_protocol_version, created_at, updated_at) - VALUES (?,?,?,?,?,?) - |] - (groupMemberId' reMember, groupMemberId' toMember, GMIntroPending, chatV, ts, ts) - introId <- insertedRowId db - pure GroupMemberIntro {introId, reMember, toMember, introStatus = GMIntroPending, introInvitation = Nothing} + createIntro_ :: UTCTime -> GroupMember -> IO (Maybe GroupMemberIntro) + createIntro_ ts reMember = + -- when members connect concurrently, host would try to create introductions between them in both directions; + -- this check avoids creating second (redundant) introduction + checkInverseIntro >>= \case + Just _ -> pure Nothing + Nothing -> do + DB.execute + db + [sql| + INSERT INTO group_member_intros + (re_group_member_id, to_group_member_id, intro_status, intro_chat_protocol_version, created_at, updated_at) + VALUES (?,?,?,?,?,?) + |] + (groupMemberId' reMember, groupMemberId' toMember, GMIntroPending, chatV, ts, ts) + introId <- insertedRowId db + pure $ Just GroupMemberIntro {introId, reMember, toMember, introStatus = GMIntroPending, introInvitation = Nothing} + where + checkInverseIntro :: IO (Maybe Int64) + checkInverseIntro = + maybeFirstRow fromOnly $ + DB.query + db + "SELECT 1 FROM group_member_intros WHERE re_group_member_id = ? AND to_group_member_id = ? LIMIT 1" + (groupMemberId' toMember, groupMemberId' reMember) updateIntroStatus :: DB.Connection -> Int64 -> GroupMemberIntroStatus -> IO () updateIntroStatus db introId introStatus = do @@ -1237,7 +1250,7 @@ createIntroReMember currentTs <- liftIO getCurrentTime newMember <- case directConnIds of Just (directCmdId, directAgentConnId) -> do - Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId chatV mcvr memberContactId Nothing customUserProfileId cLevel currentTs subMode PQSupportOff + Connection {connId = directConnId} <- liftIO $ createConnection_ db userId ConnContact Nothing directAgentConnId ConnNew chatV mcvr memberContactId Nothing customUserProfileId cLevel currentTs subMode PQSupportOff liftIO $ setCommandConnId db user directCmdId directConnId (localDisplayName, contactId, memProfileId) <- createContact_ db userId memberProfile "" (Just groupId) currentTs False liftIO $ DB.execute db "UPDATE connections SET contact_id = ?, updated_at = ? WHERE connection_id = ?" (contactId, currentTs, directConnId) @@ -1258,7 +1271,7 @@ createIntroToMemberContact db user@User {userId} GroupMember {memberContactId = Connection {connId = groupConnId} <- createMemberConnection_ db userId groupMemberId groupAgentConnId chatV mcvr viaContactId cLevel currentTs subMode setCommandConnId db user groupCmdId groupConnId forM_ directConnIds $ \(directCmdId, directAgentConnId) -> do - Connection {connId = directConnId} <- createConnection_ db userId ConnContact Nothing directAgentConnId chatV mcvr viaContactId Nothing customUserProfileId cLevel currentTs subMode PQSupportOff + Connection {connId = directConnId} <- createConnection_ db userId ConnContact Nothing directAgentConnId ConnNew chatV mcvr viaContactId Nothing customUserProfileId cLevel currentTs subMode PQSupportOff setCommandConnId db user directCmdId directConnId contactId <- createMemberContact_ directConnId currentTs updateMember_ contactId currentTs @@ -1290,7 +1303,7 @@ createIntroToMemberContact db user@User {userId} GroupMember {memberContactId = createMemberConnection_ :: DB.Connection -> UserId -> Int64 -> ConnId -> VersionChat -> VersionRangeChat -> Maybe Int64 -> Int -> UTCTime -> SubscriptionMode -> IO Connection createMemberConnection_ db userId groupMemberId agentConnId chatV peerChatVRange viaContact connLevel currentTs subMode = - createConnection_ db userId ConnMember (Just groupMemberId) agentConnId chatV peerChatVRange viaContact Nothing Nothing connLevel currentTs subMode PQSupportOff + createConnection_ db userId ConnMember (Just groupMemberId) agentConnId ConnNew chatV peerChatVRange viaContact Nothing Nothing connLevel currentTs subMode PQSupportOff getViaGroupMember :: DB.Connection -> VersionRangeChat -> User -> Contact -> IO (Maybe (GroupInfo, GroupMember)) getViaGroupMember db vr User {userId, userContactId} Contact {contactId} = @@ -1313,7 +1326,7 @@ getViaGroupMember db vr User {userId, userContactId} Contact {contactId} = m.invited_by, m.invited_by_group_member_id, m.local_display_name, m.contact_id, m.contact_profile_id, p.contact_profile_id, p.display_name, p.full_name, p.image, p.contact_link, p.local_alias, p.preferences, c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, - c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM group_members m JOIN contacts ct ON ct.contact_id = m.contact_id @@ -1957,10 +1970,11 @@ createMemberContact pqEncryption = PQEncOff, pqSndEnabled = Nothing, pqRcvEnabled = Nothing, - authErrCounter = 0 + authErrCounter = 0, + quotaErrCounter = 0 } mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn - pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, uiThemes = Nothing, customData = Nothing} + pure Contact {contactId, localDisplayName, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Just groupMemberId, contactGrpInvSent = False, uiThemes = Nothing, chatDeleted = False, customData = Nothing} getMemberContact :: DB.Connection -> VersionRangeChat -> User -> ContactId -> ExceptT StoreError IO (GroupInfo, GroupMember, Contact, ConnReqInvitation) getMemberContact db vr user contactId = do @@ -1997,7 +2011,7 @@ createMemberContactInvited contactId <- createContactUpdateMember currentTs userPreferences ctConn <- createMemberContactConn_ db user connIds gInfo mConn contactId subMode let mergedPreferences = contactUserPreferences user userPreferences preferences $ connIncognito ctConn - mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False, uiThemes = Nothing, customData = Nothing} + mCt' = Contact {contactId, localDisplayName = memberLDN, profile = memberProfile, activeConn = Just ctConn, viaGroup = Nothing, contactUsed = True, contactStatus = CSActive, chatSettings = defaultChatSettings, userPreferences, mergedPreferences, createdAt = currentTs, updatedAt = currentTs, chatTs = Just currentTs, contactGroupMemberId = Nothing, contactGrpInvSent = False, uiThemes = Nothing, chatDeleted = False, customData = Nothing} m' = m {memberContactId = Just contactId} pure (mCt', m') where @@ -2090,7 +2104,8 @@ createMemberContactConn_ pqEncryption = PQEncOff, pqSndEnabled = Nothing, pqRcvEnabled = Nothing, - authErrCounter = 0 + authErrCounter = 0, + quotaErrCounter = 0 } updateMemberProfile :: DB.Connection -> User -> GroupMember -> Profile -> ExceptT StoreError IO GroupMember diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 8163bd336c..6dbd9124c5 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -19,10 +19,11 @@ module Simplex.Chat.Store.Messages -- * Message and chat item functions deleteContactCIs, getGroupFileInfo, - deleteGroupCIs, + deleteGroupChatItemsMessages, createNewSndMessage, createSndMsgDelivery, createNewMessageAndRcvMsgDelivery, + getLastRcvMsgInfo, createNewRcvMessage, updateSndMsgDeliveryStatus, createPendingGroupMessage, @@ -95,6 +96,7 @@ module Simplex.Chat.Store.Messages lookupChatItemByFileId, getChatItemByGroupId, updateDirectChatItemStatus, + setDirectSndChatItemViaProxy, getTimedItems, getChatItemTTL, setChatItemTTL, @@ -108,6 +110,7 @@ module Simplex.Chat.Store.Messages createGroupSndStatus, getGroupSndStatus, updateGroupSndStatus, + setGroupSndViaProxy, getGroupSndStatuses, getGroupSndStatusCounts, getGroupHistoryItems, @@ -167,8 +170,8 @@ getGroupFileInfo db User {userId} GroupInfo {groupId} = map toFileInfo <$> DB.query db (fileInfoQuery <> " WHERE i.user_id = ? AND i.group_id = ?") (userId, groupId) -deleteGroupCIs :: DB.Connection -> User -> GroupInfo -> IO () -deleteGroupCIs db User {userId} GroupInfo {groupId} = do +deleteGroupChatItemsMessages :: DB.Connection -> User -> GroupInfo -> IO () +deleteGroupChatItemsMessages db User {userId} GroupInfo {groupId} = do DB.execute db "DELETE FROM messages WHERE group_id = ?" (Only groupId) DB.execute db "DELETE FROM chat_item_reactions WHERE group_id = ?" (Only groupId) DB.execute db "DELETE FROM chat_items WHERE user_id = ? AND group_id = ?" (userId, groupId) @@ -224,6 +227,23 @@ createNewMessageAndRcvMsgDelivery db connOrGroupId newMessage sharedMsgId_ RcvMs (msgId, connId, agentMsgId, msgMetaJson agentMsgMeta, snd $ broker agentMsgMeta, currentTs, currentTs, MDSRcvAgent) pure msg +getLastRcvMsgInfo :: DB.Connection -> Int64 -> IO (Maybe RcvMsgInfo) +getLastRcvMsgInfo db connId = + maybeFirstRow rcvMsgInfo $ + DB.query + db + [sql| + SELECT message_id, msg_delivery_id, delivery_status, agent_msg_id, agent_msg_meta + FROM msg_deliveries + WHERE connection_id = ? AND delivery_status IN (?, ?) + ORDER BY created_at DESC, msg_delivery_id DESC + LIMIT 1 + |] + (connId, MDSRcvAgent, MDSRcvAcknowledged) + where + rcvMsgInfo (msgId, msgDeliveryId, msgDeliveryStatus, agentMsgId, agentMsgMeta) = + RcvMsgInfo {msgId, msgDeliveryId, msgDeliveryStatus, agentMsgId, agentMsgMeta} + createNewRcvMessage :: forall e. MsgEncodingI e => DB.Connection -> ConnOrGroupId -> NewRcvMessage e -> Maybe SharedMsgId -> Maybe GroupMemberId -> Maybe GroupMemberId -> ExceptT StoreError IO RcvMessage createNewRcvMessage db connOrGroupId NewRcvMessage {chatMsgEvent, msgBody} sharedMsgId_ authorMember forwardedByMember = case connOrGroupId of @@ -283,22 +303,22 @@ createPendingGroupMessage db groupMemberId messageId introId_ = do |] (groupMemberId, messageId, introId_, currentTs, currentTs) -getPendingGroupMessages :: DB.Connection -> Int64 -> IO [PendingGroupMessage] +getPendingGroupMessages :: DB.Connection -> Int64 -> IO [(SndMessage, ACMEventTag, Maybe Int64)] getPendingGroupMessages db groupMemberId = map pendingGroupMessage <$> DB.query db [sql| - SELECT pgm.message_id, m.chat_msg_event, m.msg_body, pgm.group_member_intro_id + SELECT pgm.message_id, m.shared_msg_id, m.msg_body, m.chat_msg_event, pgm.group_member_intro_id FROM pending_group_messages pgm JOIN messages m USING (message_id) WHERE pgm.group_member_id = ? - ORDER BY pgm.message_id ASC + ORDER BY pgm.created_at ASC, pgm.message_id ASC |] (Only groupMemberId) where - pendingGroupMessage (msgId, cmEventTag, msgBody, introId_) = - PendingGroupMessage {msgId, cmEventTag, msgBody, introId_} + pendingGroupMessage (msgId, sharedMsgId, msgBody, cmEventTag, introId_) = + (SndMessage {msgId, sharedMsgId, msgBody}, cmEventTag, introId_) deletePendingGroupMessage :: DB.Connection -> Int64 -> MessageId -> IO () deletePendingGroupMessage db groupMemberId messageId = @@ -315,7 +335,7 @@ updateChatTs db User {userId} chatDirection chatTs = case toChatInfo chatDirecti DirectChat Contact {contactId} -> DB.execute db - "UPDATE contacts SET chat_ts = ? WHERE user_id = ? AND contact_id = ?" + "UPDATE contacts SET chat_ts = ?, chat_deleted = 0 WHERE user_id = ? AND contact_id = ?" (chatTs, userId, contactId) GroupChat GroupInfo {groupId} -> DB.execute @@ -806,7 +826,7 @@ getLocalChatPreview_ db user (LocalChatPD _ noteFolderId lastItemId_ stats) = do -- this function can be changed so it never fails, not only avoid failure on invalid json toLocalChatItem :: UTCTime -> ChatItemRow -> Either StoreError (CChatItem 'CTLocal) -toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) = +toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) = chatItem $ fromRight invalid $ dbParseACIContent itemContentText where invalid = ACIContent msgDir $ CIInvalidJSON itemContentText @@ -836,10 +856,10 @@ toLocalChatItem currentTs ((itemId, itemTs, AMsgDirection msgDir, itemContentTex ciMeta content status = let itemDeleted' = case itemDeleted of DBCINotDeleted -> Nothing - _ -> Just (CIDeleted @CTLocal deletedTs) + _ -> Just (CIDeleted @'CTLocal deletedTs) itemEdited' = fromMaybe False itemEdited itemForwarded = toCIForwardedFrom forwardedFromRow - in mkCIMeta itemId content itemText status sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs Nothing createdAt updatedAt + in mkCIMeta itemId content itemText status sentViaProxy sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs Nothing createdAt updatedAt ciTimed :: Maybe CITimed ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt} @@ -1407,7 +1427,7 @@ type ChatItemModeRow = (Maybe Int, Maybe UTCTime, Maybe Bool) type ChatItemForwardedFromRow = (Maybe CIForwardedFromTag, Maybe Text, Maybe MsgDirection, Maybe Int64, Maybe Int64, Maybe Int64) type ChatItemRow = - (Int64, ChatItemTs, AMsgDirection, Text, Text, ACIStatus, Maybe SharedMsgId) + (Int64, ChatItemTs, AMsgDirection, Text, Text, ACIStatus, Maybe Bool, Maybe SharedMsgId) :. (Int, Maybe UTCTime, Maybe Bool, UTCTime, UTCTime) :. ChatItemForwardedFromRow :. ChatItemModeRow @@ -1426,7 +1446,7 @@ toQuote (quotedItemId, quotedSharedMsgId, quotedSentAt, quotedMsgContent, _) dir -- this function can be changed so it never fails, not only avoid failure on invalid json toDirectChatItem :: UTCTime -> ChatItemRow :. QuoteRow -> Either StoreError (CChatItem 'CTDirect) -toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. quoteRow) = +toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. quoteRow) = chatItem $ fromRight invalid $ dbParseACIContent itemContentText where invalid = ACIContent msgDir $ CIInvalidJSON itemContentText @@ -1456,10 +1476,10 @@ toDirectChatItem currentTs (((itemId, itemTs, AMsgDirection msgDir, itemContentT ciMeta content status = let itemDeleted' = case itemDeleted of DBCINotDeleted -> Nothing - _ -> Just (CIDeleted @CTDirect deletedTs) + _ -> Just (CIDeleted @'CTDirect deletedTs) itemEdited' = fromMaybe False itemEdited itemForwarded = toCIForwardedFrom forwardedFromRow - in mkCIMeta itemId content itemText status sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs Nothing createdAt updatedAt + in mkCIMeta itemId content itemText status sentViaProxy sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs Nothing createdAt updatedAt ciTimed :: Maybe CITimed ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt} @@ -1483,7 +1503,7 @@ toGroupQuote qr@(_, _, _, _, quotedSent) quotedMember_ = toQuote qr $ direction -- this function can be changed so it never fails, not only avoid failure on invalid json toGroupChatItem :: UTCTime -> Int64 -> ChatItemRow :. Only (Maybe GroupMemberId) :. MaybeGroupMemberRow :. GroupQuoteRow :. MaybeGroupMemberRow -> Either StoreError (CChatItem 'CTGroup) -toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. Only forwardedByMember :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) = do +toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, itemContentText, itemText, itemStatus, sentViaProxy, sharedMsgId) :. (itemDeleted, deletedTs, itemEdited, createdAt, updatedAt) :. forwardedFromRow :. (timedTTL, timedDeleteAt, itemLive) :. (fileId_, fileName_, fileSize_, filePath, fileKey, fileNonce, fileStatus_, fileProtocol_)) :. Only forwardedByMember :. memberRow_ :. (quoteRow :. quotedMemberRow_) :. deletedByGroupMemberRow_) = do chatItem $ fromRight invalid $ dbParseACIContent itemContentText where member_ = toMaybeGroupMember userContactId memberRow_ @@ -1518,10 +1538,10 @@ toGroupChatItem currentTs userContactId (((itemId, itemTs, AMsgDirection msgDir, DBCINotDeleted -> Nothing DBCIBlocked -> Just (CIBlocked deletedTs) DBCIBlockedByAdmin -> Just (CIBlockedByAdmin deletedTs) - _ -> Just (maybe (CIDeleted @CTGroup deletedTs) (CIModerated deletedTs) deletedByGroupMember_) + _ -> Just (maybe (CIDeleted @'CTGroup deletedTs) (CIModerated deletedTs) deletedByGroupMember_) itemEdited' = fromMaybe False itemEdited itemForwarded = toCIForwardedFrom forwardedFromRow - in mkCIMeta itemId content itemText status sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs forwardedByMember createdAt updatedAt + in mkCIMeta itemId content itemText status sentViaProxy sharedMsgId itemForwarded itemDeleted' itemEdited' ciTimed itemLive currentTs itemTs forwardedByMember createdAt updatedAt ciTimed :: Maybe CITimed ciTimed = timedTTL >>= \ttl -> Just CITimed {ttl, deleteAt = timedDeleteAt} @@ -1600,6 +1620,11 @@ updateDirectChatItemStatus db user@User {userId} ct@Contact {contactId} itemId i liftIO $ DB.execute db "UPDATE chat_items SET item_status = ?, updated_at = ? WHERE user_id = ? AND contact_id = ? AND chat_item_id = ?" (itemStatus, currentTs, userId, contactId, itemId) pure ci {meta = (meta ci) {itemStatus}} +setDirectSndChatItemViaProxy :: DB.Connection -> User -> Contact -> ChatItem 'CTDirect 'MDSnd -> Bool -> IO (ChatItem 'CTDirect 'MDSnd) +setDirectSndChatItemViaProxy db User {userId} Contact {contactId} ci viaProxy = do + DB.execute db "UPDATE chat_items SET via_proxy = ? WHERE user_id = ? AND contact_id = ? AND chat_item_id = ?" (viaProxy, userId, contactId, chatItemId' ci) + pure ci {meta = (meta ci) {sentViaProxy = Just viaProxy}} + updateDirectChatItem :: MsgDirectionI d => DB.Connection -> User -> Contact -> ChatItemId -> CIContent d -> Bool -> Bool -> Maybe CITimed -> Maybe MessageId -> ExceptT StoreError IO (ChatItem 'CTDirect d) updateDirectChatItem db user ct@Contact {contactId} itemId newContent edited live timed_ msgId_ = do ci <- liftEither . correctDir =<< getDirectCIWithReactions db user ct itemId @@ -1708,11 +1733,10 @@ deleteChatItemVersions_ :: DB.Connection -> ChatItemId -> IO () deleteChatItemVersions_ db itemId = DB.execute db "DELETE FROM chat_item_versions WHERE chat_item_id = ?" (Only itemId) -markDirectChatItemDeleted :: DB.Connection -> User -> Contact -> ChatItem 'CTDirect d -> MessageId -> UTCTime -> IO (ChatItem 'CTDirect d) -markDirectChatItemDeleted db User {userId} Contact {contactId} ci@ChatItem {meta} msgId deletedTs = do +markDirectChatItemDeleted :: DB.Connection -> User -> Contact -> ChatItem 'CTDirect d -> UTCTime -> IO (ChatItem 'CTDirect d) +markDirectChatItemDeleted db User {userId} Contact {contactId} ci@ChatItem {meta} deletedTs = do currentTs <- liftIO getCurrentTime let itemId = chatItemId' ci - insertChatItemMessage_ db itemId msgId currentTs DB.execute db [sql| @@ -1721,7 +1745,7 @@ markDirectChatItemDeleted db User {userId} Contact {contactId} ci@ChatItem {meta WHERE user_id = ? AND contact_id = ? AND chat_item_id = ? |] (DBCIDeleted, deletedTs, currentTs, userId, contactId, itemId) - pure ci {meta = meta {itemDeleted = Just $ CIDeleted $ Just deletedTs}} + pure ci {meta = meta {itemDeleted = Just $ CIDeleted $ Just deletedTs, editable = False, deletable = False}} getDirectChatItemBySharedMsgId :: DB.Connection -> User -> ContactId -> SharedMsgId -> ExceptT StoreError IO (CChatItem 'CTDirect) getDirectChatItemBySharedMsgId db user@User {userId} contactId sharedMsgId = do @@ -1758,7 +1782,7 @@ getDirectChatItem db User {userId} contactId itemId = ExceptT $ do [sql| SELECT -- ChatItem - i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, + i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id, i.timed_ttl, i.timed_delete_at, i.item_live, @@ -1875,7 +1899,7 @@ updateGroupChatItemModerated db User {userId} GroupInfo {groupId} ci m@GroupMemb WHERE user_id = ? AND group_id = ? AND chat_item_id = ? |] (deletedTs, groupMemberId, toContent, toText, currentTs, userId, groupId, itemId) - pure $ ci {content = toContent, meta = (meta ci) {itemText = toText, itemDeleted = Just (CIModerated (Just deletedTs) m), editable = False, deletable = False}, formattedText = Nothing} + pure ci {content = toContent, meta = (meta ci) {itemText = toText, itemDeleted = Just (CIModerated (Just deletedTs) m), editable = False, deletable = False}, formattedText = Nothing} updateGroupCIBlockedByAdmin :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup d -> UTCTime -> IO (ChatItem 'CTGroup d) updateGroupCIBlockedByAdmin db User {userId} GroupInfo {groupId} ci deletedTs = do @@ -1906,14 +1930,13 @@ pattern DBCIBlocked = 2 pattern DBCIBlockedByAdmin :: Int pattern DBCIBlockedByAdmin = 3 -markGroupChatItemDeleted :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup d -> MessageId -> Maybe GroupMember -> UTCTime -> IO (ChatItem 'CTGroup d) -markGroupChatItemDeleted db User {userId} GroupInfo {groupId} ci@ChatItem {meta} msgId byGroupMember_ deletedTs = do +markGroupChatItemDeleted :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup d -> Maybe GroupMember -> UTCTime -> IO (ChatItem 'CTGroup d) +markGroupChatItemDeleted db User {userId} GroupInfo {groupId} ci@ChatItem {meta} byGroupMember_ deletedTs = do currentTs <- liftIO getCurrentTime let itemId = chatItemId' ci (deletedByGroupMemberId, itemDeleted) = case byGroupMember_ of Just m@GroupMember {groupMemberId} -> (Just groupMemberId, Just $ CIModerated (Just deletedTs) m) - _ -> (Nothing, Just $ CIDeleted @CTGroup (Just deletedTs)) - insertChatItemMessage_ db itemId msgId currentTs + _ -> (Nothing, Just $ CIDeleted @'CTGroup (Just deletedTs)) DB.execute db [sql| @@ -1922,7 +1945,7 @@ markGroupChatItemDeleted db User {userId} GroupInfo {groupId} ci@ChatItem {meta} WHERE user_id = ? AND group_id = ? AND chat_item_id = ? |] (DBCIDeleted, deletedTs, deletedByGroupMemberId, currentTs, userId, groupId, itemId) - pure ci {meta = meta {itemDeleted}} + pure ci {meta = meta {itemDeleted, editable = False, deletable = False}} markGroupChatItemBlocked :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup 'MDRcv -> IO (ChatItem 'CTGroup 'MDRcv) markGroupChatItemBlocked db User {userId} GroupInfo {groupId} ci@ChatItem {meta} = do @@ -1935,7 +1958,7 @@ markGroupChatItemBlocked db User {userId} GroupInfo {groupId} ci@ChatItem {meta} WHERE user_id = ? AND group_id = ? AND chat_item_id = ? |] (DBCIBlocked, deletedTs, deletedTs, userId, groupId, chatItemId' ci) - pure ci {meta = meta {itemDeleted = Just $ CIBlocked $ Just deletedTs}} + pure ci {meta = meta {itemDeleted = Just $ CIBlocked $ Just deletedTs, editable = False, deletable = False}} markGroupCIBlockedByAdmin :: DB.Connection -> User -> GroupInfo -> ChatItem 'CTGroup 'MDRcv -> IO (ChatItem 'CTGroup 'MDRcv) markGroupCIBlockedByAdmin db User {userId} GroupInfo {groupId} ci@ChatItem {meta} = do @@ -1948,7 +1971,7 @@ markGroupCIBlockedByAdmin db User {userId} GroupInfo {groupId} ci@ChatItem {meta WHERE user_id = ? AND group_id = ? AND chat_item_id = ? |] (DBCIBlockedByAdmin, deletedTs, deletedTs, userId, groupId, chatItemId' ci) - pure ci {meta = meta {itemDeleted = Just $ CIBlockedByAdmin $ Just deletedTs}} + pure ci {meta = meta {itemDeleted = Just $ CIBlockedByAdmin $ Just deletedTs, editable = False, deletable = False}} getGroupChatItemBySharedMsgId :: DB.Connection -> User -> GroupId -> GroupMemberId -> SharedMsgId -> ExceptT StoreError IO (CChatItem 'CTGroup) getGroupChatItemBySharedMsgId db user@User {userId} groupId groupMemberId sharedMsgId = do @@ -2001,7 +2024,7 @@ getGroupChatItem db User {userId, userContactId} groupId itemId = ExceptT $ do [sql| SELECT -- ChatItem - i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, + i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id, i.timed_ttl, i.timed_delete_at, i.item_live, @@ -2105,7 +2128,7 @@ getLocalChatItem db User {userId} folderId itemId = ExceptT $ do [sql| SELECT -- ChatItem - i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.shared_msg_id, + i.chat_item_id, i.item_ts, i.item_sent, i.item_content, i.item_text, i.item_status, i.via_proxy, i.shared_msg_id, i.item_deleted, i.item_deleted_ts, i.item_edited, i.created_at, i.updated_at, i.fwd_from_tag, i.fwd_from_chat_name, i.fwd_from_msg_dir, i.fwd_from_contact_id, i.fwd_from_group_id, i.fwd_from_chat_item_id, i.timed_ttl, i.timed_delete_at, i.item_live, @@ -2506,14 +2529,14 @@ deleteCIModeration db GroupInfo {groupId} itemMemberId (Just sharedMsgId) = "DELETE FROM chat_item_moderations WHERE group_id = ? AND item_member_id = ? AND shared_msg_id = ?" (groupId, itemMemberId, sharedMsgId) -createGroupSndStatus :: DB.Connection -> ChatItemId -> GroupMemberId -> CIStatus 'MDSnd -> IO () +createGroupSndStatus :: DB.Connection -> ChatItemId -> GroupMemberId -> GroupSndStatus -> IO () createGroupSndStatus db itemId memberId status = DB.execute db "INSERT INTO group_snd_item_statuses (chat_item_id, group_member_id, group_snd_item_status) VALUES (?,?,?)" (itemId, memberId, status) -getGroupSndStatus :: DB.Connection -> ChatItemId -> GroupMemberId -> ExceptT StoreError IO (CIStatus 'MDSnd) +getGroupSndStatus :: DB.Connection -> ChatItemId -> GroupMemberId -> ExceptT StoreError IO GroupSndStatus getGroupSndStatus db itemId memberId = ExceptT . firstRow fromOnly (SENoGroupSndStatus itemId memberId) $ DB.query @@ -2526,7 +2549,7 @@ getGroupSndStatus db itemId memberId = |] (itemId, memberId) -updateGroupSndStatus :: DB.Connection -> ChatItemId -> GroupMemberId -> CIStatus 'MDSnd -> IO () +updateGroupSndStatus :: DB.Connection -> ChatItemId -> GroupMemberId -> GroupSndStatus -> IO () updateGroupSndStatus db itemId memberId status = do currentTs <- liftIO getCurrentTime DB.execute @@ -2538,18 +2561,33 @@ updateGroupSndStatus db itemId memberId status = do |] (status, currentTs, itemId, memberId) -getGroupSndStatuses :: DB.Connection -> ChatItemId -> IO [(GroupMemberId, CIStatus 'MDSnd)] -getGroupSndStatuses db itemId = - DB.query +setGroupSndViaProxy :: DB.Connection -> ChatItemId -> GroupMemberId -> Bool -> IO () +setGroupSndViaProxy db itemId memberId viaProxy = + DB.execute db [sql| - SELECT group_member_id, group_snd_item_status - FROM group_snd_item_statuses - WHERE chat_item_id = ? + UPDATE group_snd_item_statuses + SET via_proxy = ? + WHERE chat_item_id = ? AND group_member_id = ? |] - (Only itemId) + (viaProxy, itemId, memberId) -getGroupSndStatusCounts :: DB.Connection -> ChatItemId -> IO [(CIStatus 'MDSnd, Int)] +getGroupSndStatuses :: DB.Connection -> ChatItemId -> IO [MemberDeliveryStatus] +getGroupSndStatuses db itemId = + map memStatus + <$> DB.query + db + [sql| + SELECT group_member_id, group_snd_item_status, via_proxy + FROM group_snd_item_statuses + WHERE chat_item_id = ? + |] + (Only itemId) + where + memStatus (groupMemberId, memberDeliveryStatus, sentViaProxy) = + MemberDeliveryStatus {groupMemberId, memberDeliveryStatus, sentViaProxy} + +getGroupSndStatusCounts :: DB.Connection -> ChatItemId -> IO [(GroupSndStatus, Int)] getGroupSndStatusCounts db itemId = DB.query db diff --git a/src/Simplex/Chat/Store/Migrations.hs b/src/Simplex/Chat/Store/Migrations.hs index 2b44778272..5c9082b361 100644 --- a/src/Simplex/Chat/Store/Migrations.hs +++ b/src/Simplex/Chat/Store/Migrations.hs @@ -106,6 +106,10 @@ import Simplex.Chat.Migrations.M20240313_drop_agent_ack_cmd_id import Simplex.Chat.Migrations.M20240324_custom_data import Simplex.Chat.Migrations.M20240402_item_forwarded import Simplex.Chat.Migrations.M20240430_ui_theme +import Simplex.Chat.Migrations.M20240501_chat_deleted +import Simplex.Chat.Migrations.M20240510_chat_items_via_proxy +import Simplex.Chat.Migrations.M20240515_rcv_files_user_approved_relays +import Simplex.Chat.Migrations.M20240528_quota_err_counter import Simplex.Messaging.Agent.Store.SQLite.Migrations (Migration (..)) schemaMigrations :: [(String, Query, Maybe Query)] @@ -211,7 +215,11 @@ schemaMigrations = ("20240313_drop_agent_ack_cmd_id", m20240313_drop_agent_ack_cmd_id, Just down_m20240313_drop_agent_ack_cmd_id), ("20240324_custom_data", m20240324_custom_data, Just down_m20240324_custom_data), ("20240402_item_forwarded", m20240402_item_forwarded, Just down_m20240402_item_forwarded), - ("20240430_ui_theme", m20240430_ui_theme, Just down_m20240430_ui_theme) + ("20240430_ui_theme", m20240430_ui_theme, Just down_m20240430_ui_theme), + ("20240501_chat_deleted", m20240501_chat_deleted, Just down_m20240501_chat_deleted), + ("20240510_chat_items_via_proxy", m20240510_chat_items_via_proxy, Just down_m20240510_chat_items_via_proxy), + ("20240515_rcv_files_user_approved_relays", m20240515_rcv_files_user_approved_relays, Just down_m20240515_rcv_files_user_approved_relays), + ("20240528_quota_err_counter", m20240528_quota_err_counter, Just down_m20240528_quota_err_counter) ] -- | The list of migrations in ascending order by date diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index 9b83e7299f..fb87662c27 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -84,6 +84,7 @@ import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme +import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..)) import Simplex.Messaging.Agent.Protocol (ACorrId, ConnId, UserId) import Simplex.Messaging.Agent.Store.SQLite (firstRow, maybeFirstRow) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB @@ -327,7 +328,7 @@ createUserContactLink db User {userId} agentConnId cReq subMode = "INSERT INTO user_contact_links (user_id, conn_req_contact, created_at, updated_at) VALUES (?,?,?,?)" (userId, cReq, currentTs, currentTs) userContactLinkId <- insertedRowId db - void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff + void $ createConnection_ db userId ConnUserContact (Just userContactLinkId) agentConnId ConnNew initialChatVersion chatInitialVRange Nothing Nothing Nothing 0 currentTs subMode CR.PQSupportOff getUserAddressConnections :: DB.Connection -> VersionRangeChat -> User -> ExceptT StoreError IO [Connection] getUserAddressConnections db vr User {userId} = do @@ -342,7 +343,7 @@ getUserAddressConnections db vr User {userId} = do [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, - c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version FROM connections c JOIN user_contact_links uc ON c.user_contact_link_id = uc.user_contact_link_id @@ -358,7 +359,7 @@ getUserContactLinks db vr User {userId} = [sql| SELECT c.connection_id, c.agent_conn_id, c.conn_level, c.via_contact, c.via_user_contact_link, c.via_group_link, c.group_link_id, c.custom_user_profile_id, c.conn_status, c.conn_type, c.contact_conn_initiated, c.local_alias, c.contact_id, c.group_member_id, c.snd_file_id, c.rcv_file_id, c.user_contact_link_id, - c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, + c.created_at, c.security_code, c.security_code_verified_at, c.pq_support, c.pq_encryption, c.pq_snd_enabled, c.pq_rcv_enabled, c.auth_err_counter, c.quota_err_counter, c.conn_chat_version, c.peer_chat_min_version, c.peer_chat_max_version, uc.user_contact_link_id, uc.conn_req_contact, uc.group_id FROM connections c diff --git a/src/Simplex/Chat/Store/Shared.hs b/src/Simplex/Chat/Store/Shared.hs index 1f16ebfef0..b80e2ce805 100644 --- a/src/Simplex/Chat/Store/Shared.hs +++ b/src/Simplex/Chat/Store/Shared.hs @@ -163,12 +163,12 @@ toFileInfo (fileId, fileStatus, filePath) = CIFileInfo {fileId, fileStatus, file type EntityIdsRow = (Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64, Maybe Int64) -type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, Bool, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime, PQSupport, PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Int, Maybe VersionChat, VersionChat, VersionChat) +type ConnectionRow = (Int64, ConnId, Int, Maybe Int64, Maybe Int64, Bool, Maybe GroupLinkId, Maybe Int64, ConnStatus, ConnType, Bool, LocalAlias) :. EntityIdsRow :. (UTCTime, Maybe Text, Maybe UTCTime, PQSupport, PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Int, Int, Maybe VersionChat, VersionChat, VersionChat) -type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe Bool, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime, Maybe PQSupport, Maybe PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Maybe Int, Maybe VersionChat, Maybe VersionChat, Maybe VersionChat) +type MaybeConnectionRow = (Maybe Int64, Maybe ConnId, Maybe Int, Maybe Int64, Maybe Int64, Maybe Bool, Maybe GroupLinkId, Maybe Int64, Maybe ConnStatus, Maybe ConnType, Maybe Bool, Maybe LocalAlias) :. EntityIdsRow :. (Maybe UTCTime, Maybe Text, Maybe UTCTime, Maybe PQSupport, Maybe PQEncryption, Maybe PQEncryption, Maybe PQEncryption, Maybe Int, Maybe Int, Maybe VersionChat, Maybe VersionChat, Maybe VersionChat) toConnection :: VersionRangeChat -> ConnectionRow -> Connection -toConnection vr ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled, authErrCounter, chatV, minVer, maxVer)) = +toConnection vr ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled, authErrCounter, quotaErrCounter, chatV, minVer, maxVer)) = Connection { connId, agentConnId = AgentConnId acId, @@ -191,6 +191,7 @@ toConnection vr ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGr pqSndEnabled, pqRcvEnabled, authErrCounter, + quotaErrCounter, createdAt } where @@ -203,12 +204,12 @@ toConnection vr ((connId, acId, connLevel, viaContact, viaUserContactLink, viaGr entityId_ ConnUserContact = userContactLinkId toMaybeConnection :: VersionRangeChat -> MaybeConnectionRow -> Maybe Connection -toMaybeConnection vr ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just contactConnInitiated, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just pqSupport, Just pqEncryption, pqSndEnabled_, pqRcvEnabled_, Just authErrCounter, connChatVersion, Just minVer, Just maxVer)) = - Just $ toConnection vr ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled_, pqRcvEnabled_, authErrCounter, connChatVersion, minVer, maxVer)) +toMaybeConnection vr ((Just connId, Just agentConnId, Just connLevel, viaContact, viaUserContactLink, Just viaGroupLink, groupLinkId, customUserProfileId, Just connStatus, Just connType, Just contactConnInitiated, Just localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (Just createdAt, code_, verifiedAt_, Just pqSupport, Just pqEncryption, pqSndEnabled_, pqRcvEnabled_, Just authErrCounter, Just quotaErrCounter, connChatVersion, Just minVer, Just maxVer)) = + Just $ toConnection vr ((connId, agentConnId, connLevel, viaContact, viaUserContactLink, viaGroupLink, groupLinkId, customUserProfileId, connStatus, connType, contactConnInitiated, localAlias) :. (contactId, groupMemberId, sndFileId, rcvFileId, userContactLinkId) :. (createdAt, code_, verifiedAt_, pqSupport, pqEncryption, pqSndEnabled_, pqRcvEnabled_, authErrCounter, quotaErrCounter, connChatVersion, minVer, maxVer)) toMaybeConnection _ _ = Nothing -createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> VersionChat -> VersionRangeChat -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> SubscriptionMode -> PQSupport -> IO Connection -createConnection_ db userId connType entityId acId connChatVersion peerChatVRange@(VersionRange minV maxV) viaContact viaUserContactLink customUserProfileId connLevel currentTs subMode pqSup = do +createConnection_ :: DB.Connection -> UserId -> ConnType -> Maybe Int64 -> ConnId -> ConnStatus -> VersionChat -> VersionRangeChat -> Maybe ContactId -> Maybe Int64 -> Maybe ProfileId -> Int -> UTCTime -> SubscriptionMode -> PQSupport -> IO Connection +createConnection_ db userId connType entityId acId connStatus connChatVersion peerChatVRange@(VersionRange minV maxV) viaContact viaUserContactLink customUserProfileId connLevel currentTs subMode pqSup = do viaLinkGroupId :: Maybe Int64 <- fmap join . forM viaUserContactLink $ \ucLinkId -> maybeFirstRow fromOnly $ DB.query db "SELECT group_id FROM user_contact_links WHERE user_id = ? AND user_contact_link_id = ? AND group_id IS NOT NULL" (userId, ucLinkId) let viaGroupLink = isJust viaLinkGroupId @@ -221,7 +222,7 @@ createConnection_ db userId connType entityId acId connChatVersion peerChatVRang conn_chat_version, peer_chat_min_version, peer_chat_max_version, to_subscribe, pq_support, pq_encryption ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) |] - ( (userId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, customUserProfileId, ConnNew, connType) + ( (userId, acId, connLevel, viaContact, viaUserContactLink, viaGroupLink, customUserProfileId, connStatus, connType) :. (ent ConnContact, ent ConnMember, ent ConnSndFile, ent ConnRcvFile, ent ConnUserContact, currentTs, currentTs) :. (connChatVersion, minV, maxV, subMode == SMOnlyCreate, pqSup, pqSup) ) @@ -241,7 +242,7 @@ createConnection_ db userId connType entityId acId connChatVersion peerChatVRang groupLinkId = Nothing, customUserProfileId, connLevel, - connStatus = ConnNew, + connStatus, localAlias = "", createdAt = currentTs, connectionCode = Nothing, @@ -249,7 +250,8 @@ createConnection_ db userId connType entityId acId connChatVersion peerChatVRang pqEncryption = CR.pqSupportToEnc pqSup, pqSndEnabled = Nothing, pqRcvEnabled = Nothing, - authErrCounter = 0 + authErrCounter = 0, + quotaErrCounter = 0 } where ent ct = if connType == ct then entityId else Nothing @@ -381,18 +383,19 @@ deleteUnusedIncognitoProfileById_ db User {userId} profileId = |] [":user_id" := userId, ":profile_id" := profileId] -type ContactRow' = (ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime, Maybe GroupMemberId, Bool, Maybe UIThemeEntityOverrides, Maybe CustomData) + +type ContactRow' = (ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. (Maybe GroupMemberId, Bool, Maybe UIThemeEntityOverrides, Bool, Maybe CustomData) type ContactRow = Only ContactId :. ContactRow' toContact :: VersionRangeChat -> User -> ContactRow :. MaybeConnectionRow -> Contact -toContact vr user ((Only contactId :. (profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, uiThemes, customData)) :. connRow) = +toContact vr user ((Only contactId :. (profileId, localDisplayName, viaGroup, displayName, fullName, image, contactLink, localAlias, contactUsed, contactStatus) :. (enableNtfs_, sendRcpts, favorite, preferences, userPreferences, createdAt, updatedAt, chatTs) :. (contactGroupMemberId, contactGrpInvSent, uiThemes, chatDeleted, customData)) :. connRow) = let profile = LocalProfile {profileId, displayName, fullName, image, contactLink, preferences, localAlias} activeConn = toMaybeConnection vr connRow chatSettings = ChatSettings {enableNtfs = fromMaybe MFAll enableNtfs_, sendRcpts, favorite} incognito = maybe False connIncognito activeConn mergedPreferences = contactUserPreferences user userPreferences preferences incognito - in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, uiThemes, customData} + in Contact {contactId, localDisplayName, profile, activeConn, viaGroup, contactUsed, contactStatus, chatSettings, userPreferences, mergedPreferences, createdAt, updatedAt, chatTs, contactGroupMemberId, contactGrpInvSent, uiThemes, chatDeleted, customData} getProfileById :: DB.Connection -> UserId -> Int64 -> ExceptT StoreError IO LocalProfile getProfileById db userId profileId = diff --git a/src/Simplex/Chat/Terminal.hs b/src/Simplex/Chat/Terminal.hs index 2060e529eb..5cc695db04 100644 --- a/src/Simplex/Chat/Terminal.hs +++ b/src/Simplex/Chat/Terminal.hs @@ -21,7 +21,8 @@ import Simplex.Chat.Options import Simplex.Chat.Terminal.Input import Simplex.Chat.Terminal.Output import Simplex.FileTransfer.Client.Presets (defaultXFTPServers) -import Simplex.Messaging.Client (defaultNetworkConfig) +import Simplex.Messaging.Agent.Env.SQLite (presetServerCfg) +import Simplex.Messaging.Client (NetworkConfig (..), SMPProxyFallback (..), SMPProxyMode (..), defaultNetworkConfig) import Simplex.Messaging.Util (raceAny_) import System.IO (hFlush, hSetEcho, stdin, stdout) @@ -31,14 +32,22 @@ terminalChatConfig = { defaultServers = DefaultAgentServers { smp = - L.fromList - [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion", - "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion", - "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion" - ], + L.fromList $ + map + (presetServerCfg True) + [ "smp://u2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU=@smp4.simplex.im,o5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion", + "smp://hpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg=@smp5.simplex.im,jjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion", + "smp://PQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo=@smp6.simplex.im,bylepyau3ty4czmn77q4fglvperknl4bi2eb2fdy2bh4jxtf32kf73yd.onion" + ], + useSMP = 3, ntf = ["ntf://FB-Uop7RTaZZEG0ZLD2CIaTjsPh-Fw0zFAnb7QyA8Ks=@ntf2.simplex.im,ntg7jdjy2i3qbib3sykiho3enekwiaqg3icctliqhtqcg6jmoh6cxiad.onion"], - xftp = defaultXFTPServers, - netCfg = defaultNetworkConfig + xftp = L.map (presetServerCfg True) defaultXFTPServers, + useXFTP = L.length defaultXFTPServers, + netCfg = + defaultNetworkConfig + { smpProxyMode = SPMUnknown, + smpProxyFallback = SPFAllowProtected + } }, deviceNameForRemote = "SimpleX CLI" } diff --git a/src/Simplex/Chat/Terminal/Input.hs b/src/Simplex/Chat/Terminal/Input.hs index 5c36994190..2d1039e585 100644 --- a/src/Simplex/Chat/Terminal/Input.hs +++ b/src/Simplex/Chat/Terminal/Input.hs @@ -71,7 +71,7 @@ runInputLoop ct@ChatTerminal {termState, liveMessageState} cc = forever $ do CRChatItems u chatName_ _ -> whenCurrUser cc u $ mapM_ (setActive ct . chatActiveTo) chatName_ CRNewChatItem u (AChatItem _ SMDSnd cInfo _) -> whenCurrUser cc u $ setActiveChat ct cInfo CRChatItemUpdated u (AChatItem _ SMDSnd cInfo _) -> whenCurrUser cc u $ setActiveChat ct cInfo - CRChatItemDeleted u (AChatItem _ _ cInfo _) _ _ _ -> whenCurrUser cc u $ setActiveChat ct cInfo + CRChatItemsDeleted u ((ChatItemDeletion (AChatItem _ _ cInfo _) _) : _) _ _ -> whenCurrUser cc u $ setActiveChat ct cInfo CRContactDeleted u c -> whenCurrUser cc u $ unsetActiveContact ct c CRGroupDeletedUser u g -> whenCurrUser cc u $ unsetActiveGroup ct g CRSentGroupInvitation u g _ _ -> whenCurrUser cc u $ setActiveGroup ct g diff --git a/src/Simplex/Chat/Terminal/Main.hs b/src/Simplex/Chat/Terminal/Main.hs index 2b26bb1d66..a946ba3483 100644 --- a/src/Simplex/Chat/Terminal/Main.hs +++ b/src/Simplex/Chat/Terminal/Main.hs @@ -6,15 +6,16 @@ module Simplex.Chat.Terminal.Main where import Control.Concurrent (forkIO, threadDelay) import Control.Concurrent.STM import Control.Monad +import Data.Maybe (fromMaybe) import Data.Time.Clock (getCurrentTime) import Data.Time.LocalTime (getCurrentTimeZone) import Network.Socket -import Simplex.Chat.Controller (ChatConfig, ChatController (..), ChatResponse (..), currentRemoteHost, versionNumber, versionString) +import Simplex.Chat.Controller (ChatConfig (..), ChatController (..), ChatResponse (..), DefaultAgentServers (DefaultAgentServers, netCfg), SimpleNetCfg (..), currentRemoteHost, versionNumber, versionString) import Simplex.Chat.Core import Simplex.Chat.Options import Simplex.Chat.Terminal -import Simplex.Chat.View (serializeChatResponse) -import Simplex.Messaging.Client (NetworkConfig (..)) +import Simplex.Chat.View (serializeChatResponse, smpProxyModeStr) +import Simplex.Messaging.Client (NetworkConfig (..), SocksMode (..)) import System.Directory (getAppUserDataDirectory) import System.Exit (exitFailure) import System.Terminal (withTerminal) @@ -22,20 +23,24 @@ import System.Terminal (withTerminal) simplexChatCLI :: ChatConfig -> Maybe (ServiceName -> ChatConfig -> ChatOpts -> IO ()) -> IO () simplexChatCLI cfg server_ = do appDir <- getAppUserDataDirectory "simplex" - opts@ChatOpts {chatCmd, chatServerPort} <- getChatOpts appDir "simplex_v1" + opts <- getChatOpts appDir "simplex_v1" + simplexChatCLI' cfg opts server_ + +simplexChatCLI' :: ChatConfig -> ChatOpts -> Maybe (ServiceName -> ChatConfig -> ChatOpts -> IO ()) -> IO () +simplexChatCLI' cfg opts@ChatOpts {chatCmd, chatCmdLog, chatCmdDelay, chatServerPort} server_ = do if null chatCmd then case chatServerPort of Just chatPort -> case server_ of Just server -> server chatPort cfg opts Nothing -> putStrLn "Not allowed to run as a WebSockets server" >> exitFailure - _ -> runCLI opts - else simplexChatCore cfg opts $ runCommand opts + _ -> runCLI + else simplexChatCore cfg opts runCommand where - runCLI opts = do - welcome opts + runCLI = do + welcome cfg opts t <- withTerminal pure simplexChatTerminal cfg opts t - runCommand ChatOpts {chatCmd, chatCmdLog, chatCmdDelay} user cc = do + runCommand user cc = do when (chatCmdLog /= CCLNone) . void . forkIO . forever $ do (_, _, r') <- atomically . readTBQueue $ outputQ cc case r' of @@ -50,15 +55,18 @@ simplexChatCLI cfg server_ = do rh <- readTVarIO $ currentRemoteHost cc putStrLn $ serializeChatResponse (rh, Just user) ts tz rh r -welcome :: ChatOpts -> IO () -welcome ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, networkConfig}} = +welcome :: ChatConfig -> ChatOpts -> IO () +welcome ChatConfig {defaultServers = DefaultAgentServers {netCfg}} ChatOpts {coreOptions = CoreChatOpts {dbFilePrefix, simpleNetCfg = SimpleNetCfg {socksProxy, socksMode, smpProxyMode_, smpProxyFallback_}}} = mapM_ putStrLn [ versionString versionNumber, "db: " <> dbFilePrefix <> "_chat.db, " <> dbFilePrefix <> "_agent.db", maybe "direct network connection - use `/network` command or `-x` CLI option to connect via SOCKS5 at :9050" - (("using SOCKS5 proxy " <>) . show) - (socksProxy networkConfig), + ((\sp -> "using SOCKS5 proxy " <> sp <> if socksMode == SMOnion then " for onion servers ONLY." else " for ALL servers.") . show) + socksProxy, + smpProxyModeStr + (fromMaybe (smpProxyMode netCfg) smpProxyMode_) + (fromMaybe (smpProxyFallback netCfg) smpProxyFallback_), "type \"/help\" or \"/h\" for usage info" ] diff --git a/src/Simplex/Chat/Terminal/Output.hs b/src/Simplex/Chat/Terminal/Output.hs index be8aa12cfe..40f14a10de 100644 --- a/src/Simplex/Chat/Terminal/Output.hs +++ b/src/Simplex/Chat/Terminal/Output.hs @@ -189,6 +189,8 @@ responseNotification t@ChatTerminal {sendNotification} cc = \case CRContactConnected u ct _ -> when (contactNtf u ct False) $ do whenCurrUser cc u $ setActiveContact t ct sendNtf (viewContactName ct <> "> ", "connected") + CRContactSndReady u ct -> + whenCurrUser cc u $ setActiveContact t ct CRContactAnotherClient u ct -> do whenCurrUser cc u $ unsetActiveContact t ct when (contactNtf u ct False) $ sendNtf (viewContactName ct <> "> ", "connected to another client") diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 0d07d5e3cb..fe40fcfcee 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -27,7 +27,6 @@ import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson as J import qualified Data.Aeson.Encoding as JE import qualified Data.Aeson.TH as JQ -import qualified Data.Aeson.Types as JT import qualified Data.Attoparsec.ByteString.Char8 as A import Data.ByteString.Char8 (ByteString, pack, unpack) import Data.Int (Int64) @@ -48,12 +47,12 @@ import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme import Simplex.Chat.Types.Util import Simplex.FileTransfer.Description (FileDigest) -import Simplex.Messaging.Agent.Protocol (ACommandTag (..), ACorrId, AParty (..), APartyCmdTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, RcvFileId, SAEntity (..), SndFileId, UserId) +import Simplex.FileTransfer.Types (RcvFileId, SndFileId) +import Simplex.Messaging.Agent.Protocol (ACorrId, AEventTag (..), AEvtTag (..), ConnId, ConnectionMode (..), ConnectionRequestUri, InvitationId, SAEntity (..), UserId) import Simplex.Messaging.Crypto.File (CryptoFileArgs (..)) import Simplex.Messaging.Crypto.Ratchet (PQEncryption (..), PQSupport, pattern PQEncOff) import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, sumTypeJSON, taggedObjectJSON) -import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI) import Simplex.Messaging.Util (safeDecodeUtf8, (<$?>)) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal @@ -125,7 +124,6 @@ data User = User data NewUser = NewUser { profile :: Maybe Profile, - sameServers :: Bool, pastTimestamp :: Bool } deriving (Show) @@ -178,6 +176,7 @@ data Contact = Contact contactGroupMemberId :: Maybe GroupMemberId, contactGrpInvSent :: Bool, uiThemes :: Maybe UIThemeEntityOverrides, + chatDeleted :: Bool, customData :: Maybe CustomData } deriving (Eq, Show) @@ -227,7 +226,7 @@ contactActive :: Contact -> Bool contactActive Contact {contactStatus} = contactStatus == CSActive contactDeleted :: Contact -> Bool -contactDeleted Contact {contactStatus} = contactStatus == CSDeleted +contactDeleted Contact {contactStatus} = contactStatus == CSDeleted || contactStatus == CSDeletedByUser contactSecurityCode :: Contact -> Maybe SecurityCode contactSecurityCode Contact {activeConn} = connectionCode =<< activeConn @@ -237,7 +236,8 @@ contactPQEnabled Contact {activeConn} = maybe PQEncOff connPQEnabled activeConn data ContactStatus = CSActive - | CSDeleted -- contact deleted by contact + | CSDeleted + | CSDeletedByUser deriving (Eq, Show, Ord) instance FromField ContactStatus where fromField = fromTextField_ textDecode @@ -255,10 +255,12 @@ instance TextEncoding ContactStatus where textDecode = \case "active" -> Just CSActive "deleted" -> Just CSDeleted + "deletedByUser" -> Just CSDeletedByUser _ -> Nothing textEncode = \case CSActive -> "active" CSDeleted -> "deleted" + CSDeletedByUser -> "deletedByUser" data ContactRef = ContactRef { contactId :: ContactId, @@ -1069,7 +1071,8 @@ data RcvFileTransfer = RcvFileTransfer data XFTPRcvFile = XFTPRcvFile { rcvFileDescription :: RcvFileDescr, agentRcvFileId :: Maybe AgentRcvFileId, - agentRcvFileDeleted :: Bool + agentRcvFileDeleted :: Bool, + userApprovedRelays :: Bool } deriving (Eq, Show) @@ -1299,6 +1302,7 @@ data Connection = Connection pqSndEnabled :: Maybe PQEncryption, pqRcvEnabled :: Maybe PQEncryption, authErrCounter :: Int, + quotaErrCounter :: Int, -- if exceeds limit messages to group members are created as pending; sending to contacts is unaffected by this createdAt :: UTCTime } deriving (Eq, Show) @@ -1312,6 +1316,15 @@ authErrDisableCount = 10 connDisabled :: Connection -> Bool connDisabled Connection {authErrCounter} = authErrCounter >= authErrDisableCount +quotaErrInactiveCount :: Int +quotaErrInactiveCount = 5 + +quotaErrSetOnMERR :: Int +quotaErrSetOnMERR = 999 + +connInactive :: Connection -> Bool +connInactive Connection {quotaErrCounter} = quotaErrCounter >= quotaErrInactiveCount + data SecurityCode = SecurityCode {securityCode :: Text, verifiedAt :: UTCTime} deriving (Eq, Show) @@ -1363,7 +1376,7 @@ data ConnStatus ConnRequested | -- | initiating party accepted connection with agent LET command (to be renamed to ACPT) (allowConnection) ConnAccepted - | -- | connection can be sent messages to (after joining party received INFO notification) + | -- | connection can be sent messages to (after joining party received INFO notification, or after securing snd queue on join) ConnSndReady | -- | connection is ready for both parties to send and receive messages ConnReady @@ -1480,9 +1493,6 @@ serializeIntroStatus = \case GMIntroToConnected -> "to-con" GMIntroConnected -> "con" -textParseJSON :: TextEncoding a => String -> J.Value -> JT.Parser a -textParseJSON name = J.withText name $ maybe (fail $ "bad " <> name) pure . textDecode - data NetworkStatus = NSUnknown | NSConnected @@ -1571,19 +1581,19 @@ instance TextEncoding CommandFunction where CFAckMessage -> "ack_message" CFDeleteConn -> "delete_conn" -commandExpectedResponse :: CommandFunction -> APartyCmdTag 'Agent +commandExpectedResponse :: CommandFunction -> AEvtTag commandExpectedResponse = \case CFCreateConnGrpMemInv -> t INV_ CFCreateConnGrpInv -> t INV_ CFCreateConnFileInvDirect -> t INV_ CFCreateConnFileInvGroup -> t INV_ - CFJoinConn -> t OK_ + CFJoinConn -> t JOINED_ CFAllowConn -> t OK_ - CFAcceptContact -> t OK_ + CFAcceptContact -> t JOINED_ CFAckMessage -> t OK_ CFDeleteConn -> t OK_ where - t = APCT SAEConn + t = AEvtTag SAEConn data CommandData = CommandData { cmdId :: CommandId, @@ -1616,14 +1626,6 @@ data NoteFolder = NoteFolder type NoteFolderId = Int64 -data ServerCfg p = ServerCfg - { server :: ProtoServerWithAuth p, - preset :: Bool, - tested :: Maybe Bool, - enabled :: Bool - } - deriving (Show) - data ChatVersion instance VersionScope ChatVersion @@ -1751,10 +1753,3 @@ $(JQ.deriveJSON defaultJSON ''Contact) $(JQ.deriveJSON defaultJSON ''ContactRef) $(JQ.deriveJSON defaultJSON ''NoteFolder) - -instance ProtocolTypeI p => ToJSON (ServerCfg p) where - toEncoding = $(JQ.mkToEncoding defaultJSON ''ServerCfg) - toJSON = $(JQ.mkToJSON defaultJSON ''ServerCfg) - -instance ProtocolTypeI p => FromJSON (ServerCfg p) where - parseJSON = $(JQ.mkParseJSON defaultJSON ''ServerCfg) diff --git a/src/Simplex/Chat/Types/Preferences.hs b/src/Simplex/Chat/Types/Preferences.hs index 4cf9f862d2..bccfd4bdce 100644 --- a/src/Simplex/Chat/Types/Preferences.hs +++ b/src/Simplex/Chat/Types/Preferences.hs @@ -36,7 +36,7 @@ import Simplex.Chat.Types.Shared import Simplex.Chat.Types.Util import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_, sumTypeJSON) -import Simplex.Messaging.Util (safeDecodeUtf8, (<$?>)) +import Simplex.Messaging.Util (decodeJSON, encodeJSON, safeDecodeUtf8, (<$?>)) data ChatFeature = CFTimedMessages diff --git a/src/Simplex/Chat/Types/UITheme.hs b/src/Simplex/Chat/Types/UITheme.hs index 9f9c106d1f..cc5290aa69 100644 --- a/src/Simplex/Chat/Types/UITheme.hs +++ b/src/Simplex/Chat/Types/UITheme.hs @@ -7,26 +7,22 @@ module Simplex.Chat.Types.UITheme where import Data.Aeson (FromJSON (..), ToJSON (..)) import qualified Data.Aeson as J +import qualified Data.Aeson.Encoding as JE +import qualified Data.Aeson.Key as JK import qualified Data.Aeson.TH as JQ -import qualified Data.Attoparsec.ByteString.Char8 as A import Data.Char (toLower) import Data.Maybe (fromMaybe) +import Data.Text (Text) import Database.SQLite.Simple.FromField (FromField (..)) import Database.SQLite.Simple.ToField (ToField (..)) import Simplex.Chat.Types.Util import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, fromTextField_) -import Simplex.Messaging.Util ((<$?>)) - -data UIThemes = UIThemes - { light :: Maybe UITheme, - dark :: Maybe UITheme, - simplex :: Maybe UITheme - } - deriving (Eq, Show) +import Simplex.Messaging.Util (decodeJSON, encodeJSON) data UITheme = UITheme - { base :: ThemeColorScheme, + { themeId :: Text, + base :: ThemeColorScheme, wallpaper :: Maybe ChatWallpaper, colors :: UIColors } @@ -48,40 +44,72 @@ data UIThemeEntityOverride = UIThemeEntityOverride } deriving (Eq, Show) -data ThemeColorScheme = TCSLight | TCSDark | TCSSimplex - deriving (Eq, Show) +data DarkColorScheme = DCSDark | DCSBlack | DCSSimplex + deriving (Eq, Ord, Show) -data UIColorScheme - = UCSSystem - | UCSLight - | UCSDark - | UCSSimplex - deriving (Show) +data ThemeColorScheme = TCSLight | TCSDark DarkColorScheme + deriving (Eq, Ord, Show) -data DarkColorScheme = DCSDark | DCSSimplex - deriving (Show) +data UIColorScheme = UCSSystem | UCSFixed ThemeColorScheme + deriving (Eq, Ord, Show) -instance StrEncoding ThemeColorScheme where - strEncode = \case +instance TextEncoding DarkColorScheme where + textEncode = \case + DCSDark -> "DARK" + DCSBlack -> "BLACK" + DCSSimplex -> "SIMPLEX" + textDecode s = + Just $ case s of + "DARK" -> DCSDark + "BLACK" -> DCSBlack + "SIMPLEX" -> DCSSimplex + _ -> DCSDark + +instance TextEncoding ThemeColorScheme where + textEncode = \case TCSLight -> "LIGHT" - TCSDark -> "DARK" - TCSSimplex -> "SIMPLEX" - strDecode = \case - "LIGHT" -> Right TCSLight - "DARK" -> Right TCSDark - "SIMPLEX" -> Right TCSSimplex - _ -> Left "bad ColorScheme" - strP = strDecode <$?> A.takeTill (== ' ') + TCSDark s -> textEncode s + textDecode = \case + "LIGHT" -> Just TCSLight + s -> TCSDark <$> textDecode s + +instance TextEncoding UIColorScheme where + textEncode = \case + UCSSystem -> "SYSTEM" + UCSFixed s -> textEncode s + textDecode = \case + "SYSTEM" -> Just UCSSystem + s -> UCSFixed <$> textDecode s + +instance FromJSON DarkColorScheme where + parseJSON = textParseJSON "DarkColorScheme" + +instance ToJSON DarkColorScheme where + toJSON = J.String . textEncode + toEncoding = JE.text . textEncode instance FromJSON ThemeColorScheme where - parseJSON = strParseJSON "ThemeColorScheme" + parseJSON = textParseJSON "ThemeColorScheme" instance ToJSON ThemeColorScheme where - toJSON = strToJSON - toEncoding = strToJEncoding + toJSON = J.String . textEncode + toEncoding = JE.text . textEncode + +instance FromJSON UIColorScheme where + parseJSON = textParseJSON "UIColorScheme" + +instance ToJSON UIColorScheme where + toJSON = J.String . textEncode + toEncoding = JE.text . textEncode + +instance J.FromJSONKey ThemeColorScheme where + fromJSONKey = J.FromJSONKeyText $ fromMaybe (TCSDark DCSDark) . textDecode + +instance J.ToJSONKey ThemeColorScheme where + toJSONKey = J.ToJSONKeyText (JK.fromText . textEncode) (JE.text . textEncode) data ChatWallpaper = ChatWallpaper - { preset :: Maybe ChatWallpaperPreset, + { preset :: Maybe Text, imageFile :: Maybe FilePath, background :: Maybe UIColor, tint :: Maybe UIColor, @@ -101,26 +129,16 @@ data UIColors = UIColors background :: Maybe UIColor, menus :: Maybe UIColor, title :: Maybe UIColor, + accentVariant2 :: Maybe UIColor, sentMessage :: Maybe UIColor, - receivedMessage :: Maybe UIColor + sentReply :: Maybe UIColor, + receivedMessage :: Maybe UIColor, + receivedReply :: Maybe UIColor } deriving (Eq, Show) defaultUIColors :: UIColors -defaultUIColors = UIColors Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing - -data ChatWallpaperPreset - = CWPKids - | CWPCats - | CWPPets - | CWPFlowers - | CWPHearts - | CWPSocial - | CWPTravel - | CWPInternet - | CWPSpace - | CWPSchool - deriving (Eq, Show) +defaultUIColors = UIColors Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing newtype UIColor = UIColor String deriving (Eq, Show) @@ -137,16 +155,10 @@ instance ToJSON UIColor where toJSON (UIColor t) = J.toJSON t toEncoding (UIColor t) = J.toEncoding t -$(JQ.deriveJSON (enumJSON $ dropPrefix "DCS") ''DarkColorScheme) - $(JQ.deriveJSON (enumJSON $ dropPrefix "UCM") ''UIColorMode) -$(JQ.deriveJSON (enumJSON $ dropPrefix "UCS") ''UIColorScheme) - $(JQ.deriveJSON (enumJSON $ dropPrefix "CWS") ''ChatWallpaperScale) -$(JQ.deriveJSON (enumJSON $ dropPrefix "CWP") ''ChatWallpaperPreset) - $(JQ.deriveJSON defaultJSON ''ChatWallpaper) $(JQ.deriveJSON defaultJSON ''UIColors) @@ -157,8 +169,6 @@ $(JQ.deriveJSON defaultJSON ''UIThemeEntityOverrides) $(JQ.deriveJSON defaultJSON ''UITheme) -$(JQ.deriveJSON defaultJSON ''UIThemes) - instance ToField UIThemeEntityOverrides where toField = toField . encodeJSON diff --git a/src/Simplex/Chat/Types/Util.hs b/src/Simplex/Chat/Types/Util.hs index 0f41931acf..47edf8eaf8 100644 --- a/src/Simplex/Chat/Types/Util.hs +++ b/src/Simplex/Chat/Types/Util.hs @@ -2,24 +2,18 @@ module Simplex.Chat.Types.Util where -import Data.Aeson (FromJSON, ToJSON) import qualified Data.Aeson as J +import qualified Data.Aeson.Types as JT import Data.ByteString (ByteString) -import qualified Data.ByteString.Lazy.Char8 as LB -import Data.Text (Text) -import Data.Text.Encoding (encodeUtf8) import Data.Typeable import Database.SQLite.Simple (ResultError (..), SQLData (..)) import Database.SQLite.Simple.FromField (FieldParser, returnError) import Database.SQLite.Simple.Internal (Field (..)) import Database.SQLite.Simple.Ok (Ok (Ok)) -import Simplex.Messaging.Util (safeDecodeUtf8) +import Simplex.Messaging.Encoding.String -encodeJSON :: ToJSON a => a -> Text -encodeJSON = safeDecodeUtf8 . LB.toStrict . J.encode - -decodeJSON :: FromJSON a => Text -> Maybe a -decodeJSON = J.decode . LB.fromStrict . encodeUtf8 +textParseJSON :: TextEncoding a => String -> J.Value -> JT.Parser a +textParseJSON name = J.withText name $ maybe (fail $ "bad " <> name) pure . textDecode fromBlobField_ :: Typeable k => (ByteString -> Either String k) -> FieldParser k fromBlobField_ p = \case diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index 96b742ffa4..e154b5b902 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -13,7 +13,6 @@ module Simplex.Chat.View where import qualified Data.Aeson as J import qualified Data.Aeson.TH as JQ -import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB import Data.Char (isSpace, toUpper) @@ -51,23 +50,24 @@ import Simplex.Chat.Types import Simplex.Chat.Types.Preferences import Simplex.Chat.Types.Shared import Simplex.Chat.Types.UITheme -import qualified Simplex.FileTransfer.Transport as XFTPTransport +import qualified Simplex.FileTransfer.Transport as XFTP import Simplex.Messaging.Agent.Client (ProtocolTestFailure (..), ProtocolTestStep (..), SubscriptionsInfo (..)) -import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..)) +import Simplex.Messaging.Agent.Env.SQLite (NetworkConfig (..), ServerCfg (..)) import Simplex.Messaging.Agent.Protocol import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) +import Simplex.Messaging.Client (SMPProxyFallback, SMPProxyMode (..), SocksMode (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..)) import qualified Simplex.Messaging.Crypto.Ratchet as CR import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String import Simplex.Messaging.Parsers (dropPrefix, taggedObjectJSON) -import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, ProtoServerWithAuth, ProtocolServer (..), ProtocolTypeI, SProtocolType (..)) +import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType, ProtocolServer (..), ProtocolTypeI, SProtocolType (..)) import qualified Simplex.Messaging.Protocol as SMP import Simplex.Messaging.Transport.Client (TransportHost (..)) -import Simplex.Messaging.Util (bshow, tshow) +import Simplex.Messaging.Util (safeDecodeUtf8, tshow) import Simplex.Messaging.Version hiding (version) -import Simplex.RemoteControl.Types (RCCtrlAddress (..)) +import Simplex.RemoteControl.Types (RCCtrlAddress (..), RCErrorType (..)) import System.Console.ANSI.Types type CurrentTime = UTCTime @@ -90,10 +90,10 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRChatRunning -> ["chat is running"] CRChatStopped -> ["chat stopped"] CRChatSuspended -> ["chat suspended"] - CRApiChats u chats -> ttyUser u $ if testView then testViewChats chats else [plain . bshow $ J.encode chats] + CRApiChats u chats -> ttyUser u $ if testView then testViewChats chats else [viewJSON chats] CRChats chats -> viewChats ts tz chats - CRApiChat u chat -> ttyUser u $ if testView then testViewChat chat else [plain . bshow $ J.encode chat] - CRApiParsedMarkdown ft -> [plain . bshow $ J.encode ft] + CRApiChat u chat -> ttyUser u $ if testView then testViewChat chat else [viewJSON chat] + CRApiParsedMarkdown ft -> [viewJSON ft] CRUserProtoServers u userServers -> ttyUser u $ viewUserServers userServers testView CRServerTestResult u srv testFailure -> ttyUser u $ viewServerTestResult srv testFailure CRChatItemTTL u ttl -> ttyUser u $ viewChatItemTTL ttl @@ -101,6 +101,10 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRContactInfo u ct cStats customUserProfile -> ttyUser u $ viewContactInfo ct cStats customUserProfile CRGroupInfo u g s -> ttyUser u $ viewGroupInfo g s CRGroupMemberInfo u g m cStats -> ttyUser u $ viewGroupMemberInfo g m cStats + CRQueueInfo _ msgInfo qInfo -> + [ "last received msg: " <> maybe "none" viewJSON msgInfo, + "server queue info: " <> viewJSON qInfo + ] CRContactSwitchStarted {} -> ["switch started"] CRGroupMemberSwitchStarted {} -> ["switch started"] CRContactSwitchAborted {} -> ["switch aborted"] @@ -123,7 +127,10 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRChatItemStatusUpdated u ci -> ttyUser u $ viewChatItemStatusUpdated ci ts tz testView showReceipts CRChatItemUpdated u (AChatItem _ _ chat item) -> ttyUser u $ unmuted u chat item $ viewItemUpdate chat item liveItems ts tz CRChatItemNotChanged u ci -> ttyUser u $ viewItemNotChanged ci - CRChatItemDeleted u (AChatItem _ _ chat deletedItem) toItem byUser timed -> ttyUser u $ unmuted u chat deletedItem $ viewItemDelete chat deletedItem toItem byUser timed ts tz testView + CRChatItemsDeleted u deletions byUser timed -> case deletions of + [ChatItemDeletion (AChatItem _ _ chat deletedItem) toItem] -> + ttyUser u $ unmuted u chat deletedItem $ viewItemDelete chat deletedItem toItem byUser timed ts tz testView + deletions' -> ttyUser u [sShow (length deletions') <> " messages deleted"] CRChatItemReaction u added (ACIReaction _ _ chat reaction) -> ttyUser u $ unmutedReaction u chat reaction $ viewItemReaction showReactions chat reaction added ts tz CRChatItemDeletedNotFound u Contact {localDisplayName = c} _ -> ttyUser u [ttyFrom $ c <> "> [deleted - original message not found]"] CRBroadcastSent u mc s f t -> ttyUser u $ viewSentBroadcast mc s f ts tz t @@ -165,6 +172,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRVersionInfo info _ _ -> viewVersionInfo logLevel info CRInvitation u cReq _ -> ttyUser u $ viewConnReqInvitation cReq CRConnectionIncognitoUpdated u c -> ttyUser u $ viewConnectionIncognitoUpdated c + CRConnectionUserChanged u c c' nu -> ttyUser u $ viewConnectionUserChanged u c nu c' CRConnectionPlan u connectionPlan -> ttyUser u $ viewConnectionPlan connectionPlan CRSentConfirmation u _ -> ttyUser u ["confirmation sent!"] CRSentInvitation u _ customUserProfile -> ttyUser u $ viewSentInvitation customUserProfile testView @@ -172,7 +180,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRContactDeleted u c -> ttyUser u [ttyContact' c <> ": contact is deleted"] CRContactDeletedByContact u c -> ttyUser u [ttyFullContact c <> " deleted contact with you"] CRChatCleared u chatInfo -> ttyUser u $ viewChatCleared chatInfo - CRAcceptingContactRequest u c -> ttyUser u [ttyFullContact c <> ": accepting contact request..."] + CRAcceptingContactRequest u c -> ttyUser u $ viewAcceptingContactRequest c CRContactAlreadyExists u c -> ttyUser u [ttyFullContact c <> ": contact already exists"] CRContactRequestAlreadyAccepted u c -> ttyUser u [ttyFullContact c <> ": sent you a duplicate contact request, but you are already connected, no action needed"] CRUserContactLinkCreated u cReq -> ttyUser u $ connReqContact_ "Your new chat address is created!" cReq @@ -207,6 +215,8 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRRcvFileSndCancelled u _ ft -> ttyUser u $ viewRcvFileSndCancelled ft CRRcvFileError u (Just ci) e _ -> ttyUser u $ receivingFile_' hu testView "error" ci <> [sShow e] CRRcvFileError u Nothing e ft -> ttyUser u $ receivingFileStandalone "error" ft <> [sShow e] + CRRcvFileWarning u (Just ci) e _ -> ttyUser u $ receivingFile_' hu testView "warning: " ci <> [sShow e] + CRRcvFileWarning u Nothing e ft -> ttyUser u $ receivingFileStandalone "warning: " ft <> [sShow e] CRSndFileStart u _ ft -> ttyUser u $ sendingFile_ "started" ft CRSndFileComplete u _ ft -> ttyUser u $ sendingFile_ "completed" ft CRSndStandaloneFileCreated u ft -> ttyUser u $ uploadingFileStandalone "started" ft @@ -218,11 +228,14 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRSndFileCancelledXFTP {} -> [] CRSndFileError u Nothing ft e -> ttyUser u $ uploadingFileStandalone "error" ft <> [plain e] CRSndFileError u (Just ci) _ e -> ttyUser u $ uploadingFile "error" ci <> [plain e] + CRSndFileWarning u Nothing ft e -> ttyUser u $ uploadingFileStandalone "warning: " ft <> [plain e] + CRSndFileWarning u (Just ci) _ e -> ttyUser u $ uploadingFile "warning: " ci <> [plain e] CRSndFileRcvCancelled u _ ft@SndFileTransfer {recipientDisplayName = c} -> ttyUser u [ttyContact c <> " cancelled receiving " <> sndFile ft] - CRStandaloneFileInfo info_ -> maybe ["no file information in URI"] (\j -> [plain . LB.toStrict $ J.encode j]) info_ + CRStandaloneFileInfo info_ -> maybe ["no file information in URI"] (\j -> [viewJSON j]) info_ CRContactConnecting u _ -> ttyUser u [] CRContactConnected u ct userCustomProfile -> ttyUser u $ viewContactConnected ct userCustomProfile testView + CRContactSndReady u ct -> ttyUser u [ttyFullContact ct <> ": you can send messages to contact"] CRContactAnotherClient u c -> ttyUser u [ttyContact' c <> ": contact is connected to another client"] CRSubscriptionEnd u acEntity -> let Connection {connId} = entityConnection acEntity @@ -322,7 +335,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe ] CRRemoteFileStored rhId (CryptoFile filePath cfArgs_) -> [plain $ "file " <> filePath <> " stored on remote host " <> show rhId] - <> maybe [] ((: []) . plain . cryptoFileArgsStr testView) cfArgs_ + <> maybe [] ((: []) . cryptoFileArgsStr testView) cfArgs_ CRRemoteCtrlList cs -> viewRemoteCtrls cs CRRemoteCtrlFound {remoteCtrl = RemoteCtrlInfo {remoteCtrlId, ctrlDeviceName}, ctrlAppInfo_, appVersion, compatible} -> [ ("remote controller " <> sShow remoteCtrlId <> " found: ") @@ -342,7 +355,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe ] CRRemoteCtrlConnected RemoteCtrlInfo {remoteCtrlId = rcId, ctrlDeviceName} -> ["remote controller " <> sShow rcId <> " session started with " <> plain ctrlDeviceName] - CRRemoteCtrlStopped {} -> ["remote controller stopped"] + CRRemoteCtrlStopped {rcStopReason} -> viewRemoteCtrlStopped rcStopReason CRContactPQEnabled u c (CR.PQEncryption pqOn) -> ttyUser u [ttyContact' c <> ": " <> (if pqOn then "quantum resistant" else "standard") <> " end-to-end encryption enabled"] CRSQLResult rows -> map plain rows CRSlowSQLQueries {chatQueries, agentQueries} -> @@ -354,10 +367,11 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe in ("Chat queries" : map viewQuery chatQueries) <> [""] <> ("Agent queries" : map viewQuery agentQueries) CRDebugLocks {chatLockName, chatEntityLocks, agentLocks} -> [ maybe "no chat lock" (("chat lock: " <>) . plain) chatLockName, - plain $ "chat entity locks: " <> LB.unpack (J.encode chatEntityLocks), - plain $ "agent locks: " <> LB.unpack (J.encode agentLocks) + "chat entity locks: " <> viewJSON chatEntityLocks, + "agent locks: " <> viewJSON agentLocks ] - CRAgentStats stats -> map (plain . intercalate ",") stats + CRAgentSubsTotal u subsTotal _ -> ttyUser u ["total subscriptions: " <> sShow subsTotal] + CRAgentServersSummary u serversSummary -> ttyUser u ["agent servers summary: " <> viewJSON serversSummary] CRAgentSubs {activeSubs, pendingSubs, removedSubs} -> [plain $ "Subscriptions: active = " <> show (sum activeSubs) <> ", pending = " <> show (sum pendingSubs) <> ", removed = " <> show (sum $ M.map length removedSubs)] <> ("active subscriptions:" : listSubs activeSubs) @@ -370,12 +384,18 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe ("active subscriptions:" : map sShow activeSubscriptions) <> ("pending subscriptions: " : map sShow pendingSubscriptions) <> ("removed subscriptions: " : map sShow removedSubscriptions) - CRAgentWorkersSummary {agentWorkersSummary} -> ["agent workers summary: " <> plain (LB.unpack $ J.encode agentWorkersSummary)] + CRAgentWorkersSummary {agentWorkersSummary} -> ["agent workers summary: " <> viewJSON agentWorkersSummary] CRAgentWorkersDetails {agentWorkersDetails} -> [ "agent workers details:", - plain . LB.unpack $ J.encode agentWorkersDetails -- this would be huge, but copypastable when has its own line + viewJSON agentWorkersDetails -- this would be huge, but copypastable when has its own line ] + CRAgentQueuesInfo {agentQueuesInfo} -> + [ "agent queues info:", + plain . LB.unpack $ J.encode agentQueuesInfo + ] + CRContactDisabled u c -> ttyUser u ["[" <> ttyContact' c <> "] connection is disabled, to enable: " <> highlight ("/enable " <> viewContactName c) <> ", to delete: " <> highlight ("/d " <> viewContactName c)] CRConnectionDisabled entity -> viewConnectionEntityDisabled entity + CRConnectionInactive entity inactive -> viewConnectionEntityInactive entity inactive CRAgentRcvQueueDeleted acId srv aqId err_ -> [ ("completed deleting rcv queue, agent connection id: " <> sShow acId) <> (", server: " <> sShow srv) @@ -386,17 +406,18 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe CRAgentConnDeleted acId -> ["completed deleting connection, agent connection id: " <> sShow acId | logLevel <= CLLInfo] CRAgentUserDeleted auId -> ["completed deleting user" <> if logLevel <= CLLInfo then ", agent user id: " <> sShow auId else ""] CRMessageError u prefix err -> ttyUser u [plain prefix <> ": " <> plain err | prefix == "error" || logLevel <= CLLWarning] - CRChatCmdError u e -> ttyUserPrefix' u $ viewChatError logLevel testView e - CRChatError u e -> ttyUser' u $ viewChatError logLevel testView e - CRChatErrors u errs -> ttyUser' u $ concatMap (viewChatError logLevel testView) errs + CRChatCmdError u e -> ttyUserPrefix' u $ viewChatError True logLevel testView e + CRChatError u e -> ttyUser' u $ viewChatError False logLevel testView e + CRChatErrors u errs -> ttyUser' u $ concatMap (viewChatError False logLevel testView) errs + CRArchiveExported archiveErrs -> if null archiveErrs then ["ok"] else ["archive export errors: " <> plain (show archiveErrs)] CRArchiveImported archiveErrs -> if null archiveErrs then ["ok"] else ["archive import errors: " <> plain (show archiveErrs)] - CRAppSettings as -> ["app settings: " <> plain (LB.unpack $ J.encode as)] + CRAppSettings as -> ["app settings: " <> viewJSON as] CRTimedAction _ _ -> [] - CRCustomChatResponse u r -> ttyUser' u $ [plain r] + CRCustomChatResponse u r -> ttyUser' u $ map plain $ T.lines r where ttyUser :: User -> [StyledString] -> [StyledString] - ttyUser user@User {showNtfs, activeUser} ss - | showNtfs || activeUser = ttyUserPrefix user ss + ttyUser user@User {showNtfs, activeUser, viewPwdHash} ss + | (showNtfs && isNothing viewPwdHash) || activeUser = ttyUserPrefix user ss | otherwise = [] ttyUserPrefix :: User -> [StyledString] -> [StyledString] ttyUserPrefix _ [] = [] @@ -949,6 +970,11 @@ viewSentInvitation incognitoProfile testView = message = ["connection request sent incognito!"] Nothing -> ["connection request sent!"] +viewAcceptingContactRequest :: Contact -> [StyledString] +viewAcceptingContactRequest ct + | contactReady ct = [ttyFullContact ct <> ": accepting contact request, you can send messages to contact"] + | otherwise = [ttyFullContact ct <> ": accepting contact request..."] + viewReceivedContactRequest :: ContactName -> Profile -> [StyledString] viewReceivedContactRequest c Profile {fullName} = [ ttyFullName c fullName <> " wants to connect to you!", @@ -1169,8 +1195,8 @@ viewUserServers (AUPS UserProtoServers {serverProtocol = p, protoServers, preset pName = protocolName p customServers = if null protoServers - then ("no " <> pName <> " servers saved, using presets: ") : viewServers id presetServers - else viewServers (\ServerCfg {server} -> server) protoServers + then ("no " <> pName <> " servers saved, using presets: ") : viewServers presetServers + else viewServers protoServers protocolName :: ProtocolTypeI p => SProtocolType p -> StyledString protocolName = plain . map toUpper . T.unpack . decodeLatin1 . strEncode @@ -1179,11 +1205,11 @@ viewServerTestResult :: AProtoServerWithAuth -> Maybe ProtocolTestFailure -> [St viewServerTestResult (AProtoServerWithAuth p _) = \case Just ProtocolTestFailure {testStep, testError} -> result - <> [pName <> " server requires authorization to create queues, check password" | testStep == TSCreateQueue && testError == SMP SMP.AUTH] - <> [pName <> " server requires authorization to upload files, check password" | testStep == TSCreateFile && testError == XFTP XFTPTransport.AUTH] + <> [pName <> " server requires authorization to create queues, check password" | testStep == TSCreateQueue && (case testError of SMP _ SMP.AUTH -> True; _ -> False)] + <> [pName <> " server requires authorization to upload files, check password" | testStep == TSCreateFile && (case testError of XFTP _ XFTP.AUTH -> True; _ -> False)] <> ["Possibly, certificate fingerprint in " <> pName <> " server address is incorrect" | testStep == TSConnect && brokerErr] where - result = [pName <> " server test failed at " <> plain (drop 2 $ show testStep) <> ", error: " <> plain (strEncode testError)] + result = [pName <> " server test failed at " <> plain (drop 2 $ show testStep) <> ", error: " <> sShow testError] brokerErr = case testError of BROKER _ NETWORK -> True _ -> False @@ -1203,12 +1229,17 @@ viewChatItemTTL = \case deletedAfter ttlStr = ["old messages are set to be deleted after: " <> ttlStr] viewNetworkConfig :: NetworkConfig -> [StyledString] -viewNetworkConfig NetworkConfig {socksProxy, tcpTimeout} = - [ plain $ maybe "direct network connection" (("using SOCKS5 proxy " <>) . show) socksProxy, +viewNetworkConfig NetworkConfig {socksProxy, socksMode, tcpTimeout, smpProxyMode, smpProxyFallback} = + [ plain $ maybe "direct network connection" ((\sp -> "using SOCKS5 proxy " <> sp <> if socksMode == SMOnion then " for onion servers ONLY." else " for ALL servers.") . show) socksProxy, "TCP timeout: " <> sShow tcpTimeout, - "use " <> highlight' "/network socks=[ timeout=]" <> " to change settings" + plain $ smpProxyModeStr smpProxyMode smpProxyFallback, + "use " <> highlight' "/network socks=[ socks-mode=always/onion][ smp-proxy=always/unknown/unprotected/never][ smp-proxy-fallback=no/protected/yes][ timeout=]" <> " to change settings" ] +smpProxyModeStr :: SMPProxyMode -> SMPProxyFallback -> String +smpProxyModeStr SPMNever _ = "private message routing disabled." +smpProxyModeStr mode fallback = T.unpack $ safeDecodeUtf8 $ "private message routing mode: " <> strEncode mode <> ", fallback: " <> strEncode fallback + viewContactInfo :: Contact -> Maybe ConnectionStats -> Maybe Profile -> [StyledString] viewContactInfo ct@Contact {contactId, profile = LocalProfile {localAlias, contactLink}, activeConn, uiThemes, customData} stats incognitoProfile = ["contact ID: " <> sShow contactId] @@ -1234,10 +1265,10 @@ viewGroupInfo GroupInfo {groupId, uiThemes, customData} s = <> viewCustomData customData viewUITheme :: Maybe UIThemeEntityOverrides -> [StyledString] -viewUITheme = maybe [] (\uiThemes -> ["UI themes: " <> plain (LB.toStrict $ J.encode uiThemes)]) +viewUITheme = maybe [] (\uiThemes -> ["UI themes: " <> viewJSON uiThemes]) viewCustomData :: Maybe CustomData -> [StyledString] -viewCustomData = maybe [] (\(CustomData v) -> ["custom data: " <> plain (LB.toStrict . J.encode $ J.Object v)]) +viewCustomData = maybe [] (\(CustomData v) -> ["custom data: " <> viewJSON (J.Object v)]) viewGroupMemberInfo :: GroupInfo -> GroupMember -> Maybe ConnectionStats -> [StyledString] viewGroupMemberInfo GroupInfo {groupId} m@GroupMember {groupMemberId, memberProfile = LocalProfile {localAlias, contactLink}, activeConn} stats = @@ -1262,8 +1293,8 @@ viewConnectionStats ConnectionStats {rcvQueuesInfo, sndQueuesInfo} = ["receiving messages via: " <> viewRcvQueuesInfo rcvQueuesInfo | not $ null rcvQueuesInfo] <> ["sending messages via: " <> viewSndQueuesInfo sndQueuesInfo | not $ null sndQueuesInfo] -viewServers :: ProtocolTypeI p => (a -> ProtoServerWithAuth p) -> NonEmpty a -> [StyledString] -viewServers f = map (plain . B.unpack . strEncode . f) . L.toList +viewServers :: ProtocolTypeI p => NonEmpty (ServerCfg p) -> [StyledString] +viewServers = map (plain . B.unpack . strEncode . (\ServerCfg {server} -> server)) . L.toList viewRcvQueuesInfo :: [RcvQueueInfo] -> StyledString viewRcvQueuesInfo = plain . intercalate ", " . map showQueueInfo @@ -1468,6 +1499,20 @@ viewConnectionIncognitoUpdated PendingContactConnection {pccConnId, customUserPr | isJust customUserProfileId = ["connection " <> sShow pccConnId <> " changed to incognito"] | otherwise = ["connection " <> sShow pccConnId <> " changed to non incognito"] +viewConnectionUserChanged :: User -> PendingContactConnection -> User -> PendingContactConnection -> [StyledString] +viewConnectionUserChanged User {localDisplayName = n} PendingContactConnection {pccConnId, connReqInv} User {localDisplayName = n'} PendingContactConnection {connReqInv = connReqInv'} = + case (connReqInv, connReqInv') of + (Just cReqInv, Just cReqInv') + | cReqInv /= cReqInv' -> [userChangedStr <> ", new link:"] <> newLink cReqInv' + _ -> [userChangedStr] + where + userChangedStr = "connection " <> sShow pccConnId <> " changed from user " <> plain n <> " to user " <> plain n' + newLink cReqInv = + [ "", + (plain . strEncode) (simplexChatInvitation cReqInv), + "" + ] + viewConnectionPlan :: ConnectionPlan -> [StyledString] viewConnectionPlan = \case CPInvitationLink ilp -> case ilp of @@ -1679,7 +1724,7 @@ receivingFile_' :: (Maybe RemoteHostId, Maybe User) -> Bool -> String -> AChatIt receivingFile_' hu testView status (AChatItem _ _ chat ChatItem {file = Just CIFile {fileId, fileName, fileSource = Just f@(CryptoFile _ cfArgs_)}, chatDir}) = [plain status <> " receiving " <> fileTransferStr fileId fileName <> fileFrom chat chatDir] <> cfArgsStr cfArgs_ <> getRemoteFileStr where - cfArgsStr (Just cfArgs) = [plain (cryptoFileArgsStr testView cfArgs) | status == "completed"] + cfArgsStr (Just cfArgs) = [cryptoFileArgsStr testView cfArgs | status == "completed"] cfArgsStr _ = [] getRemoteFileStr = case hu of (Just rhId, Just User {userId}) @@ -1700,10 +1745,10 @@ viewLocalFile to CIFile {fileId, fileSource} ts tz = case fileSource of Just (CryptoFile fPath _) -> sentWithTime_ ts tz [to <> fileTransferStr fileId fPath] _ -> const [] -cryptoFileArgsStr :: Bool -> CryptoFileArgs -> ByteString +cryptoFileArgsStr :: Bool -> CryptoFileArgs -> StyledString cryptoFileArgsStr testView cfArgs@(CFArgs key nonce) - | testView = LB.toStrict $ J.encode cfArgs - | otherwise = "encryption key: " <> strEncode key <> ", nonce: " <> strEncode nonce + | testView = viewJSON cfArgs + | otherwise = plain $ "encryption key: " <> strEncode key <> ", nonce: " <> strEncode nonce fileFrom :: ChatInfo c -> CIDirection c d -> StyledString fileFrom (DirectChat ct) CIDirectRcv = " from " <> ttyContact' ct @@ -1760,13 +1805,16 @@ viewFileTransferStatusXFTP (AChatItem _ _ _ ChatItem {file = Just CIFile {fileId CIFSSndTransfer progress total -> ["sending " <> fstr <> " in progress " <> fileProgressXFTP progress total fileSize] CIFSSndCancelled -> ["sending " <> fstr <> " cancelled"] CIFSSndComplete -> ["sending " <> fstr <> " complete"] - CIFSSndError -> ["sending " <> fstr <> " error"] + CIFSSndError sndFileErr -> ["sending " <> fstr <> " error: " <> plain (show sndFileErr)] + CIFSSndWarning sndFileErr -> ["sending " <> fstr <> " warning: " <> plain (show sndFileErr)] CIFSRcvInvitation -> ["receiving " <> fstr <> " not accepted yet, use " <> highlight ("/fr " <> show fileId) <> " to receive file"] CIFSRcvAccepted -> ["receiving " <> fstr <> " just started"] CIFSRcvTransfer progress total -> ["receiving " <> fstr <> " progress " <> fileProgressXFTP progress total fileSize] + CIFSRcvAborted -> ["receiving " <> fstr <> " aborted, use " <> highlight ("/fr " <> show fileId) <> " to receive file"] CIFSRcvComplete -> ["receiving " <> fstr <> " complete" <> maybe "" (\(CryptoFile fp _) -> ", path: " <> plain fp) fileSource] CIFSRcvCancelled -> ["receiving " <> fstr <> " cancelled"] - CIFSRcvError -> ["receiving " <> fstr <> " error"] + CIFSRcvError rcvFileErr -> ["receiving " <> fstr <> " error: " <> plain (show rcvFileErr)] + CIFSRcvWarning rcvFileErr -> ["receiving " <> fstr <> " warning: " <> plain (show rcvFileErr)] CIFSInvalid text -> [fstr <> " invalid status: " <> plain text] where fstr = fileTransferStr fileId fileName @@ -1820,7 +1868,7 @@ viewCallAnswer ct WebRTCSession {rtcSession = answer, rtcIceCandidates = iceCand [ ttyContact' ct <> " continued the WebRTC call", "To connect, please paste the data below in your browser window you opened earlier and click Connect button", "", - plain . LB.toStrict . J.encode $ WCCallAnswer {answer, iceCandidates} + viewJSON WCCallAnswer {answer, iceCandidates} ] callMediaStr :: CallType -> StyledString @@ -1891,8 +1939,14 @@ viewRemoteCtrl CtrlAppInfo {deviceName, appVersionRange = AppVersionRange _ (App | otherwise = "" showCompatible = if compatible then "" else ", " <> bold' "not compatible" -viewChatError :: ChatLogLevel -> Bool -> ChatError -> [StyledString] -viewChatError logLevel testView = \case +viewRemoteCtrlStopped :: RemoteCtrlStopReason -> [StyledString] +viewRemoteCtrlStopped = \case + RCSRConnectionFailed (ChatErrorAgent (RCP RCEIdentity) _) -> + ["remote controller stopped: this link was used with another controller, please create a new link on the host"] + _ -> ["remote controller stopped"] + +viewChatError :: Bool -> ChatLogLevel -> Bool -> ChatError -> [StyledString] +viewChatError isCmd logLevel testView = \case ChatError err -> case err of CENoActiveUser -> ["error: active user is required"] CENoConnectionUser agentConnId -> ["error: message user not found, conn id: " <> sShow agentConnId | logLevel <= CLLError] @@ -1950,7 +2004,6 @@ viewChatError logLevel testView = \case ] CEGroupMemberUserRemoved -> ["you are no longer a member of the group"] CEGroupMemberNotFound -> ["group doesn't have this member"] - CEGroupMemberIntroNotFound c -> ["group member intro not found for " <> ttyContact c] CEGroupCantResendInvitation g c -> viewCannotResendInvitation g c CEGroupInternal s -> ["chat group bug: " <> plain s] CEFileNotFound f -> ["file not found: " <> plain f] @@ -1967,8 +2020,7 @@ viewChatError logLevel testView = \case CEFileImageType _ -> ["image type must be jpg, send as a file using " <> highlight' "/f"] CEFileImageSize _ -> ["max image size: " <> sShow maxImageSize <> " bytes, resize it or send as a file using " <> highlight' "/f"] CEFileNotReceived fileId -> ["file " <> sShow fileId <> " not received"] - CEXFTPRcvFile fileId aFileId e -> ["error receiving XFTP file " <> sShow fileId <> ", agent file id " <> sShow aFileId <> ": " <> sShow e | logLevel == CLLError] - CEXFTPSndFile fileId aFileId e -> ["error sending XFTP file " <> sShow fileId <> ", agent file id " <> sShow aFileId <> ": " <> sShow e | logLevel == CLLError] + CEFileNotApproved fileId unknownSrvs -> ["file " <> sShow fileId <> " aborted, unknwon XFTP servers:"] <> map (plain . show) unknownSrvs CEFallbackToSMPProhibited fileId -> ["recipient tried to accept file " <> sShow fileId <> " via old protocol, prohibited"] CEInlineFileProhibited _ -> ["A small file sent without acceptance - you can enable receiving such files with -f option."] CEInvalidQuote -> ["cannot reply to this message"] @@ -1988,6 +2040,7 @@ viewChatError logLevel testView = \case CEAgentCommandError e -> ["agent command error: " <> plain e] CEInvalidFileDescription e -> ["invalid file description: " <> plain e] CEConnectionIncognitoChangeProhibited -> ["incognito mode change prohibited"] + CEConnectionUserChangeProhibited -> ["incognito mode change prohibited for user"] CEPeerChatVRangeIncompatible -> ["peer chat protocol version range incompatible"] CEInternalError e -> ["internal chat error: " <> plain e] CEException e -> ["exception: " <> plain e] @@ -2024,18 +2077,20 @@ viewChatError logLevel testView = \case DBErrorOpen e -> ["error opening database after encryption: " <> sqliteError' e] e -> ["chat database error: " <> sShow e] ChatErrorAgent err entity_ -> case err of - CMD PROHIBITED -> [withConnEntity <> "error: command is prohibited"] - SMP SMP.AUTH -> + CMD PROHIBITED cxt -> [withConnEntity <> plain ("error: command is prohibited, " <> cxt)] + SMP _ SMP.AUTH -> [ withConnEntity <> "error: connection authorization failed - this could happen if connection was deleted,\ \ secured with different credentials, or due to a bug - please re-create the connection" ] - AGENT A_DUPLICATE -> [withConnEntity <> "error: AGENT A_DUPLICATE" | logLevel == CLLDebug] - AGENT A_PROHIBITED -> [withConnEntity <> "error: AGENT A_PROHIBITED" | logLevel <= CLLWarning] - CONN NOT_FOUND -> [withConnEntity <> "error: CONN NOT_FOUND" | logLevel <= CLLWarning] + BROKER _ NETWORK | not isCmd -> [] + BROKER _ TIMEOUT | not isCmd -> [] + AGENT A_DUPLICATE -> [withConnEntity <> "error: AGENT A_DUPLICATE" | logLevel == CLLDebug || isCmd] + AGENT (A_PROHIBITED e) -> [withConnEntity <> "error: AGENT A_PROHIBITED, " <> plain e | logLevel <= CLLWarning || isCmd] + CONN NOT_FOUND -> [withConnEntity <> "error: CONN NOT_FOUND" | logLevel <= CLLWarning || isCmd] CRITICAL restart e -> [plain $ "critical error: " <> e] <> ["please restart the app" | restart] INTERNAL e -> [plain $ "internal error: " <> e] - e -> [withConnEntity <> "smp agent error: " <> sShow e | logLevel <= CLLWarning] + e -> [withConnEntity <> "smp agent error: " <> sShow e | logLevel <= CLLWarning || isCmd] where withConnEntity = case entity_ of Just entity@(RcvDirectMsgConnection conn contact_) -> case contact_ of @@ -2071,6 +2126,14 @@ viewConnectionEntityDisabled entity = case entity of where entityLabel = connEntityLabel entity +viewConnectionEntityInactive :: ConnectionEntity -> Bool -> [StyledString] +viewConnectionEntityInactive entity inactive + | inactive = ["[" <> connEntityLabel entity <> "] connection is marked as inactive"] + | otherwise = ["[" <> connEntityLabel entity <> "] inactive connection is marked as active"] + +viewJSON :: J.ToJSON a => a -> StyledString +viewJSON = plain . LB.toStrict . J.encode + connEntityLabel :: ConnectionEntity -> StyledString connEntityLabel = \case RcvDirectMsgConnection _ (Just Contact {localDisplayName = c}) -> plain c diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index 34ab002d37..e3d557166b 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -25,7 +25,7 @@ import Data.Maybe (isNothing) import qualified Data.Text as T import Network.Socket import Simplex.Chat -import Simplex.Chat.Controller (ChatCommand (..), ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..)) +import Simplex.Chat.Controller (ChatCommand (..), ChatConfig (..), ChatController (..), ChatDatabase (..), ChatLogLevel (..), defaultSimpleNetCfg) import Simplex.Chat.Core import Simplex.Chat.Options import Simplex.Chat.Protocol (currentChatVersion, pqEncryptionCompressionVersion) @@ -36,20 +36,23 @@ import Simplex.Chat.Terminal.Output (newChatTerminal) import Simplex.Chat.Types import Simplex.FileTransfer.Description (kb, mb) import Simplex.FileTransfer.Server (runXFTPServerBlocking) -import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defaultFileExpiration) +import Simplex.FileTransfer.Server.Env (XFTPServerConfig (..), defaultFileExpiration, supportedXFTPhandshakes) +import Simplex.FileTransfer.Transport (supportedFileServerVRange) import Simplex.Messaging.Agent (disposeAgentClient) import Simplex.Messaging.Agent.Env.SQLite import Simplex.Messaging.Agent.Protocol (currentSMPAgentVersion, duplexHandshakeSMPAgentVersion, pqdrSMPAgentVersion, supportedSMPAgentVRange) import Simplex.Messaging.Agent.RetryInterval import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), closeSQLiteStore) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB -import Simplex.Messaging.Client (ProtocolClientConfig (..), defaultNetworkConfig) +import Simplex.Messaging.Client (ProtocolClientConfig (..)) +import Simplex.Messaging.Client.Agent (defaultSMPClientAgentConfig) import Simplex.Messaging.Crypto.Ratchet (supportedE2EEncryptVRange) import qualified Simplex.Messaging.Crypto.Ratchet as CR +import Simplex.Messaging.Protocol (srvHostnamesSMPClientVersion) import Simplex.Messaging.Server (runSMPServerBlocking) import Simplex.Messaging.Server.Env.STM import Simplex.Messaging.Transport -import Simplex.Messaging.Transport.Server (defaultTransportServerConfig) +import Simplex.Messaging.Transport.Server (TransportServerConfig (..), defaultTransportServerConfig) import Simplex.Messaging.Version import Simplex.Messaging.Version.Internal import System.Directory (createDirectoryIfMissing, removeDirectoryRecursive) @@ -92,14 +95,15 @@ testCoreOpts = -- dbKey = "this is a pass-phrase to encrypt the database", smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001"], xftpServers = ["xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002"], - networkConfig = defaultNetworkConfig, + simpleNetCfg = defaultSimpleNetCfg, logLevel = CLLImportant, logConnections = False, logServerHosts = False, logAgent = Nothing, logFile = Nothing, tbqSize = 16, - highlyAvailable = False + highlyAvailable = False, + yesToUpMigrations = False } getTestOpts :: Bool -> ScrubbedBytes -> ChatOpts @@ -129,8 +133,15 @@ aCfg = (agentConfig defaultChatConfig) {tbqSize = 16} testAgentCfg :: AgentConfig testAgentCfg = aCfg - { reconnectInterval = (reconnectInterval aCfg) {initialInterval = 50000}, - xftpNotifyErrsOnRetry = False + { reconnectInterval = (reconnectInterval aCfg) {initialInterval = 50000} + } + +testAgentCfgSlow :: AgentConfig +testAgentCfgSlow = + testAgentCfg + { smpClientVRange = mkVersionRange (Version 1) srvHostnamesSMPClientVersion, -- v2 + smpAgentVRange = mkVersionRange duplexHandshakeSMPAgentVersion pqdrSMPAgentVersion, -- v5 + smpCfg = (smpCfg testAgentCfg) {serverVRange = mkVersionRange batchCmdsSMPVersion sendingProxySMPVersion} -- v8 } testCfg :: ChatConfig @@ -142,6 +153,9 @@ testCfg = tbqSize = 16 } +testCfgSlow :: ChatConfig +testCfgSlow = testCfg {agentConfig = testAgentCfgSlow} + testAgentCfgVPrev :: AgentConfig testAgentCfgVPrev = testAgentCfg @@ -210,7 +224,11 @@ testCfgCreateGroupDirect = mkCfgCreateGroupDirect testCfg mkCfgCreateGroupDirect :: ChatConfig -> ChatConfig -mkCfgCreateGroupDirect cfg = cfg {chatVRange = groupCreateDirectVRange} +mkCfgCreateGroupDirect cfg = + cfg + { chatVRange = groupCreateDirectVRange, + agentConfig = testAgentCfgSlow + } groupCreateDirectVRange :: VersionRangeChat groupCreateDirectVRange = mkVersionRange (VersionChat 1) (VersionChat 1) @@ -399,8 +417,8 @@ testChatCfg4 cfg p1 p2 p3 p4 test = testChatN cfg testOpts [p1, p2, p3, p4] test concurrentlyN_ :: [IO a] -> IO () concurrentlyN_ = mapConcurrently_ id -serverCfg :: ServerConfig -serverCfg = +smpServerCfg :: ServerConfig +smpServerCfg = ServerConfig { transports = [(serverPort, transport @TLS)], tbqSize = 1, @@ -425,13 +443,20 @@ serverCfg = serverStatsLogFile = "tests/smp-server-stats.daily.log", serverStatsBackupFile = Nothing, smpServerVRange = supportedServerSMPRelayVRange, - transportConfig = defaultTransportServerConfig, + transportConfig = defaultTransportServerConfig {alpn = Just supportedSMPHandshakes}, smpHandshakeTimeout = 1000000, - controlPort = Nothing + controlPort = Nothing, + smpAgentCfg = defaultSMPClientAgentConfig, + allowSMPProxy = True, + serverClientConcurrency = 16, + information = Nothing } withSmpServer :: IO () -> IO () -withSmpServer = serverBracket (`runSMPServerBlocking` serverCfg) +withSmpServer = withSmpServer' smpServerCfg + +withSmpServer' :: ServerConfig -> IO () -> IO () +withSmpServer' cfg = serverBracket (`runSMPServerBlocking` cfg) xftpTestPort :: ServiceName xftpTestPort = "7002" @@ -458,12 +483,13 @@ xftpServerConfig = caCertificateFile = "tests/fixtures/tls/ca.crt", privateKeyFile = "tests/fixtures/tls/server.key", certificateFile = "tests/fixtures/tls/server.crt", + xftpServerVRange = supportedFileServerVRange, logStatsInterval = Nothing, logStatsStartTime = 0, serverStatsLogFile = "tests/tmp/xftp-server-stats.daily.log", serverStatsBackupFile = Nothing, controlPort = Nothing, - transportConfig = defaultTransportServerConfig, + transportConfig = defaultTransportServerConfig {alpn = Just supportedXFTPhandshakes}, responseDelay = 0 } diff --git a/tests/ChatTests/Direct.hs b/tests/ChatTests/Direct.hs index 1579dedaa7..09b2d7d51c 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -15,6 +15,7 @@ import Data.Aeson (ToJSON) import qualified Data.Aeson as J import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as LB +import Data.List (intercalate) import qualified Data.Text as T import Simplex.Chat.AppSettings (defaultAppSettings) import qualified Simplex.Chat.AppSettings as AS @@ -37,11 +38,15 @@ chatDirectTests = do describe "add contact and send/receive messages" testAddContact it "clear chat with contact" testContactClear it "deleting contact deletes profile" testDeleteContactDeletesProfile + it "delete contact keeping conversation" testDeleteContactKeepConversation + it "delete conversation keeping contact" testDeleteConversationKeepContact it "unused contact is deleted silently" testDeleteUnusedContactSilent it "direct message quoted replies" testDirectMessageQuotedReply it "direct message update" testDirectMessageUpdate it "direct message edit history" testDirectMessageEditHistory it "direct message delete" testDirectMessageDelete + it "direct message delete multiple" testDirectMessageDeleteMultiple + it "direct message delete multiple (many chat batches)" testDirectMessageDeleteMultipleManyBatches it "direct live message" testDirectLiveMessage it "direct timed message" testDirectTimedMessage it "repeat AUTH errors disable contact" testRepeatAuthErrorsDisableContact @@ -61,11 +66,22 @@ chatDirectTests = do it "get and set XFTP servers" testGetSetXFTPServers it "test XFTP server connection" testTestXFTPServer describe "async connection handshake" $ do - it "connect when initiating client goes offline" testAsyncInitiatingOffline - it "connect when accepting client goes offline" testAsyncAcceptingOffline + describe "connect when initiating client goes offline" $ do + it "curr" $ testAsyncInitiatingOffline testCfg testCfg + it "v5" $ testAsyncInitiatingOffline testCfgSlow testCfgSlow + it "v5/curr" $ testAsyncInitiatingOffline testCfgSlow testCfg + it "curr/v5" $ testAsyncInitiatingOffline testCfg testCfgSlow + describe "connect when accepting client goes offline" $ do + it "curr" $ testAsyncAcceptingOffline testCfg testCfg + it "v5" $ testAsyncAcceptingOffline testCfgSlow testCfgSlow + it "v5/curr" $ testAsyncAcceptingOffline testCfgSlow testCfg + it "curr/v5" $ testAsyncAcceptingOffline testCfg testCfgSlow describe "connect, fully asynchronous (when clients are never simultaneously online)" $ do + it "curr" testFullAsyncFast -- fails in CI - xit'' "v2" testFullAsync + xit'' "v5" $ testFullAsyncSlow testCfgSlow testCfgSlow + xit'' "v5/curr" $ testFullAsyncSlow testCfgSlow testCfg + xit'' "curr/v5" $ testFullAsyncSlow testCfg testCfgSlow describe "webrtc calls api" $ do it "negotiate call" testNegotiateCall describe "maintenance mode" $ do @@ -81,7 +97,6 @@ chatDirectTests = do it "create second user" testCreateSecondUser it "multiple users subscribe and receive messages after restart" testUsersSubscribeAfterRestart it "both users have contact link" testMultipleUserAddresses - it "create user with default servers" testCreateUserDefaultServers it "create user with same servers" testCreateUserSameServers it "delete user" testDeleteUser it "users have different chat item TTL configuration, chat items expire" testUsersDifferentCIExpirationTTL @@ -296,6 +311,7 @@ testPlanInvitationLinkOwn tmp = alice ##> ("/_connect plan 1 " <> inv) alice <## "invitation link: ok to connect" -- conn_req_inv is forgotten after connection + threadDelay 100000 alice @@@ [("@alice_1", lastChatFeature), ("@alice_2", lastChatFeature)] alice `send` "@alice_2 hi" alice @@ -344,7 +360,7 @@ testDeleteContactDeletesProfile = connectUsers alice bob alice <##> bob -- alice deletes contact, profile is deleted - alice ##> "/d bob" + alice ##> "/_delete @2 full notify=on" alice <## "bob: contact is deleted" bob <## "alice (Alice) deleted contact with you" alice ##> "/_contacts 1" @@ -357,6 +373,43 @@ testDeleteContactDeletesProfile = (bob FilePath -> IO () +testDeleteContactKeepConversation = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + alice <##> bob + + alice ##> "/_delete @2 entity notify=on" + alice <## "bob: contact is deleted" + bob <## "alice (Alice) deleted contact with you" + + alice @@@ [("@bob", "hey")] + alice ##> "@bob hi" + alice <## "bob: not ready" + bob @@@ [("@alice", "contact deleted")] + bob ##> "@alice hey" + bob <## "alice: not ready" + +testDeleteConversationKeepContact :: HasCallStack => FilePath -> IO () +testDeleteConversationKeepContact = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + alice <##> bob + + alice @@@ [("@bob", "hey")] + + alice ##> "/_delete @2 messages" + alice <## "bob: contact is deleted" + + alice @@@ [("@bob", "")] -- UI would filter + bob @@@ [("@alice", "hey")] + bob #> "@alice hi" + alice <# "bob> hi" + alice @@@ [("@bob", "hi")] + alice <##> bob + testDeleteUnusedContactSilent :: HasCallStack => FilePath -> IO () testDeleteUnusedContactSilent = testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $ @@ -634,6 +687,52 @@ testDirectMessageDelete = bob #$> ("/_delete item @2 " <> itemId 4 <> " internal", id, "message deleted") bob #$> ("/_get chat @2 count=100", chat', chatFeatures' <> [((0, "hello 🙂"), Nothing), ((1, "do you receive my messages?"), Just (0, "hello 🙂"))]) +testDirectMessageDeleteMultiple :: HasCallStack => FilePath -> IO () +testDirectMessageDeleteMultiple = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + + alice #> "@bob hello" + bob <# "alice> hello" + msgId1 <- lastItemId alice + + alice #> "@bob hey" + bob <# "alice> hey" + msgId2 <- lastItemId alice + + alice ##> ("/_delete item @2 " <> msgId1 <> "," <> msgId2 <> " broadcast") + alice <## "2 messages deleted" + bob <# "alice> [marked deleted] hello" + bob <# "alice> [marked deleted] hey" + + alice #$> ("/_get chat @2 count=2", chat, [(1, "hello [marked deleted]"), (1, "hey [marked deleted]")]) + bob #$> ("/_get chat @2 count=2", chat, [(0, "hello [marked deleted]"), (0, "hey [marked deleted]")]) + +testDirectMessageDeleteMultipleManyBatches :: HasCallStack => FilePath -> IO () +testDirectMessageDeleteMultipleManyBatches = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + connectUsers alice bob + + alice #> "@bob message 0" + bob <# "alice> message 0" + msgIdFirst <- lastItemId alice + + forM_ [(1 :: Int) .. 300] $ \i -> do + alice #> ("@bob message " <> show i) + bob <# ("alice> message " <> show i) + msgIdLast <- lastItemId alice + + let mIdFirst = read msgIdFirst :: Int + mIdLast = read msgIdLast :: Int + deleteIds = intercalate "," (map show [mIdFirst .. mIdLast]) + alice `send` ("/_delete item @2 " <> deleteIds <> " broadcast") + _ <- getTermLine alice + alice <## "301 messages deleted" + forM_ [(0 :: Int) .. 300] $ \i -> do + bob <# ("alice> [marked deleted] message " <> show i) + testDirectLiveMessage :: HasCallStack => FilePath -> IO () testDirectLiveMessage = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -768,7 +867,7 @@ testTestSMPServerConnection = alice ##> "/smp test smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001" alice <## "SMP server test passed" alice ##> "/smp test smp://LcJU@localhost:7001" - alice <## "SMP server test failed at Connect, error: BROKER smp://LcJU@localhost:7001 NETWORK" + alice <## "SMP server test failed at Connect, error: BROKER {brokerAddress = \"smp://LcJU@localhost:7001\", brokerErr = NETWORK}" alice <## "Possibly, certificate fingerprint in SMP server address is incorrect" testGetSetXFTPServers :: HasCallStack => FilePath -> IO () @@ -799,44 +898,41 @@ testTestXFTPServer = alice ##> "/xftp test xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002" alice <## "XFTP server test passed" alice ##> "/xftp test xftp://LcJU@localhost:7002" - alice <## "XFTP server test failed at Connect, error: BROKER xftp://LcJU@localhost:7002 NETWORK" + alice <## "XFTP server test failed at Connect, error: BROKER {brokerAddress = \"xftp://LcJU@localhost:7002\", brokerErr = NETWORK}" alice <## "Possibly, certificate fingerprint in XFTP server address is incorrect" -testAsyncInitiatingOffline :: HasCallStack => FilePath -> IO () -testAsyncInitiatingOffline tmp = do - putStrLn "testAsyncInitiatingOffline" - inv <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do +testAsyncInitiatingOffline :: HasCallStack => ChatConfig -> ChatConfig -> FilePath -> IO () +testAsyncInitiatingOffline aliceCfg bobCfg tmp = do + inv <- withNewTestChatCfg tmp aliceCfg "alice" aliceProfile $ \alice -> do threadDelay 250000 alice ##> "/c" getInvitation alice - withNewTestChat tmp "bob" bobProfile $ \bob -> do + withNewTestChatCfg tmp bobCfg "bob" bobProfile $ \bob -> do threadDelay 250000 bob ##> ("/c " <> inv) bob <## "confirmation sent!" - withTestChat tmp "alice" $ \alice -> do + withTestChatCfg tmp aliceCfg "alice" $ \alice -> do concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") -testAsyncAcceptingOffline :: HasCallStack => FilePath -> IO () -testAsyncAcceptingOffline tmp = do - putStrLn "testAsyncAcceptingOffline" - inv <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do +testAsyncAcceptingOffline :: HasCallStack => ChatConfig -> ChatConfig -> FilePath -> IO () +testAsyncAcceptingOffline aliceCfg bobCfg tmp = do + inv <- withNewTestChatCfg tmp aliceCfg "alice" aliceProfile $ \alice -> do alice ##> "/c" getInvitation alice - withNewTestChat tmp "bob" bobProfile $ \bob -> do + withNewTestChatCfg tmp bobCfg "bob" bobProfile $ \bob -> do threadDelay 250000 bob ##> ("/c " <> inv) bob <## "confirmation sent!" - withTestChat tmp "alice" $ \alice -> do - withTestChat tmp "bob" $ \bob -> do + withTestChatCfg tmp aliceCfg "alice" $ \alice -> do + withTestChatCfg tmp bobCfg "bob" $ \bob -> do concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") -testFullAsync :: HasCallStack => FilePath -> IO () -testFullAsync tmp = do - putStrLn "testFullAsync" +testFullAsyncFast :: HasCallStack => FilePath -> IO () +testFullAsyncFast tmp = do inv <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do threadDelay 250000 alice ##> "/c" @@ -845,143 +941,33 @@ testFullAsync tmp = do threadDelay 250000 bob ##> ("/c " <> inv) bob <## "confirmation sent!" - withTestChat tmp "alice" $ \_ -> pure () -- connecting... notification in UI - withTestChat tmp "bob" $ \_ -> pure () -- connecting... notification in UI - withTestChat tmp "alice" $ \alice -> do - alice <## "1 contacts connected (use /cs for the list)" + threadDelay 250000 + withTestChat tmp "alice" $ \alice -> alice <## "bob (Bob): contact is connected" - withTestChat tmp "bob" $ \bob -> do - bob <## "1 contacts connected (use /cs for the list)" + withTestChat tmp "bob" $ \bob -> bob <## "alice (Alice): contact is connected" -testFullAsyncV1 :: HasCallStack => FilePath -> IO () -testFullAsyncV1 tmp = do - putStrLn "testFullAsyncV1" - inv <- withNewAlice $ \alice -> do - putStrLn "1" +testFullAsyncSlow :: HasCallStack => ChatConfig -> ChatConfig -> FilePath -> IO () +testFullAsyncSlow aliceCfg bobCfg tmp = do + inv <- withNewTestChatCfg tmp aliceCfg "alice" aliceProfile $ \alice -> do + threadDelay 250000 alice ##> "/c" - putStrLn "2" getInvitation alice - putStrLn "3" - withNewBob $ \bob -> do - putStrLn "4" + withNewTestChatCfg tmp bobCfg "bob" bobProfile $ \bob -> do + threadDelay 250000 bob ##> ("/c " <> inv) - putStrLn "5" bob <## "confirmation sent!" - putStrLn "6" - withAlice $ \_ -> pure () - putStrLn "7" - withBob $ \_ -> pure () - putStrLn "8" + withAlice $ \_ -> pure () -- connecting... notification in UI + withBob $ \_ -> pure () -- connecting... notification in UI withAlice $ \alice -> do - putStrLn "9" alice <## "1 contacts connected (use /cs for the list)" - putStrLn "10" - withBob $ \_ -> pure () - putStrLn "11" - withAlice $ \alice -> do - putStrLn "12" - alice <## "1 contacts connected (use /cs for the list)" - putStrLn "13" alice <## "bob (Bob): contact is connected" - putStrLn "14" withBob $ \bob -> do - putStrLn "15" bob <## "1 contacts connected (use /cs for the list)" - putStrLn "16" bob <## "alice (Alice): contact is connected" where - withNewAlice = withNewTestChatV1 tmp "alice" aliceProfile - withAlice = withTestChatV1 tmp "alice" - withNewBob = withNewTestChatV1 tmp "bob" bobProfile - withBob = withTestChatV1 tmp "bob" - -testFullAsyncV1toV2 :: HasCallStack => FilePath -> IO () -testFullAsyncV1toV2 tmp = do - putStrLn "testFullAsyncV1toV2" - inv <- withNewAlice $ \alice -> do - putStrLn "1" - alice ##> "/c" - putStrLn "2" - getInvitation alice - putStrLn "3" - withNewBob $ \bob -> do - putStrLn "4" - bob ##> ("/c " <> inv) - putStrLn "5" - bob <## "confirmation sent!" - withAlice $ \_ -> pure () - putStrLn "6" - withBob $ \_ -> pure () - putStrLn "7" - withAlice $ \alice -> do - putStrLn "8" - alice <## "1 contacts connected (use /cs for the list)" - putStrLn "9" - withBob $ \_ -> pure () - putStrLn "10" - withAlice $ \alice -> do - putStrLn "11" - alice <## "1 contacts connected (use /cs for the list)" - putStrLn "12" - alice <## "bob (Bob): contact is connected" - putStrLn "13" - withBob $ \bob -> do - putStrLn "14" - bob <## "1 contacts connected (use /cs for the list)" - putStrLn "15" - bob <## "alice (Alice): contact is connected" - where - withNewAlice = withNewTestChat tmp "alice" aliceProfile - withAlice = withTestChat tmp "alice" - withNewBob = withNewTestChatV1 tmp "bob" bobProfile - withBob = withTestChatV1 tmp "bob" - -testFullAsyncV2toV1 :: HasCallStack => FilePath -> IO () -testFullAsyncV2toV1 tmp = do - putStrLn "testFullAsyncV2toV1" - inv <- withNewAlice $ \alice -> do - putStrLn "1" - alice ##> "/c" - putStrLn "2" - getInvitation alice - putStrLn "3" - withNewBob $ \bob -> do - putStrLn "4" - bob ##> ("/c " <> inv) - putStrLn "5" - bob <## "confirmation sent!" - putStrLn "6" - withAlice $ \_ -> pure () - putStrLn "7" - withBob $ \_ -> pure () - putStrLn "8" - withAlice $ \alice -> do - putStrLn "9" - alice <## "1 contacts connected (use /cs for the list)" - putStrLn "10" - withBob $ \_ -> pure () - putStrLn "11" - withAlice $ \alice -> do - putStrLn "12" - alice <## "1 contacts connected (use /cs for the list)" - putStrLn "13" - alice <## "bob (Bob): contact is connected" - putStrLn "14" - withBob $ \bob -> do - putStrLn "15" - bob <## "1 contacts connected (use /cs for the list)" - putStrLn "16" - bob <## "alice (Alice): contact is connected" - where - withNewAlice = withNewTestChatV1 tmp "alice" aliceProfile - {-# INLINE withNewAlice #-} - withAlice = withTestChatV1 tmp "alice" - {-# INLINE withAlice #-} - withNewBob = withNewTestChat tmp "bob" bobProfile - {-# INLINE withNewBob #-} - withBob = withTestChat tmp "bob" - {-# INLINE withBob #-} + withAlice = withTestChatCfg tmp aliceCfg "alice" + withBob = withTestChatCfg tmp aliceCfg "bob" testCallType :: CallType testCallType = CallType {media = CMVideo, capabilities = CallCapabilities {encryption = True}} @@ -1029,20 +1015,25 @@ testNegotiateCall = -- alice confirms call by sending WebRTC answer alice ##> ("/_call answer @2 " <> serialize testWebRTCSession) alice <## "ok" + threadDelay 100000 alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "outgoing call: connecting...")]) bob <## "alice continued the WebRTC call" repeatM_ 3 $ getTermLine bob + threadDelay 100000 bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "incoming call: connecting...")]) -- participants can update calls as connected alice ##> "/_call status @2 connected" alice <## "ok" + threadDelay 100000 alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "outgoing call: in progress (00:00)")]) bob ##> "/_call status @2 connected" bob <## "ok" + threadDelay 100000 bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "incoming call: in progress (00:00)")]) -- either party can end the call bob ##> "/_call end @2" bob <## "ok" + threadDelay 100000 bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "incoming call: ended (00:00)")]) alice <## "call with bob ended" alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "outgoing call: ended (00:00)")]) @@ -1195,6 +1186,7 @@ testSubscribeAppNSE tmp = alice <## "chat suspended" nseAlice ##> "/_start main=off" nseAlice <## "chat started" + threadDelay 100000 nseAlice ##> "/ad" cLink <- getContactLink nseAlice True bob ##> ("/c " <> cLink) @@ -1207,7 +1199,7 @@ testSubscribeAppNSE tmp = alice <## "to accept: /ac bob" alice <## "to reject: /rc bob (the sender will NOT be notified)" alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -1433,7 +1425,7 @@ testMultipleUserAddresses = alice <#? bob alice @@@ [("<@bob", "")] alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -1449,14 +1441,14 @@ testMultipleUserAddresses = cLinkAlisa <- getContactLink alice True bob ##> ("/c " <> cLinkAlisa) alice <#? bob - alice #$> ("/_get chats 2 pcc=on", chats, [("<@bob", ""), ("*", "")]) + alice #$> ("/_get chats 2 pcc=on", chats, [("<@bob", ""), ("@SimpleX Chat team", ""), ("@SimpleX-Status", ""), ("*", "")]) alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alisa: contact is connected") (alice <## "bob (Bob): contact is connected") threadDelay 100000 - alice #$> ("/_get chats 2 pcc=on", chats, [("@bob", lastChatFeature), ("*", "")]) + alice #$> ("/_get chats 2 pcc=on", chats, [("@bob", lastChatFeature), ("@SimpleX Chat team", ""), ("@SimpleX-Status", ""), ("*", "")]) alice <##> bob bob #> "@alice hey alice" @@ -1482,12 +1474,12 @@ testMultipleUserAddresses = showActiveUser alice "alisa" alice ##> "/ac cath" - alice <## "cath (Catherine): accepting contact request..." + alice <## "cath (Catherine): accepting contact request, you can send messages to contact" concurrently_ (cath <## "alisa: contact is connected") (alice <## "cath (Catherine): contact is connected") threadDelay 100000 - alice #$> ("/_get chats 2 pcc=on", chats, [("@cath", lastChatFeature), ("@bob", "hey"), ("*", "")]) + alice #$> ("/_get chats 2 pcc=on", chats, [("@cath", lastChatFeature), ("@bob", "hey"), ("@SimpleX Chat team", ""), ("@SimpleX-Status", ""), ("*", "")]) alice <##> cath -- first user doesn't have cath as contact @@ -1495,39 +1487,6 @@ testMultipleUserAddresses = showActiveUser alice "alice (Alice)" alice @@@ [("@bob", "hey alice")] -testCreateUserDefaultServers :: HasCallStack => FilePath -> IO () -testCreateUserDefaultServers = - testChat2 aliceProfile bobProfile $ - \alice _ -> do - alice #$> ("/smp smp://2345-w==@smp2.example.im smp://3456-w==@smp3.example.im:5224", id, "ok") - alice #$> ("/xftp xftp://2345-w==@xftp2.example.im xftp://3456-w==@xftp3.example.im:5224", id, "ok") - checkCustomServers alice - - alice ##> "/create user alisa" - showActiveUser alice "alisa" - - alice #$> ("/smp", id, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001") - alice #$> ("/xftp", id, "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002") - - -- with same_servers=off - alice ##> "/user alice" - showActiveUser alice "alice (Alice)" - checkCustomServers alice - - alice ##> "/create user same_servers=off alisa2" - showActiveUser alice "alisa2" - - alice #$> ("/smp", id, "smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7001") - alice #$> ("/xftp", id, "xftp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7002") - where - checkCustomServers alice = do - alice ##> "/smp" - alice <## "smp://2345-w==@smp2.example.im" - alice <## "smp://3456-w==@smp3.example.im:5224" - alice ##> "/xftp" - alice <## "xftp://2345-w==@xftp2.example.im" - alice <## "xftp://3456-w==@xftp3.example.im:5224" - testCreateUserSameServers :: HasCallStack => FilePath -> IO () testCreateUserSameServers = testChat2 aliceProfile bobProfile $ @@ -1536,7 +1495,7 @@ testCreateUserSameServers = alice #$> ("/xftp xftp://2345-w==@xftp2.example.im xftp://3456-w==@xftp3.example.im:5224", id, "ok") checkCustomServers alice - alice ##> "/create user same_servers=on alisa" + alice ##> "/create user alisa" showActiveUser alice "alisa" checkCustomServers alice @@ -1690,7 +1649,7 @@ testUsersDifferentCIExpirationTTL tmp = do bob #> "@alisa alisa 4" alice <# "bob> alisa 4" - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) threadDelay 3000000 @@ -1703,11 +1662,11 @@ testUsersDifferentCIExpirationTTL tmp = do -- second user messages alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) threadDelay 2000000 - alice #$> ("/_get chat @4 count=100", chat, []) + alice #$> ("/_get chat @6 count=100", chat, []) where cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000} @@ -1773,7 +1732,7 @@ testUsersRestartCIExpiration tmp = do bob #> "@alisa alisa 4" alice <# "bob> alisa 4" - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) threadDelay 3000000 @@ -1786,11 +1745,11 @@ testUsersRestartCIExpiration tmp = do -- second user messages alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) threadDelay 3000000 - alice #$> ("/_get chat @4 count=100", chat, []) + alice #$> ("/_get chat @6 count=100", chat, []) where cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000} @@ -1832,7 +1791,7 @@ testEnableCIExpirationOnlyForOneUser tmp = do bob #> "@alisa alisa 4" alice <# "bob> alisa 4" - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) threadDelay 2000000 @@ -1844,14 +1803,14 @@ testEnableCIExpirationOnlyForOneUser tmp = do -- messages are not deleted for second user alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) withTestChatCfg tmp cfg "alice" $ \alice -> do alice <## "1 contacts connected (use /cs for the list)" alice <## "[user: alice] 1 contacts connected (use /cs for the list)" -- messages are not deleted for second user after restart - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4")]) alice #> "@bob alisa 5" bob <# "alisa> alisa 5" @@ -1861,7 +1820,7 @@ testEnableCIExpirationOnlyForOneUser tmp = do threadDelay 2000000 -- new messages are not deleted for second user - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4"), (1, "alisa 5"), (0, "alisa 6")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2"), (1, "alisa 3"), (0, "alisa 4"), (1, "alisa 5"), (0, "alisa 6")]) where cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000} @@ -1895,12 +1854,12 @@ testDisableCIExpirationOnlyForOneUser tmp = do bob #> "@alisa alisa 2" alice <# "bob> alisa 2" - alice #$> ("/_get chat @4 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2")]) + alice #$> ("/_get chat @6 count=100", chat, chatFeatures <> [(1, "alisa 1"), (0, "alisa 2")]) threadDelay 2000000 -- second user messages are deleted - alice #$> ("/_get chat @4 count=100", chat, []) + alice #$> ("/_get chat @6 count=100", chat, []) withTestChatCfg tmp cfg "alice" $ \alice -> do alice <## "1 contacts connected (use /cs for the list)" @@ -1914,12 +1873,12 @@ testDisableCIExpirationOnlyForOneUser tmp = do bob #> "@alisa alisa 4" alice <# "bob> alisa 4" - alice #$> ("/_get chat @4 count=100", chat, [(1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, [(1, "alisa 3"), (0, "alisa 4")]) threadDelay 2000000 -- second user messages are deleted - alice #$> ("/_get chat @4 count=100", chat, []) + alice #$> ("/_get chat @6 count=100", chat, []) where cfg = testCfg {initialCleanupManagerDelay = 0, cleanupManagerStepDelay = 0, ciExpirationInterval = 500000} @@ -1934,7 +1893,7 @@ testUsersTimedMessages tmp = do alice ##> "/create user alisa" showActiveUser alice "alisa" connectUsers alice bob - configureTimedMessages alice bob "4" "3" + configureTimedMessages alice bob "6" "3" -- first user messages alice ##> "/user alice" @@ -1963,7 +1922,7 @@ testUsersTimedMessages tmp = do alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, [(1, "alisa 1"), (0, "alisa 2")]) + alice #$> ("/_get chat @6 count=100", chat, [(1, "alisa 1"), (0, "alisa 2")]) threadDelay 1000000 @@ -1978,7 +1937,7 @@ testUsersTimedMessages tmp = do alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, [(1, "alisa 1"), (0, "alisa 2")]) + alice #$> ("/_get chat @6 count=100", chat, [(1, "alisa 1"), (0, "alisa 2")]) threadDelay 1000000 @@ -1989,7 +1948,7 @@ testUsersTimedMessages tmp = do alice ##> "/user" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, []) + alice #$> ("/_get chat @6 count=100", chat, []) -- first user messages alice ##> "/user alice" @@ -2019,7 +1978,7 @@ testUsersTimedMessages tmp = do alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, [(1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, [(1, "alisa 3"), (0, "alisa 4")]) -- messages are deleted after restart threadDelay 1000000 @@ -2035,7 +1994,7 @@ testUsersTimedMessages tmp = do alice ##> "/user alisa" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, [(1, "alisa 3"), (0, "alisa 4")]) + alice #$> ("/_get chat @6 count=100", chat, [(1, "alisa 3"), (0, "alisa 4")]) threadDelay 1000000 @@ -2046,7 +2005,7 @@ testUsersTimedMessages tmp = do alice ##> "/user" showActiveUser alice "alisa" - alice #$> ("/_get chat @4 count=100", chat, []) + alice #$> ("/_get chat @6 count=100", chat, []) where configureTimedMessages :: HasCallStack => TestCC -> TestCC -> String -> String -> IO () configureTimedMessages alice bob bobId ttl = do @@ -2073,6 +2032,8 @@ testUserPrivacy = bob <# "alisa> hello" bob #> "@alisa hey" alice <# "bob> hey" + bob #> "@alice hey" + (alice, "[user: alice] ") ^<# "bob> hey" -- hide user profile alice ##> "/hide user my_password" userHidden alice "current " @@ -2251,6 +2212,7 @@ testSwitchContact = alice <## "bob: you started changing address" bob <## "alice changed address for you" alice <## "bob: you changed address" + threadDelay 100000 alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "started changing address..."), (1, "you changed address")]) bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "started changing address for you..."), (0, "changed address for you")]) alice <##> bob @@ -2264,12 +2226,12 @@ testAbortSwitchContact tmp = do alice <## "bob: you started changing address" -- repeat switch is prohibited alice ##> "/switch bob" - alice <## "error: command is prohibited" + alice <## "error: command is prohibited, switchConnectionAsync: already switching" -- stop switch alice #$> ("/abort switch bob", id, "switch aborted") -- repeat switch stop is prohibited alice ##> "/abort switch bob" - alice <## "error: command is prohibited" + alice <## "error: command is prohibited, abortConnectionSwitch: not allowed" withTestChatContactConnected tmp "bob" $ \bob -> do bob <## "alice started changing address for you" -- alice changes address again @@ -2278,6 +2240,7 @@ testAbortSwitchContact tmp = do bob <## "alice started changing address for you" bob <## "alice changed address for you" alice <## "bob: you changed address" + threadDelay 100000 alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "started changing address..."), (1, "started changing address..."), (1, "you changed address")]) bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "started changing address for you..."), (0, "started changing address for you..."), (0, "changed address for you")]) alice <##> bob @@ -2292,6 +2255,7 @@ testSwitchGroupMember = alice <## "#team: you started changing address for bob" bob <## "#team: alice changed address for you" alice <## "#team: you changed address for bob" + threadDelay 100000 alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "started changing address for bob..."), (1, "you changed address for bob")]) bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "started changing address for you..."), (0, "changed address for you")]) alice #> "#team hey" @@ -2308,12 +2272,12 @@ testAbortSwitchGroupMember tmp = do alice <## "#team: you started changing address for bob" -- repeat switch is prohibited alice ##> "/switch #team bob" - alice <## "error: command is prohibited" + alice <## "error: command is prohibited, switchConnectionAsync: already switching" -- stop switch alice #$> ("/abort switch #team bob", id, "switch aborted") -- repeat switch stop is prohibited alice ##> "/abort switch #team bob" - alice <## "error: command is prohibited" + alice <## "error: command is prohibited, abortConnectionSwitch: not allowed" withTestChatContactConnected tmp "bob" $ \bob -> do bob <## "#team: connected to server(s)" bob <## "#team: alice started changing address for you" @@ -2323,6 +2287,7 @@ testAbortSwitchGroupMember tmp = do bob <## "#team: alice started changing address for you" bob <## "#team: alice changed address for you" alice <## "#team: you changed address for bob" + threadDelay 100000 alice #$> ("/_get chat #1 count=100", chat, [(1, e2eeInfoNoPQStr), (0, "connected"), (1, "started changing address for bob..."), (1, "started changing address for bob..."), (1, "you changed address for bob")]) bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "started changing address for you..."), (0, "started changing address for you..."), (0, "changed address for you")]) alice #> "#team hey" @@ -2414,7 +2379,7 @@ testMsgDecryptError tmp = withTestChat tmp "bob" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" alice #> "@bob hello again" - bob <# "alice> skipped message ID 10..12" + bob <# "alice> skipped message ID 9..11" bob <# "alice> hello again" bob #> "@alice received!" alice <# "bob> received!" @@ -2436,7 +2401,7 @@ setupDesynchronizedRatchet tmp alice = do withTestChat tmp "bob_old" $ \bob -> do bob <## "1 contacts connected (use /cs for the list)" bob ##> "/sync alice" - bob <## "error: command is prohibited" + bob <## "error: command is prohibited, synchronizeRatchet: not allowed" alice #> "@bob 1" bob <## "alice: decryption error (connection out of sync), synchronization required" bob <## "use /sync alice to synchronize" @@ -2446,7 +2411,7 @@ setupDesynchronizedRatchet tmp alice = do bob ##> "/tail @alice 1" bob <# "alice> decryption error, possibly due to the device change (header, 3 messages)" bob ##> "@alice 1" - bob <## "error: command is prohibited" + bob <## "error: command is prohibited, sendMessagesB: send prohibited" (alice ("/_get chat @2 count=3", chat, [(1, "connection synchronization started"), (0, "connection synchronization agreed"), (0, "connection synchronized")]) alice #$> ("/_get chat @2 count=2", chat, [(0, "connection synchronization agreed"), (0, "connection synchronized")]) @@ -2511,6 +2477,7 @@ testSyncRatchetCodeReset tmp = alice <## "bob: connection synchronized" bob <## "alice: connection synchronized" + threadDelay 100000 bob #$> ("/_get chat @2 count=4", chat, [(1, "connection synchronization started"), (0, "connection synchronization agreed"), (0, "security code changed"), (0, "connection synchronized")]) alice #$> ("/_get chat @2 count=2", chat, [(0, "connection synchronization agreed"), (0, "connection synchronized")]) @@ -2700,7 +2667,7 @@ testConnReqChatVRange ct1VRange ct2VRange tmp = bob ##> ("/c " <> cLink) alice <#? bob alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") diff --git a/tests/ChatTests/Files.hs b/tests/ChatTests/Files.hs index 572d9294a9..0016666688 100644 --- a/tests/ChatTests/Files.hs +++ b/tests/ChatTests/Files.hs @@ -738,7 +738,7 @@ testXFTPRcvError tmp = do _ <- getTermLine bob bob ##> "/fs 1" - bob <## "receiving file 1 (test.pdf) error" + bob <## "receiving file 1 (test.pdf) error: FileErrAuth" testXFTPCancelRcvRepeat :: HasCallStack => FilePath -> IO () testXFTPCancelRcvRepeat = diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 4d9d94e24d..d6849d3074 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -1,6 +1,8 @@ +{-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE PostfixOperators #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} module ChatTests.Groups where @@ -8,16 +10,21 @@ import ChatClient import ChatTests.Utils import Control.Concurrent (threadDelay) import Control.Concurrent.Async (concurrently_) -import Control.Monad (void, when) +import Control.Monad (forM_, void, when) import qualified Data.ByteString.Char8 as B -import Data.List (isInfixOf) +import Data.List (intercalate, isInfixOf) import qualified Data.Text as T import Simplex.Chat.Controller (ChatConfig (..)) +import Simplex.Chat.Options import Simplex.Chat.Protocol (supportedChatVRange) import Simplex.Chat.Store (agentStoreFile, chatStoreFile) import Simplex.Chat.Types (VersionRangeChat) import Simplex.Chat.Types.Shared (GroupMemberRole (..)) +import Simplex.Messaging.Agent.Env.SQLite +import Simplex.Messaging.Agent.RetryInterval import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB +import Simplex.Messaging.Server.Env.STM hiding (subscriptions) +import Simplex.Messaging.Transport import System.Directory (copyFile) import System.FilePath (()) import Test.Hspec hiding (it) @@ -44,12 +51,16 @@ chatGroupTests = do it "group message update" testGroupMessageUpdate it "group message edit history" testGroupMessageEditHistory it "group message delete" testGroupMessageDelete + it "group message delete multiple" testGroupMessageDeleteMultiple + it "group message delete multiple (many chat batches)" testGroupMessageDeleteMultipleManyBatches it "group live message" testGroupLiveMessage it "update group profile" testUpdateGroupProfile it "update member role" testUpdateMemberRole it "unused contacts are deleted after all their groups are deleted" testGroupDeleteUnusedContacts it "group description is shown as the first message to new members" testGroupDescription it "moderate message of another group member" testGroupModerate + it "moderate own message (should process as deletion)" testGroupModerateOwn + it "moderate multiple messages" testGroupModerateMultiple it "moderate message of another group member (full delete)" testGroupModerateFullDelete it "moderate message that arrives after the event of moderation" testGroupDelayedModeration it "moderate message that arrives after the event of moderation (full delete)" testGroupDelayedModerationFullDelete @@ -82,6 +93,7 @@ chatGroupTests = do describe "group links without contact connection plan" $ do it "group link without contact - known group" testPlanGroupLinkNoContactKnown it "group link without contact - connecting" testPlanGroupLinkNoContactConnecting + it "group link without contact - connecting (slow handshake)" testPlanGroupLinkNoContactConnectingSlow describe "group message errors" $ do it "show message decryption error" testGroupMsgDecryptError it "should report ratchet de-synchronization, synchronize ratchets" testGroupSyncRatchet @@ -150,6 +162,8 @@ chatGroupTests = do it "another admin can unblock" testBlockForAllAnotherAdminUnblocks it "member was blocked before joining group" testBlockForAllBeforeJoining it "can't repeat block, unblock" testBlockForAllCantRepeat + describe "group member inactivity" $ do + it "mark member inactive on reaching quota" testGroupMemberInactive where _0 = supportedChatVRange -- don't create direct connections _1 = groupCreateDirectVRange @@ -296,28 +310,6 @@ testGroupShared alice bob cath checkMessages directConnections = do alice ##> "/d bob" alice <## "bob: contact is deleted" bob <## "alice (Alice) deleted contact with you" - alice `send` "@bob hey" - if directConnections - then - alice - <### [ "@bob hey", - "member #team bob does not have direct connection, creating", - "peer chat protocol version range incompatible" - ] - else do - alice - <### [ WithTime "@bob hey", - "member #team bob does not have direct connection, creating", - "contact for member #team bob is created", - "sent invitation to connect directly to member #team bob", - "bob (Bob): contact is connected" - ] - bob - <### [ "#team alice is creating direct contact alice with you", - WithTime "alice> hey", - "alice: security code changed", - "alice (Alice): contact is connected" - ] when checkMessages $ threadDelay 1000000 alice #> "#team checking connection" bob <# "#team alice> checking connection" @@ -659,6 +651,7 @@ testGroupSameName :: HasCallStack => FilePath -> IO () testGroupSameName = testChat2 aliceProfile bobProfile $ \alice _ -> do + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team or /create link #team" @@ -807,6 +800,7 @@ testGroupDeleteInvitedContact = WithTime "alice> hey", "alice: security code changed" ] + bob <## "alice (Alice): you can send messages to contact" concurrently_ (alice <## "bob (Bob): contact is connected") (bob <## "alice (Alice): contact is connected") @@ -1263,6 +1257,77 @@ testGroupMessageDelete = bob #$> ("/_get chat #1 count=3", chat', [((0, "hello!"), Nothing), ((1, "hi alice"), Just (0, "hello!")), ((0, "how are you? [marked deleted]"), Nothing)]) cath #$> ("/_get chat #1 count=3", chat', [((0, "hello!"), Nothing), ((0, "hi alice"), Just (0, "hello!")), ((1, "how are you? [marked deleted]"), Nothing)]) +testGroupMessageDeleteMultiple :: HasCallStack => FilePath -> IO () +testGroupMessageDeleteMultiple = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + + threadDelay 1000000 + alice #> "#team hello" + concurrently_ + (bob <# "#team alice> hello") + (cath <# "#team alice> hello") + msgId1 <- lastItemId alice + + threadDelay 1000000 + alice #> "#team hey" + concurrently_ + (bob <# "#team alice> hey") + (cath <# "#team alice> hey") + msgId2 <- lastItemId alice + + threadDelay 1000000 + alice ##> ("/_delete item #1 " <> msgId1 <> "," <> msgId2 <> " broadcast") + alice <## "2 messages deleted" + concurrentlyN_ + [ do + bob <# "#team alice> [marked deleted] hello" + bob <# "#team alice> [marked deleted] hey", + do + cath <# "#team alice> [marked deleted] hello" + cath <# "#team alice> [marked deleted] hey" + ] + + alice #$> ("/_get chat #1 count=2", chat, [(1, "hello [marked deleted]"), (1, "hey [marked deleted]")]) + bob #$> ("/_get chat #1 count=2", chat, [(0, "hello [marked deleted]"), (0, "hey [marked deleted]")]) + cath #$> ("/_get chat #1 count=2", chat, [(0, "hello [marked deleted]"), (0, "hey [marked deleted]")]) + +testGroupMessageDeleteMultipleManyBatches :: HasCallStack => FilePath -> IO () +testGroupMessageDeleteMultipleManyBatches = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + + bob ##> "/set receipts all off" + bob <## "ok" + cath ##> "/set receipts all off" + cath <## "ok" + + alice #> "#team message 0" + concurrently_ + (bob <# "#team alice> message 0") + (cath <# "#team alice> message 0") + msgIdFirst <- lastItemId alice + + forM_ [(1 :: Int) .. 300] $ \i -> do + alice #> ("#team message " <> show i) + concurrently_ + (bob <# ("#team alice> message " <> show i)) + (cath <# ("#team alice> message " <> show i)) + msgIdLast <- lastItemId alice + + let mIdFirst = read msgIdFirst :: Int + mIdLast = read msgIdLast :: Int + deleteIds = intercalate "," (map show [mIdFirst .. mIdLast]) + alice `send` ("/_delete item #1 " <> deleteIds <> " broadcast") + _ <- getTermLine alice + alice <## "301 messages deleted" + forM_ [(0 :: Int) .. 300] $ \i -> + concurrently_ + (bob <# ("#team alice> [marked deleted] message " <> show i)) + (cath <# ("#team alice> [marked deleted] message " <> show i)) + testGroupLiveMessage :: HasCallStack => FilePath -> IO () testGroupLiveMessage = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do @@ -1548,7 +1613,7 @@ testGroupModerate = (bob <# "#team alice> hello") (cath <# "#team alice> hello") bob ##> "\\\\ #team @alice hello" - bob <## "#team: you have insufficient permissions for this action, the required role is owner" + bob <## "cannot delete this item" threadDelay 1000000 cath #> "#team hi" concurrently_ @@ -1563,6 +1628,55 @@ testGroupModerate = bob #$> ("/_get chat #1 count=1", chat, [(0, "hi [marked deleted by you]")]) cath #$> ("/_get chat #1 count=1", chat, [(1, "hi [marked deleted by bob]")]) +testGroupModerateOwn :: HasCallStack => FilePath -> IO () +testGroupModerateOwn = + testChat2 aliceProfile bobProfile $ + \alice bob -> do + createGroup2 "team" alice bob + threadDelay 1000000 + alice #> "#team hello" + bob <# "#team alice> hello" + alice ##> "\\\\ #team @alice hello" + alice <## "message marked deleted by you" + bob <# "#team alice> [marked deleted by alice] hello" + alice #$> ("/_get chat #1 count=1", chat, [(1, "hello [marked deleted by you]")]) + bob #$> ("/_get chat #1 count=1", chat, [(0, "hello [marked deleted by alice]")]) + +testGroupModerateMultiple :: HasCallStack => FilePath -> IO () +testGroupModerateMultiple = + testChat3 aliceProfile bobProfile cathProfile $ + \alice bob cath -> do + createGroup3 "team" alice bob cath + + threadDelay 1000000 + alice #> "#team hello" + concurrently_ + (bob <# "#team alice> hello") + (cath <# "#team alice> hello") + msgId1 <- lastItemId alice + + threadDelay 1000000 + bob #> "#team hey" + concurrently_ + (alice <# "#team bob> hey") + (cath <# "#team bob> hey") + msgId2 <- lastItemId alice + + alice ##> ("/_delete member item #1 " <> msgId1 <> "," <> msgId2) + alice <## "2 messages deleted" + concurrentlyN_ + [ do + bob <# "#team alice> [marked deleted by alice] hello" + bob <# "#team bob> [marked deleted by alice] hey", + do + cath <# "#team alice> [marked deleted by alice] hello" + cath <# "#team bob> [marked deleted by alice] hey" + ] + + alice #$> ("/_get chat #1 count=2", chat, [(1, "hello [marked deleted by you]"), (0, "hey [marked deleted by you]")]) + bob #$> ("/_get chat #1 count=2", chat, [(0, "hello [marked deleted by alice]"), (1, "hey [marked deleted by alice]")]) + cath #$> ("/_get chat #1 count=2", chat, [(0, "hello [marked deleted by alice]"), (0, "hey [marked deleted by alice]")]) + testGroupModerateFullDelete :: HasCallStack => FilePath -> IO () testGroupModerateFullDelete = testChatCfg3 testCfgCreateGroupDirect aliceProfile bobProfile cathProfile $ @@ -1854,6 +1968,7 @@ testGroupLink :: HasCallStack => FilePath -> IO () testGroupLink = testChatCfg3 testCfgGroupLinkViaContact aliceProfile bobProfile cathProfile $ \alice bob cath -> do + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team or /create link #team" @@ -1896,7 +2011,7 @@ testGroupLink = cath ##> ("/c " <> cLink) alice <#? cath alice ##> "/ac cath" - alice <## "cath (Catherine): accepting contact request..." + alice <## "cath (Catherine): accepting contact request, you can send messages to contact" concurrently_ (cath <## "alice (Alice): contact is connected") (alice <## "cath (Catherine): contact is connected") @@ -1957,6 +2072,7 @@ testGroupLinkDeleteGroupRejoin :: HasCallStack => FilePath -> IO () testGroupLinkDeleteGroupRejoin = testChatCfg2 testCfgGroupLinkViaContact aliceProfile bobProfile $ \alice bob -> do + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team or /create link #team" @@ -2013,6 +2129,7 @@ testGroupLinkContactUsed :: HasCallStack => FilePath -> IO () testGroupLinkContactUsed = testChatCfg2 testCfgGroupLinkViaContact aliceProfile bobProfile $ \alice bob -> do + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team or /create link #team" @@ -2161,6 +2278,7 @@ testGroupLinkUnusedHostContactDeleted = testChatCfg2 cfg aliceProfile bobProfile $ \alice bob -> do -- create group 1 + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team or /create link #team" @@ -2295,6 +2413,7 @@ testGroupLinkMemberRole :: HasCallStack => FilePath -> IO () testGroupLinkMemberRole = testChatCfg3 testCfgGroupLinkViaContact aliceProfile bobProfile cathProfile $ \alice bob cath -> do + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team or /create link #team" @@ -2430,6 +2549,7 @@ testPlanGroupLinkOkKnown :: HasCallStack => FilePath -> IO () testPlanGroupLinkOkKnown = testChatCfg2 testCfgGroupLinkViaContact aliceProfile bobProfile $ \alice bob -> do + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team or /create link #team" @@ -2473,6 +2593,7 @@ testPlanHostContactDeletedGroupLinkKnown :: HasCallStack => FilePath -> IO () testPlanHostContactDeletedGroupLinkKnown = testChatCfg2 testCfgGroupLinkViaContact aliceProfile bobProfile $ \alice bob -> do + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team or /create link #team" @@ -2517,7 +2638,7 @@ testPlanHostContactDeletedGroupLinkKnown = testPlanGroupLinkOwn :: HasCallStack => FilePath -> IO () testPlanGroupLinkOwn tmp = - withNewTestChatCfg tmp testCfgGroupLinkViaContact "alice" aliceProfile $ \alice -> do + withNewTestChatCfg tmp (mkCfgGroupLinkViaContact testCfgSlow) "alice" aliceProfile $ \alice -> do threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" @@ -2578,12 +2699,15 @@ testPlanGroupLinkOwn tmp = testPlanGroupLinkConnecting :: HasCallStack => FilePath -> IO () testPlanGroupLinkConnecting tmp = do + -- gLink <- withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do gLink <- withNewTestChatCfg tmp cfg "alice" aliceProfile $ \alice -> do + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team or /create link #team" alice ##> "/create link #team" getGroupLink alice "team" GRMember True + -- withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do withNewTestChatCfg tmp cfg "bob" bobProfile $ \bob -> do threadDelay 100000 @@ -2598,12 +2722,14 @@ testPlanGroupLinkConnecting tmp = do bob <## "group link: connecting, allowed to reconnect" threadDelay 100000 + -- withTestChatCfg tmp cfg "alice" $ \alice -> do withTestChatCfg tmp cfg "alice" $ \alice -> do alice <### [ "1 group links active", "#team: group is empty", "bob (Bob): accepting request to join group #team..." ] + -- withTestChatCfg tmp cfg "bob" $ \bob -> do withTestChatCfg tmp cfg "bob" $ \bob -> do threadDelay 500000 bob ##> ("/_connect plan 1 " <> gLink) @@ -2616,12 +2742,13 @@ testPlanGroupLinkConnecting tmp = do bob ##> ("/c " <> gLink) bob <## "group link: connecting" where - cfg = testCfgGroupLinkViaContact + cfg = mkCfgGroupLinkViaContact testCfgSlow testPlanGroupLinkLeaveRejoin :: HasCallStack => FilePath -> IO () testPlanGroupLinkLeaveRejoin = testChatCfg2 testCfgGroupLinkViaContact aliceProfile bobProfile $ \alice bob -> do + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team or /create link #team" @@ -2641,6 +2768,8 @@ testPlanGroupLinkLeaveRejoin = bob <## "#team: you joined the group" ] + threadDelay 100000 + bob ##> ("/_connect plan 1 " <> gLink) bob <## "group link: known group #team" bob <## "use #team to send messages" @@ -2649,6 +2778,8 @@ testPlanGroupLinkLeaveRejoin = bob <## "group link: known group #team" bob <## "use #team to send messages" + threadDelay 100000 + bob ##> "/leave #team" concurrentlyN_ [ do @@ -2657,6 +2788,8 @@ testPlanGroupLinkLeaveRejoin = alice <## "#team: bob left the group" ] + threadDelay 100000 + bob ##> ("/_connect plan 1 " <> gLink) bob <## "group link: ok to connect" @@ -2704,6 +2837,7 @@ testGroupLinkNoContact :: HasCallStack => FilePath -> IO () testGroupLinkNoContact = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team or /create link #team" @@ -2927,6 +3061,7 @@ testGroupLinkNoContactMemberRole :: HasCallStack => FilePath -> IO () testGroupLinkNoContactMemberRole = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team or /create link #team" @@ -3040,6 +3175,7 @@ testGroupLinkNoContactInviteeIncognito :: HasCallStack => FilePath -> IO () testGroupLinkNoContactInviteeIncognito = testChat2 aliceProfile bobProfile $ \alice bob -> do + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team or /create link #team" @@ -3144,6 +3280,7 @@ testPlanGroupLinkNoContactKnown :: HasCallStack => FilePath -> IO () testPlanGroupLinkNoContactKnown = testChat2 aliceProfile bobProfile $ \alice bob -> do + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team or /create link #team" @@ -3179,6 +3316,7 @@ testPlanGroupLinkNoContactKnown = testPlanGroupLinkNoContactConnecting :: HasCallStack => FilePath -> IO () testPlanGroupLinkNoContactConnecting tmp = do gLink <- withNewTestChat tmp "alice" aliceProfile $ \alice -> do + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team or /create link #team" @@ -3207,6 +3345,53 @@ testPlanGroupLinkNoContactConnecting tmp = do withTestChat tmp "bob" $ \bob -> do threadDelay 500000 bob <## "#team: joining the group..." + bob <## "#team: you joined the group" + + bob ##> ("/_connect plan 1 " <> gLink) + bob <## "group link: known group #team" + bob <## "use #team to send messages" + + let gLinkSchema2 = linkAnotherSchema gLink + bob ##> ("/_connect plan 1 " <> gLinkSchema2) + bob <## "group link: known group #team" + bob <## "use #team to send messages" + + bob ##> ("/c " <> gLink) + bob <## "group link: known group #team" + bob <## "use #team to send messages" + +testPlanGroupLinkNoContactConnectingSlow :: HasCallStack => FilePath -> IO () +testPlanGroupLinkNoContactConnectingSlow tmp = do + gLink <- withNewTestChatCfg tmp testCfgSlow "alice" aliceProfile $ \alice -> do + threadDelay 100000 + alice ##> "/g team" + alice <## "group #team is created" + alice <## "to add members use /a team or /create link #team" + alice ##> "/create link #team" + getGroupLink alice "team" GRMember True + withNewTestChatCfg tmp testCfgSlow "bob" bobProfile $ \bob -> do + threadDelay 100000 + + bob ##> ("/c " <> gLink) + bob <## "connection request sent!" + + bob ##> ("/_connect plan 1 " <> gLink) + bob <## "group link: connecting, allowed to reconnect" + + let gLinkSchema2 = linkAnotherSchema gLink + bob ##> ("/_connect plan 1 " <> gLinkSchema2) + bob <## "group link: connecting, allowed to reconnect" + + threadDelay 100000 + withTestChatCfg tmp testCfgSlow "alice" $ \alice -> do + alice + <### [ "1 group links active", + "#team: group is empty", + "bob (Bob): accepting request to join group #team..." + ] + withTestChatCfg tmp testCfgSlow "bob" $ \bob -> do + threadDelay 500000 + bob <## "#team: joining the group..." bob ##> ("/_connect plan 1 " <> gLink) bob <## "group link: connecting to group #team" @@ -3232,7 +3417,7 @@ testGroupMsgDecryptError tmp = bob <## "1 contacts connected (use /cs for the list)" bob <## "#team: connected to server(s)" alice #> "#team hello again" - bob <# "#team alice> skipped message ID 10..12" + bob <# "#team alice> skipped message ID 9..11" bob <# "#team alice> hello again" bob #> "#team received!" alice <# "#team bob> received!" @@ -3255,7 +3440,7 @@ setupDesynchronizedRatchet tmp alice = do bob <## "1 contacts connected (use /cs for the list)" bob <## "#team: connected to server(s)" bob ##> "/sync #team alice" - bob <## "error: command is prohibited" + bob <## "error: command is prohibited, synchronizeRatchet: not allowed" alice #> "#team 1" bob <## "#team alice: decryption error (connection out of sync), synchronization required" bob <## "use /sync #team alice to synchronize" @@ -3283,7 +3468,7 @@ testGroupSyncRatchet tmp = bob <## "1 contacts connected (use /cs for the list)" bob <## "#team: connected to server(s)" bob `send` "#team 1" - bob <## "error: command is prohibited" -- silence? + bob <## "error: command is prohibited, sendMessagesB: send prohibited" -- silence? bob <# "#team 1" (alice ("/_get chat #1 count=3", chat, [(1, "connection synchronization started for alice"), (0, "connection synchronization agreed"), (0, "connection synchronized")]) alice #$> ("/_get chat #1 count=2", chat, [(0, "connection synchronization agreed"), (0, "connection synchronized")]) @@ -3334,6 +3520,7 @@ testGroupSyncRatchetCodeReset tmp = alice <## "#team bob: connection synchronized" bob <## "#team alice: connection synchronized" + threadDelay 100000 bob #$> ("/_get chat #1 count=4", chat, [(1, "connection synchronization started for alice"), (0, "connection synchronization agreed"), (0, "security code changed"), (0, "connection synchronized")]) alice #$> ("/_get chat #1 count=2", chat, [(0, "connection synchronization agreed"), (0, "connection synchronized")]) @@ -3895,6 +4082,7 @@ testMemberContactMessage = <### [ "#team alice is creating direct contact alice with you", WithTime "alice> hi" ] + bob <## "alice (Alice): you can send messages to contact" concurrently_ (alice <## "bob (Bob): contact is connected") (bob <## "alice (Alice): contact is connected") @@ -3928,6 +4116,7 @@ testMemberContactMessage = <### [ "#team bob is creating direct contact bob with you", WithTime "bob> hi" ] + cath <## "bob (Bob): you can send messages to contact" concurrently_ (bob <## "cath (Catherine): contact is connected") (cath <## "bob (Bob): contact is connected") @@ -3948,6 +4137,7 @@ testMemberContactNoMessage = bob ##> "/_invite member contact @3" bob <## "sent invitation to connect directly to member #team cath" cath <## "#team bob is creating direct contact bob with you" + cath <## "bob (Bob): you can send messages to contact" concurrently_ (bob <## "cath (Catherine): contact is connected") (cath <## "bob (Bob): contact is connected") @@ -3988,6 +4178,7 @@ testMemberContactProhibitedRepeatInv = <### [ "#team bob is creating direct contact bob with you", WithTime "bob> hi" ] + cath <## "bob (Bob): you can send messages to contact" concurrently_ (bob <## "cath (Catherine): contact is connected") (cath <## "bob (Bob): contact is connected") @@ -4017,6 +4208,7 @@ testMemberContactInvitedConnectionReplaced tmp = do WithTime "alice> hi", "alice: security code changed" ] + bob <## "alice (Alice): you can send messages to contact" concurrently_ (alice <## "bob (Bob): contact is connected") (bob <## "alice (Alice): contact is connected") @@ -4069,6 +4261,7 @@ testMemberContactIncognito = testChatCfg3 testCfgGroupLinkViaContact aliceProfile bobProfile cathProfile $ \alice bob cath -> do -- create group, bob joins incognito + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team or /create link #team" @@ -4126,6 +4319,7 @@ testMemberContactIncognito = <### [ ConsoleString ("#team " <> bobIncognito <> " is creating direct contact " <> bobIncognito <> " with you"), WithTime ("i " <> bobIncognito <> "> hi") ] + cath <## (bobIncognito <> ": you can send messages to contact") _ <- getTermLine bob _ <- getTermLine cath concurrentlyN_ @@ -4197,6 +4391,7 @@ testMemberContactProfileUpdate = <### [ "#team bob is creating direct contact bob with you", WithTime "bob> hi" ] + cath <## "bob (Bob): you can send messages to contact" concurrentlyN_ [ do bob <## "contact cath changed to kate (Kate)" @@ -5315,6 +5510,7 @@ testMembershipProfileUpdateNextGroupMessage = testChat3 aliceProfile bobProfile cathProfile $ \alice bob cath -> do -- create group 1 + threadDelay 100000 alice ##> "/g team" alice <## "group #team is created" alice <## "to add members use /a team or /create link #team" @@ -6059,3 +6255,73 @@ testBlockForAllCantRepeat = [alice, cath] *<# "#team bob> 3" bob #$> ("/_get chat #1 count=3", chat, [(1, "1"), (1, "2"), (1, "3")]) + +testGroupMemberInactive :: HasCallStack => FilePath -> IO () +testGroupMemberInactive tmp = do + withSmpServer' serverCfg' $ do + withNewTestChatCfgOpts tmp cfg' opts' "alice" aliceProfile $ \alice -> do + withNewTestChatCfgOpts tmp cfg' opts' "bob" bobProfile $ \bob -> do + createGroup2 "team" alice bob + + alice #> "#team hi" + bob <# "#team alice> hi" + bob #> "#team hey" + alice <# "#team bob> hey" + + -- bob is offline + alice #> "#team 1" + alice #> "#team 2" + alice #> "#team 3" + alice <## "[#team bob] connection is marked as inactive" + -- 4 and 5 will be sent to bob as pending messages + alice #> "#team 4" + alice #> "#team 5" + + pgmCount <- withCCTransaction alice $ \db -> + DB.query_ db "SELECT count(1) FROM pending_group_messages" :: IO [[Int]] + pgmCount `shouldBe` [[2]] + + threadDelay 1500000 + + withTestChatCfgOpts tmp cfg' opts' "bob" $ \bob -> do + bob <## "1 contacts connected (use /cs for the list)" + bob <## "#team: connected to server(s)" + bob <# "#team alice> 1" + bob <# "#team alice> 2" + bob <#. "#team alice> skipped message ID" + alice <## "[#team bob] inactive connection is marked as active" + + bob <# "#team alice> 4" + bob <# "#team alice> 5" + + pgmCount' <- withCCTransaction alice $ \db -> + DB.query_ db "SELECT count(1) FROM pending_group_messages" :: IO [[Int]] + pgmCount' `shouldBe` [[0]] + + -- delivery works + alice #> "#team hi" + bob <# "#team alice> hi" + bob #> "#team hey" + alice <# "#team bob> hey" + where + serverCfg' = + smpServerCfg + { transports = [("7003", transport @TLS)], + msgQueueQuota = 2 + } + fastRetryInterval = defaultReconnectInterval {initialInterval = 50_000} -- same as in agent tests + cfg' = + testCfg + { agentConfig = + testAgentCfg + { quotaExceededTimeout = 1, + messageRetryInterval = RetryInterval2 {riFast = fastRetryInterval, riSlow = fastRetryInterval} + } + } + opts' = + testOpts + { coreOptions = + testCoreOpts + { smpServers = ["smp://LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI=:server_password@localhost:7003"] + } + } diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index a8afa05af3..43ad5ba841 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -17,8 +17,8 @@ import Simplex.Chat.Store.Shared (createContact) import Simplex.Chat.Types (ConnStatus (..), Profile (..)) import Simplex.Chat.Types.Shared (GroupMemberRole (..)) import Simplex.Chat.Types.UITheme -import Simplex.Chat.Types.Util (encodeJSON) import Simplex.Messaging.Encoding.String (StrEncoding (..)) +import Simplex.Messaging.Util (encodeJSON) import System.Directory (copyFile, createDirectoryIfMissing) import Test.Hspec hiding (it) @@ -42,6 +42,7 @@ chatProfileTests = do it "contact address ok to connect; known contact" testPlanAddressOkKnown it "own contact address" testPlanAddressOwn it "connecting via contact address" testPlanAddressConnecting + it "connecting via contact address (slow handshake)" testPlanAddressConnectingSlow it "re-connect with deleted contact" testPlanAddressContactDeletedReconnected it "contact via address" testPlanAddressContactViaAddress describe "incognito" $ do @@ -51,6 +52,7 @@ chatProfileTests = do it "set connection incognito" testSetConnectionIncognito it "reset connection incognito" testResetConnectionIncognito it "set connection incognito prohibited during negotiation" testSetConnectionIncognitoProhibitedDuringNegotiation + it "set connection incognito prohibited during negotiation (slow handshake)" testSetConnectionIncognitoProhibitedDuringNegotiationSlow it "connection incognito unchanged errors" testConnectionIncognitoUnchangedErrors it "set, reset, set connection incognito" testSetResetSetConnectionIncognito it "join group incognito" testJoinGroupIncognito @@ -61,6 +63,11 @@ chatProfileTests = do describe "contact aliases" $ do it "set contact alias" testSetAlias it "set connection alias" testSetConnectionAlias + describe "pending connection users" $ do + it "change user for pending connection" testChangePCCUser + it "change from incognito profile connects as new user" testChangePCCUserFromIncognito + it "change user for pending connection and later set incognito connects as incognito in changed profile" testChangePCCUserAndThenIncognito + it "change user for user without matching servers creates new connection" testChangePCCUserDiffSrv describe "preferences" $ do it "set contact preferences" testSetContactPrefs it "feature offers" testFeatureOffers @@ -196,6 +203,7 @@ testMultiWordProfileNames = ] cath <## "#'Our Team' 'Alice Jones' is creating direct contact 'Alice Jones' with you" cath <# "'Alice Jones'> hello" + cath <## "'Alice Jones': you can send messages to contact" cath <## "'Alice Jones': contact is connected" alice <## "'Cath Johnson': contact is connected" cath ##> "/p 'Cath J'" @@ -222,7 +230,7 @@ testUserContactLink = alice <#? bob alice @@@ [("<@bob", "")] alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -234,7 +242,7 @@ testUserContactLink = alice <#? cath alice @@@ [("<@cath", ""), ("@bob", "hey")] alice ##> "/ac cath" - alice <## "cath (Catherine): accepting contact request..." + alice <## "cath (Catherine): accepting contact request, you can send messages to contact" concurrently_ (cath <## "alice (Alice): contact is connected") (alice <## "cath (Catherine): contact is connected") @@ -252,7 +260,7 @@ testProfileLink = bob ##> ("/c " <> cLink) alice <#? bob alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -267,7 +275,7 @@ testProfileLink = cath ##> ("/c " <> cLink) alice <#? cath alice ##> "/ac cath" - alice <## "cath (Catherine): accepting contact request..." + alice <## "cath (Catherine): accepting contact request, you can send messages to contact" concurrently_ (cath <## "alice (Alice): contact is connected") (alice <## "cath (Catherine): contact is connected") @@ -334,7 +342,7 @@ testUserContactLinkAutoAccept = alice <#? bob alice @@@ [("<@bob", "")] alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -348,6 +356,7 @@ testUserContactLinkAutoAccept = cath ##> ("/c " <> cLink) cath <## "connection request sent!" alice <## "cath (Catherine): accepting contact request..." + alice <## "cath (Catherine): you can send messages to contact" concurrently_ (cath <## "alice (Alice): contact is connected") (alice <## "cath (Catherine): contact is connected") @@ -362,7 +371,7 @@ testUserContactLinkAutoAccept = alice <#? dan alice @@@ [("<@dan", ""), ("@cath", "hey"), ("@bob", "hey")] alice ##> "/ac dan" - alice <## "dan (Daniel): accepting contact request..." + alice <## "dan (Daniel): accepting contact request, you can send messages to contact" concurrently_ (dan <## "alice (Alice): contact is connected") (alice <## "dan (Daniel): contact is connected") @@ -389,7 +398,7 @@ testDeduplicateContactRequests = testChat3 aliceProfile bobProfile cathProfile $ bob @@@! [(":3", "", Just ConnJoined), (":2", "", Just ConnJoined), (":1", "", Just ConnJoined)] alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -397,6 +406,7 @@ testDeduplicateContactRequests = testChat3 aliceProfile bobProfile cathProfile $ bob ##> ("/c " <> cLink) bob <## "contact address: known contact alice" bob <## "use @alice to send messages" + threadDelay 100000 alice @@@ [("@bob", lastChatFeature)] bob @@@ [("@alice", lastChatFeature), (":2", ""), (":1", "")] bob ##> "/_delete :1" @@ -420,7 +430,7 @@ testDeduplicateContactRequests = testChat3 aliceProfile bobProfile cathProfile $ alice <#? cath alice @@@ [("<@cath", ""), ("@bob", "hey")] alice ##> "/ac cath" - alice <## "cath (Catherine): accepting contact request..." + alice <## "cath (Catherine): accepting contact request, you can send messages to contact" concurrently_ (cath <## "alice (Alice): contact is connected") (alice <## "cath (Catherine): contact is connected") @@ -462,7 +472,7 @@ testDeduplicateContactRequestsProfileChange = testChat3 aliceProfile bobProfile alice ##> "/ac bob" alice <## "no contact request from bob" alice ##> "/ac robert" - alice <## "robert (Robert): accepting contact request..." + alice <## "robert (Robert): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "robert (Robert): contact is connected") @@ -470,6 +480,7 @@ testDeduplicateContactRequestsProfileChange = testChat3 aliceProfile bobProfile bob ##> ("/c " <> cLink) bob <## "contact address: known contact alice" bob <## "use @alice to send messages" + threadDelay 100000 alice @@@ [("@robert", lastChatFeature)] bob @@@ [("@alice", lastChatFeature), (":3", ""), (":2", ""), (":1", "")] bob ##> "/_delete :1" @@ -488,6 +499,7 @@ testDeduplicateContactRequestsProfileChange = testChat3 aliceProfile bobProfile bob <## "use @alice to send messages" alice <##> bob + threadDelay 100000 alice #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(1, "hi"), (0, "hey"), (1, "hi"), (0, "hey")]) bob #$> ("/_get chat @2 count=100", chat, chatFeatures <> [(0, "hi"), (1, "hey"), (0, "hi"), (1, "hey")]) @@ -495,7 +507,7 @@ testDeduplicateContactRequestsProfileChange = testChat3 aliceProfile bobProfile alice <#? cath alice @@@ [("<@cath", ""), ("@robert", "hey")] alice ##> "/ac cath" - alice <## "cath (Catherine): accepting contact request..." + alice <## "cath (Catherine): accepting contact request, you can send messages to contact" concurrently_ (cath <## "alice (Alice): contact is connected") (alice <## "cath (Catherine): contact is connected") @@ -561,13 +573,13 @@ testAutoReplyMessage = testChat2 aliceProfile bobProfile $ bob ##> ("/c " <> cLink) bob <## "connection request sent!" alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): you can send messages to contact" + alice <# "@bob hello!" concurrentlyN_ [ do - bob <## "alice (Alice): contact is connected" - bob <# "alice> hello!", - do - alice <## "bob (Bob): contact is connected" - alice <# "@bob hello!" + bob <# "alice> hello!" + bob <## "alice (Alice): contact is connected", + alice <## "bob (Bob): contact is connected" ] testAutoReplyMessageInIncognito :: HasCallStack => FilePath -> IO () @@ -583,17 +595,16 @@ testAutoReplyMessageInIncognito = testChat2 aliceProfile bobProfile $ bob ##> ("/c " <> cLink) bob <## "connection request sent!" alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): you can send messages to contact" + alice <# "i @bob hello!" aliceIncognito <- getTermLine alice concurrentlyN_ [ do - bob <## (aliceIncognito <> ": contact is connected") - bob <# (aliceIncognito <> "> hello!"), + bob <# (aliceIncognito <> "> hello!") + bob <## (aliceIncognito <> ": contact is connected"), do alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> aliceIncognito) - alice - <### [ "use /i bob to print out this incognito profile again", - WithTime "i @bob hello!" - ] + alice <## "use /i bob to print out this incognito profile again" ] testPlanAddressOkKnown :: HasCallStack => FilePath -> IO () @@ -610,7 +621,7 @@ testPlanAddressOkKnown = alice <#? bob alice @@@ [("<@bob", "")] alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -649,12 +660,13 @@ testPlanAddressOwn tmp = alice <## "to reject: /rc alice_1 (the sender will NOT be notified)" alice @@@ [("<@alice_1", ""), (":2", "")] alice ##> "/ac alice_1" - alice <## "alice_1 (Alice): accepting contact request..." + alice <## "alice_1 (Alice): accepting contact request, you can send messages to contact" alice <### [ "alice_1 (Alice): contact is connected", "alice_2 (Alice): contact is connected" ] + threadDelay 100000 alice @@@ [("@alice_1", lastChatFeature), ("@alice_2", lastChatFeature)] alice `send` "@alice_2 hi" alice @@ -699,8 +711,51 @@ testPlanAddressConnecting tmp = do alice <## "to accept: /ac bob" alice <## "to reject: /rc bob (the sender will NOT be notified)" alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" withTestChat tmp "bob" $ \bob -> do + threadDelay 500000 + bob <## "alice (Alice): contact is connected" + bob @@@ [("@alice", "Audio/video calls: enabled")] + bob ##> ("/_connect plan 1 " <> cLink) + bob <## "contact address: known contact alice" + bob <## "use @alice to send messages" + + let cLinkSchema2 = linkAnotherSchema cLink + bob ##> ("/_connect plan 1 " <> cLinkSchema2) + bob <## "contact address: known contact alice" + bob <## "use @alice to send messages" + + bob ##> ("/c " <> cLink) + bob <## "contact address: known contact alice" + bob <## "use @alice to send messages" + +testPlanAddressConnectingSlow :: HasCallStack => FilePath -> IO () +testPlanAddressConnectingSlow tmp = do + cLink <- withNewTestChatCfg tmp testCfgSlow "alice" aliceProfile $ \alice -> do + alice ##> "/ad" + getContactLink alice True + withNewTestChatCfg tmp testCfgSlow "bob" bobProfile $ \bob -> do + threadDelay 100000 + + bob ##> ("/c " <> cLink) + bob <## "connection request sent!" + + bob ##> ("/_connect plan 1 " <> cLink) + bob <## "contact address: connecting, allowed to reconnect" + + let cLinkSchema2 = linkAnotherSchema cLink + bob ##> ("/_connect plan 1 " <> cLinkSchema2) + bob <## "contact address: connecting, allowed to reconnect" + + threadDelay 100000 + withTestChatCfg tmp testCfgSlow "alice" $ \alice -> do + alice <## "Your address is active! To show: /sa" + alice <## "bob (Bob) wants to connect to you!" + alice <## "to accept: /ac bob" + alice <## "to reject: /rc bob (the sender will NOT be notified)" + alice ##> "/ac bob" + alice <## "bob (Bob): accepting contact request..." + withTestChatCfg tmp testCfgSlow "bob" $ \bob -> do threadDelay 500000 bob @@@ [("@alice", "")] bob ##> ("/_connect plan 1 " <> cLink) @@ -723,7 +778,7 @@ testPlanAddressContactDeletedReconnected = bob ##> ("/c " <> cLink) alice <#? bob alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -754,7 +809,7 @@ testPlanAddressContactDeletedReconnected = alice <## "to accept: /ac bob" alice <## "to reject: /rc bob (the sender will NOT be notified)" alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice_1 (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -828,7 +883,7 @@ testPlanAddressContactViaAddress = alice <## "to accept: /ac bob" alice <## "to reject: /rc bob (the sender will NOT be notified)" alice ##> "/ac bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -922,7 +977,7 @@ testConnectIncognitoContactAddress = testChat2 aliceProfile bobProfile $ alice <## ("to accept: /ac " <> bobIncognito) alice <## ("to reject: /rc " <> bobIncognito <> " (the sender will NOT be notified)") alice ##> ("/ac " <> bobIncognito) - alice <## (bobIncognito <> ": accepting contact request...") + alice <## (bobIncognito <> ": accepting contact request, you can send messages to contact") _ <- getTermLine bob concurrentlyN_ [ do @@ -956,7 +1011,7 @@ testAcceptContactRequestIncognito = testChat3 aliceProfile bobProfile cathProfil bob ##> ("/c " <> cLink) alice <#? bob alice ##> "/accept incognito bob" - alice <## "bob (Bob): accepting contact request..." + alice <## "bob (Bob): accepting contact request, you can send messages to contact" aliceIncognitoBob <- getTermLine alice concurrentlyN_ [ bob <## (aliceIncognitoBob <> ": contact is connected"), @@ -984,7 +1039,7 @@ testAcceptContactRequestIncognito = testChat3 aliceProfile bobProfile cathProfil cath ##> ("/c " <> cLink) alice <#? cath alice ##> "/_accept incognito=on 1" - alice <## "cath (Catherine): accepting contact request..." + alice <## "cath (Catherine): accepting contact request, you can send messages to contact" aliceIncognitoCath <- getTermLine alice concurrentlyN_ [ cath <## (aliceIncognitoCath <> ": contact is connected"), @@ -1046,9 +1101,30 @@ testSetConnectionIncognitoProhibitedDuringNegotiation tmp = do bob <## "confirmation sent!" withTestChat tmp "alice" $ \alice -> do threadDelay 250000 + alice <## "bob (Bob): contact is connected" alice ##> "/_set incognito :1 on" alice <## "chat db error: SEPendingConnectionNotFound {connId = 1}" withTestChat tmp "bob" $ \bob -> do + bob <## "alice (Alice): contact is connected" + alice <##> bob + alice `hasContactProfiles` ["alice", "bob"] + bob `hasContactProfiles` ["alice", "bob"] + +testSetConnectionIncognitoProhibitedDuringNegotiationSlow :: HasCallStack => FilePath -> IO () +testSetConnectionIncognitoProhibitedDuringNegotiationSlow tmp = do + inv <- withNewTestChatCfg tmp testCfgSlow "alice" aliceProfile $ \alice -> do + threadDelay 250000 + alice ##> "/connect" + getInvitation alice + withNewTestChatCfg tmp testCfgSlow "bob" bobProfile $ \bob -> do + threadDelay 250000 + bob ##> ("/c " <> inv) + bob <## "confirmation sent!" + withTestChatCfg tmp testCfgSlow "alice" $ \alice -> do + threadDelay 250000 + alice ##> "/_set incognito :1 on" + alice <## "chat db error: SEPendingConnectionNotFound {connId = 1}" + withTestChatCfg tmp testCfgSlow "bob" $ \bob -> do concurrently_ (bob <## "alice (Alice): contact is connected") (alice <## "bob (Bob): contact is connected") @@ -1486,6 +1562,126 @@ testSetAlias = testChat2 aliceProfile bobProfile $ alice ##> "/contacts" alice <## "bob (Bob)" +testChangePCCUser :: HasCallStack => FilePath -> IO () +testChangePCCUser = testChat2 aliceProfile bobProfile $ + \alice bob -> do + -- Create a new invite + alice ##> "/connect" + inv <- getInvitation alice + -- Create new user and go back to original user + alice ##> "/create user alisa" + showActiveUser alice "alisa" + alice ##> "/create user alisa2" + showActiveUser alice "alisa2" + alice ##> "/user alice" + showActiveUser alice "alice (Alice)" + -- Change connection to newly created user + alice ##> "/_set conn user :1 2" + alice <## "connection 1 changed from user alice to user alisa" + alice ##> "/user alisa" + showActiveUser alice "alisa" + -- Change connection back to other user + alice ##> "/_set conn user :1 3" + alice <## "connection 1 changed from user alisa to user alisa2" + alice ##> "/user alisa2" + showActiveUser alice "alisa2" + -- Connect + bob ##> ("/connect " <> inv) + bob <## "confirmation sent!" + concurrently_ + (alice <## "bob (Bob): contact is connected") + (bob <## "alisa2: contact is connected") + +testChangePCCUserFromIncognito :: HasCallStack => FilePath -> IO () +testChangePCCUserFromIncognito = testChat2 aliceProfile bobProfile $ + \alice bob -> do + -- Create a new invite and set as incognito + alice ##> "/connect" + inv <- getInvitation alice + alice ##> "/_set incognito :1 on" + alice <## "connection 1 changed to incognito" + -- Create new user and go back to original user + alice ##> "/create user alisa" + showActiveUser alice "alisa" + alice ##> "/user alice" + showActiveUser alice "alice (Alice)" + -- Change connection to newly created user + alice ##> "/_set conn user :1 2" + alice <## "connection 1 changed from user alice to user alisa" + alice `hasContactProfiles` ["alice"] + alice ##> "/user alisa" + showActiveUser alice "alisa" + -- Change connection back to initial user + alice ##> "/_set conn user :1 1" + alice <## "connection 1 changed from user alisa to user alice" + alice ##> "/user alice" + showActiveUser alice "alice (Alice)" + -- Connect + bob ##> ("/connect " <> inv) + bob <## "confirmation sent!" + concurrently_ + (alice <## "bob (Bob): contact is connected") + (bob <## "alice (Alice): contact is connected") + +testChangePCCUserAndThenIncognito :: HasCallStack => FilePath -> IO () +testChangePCCUserAndThenIncognito = testChat2 aliceProfile bobProfile $ + \alice bob -> do + -- Create a new invite and set as incognito + alice ##> "/connect" + inv <- getInvitation alice + -- Create new user and go back to original user + alice ##> "/create user alisa" + showActiveUser alice "alisa" + alice ##> "/user alice" + showActiveUser alice "alice (Alice)" + -- Change connection to newly created user + alice ##> "/_set conn user :1 2" + alice <## "connection 1 changed from user alice to user alisa" + alice ##> "/user alisa" + showActiveUser alice "alisa" + -- Change connection to incognito and make sure it's attached to the newly created user profile + alice ##> "/_set incognito :1 on" + alice <## "connection 1 changed to incognito" + bob ##> ("/connect " <> inv) + bob <## "confirmation sent!" + alisaIncognito <- getTermLine alice + concurrentlyN_ + [ bob <## (alisaIncognito <> ": contact is connected"), + do + alice <## ("bob (Bob): contact is connected, your incognito profile for this contact is " <> alisaIncognito) + alice <## ("use /i bob to print out this incognito profile again") + ] + +testChangePCCUserDiffSrv :: HasCallStack => FilePath -> IO () +testChangePCCUserDiffSrv = testChat2 aliceProfile bobProfile $ + \alice bob -> do + -- Create a new invite + alice ##> "/connect" + _ <- getInvitation alice + alice ##> "/_set incognito :1 on" + alice <## "connection 1 changed to incognito" + -- Create new user with different servers + alice ##> "/create user alisa" + showActiveUser alice "alisa" + alice #$> ("/smp smp://2345-w==@smp2.example.im smp://3456-w==@smp3.example.im:5224", id, "ok") + alice ##> "/user alice" + showActiveUser alice "alice (Alice)" + -- Change connection to newly created user and use the newly created connection + alice ##> "/_set conn user :1 2" + alice <## "connection 1 changed from user alice to user alisa, new link:" + alice <## "" + inv <- getTermLine alice + alice <## "" + alice `hasContactProfiles` ["alice"] + alice ##> "/user alisa" + showActiveUser alice "alisa" + -- Connect + bob ##> ("/connect " <> inv) + bob <## "confirmation sent!" + concurrently_ + (alice <## "bob (Bob): contact is connected") + (bob <## "alisa: contact is connected") + testSetConnectionAlias :: HasCallStack => FilePath -> IO () testSetConnectionAlias = testChat2 aliceProfile bobProfile $ \alice bob -> do @@ -1948,21 +2144,30 @@ testGroupPrefsDirectForRole = testChat4 aliceProfile bobProfile cathProfile danP bob <## "#team: cath added dan (Daniel) to the group (connecting...)" bob <## "#team: new member dan is connected" ] - -- dan cannot send direct messages to alice (owner) + + -- dan cannot send direct messages to alice dan ##> "@alice hello alice" dan <## "bad chat command: direct messages not allowed" (alice hello dan" - dan <## "alice (Alice): contact is connected" - -- and now dan can too + alice + <### [ "member #team dan does not have direct connection, creating", + "contact for member #team dan is created", + "sent invitation to connect directly to member #team dan", + WithTime "@dan hello dan" + ] + dan + <### [ "#team alice is creating direct contact alice with you", + WithTime "alice> hello dan" + ] + dan <## "alice (Alice): you can send messages to contact" + concurrently_ + (alice <## "dan (Daniel): contact is connected") + (dan <## "alice (Alice): contact is connected") + + -- now dan can send messages to alice dan #> "@alice hi alice" alice <# "dan> hi alice" where @@ -2020,10 +2225,19 @@ testGroupPrefsSimplexLinksForRole = testChat3 aliceProfile bobProfile cathProfil threadDelay 1000000 bob ##> "/c" inv <- getInvitation bob - bob ##> ("#team " <> inv) + bob ##> ("#team \"" <> inv <> "\\ntest\"") + bob <## "bad chat command: feature not allowed SimpleX links" + bob ##> ("/_send #1 json {\"msgContent\": {\"type\": \"text\", \"text\": \"" <> inv <> "\\ntest\"}}") bob <## "bad chat command: feature not allowed SimpleX links" (alice inv <> "\\ntest\"") + bob <# ("@alice " <> inv) + bob <## "test" + alice <# ("bob> " <> inv) + alice <## "test" + bob ##> "#team <- @alice https://simplex.chat" + bob <## "bad chat command: feature not allowed SimpleX links" alice #> ("#team " <> inv) bob <# ("#team alice> " <> inv) cath <# ("#team alice> " <> inv) @@ -2080,7 +2294,7 @@ testSetUITheme = a <## "you've shared main profile with this contact" a <## "connection not verified, use /code command to see security code" a <## "quantum resistant end-to-end encryption" - a <## "peer chat protocol version range: (Version 1, Version 8)" + a <## "peer chat protocol version range: (Version 1, Version 9)" groupInfo a = do a <## "group ID: 1" a <## "current members: 1" diff --git a/tests/ChatTests/Utils.hs b/tests/ChatTests/Utils.hs index a3ee0d5ca8..8c022d5bfd 100644 --- a/tests/ChatTests/Utils.hs +++ b/tests/ChatTests/Utils.hs @@ -298,7 +298,7 @@ itemId i = show $ length chatFeatures + i (@@@) :: HasCallStack => TestCC -> [(String, String)] -> Expectation (@@@) cc res = do - threadDelay 10000 + threadDelay 100000 getChats mapChats cc res mapChats :: [(String, String, Maybe ConnStatus)] -> [(String, String)] diff --git a/tests/MarkdownTests.hs b/tests/MarkdownTests.hs index d2d15dc166..615edc02c4 100644 --- a/tests/MarkdownTests.hs +++ b/tests/MarkdownTests.hs @@ -149,6 +149,11 @@ textWithUri = describe "text with Uri" do parseMarkdown "http://simplex.chat" `shouldBe` uri "http://simplex.chat" parseMarkdown "this is https://simplex.chat" `shouldBe` "this is " <> uri "https://simplex.chat" parseMarkdown "https://simplex.chat site" `shouldBe` uri "https://simplex.chat" <> " site" + parseMarkdown "SimpleX on GitHub: https://github.com/simplex-chat/" `shouldBe` "SimpleX on GitHub: " <> uri "https://github.com/simplex-chat/" + parseMarkdown "SimpleX on GitHub: https://github.com/simplex-chat." `shouldBe` "SimpleX on GitHub: " <> uri "https://github.com/simplex-chat" <> "." + parseMarkdown "https://github.com/simplex-chat/ - SimpleX on GitHub" `shouldBe` uri "https://github.com/simplex-chat/" <> " - SimpleX on GitHub" + -- parseMarkdown "SimpleX on GitHub (https://github.com/simplex-chat/)" `shouldBe` "SimpleX on GitHub (" <> uri "https://github.com/simplex-chat/" <> ")" + parseMarkdown "https://en.m.wikipedia.org/wiki/Servo_(software)" `shouldBe` uri "https://en.m.wikipedia.org/wiki/Servo_(software)" it "ignored as markdown" do parseMarkdown "_https://simplex.chat" `shouldBe` "_https://simplex.chat" parseMarkdown "this is _https://simplex.chat" `shouldBe` "this is _https://simplex.chat" @@ -210,3 +215,10 @@ multilineMarkdownList = describe "multiline markdown" do parseMaybeMarkdownList "http://simplex.chat\ntext 1\ntext 2\nhttp://app.simplex.chat" `shouldBe` Just [uri' "http://simplex.chat", "\ntext 1\ntext 2\n", uri' "http://app.simplex.chat"] it "no markdown" do parseMaybeMarkdownList "not a\nmarkdown" `shouldBe` Nothing + let inv = "/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D" + it "multiline with simplex link" do + parseMaybeMarkdownList ("https://simplex.chat" <> inv <> "\ntext") + `shouldBe` Just + [ FormattedText (Just $ SimplexLink XLInvitation ("simplex:" <> inv) ["smp.simplex.im"]) ("https://simplex.chat" <> inv), + "\ntext" + ] diff --git a/tests/MobileTests.hs b/tests/MobileTests.hs index f41d0172de..cd4b3980eb 100644 --- a/tests/MobileTests.hs +++ b/tests/MobileTests.hs @@ -294,7 +294,7 @@ testFileCApi fileName tmp = do let sz' = fromIntegral sz contents <- create sz' $ \toPtr -> copyBytes toPtr (ptr' `plusPtr` 5) sz' contents `shouldBe` src - sz' `shouldBe` fromIntegral len + sz' `shouldBe` len testMissingFileCApi :: FilePath -> IO () testMissingFileCApi tmp = do diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index c4dfa7da25..d9552452cd 100644 --- a/tests/ProtocolTests.hs +++ b/tests/ProtocolTests.hs @@ -34,7 +34,8 @@ queue = SMPQueueAddress { smpServer = srv, senderId = "\223\142z\251", - dhPublicKey = "MCowBQYDK2VuAyEAjiswwI3O/NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o=" + dhPublicKey = "MCowBQYDK2VuAyEAjiswwI3O/NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o=", + sndSecure = False } connReqData :: ConnReqUriData @@ -132,7 +133,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" ##==## ChatMessage chatInitialVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing))) it "x.msg.new chat message with chat version range" $ - "{\"v\":\"1-8\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" + "{\"v\":\"1-9\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" ##==## ChatMessage supportedChatVRange (Just $ SharedMsgId "\1\2\3\4") (XMsgNew (MCSimple (extMsgContent (MCText "hello") Nothing))) it "x.msg.new quote" $ "{\"v\":\"1\",\"msgId\":\"AQIDBA==\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello to you too\",\"type\":\"text\"},\"quote\":{\"content\":{\"text\":\"hello there!\",\"type\":\"text\"},\"msgRef\":{\"msgId\":\"BQYHCA==\",\"sent\":true,\"sentAt\":\"1970-01-01T00:00:01.000000001Z\"}}}}" @@ -194,7 +195,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.msg.deleted\",\"params\":{}}" #==# XMsgDeleted it "x.file" $ - "{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" + "{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" #==# XFile FileInvitation {fileName = "photo.jpg", fileSize = 12345, fileDigest = Nothing, fileConnReq = Just testConnReq, fileInline = Nothing, fileDescr = Nothing} it "x.file without file invitation" $ "{\"v\":\"1\",\"event\":\"x.file\",\"params\":{\"file\":{\"fileSize\":12345,\"fileName\":\"photo.jpg\"}}}" @@ -203,7 +204,7 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.file.acpt\",\"params\":{\"fileName\":\"photo.jpg\"}}" #==# XFileAcpt "photo.jpg" it "x.file.acpt.inv" $ - "{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\",\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" + "{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\",\"fileConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" #==# XFileAcptInv (SharedMsgId "\1\2\3\4") (Just testConnReq) "photo.jpg" it "x.file.acpt.inv" $ "{\"v\":\"1\",\"event\":\"x.file.acpt.inv\",\"params\":{\"msgId\":\"AQIDBA==\",\"fileName\":\"photo.jpg\"}}" @@ -230,10 +231,10 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.contact\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"},\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" ==# XContact testProfile Nothing it "x.grp.inv" $ - "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}}}}" #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, groupLinkId = Nothing, groupSize = Nothing} it "x.grp.inv with group link id" $ - "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}" + "{\"v\":\"1\",\"event\":\"x.grp.inv\",\"params\":{\"groupInvitation\":{\"connRequest\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"invitedMember\":{\"memberRole\":\"member\",\"memberId\":\"BQYHCA==\"},\"groupProfile\":{\"fullName\":\"Team\",\"displayName\":\"team\",\"groupPreferences\":{\"reactions\":{\"enable\":\"on\"},\"voice\":{\"enable\":\"on\"}}},\"fromMember\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\"}, \"groupLinkId\":\"AQIDBA==\"}}}" #==# XGrpInv GroupInvitation {fromMember = MemberIdRole (MemberId "\1\2\3\4") GRAdmin, invitedMember = MemberIdRole (MemberId "\5\6\7\8") GRMember, connRequest = testConnReq, groupProfile = testGroupProfile, groupLinkId = Just $ GroupLinkId "\1\2\3\4", groupSize = Nothing} it "x.grp.acpt without incognito profile" $ "{\"v\":\"1\",\"event\":\"x.grp.acpt\",\"params\":{\"memberId\":\"AQIDBA==\"}}" @@ -242,28 +243,28 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} it "x.grp.mem.new with member chat version range" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-8\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.new\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-9\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemNew MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} it "x.grp.mem.intro" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} Nothing it "x.grp.mem.intro with member chat version range" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-8\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-9\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} Nothing it "x.grp.mem.intro with member restrictions" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.intro\",\"params\":{\"memberRestrictions\":{\"restriction\":\"blocked\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemIntro MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} (Just MemberRestrictions {restriction = MRSBlocked}) it "x.grp.mem.inv" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" #==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq} it "x.grp.mem.inv w/t directConnReq" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.inv\",\"params\":{\"memberId\":\"AQIDBA==\",\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}}" #==# XGrpMemInv (MemberId "\1\2\3\4") IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing} it "x.grp.mem.fwd" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"directConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\",\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Nothing, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Just testConnReq} it "x.grp.mem.fwd with member chat version range and w/t directConnReq" $ - "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-8\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" + "{\"v\":\"1\",\"event\":\"x.grp.mem.fwd\",\"params\":{\"memberIntro\":{\"groupConnReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"},\"memberInfo\":{\"memberRole\":\"admin\",\"memberId\":\"AQIDBA==\",\"v\":\"1-9\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}}" #==# XGrpMemFwd MemberInfo {memberId = MemberId "\1\2\3\4", memberRole = GRAdmin, v = Just $ ChatVersionRange supportedChatVRange, profile = testProfile} IntroInvitation {groupConnReq = testConnReq, directConnReq = Nothing} it "x.grp.mem.info" $ "{\"v\":\"1\",\"event\":\"x.grp.mem.info\",\"params\":{\"memberId\":\"AQIDBA==\",\"profile\":{\"fullName\":\"Alice\",\"displayName\":\"alice\",\"image\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII=\",\"preferences\":{\"reactions\":{\"allow\":\"yes\"},\"voice\":{\"allow\":\"yes\"}}}}}" @@ -284,10 +285,10 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"v\":\"1\",\"event\":\"x.grp.del\",\"params\":{}}" ==# XGrpDel it "x.grp.direct.inv" $ - "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" + "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\", \"content\":{\"text\":\"hello\",\"type\":\"text\"}}}" #==# XGrpDirectInv testConnReq (Just $ MCText "hello") it "x.grp.direct.inv without content" $ - "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" + "{\"v\":\"1\",\"event\":\"x.grp.direct.inv\",\"params\":{\"connReq\":\"simplex:/invitation#/?v=1&smp=smp%3A%2F%2F1234-w%3D%3D%40smp.simplex.im%3A5223%2F3456-w%3D%3D%23%2F%3Fv%3D1-3%26dh%3DMCowBQYDK2VuAyEAjiswwI3O_NlS8Fk3HJUW870EY2bAwmttMBsvRB9eV3o%253D&e2e=v%3D2-3%26x3dh%3DMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D%2CMEIwBQYDK2VvAzkAmKuSYeQ_m0SixPDS8Wq8VBaTS1cW-Lp0n0h4Diu-kUpR-qXx4SDJ32YGEFoGFGSbGPry5Ychr6U%3D\"}}" #==# XGrpDirectInv testConnReq Nothing -- it "x.grp.msg.forward" -- $ "{\"v\":\"1\",\"event\":\"x.grp.msg.forward\",\"params\":{\"msgForward\":{\"memberId\":\"AQIDBA==\",\"msg\":\"{\"v\":\"1\",\"event\":\"x.msg.new\",\"params\":{\"content\":{\"text\":\"hello\",\"type\":\"text\"}}}\",\"msgTs\":\"1970-01-01T00:00:01.000000001Z\"}}}" diff --git a/tests/RandomServers.hs b/tests/RandomServers.hs new file mode 100644 index 0000000000..0c6baa71bb --- /dev/null +++ b/tests/RandomServers.hs @@ -0,0 +1,51 @@ +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# OPTIONS_GHC -Wno-orphans #-} + +module RandomServers where + +import Control.Monad (replicateM) +import qualified Data.List.NonEmpty as L +import Simplex.Chat (cfgServers, cfgServersToUse, defaultChatConfig, randomServers) +import Simplex.Chat.Controller (ChatConfig (..)) +import Simplex.Messaging.Agent.Env.SQLite (ServerCfg (..)) +import Simplex.Messaging.Protocol (ProtoServerWithAuth (..), SProtocolType (..), UserProtocol) +import Test.Hspec + +randomServersTests :: Spec +randomServersTests = describe "choosig random servers" $ do + it "should choose 4 random SMP servers and keep the rest disabled" testRandomSMPServers + it "should keep all 6 XFTP servers" testRandomXFTPServers + +deriving instance Eq (ServerCfg p) + +testRandomSMPServers :: IO () +testRandomSMPServers = do + [srvs1, srvs2, srvs3] <- + replicateM 3 $ + checkEnabled SPSMP 4 False =<< randomServers SPSMP defaultChatConfig + (srvs1 == srvs2 && srvs2 == srvs3) `shouldBe` False -- && to avoid rare failures + +testRandomXFTPServers :: IO () +testRandomXFTPServers = do + [srvs1, srvs2, srvs3] <- + replicateM 3 $ + checkEnabled SPXFTP 6 True =<< randomServers SPXFTP defaultChatConfig + (srvs1 == srvs2 && srvs2 == srvs3) `shouldBe` True + +checkEnabled :: UserProtocol p => SProtocolType p -> Int -> Bool -> (L.NonEmpty (ServerCfg p), [ServerCfg p]) -> IO [ServerCfg p] +checkEnabled p n allUsed (srvs, _) = do + let def = defaultServers defaultChatConfig + cfgSrvs = L.sortWith server' $ cfgServers p def + toUse = cfgServersToUse p def + srvs == cfgSrvs `shouldBe` allUsed + L.map enable srvs `shouldBe` L.map enable cfgSrvs + let enbldSrvs = L.filter (\ServerCfg {enabled} -> enabled) srvs + toUse `shouldBe` n + length enbldSrvs `shouldBe` n + pure enbldSrvs + where + server' ServerCfg {server = ProtoServerWithAuth srv _} = srv + enable :: forall p. ServerCfg p -> ServerCfg p + enable srv = (srv :: ServerCfg p) {enabled = False} diff --git a/tests/RemoteTests.hs b/tests/RemoteTests.hs index ac6fa7b23a..3f1bad613a 100644 --- a/tests/RemoteTests.hs +++ b/tests/RemoteTests.hs @@ -119,7 +119,7 @@ remoteHandshakeRejectTest = testChat3 aliceProfile aliceDesktopProfile bobProfil inv <- getTermLine desktop mobileBob ##> ("/connect remote ctrl " <> inv) mobileBob <## ("connecting new remote controller: My desktop, v" <> versionNumber) - mobileBob <## "remote controller stopped" + mobileBob <## "remote controller stopped: this link was used with another controller, please create a new link on the host" -- the server remains active after rejecting invalid client mobile ##> ("/connect remote ctrl " <> inv) diff --git a/tests/Test.hs b/tests/Test.hs index cbe8b627ec..3d59b840dd 100644 --- a/tests/Test.hs +++ b/tests/Test.hs @@ -10,6 +10,7 @@ import MarkdownTests import MessageBatching import MobileTests import ProtocolTests +import RandomServers import RemoteTests import SchemaDump import Test.Hspec hiding (it) @@ -30,6 +31,7 @@ main = do around tmpBracket $ describe "WebRTC encryption" webRTCTests describe "Valid names" validNameTests describe "Message batching" batchingTests + describe "Random servers" randomServersTests around testBracket $ do describe "Mobile API Tests" mobileTests describe "SimpleX chat client" chatTests diff --git a/website/.eleventy.js b/website/.eleventy.js index 64aedbc04c..a0a35f3366 100644 --- a/website/.eleventy.js +++ b/website/.eleventy.js @@ -6,6 +6,7 @@ const uri = require('fast-uri') const i18n = require('eleventy-plugin-i18n') const fs = require("fs") const path = require("path") +const matter = require('gray-matter') const pluginRss = require('@11ty/eleventy-plugin-rss') const { JSDOM } = require('jsdom') @@ -388,16 +389,36 @@ module.exports = function (ty) { linkify: true, replaceLink: function (link, _env) { let parsed = uri.parse(link) - if (parsed.scheme || parsed.host || !parsed.path.endsWith(".md")) { - return link + if (parsed.scheme || parsed.host) return link + + let hostFile = path.resolve(_env.page.inputPath) + let linkFile = path.resolve(hostFile, '..', parsed.path) + if (parsed.path.startsWith('/')) { + let srcIndex = hostFile.indexOf("/src") + if (srcIndex !== -1) { + linkFile = path.join(hostFile.slice(0, srcIndex + 4), parsed.path) + } } - if (parsed.path.startsWith("../../blog")) { - parsed.path = parsed.path.replace("../../blog", "/blog") + + if (fs.existsSync(linkFile) && fs.statSync(linkFile).isFile()) { + // this condition works if the link is a valid website file + const fileContent = fs.readFileSync(linkFile, 'utf8') + parsed.path = (matter(fileContent).data?.permalink || parsed.path).replace(/\.md$/, ".html").toLowerCase() + } else if (!fs.existsSync(linkFile)) { + linkFile = linkFile.replace('/website/src', '') + if (fs.existsSync(linkFile)) { + // this condition works if the link is a valid project file + const githubUrl = "https://github.com/simplex-chat/simplex-chat/blob/stable" + const keyword = "/simplex-chat" + index = linkFile.indexOf(keyword) + linkFile = linkFile.substring(index + keyword.length) + parsed.path = `${githubUrl}${linkFile}` + } else { + // if the link is not a valid website file or project file + throw new Error(`Broken link: ${parsed.path} in ${hostFile}`) + } } - if (parsed.path.startsWith("../PRIVACY.md")) { - parsed.path = parsed.path.replace("../PRIVACY.md", "/privacy") - } - parsed.path = parsed.path.replace(/\.md$/, ".html").toLowerCase() + return uri.serialize(parsed) } }).use(markdownItAnchor, { @@ -422,4 +443,4 @@ module.exports = function (ty) { htmlTemplateEngine: 'njk', dataTemplateEngine: 'njk', } -} +} \ No newline at end of file diff --git a/website/langs/ar.json b/website/langs/ar.json index 7b087aab42..956b55e72c 100644 --- a/website/langs/ar.json +++ b/website/langs/ar.json @@ -31,7 +31,7 @@ "simplex-explained-tab-2-p-1": "لكل اتصال، تستخدم قائمتي انتظار منفصلتين للمُراسلة لإرسال واستلام الرسائل عبر خوادم مختلفة.", "simplex-explained-tab-2-p-2": "تقوم الخوادم بتمرير الرسائل في اتجاه واحد فقط، دون الحصول على الصورة الكاملة لمُحادثات المستخدم أو اتصالاته.", "simplex-explained-tab-3-p-1": "تحتوي الخوادم على بيانات اعتماد مجهولة منفصلة لكل قائمة انتظار، ولا تعرف المستخدمين الذين ينتمون إليهم.", - "copyright-label": "مشروع مفتوح المصدر © SimpleX 2020-2023", + "copyright-label": "مشروع مفتوح المصدر © SimpleX 2020-2024", "simplex-chat-protocol": "بروتوكول دردشة SimpleX", "developers": "المطورين", "hero-subheader": "أول نظام مُراسلة
دون معرّفات مُستخدم", @@ -240,7 +240,7 @@ "signing-key-fingerprint": "توقيع مفتاح البصمة (SHA-256)", "f-droid-org-repo": "مستودع F-Droid.org", "stable-versions-built-by-f-droid-org": "الإصدارات الثابتة التي تم إنشاؤها بواسطة F-Droid.org", - "releases-to-this-repo-are-done-1-2-days-later": "يتم إصدار الإصدارات إلى هذا المستودع بعد يوم أو يومين", + "releases-to-this-repo-are-done-1-2-days-later": "تتم الإصدارات إلى هذا المستودع بعد عِدة أيام", "f-droid-page-simplex-chat-repo-section-text": "لإضافته إلى عميل F-Droid، امسح رمز QR أو استخدم عنوان URL هذا:", "f-droid-page-f-droid-org-repo-section-text": "مستودعات SimpleX Chat و F-Droid.org مبنية على مفاتيح مختلفة. للتبديل، يُرجى تصدير قاعدة بيانات الدردشة وإعادة تثبيت التطبيق.", "comparison-section-list-point-4a": "مُرحلات SimpleX لا يمكنها أن تتنازل عن تعمية بين الطرفين. تحقق من رمز الأمان للتخفيف من الهجوم على القناة خارج النطاق", diff --git a/website/langs/bg.json b/website/langs/bg.json index b70a6882e4..963f850e53 100644 --- a/website/langs/bg.json +++ b/website/langs/bg.json @@ -21,7 +21,7 @@ "smp-protocol": "СМП Протокол", "chat-protocol": "Чат протокол", "donate": "Дарете", - "copyright-label": "© 2020-2023 SimpleX | Проект с отворен код", + "copyright-label": "© 2020-2024 SimpleX | Проект с отворен код", "simplex-chat-protocol": "SimpleX Чат протокол", "terminal-cli": "Системна конзола", "terms-and-privacy-policy": "Условия и политика за поверителност", diff --git a/website/langs/cs.json b/website/langs/cs.json index b8777eea38..8429b5d8ad 100644 --- a/website/langs/cs.json +++ b/website/langs/cs.json @@ -25,10 +25,10 @@ "smp-protocol": "SMP protokol", "chat-protocol": "Chat protokol", "donate": "Darovat", - "copyright-label": "© 2020-2023 SimpleX | Projekt s otevřeným zdrojovým kódem", + "copyright-label": "© 2020-2024 SimpleX | Projekt s otevřeným zdrojovým kódem", "simplex-chat-protocol": "SimpleX Chat protokol", "terminal-cli": "Terminálové rozhraní příkazového řádku", - "terms-and-privacy-policy": "Podmínky a zásady ochrany osobních údajů", + "terms-and-privacy-policy": "Ochrana soukromí", "hero-header": "Nová definice ochrany osobních údajů", "hero-subheader": "První messenger
bez uživatelských ID", "hero-overlay-1-textlink": "Proč jsou uživatelská ID špatná pro soukromí?", @@ -40,7 +40,7 @@ "feature-3-title": "E2E-šifrované decentralizované skupiny — pouze uživatelé vědí, že existují", "feature-4-title": "Hlasové zprávy šifrované E2E", "feature-6-title": "E2E šifrované
hlasové a videohovory", - "feature-7-title": "Přenosné šifrované ůložiště aplikace — přeneste profil do jiného zařízení", + "feature-7-title": "Přenosné šifrované úložiště aplikace — přesuňte profil do jiného zařízení", "feature-8-title": "režim Inkognito —
Unikátní pro SimpleX Chat", "simplex-network-overlay-1-title": "Srovnání s protokoly zpráv P2P", "simplex-private-2-title": "Další vrstva
šifrování serveru", @@ -248,10 +248,12 @@ "signing-key-fingerprint": "Otisk podpisového klíče (SHA-256)", "f-droid-org-repo": "F-Droid.org repozitář", "stable-versions-built-by-f-droid-org": "Stabilní verze vytvořené F-Droid.org", - "releases-to-this-repo-are-done-1-2-days-later": "Vydání v tomto repozitáři se provádí o 1-2 dny později", + "releases-to-this-repo-are-done-1-2-days-later": "Vydání v tomto repozitáři se provádí o několik dní později", "jobs": "Připojit k týmu", "hero-overlay-card-3-p-2": "Trail of Bits přezkoumala kryptografii a síťové komponenty SimpleX platformy v listopadu 2022.", "hero-overlay-card-3-p-3": "Přečtěte si více v ohlášení.", "docs-dropdown-9": "Ke stažení", - "docs-dropdown-10": "Transparentnost" + "docs-dropdown-10": "Transparentnost", + "docs-dropdown-11": "FAQ (často kladené dotazy)", + "docs-dropdown-12": "Bezpečnost" } diff --git a/website/langs/de.json b/website/langs/de.json index c6bcc9920f..c8d58d0343 100644 --- a/website/langs/de.json +++ b/website/langs/de.json @@ -18,62 +18,62 @@ "simplex-explained-tab-2-p-2": "Die Server leiten Nachrichten immer nur in eine Richtung weiter, ohne den vollständigen Verlauf der Nutzer-Unterhaltung oder seiner Verbindungen zu kennen.", "simplex-explained-tab-3-p-1": "Die Server nutzen für jede Warteschlange separate, anonyme Anmeldeinformationen und wissen nicht welchem Nutzer diese gehören.", "simplex-explained-tab-3-p-2": "Durch die Verwendung von Tor-Zugangsservern können Nutzer ihre Metadaten-Privatsphäre weiter verbessern und Korellationen von IP-Adressen verhindern.", - "smp-protocol": "SMP Protokoll", + "smp-protocol": "SMP-Protokoll", "chat-bot-example": "Beispiel für einen Chatbot", "donate": "Spenden", - "copyright-label": "© 2020-2023 SimpleX | Open-Source Projekt", - "chat-protocol": "Chat Protokoll", - "simplex-chat-protocol": "SimpleX Chat Protokoll", - "terminal-cli": "Terminal Kommandozeilen-Schnittstelle", + "copyright-label": "© 2020-2024 SimpleX | Open-Source-Projekt", + "chat-protocol": "Chat-Protokoll", + "simplex-chat-protocol": "SimpleX-Chat-Protokoll", + "terminal-cli": "Terminal-Kommandozeilen-Schnittstelle", "terms-and-privacy-policy": "Datenschutzrichtlinie", "hero-header": "Privatsphäre neu definiert", "hero-overlay-2-textlink": "Wie funktioniert SimpleX?", "hero-subheader": "Der erste Messenger
ohne Nutzerkennungen", "hero-p-1": "Andere Apps haben Nutzerkennungen: Signal, Matrix, Session, Briar, Jami, Cwtch usw.
SimpleX arbeitet ohne diese, nicht einmal mit Zufallszahlen.
Dies steigert die Privatsphäre ungemein.", "hero-overlay-1-textlink": "Warum sind Nutzerkennungen schlecht für die Privatspäre?", - "hero-overlay-2-title": "Warum Benutzerkennungen schlecht für die Privatsphäre sind?", + "hero-overlay-2-title": "Warum sind Benutzerkennungen schlecht für die Privatsphäre?", "hero-2-header": "Aufbau einer privaten Verbindung", "hero-overlay-1-title": "Wie funktioniert SimpleX?", "hero-2-header-desc": "Das Video zeigt Ihnen, wie Sie sich mit einem Kontakt mit dessen Einmal-QR-Code persönlich oder per Videotreff verbinden. Sie können sich auch über einen geteilten Einladungslink miteinander verbinden.", - "feature-2-title": "Ende-zu-Ende verschlüsselte
Bilder, Videos und Dateien", - "feature-4-title": "Ende-zu-Ende verschlüsselte Sprachnachrichten", - "feature-1-title": "Ende-zu-Ende verschlüsselte Nachrichten mit Markdowns und Bearbeitungsmöglichkeiten", - "feature-3-title": "Ende-zu-Ende verschlüsselte dezentralisierte Gruppen — Nur die Nutzer wissen, dass diese überhaupt existieren", + "feature-2-title": "Ende-zu-Ende-verschlüsselte
Bilder, Videos und Dateien", + "feature-4-title": "Ende-zu-Ende-verschlüsselte Sprachnachrichten", + "feature-1-title": "Ende-zu-Ende-verschlüsselte Nachrichten mit Markdowns und Bearbeitungsmöglichkeiten", + "feature-3-title": "Ende-zu-Ende-verschlüsselte dezentralisierte Gruppen — Nur die Nutzer wissen, dass diese überhaupt existieren", "simplex-private-8-title": "Mischen von Nachrichten,
um Korrelationen zu reduzieren", "feature-5-title": "Verschwindende geheime Unterhaltungen", - "feature-6-title": "Ende-zu-Ende verschlüsselte Sprach- und Videoanrufe", + "feature-6-title": "Ende-zu-Ende-verschlüsselte Sprach- und Videoanrufe", "feature-7-title": "Portable und verschlüsselte App-Datenspeicherung — verschieben Sie das komplette Profil einfach auf ein anderes Gerät", "feature-8-title": "Inkognito-Modus —
Einzigartig in SimpleX Chat", "simplex-network-overlay-1-title": "Vergleich mit P2P Nachrichten-Protokollen", - "simplex-private-1-title": "Zwei Schichten der
Ende-zu-Ende Verschlüsselung", + "simplex-private-1-title": "Zwei Schichten der
Ende-zu-Ende-Verschlüsselung", "simplex-private-2-title": "Zusätzliche Schicht der
Server-Verschlüsselung", "simplex-private-3-title": "Sichere authentifizierte
TLS-Transportschicht", "simplex-private-4-title": "Optionaler
Zugang per Tor", "simplex-private-5-title": "Mehrere Schichten der
Inhalteauffüllung", - "simplex-private-7-title": "Nachrichten-Integrität
Überprüfung", - "simplex-private-6-title": "Out-of-Band
Schlüsselaustausch", + "simplex-private-7-title": "Nachrichten-Integritäts-
Überprüfung", + "simplex-private-6-title": "Out-of-Band-
Schlüsselaustausch", "simplex-private-9-title": "Unidirektionale
Nachrichten-Warteschlangen", "simplex-private-10-title": "Temporäre, anonyme paarweise Kennungen", - "simplex-private-card-1-point-1": "Doppeltes-Ratchet Protokoll —
Off-the-Record Nachrichten mit Perfect Forward Secrecy und Einbruchserholung.", - "simplex-private-card-1-point-2": "NaCL Kryptobox in jeder Warteschlange, um eine Korrelation des Datenverkehrs zwischen Nachrichtenwarteschlangen zu verhindern, falls TLS kompromittiert wurde.", + "simplex-private-card-1-point-1": "Double-Ratchet-Protokoll —
Off-the-Record-Nachrichten mit Perfect Forward Secrecy und Einbruchsresistenz.", + "simplex-private-card-1-point-2": "NaCL-Kryptobox in jeder Warteschlange, um eine Korrelation des Datenverkehrs zwischen Nachrichtenwarteschlangen zu verhindern, falls TLS kompromittiert wurde.", "simplex-private-card-3-point-1": "Für Client-Server-Verbindungen wird nur TLS 1.2/1.3 mit starken Algorithmen verwendet.", "simplex-private-card-2-point-1": "Zusätzliche Server-Verschlüsselungs-Schicht für die Zustellung an den Empfänger, um eine Korrelation zwischen empfangenen und gesendeten Server-Daten zu vermeiden, falls TLS kompromittiert wurde.", "simplex-private-card-3-point-2": "Server-Fingerabdrücke und Kanalbindung verhindern MITM- und Replay-Angriffe.", "simplex-private-card-3-point-3": "Die Wiederaufnahme von Verbindungen ist deaktiviert, um Sitzungs-Angriffe zu verhindern.", "simplex-private-card-4-point-1": "Um Ihre IP-Adresse zu schützen, können Sie per Tor oder irgendeinem anderen Transportschichten-Netzwerk auf Server zugreifen.", - "simplex-private-card-4-point-2": "Um SimpleX per Tor zu nutzen, installieren Sie unter Android bitte die Orbot App und aktivieren Sie den SOCKS5 Proxy oder unter iOS per VPN.", + "simplex-private-card-4-point-2": "Um SimpleX per Tor zu nutzen, installieren Sie unter Android bitte die Orbot-App und aktivieren Sie den SOCKS5-Proxy oder unter iOS per VPN.", "simplex-private-card-5-point-1": "SimpleX nutzt Inhalte-Auffüllung für jede Verschlüsselungs-Schicht, um Angriffe auf die Nachrichtengröße zu vereiteln.", "simplex-private-card-5-point-2": "Erzeugt Nachrichten mit unterschiedlichen Größen, die für Server und Netzwerk-Beobachter identisch aussehen.", - "simplex-private-card-6-point-1": "Viele Kommunikations-Plattformen sind für MITM-Angriffe durch Server oder Netzwerk-Provider anfällig.", + "simplex-private-card-6-point-1": "Viele Kommunikations-Plattformen sind für MITM-Angriffe durch Server oder Netzwerk-Anbieter anfällig.", "simplex-private-card-9-point-1": "Jede Nachrichten-Warteschlange leitet Nachrichten mit unterschiedlichen Sende- und Empfängeradressen jeweils nur in einer Richtung weiter.", - "simplex-private-card-9-point-2": "Verglichen mit traditionellen Nachrichten-Brokern, werden mögliche Angriffs-Vektoren und vorhandene Meta-Daten reduziert.", + "simplex-private-card-9-point-2": "Verglichen mit traditionellen Nachrichten-Brokern, werden mögliche Angriffs-Vektoren und vorhandene Metadaten reduziert.", "simplex-private-card-10-point-1": "SimpleX nutzt für jeden Nutzer-Kontakt oder jedes Gruppenmitglied eigene temporäre, anonyme und paarweise Adressen und Berechtigungsnachweise.", "simplex-private-card-10-point-2": "SimpleX erlaubt es, Nachrichten ohne Nutzerprofil-Bezeichner zu versenden und bietet dabei bessere Metadaten-Privatsphäre an als andere Alternativen.", - "simplex-unique-4-title": "Sie besitzen das SimpleX Netzwerk", + "simplex-unique-4-title": "Sie besitzen das SimpleX-Netzwerk", "privacy-matters-1-title": "Werbung und Preisdiskriminierung", - "privacy-matters-1-overlay-1-title": "Privatsphäre sichert Ihnen Geld", - "privacy-matters-2-overlay-1-title": "Privatsphäre gibt Ihnen Kraft", - "privacy-matters-1-overlay-1-linkText": "Privatsphäre sichert Ihnen Geld", + "privacy-matters-1-overlay-1-title": "Privatsphäre spart Ihnen Geld", + "privacy-matters-2-overlay-1-title": "Privatsphäre gibt Ihnen Macht", + "privacy-matters-1-overlay-1-linkText": "Privatsphäre spart Ihnen Geld", "privacy-matters-2-overlay-1-linkText": "Privatsphäre gibt Ihnen Kraft", "privacy-matters-3-title": "Strafverfolgung wegen einer harmlosen Verbindung", "privacy-matters-3-overlay-1-title": "Privatsphäre schützt Ihre Freiheit", @@ -81,27 +81,27 @@ "privacy-matters-3-overlay-1-linkText": "Privatsphäre schützt Ihre Freiheit", "simplex-unique-1-title": "Sie erhalten eine vollumfängliche Privatsphäre", "simplex-unique-1-overlay-1-title": "Komplette Privatsphäre für Ihre Identität, ihr Profil, Ihre Kontakte und Metadaten", - "simplex-unique-2-title": "Sie sind geschützt vor
SPAM und Missbrauch", - "simplex-unique-2-overlay-1-title": "Der beste Schutz vor SPAM und Missbrauch", + "simplex-unique-2-title": "Sie sind geschützt vor
Spam und Missbrauch", + "simplex-unique-2-overlay-1-title": "Der beste Schutz vor Spam und Missbrauch", "simplex-unique-3-title": "Sie haben die Kontrolle über Ihre Daten", "simplex-unique-3-overlay-1-title": "Besitz, Kontrolle und Sicherheit Ihrer Daten", - "simplex-unique-4-overlay-1-title": "Voll dezentralisiert — Die Nutzer besitzen das SimpleX Netzwerk", - "hero-overlay-card-1-p-1": "Viele Nutzer fragen:Woher weiß SimpleX, an wenn es Nachrichten versenden muss, wenn es keine Benutzerkennungen gibt?", + "simplex-unique-4-overlay-1-title": "Voll dezentralisiert — Die Nutzer besitzen das SimpleX-Netzwerk", + "hero-overlay-card-1-p-1": "Viele Nutzer fragen:Woher weiß SimpleX, an wen es Nachrichten versenden muss, wenn es keine Benutzerkennungen gibt?", "simplex-private-card-7-point-1": "Um die Integrität von Nachrichten sicherzustellen, werden sie sequentiell durchnummeriert und beinhalten den Hash der vorhergehenden Nachricht.", "simplex-private-card-7-point-2": "Der Empfänger wird alarmiert, sobald eine Nachricht ergänzt, entfernt oder verändert wird.", - "simplex-private-card-8-point-1": "Die SimpleX Server arbeiten als Mix-Knoten mit geringer Verzögerung — eingehende und ausgehende Nachrichten haben eine unterschiedliche Reihenfolge.", + "simplex-private-card-8-point-1": "Die SimpleX-Server arbeiten als Mix-Knoten mit geringer Verzögerung — eingehende und ausgehende Nachrichten haben eine unterschiedliche Reihenfolge.", "hero-overlay-card-1-p-3": "Sie definieren, welche(n) Server Sie für den Empfang von Nachrichten nutzen. Ihre Kontakte — nutzen diese Server, um ihnen Nachrichten darüber zu senden. Jede Konversation nutzt üblicherweise also zwei unterschiedliche Server.", - "hero-overlay-card-1-p-5": "Die Benutzer-Profile, Kontakte und Gruppen werden nur auf den Endgerät des Nutzers gespeichert. Die Nachrichten werden mit einer 2-Schichten Ende-zu-Ende Verschlüsselung versendet.", - "hero-overlay-card-1-p-6": "Lesen Sie mehr darüber im SimpleX Whitepaper.", + "hero-overlay-card-1-p-5": "Die Benutzer-Profile, Kontakte und Gruppen werden nur auf den Endgerät des Nutzers gespeichert. Die Nachrichten werden mit einer 2-Schichten-Ende-zu-Ende-Verschlüsselung versendet.", + "hero-overlay-card-1-p-6": "Lesen Sie mehr darüber im SimpleX-Whitepaper.", "hero-overlay-card-2-p-2": "Sie können diese Informationen mit bestehenden öffentlichen sozialen Netzwerken korrelieren und damit wahre Identitäten herausfinden.", - "hero-overlay-card-2-p-3": "Wenn Sie sich mit zwei unterschiedlichen Kontakten über das selbe Profil unterhalten, können sie, selbst bei sehr auf Privatsphäre achtenden Apps, die Tor v3 Dienste nutzen, feststellen, dass diese Kontakte mit der selben Person verbunden sind.", - "hero-overlay-card-1-p-2": "Um Nachrichten auszuliefern, nutzt SimpleX statt Benutzerkennungen wie auf allen anderen Plattformen, temporäre, anonyme und paarweise Kennungen für Nachrichten-Warteschlangen, die für jede Ihrer Verbindungen unterschiedlich sind — Es gibt keinerlei Langzeit-Kennungen.", + "hero-overlay-card-2-p-3": "Wenn Sie sich mit zwei unterschiedlichen Kontakten über dasselbe Profil unterhalten, können sie, selbst bei sehr auf Privatsphäre bedachten Apps, die Tor-v3-Dienste nutzen, feststellen, dass diese Kontakte mit derselben Person verbunden sind.", + "hero-overlay-card-1-p-2": "Um Nachrichten auszuliefern, nutzt SimpleX statt Benutzerkennungen wie auf allen anderen Plattformen temporäre, anonyme und paarweise Kennungen für Nachrichten-Warteschlangen, die für jede Ihrer Verbindungen unterschiedlich sind — Es gibt keinerlei Langzeit-Kennungen.", "hero-overlay-card-1-p-4": "Dieses Design verhindert schon auf der Applikations-Ebene Datenlecks für jegliche Benutzer'Metadaten. Sie können sich über Tor mit Nachrichten-Servern verbinden, um Ihre Privatsphäre weiter zu verbessern und die von Ihnen genutzte IP-Adresse zu schützen.", "hero-overlay-card-2-p-1": "Wenn Nutzer dauerhafte Identitäten besitzen, selbst wenn diese eine Zufallsnummer, wie eine Sitzungs-ID, ist, besteht ein Risiko, das Provider oder Angreifer feststellen können, wie Nutzer miteinander verbunden sind und wie viele Nachrichten sie versenden.", "hero-overlay-card-2-p-4": "SimpleX schützt gegen solche Angriffe, weil es vom Design her keinerlei Benutzerkennungen besitzt. Und Sie haben sogar unterschiedliche Anzeigenamen für jeden Kontakt und vermeiden jegliche geteilte Daten zwischen diesen, wenn Sie den Inkognito-Modus nutzen.", "simplex-network-overlay-card-1-p-1": "Peer-to-Peer-Nachrichten-Protokolle und -Applikationen haben verschiedene Probleme, die diese weniger vertrauenswürdig, die Analyse wesentlich komplexer und anfälliger gegen verschiedene Arten von Angriffen, als bei SimpleX machen.", "simplex-network-overlay-card-1-li-2": "Das SimpleX Design hat, im Gegensatz zu den meisten P2P-Netzwerken, keinerlei globalen Benutzerkennungen, auch keine temporären. Es nutzt ausschließlich temporäre paarweise Kennungen, die bessere Anonymität und Metadaten-Schutz bieten.", - "simplex-network-overlay-card-1-li-4": "P2P-Implementierungen können durch Internet-Provider blockiert werden, wie beispielweise BitTorrent). SimpleX ist transportunabhängig - es kann über Standard-Web-Protokolle, wie beispielsweise WebSockets, arbeiten.", + "simplex-network-overlay-card-1-li-4": "P2P-Implementierungen können durch Internetanbieter blockiert werden, wie beispielweise BitTorrent). SimpleX ist transportunabhängig – es kann über Standard-Web-Protokolle, wie beispielsweise WebSockets, arbeiten.", "simplex-network-overlay-card-1-li-6": "P2P-Netzwerke können anfällig für DRDoS-Angriffe sein, wenn die Clients den Datenverkehr erneut senden und verstärken können, was zu einem netzwerkweiten Denial-of-Service führt. SimpleX-Clients leiten nur Datenverkehr von bekannten Verbindungen weiter und können von einem Angreifer nicht dazu verwendet werden, den Datenverkehr im gesamten Netzwerk zu verstärken.", "privacy-matters-overlay-card-1-p-1": "Viele große Unternehmen nutzen Informationen, mit wem Sie in Verbindung stehen, um Ihr Einkommen zu schätzen, Ihnen Produkte zu verkaufen, die Sie nicht wirklich benötigen und um die Preise zu bestimmen.", "privacy-matters-overlay-card-1-p-2": "Online-Händler wissen, dass Menschen mit geringerem Einkommen eher dringende Einkäufe tätigen, sodass sie möglicherweise höhere Preise verlangen können oder Rabatte streichen.", @@ -131,28 +131,28 @@ "learn-more": "Erfahren Sie mehr darüber", "more-info": "Weitere Informationen", "hide-info": "Informationen verbergen", - "contact-hero-subheader": "Scannen Sie den QR-Code mit der SimpleX Chat-Applikation auf Ihrem Mobilgerät oder Tablet.", - "contact-hero-p-2": "Bisher SimpleX Chat noch nicht herunter geladen?", + "contact-hero-subheader": "Scannen Sie den QR-Code mit der SimpleX-Chat-Applikation auf Ihrem Mobilgerät oder Tablet.", + "contact-hero-p-2": "SimpleX Chat noch nicht heruntergeladen?", "scan-qr-code-from-mobile-app": "Scannen Sie den QR-Code aus der mobilen Applikation", - "install-simplex-app": "Installieren Sie die SimpleX Applikation", + "install-simplex-app": "Installieren Sie die SimpleX-App", "connect-in-app": "Verbinden Sie sich in der Applikation", - "open-simplex-app": "Öffnen Sie die SimpleX Applikation", + "open-simplex-app": "Öffnen Sie die SimpleX-App", "scan-the-qr-code-with-the-simplex-chat-app-description": "Die öffentlichen Schlüssel und die Adresse der Nachrichtenwarteschlange in diesem Link werden NICHT über das Netzwerk gesendet, wenn Sie diese Seite aufrufen —
sie sind in dem Hash-Fragment der Link-URL enthalten.", "installing-simplex-chat-to-terminal": "Installation von SimpleX-Chat im Terminal", "use-this-command": "Benutzen Sie dieses Kommando:", "see-simplex-chat": "Siehe SimpleX-Chat", - "github-repository": "GitHub Repository", - "the-instructions--source-code": "Die Anleitung, wie Sie es herunter laden und aus dem Quellcode kompilieren.", + "github-repository": "GitHub-Repository", + "the-instructions--source-code": "Die Anleitung, wie Sie es herunterladen und aus dem Quellcode kompilieren.", "if-you-already-installed": "Wenn Sie es schon installiert haben", "simplex-chat-for-the-terminal": "SimpleX Chat für das Terminal", "privacy-matters-section-header": "Warum es auf Privatsphäre ankommt", "privacy-matters-section-label": "Stellen Sie sicher, dass Ihr Messenger keinen Zugriff auf Ihre Daten hat!", "tap-to-close": "Zum Schließen drücken", - "simplex-network-section-header": "SimpleX Netzwerk", + "simplex-network-section-header": "SimpleX-Netzwerk", "simplex-network-1-header": "Im Gegensatz zu P2P-Netzwerken", "simplex-network-1-desc": "Alle Nachrichten werden über Server versandt, was sowohl einen besseren Schutz der Metadaten als auch eine zuverlässigere asynchrone Nachrichtenübermittlung ermöglicht und dabei Vieles vermeidet", "simplex-network-1-overlay-linktext": "Probleme von P2P-Netzwerken", - "simplex-network-2-desc": "Simple X-Relay-Server speichern KEINE Benutzerprofile, Kontakte und zugestellte Nachrichten und stellen KEINE Verbindungen untereinander her und es gibt KEIN Serververzeichnis.", + "simplex-network-2-desc": "SimpleX-Relay-Server speichern KEINE Benutzerprofile, Kontakte und zugestellte Nachrichten und stellen KEINE Verbindungen untereinander her, und es gibt KEIN Serververzeichnis.", "simplex-network-3-header": "SimpleX-Netzwerk", "comparison-section-header": "Vergleich mit anderen Protokollen", "protocol-1-text": "Signal, große Plattformen", @@ -164,28 +164,28 @@ "comparison-point-4-text": "Einzelnes oder zentralisiertes Netzwerk", "yes": "Ja", "no": "Nein", - "no-private": "Nein - vertraulich", - "no-secure": "Nein - sicher", - "no-resilient": "Nein - widerstandsfähig", - "no-federated": "Nein - föderiert", + "no-private": "Nein – vertraulich", + "no-secure": "Nein – sicher", + "no-resilient": "Nein – widerstandsfähig", + "no-federated": "Nein – föderiert", "comparison-section-list-point-2": "DNS-basierte Adressen", "comparison-section-list-point-3": "Öffentlicher Schlüssel oder eine andere weltweit eindeutige ID", - "comparison-section-list-point-4": "Wenn die Server des Betreibers kompromittiert werden. In Signal und weiteren Apps kann der Securitycode überprüft werden, um dies zu entschärfen", + "comparison-section-list-point-4": "Wenn die Server des Betreibers kompromittiert werden. In Signal und weiteren Apps kann der Sicherheitscode überprüft werden, um dies zu entschärfen", "comparison-section-list-point-6": "P2P sind zwar verteilt, aber nicht föderiert - sie arbeiten als ein einziges Netzwerk", "comparison-section-list-point-7": "P2P-Netzwerke haben entweder eine zentrale Verwaltung oder das gesamte Netzwerk kann kompromittiert werden", "see-here": "Siehe hier", "simplex-unique-card-4-p-1": "Das SimpleX-Netzwerk ist vollständig dezentralisiert und unabhängig von Kryptowährungen oder anderen Plattformen außer dem Internet.", "unique": "einmalig", "privacy-matters-overlay-card-2-p-1": "Vor nicht allzu langer Zeit beobachteten wir, wie große Wahlen von einem angesehenen Beratungsunternehmen manipuliert wurden, welches unsere sozialen Graphen nutzte, um unsere Sicht auf die reale Welt zu verzerren und unsere Stimmen zu manipulieren.", - "privacy-matters-overlay-card-3-p-2": "Eine der schockierendsten Geschichten ist die Erfahrung von Mohamedou Ould Salahi, welche in seinen Memoiren beschrieben und im Film \"The Mauritanian\" gezeigt wird. Er kam nach einem Anruf bei seinen Verwandten in Afghanistan und ohne Gerichtsverfahren in das Guantanamo-Lager und wurde dort einige Jahre lang gefoltert, weil er verdächtigt wurde, an den 9/11-Angriffen beteiligt gewesen zu sein, obwohl er die vorhergehenden 10 Jahre in Deutschland gelebt hatte.", + "privacy-matters-overlay-card-3-p-2": "Eine der schockierendsten Geschichten ist die Erfahrung von Mohamedou Ould Salahi, welche in seinen Memoiren beschrieben und im Film „The Mauritanian“ gezeigt wird. Er kam nach einem Anruf bei seinen Verwandten in Afghanistan und ohne Gerichtsverfahren in das Guantanamo-Lager und wurde dort einige Jahre lang gefoltert, weil er verdächtigt wurde, an den 9/11-Angriffen beteiligt gewesen zu sein, obwohl er die vorhergehenden 10 Jahre in Deutschland gelebt hatte.", "simplex-unique-overlay-card-1-p-1": "Im Gegensatz zu anderen Nachrichten-Plattformen weist SimpleX den Benutzern keine Kennungen zu. Es verlässt sich nicht auf Telefonnummern, domänenbasierte Adressen (wie E-Mail oder XMPP), Benutzernamen, öffentliche Schlüssel oder sogar Zufallszahlen, um seine Benutzer zu identifizieren — Wir wissen nicht, wie viele Personen unsere SimpleX-Server verwenden.", "simplex-unique-overlay-card-1-p-2": "Um Nachrichten auszuliefern nutzt SimpleX paarweise anonyme Adressen aus unidirektionalen Nachrichten-Warteschlangen, die für empfangene und gesendete Nachrichten separiert sind und gewöhnlich über verschiedene Server gesendet werden. Die Nutzung von SimpleX entspricht der Nutzung von unterschiedlichen Mailservern oder Telefonen für jeden einzelnen Kontakt und vermeidet dabei eine mühsame Verwaltung.", "simplex-network-overlay-card-1-li-5": "Alle bekannten P2P-Netzwerke können anfällig für Sybil-Angriffe sein, da jeder Knoten ermittelbar ist und das Netzwerk als Ganzes funktioniert. Bekannte Maßnahmen zur Verhinderung erfordern entweder eine zentralisierte Komponente oder einen teuren Ausführungsnachweis. Das SimpleX-Netzwerk bietet keine Ermittlung der Server, ist fragmentiert und arbeitet mit mehreren isolierten Subnetzwerken, wodurch netzwerkweite Angriffe unmöglich werden.", "simplex-network-overlay-card-1-li-3": "P2P löst nicht das Problem des MITM-Angriffs und die meisten bestehenden Implementierungen nutzen für den initialen Schlüsselaustausch keine Out-of-Band-Nachrichten. Im Gegensatz hierzu nutzt SimpleX für den initialen Schlüsselaustausch Out-of-Band-Nachrichten oder zum Teil schon bestehende sichere und vertrauenswürdige Verbindungen.", - "tap-the-connect-button-in-the-app": "Drücken Sie die ‘Verbinden’ Taste in der Applikation", + "tap-the-connect-button-in-the-app": "Drücken Sie die „Verbinden“-Taste in der Applikation", "to-make-a-connection": "Um eine Verbindung zu starten:", "contact-hero-p-3": "Nutzen Sie die unten genannten Links, um die Applikation herunterzuladen.", - "scan-the-qr-code-with-the-simplex-chat-app": "Scannen Sie den QR-Code mit der SimpleX Chat-Applikation", + "scan-the-qr-code-with-the-simplex-chat-app": "Scannen Sie den QR-Code mit der SimpleX-Chat-Applikation", "copy-the-command-below-text": "Kopieren Sie sich das unten genannte Kommando und nutzen Sie es im Chat:", "privacy-matters-section-subheader": "Die Wahrung der Privatsphäre Ihrer Metadaten — mit wem Sie wann Kontakt haben — schützt Sie vor:", "simplex-private-section-header": "Was macht SimpleX vertraulich", @@ -193,17 +193,17 @@ "simplex-network-2-header": "Im Gegensatz zu föderierten Netzwerken", "comparison-section-list-point-1": "Normalerweise auf der Grundlage einer Telefonnummer, in einigen Fällen auf der Grundlage von Benutzernamen", "comparison-point-5-text": "Zentrale Komponente oder andere Netzwerk-weite Angriffe", - "no-decentralized": "Nein - dezentralisiert", + "no-decentralized": "Nein – dezentralisiert", "comparison-section-list-point-5": "Die Privatsphäre-Metadaten des Nutzers werden nicht geschützt", - "simplex-network-overlay-card-1-li-1": "P2P-Netzwerke vertrauen auf Varianten von DHT, um Nachrichten zu routen. DHT-Designs müssen zwischen Zustellungsgarantie und Latenz ausgleichen. Verglichen mit P2P bietet SimpleX sowohl eine bessere Zustellungsgarantie, als auch eine niedrigere Latenz, weil eine Nachricht redundant und parallel über mehrere Server gesendet werden kann, wobei die durch den Empfänger ausgewählten Server genutzt werden. In P2P-Netzwerken werden Nachrichten sequentiell über O(log N) Knoten gesendet, wobei die Knoten durch einen Algorithmus ausgewählt werden.", + "simplex-network-overlay-card-1-li-1": "P2P-Netzwerke vertrauen auf Varianten von DHT, um Nachrichten zu routen. DHT-Designs müssen zwischen Zustellungsgarantie und Latenz ausgleichen. Verglichen mit P2P bietet SimpleX sowohl eine bessere Zustellungsgarantie als auch eine niedrigere Latenz, weil eine Nachricht redundant und parallel über mehrere Server gesendet werden kann, wobei die durch den Empfänger ausgewählten Server genutzt werden. In P2P-Netzwerken werden Nachrichten sequentiell über O(log N)-Knoten gesendet, wobei die Knoten durch einen Algorithmus ausgewählt werden.", "simplex-unique-overlay-card-3-p-4": "Zwischen dem gesendeten und empfangenen Serververkehr gibt es keine gemeinsamen Kennungen oder Chiffriertexte — sodass ein Beobachter nicht ohne weiteres feststellen kann, wer mit wem kommuniziert, selbst wenn TLS kompromittiert wurde.", "simplex-unique-overlay-card-4-p-3": "Wenn Sie darüber nachdenken, für die SimpleX-Plattform entwickeln zu wollen, z.B. einen Chatbot für SimpleX-App-Nutzer oder die Integration der SimpleX-Chat-Bibliothek in Ihre mobilen Apps, kontaktieren Sie uns bitte für eine weitere Beratung und Unterstützung.", "privacy-matters-overlay-card-1-p-4": "Die SimpleX-Plattform schützt die Privatsphäre Ihrer Verbindungen besser als jede Alternative und verhindert vollständig, dass Ihr sozialer Graph für Unternehmen oder Organisationen verfügbar wird. Selbst wenn die Anwender SimpleX-Chat-Server verwenden, kennen wir die Anzahl der Benutzer oder ihre Verbindungen nicht.", "contact-hero-header": "Sie haben eine Adresse zur Verbindung mit SimpleX Chat erhalten", "invitation-hero-header": "Sie haben einen Einmal-Link zur Verbindung mit SimpleX Chat erhalten", - "privacy-matters-overlay-card-3-p-3": "Normale Menschen werden für das, was sie online teilen, sogar unter Nutzung ihrer \"anonymen\" Konten, selbst in demokratischen Ländern verhaftet.", + "privacy-matters-overlay-card-3-p-3": "Normale Menschen werden für das, was sie online teilen, sogar unter Nutzung ihrer „anonymen“ Konten, selbst in demokratischen Ländern verhaftet.", "simplex-unique-overlay-card-3-p-1": "SimpleX Chat speichert alle Benutzerdaten ausschließlich auf den Endgeräten in einem portablen und verschlüsselten Datenbankformat, welches exportiert und auf jedes unterstützte Gerät übertragen werden kann.", - "simplex-unique-overlay-card-2-p-2": "Auch wenn die optionale Benutzeradresse zum Versenden von SPAM-Kontaktanfragen verwendet werden kann, können Sie sie ändern oder ganz löschen, ohne dass Ihre Verbindungen verloren gehen.", + "simplex-unique-overlay-card-2-p-2": "Auch wenn die optionale Benutzeradresse zum Versenden von Spam-Kontaktanfragen verwendet werden kann, können Sie sie ändern oder ganz löschen, ohne dass Ihre Verbindungen verloren gehen.", "simplex-unique-overlay-card-4-p-2": "Die SimpleX-Plattform verwendet ein offenes Protokoll und bietet ein SDK zur Erstellung von Chatbots an, welches die Erstellung von Diensten ermöglicht, mit denen Nutzer über SimpleX-Chat interagieren können — Wir sind gespannt, welche SimpleX-Dienste Sie erstellen können.", "simplex-unique-card-4-p-2": "Sie können SimpleX mit Ihren eigenen Servern oder mit den von uns zur Verfügung gestellten Servern verwenden — und sich trotzdem mit jedem Benutzer verbinden.", "why-simplex-is": "Warum ist SimpleX", @@ -234,15 +234,15 @@ "docs-dropdown-7": "SimpleX Chat übersetzen", "glossary": "Glossar", "signing-key-fingerprint": "Fingerabdruck des Signaturschlüssels (SHA-256)", - "f-droid-org-repo": "F-Droid.org Repository", + "f-droid-org-repo": "F-Droid.org-Repository", "stable-versions-built-by-f-droid-org": "Von F-Droid.org erstellte stabile Versionen", - "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat- und F-Droid.org-Repositorys signieren ihre Builds mit verschiedenen Schlüsseln. Zum Umschalten bitte die Chat-Datenbank exportieren und die App neu installieren.", - "releases-to-this-repo-are-done-1-2-days-later": "Die Versionen für dieses Repository werden 1..2 Tage später erstellt", - "docs-dropdown-8": "SimpleX Verzeichnisdienst", + "f-droid-page-f-droid-org-repo-section-text": "SimpleX-Chat- und F-Droid.org-Repositorys signieren ihre Builds mit verschiedenen Schlüsseln. Zum Umschalten bitte die Chat-Datenbank exportieren und die App neu installieren.", + "releases-to-this-repo-are-done-1-2-days-later": "Die Versionen für dieses Repository werden einige Tage später erstellt", + "docs-dropdown-8": "SimpleX-Verzeichnisdienst", "simplex-chat-via-f-droid": "SimpleX Chat per F-Droid", - "simplex-chat-repo": "SimpleX Chat Repository", + "simplex-chat-repo": "SimpleX-Chat-Repository", "stable-and-beta-versions-built-by-developers": "Von den Entwicklern erstellte stabile und Beta-Versionen", - "f-droid-page-simplex-chat-repo-section-text": "Um es Ihrem F-Droid-Client hinzuzufügen scannen Sie den QR-Code oder nutzen Sie diese URL:", + "f-droid-page-simplex-chat-repo-section-text": "Um es Ihrem F-Droid-Client hinzuzufügen, scannen Sie den QR-Code oder nutzen Sie diese URL:", "comparison-section-list-point-4a": "SimpleX-Relais können die E2E-Verschlüsselung nicht kompromittieren. Überprüfen Sie den Sicherheitscode, um einen möglichen Angriff auf den Out-of-Band-Kanal zu entschärfen", "hero-overlay-3-title": "Sicherheits-Gutachten", "hero-overlay-card-3-p-2": "Trail of Bits untersuchte im November 2022 die kryptografischen und Netzwerk-Komponenten der SimpleX-Plattform.", diff --git a/website/langs/en.json b/website/langs/en.json index e606fb9bf2..89bd17f5d4 100644 --- a/website/langs/en.json +++ b/website/langs/en.json @@ -21,7 +21,7 @@ "smp-protocol": "SMP protocol", "chat-protocol": "Chat protocol", "donate": "Donate", - "copyright-label": "© 2020-2023 SimpleX | Open-Source Project", + "copyright-label": "© 2020-2024 SimpleX | Open-Source Project", "simplex-chat-protocol": "SimpleX Chat protocol", "terminal-cli": "Terminal CLI", "terms-and-privacy-policy": "Privacy Policy", @@ -251,7 +251,7 @@ "signing-key-fingerprint": "Signing key fingerprint (SHA-256)", "f-droid-org-repo": "F-Droid.org repo", "stable-versions-built-by-f-droid-org": "Stable versions built by F-Droid.org", - "releases-to-this-repo-are-done-1-2-days-later": "The releases to this repo are done 1-2 days later", + "releases-to-this-repo-are-done-1-2-days-later": "The releases to this repo are done several days later", "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat and F-Droid.org repositories sign builds with the different keys. To switch, please export the chat database and re-install the app.", "jobs": "Join team", "please-enable-javascript": "Please enable JavaScript to see the QR code.", diff --git a/website/langs/es.json b/website/langs/es.json index d8f9c29c26..443f04b2ac 100644 --- a/website/langs/es.json +++ b/website/langs/es.json @@ -10,9 +10,9 @@ "simplex-explained-tab-3-p-2": "El usuario puede mejorar aún más la privacidad de sus metadatos haciendo uso de la red Tor para acceder a los servidores, evitando así la correlación por dirección IP.", "smp-protocol": "Protocolo SMP", "donate": "Donación", - "copyright-label": "© 2020-2023 SimpleX | Proyecto de Código Abierto", + "copyright-label": "© 2020-2024 SimpleX | Proyecto de Código Abierto", "simplex-chat-protocol": "Protocolo de SimpleX Chat", - "terms-and-privacy-policy": "Términos y Política de Privacidad", + "terms-and-privacy-policy": "Política de Privacidad", "hero-header": "Privacidad redefinida", "hero-overlay-1-textlink": "¿Por qué los ID de usuario son perjudiciales para la privacidad?", "hero-overlay-2-textlink": "¿Cómo funciona SimpleX?", @@ -242,7 +242,7 @@ "stable-versions-built-by-f-droid-org": "Versión estable compilada por F-Droid.org", "f-droid-page-f-droid-org-repo-section-text": "Los repositorios de SimpleX Chat y F-Droid.org firman con distinto certificado. Para cambiar, por favor exportar la base de datos y reinstala la aplicación.", "signing-key-fingerprint": "Huella digital de la clave de firma (SHA-256)", - "releases-to-this-repo-are-done-1-2-days-later": "Las versiones aparecen 1-2 días más tarde en este repositorio", + "releases-to-this-repo-are-done-1-2-days-later": "Las versiones aparecen varios días más tarde en este repositorio", "comparison-section-list-point-4a": "Los servidores de retransmisión no pueden comprometer la encriptación e2e. Para evitar posibles ataques, verifique el código de seguridad mediante un canal alternativo", "hero-overlay-3-title": "Evaluación de la seguridad", "hero-overlay-card-3-p-2": "Trail of Bits revisó la criptografía y los componentes de red de la plataforma SimpleX en noviembre de 2022.", @@ -253,5 +253,7 @@ "docs-dropdown-9": "Descargas", "please-enable-javascript": "Habilita JavaScript para ver el código QR.", "please-use-link-in-mobile-app": "Usa el enlace en la apliación móvil,", - "docs-dropdown-10": "Transparencia" + "docs-dropdown-10": "Transparencia", + "docs-dropdown-11": "FAQ", + "docs-dropdown-12": "Seguridad" } diff --git a/website/langs/fr.json b/website/langs/fr.json index 7ead7882db..678753d1e6 100644 --- a/website/langs/fr.json +++ b/website/langs/fr.json @@ -21,7 +21,7 @@ "smp-protocol": "Protocole SMP", "chat-protocol": "Protocole de chat", "donate": "Faire un don", - "copyright-label": "© 2020-2023 SimpleX | Projet Open-Source", + "copyright-label": "© 2020-2024 SimpleX | Projet Open-Source", "simplex-chat-protocol": "Protocole SimpleX Chat", "terminal-cli": "Terminal CLI", "terms-and-privacy-policy": "Politique de confidentialité", @@ -234,7 +234,7 @@ "docs-dropdown-4": "Hébergement d'un serveur SMP", "on-this-page": "Sur cette page", "glossary": "Glossaire", - "releases-to-this-repo-are-done-1-2-days-later": "Les mises à jour de ce dépôt sont faites 1 à 2 jours plus tard", + "releases-to-this-repo-are-done-1-2-days-later": "Les mises à jour de ce dépôt sont faites quelques jours plus tard", "f-droid-page-f-droid-org-repo-section-text": "Les dépôts SimpleX Chat et F-Droid.org signent les builds avec des clés différentes. Pour changer, veuillez exporter la base de données des chats et réinstaller l'application.", "docs-dropdown-8": "Service de répertoire SimpleX", "simplex-chat-via-f-droid": "SimpleX Chat via F-Droid", diff --git a/website/langs/he.json b/website/langs/he.json index 9effb8474b..72411c8cd1 100644 --- a/website/langs/he.json +++ b/website/langs/he.json @@ -12,7 +12,7 @@ "simplex-explained-tab-2-text": "2. איך זה עובד", "simplex-chat-protocol": "פרוטוקול SimpleX Chat", "terminal-cli": "ממשק שורת פקודה", - "terms-and-privacy-policy": "תנאים ומדיניות פרטיות", + "terms-and-privacy-policy": "מדיניות הפרטיות", "hero-header": "פרטיות מוגדרת מחדש", "hero-subheader": "מערכת העברת ההודעות הראשונה
ללא מזהי שתמש", "hero-overlay-1-textlink": "מדוע מזהי משתמש מזיקים לפרטיות?", @@ -53,7 +53,7 @@ "smp-protocol": "פרוטוקול SMP", "chat-protocol": "פרוטוקול צ'אט", "donate": "תרומה", - "copyright-label": "© 2020-2023 SimpleX | פרויקט קוד פתוח", + "copyright-label": "© 2020-2024 SimpleX | פרויקט קוד פתוח", "hero-p-1": "לאפליקציות אחרות יש מזהי משתמש: Signal, Matrix, Session, Briar, Jami, Cwtch וכו'.
ל-SimpleX אין, אפילו לא מספרים אקראיים.
זה משפר באופן קיצוני את הפרטיות שלך.", "hero-overlay-2-title": "מדוע מזהי משתמש מזיקים לפרטיות?", "feature-6-title": "שיחות שמע ווידאו
מוצפנות מקצה לקצה", @@ -242,7 +242,7 @@ "simplex-chat-via-f-droid": "SimpleX Chat דרך F-Droid", "glossary": "מילון מונחים", "jobs": "הצטרפו לצוות", - "releases-to-this-repo-are-done-1-2-days-later": "גרסאות למאגר זה משוחררות לאחר יום או יומיים", + "releases-to-this-repo-are-done-1-2-days-later": "גרסאות למאגר זה משוחררות לאחר כמה ימים", "f-droid-org-repo": "מאגר F-Droid.org", "stable-versions-built-by-f-droid-org": "גרסאות יציבות שנבנו על ידי F-Droid.org", "back-to-top": "חזרה למעלה", @@ -252,5 +252,8 @@ "docs-dropdown-8": "שירות מדריך כתובות SimpleX", "f-droid-page-f-droid-org-repo-section-text": "מאגרי SimpleX Chat ו-F-Droid.org חותמים על גרסאות עם מפתחות שונים. כדי לעבור, אנא ייצא את מסד הנתונים של הצ'אט והתקן מחדש את האפליקציה.", "please-enable-javascript": "אנא הפעל JavaScript כדי לראות את קוד ה-QR.", - "please-use-link-in-mobile-app": "אנא השתמש בקישור באפליקציה במכשיר נייד" + "please-use-link-in-mobile-app": "אנא השתמש בקישור באפליקציה במכשיר נייד", + "docs-dropdown-10": "שקיפות", + "docs-dropdown-11": "שאלות ותשובות", + "docs-dropdown-12": "אבטחה" } diff --git a/website/langs/hu.json b/website/langs/hu.json new file mode 100644 index 0000000000..7b6b856065 --- /dev/null +++ b/website/langs/hu.json @@ -0,0 +1,259 @@ +{ + "home": "Főoldal", + "developers": "Fejlesztők", + "reference": "Referencia", + "blog": "Blog", + "features": "Funkciók", + "why-simplex": "Miért válassza a SimpleX-t", + "simplex-privacy": "SimpleX adatvédelem", + "simplex-network": "SimpleX hálózat", + "simplex-explained": "Simplex bemutatása", + "simplex-explained-tab-1-text": "1. Felhasználói élmény", + "simplex-explained-tab-2-text": "2. Hogyan működik", + "simplex-explained-tab-3-text": "3. Mit látnak a kiszolgálók", + "simplex-explained-tab-1-p-1": "Létrehozhat kapcsolatokat és csoportokat, valamint kétirányú beszélgetéseket folytathat, mint bármely más üzenetküldőben.", + "simplex-explained-tab-1-p-2": "Hogyan működhet egyirányú üzenet várakoztatással és felhasználói profil azonosítók nélkül?", + "simplex-explained-tab-2-p-1": "Minden kapcsolathoz két különböző üzenetküldési várakoztatást használ a különböző kiszolgálókon keresztül történő üzenetküldéshez és -fogadáshoz.", + "simplex-explained-tab-2-p-2": "A kiszolgálók csak egyirányú üzeneteket továbbítanak, anélkül, hogy teljes képet kapnának a felhasználók beszélgetéseiről vagy kapcsolatairól.", + "simplex-explained-tab-3-p-1": "A kiszolgálók minden egyes üzenet várakoztatáshoz külön névtelen hitelesítő adatokkal rendelkeznek, és nem tudják, hogy melyik felhasználóhoz tartoznak.", + "simplex-explained-tab-3-p-2": "A felhasználók tovább fokozhatják a metaadatok adatvédelmét, ha a Tor segítségével férnek hozzá a kiszolgálókhoz, megakadályozva az IP-cím szerinti korrelációt.", + "smp-protocol": "SMP protokoll", + "chat-protocol": "Csevegés protokoll", + "donate": "Támogatás", + "copyright-label": "© 2020-2024 SimpleX | Nyílt forráskódú projekt", + "simplex-chat-protocol": "SimpleX Chat protokoll", + "terminal-cli": "Terminál CLI", + "terms-and-privacy-policy": "Adatvédelmi irányelvek", + "hero-header": "Újradefiniált adatvédelem", + "hero-subheader": "Az első üzenetküldő
felhasználói azonosítók nélkül", + "hero-p-1": "Más alkalmazások felhasználói azonosítókkal rendelkeznek: Signal, Matrix, Session, Briar, Jami, Cwtch, stb.
A SimpleX nem, még véletlenszerű számokkal sem.
Ez radikálisan javítja az adatvédelmet.", + "hero-overlay-1-textlink": "Miért ártanak a felhasználói azonosítók az adatvédelemnek?", + "hero-overlay-2-textlink": "Hogyan működik a SimpleX?", + "hero-overlay-3-textlink": "A biztonság értékelése", + "hero-2-header": "Privát kapcsolat létrehozása", + "hero-2-header-desc": "A videó bemutatja, hogyan kapcsolódhat az ismerőséhez egy egyszer használatos QR-kód segítségével, személyesen vagy videokapcsolaton keresztül. Ugyanakkor egy meghívó megosztásával is kapcsolódhat.", + "hero-overlay-1-title": "Hogyan működik a SimpleX?", + "hero-overlay-2-title": "Miért ártanak a felhasználói azonosítók az adatvédelemnek?", + "hero-overlay-3-title": "A biztonság értékelése", + "feature-1-title": "E2E-titkosított üzenetek markdown formázással és szerkesztéssel", + "feature-2-title": "E2E-titkosított
képek, videók és fájlok", + "feature-3-title": "E2E-titkosított decentralizált csoportok — csak a felhasználók tudják, hogy ezek léteznek", + "feature-4-title": "E2E-titkosított hangüzenetek", + "feature-5-title": "Eltűnő üzenetek", + "feature-6-title": "E2E-titkosított
hang- és videohívások", + "feature-7-title": "Hordozható titkosított alkalmazás-adattárolás — profil áthelyezése egy másik eszközre", + "feature-8-title": "Az inkognitó mód —
egyedülálló a SimpleX Chat-ben", + "simplex-network-overlay-1-title": "Összehasonlítás más P2P üzenetküldő protokollokkal", + "simplex-private-1-title": "2 rétegű végpontok közötti titkosítás", + "simplex-private-2-title": "További rétege a
kiszolgáló titkosítás", + "simplex-private-4-title": "Opcionális
hozzáférés Tor-on keresztül", + "simplex-private-5-title": "Több rétegű
tartalom kitöltés", + "simplex-private-6-title": "Sávon kívüli
kulcscsere", + "simplex-private-7-title": "Üzenetintegritás
hitelesítés", + "simplex-private-8-title": "Üzenetek keverése
a korreláció csökkentése érdekében", + "simplex-private-9-title": "Egyirányú
üzenet várakoztatás", + "simplex-private-10-title": "Ideiglenes névtelen páronkénti azonosítók", + "simplex-private-card-1-point-1": "Dupla-ratchet protokoll —
OTR üzenetküldés, sérülés utáni titkosság-védelemmel és -helyreállítással.", + "simplex-private-card-1-point-2": "NaCL cryptobox minden egyes üzenet várakoztatáshoz, hogy megakadályozza a forgalom korrelációját az üzenet várakoztatások között, ha a TLS veszélybe kerül.", + "simplex-private-card-2-point-1": "Kiegészítő kiszolgáló titkosítási réteg a címzettnek történő kézbesítéshez, hogy megakadályozza a fogadott és az elküldött kiszolgálóforgalom közötti korrelációt, ha a TLS veszélybe kerül.", + "simplex-private-card-3-point-1": "Az ügyfél-kiszolgáló kapcsolatokhoz csak az erős algoritmusokkal rendelkező TLS 1.2/1.3 protokollt használ.", + "simplex-private-card-3-point-2": "A kiszolgáló ujjlenyomata és a csatornakötés megakadályozza a MITM- és a visszajátszási támadásokat.", + "simplex-private-card-3-point-3": "Az újrakapcsolódás le van tiltva a munkamenet elleni támadások megelőzése érdekében.", + "simplex-private-card-4-point-1": "Az IP-címe védelme érdekében a kiszolgálókat a Tor-on vagy más átviteli fedett hálózaton keresztül érheti el.", + "simplex-private-card-6-point-1": "Számos kommunikációs platform sebezhető a kiszolgálók vagy a hálózati szolgáltatók MITM-támadásaival szemben.", + "simplex-private-card-6-point-2": "Ennek megakadályozása érdekében a SimpleX-alkalmazások egyszeri kulcsokat adnak át sávon kívül, amikor egy címet hivatkozásként vagy QR-kódként oszt meg.", + "simplex-private-card-7-point-1": "Az integritás garantálása érdekében az üzenetek sorszámozással vannak ellátva, és tartalmazzák az előző üzenet hash-ét.", + "simplex-private-card-7-point-2": "Ha bármilyen üzenetet hozzáadnak, eltávolítanak vagy módosítanak, a címzett értesítést kap róla.", + "simplex-private-card-8-point-1": "A SimpleX-kiszolgálók alacsony késleltetésű keverési csomópontokként működnek — a bejövő és kimenő üzenetek sorrendje eltérő.", + "simplex-private-card-9-point-1": "Minden üzenetet egyetlen irányba várakoztat, a különböző küldési és vételi címekkel.", + "simplex-private-card-9-point-2": "A hagyományos üzenetküldőkhöz képest csökkenti a támadási vektorokat és a rendelkezésre álló metaadatokat.", + "simplex-private-card-10-point-1": "A SimpleX ideiglenes névtelen páros címeket és hitelesítő adatokat használ minden egyes felhasználói kapcsolat vagy csoporttag számára.", + "simplex-private-card-10-point-2": "Lehetővé teszi az üzenetek felhasználói profilazonosítók nélküli kézbesítését, ami az alternatíváknál jobb metaadat-védelmet biztosít.", + "privacy-matters-1-overlay-1-title": "Az adatvédelemmel pénzt spórol meg", + "privacy-matters-1-overlay-1-linkText": "Az adatvédelemmel pénzt spórol meg", + "privacy-matters-2-title": "A választások manipulálása", + "privacy-matters-2-overlay-1-title": "Az adatvédelem hatalmat ad", + "privacy-matters-2-overlay-1-linkText": "Az adatvédelem hatalmat ad", + "privacy-matters-3-title": "Ártatlan összefüggés miatti vádemelés", + "privacy-matters-3-overlay-1-title": "Az adatvédelem szabaddá tesz", + "privacy-matters-3-overlay-1-linkText": "Az adatvédelem szabaddá tesz", + "simplex-unique-1-title": "Teljes magánéletet élvezhet", + "simplex-unique-1-overlay-1-title": "Személyazonosságának, profiljának, kapcsolatainak és metaadatainak teljes körű védelme", + "simplex-unique-2-title": "Véd
a spamektől és a visszaélésektől", + "simplex-unique-2-overlay-1-title": "A legjobb védelem a spam és a visszaélések ellen", + "simplex-unique-3-title": "Az ön adatai fölött csak ön rendelkezik", + "simplex-unique-3-overlay-1-title": "Az ön adatai fölött csak ön rendelkezik", + "simplex-unique-4-title": "Öné a SimpleX hálózat", + "simplex-unique-4-overlay-1-title": "Teljesen decentralizált — a SimpleX hálózat a felhasználóké", + "hero-overlay-card-1-p-1": "Sok felhasználó kérdezte: ha a SimpleX-nek nincsenek felhasználói azonosítói, honnan tudja, hová kell eljuttatni az üzeneteket?", + "hero-overlay-card-1-p-2": "Az üzenetek kézbesítéséhez az összes többi platform által használt felhasználói azonosítók helyett a SimpleX az üzenetek várakoztatásához ideiglenes, névtelen, páros azonosítókat használ, külön-külön minden egyes kapcsolathoz — nincsenek hosszú távú azonosítók.", + "hero-overlay-card-1-p-4": "Ez a kialakítás megakadályozza a felhasználók metaadatainak kiszivárgását az alkalmazás szintjén. Az adatvédelem további javítása és az IP-cím védelme érdekében az üzenetküldő kiszolgálókhoz Tor hálózaton keresztül is kapcsolódhat.", + "hero-overlay-card-1-p-5": "Csak a kliensek tárolják a felhasználói profilokat, kapcsolatokat és csoportokat; az üzenetek küldése 2 rétegű végpontok közötti titkosítással történik.", + "hero-overlay-card-1-p-6": "További leírást a SimpleX ismertetőben olvashat.", + "hero-overlay-card-2-p-1": "Ha a felhasználók állandó azonosítóval rendelkeznek, még akkor is, ha ez csak egy véletlenszerű szám, például egy munkamenet-azonosító, fennáll annak a veszélye, hogy a szolgáltató vagy egy támadó megfigyelheti, hogyan kapcsolódnak a felhasználók, és hány üzenetet küldenek.", + "hero-overlay-card-2-p-2": "Ezt az információt aztán összefüggésbe hozhatják a meglévő nyilvános közösségi hálózatokkal, és meghatározhatnak néhány valódi személyazonosságot.", + "hero-overlay-card-2-p-3": "Még a Tor v3 szolgáltatásokat használó, legprivátabb alkalmazások esetében is, ha két különböző kapcsolattartóval beszél ugyanazon a profilon keresztül, bizonyítani tudják, hogy ugyanahhoz a személyhez kapcsolódnak.", + "hero-overlay-card-2-p-4": "A SimpleX úgy védekezik ezen támadások ellen, hogy nem tartalmaz felhasználói azonosítókat. Ha pedig használja az inkognitó módot, akkor minden egyes létrejött kapcsolatban más-más felhasználó név jelenik meg, így elkerülhető a közöttük lévő összefüggések bizonyítása.", + "hero-overlay-card-3-p-1": "Trail of Bits egy vezető biztonsági és technológiai tanácsadó cég, amelynek ügyfelei közé tartoznak a nagy technológiai cégek, kormányzati ügynökségek és jelentős blokklánc projektek.", + "hero-overlay-card-3-p-2": "A Trail of Bits 2022 novemberében áttekintette a SimpleX platform kriptográfiai és hálózati komponenseit.", + "simplex-network-overlay-card-1-li-1": "A P2P-hálózatok az üzenetek továbbítására a DHT valamelyik változatát használják. A DHT kialakításakor egyensúlyt kell teremteni a kézbesítési garancia és a késleltetés között. A SimpleX jobb kézbesítési garanciával és alacsonyabb késleltetéssel rendelkezik, mint a P2P, mivel az üzenet redundánsan, a címzett által kiválasztott kiszolgálók segítségével több kiszolgálón keresztül párhuzamosan továbbítható. A P2P-hálózatokban az üzenet O(log N) csomóponton halad át szekvenciálisan, az algoritmus által kiválasztott csomópontok segítségével.", + "simplex-network-overlay-card-1-li-2": "A SimpleX kialakítása a legtöbb P2P-hálózattól eltérően nem rendelkezik semmiféle globális felhasználói azonosítóval, még ideiglenesen sem, és csak ideiglenes páros azonosítókat használ, ami jobb névtelenséget és metaadatvédelmet biztosít.", + "simplex-network-overlay-card-1-li-3": "A P2P nem oldja meg a MITM-támadás problémát, és a legtöbb létező implementáció nem használ sávon kívüli üzeneteket a kezdeti kulcscseréhez. A SimpleX a kezdeti kulcscseréhez sávon kívüli üzeneteket, vagy bizonyos esetekben már meglévő biztonságos és megbízható kapcsolatokat használ.", + "simplex-network-overlay-card-1-li-5": "Minden ismert P2P-hálózat sebezhető Sybil támadással, mert minden egyes csomópont felderíthető, és a hálózat egészként működik. A támadások enyhítésére szolgáló ismert intézkedés lehet egy központi kiszolgáló (pl.: tracker), vagy egy drága tanúsítvány. A SimpleX hálózat nem ismeri fel a kiszolgálókat, töredezett és több elszigetelt alhálózatként működik, ami lehetetlenné teszi az egész hálózatra kiterjedő támadásokat.", + "simplex-network-overlay-card-1-li-6": "A P2P-hálózatok sebezhetőek lehetnek a DRDoS-támadással szemben, amikor a kliensek képesek a forgalmat újraközvetíteni és felerősíteni, ami az egész hálózatra kiterjedő szolgáltatásmegtagadást eredményez. A SimpleX kliensek csak az ismert kapcsolatból származó forgalmat továbbítják, és a támadó nem használhatja őket arra, hogy az egész hálózatban felerősítse a forgalmat.", + "privacy-matters-overlay-card-1-p-1": "Sok nagyvállalat arra használja fel az önnel kapcsolatban álló személyek adatait, hogy megbecsülje az ön jövedelmét, hogy olyan termékeket adjon el önnek, amelyekre valójában nincs is szüksége, és hogy meghatározza az árakat.", + "privacy-matters-overlay-card-1-p-2": "Az online kiskereskedők tudják, hogy az alacsonyabb jövedelműek nagyobb valószínűséggel vásárolnak azonnal, ezért magasabb árakat számíthatnak fel, vagy eltörölhetik a kedvezményeket.", + "privacy-matters-overlay-card-1-p-3": "Egyes pénzügyi és biztosítótársaságok szociális grafikonokat használnak a kamatlábak és a díjak meghatározásához. Ez gyakran arra készteti az alacsonyabb jövedelmű embereket, hogy többet fizessenek — ez az úgynevezett „szegénységi prémium”.", + "privacy-matters-overlay-card-1-p-4": "A SimpleX platform minden alternatívánál jobban védi a kapcsolatainak adatait, teljes mértékben megakadályozva, hogy a ismeretségi-hálója bármilyen vállalat vagy szervezet számára elérhetővé váljon. Még ha az emberek a SimpleX Chat által biztosított kiszolgálókat is használják, sem a felhasználók számát, sem a kapcsolataikat nem ismerjük.", + "privacy-matters-overlay-card-2-p-1": "Nem is olyan régen megfigyelhettük, hogy a nagy választásokat manipulálta egy neves tanácsadó cég, amely az ismeretségi-háló segítségével eltorzította a valós világról alkotott képünket, és manipulálta a szavazatainkat.", + "privacy-matters-overlay-card-2-p-2": "Ahhoz, hogy objektív legyen és független döntéseket tudjon hozni, az információs terét is kézben kell tartania. Ez csak akkor lehetséges, ha privát kommunikációs platformot használ, amely nem fér hozzá az ismeretségi-hálójához.", + "privacy-matters-overlay-card-2-p-3": "A SimpleX az első olyan platform, amely eleve nem rendelkezik felhasználói azonosítókkal, így jobban védi az ismeretségi-hálóját, mint bármely ismert alternatíva.", + "privacy-matters-overlay-card-3-p-1": "Mindenkinek törődnie kell a magánélet és a kommunikáció biztonságával — az ártalmatlan beszélgetések veszélybe sodorhatják, még akkor is, ha nincs semmi rejtegetnivalója.", + "privacy-matters-overlay-card-3-p-2": "Az egyik legmegdöbbentőbb a Mohamedou Ould Salahi memoárjában leírt és az „A mauritániai” c. filmben bemutatott történet. Őt bírósági tárgyalás nélkül a guantánamói táborba zárták, és ott kínozták 15 éven át, miután egy afganisztáni rokonát telefonon felhívta, akit azzal gyanúsítottak a hatóságok, hogy köze van a 9/11-es merényletekhez, holott Salahi az előző 10 évben Németországban élt.", + "privacy-matters-overlay-card-3-p-3": "Átlagos embereket letartóztatnak azért, amit online megosztanak, még „névtelen” fiókjaikon keresztül is, még demokratikus országokban is.", + "privacy-matters-overlay-card-3-p-4": "Nem elég, ha csak egy végpontok között titkosított üzenetküldőt használunk, mindannyiunknak olyan üzenetküldőket kell használnunk, amelyek védik személyes ismerőseink magánéletét — akikkel kapcsolatban állunk.", + "simplex-unique-overlay-card-1-p-1": "Más üzenetküldő platformoktól eltérően a SimpleX nem rendel azonosítókat a felhasználókhoz. Nem támaszkodik telefonszámokra, tartomány-alapú címekre (mint az e-mail, XMPP vagy a Matrix), felhasználónevekre, nyilvános kulcsokra vagy akár véletlenszerű számokra a felhasználók azonosításához — nem tudjuk, hogy hányan használják a SimpleX-kiszolgálóinkat.", + "simplex-unique-overlay-card-1-p-2": "Az üzenetek kézbesítéséhez a SimpleX az egyirányú üzenet várakoztatást használ páronkénti névtelen címekkel, külön a fogadott és külön az elküldött üzenetek számára, általában különböző kiszolgálókon keresztül. A SimpleX használata olyan, mintha minden egyes kapcsolatnak más-más “eldobható” e-mail címe vagy telefonja lenne és nem kell ezeket gondosan kezelni.", + "simplex-unique-overlay-card-1-p-3": "Ez a kialakítás megvédi annak titkosságát, hogy kivel kommunikál, elrejtve azt a SimpleX platform kiszolgálói és a megfigyelők elől. IP-címének a kiszolgálók elől való elrejtéséhez azt teheti meg, hogy Tor-on keresztül kapcsolódik a SimpleX kiszolgálókhoz.", + "simplex-unique-overlay-card-2-p-1": "Mivel ön nem rendelkezik azonosítóval a SimpleX platformon, senki sem tud kapcsolatba lépni önnel, hacsak nem oszt meg egy egyszeri vagy ideiglenes felhasználói címet, például QR-kódot vagy hivatkozást.", + "simplex-unique-overlay-card-2-p-2": "Még az opcionális felhasználói cím esetében is, bár spam kapcsolatfelvételi kérések küldésére használható, megváltoztathatja vagy teljesen törölheti azt anélkül, hogy elveszítené a meglévő kapcsolatait.", + "simplex-unique-overlay-card-3-p-1": "A SimpleX Chat az összes felhasználói adatot kizárólag a klienseken tárolja egy hordozható titkosított adatbázis-formátumban, amely exportálható és átvihető bármely más támogatott eszközre.", + "simplex-unique-overlay-card-3-p-2": "A végpontok között titkosított üzenetek átmenetileg a SimpleX átjátszó-kiszolgálókon tartózkodnak, amíg be nem érkeznek a címzetthez, majd véglegesen törlődnek onnan.", + "simplex-unique-overlay-card-3-p-3": "A föderált hálózatok kiszolgálóitól (e-mail, XMPP vagy Matrix) eltérően a SimpleX kiszolgálók nem tárolják a felhasználói fiókokat, csak továbbítják az üzeneteket, így védve mindkét fél magánéletét.", + "simplex-unique-overlay-card-3-p-4": "A küldött és a fogadott kiszolgálóforgalom között nincsenek közös azonosítók vagy titkosított szövegek — ha bárki megfigyeli, nem tudja könnyen megállapítani, hogy ki kivel kommunikál, még akkor sem, ha a TLS-t kompromittálják.", + "simplex-unique-overlay-card-4-p-1": "Használhatja a SimpleX-et saját kiszolgálóival, és továbbra is kommunikálhat azokkal, akik az általunk biztosított, előre konfigurált kiszolgálókat használják.", + "simplex-unique-overlay-card-4-p-2": "A SimpleX platform nyitott protokollt használ és SDK-t biztosít a chatbotok létrehozásához, lehetővé téve olyan szolgáltatások megvalósítását, amelyekkel a felhasználók a SimpleX Chat alkalmazásokon keresztül léphetnek kapcsolatba — mi már nagyon várjuk, hogy milyen SimpleX szolgáltatásokat készítenek a lelkes közreműködők.", + "simplex-unique-overlay-card-4-p-3": "Ha a SimpleX platformra való fejlesztést fontolgatja, például a SimpleX-alkalmazások felhasználóinak szánt chatbotot, vagy a SimpleX Chat Jegyzék bot integrálását más mobilalkalmazásba, lépjen velünk kapcsolatba, ha bármilyen tanácsot vagy támogatást szeretne kapni.", + "simplex-unique-card-1-p-1": "A SimpleX védi az ön profiljához tartozó kapcsolatait és metaadatait, elrejtve azokat a SimpleX platform kiszolgálói és a megfigyelők elől.", + "simplex-unique-card-1-p-2": "Minden más létező üzenetküldő platformtól eltérően a SimpleX nem rendelkezik a felhasználókhoz rendelt azonosítókkal — még véletlenszerű számokkal sem.", + "simplex-unique-card-2-p-1": "Mivel a SimpleX platformon nincs azonosítója vagy állandó címe, senki sem tud kapcsolatba lépni önnel, hacsak nem oszt meg egy egyszeri vagy ideiglenes felhasználói címet, például QR-kódot vagy hivatkozást.", + "simplex-unique-card-3-p-1": "A SimpleX Chat az összes felhasználói adatot kizárólag a klienseken tárolja egy hordozható titkosított adatbázis-formátumban —, amely exportálható és átvihető bármely más támogatott eszközre.", + "simplex-unique-card-3-p-2": "A végpontok között titkosított üzenetek átmenetileg a SimpleX átjátszó-kiszolgálókon tartózkodnak, míg meg nem érkeznek a címzetthez, majd ezt követően végleges törlésre kerülnek.", + "simplex-unique-card-4-p-1": "A SimpleX hálózat teljesen decentralizált és független bármely kriptopénztől vagy bármely más platformtól, kivéve az internetet.", + "simplex-unique-card-4-p-2": "Használhatja a SimpleX-et saját kiszolgálóival vagy az általunk biztosított kiszolgálókkal, és továbbra is kapcsolódhat bármely felhasználóhoz.", + "join": "Csatlakozás", + "we-invite-you-to-join-the-conversation": "Meghívjuk önt, hogy csatlakozzon a beszélgetéshez", + "join-the-REDDIT-community": "Csatlakozzon a REDDIT közösséghez", + "join-us-on-GitHub": "Csatlakozzon hozzánk a GitHubon", + "donate-here-to-help-us": "Adományozzon és segítsen nekünk", + "sign-up-to-receive-our-updates": "Regisztráljon az oldalra, hogy megkapja frissítéseinket", + "enter-your-email-address": "Adja meg az e-mail címét", + "get-simplex": "SimpleX Desktop alkalmazás letöltése", + "why-simplex-is": "A SimpleX mitől", + "unique": "egyedülálló", + "learn-more": "Tudjon meg többet", + "more-info": "További információ", + "hide-info": "Információ elrejtése", + "contact-hero-subheader": "Szkennelje be a QR-kódot a SimpleX Chat alkalmazással telefonján vagy táblagépén.", + "contact-hero-p-1": "A hivatkozásban szereplő nyilvános kulcsokat és az üzenetek várakoztatási címét NEM küldi el a hálózaton keresztül az oldal megtekintésekor — ezeket a hivatkozás URL-jének hash-töredéke tartalmazza.", + "contact-hero-p-2": "Még nem töltötte le a SimpleX Chatet?", + "contact-hero-p-3": "Az alkalmazás letöltéséhez használja az alábbi linkeket.", + "scan-qr-code-from-mobile-app": "QR-kód beolvasása mobilalkalmazásból", + "to-make-a-connection": "A kapcsolat létrehozásához:", + "install-simplex-app": "Telepítse a SimpleX alkalmazást", + "open-simplex-app": "Simplex alkalmazás megnyitása", + "tap-the-connect-button-in-the-app": "Koppintson a „kapcsolódás” gombra az alkalmazásban", + "scan-the-qr-code-with-the-simplex-chat-app": "A QR-kód beolvasása a SimpleX Chat alkalmazással", + "scan-the-qr-code-with-the-simplex-chat-app-description": "A hivatkozásban szereplő nyilvános kulcsokat és az üzenetek várakoztatási címét NEM küldjük el a hálózaton keresztül, amikor ezt az oldalt megtekinti —
ezek a hivatkozás URL-jének hash-töredékében szerepelnek.", + "installing-simplex-chat-to-terminal": "A SimpleX chat telepítése terminálba", + "use-this-command": "Használja ezt a parancsot:", + "see-simplex-chat": "Lásd SimpleX Chat", + "connect-in-app": "Kapcsolódás az alkalmazásban", + "the-instructions--source-code": "az utasításokat, hogyan töltse le vagy fordítsa le a forráskódból.", + "if-you-already-installed-simplex-chat-for-the-terminal": "Ha már telepítette a SimpleX Chat-et a terminálba", + "if-you-already-installed": "Ha már telepítette a", + "simplex-chat-for-the-terminal": "SimpleX Chat-et a terminálba", + "copy-the-command-below-text": "másolja be az alábbi parancsot, és használja a csevegésben:", + "privacy-matters-section-header": "Miért számít az adatvédelem", + "privacy-matters-section-subheader": "A metaadatok védelmének megőrzése — kivel beszélget — megvédi a következőktől:", + "privacy-matters-section-label": "Győződjön meg róla, hogy az üzenetküldő amit használ nem fér hozzá az adataidhoz!", + "simplex-private-section-header": "Mitől lesz a SimpleX privát", + "simplex-network-section-header": "SimpleX hálózat", + "simplex-network-section-desc": "A Simplex Chat a P2P és a föderált hálózatok előnyeinek kombinálásával biztosítja a legjobb adatvédelmet.", + "simplex-network-1-desc": "Minden üzenet a kiszolgálókon keresztül kerül elküldésre, ami jobb metaadat-védelmet és megbízható aszinkron üzenetkézbesítést biztosít, miközben elkerülhető a sok", + "simplex-network-2-header": "A föderált hálózatokkal ellentétben", + "simplex-network-2-desc": "A SimpleX átjátszó kiszolgálók NEM tárolnak felhasználói profilokat, kapcsolatokat és kézbesített üzeneteket, NEM csatlakoznak egymáshoz, és NINCS kiszolgáló könyvtár.", + "simplex-network-3-header": "SimpleX hálózat", + "simplex-network-3-desc": "a kiszolgálók egyirányú üzenet várakoztatásokat biztosítanak a felhasználók összekapcsolásához, de nem látják a hálózati kapcsolati gráfot; azt csak a felhasználók látják.", + "comparison-section-header": "Összehasonlítás más protokollokkal", + "protocol-1-text": "Signal, nagy platformok", + "protocol-2-text": "XMPP, Matrix", + "protocol-3-text": "P2P protokollok", + "comparison-point-1-text": "Globális személyazonosságot igényel", + "comparison-point-2-text": "MITM lehetősége", + "comparison-point-4-text": "Egyetlen vagy központosított hálózat", + "comparison-point-5-text": "Központi komponens vagy más hálózati szintű támadás", + "no": "Nem", + "no-private": "Nem - privát", + "no-secure": "Nem - biztonságos", + "no-resilient": "Nem - ellenálló", + "no-decentralized": "Nem - decentralizált", + "no-federated": "Nem - föderált", + "comparison-section-list-point-1": "Általában telefonszám alapján, néhány esetben felhasználónév alapján", + "comparison-section-list-point-2": "DNS-alapú címek", + "comparison-section-list-point-3": "Nyilvános kulcs vagy más globális egyedi azonosító", + "comparison-section-list-point-4a": "A SimpleX átjátszók nem veszélyeztethetik az e2e titkosítást. Biztonsági kód ellenőrzése a sávon kívüli csatorna elleni támadás mérséklésére", + "comparison-section-list-point-4": "Ha az üzemeltető kiszolgálói veszélybe kerülnek. Ellenőrizze a biztonsági kódot a Signal-ban és néhány más alkalmazásban, hogy csökkentse a veszélyt", + "comparison-section-list-point-5": "Nem védi a felhasználók metaadatait", + "comparison-section-list-point-6": "Bár a P2P elosztott, de nem föderált - egyetlen hálózatként működnek", + "comparison-section-list-point-7": "A P2P-hálózatoknak vagy van egy központi hitelesítője, vagy az egész hálózat kompromittálódhat", + "see-here": "lásd itt", + "guide-dropdown-1": "Gyors indítás", + "guide-dropdown-2": "Üzenetek küldése", + "guide-dropdown-3": "Titkos csoportok", + "guide-dropdown-4": "Csevegő profilok", + "guide-dropdown-5": "Adatkezelés", + "guide-dropdown-6": "Hang- és videó hívások", + "guide-dropdown-7": "Adatvédelem és biztonság", + "guide-dropdown-8": "Alkalmazás beállításai", + "guide": "Útmutató", + "docs-dropdown-1": "SimpleX platform", + "docs-dropdown-2": "Android fájlok elérése", + "docs-dropdown-3": "Hozzáférés a csevegő adatbázishoz", + "docs-dropdown-8": "SimpleX jegyzék szolgáltatás", + "docs-dropdown-9": "Letöltések", + "f-droid-page-simplex-chat-repo-section-text": "Ha hozzá szeretné adni az F-Droid klienséhez, olvassa be a QR-kódot, vagy használja ezt az URL-t:", + "signing-key-fingerprint": "Aláíró kulcs ujjlenyomata (SHA-256)", + "f-droid-org-repo": "F-Droid.org tároló", + "stable-versions-built-by-f-droid-org": "F-Droid.org által készített stabil verziók", + "releases-to-this-repo-are-done-1-2-days-later": "A kiadások ebben a tárolóban néhány napot késnek", + "f-droid-page-f-droid-org-repo-section-text": "A SimpleX Chat és az F-Droid.org tárolók különböző kulcsokkal írják alá az összeállításokat. A váltáshoz exportálja a csevegési adatbázist és telepítse újra az alkalmazást.", + "jobs": "Csatlakozzon a csapathoz", + "please-enable-javascript": "Engedélyezze a JavaScriptet a QR-kód megjelenítéséhez.", + "please-use-link-in-mobile-app": "Használja a mobilalkalmazásban található hivatkozást", + "contact-hero-header": "Kapott egy címet a SimpleX Chat-en való kapcsolódáshoz", + "invitation-hero-header": "Kapott egy egyszer használatos hivatkozást a SimpleX Chat-en való kapcsolódáshoz", + "simplex-network-overlay-card-1-li-4": "A P2P-megvalósításokat egyes internetszolgáltatók blokkolhatják (mint például a BitTorrent). A SimpleX átvitel-független - a szabványos webes protokollokon, pl. WebSockets-en keresztül is működik.", + "simplex-private-card-4-point-2": "A SimpleX Tor-on keresztüli használatához telepítse az Orbot alkalmazást és engedélyezze a SOCKS5 proxy-t (vagy a VPN-t az iOS-ban).", + "simplex-private-card-5-point-1": "A SimpleX minden titkosítási réteghez tartalomkitöltést használ, hogy meghiúsítsa az üzenetméret ellen irányuló támadásokat.", + "simplex-private-card-5-point-2": "A kiszolgálók és a hálózatot megfigyelők számára a különböző méretű üzenetek egyformának tűnnek.", + "privacy-matters-1-title": "Hirdetés és árdiszkrimináció", + "hero-overlay-card-1-p-3": "Ön határozza meg, hogy melyik kiszolgáló(ka)t használja az üzenetek fogadására, a kapcsolatokhoz — azokat a kiszolgálókat, amelyeket az üzenetek küldésére használ. Minden beszélgetés két különböző kiszolgálót használ.", + "hero-overlay-card-3-p-3": "További információk a közleményben.", + "simplex-network-overlay-card-1-p-1": "A P2P üzenetküldő protokollok és alkalmazások számos problémával küzdenek, amelyek miatt kevésbé megbízhatóak, mint a SimpleX, bonyolultabb az elemzésük és többféle támadással szemben sebezhetőek.", + "chat-bot-example": "Chat bot példa", + "simplex-private-3-title": "Biztonságos, hitelesített
TLS adatátvitel", + "github-repository": "GitHub tároló", + "tap-to-close": "Koppintson a bezáráshoz", + "simplex-network-1-header": "A P2P hálózatokkal ellentétben", + "simplex-network-1-overlay-linktext": "a P2P hálózat problémái", + "comparison-point-3-text": "Függés a DNS-től", + "yes": "Igen", + "guide-dropdown-9": "Kapcsolatok létrehozása", + "docs-dropdown-4": "SMP-kiszolgáló üzemeltetése", + "docs-dropdown-5": "XFTP-kiszolgáló üzemeltetése", + "docs-dropdown-6": "WebRTC kiszolgálók", + "docs-dropdown-7": "SimpleX Chat honosítása", + "docs-dropdown-10": "Átláthatóság", + "docs-dropdown-11": "GY.I.K.", + "docs-dropdown-12": "Biztonság", + "newer-version-of-eng-msg": "Ennek az oldalnak van egy újabb angol nyelvű változata.", + "click-to-see": "Kattintson a megtekintéséhez", + "menu": "Menü", + "on-this-page": "Ezen az oldalon", + "back-to-top": "Vissza a tetejére", + "glossary": "Fogalomtár", + "simplex-chat-via-f-droid": "SimpleX Chat az F-Droidon keresztül", + "simplex-chat-repo": "SimpleX Chat tároló", + "stable-and-beta-versions-built-by-developers": "A fejlesztők által készített stabil és béta verziók" +} diff --git a/website/langs/it.json b/website/langs/it.json index c2425f93fd..ffbb28903a 100644 --- a/website/langs/it.json +++ b/website/langs/it.json @@ -10,7 +10,7 @@ "simplex-explained-tab-3-p-1": "I server hanno credenziali anonime separate per ogni coda e non sanno a quali utenti appartengano.", "chat-protocol": "Protocollo di chat", "donate": "Dona", - "copyright-label": "© 2020-2023 SimpleX | Progetto Open-Source", + "copyright-label": "© 2020-2024 SimpleX | Progetto Open-Source", "simplex-chat-protocol": "Protocollo di SimpleX Chat", "terminal-cli": "Terminale CLI", "terms-and-privacy-policy": "Informativa sulla privacy", @@ -233,7 +233,7 @@ "guide-dropdown-8": "Impostazioni dell'app", "docs-dropdown-7": "Traduci SimpleX Chat", "glossary": "Glossario", - "releases-to-this-repo-are-done-1-2-days-later": "Le pubblicazioni su questo repo avvengono 1-2 giorni dopo", + "releases-to-this-repo-are-done-1-2-days-later": "Le pubblicazioni su questo repo avvengono diversi giorni dopo", "f-droid-page-f-droid-org-repo-section-text": "I repository di SimpleX Chat e F-Droid.org firmano i pacchetti con chiavi diverse. Per passare da uno all'altro, esporta il database della chat e reinstalla l'app.", "signing-key-fingerprint": "Impronta della chiave di firma (SHA-256)", "f-droid-org-repo": "Repo di F-Droid.org", diff --git a/website/langs/ja.json b/website/langs/ja.json index 3ac040272f..48aaa41493 100644 --- a/website/langs/ja.json +++ b/website/langs/ja.json @@ -52,7 +52,7 @@ "chat-protocol": "チャットプロトコル", "chat-bot-example": "チャットボットの例", "donate": "寄付", - "copyright-label": "© 2020-2023 SimpleX | Open-Source Project", + "copyright-label": "© 2020-2024 SimpleX | Open-Source Project", "hero-p-1": "他のアプリにはユーザー ID があります: Signal、Matrix、Session、Briar、Jami、Cwtch など。
SimpleX にはありません。乱数さえもありません
これにより、プライバシーが大幅に向上します。", "copy-the-command-below-text": "以下のコマンドをコピーしてチャットで使用します:", "simplex-private-card-9-point-1": "各メッセージ キューは、異なる送信アドレスと受信アドレスを使用してメッセージを一方向に渡します。", @@ -123,7 +123,7 @@ "no-resilient": "いいえ - 弾力性", "hide-info": "情報を隠す", "privacy-matters-overlay-card-3-p-4": "エンドツーエンドで暗号化されたメッセンジャーを使用するだけでは十分ではありません。私たちは皆、個人ネットワークのプライバシーを保護するメッセンジャーを使用する必要があります — 私たちがつながっているのは誰なのか。", - "releases-to-this-repo-are-done-1-2-days-later": "このリポジトリへのリリースは 1 ~ 2 日後に行われます", + "releases-to-this-repo-are-done-1-2-days-later": "このリポジトリへのリリースは数日後に行われます", "comparison-point-1-text": "グローバル ID が必要", "comparison-section-list-point-5": "ユーザーのメタデータのプライバシーを保護しない", "hero-overlay-card-2-p-2": "その後、この情報を既存の公開ソーシャル ネットワークと関連付けて、本当の身元を特定することができます。", diff --git a/website/langs/nl.json b/website/langs/nl.json index 15729fbfe9..1d6dc4c2df 100644 --- a/website/langs/nl.json +++ b/website/langs/nl.json @@ -17,7 +17,7 @@ "chat-bot-example": "Chatbot voorbeeld", "smp-protocol": "SMP protocol", "donate": "Doneer", - "copyright-label": "© 2020-2023 SimpleX | Open-sourceproject", + "copyright-label": "© 2020-2024 SimpleX | Open-sourceproject", "simplex-chat-protocol": "SimpleX Chat protocol", "terminal-cli": "Terminal CLI", "terms-and-privacy-policy": "Privacybeleid", @@ -240,7 +240,7 @@ "f-droid-org-repo": "F-Droid.org repo", "signing-key-fingerprint": "Signing key fingerprint (SHA-256)", "stable-versions-built-by-f-droid-org": "Stabiele versies gebouwd door F-Droid.org", - "releases-to-this-repo-are-done-1-2-days-later": "De releases voor deze repository vinden 1-2 dagen later plaats", + "releases-to-this-repo-are-done-1-2-days-later": "De releases voor deze repository vinden enkele dagen later plaats", "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat- en F-Droid.org-repository's ondertekenen builds met de verschillende sleutels. Om over te stappen, alstublieft exporteer de chatdatabase en installeer de app opnieuw.", "docs-dropdown-8": "SimpleX Directory Service", "comparison-section-list-point-4a": "SimpleX relais kunnen de e2e-versleuteling niet in gevaar brengen. Controleer de beveiligingscode om aanvallen op out-of-band kanalen te beperken", diff --git a/website/langs/pl.json b/website/langs/pl.json index bbfb2dd9b7..e976be8295 100644 --- a/website/langs/pl.json +++ b/website/langs/pl.json @@ -15,7 +15,7 @@ "smp-protocol": "Protokół SMP", "chat-protocol": "Protokół czatu", "donate": "Darowizna", - "copyright-label": "© 2020-2023 SimpleX | Projekt Open-Source", + "copyright-label": "© 2020-2024 SimpleX | Projekt Open-Source", "simplex-chat-protocol": "Protokół SimpleX Chat", "terminal-cli": "Terminal CLI", "terms-and-privacy-policy": "Polityka prywatności", @@ -242,7 +242,7 @@ "signing-key-fingerprint": "Odcisk klucza podpisu (SHA-256)", "f-droid-org-repo": "Repo F-Droid.org", "stable-versions-built-by-f-droid-org": "Wersje stabilne zbudowane przez F-Droid.org", - "releases-to-this-repo-are-done-1-2-days-later": "Wydania na tym repo są 1-2 dni później", + "releases-to-this-repo-are-done-1-2-days-later": "Wydania na tym repo są kilka dni później", "comparison-section-list-point-4a": "Przekaźniki SimpleX nie mogą skompromitować szyfrowania e2e. Zweryfikuj kody bezpieczeństwa aby złagodzić atak na kanał pozapasmowy", "hero-overlay-3-title": "Ocena bezpieczeństwa", "hero-overlay-card-3-p-2": "Trail of Bits przejrzał komponenty kryptograficzne i sieciowe platformy SimpleX w listopadzie 2022.", diff --git a/website/langs/pt_BR.json b/website/langs/pt_BR.json index 1bb21372e6..784e9e7ca0 100644 --- a/website/langs/pt_BR.json +++ b/website/langs/pt_BR.json @@ -25,7 +25,7 @@ "smp-protocol": "Protocolo SMP", "chat-protocol": "Protocolo de bate-papo", "donate": "Doar", - "copyright-label": "© 2020-2023 SimpleX | Projeto de Código Livre", + "copyright-label": "© 2020-2024 SimpleX | Projeto de Código Livre", "simplex-chat-protocol": "Protocolo Chat SimpleX", "terminal-cli": "CLI Terminal", "hero-header": "Privacidade redefinida", diff --git a/website/langs/ru.json b/website/langs/ru.json index 4a9100c76d..827d0c0544 100644 --- a/website/langs/ru.json +++ b/website/langs/ru.json @@ -1,6 +1,6 @@ { "copy-the-command-below-text": "скопируйте приведенную ниже команду и используйте ее в чате:", - "copyright-label": "© 2020-2023 SimpleX | Проект с открытым исходным кодом", + "copyright-label": "© 2020-2024 SimpleX | Проект с открытым исходным кодом", "chat-bot-example": "Пример Чат бота", "simplex-private-card-9-point-1": "Каждая очередь сообщений передает сообщения в одном направлении с разными адресами отправки и получения.", "simplex-private-card-1-point-2": "Криптобокс NaCL в каждой очереди для предотвращения корреляции трафика между очередями сообщений, в случае компрометации TLS.", @@ -91,7 +91,7 @@ "no-resilient": "Нет - устойчив", "hide-info": "Спрятать информацию", "privacy-matters-overlay-card-3-p-4": "Недостаточно просто использовать мессенджер со сквозным шифрованием, мы все должны использовать мессенджеры, которые защищают конфиденциальность наших личных сетей — с какими людьми мы связаны.", - "releases-to-this-repo-are-done-1-2-days-later": "Выпуск новых версий в этом репозитории выходит с задержкой в 1-2 дня", + "releases-to-this-repo-are-done-1-2-days-later": "Выпуск новых версий в этом репозитории выходит с задержкой в несколько дней", "comparison-point-1-text": "Требуется глобальный идентификатор", "comparison-section-list-point-5": "Не защищает конфиденциальность пользовательских метаданных", "hero-overlay-card-2-p-2": "Затем они могли бы сопоставить эту информацию с существующими общедоступными социальными сетями и определить некоторые реальные личности.", @@ -254,5 +254,6 @@ "please-use-link-in-mobile-app": "Пожалуйста, воспользуйтесь ссылкой в мобильном приложении", "please-enable-javascript": "Пожалуйста, включите JavaScript, чтобы увидеть QR-код.", "docs-dropdown-10": "Прозрачность", - "docs-dropdown-12": "Безопасность" + "docs-dropdown-12": "Безопасность", + "docs-dropdown-11": "Часто задаваемые вопросы" } diff --git a/website/langs/uk.json b/website/langs/uk.json index 6289b44ca8..cad05772da 100644 --- a/website/langs/uk.json +++ b/website/langs/uk.json @@ -1,7 +1,7 @@ { "features": "Особливості", "simplex-explained-tab-3-text": "3. Що бачать сервери", - "terms-and-privacy-policy": "Умови та політика конфіденційності", + "terms-and-privacy-policy": "Політика конфіденційності", "feature-4-title": "Голосові повідомлення з шифруванням від кінця до кінця", "feature-5-title": "Зникнення повідомлень", "simplex-private-card-3-point-3": "Відновлення з'єднання вимкнено для запобігання атакам на сесію.", @@ -78,7 +78,7 @@ "smp-protocol": "Протокол SMP", "chat-protocol": "Протокол чату", "donate": "Пожертвувати", - "copyright-label": "© 2020-2023 SimpleX | Проект з відкритим кодом", + "copyright-label": "© 2020-2024 SimpleX | Проект з відкритим кодом", "simplex-chat-protocol": "Протокол чату SimpleX", "terminal-cli": "Термінал CLI", "hero-header": "Приватність переосмислена", @@ -240,7 +240,7 @@ "stable-versions-built-by-f-droid-org": "Стабільні версії, побудовані F-Droid.org", "simplex-chat-repo": "Репозитарій SimpleX Chat", "f-droid-org-repo": "Репозитарій F-Droid.org", - "releases-to-this-repo-are-done-1-2-days-later": "Релізи в цей репозитарій робляться за 1-2 дні пізніше", + "releases-to-this-repo-are-done-1-2-days-later": "Релізи в це репо відбуваються на кілька днів пізніше", "stable-and-beta-versions-built-by-developers": "Стабільні та бета-версії, побудовані розробниками", "f-droid-page-f-droid-org-repo-section-text": "SimpleX Chat та репозитарії F-Droid.org підписують збірки різними ключами. Щоб переключитися, будь ласка, експортуйте базу даних чату та перевстановіть додаток.", "hero-overlay-3-title": "Оцінка безпеки", @@ -252,5 +252,8 @@ "comparison-section-list-point-4a": "Ретранслятори SimpleX не можуть порушити e2e-шифрування. Перевірте безпековий код для зменшення ризику атаки на зовнішньобандовий канал", "docs-dropdown-9": "Завантаження", "please-enable-javascript": "Будь ласка, увімкніть JavaScript, щоб побачити QR-код.", - "please-use-link-in-mobile-app": "Будь ласка, скористайтеся посиланням у мобільному додатку" + "please-use-link-in-mobile-app": "Будь ласка, скористайтеся посиланням у мобільному додатку", + "docs-dropdown-11": "ПОШИРЕНІ ЗАПИТАННЯ", + "docs-dropdown-10": "Прозорість", + "docs-dropdown-12": "Безпека" } diff --git a/website/langs/zh_Hans.json b/website/langs/zh_Hans.json index a81f4d123c..03da9db140 100644 --- a/website/langs/zh_Hans.json +++ b/website/langs/zh_Hans.json @@ -57,7 +57,7 @@ "simplex-chat-protocol": "SimpleX 聊天协议", "smp-protocol": "SMP协议", "chat-protocol": "聊天协议", - "copyright-label": "© 2020-2023 SimpleX | 开源项目", + "copyright-label": "© 2020-2024 SimpleX | 开源项目", "terminal-cli": "命令行程式", "simplex-explained-tab-1-p-1": "您可以创建联系人和群组,并进行双向对话,就像是任何其他即时通讯软件一样。", "hero-p-1": "其他应用——如Signal、Matrix、Session、Briar、Jami、Cwtch 等——都需要用户 ID。
而SimpleX 不需要用户ID,连随机生成的也不需要。
这从根本上改善了您的隐私。", @@ -235,7 +235,7 @@ "glossary": "术语表", "signing-key-fingerprint": "签名密钥指纹 (SHA-256)", "simplex-chat-via-f-droid": "通过 F-Droid 下载 SimpleX", - "releases-to-this-repo-are-done-1-2-days-later": "此存储库的版本将延迟 1-2 天发布", + "releases-to-this-repo-are-done-1-2-days-later": "此存储库的版本将延迟数天发布", "f-droid-org-repo": "F-Droid.org 存储库", "stable-versions-built-by-f-droid-org": "由 F-Droid.org 构建的稳定版本", "simplex-chat-repo": "SimpleX 存储库", diff --git a/website/src/.well-known/org.flathub.VerifiedApps.txt b/website/src/.well-known/org.flathub.VerifiedApps.txt new file mode 100644 index 0000000000..3a9a2d04de --- /dev/null +++ b/website/src/.well-known/org.flathub.VerifiedApps.txt @@ -0,0 +1 @@ +ae8b5b2e-76c9-4a31-a044-bcbda1cdf264 diff --git a/website/src/_data/glossary.json b/website/src/_data/glossary.json index 3420ba3700..a16c2b9541 100644 --- a/website/src/_data/glossary.json +++ b/website/src/_data/glossary.json @@ -59,6 +59,10 @@ "term": "Man-in-the-middle attack", "definition": "Man-in-the-middle attack" }, + { + "term": "MITM attack", + "definition": "Man-in-the-middle attack" + }, { "term": "Merkle directed acyclic graph", "definition": "Merkle directed acyclic graph" diff --git a/website/src/_includes/blog_previews/20240516.html b/website/src/_includes/blog_previews/20240516.html new file mode 100644 index 0000000000..0d434eac36 --- /dev/null +++ b/website/src/_includes/blog_previews/20240516.html @@ -0,0 +1,17 @@ +

When it comes to open source privacy tools, the status quo often dictates the limitations of + existing protocols and + structures. However, these norms need to be challenged to radically shift how we approach genuinely + private communication. This requires doing some uncomfortable things, like making hard choices as it relates to + funding, alternative decentralization models, doubling down on privacy over convenience, and more. +

+ +

In this post we explain a bit more about why SimpleX operates and makes decisions the way it does: +

+ +
    +
  • No user accounts.
  • +
  • Privacy over convenience.
  • +
  • Network decentralization.
  • +
  • Funding and profitability.
  • +
  • Company jurisdiction.
  • +
\ No newline at end of file diff --git a/website/src/_includes/blog_previews/20240601.html b/website/src/_includes/blog_previews/20240601.html new file mode 100644 index 0000000000..5e0ca2de49 --- /dev/null +++ b/website/src/_includes/blog_previews/20240601.html @@ -0,0 +1,2 @@ +

As lawmakers grapple with the serious issue of child exploitation online, + some proposed solutions would fuel the very problem they aim to solve.

\ No newline at end of file diff --git a/website/src/_includes/blog_previews/20240604.html b/website/src/_includes/blog_previews/20240604.html new file mode 100644 index 0000000000..50ae43161d --- /dev/null +++ b/website/src/_includes/blog_previews/20240604.html @@ -0,0 +1,14 @@ +

v5.8 is released:

+ +
    +
  • private message routing.
  • +
  • server transparency.
  • +
  • protect IP address when downloading files & media.
  • +
  • chat themes* for better conversation privacy.
  • +
  • group improvements - reduced traffic and additional preferences.
  • +
  • improved networking, message and file delivery.
  • +
+ +

Also, we added Persian interface language*, thanks to our users and Weblate.

+ +

* Android and desktop apps only.

\ No newline at end of file diff --git a/website/src/_includes/blog_previews/20240704.html b/website/src/_includes/blog_previews/20240704.html new file mode 100644 index 0000000000..581ed20720 --- /dev/null +++ b/website/src/_includes/blog_previews/20240704.html @@ -0,0 +1,3 @@ +

It's time we shift the focus: privacy should be a non-negotiable duty of technology providers, not just a + right users must fight to protect, and not something that users can be asked to consent away as a + condition of access to a service.

\ No newline at end of file diff --git a/website/src/_includes/blog_previews/20240814.html b/website/src/_includes/blog_previews/20240814.html new file mode 100644 index 0000000000..56d9711098 --- /dev/null +++ b/website/src/_includes/blog_previews/20240814.html @@ -0,0 +1,18 @@ +

SimpleX Chat vision and funding 2.0: past, present, future.

+ +

Announcing the investment from Jack Dorsey and Asymmetric.

+ +

v6.0 is released:

+ +
    +
  • Private message routing - enabled by default
  • +
  • +

    New chat experience:

    +
      +
    • connect to your friends faster.
    • +
    • new reachable interface.
    • +
    • and much more!
    • +
    +
  • +
  • Improved networking and battery usage
  • +
\ No newline at end of file diff --git a/website/src/blog.html b/website/src/blog.html index 11f057ce70..571b240451 100644 --- a/website/src/blog.html +++ b/website/src/blog.html @@ -58,7 +58,7 @@ active_blog: true
-

+

{{ blog.data.title | safe }}

diff --git a/website/src/blogs-atom-feed.njk b/website/src/blogs-atom-feed.njk index 11e1d72a7a..68f396ae40 100644 --- a/website/src/blogs-atom-feed.njk +++ b/website/src/blogs-atom-feed.njk @@ -11,28 +11,31 @@ metadata: email: chat@simplex.chat --- - + + {{ metadata.url }} + + {{ metadata.title }} {{ metadata.subtitle }} - - {{ collections.blogs | getNewestCollectionItemDate | dateToRfc3339 }} - {{ metadata.url }} {{ metadata.author.name }} {{ metadata.author.email }} {%- for blog in collections.blogs | reverse %} {%- if not blog.data.draft %} - {%- set absolutePostUrl = blog.url | absoluteUrl(metadata.url) %} + {%- set absolutePostUrl = blog.data.permalink | absoluteUrl(metadata.url) %} + {{ blog.data.permalink | absoluteUrl(metadata.url) }} + + {{ blog.data.date | dateToRfc3339 }} + {{ blog.data.title }} - - {# {{ blog.date | dateToRfc3339 }} #} - {{ blog.data.date.toUTCString().split(' ').slice(1, 4).join(' ') }} - {{ absolutePostUrl }} - {{ blog.templateContent | htmlToAbsoluteUrls(absolutePostUrl) }} - {# {{ blog.templateContent | striptags | truncate(200) }} #} + {{ blog.templateContent | htmlToAbsoluteUrls(absolutePostUrl) }} + + {{ metadata.author.name }} + {{ metadata.author.email }} + {%- endif %} {%- endfor %} diff --git a/website/src/blogs-rss-feed.njk b/website/src/blogs-rss-feed.njk index 54b63e088c..a2e675ec40 100644 --- a/website/src/blogs-rss-feed.njk +++ b/website/src/blogs-rss-feed.njk @@ -26,8 +26,8 @@ metadata: {{ absolutePostUrl }} {{ blog.templateContent | htmlToAbsoluteUrls(absolutePostUrl) }} {# {{ blog.templateContent | striptags | truncate(200) }} #} - {# {{ blog.data.date | dateToRfc822 }} #} - {{ blog.data.date.toUTCString().split(' ').slice(1, 4).join(' ') }} + {{ blog.data.date | dateToRfc822 }} + {# {{ blog.data.date.toUTCString().split(' ').slice(1, 4).join(' ') }} #} {{ metadata.author.name }} {{ absolutePostUrl }} diff --git a/website/src/call/call.js b/website/src/call/call.js index 8104470686..b247431f4b 100644 --- a/website/src/call/call.js +++ b/website/src/call/call.js @@ -24,9 +24,8 @@ var TransformOperation; let activeCall; const processCommand = (function () { const defaultIceServers = [ - { urls: ["stuns:stun.simplex.im:443"] }, { urls: ["stun:stun.simplex.im:443"] }, - { urls: ["turns:turn.simplex.im:443"], username: "private2", credential: "Hxuq2QxUjnhj96Zq2r4HjqHRj" }, + { urls: ["turn:turn.simplex.im:443"], username: "private", credential: "yleob6AVkiNI87hpR94Z" }, ]; function getCallConfig(encodedInsertableStreams, iceServers, relay) { return { diff --git a/website/src/css/blog.css b/website/src/css/blog.css index 897fbafb10..dcbe842785 100644 --- a/website/src/css/blog.css +++ b/website/src/css/blog.css @@ -232,4 +232,10 @@ h6 { #article ol>li::marker { font-weight: 500; +} + +#article blockquote { + padding-left: 1em; + border-left: 2px solid #c0c0c0; + font-style: italic; } \ No newline at end of file diff --git a/website/src/css/style.css b/website/src/css/style.css index 71eb56fa67..475eddc404 100644 --- a/website/src/css/style.css +++ b/website/src/css/style.css @@ -46,6 +46,10 @@ img{ -ms-user-select: none; /* For Internet Explorer and Edge */ } +a{ + word-wrap: break-word; +} + /* #comparison::before { display: block; content: " "; diff --git a/website/src/monerokon.html b/website/src/monerokon.html index e854065b6b..060c6883bb 100644 --- a/website/src/monerokon.html +++ b/website/src/monerokon.html @@ -2,7 +2,7 @@ layout: layouts/group_link.html title: "SimpleX Chat - MoneroKon group" description: "Join the group of attendees of Monero Konferenco 3 - Praha 2023" -groupLink: "https://simplex.chat/contact#/?v=1-2&smp=smp%3A%2F%2Fu2dS9sG8nMNURyZwqASV4yROM28Er0luVTx5X1CsMrU%3D%40smp4.simplex.im%2F1OXnPP15cK8HAJ3YM_7UfQhlW-9WFE8P%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAHf0AClqIM2SnOJ7OP06pr7UXlcnzGaBUyx3MLmRP0ko%253D%26srv%3Do5vmywmrnaxalvz6wi3zicyftgio6psuvyniis6gco6bp6ekl4cqj4id.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%22CIdAO_gOEDOsW9oZrtAHiA%3D%3D%22%7D" +groupLink: "https://simplex.chat/contact/#/?v=1-4&smp=smp%3A%2F%2Fhpq7_4gGJiilmz5Rf-CswuU5kZGkm_zOIooSw6yALRg%3D%40smp5.simplex.im%2FIE3ZKT3daRLKdQg1nSXK4U1cUK4A81XQ%23%2F%3Fv%3D1-2%26dh%3DMCowBQYDK2VuAyEAN2vLBKKQiTG58nokhiBIpqvLTyfeyey6UbaFGy4cYH8%253D%26srv%3Djjbyvoemxysm7qxap7m5d5m35jzv5qq6gnlv7s4rsn7tdwwmuqciwpid.onion&data=%7B%22type%22%3A%22group%22%2C%22groupLinkId%22%3A%227LTn4BEWw4bD9Gs8snVEJA%3D%3D%22%7D" groupLinkText: Open MoneroKon group link templateEngineOverride: njk ---- +--- \ No newline at end of file