diff --git a/README.md b/README.md index 4e74bbc866..29ee7e189e 100644 --- a/README.md +++ b/README.md @@ -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, diff --git a/apps/ios/Shared/AppDelegate.swift b/apps/ios/Shared/AppDelegate.swift index 7204625ad4..d52c950d81 100644 --- a/apps/ios/Shared/AppDelegate.swift +++ b/apps/ios/Shared/AppDelegate.swift @@ -9,6 +9,7 @@ import Foundation import UIKit import SimpleXChat +import SwiftUI class AppDelegate: NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { @@ -17,6 +18,7 @@ class AppDelegate: NSObject, UIApplicationDelegate { if #available(iOS 17.0, *) { trackKeyboard() } NotificationCenter.default.addObserver(self, selector: #selector(pasteboardChanged), name: UIPasteboard.changedNotification, object: nil) removePasscodesIfReinstalled() + prepareForLaunch() return true } @@ -141,6 +143,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 +154,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..d93625986e 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 @@ -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() + } + .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() + } + .onChange(of: theme.name) { _ in + ThemeManager.adjustWindowStyle() + } } @ViewBuilder private func contentView() -> some View { @@ -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..141e6d5c42 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -47,7 +47,11 @@ 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? @@ -332,12 +336,12 @@ final class ChatModel: ObservableObject { 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 @@ -357,7 +361,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 { @@ -422,6 +426,16 @@ 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 { @@ -512,11 +526,13 @@ final class ChatModel: ObservableObject { } func markChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) { - // update preview - decreaseUnreadCounter(cInfo) - // update current chat if chatId == cInfo.id, let i = getChatItemIndex(cItem) { - markChatItemRead_(i) + if reversedChatItems[i].isRcvNew { + // update current chat + markChatItemRead_(i) + // update preview + decreaseUnreadCounter(cInfo) + } } } @@ -686,7 +702,7 @@ final class ChatModel: ObservableObject { } i += 1 } - return UnreadChatItemCounts(totalBelow: totalBelow, unreadBelow: unreadBelow) + return UnreadChatItemCounts(isNearBottom: totalBelow < 16, unreadBelow: unreadBelow) } func topItemInView(itemsInView: Set) -> ChatItem? { @@ -723,8 +739,8 @@ struct NTFContactRequest { var chatId: String } -struct UnreadChatItemCounts { - var totalBelow: Int +struct UnreadChatItemCounts: Equatable { + var isNearBottom: Bool var unreadBelow: Int } diff --git a/apps/ios/Shared/Model/ImageUtils.swift b/apps/ios/Shared/Model/ImageUtils.swift index 6437597b19..073621caa4 100644 --- a/apps/ios/Shared/Model/ImageUtils.swift +++ b/apps/ios/Shared/Model/ImageUtils.swift @@ -205,6 +205,43 @@ func moveTempFileFromURL(_ url: URL) -> CryptoFile? { } } +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 + } +} + +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 + } +} + +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) + } +} + func generateNewFileName(_ prefix: String, _ ext: String, fullPath: Bool = false) -> String { uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)", fullPath: fullPath) } diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 49152283ee..8c90c4b1ac 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -92,18 +92,22 @@ 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 } @@ -242,14 +246,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 } @@ -309,8 +307,10 @@ private func apiChatsResponse(_ r: ChatResponse) throws -> [ChatData] { throw r } +let loadItemsPerPage = 50 + 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 r = chatSendCmdSync(.apiGetChat(type: type, id: id, pagination: .last(count: loadItemsPerPage), search: search)) if case let .apiChat(_, chat) = r { return Chat.init(chat) } throw r } @@ -541,6 +541,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)) } @@ -831,6 +836,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)) @@ -1334,6 +1354,18 @@ func apiGetVersion() throws -> CoreVersionInfo { 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 @@ -1353,8 +1385,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() @@ -1439,8 +1470,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 } diff --git a/apps/ios/Shared/SimpleXApp.swift b/apps/ios/Shared/SimpleXApp.swift index 7d69466c07..34c66d6705 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 diff --git a/apps/ios/Shared/Theme/Theme.swift b/apps/ios/Shared/Theme/Theme.swift new file mode 100644 index 0000000000..3f7013bad7 --- /dev/null +++ b/apps/ios/Shared/Theme/Theme.swift @@ -0,0 +1,200 @@ +// +// 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() { + if currentThemeDefault.get() == DefaultTheme.SYSTEM_THEME_NAME && CurrentColors.colors.isLight == systemInDarkThemeCurrently { + // 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 data = Data(base64Encoded: dropImagePrefix(image)), + let parsed = UIImage(data: data), + 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..55f9a08878 --- /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) + } + + 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/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/Chat/ChatInfoToolbar.swift b/apps/ios/Shared/Views/Chat/ChatInfoToolbar.swift index 140b609902..26749ed3ce 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,9 +25,7 @@ struct ChatInfoToolbar: View { ChatInfoImage( chat: chat, size: imageSize, - color: colorScheme == .dark - ? chatImageColorDark - : chatImageColorLight + color: Color(uiColor: .tertiaryLabel) ) .padding(.trailing, 4) VStack { @@ -41,14 +38,14 @@ struct ChatInfoToolbar: View { } } } - .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 +54,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 f5abfe9c58..ed589ec083 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -36,14 +36,14 @@ 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) } } @@ -90,6 +90,7 @@ enum SendReceipts: Identifiable, Hashable { struct ChatInfoView: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme @Environment(\.dismiss) var dismiss: DismissAction @ObservedObject var chat: Chat @State var contact: Contact @@ -143,7 +144,7 @@ struct ChatInfoView: View { .listRowSeparator(.hidden) if let customUserProfile = customUserProfile { - Section("Incognito") { + Section(header: Text("Incognito").foregroundColor(theme.colors.secondary)) { HStack { Text("Your random profile") Spacer() @@ -161,6 +162,11 @@ struct ChatInfoView: View { connStats.ratchetSyncAllowed { synchronizeConnectionButton() } + NavigationLink { + ChatWallpaperEditorSheet(chat: chat) + } label: { + Label("Chat theme", systemImage: "photo") + } // } else if developerTools { // synchronizeConnectionButtonForce() // } @@ -183,13 +189,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 @@ -211,8 +219,8 @@ 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) } } } @@ -223,7 +231,7 @@ struct ChatInfoView: 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)") Button ("Debug delivery") { @@ -241,6 +249,7 @@ struct ChatInfoView: View { } } } + .modifier(ThemedBackground(grouped: true)) .navigationBarHidden(true) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) @@ -292,7 +301,7 @@ struct ChatInfoView: View { if contact.verified { ( Text(Image(systemName: "checkmark.shield")) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .font(.title2) + Text(" ") + Text(contact.profile.displayName) @@ -332,7 +341,7 @@ struct ChatInfoView: View { setContactAlias() } .multilineTextAlignment(.center) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } private func setContactAlias() { @@ -370,6 +379,7 @@ struct ChatInfoView: View { ) .navigationBarTitleDisplayMode(.inline) .navigationTitle("Security code") + .modifier(ThemedBackground(grouped: true)) } label: { Label( contact.verified ? "View security code" : "Verify security code", @@ -386,6 +396,7 @@ struct ChatInfoView: View { currentFeaturesAllowed: contactUserPrefsToFeaturesAllowed(contact.mergedPreferences) ) .navigationBarTitle("Contact preferences") + .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) } label: { Label("Contact preferences", systemImage: "switch.2") @@ -434,11 +445,11 @@ struct ChatInfoView: 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) + .foregroundColor(theme.colors.secondary) serverImage() } } @@ -446,7 +457,7 @@ struct ChatInfoView: View { private func serverImage() -> some View { let status = chatModel.contactNetworkStatus(contact) return Image(systemName: status.imageName) - .foregroundColor(status == .connected ? .green : .secondary) + .foregroundColor(status == .connected ? .green : theme.colors.secondary) .font(.system(size: 12)) } @@ -565,6 +576,148 @@ struct ChatInfoView: View { } } +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?"), 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..c41039a4ef 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIChatFeatureView.swift @@ -12,6 +12,7 @@ import SimpleXChat struct CIChatFeatureView: View { @EnvironmentObject var m: ChatModel @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme var chatItem: ChatItem @Binding var revealed: Bool var feature: Feature @@ -66,10 +67,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 +82,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 +94,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 +105,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 6def90ebe9..414da5371a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIFileView.swift @@ -11,7 +11,7 @@ import SimpleXChat struct CIFileView: View { @EnvironmentObject var m: ChatModel - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme let file: CIFile? let edited: Bool @@ -30,12 +30,12 @@ struct CIFileView: View { Text(file.fileName) .lineLimit(1) .multilineTextAlignment(.leading) - .foregroundColor(.primary) + .foregroundColor(theme.colors.onBackground) Text(prettyFileSize + metaReserve) .font(.caption) .lineLimit(1) .multilineTextAlignment(.leading) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } } else { Text(metaReserve) @@ -170,7 +170,7 @@ struct CIFileView: View { 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) } @@ -182,7 +182,7 @@ struct CIFileView: View { progressView() } case .rcvAborted: - fileIcon("doc.fill", color: .accentColor, innerIcon: "exclamationmark.arrow.circlepath", innerIconSize: 12) + 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) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift index 2f92f778c3..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 @@ -42,7 +42,7 @@ 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, showViaProxy: showSentViaProxy) @@ -65,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 @@ -99,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 cfead635fe..9c12653343 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIImageView.swift @@ -11,12 +11,10 @@ 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? + var imgWidth: CGFloat? @State private var showFullScreenImage = false var body: some View { @@ -25,15 +23,14 @@ struct CIImageView: View { if let uiImage = getLoadedImage(file) { imageView(uiImage) .fullScreenCover(isPresented: $showFullScreenImage) { - FullScreenMediaView(chatItem: chatItem, image: uiImage, showView: $showFullScreenImage, scrollProxy: scrollProxy) + FullScreenMediaView(chatItem: chatItem, image: uiImage, showView: $showFullScreenImage) } .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 { + imageView(preview) .onTapGesture { if let file = file { switch file.fileStatus { @@ -90,7 +87,6 @@ struct CIImageView: View { 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) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIInvalidJSONView.swift index 40ed8bc76c..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,7 +22,6 @@ struct CIInvalidJSONView: View { .padding(.horizontal, 12) .padding(.vertical, 6) .background(Color(uiColor: .tertiarySystemGroupedBackground)) - .cornerRadius(18) .textSelection(.disabled) .onTapGesture { showJSON = true } .appSheet(isPresented: $showJSON) { @@ -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..0f7ea9a716 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CILinkView.swift @@ -10,7 +10,7 @@ import SwiftUI import SimpleXChat struct CILinkView: View { - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme let linkPreview: LinkPreview var body: some View { @@ -32,7 +32,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 24c2c07962..66b810cf2f 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift @@ -11,8 +11,9 @@ 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 @@ -63,6 +64,7 @@ func ciMetaText( chatTTL: Int?, encrypted: Bool?, color: Color = .clear, + primaryColor: Color = .accentColor, transparent: Bool = false, sent: SentCheckmark? = nil, showStatus: Bool = true, @@ -85,7 +87,7 @@ func ciMetaText( 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)) @@ -112,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 da9d5e7d50..7023449e9f 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift @@ -13,6 +13,7 @@ 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 @@ -114,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, 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) } @@ -145,13 +145,12 @@ struct CIRcvDecryptionError: View { + 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 d84ad8f5fc..c31c4a0da9 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVideoView.swift @@ -13,16 +13,13 @@ 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? @State private var player: AVPlayer? @State private var fullPlayer: AVPlayer? @State private var url: URL? @@ -33,13 +30,12 @@ struct CIVideoView: View { @State private var fullScreenTimeObserver: Any? = nil @State private var publisher: AnyCancellable? = nil - init(chatItem: ChatItem, image: String, duration: Int, maxWidth: CGFloat, videoWidth: Binding, scrollProxy: ScrollViewProxy?) { + init(chatItem: ChatItem, preview: UIImage?, duration: Int, maxWidth: CGFloat, videoWidth: CGFloat?) { self.chatItem = chatItem - self.image = image + self.preview = preview self._duration = State(initialValue: duration) self.maxWidth = maxWidth - self._videoWidth = videoWidth - self.scrollProxy = scrollProxy + self.videoWidth = videoWidth if let url = getLoadedVideo(chatItem.file) { let decrypted = chatItem.file?.fileSource?.cryptoArgs == nil ? url : chatItem.file?.fileSource?.decryptedGet() self._urlDecrypted = State(initialValue: decrypted) @@ -49,10 +45,6 @@ struct CIVideoView: View { } self._url = State(initialValue: url) } - if let data = Data(base64Encoded: dropImagePrefix(image)), - let uiImage = UIImage(data: data) { - self._preview = State(initialValue: uiImage) - } } var body: some View { @@ -63,9 +55,8 @@ struct CIVideoView: View { videoView(player, decrypted, file, preview, duration) } else if let file = file, let defaultPreview = preview, file.loaded && urlDecrypted == nil { videoViewEncrypted(file, defaultPreview, duration) - } else if let data = Data(base64Encoded: dropImagePrefix(image)), - let uiImage = UIImage(data: data) { - imageView(uiImage) + } else if let preview { + imageView(preview) .onTapGesture { if let file = file { switch file.fileStatus { @@ -152,7 +143,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) @@ -252,7 +242,6 @@ 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() diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift index 4d950a0d99..ad15d0d342 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIVoiceView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct CIVoiceView: View { @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme var chatItem: ChatItem let recordingFile: CIFile? let duration: Int @@ -72,7 +73,7 @@ struct CIVoiceView: View { playbackState: $playbackState, playbackTime: $playbackTime ) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } private func playbackSlider() -> some View { @@ -89,10 +90,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) } } @@ -118,7 +120,7 @@ struct VoiceMessagePlayerTime: View { struct VoiceMessagePlayer: View { @EnvironmentObject var chatModel: ChatModel - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme var chatItem: ChatItem var recordingFile: CIFile? var recordingTime: TimeInterval @@ -216,26 +218,26 @@ struct VoiceMessagePlayer: View { startPlayback(recordingSource) } } label: { - playPauseIcon("play.fill") + playPauseIcon("play.fill", theme.colors.primary) } case .playing: Button { audioPlayer?.pause() playbackState = .paused } label: { - playPauseIcon("pause.fill") + playPauseIcon("pause.fill", theme.colors.primary) } case .paused: Button { audioPlayer?.play() playbackState = .playing } label: { - playPauseIcon("play.fill") + playPauseIcon("play.fill", theme.colors.primary) } } } - 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() @@ -244,7 +246,7 @@ struct VoiceMessagePlayer: View { .foregroundColor(color) .padding(.leading, image == "play.fill" ? 4 : 0) .frame(width: 56, height: 56) - .background(showBackground ? chatItemFrameColor(chatItem, colorScheme) : .clear) + .background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear) .clipShape(Circle()) if recordingTime > 0 { ProgressCircle(length: recordingTime, progress: $playbackTime) @@ -261,11 +263,12 @@ struct VoiceMessagePlayer: View { } } } label: { - playPauseIcon(icon) + playPauseIcon(icon, theme.colors.primary) } } private struct ProgressCircle: View { + @EnvironmentObject var theme: AppTheme var length: TimeInterval @Binding var progress: TimeInterval? @@ -273,7 +276,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)) @@ -288,7 +291,7 @@ struct VoiceMessagePlayer: View { .frame(width: size, height: size) .foregroundColor(Color(uiColor: .tertiaryLabel)) .frame(width: 56, height: 56) - .background(showBackground ? chatItemFrameColor(chatItem, colorScheme) : .clear) + .background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear) .clipShape(Circle()) } @@ -296,7 +299,7 @@ struct VoiceMessagePlayer: View { ProgressView() .frame(width: 30, height: 30) .frame(width: 56, height: 56) - .background(showBackground ? chatItemFrameColor(chatItem, colorScheme) : .clear) + .background(showBackground ? chatItemFrameColor(chatItem, theme) : .clear) .clipShape(Circle()) } 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..59fabb3901 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedCIVoiceView.swift @@ -12,6 +12,7 @@ import SwiftUI import SimpleXChat struct FramedCIVoiceView: View { + @EnvironmentObject var theme: AppTheme var chatItem: ChatItem let recordingFile: CIFile? let duration: Int @@ -42,7 +43,7 @@ struct FramedCIVoiceView: View { 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 9b4cecf526..5d6327139a 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -9,25 +9,19 @@ 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 @@ -58,10 +52,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 = m.reversedChatItems.first(where: { $0.id == qi.itemId }) { withAnimation { - proxy.scrollTo(ci.viewId, anchor: .bottom) + scrollModel.scrollToItem(id: ci.id) } } } @@ -73,18 +66,16 @@ 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 } if let (title, text) = chatItem.meta.itemStatus.statusInfo { @@ -114,29 +105,33 @@ 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) + case let .image(text, _): + CIImageView(chatItem: chatItem, preview: preview, maxWidth: maxWidth, imgWidth: imgWidth) .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) .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) } @@ -175,13 +170,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 { @@ -233,7 +228,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) @@ -248,7 +243,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) } @@ -347,13 +342,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 == "" @@ -363,22 +351,22 @@ 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 { 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..f8bd9156da 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) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index 11e94cb2c9..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 @@ -65,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) @@ -79,7 +80,7 @@ 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 { @@ -87,7 +88,7 @@ struct MsgContentView: View { } } -func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: String?, icon: String? = nil, preview: Bool = false, showSecrets: Bool) -> Text { +func messageText(_ text: String, _ formattedText: [FormattedText]?, _ sender: String?, icon: String? = nil, preview: Bool = false, showSecrets: Bool, secondaryColor: Color) -> Text { let s = text var res: Text if let ft = formattedText, ft.count > 0 && ft.count <= 200 { @@ -102,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 f90653534c..1814419623 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 @@ -38,6 +39,7 @@ struct ChatItemForwardingView: View { } } } + .modifier(ThemedBackground()) .alert(item: $alert) { $0.alert } } @@ -45,7 +47,7 @@ struct ChatItemForwardingView: View { VStack(alignment: .leading) { if !chatsToForwardTo.isEmpty { List { - searchFieldView(text: $searchText, focussed: $searchFocused) + 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) } @@ -54,8 +56,13 @@ struct ChatItemForwardingView: View { .disabled(chatModel.deletedChats.contains(chat.chatInfo.id)) } } + .modifier(ThemedBackground(grouped: true)) } else { - emptyList() + ZStack { + emptyList() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .modifier(ThemedBackground()) } } } @@ -106,10 +113,10 @@ struct ChatItemForwardingView: View { private func emptyList() -> some View { Text("No filtered chats") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .frame(maxWidth: .infinity) } - + @ViewBuilder private func forwardListChatView(_ chat: Chat) -> some View { let prohibited = prohibitedByPref(chat) Button { @@ -139,7 +146,7 @@ struct ChatItemForwardingView: View { ChatInfoImage(chat: chat, size: 30) .padding(.trailing, 2) Text(chat.chatInfo.chatViewName) - .foregroundColor(prohibited ? .secondary : .primary) + .foregroundColor(prohibited ? theme.colors.secondary : theme.colors.onBackground) .lineLimit(1) if chat.chatInfo.incognito { Spacer() @@ -147,7 +154,7 @@ struct ChatItemForwardingView: View { .resizable() .scaledToFit() .frame(width: 22, height: 22) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } } .frame(maxWidth: .infinity, alignment: .leading) @@ -181,5 +188,5 @@ private func canForwardToChat(_ chat: Chat) -> Bool { 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 f5da473fd6..a46cf03bdf 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift @@ -12,7 +12,7 @@ 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 @@ -101,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) @@ -114,6 +116,7 @@ struct ChatItemInfoView: View { Label(local ? "Saved" : "Forwarded", systemImage: "arrowshape.turn.up.forward") } .tag(CIInfoTab.forwarded) + .modifier(ThemedBackground()) } } .onAppear { @@ -123,6 +126,7 @@ struct ChatItemInfoView: View { } } else { historyTab() + .modifier(ThemedBackground()) } } @@ -212,7 +216,7 @@ struct ChatItemInfoView: View { } else { Text("No history") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .frame(maxWidth: .infinity) } } @@ -227,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 { @@ -258,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)) } } @@ -296,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 { @@ -320,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 { @@ -358,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) } } } @@ -372,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) } } @@ -410,7 +415,7 @@ 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 @@ -418,12 +423,12 @@ struct ChatItemInfoView: View { } } else { Text("No delivery information") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } } } - private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, CIStatus, Bool?)] { + private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, GroupSndStatus, Bool?)] { memberDeliveryStatuses.compactMap({ mds in if let mem = chatModel.getGroupMember(mds.groupMemberId) { return (mem.wrapped, mds.memberDeliveryStatus, mds.sentViaProxy) @@ -433,7 +438,7 @@ struct ChatItemInfoView: View { }) } - private func memberDeliveryStatusView(_ member: GroupMember, _ status: CIStatus, _ sentViaProxy: Bool?) -> some View { + private func memberDeliveryStatusView(_ member: GroupMember, _ status: GroupSndStatus, _ sentViaProxy: Bool?) -> some View { HStack{ ProfileImage(imageStr: member.image, size: 30) .padding(.trailing, 2) @@ -442,26 +447,22 @@ struct ChatItemInfoView: View { Spacer() if sentViaProxy == true { Image(systemName: "arrow.forward") - .foregroundColor(.secondary).opacity(0.67) + .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) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index d580fb5f3e..4cb97112ca 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -11,9 +11,9 @@ 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? @@ -24,7 +24,6 @@ struct ChatItemView: View { chatItem: ChatItem, showMember: Bool = false, maxWidth: CGFloat = .infinity, - scrollProxy: ScrollViewProxy? = nil, revealed: Binding, allowMenu: Binding = .constant(false), audioPlayer: Binding = .constant(nil), @@ -34,7 +33,6 @@ struct ChatItemView: View { self.chat = chat self.chatItem = chatItem self.maxWidth = maxWidth - _scrollProxy = .init(initialValue: scrollProxy) _revealed = revealed _allowMenu = allowMenu _audioPlayer = audioPlayer @@ -62,12 +60,43 @@ 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 + } + } + .map { dropImagePrefix($0) } + .flatMap { Data(base64Encoded: $0) } + .flatMap { UIImage(data: $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, + audioPlayer: $audioPlayer, + playbackState: $playbackState, + playbackTime: $playbackTime + ) } } struct ChatItemContentView: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat var chatItem: ChatItem @Binding var revealed: Bool @@ -97,14 +126,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 +160,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 +208,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 +216,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 27eb3bd653..d0e73adccd 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -9,16 +9,20 @@ import SwiftUI import SimpleXChat import SwiftyGif +import Combine private let memberImageSize: CGFloat = 34 struct ChatView: View { @EnvironmentObject var chatModel: ChatModel - @Environment(\.colorScheme) var colorScheme + @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,11 +30,9 @@ 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 @@ -59,15 +61,19 @@ struct ChatView: View { searchToolbar() Divider() } - ZStack(alignment: .trailing) { + ZStack(alignment: .bottomTrailing) { + 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) chatItemsList() - if let proxy = scrollProxy { - floatingButtons(proxy) + .if(wallpaperImage != nil) { view in + view.modifier( + ChatViewBackground(image: wallpaperImage!, imageType: wallpaperType, background: backgroundColor, tint: tintColor) + ) } + floatingButtons(counts: floatingButtonModel.unreadChatItemCounts) } - - Spacer(minLength: 0) - connectingText() ComposeView( chat: chat, @@ -78,8 +84,11 @@ struct ChatView: View { } .padding(.top, 1) .navigationTitle(cInfo.chatViewName) + .background(theme.colors.background) .navigationBarTitleDisplayMode(.inline) + .environmentObject(theme) .onAppear { + loadChat(chat: chat) initChatView() } .onChange(of: chatModel.chatId) { cId in @@ -89,10 +98,20 @@ struct ChatView: View { chat = c } initChatView() + theme = buildTheme() } else { dismiss() } } + .onChange(of: revealedChatItem) { _ in + NotificationCenter.postReverseListNeedsLayout() + } + .onChange(of: chatModel.reversedChatItems) { reversedChatItems in + if reversedChatItems.count <= loadItemsPerPage && filtered(reversedChatItems).count < 10 { + loadChatItems(chat.chatInfo) + } + } + .environmentObject(scrollModel) .onDisappear { VideoPlayerView.players.removeAll() if chatModel.chatId == cInfo.id && !presentationMode.wrappedValue.isPresented { @@ -107,6 +126,9 @@ struct ChatView: View { } } } + .onChange(of: colorScheme) { _ in + theme = buildTheme() + } .toolbar { ToolbarItem(placement: .principal) { if case let .direct(contact) = cInfo { @@ -135,6 +157,7 @@ struct ChatView: View { connectionStats = nil customUserProfile = nil connectionCode = nil + theme = buildTheme() }) { ChatInfoView(chat: chat, contact: contact, connectionStats: $connectionStats, customUserProfile: $customUserProfile, localAlias: chat.chatInfo.localAlias, connectionCode: $connectionCode) } @@ -143,8 +166,9 @@ struct ChatView: View { Task { await 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( @@ -272,7 +296,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 +306,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) @@ -291,7 +315,6 @@ struct ChatView: View { searchMode = false searchFocussed = false DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) { - chatModel.reversedChatItems = [] loadChat(chat: chat) } } @@ -299,50 +322,49 @@ struct ChatView: View { .padding(.horizontal) .padding(.vertical, 8) } - + private func voiceWithoutFrame(_ ci: ChatItem) -> Bool { 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 > .zero { + mergeCategory != reversedChatItems[index - 1].mergeCategory + } else { + true + } + } + .map { $0.element } + } + + private func chatItemsList() -> some View { let cInfo = chat.chatInfo + let mergedItems = filtered(chatModel.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 chatItemView(ci, maxWidth) + .onAppear { + floatingButtonModel.appeared(viewId: ci.viewId) } - } - .onAppear { - scrollProxy = proxy - } + .onDisappear { + floatingButtonModel.disappeared(viewId: ci.viewId) + } + .id(ci.id) // Required to trigger `onAppear` on iOS15 + } loadPage: { + loadChatItems(cInfo) + } .onTapGesture { hideKeyboard() } .onChange(of: searchText) { _ in loadChat(chat: chat, search: searchText) @@ -352,14 +374,12 @@ struct ChatView: View { chat = c showChatInfoSheet = false loadChat(chat: c) - DispatchQueue.main.async { - scrollToBottom(proxy) - } } } - } + .onChange(of: chatModel.reversedChatItems) { _ in + floatingButtonModel.chatItemsChanged() + } } - .scaleEffect(x: 1, y: -1, anchor: .center) } @ViewBuilder private func connectingText() -> some View { @@ -369,30 +389,78 @@ struct ChatView: View { !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, + unreadBelow: .zero + ) + events + .receive(on: DispatchQueue.global(qos: .background)) + .scan(Set()) { itemsInView, event in + return switch event { + case let .appeared(viewId): + itemsInView.union([viewId]) + case let .disappeared(viewId): + itemsInView.subtracting([viewId]) + case .chatItemsChanged: + itemsInView + } + } + .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 +472,24 @@ struct ChatView: View { circleButton { unreadCountText(counts.unreadBelow) .font(.callout) - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) } - .onTapGesture { scrollToBottom(proxy) } - } else if counts.totalBelow > 16 { + .onTapGesture { + if let latestUnreadItem = filtered(chatModel.reversedChatItems).last(where: { $0.isRcvNew }) { + scrollModel.scrollToItem(id: latestUnreadItem.id) + } + } + } 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 +498,7 @@ struct ChatView: View { content() } } - + private func callButton(_ contact: Contact, _ media: CallMediaType, imageName: String) -> some View { Button { CallController.shared.startCall(contact, media) @@ -456,7 +528,7 @@ struct ChatView: View { Label("Search", systemImage: "magnifyingglass") } } - + private func addMembersButton() -> some View { Button { if case let .group(gInfo) = chat.chatInfo { @@ -486,34 +558,45 @@ struct ChatView: View { } } - private func loadChatItems(_ cInfo: ChatInfo, _ ci: ChatItem, _ proxy: ScrollViewProxy) { - if let firstItem = chatModel.reversedChatItems.last, firstItem.id == ci.id { + private func loadChatItems(_ cInfo: ChatInfo) { + Task { if loadingItems || firstPage { return } loadingItems = true - Task { - do { - let items = try await apiGetChatItems( + 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 ?? chatModel.reversedChatItems.last { + .before(chatItemId: lastItem.id, count: loadItemsPerPage) + } else { + .last(count: loadItemsPerPage) + } + let chatItems = try await apiGetChatItems( type: cInfo.chatType, id: cInfo.apiId, - pagination: .before(chatItemId: firstItem.id, count: 50), + pagination: pagination, 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 } + chatItemsAvailable = !chatItems.isEmpty + reversedPage.append(contentsOf: chatItems.reversed()) } + await MainActor.run { + if reversedPage.count == 0 { + firstPage = true + } else { + chatModel.reversedChatItems.append(contentsOf: reversedPage) + } + loadingItems = false + } + } catch let error { + logger.error("apiGetChat error: \(responseError(error))") + await MainActor.run { loadingItems = false } } } } - + @ViewBuilder private func chatItemView(_ ci: ChatItem, _ maxWidth: CGFloat) -> some View { ChatItemWithMenu( chat: chat, @@ -522,26 +605,27 @@ struct ChatView: View { itemWidth: maxWidth, composeState: $composeState, selectedMember: $selectedMember, + revealedChatItem: $revealedChatItem, chatView: self ) } private struct ChatItemWithMenu: View { @EnvironmentObject var m: ChatModel - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme @ObservedObject var chat: Chat var chatItem: ChatItem var maxWidth: CGFloat @State var itemWidth: CGFloat @Binding var composeState: ComposeState @Binding var selectedMember: GMember? + @Binding var revealedChatItem: ChatItem? var chatView: ChatView @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 @@ -552,15 +636,14 @@ struct ChatView: View { @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) + Group { if revealed, let range = range { let items = Array(zip(Array(range), m.reversedChatItems[range])) ForEach(items, id: \.1.viewId) { (i, ci) in @@ -568,11 +651,26 @@ struct ChatView: View { chatItemView(ci, nil, prev) } } 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) + } + } + .onAppear { + markRead( + chatItems: range.flatMap { m.reversedChatItems[$0] } + ?? [chatItem] + ) + } + } + + private func markRead(chatItems: Array.SubSequence) { + let unreadItems = chatItems.filter { $0.isRcvNew } + if unreadItems.isEmpty { return } + DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) { + if m.chatId == chat.chatInfo.id { + Task { + for unreadItem in unreadItems { + await apiMarkChatItemRead(chat.chatInfo, unreadItem) + } } } } @@ -598,7 +696,7 @@ struct ChatView: View { .padding(.top, 7) } HStack(alignment: .top, spacing: 8) { - ProfileImage(imageStr: member.memberProfile.image, size: memberImageSize) + ProfileImage(imageStr: member.memberProfile.image, size: memberImageSize, backgroundColor: theme.colors.background) .onTapGesture { if chatView.membersLoaded { selectedMember = m.getGroupMember(member.groupMemberId) @@ -645,24 +743,19 @@ 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, + revealed: .constant(revealed), allowMenu: $allowMenu, audioPlayer: $audioPlayer, playbackState: $playbackState, playbackTime: $playbackTime ) - .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) @@ -729,7 +822,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 +839,152 @@ 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)) + 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() } - 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 ?? .zero)..<(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 +1007,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 +1031,64 @@ 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 viewInfoButton(_ ci: ChatItem) -> Button { + Button { Task { do { let cInfo = chat.chatInfo @@ -1002,15 +1104,16 @@ struct ChatView: View { } 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 +1126,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 { @@ -1062,6 +1168,11 @@ struct ChatView: View { showDeleteMessage = true deletingItem = ci } + } label: { + Label( + NSLocalizedString("Delete", comment: "chat item action"), + systemImage: "trash" + ) } } @@ -1075,12 +1186,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( @@ -1094,39 +1201,50 @@ struct ChatView: View { }, 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" + ) } } @@ -1205,17 +1323,26 @@ struct ChatView: View { } } } +} - private func scrollToBottom(_ proxy: ScrollViewProxy) { - if let ci = chatModel.reversedChatItems.first { - withAnimation { proxy.scrollTo(ci.viewId, anchor: .top) } +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? } - } - - private func scrollUp(_ proxy: ScrollViewProxy) { - if let ci = chatModel.topItemInView(itemsInView: itemsInView) { - withAnimation { proxy.scrollTo(ci.viewId, anchor: .top) } + 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 } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeFileView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeFileView.swift index bc6a96aa86..488fe0a65d 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 @@ -33,7 +33,7 @@ struct ComposeFileView: View { .padding(.vertical, 1) .padding(.trailing, 12) .frame(height: 50) - .background(colorScheme == .light ? sentColorLight : sentColorDark) + .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..52655f1c6a 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 @@ -48,7 +48,7 @@ struct ComposeImageView: View { } .padding(.vertical, 1) .padding(.trailing, 12) - .background(colorScheme == .light ? sentColorLight : sentColorDark) + .background(theme.appColors.sentMessage) .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..4137370e3f 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeLinkView.swift @@ -40,7 +40,7 @@ func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) { } struct ComposeLinkView: View { - @Environment(\.colorScheme) var colorScheme + @EnvironmentObject var theme: AppTheme let linkPreview: LinkPreview? var cancelPreview: (() -> Void)? = nil let cancelEnabled: Bool @@ -62,7 +62,7 @@ struct ComposeLinkView: View { } .padding(.vertical, 1) .padding(.trailing, 12) - .background(colorScheme == .light ? sentColorLight : sentColorDark) + .background(theme.appColors.sentMessage) .frame(maxWidth: .infinity) .padding(.top, 8) } @@ -82,7 +82,7 @@ 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) diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 8a0ad9a023..819b337a73 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 @@ -314,6 +315,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 { @@ -354,16 +356,16 @@ 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) + .background(theme.colors.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( @@ -655,7 +657,7 @@ struct ComposeView: View { 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) diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift index 2617bc77bc..4b813d35cb 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,7 +50,7 @@ struct ComposeVoiceView: View { } .padding(.vertical, 1) .frame(height: ComposeVoiceView.previewHeight) - .background(colorScheme == .light ? sentColorLight : sentColorDark) + .background(theme.appColors.sentMessage) .frame(maxWidth: .infinity) .padding(.top, 8) } @@ -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..cd6f5961cd 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextInvitingContactMemberView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextInvitingContactMemberView.swift @@ -9,18 +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(maxWidth: .infinity, alignment: .leading) - .background(colorScheme == .light ? sentColorLight : sentColorDark) + .background(theme.appColors.sentMessage) .padding(.top, 8) } } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ContextItemView.swift index 2777d8321c..3c89cbeb85 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,11 +40,12 @@ struct ContextItemView: View { } label: { Image(systemName: "multiply") } + .tint(theme.colors.primary) } .padding(12) .frame(minHeight: 50) .frame(maxWidth: .infinity) - .background(chatItemFrameColor(contextItem, colorScheme)) + .background(chatItemFrameColor(contextItem, theme)) .padding(.top, 8) } @@ -55,7 +56,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 a180efbd28..a52cc7f71a 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 @@ -49,7 +50,7 @@ struct SendMessageView: View { Text("Voice message…") .font(teFont.italic()) .multilineTextAlignment(.leading) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .padding(.horizontal, 10) .padding(.vertical, 8) .frame(maxWidth: .infinity) @@ -247,6 +248,7 @@ struct SendMessageView: View { } private struct RecordVoiceMessageButton: View { + @EnvironmentObject var theme: AppTheme var startVoiceMessageRecording: (() -> Void)? var finishVoiceMessageRecording: (() -> Void)? @Binding var holdingVMR: Bool @@ -256,7 +258,7 @@ struct SendMessageView: View { var body: some View { Button(action: {}) { Image(systemName: "mic.fill") - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) } .disabled(disabled) .frame(width: 29, height: 29) @@ -309,7 +311,7 @@ struct SendMessageView: View { } } label: { Image(systemName: "mic") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } .disabled(composeState.inProgress) .frame(width: 29, height: 29) @@ -323,7 +325,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) @@ -340,7 +342,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) @@ -383,7 +385,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) } @@ -394,7 +396,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..49239c8fa5 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 @@ -70,7 +71,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 +91,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) } @@ -125,6 +128,7 @@ struct AddGroupMembersViewCommon: View { .onChange(of: selectedContacts) { _ in searchFocussed = false } + .modifier(ThemedBackground(grouped: true)) } private func inviteMembersButton() -> some View { @@ -172,14 +176,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 +201,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 +211,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 +220,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 +232,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..eabf7ad8c9 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -13,6 +13,7 @@ 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 @@ -81,13 +82,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 +106,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 +136,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) @@ -210,6 +218,7 @@ struct GroupChatInfoView: View { private struct MemberRowView: View { var groupInfo: GroupInfo @ObservedObject var groupMember: GMember + @EnvironmentObject var theme: AppTheme var user: Bool = false @Binding var alert: GroupChatInfoViewAlert? @@ -220,14 +229,13 @@ struct GroupChatInfoView: View { .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 +265,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 +294,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 +312,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 +334,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 { @@ -333,6 +359,7 @@ struct GroupChatInfoView: View { creatingGroup: false ) .navigationBarTitle("Group link") + .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) } label: { if groupLink == nil { @@ -350,6 +377,7 @@ struct GroupChatInfoView: View { groupProfile: groupInfo.groupProfile ) .navigationBarTitle("Group profile") + .modifier(ThemedBackground()) .navigationBarTitleDisplayMode(.large) } label: { Label("Edit group profile", systemImage: "pencil") @@ -364,6 +392,7 @@ struct GroupChatInfoView: View { welcomeText: groupInfo.groupProfile.description ?? "" ) .navigationTitle("Welcome message") + .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) } label: { groupInfo.groupProfile.description == nil @@ -518,6 +547,7 @@ func groupPreferencesButton(_ groupInfo: Binding, _ creatingGroup: Bo creatingGroup: creatingGroup ) .navigationBarTitle("Group preferences") + .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) } label: { if creatingGroup { @@ -528,14 +558,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!"), diff --git a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift index c782e2a717..adf5f998a4 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupLinkView.swift @@ -133,6 +133,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 a851e3fc1d..c04ac6ead9 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 @@ -120,12 +121,14 @@ struct GroupMemberInfoView: View { } } 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) } } - Section("Member") { + Section(header: Text("Member").foregroundColor(theme.colors.secondary)) { infoRow("Group", groupInfo.displayName) if let roles = member.canChangeRoleTo(groupInfo: groupInfo) { @@ -138,16 +141,10 @@ struct GroupMemberInfoView: View { } else { infoRow("Role", member.memberRole.text) } - - // TODO invited by - need to get contact by contact id - 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") { + Section(header: Text("Servers").foregroundColor(theme.colors.secondary)) { // TODO network connection status Button("Change receiving address") { alert = .switchAddressAlert @@ -165,8 +162,8 @@ struct GroupMemberInfoView: 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) } } @@ -177,9 +174,13 @@ struct GroupMemberInfoView: View { } if developerTools { - Section("For console") { + 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) + } Button ("Debug delivery") { Task { do { @@ -247,6 +248,7 @@ struct GroupMemberInfoView: View { ProgressView().scaleEffect(2) } } + .modifier(ThemedBackground(grouped: true)) } func connectViaAddressButton(_ contactLink: String) -> some View { @@ -326,7 +328,7 @@ struct GroupMemberInfoView: View { if mem.verified { ( Text(Image(systemName: "checkmark.shield")) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .font(.title2) + Text(" ") + Text(mem.displayName) @@ -374,6 +376,7 @@ struct GroupMemberInfoView: View { ) .navigationBarTitleDisplayMode(.inline) .navigationTitle("Security code") + .modifier(ThemedBackground()) } label: { Label( member.verified ? "View security code" : "Verify security code", @@ -423,7 +426,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 { diff --git a/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift b/apps/ios/Shared/Views/Chat/Group/GroupPreferencesView.swift index 6ae5032be5..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 @@ -73,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 { @@ -111,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..ae6d900eb6 --- /dev/null +++ b/apps/ios/Shared/Views/Chat/ReverseList.swift @@ -0,0 +1,273 @@ +// +// 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: .zero, 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 = .zero + private var bag = Set() + + init(representer: ReverseList) { + self.representer = representer + super.init(style: .plain) + + // 1. Style + 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, .zero) + .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 + ) + } + + /// 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) { + if let index { + var animated = false + if #available(iOS 16.0, *) { + animated = true + } + tableView.scrollToRow( + at: IndexPath(row: index, section: .zero), + at: position, + 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 != .zero && abs(items.count - itemCount) == 1 + ) + 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() + } +} 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/ChatListNavLink.swift b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift index 73c3c73556..c8a6efd282 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListNavLink.swift @@ -26,6 +26,7 @@ private let rowHeights: [DynamicTypeSize: CGFloat] = [ struct ChatListNavLink: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme @Environment(\.dynamicTypeSize) private var dynamicTypeSize @ObservedObject var chat: Chat @State private var showContactRequestDialog = false @@ -224,7 +225,7 @@ struct ChatListNavLink: View { } label: { Label("Join", systemImage: chat.chatInfo.incognito ? "theatermasks" : "ipad.and.arrow.forward") } - .tint(chat.chatInfo.incognito ? .indigo : .accentColor) + .tint(chat.chatInfo.incognito ? .indigo : theme.colors.primary) } @ViewBuilder private func markReadButton() -> some View { @@ -234,14 +235,14 @@ struct ChatListNavLink: View { } label: { Label("Read", systemImage: "checkmark") } - .tint(Color.accentColor) + .tint(theme.colors.primary) } else { Button { Task { await markChatUnread(chat) } } label: { Label("Unread", systemImage: "circlebadge.fill") } - .tint(Color.accentColor) + .tint(theme.colors.primary) } } @@ -306,7 +307,7 @@ struct ChatListNavLink: View { Button { Task { await acceptContactRequest(incognito: false, contactRequest: contactRequest) } } label: { Label("Accept", systemImage: "checkmark") } - .tint(.accentColor) + .tint(theme.colors.primary) Button { Task { await acceptContactRequest(incognito: true, contactRequest: contactRequest) } } label: { @@ -346,7 +347,7 @@ struct ChatListNavLink: View { } label: { Label("Name", systemImage: "pencil") } - .tint(.accentColor) + .tint(theme.colors.primary) } .frame(height: rowHeights[dynamicTypeSize]) .appSheet(isPresented: $showContactConnectionInfo) { @@ -354,6 +355,7 @@ struct ChatListNavLink: View { if case let .contactConnection(contactConnection) = chat.chatInfo { ContactConnectionInfo(contactConnection: contactConnection) .environment(\EnvironmentValues.refresh as! WritableKeyPath, nil) + .modifier(ThemedBackground(grouped: true)) } } } diff --git a/apps/ios/Shared/Views/ChatList/ChatListView.swift b/apps/ios/Shared/Views/ChatList/ChatListView.swift index 6bf63bb2e3..886b7465f3 100644 --- a/apps/ios/Shared/Views/ChatList/ChatListView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatListView.swift @@ -11,6 +11,7 @@ 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 @@ -86,6 +87,7 @@ struct ChatListView: View { )) } .listStyle(.plain) + .background(theme.colors.background) .navigationBarTitleDisplayMode(.inline) .navigationBarHidden(searchMode) .toolbar { @@ -115,9 +117,7 @@ struct ChatListView: View { HStack(spacing: 4) { Text("Chats") .font(.headline) - if chatModel.chats.count > 0 { - toggleFilterButton() - } + SubsStatusIndicator() } .frame(maxWidth: .infinity, alignment: .center) } @@ -131,15 +131,6 @@ struct ChatListView: View { } } - private func toggleFilterButton() -> some View { - Button { - showUnreadAndFavorites = !showUnreadAndFavorites - } label: { - Image(systemName: "line.3.horizontal.decrease.circle" + (showUnreadAndFavorites ? ".fill" : "")) - .foregroundColor(.accentColor) - } - } - @ViewBuilder private var chatList: some View { let cs = filteredChats() ZStack { @@ -154,12 +145,14 @@ struct ChatListView: View { searchChatFilteredBySimplexLink: $searchChatFilteredBySimplexLink ) .listRowSeparator(.hidden) + .listRowBackground(Color.clear) .frame(maxWidth: .infinity) } ForEach(cs, id: \.viewId) { chat in ChatListNavLink(chat: chat) .padding(.trailing, -16) .disabled(chatModel.chatRunning != true || chatModel.deletedChats.contains(chat.chatInfo.id)) + .listRowBackground(Color.clear) } .offset(x: -8) } @@ -171,7 +164,7 @@ struct ChatListView: View { } } if cs.isEmpty && !chatModel.chats.isEmpty { - Text("No filtered chats").foregroundColor(.secondary) + Text("No filtered chats").foregroundColor(theme.colors.secondary) } } } @@ -179,7 +172,7 @@ struct ChatListView: View { private func unreadBadge(_ text: Text? = Text(" "), size: CGFloat = 18) -> some View { Circle() .frame(width: size, height: size) - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) } private func onboardingButtons() -> some View { @@ -190,7 +183,7 @@ struct ChatListView: View { p.addLine(to: CGPoint(x: 0, y: 10)) p.addLine(to: CGPoint(x: 8, y: 0)) } - .fill(Color.accentColor) + .fill(theme.colors.primary) .frame(width: 20, height: 10) .padding(.trailing, 12) @@ -200,7 +193,7 @@ struct ChatListView: View { Spacer() Text("You have no chats") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .frame(maxWidth: .infinity) } .padding(.trailing, 6) @@ -213,16 +206,14 @@ struct ChatListView: View { .padding(.vertical, 10) .padding(.horizontal, 20) } - .background(Color.accentColor) + .background(theme.colors.primary) .foregroundColor(.white) .clipShape(RoundedRectangle(cornerRadius: 16)) } @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) } } @@ -274,17 +265,87 @@ struct ChatListView: View { } } +struct SubsStatusIndicator: View { + @State private var subs: SMPServerSubs = SMPServerSubs.newSMPServerSubs + @State private var sess: ServerSessions = ServerSessions.newServerSessions + @State private var timer: Timer? = nil + @State private var timerCounter = 0 + @State private var showServersSummary = false + + @AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false + + // Constants for the intervals + let initialInterval: TimeInterval = 1.0 + let regularInterval: TimeInterval = 3.0 + let initialPhaseDuration: TimeInterval = 10.0 // Duration for initial phase in seconds + + var body: some View { + Button { + showServersSummary = true + } label: { + HStack(spacing: 4) { + SubscriptionStatusIndicatorView(subs: subs, sess: sess) + if showSubscriptionPercentage { + SubscriptionStatusPercentageView(subs: subs, sess: sess) + } + } + } + .onAppear { + startInitialTimer() + } + .onDisappear { + stopTimer() + } + .sheet(isPresented: $showServersSummary) { + ServersSummaryView() + } + } + + private func startInitialTimer() { + timer = Timer.scheduledTimer(withTimeInterval: initialInterval, repeats: true) { _ in + getServersSummary() + timerCounter += 1 + // Switch to the regular timer after the initial phase + if timerCounter * Int(initialInterval) >= Int(initialPhaseDuration) { + switchToRegularTimer() + } + } + } + + func switchToRegularTimer() { + timer?.invalidate() + timer = Timer.scheduledTimer(withTimeInterval: regularInterval, repeats: true) { _ in + getServersSummary() + } + } + + func stopTimer() { + timer?.invalidate() + timer = nil + } + + private func getServersSummary() { + do { + let summ = try getAgentServersSummary() + (subs, sess) = (summ.allUsersSMP.smpTotals.subs, summ.allUsersSMP.smpTotals.sessions) + } catch let error { + logger.error("getAgentServersSummary error: \(responseError(error))") + } + } +} + 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 +353,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,48 +362,26 @@ 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 +415,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, diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index 4cfd5ae068..c1156225d8 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -11,10 +11,10 @@ import SimpleXChat struct ChatPreviewView: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme @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) @AppStorage(DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) private var showChatPreviews = true @@ -36,7 +36,7 @@ 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) @@ -94,9 +94,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,7 +108,7 @@ struct ChatPreviewView: View { private var verifiedIcon: Text { (Text(Image(systemName: "checkmark.shield")) + Text(" ")) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .baselineOffset(1) .kerning(-2) } @@ -133,11 +133,11 @@ struct ChatPreviewView: View { .foregroundColor(.white) .padding(.horizontal, 4) .frame(minWidth: 18, minHeight: 18) - .background(chat.chatInfo.ntfsEnabled || chat.chatInfo.chatType == .local ? Color.accentColor : Color.secondary) + .background(chat.chatInfo.ntfsEnabled || chat.chatInfo.chatType == .local ? theme.colors.primary : theme.colors.secondary) .cornerRadius(10) } else if !chat.chatInfo.ntfsEnabled && chat.chatInfo.chatType != .local { Image(systemName: "speaker.slash.fill") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } else if chat.chatInfo.chatSettings?.favorite ?? false { Image(systemName: "star.fill") .resizable() @@ -151,9 +151,9 @@ struct ChatPreviewView: View { 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 +172,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: attachment(), 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 @@ -206,7 +206,7 @@ struct ChatPreviewView: View { case let .direct(contact): if contact.activeConn == nil && contact.profile.contactLink != nil { chatPreviewInfoText("Tap to Connect") - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) } else if !contact.ready && contact.activeConn != nil { if contact.nextSendGrpInv { chatPreviewInfoText("send direct message") @@ -257,38 +257,38 @@ struct ChatPreviewView: View { case let .direct(contact): if contact.active && contact.activeConn != nil { switch (chatModel.contactNetworkStatus(contact)) { - case .connected: incognitoIcon(chat.chatInfo.incognito) + case .connected: incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary) case .error: Image(systemName: "exclamationmark.circle") .resizable() .scaledToFit() .frame(width: 17, height: 17) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) default: ProgressView() } } else { - incognitoIcon(chat.chatInfo.incognito) + incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary) } case .group: if progressByTimeout { ProgressView() } else { - incognitoIcon(chat.chatInfo.incognito) + incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary) } default: - incognitoIcon(chat.chatInfo.incognito) + incognitoIcon(chat.chatInfo.incognito, theme.colors.secondary) } } } -@ViewBuilder func incognitoIcon(_ incognito: Bool) -> some View { +@ViewBuilder func incognitoIcon(_ incognito: Bool, _ secondaryColor: Color) -> some View { if incognito { Image(systemName: "theatermasks") .resizable() .scaledToFit() .frame(width: 22, height: 22) - .foregroundColor(.secondary) + .foregroundColor(secondaryColor) } else { EmptyView() } diff --git a/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift b/apps/ios/Shared/Views/ChatList/ContactConnectionInfo.swift index 42e90232d6..b7e641a338 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? @@ -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 { @@ -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..bb224b7844 100644 --- a/apps/ios/Shared/Views/ChatList/ContactConnectionView.swift +++ b/apps/ios/Shared/Views/ChatList/ContactConnectionView.swift @@ -12,6 +12,7 @@ import SimpleXChat struct ContactConnectionView: View { @EnvironmentObject var m: ChatModel @ObservedObject var chat: Chat + @EnvironmentObject var theme: AppTheme @State private var localAlias = "" @FocusState private var aliasTextFieldFocused: Bool @State private var showContactConnectionInfo = false @@ -29,7 +30,7 @@ struct ContactConnectionView: View { .resizable() .scaledToFill() .frame(width: 48, height: 48) - .foregroundColor(Color(uiColor: .secondarySystemBackground)) + .foregroundColor(Color(uiColor: .tertiarySystemGroupedBackground).asAnotherColorFromSecondaryVariant(theme)) .onTapGesture { showContactConnectionInfo = true } } .frame(width: 63, height: 63) @@ -41,7 +42,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 +55,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) .padding(.top, 26) .frame(maxWidth: .infinity, alignment: .trailing) } diff --git a/apps/ios/Shared/Views/ChatList/ContactRequestView.swift b/apps/ios/Shared/Views/ChatList/ContactRequestView.swift index dacf51a5e8..e36a2f7596 100644 --- a/apps/ios/Shared/Views/ChatList/ContactRequestView.swift +++ b/apps/ios/Shared/Views/ChatList/ContactRequestView.swift @@ -11,6 +11,7 @@ import SimpleXChat struct ContactRequestView: View { @EnvironmentObject var chatModel: ChatModel + @EnvironmentObject var theme: AppTheme var contactRequest: UserContactRequest @ObservedObject var chat: Chat @@ -23,7 +24,7 @@ struct ContactRequestView: View { Text(contactRequest.chatViewName) .font(.title3) .fontWeight(.bold) - .foregroundColor(.accentColor) + .foregroundColor(theme.colors.primary) .padding(.leading, 8) .frame(alignment: .topLeading) Spacer() @@ -32,7 +33,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/ServersSummaryView.swift b/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift new file mode 100644 index 0000000000..1fb889f863 --- /dev/null +++ b/apps/ios/Shared/Views/ChatList/ServersSummaryView.swift @@ -0,0 +1,738 @@ +// +// 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 + @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 + getServersSummary() + } + } + + 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 users").tag(PresentedUserCategory.allUsers) + Text("Current user").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") + } + } + + private func smpSubsSection(_ totals: SMPTotals) -> some View { + Section { + infoRow("Connections subscribed", numOrDash(totals.subs.ssActive)) + infoRow("Total", numOrDash(totals.subs.total)) + } header: { + HStack { + Text("Message subscriptions") + SubscriptionStatusIndicatorView(subs: totals.subs, sess: totals.sessions) + if showSubscriptionPercentage { + SubscriptionStatusPercentageView(subs: totals.subs, sess: totals.sessions) + } + } + } + } + + 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, sess: srvSumm.sessionsOrNew) + } + SubscriptionStatusIndicatorView(subs: subs, sess: srvSumm.sessionsOrNew) + } 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") + } + } + + private func getServersSummary() { + do { + serversSummary = try getAgentServersSummary() + } catch let error { + logger.error("getAgentServersSummary error: \(responseError(error))") + } + } +} + +struct SubscriptionStatusIndicatorView: View { + @EnvironmentObject var m: ChatModel + var subs: SMPServerSubs + var sess: ServerSessions + + var body: some View { + let onionHosts = networkUseOnionHostsGroupDefault.get() + let (color, variableValue, opacity, _) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, sess) + 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 sess: ServerSessions + + var body: some View { + let onionHosts = networkUseOnionHostsGroupDefault.get() + let (_, _, _, statusPercent) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, sess) + Text(verbatim: "\(Int(floor(statusPercent * 100)))%") + .foregroundColor(.secondary) + .font(.caption) + } +} + +func subscriptionStatusColorAndPercentage(_ online: Bool, _ onionHosts: OnionHosts, _ subs: SMPServerSubs, _ sess: ServerSessions) -> (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 && subs.total > 0 + ? ( + subs.ssActive == 0 + ? ( + sess.ssConnected == 0 ? noConnColorAndPercent : (activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) + ) + : ( // ssActive > 0 + sess.ssConnected == 0 + ? (.orange, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) // This would mean implementation error + : (activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) + ) + ) + : noConnColorAndPercent +} + +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("Connections subscribed", numOrDash(subs.ssActive)) + infoRow("Pending", numOrDash(subs.ssPending)) + infoRow("Total", numOrDash(subs.total)) + reconnectButton() + } header: { + HStack { + Text("Message subscriptions") + SubscriptionStatusIndicatorView(subs: subs, sess: summary.sessionsOrNew) + if showSubscriptionPercentage { + SubscriptionStatusPercentageView(subs: subs, sess: summary.sessionsOrNew) + } + } + } + } + + 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 { + 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)) + } header: { + Text("Connections") + } 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..eeb7bf14f4 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) ) @@ -131,13 +125,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 +139,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 +150,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/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/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index 2e0cd7738f..58000a7ee7 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -41,6 +41,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 +83,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 +108,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 +137,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 +146,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 +191,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) } } } @@ -355,6 +367,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) diff --git a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift index ae6af24f53..6d3026f11f 100644 --- a/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift +++ b/apps/ios/Shared/Views/Database/MigrateToAppGroupView.swift @@ -189,6 +189,7 @@ struct MigrateToAppGroupView: View { Task { do { try apiSaveAppSettings(settings: AppSettings.current.prepareForExport()) + try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true) try await apiExportArchive(config: config) await MainActor.run { setV3DBMigration(.exported) } } catch let error { @@ -231,6 +232,7 @@ func exportChatArchive(_ storagePath: URL? = nil) async throws -> URL { if !ChatModel.shared.chatDbChanged { try apiSaveAppSettings(settings: AppSettings.current.prepareForExport()) } + try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true) try await apiExportArchive(config: config) if storagePath == nil { deleteOldArchive() diff --git a/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift b/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift index 0180b066ab..844b5ab4d3 100644 --- a/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift +++ b/apps/ios/Shared/Views/Helpers/ChatInfoImage.swift @@ -10,7 +10,7 @@ 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) @@ -24,8 +24,7 @@ struct ChatInfoImage: View { 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, 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..6eef843d37 --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/ChatWallpaper.swift @@ -0,0 +1,102 @@ +// +// 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 { + @EnvironmentObject var theme: AppTheme + var image: Image + var imageType: WallpaperType + var background: Color + var tint: Color + + func body(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) { + image.shading = .color(tint) + let scale = imageScale * 1.57 // for some reason a wallpaper on iOS looks smaller than on Android + 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: () + } + } + ) + } +} + +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 })?.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/ProfileImage.swift b/apps/ios/Shared/Views/Helpers/ProfileImage.swift index 6b8439504a..0a0aab6253 100644 --- a/apps/ios/Shared/Views/Helpers/ProfileImage.swift +++ b/apps/ios/Shared/Views/Helpers/ProfileImage.swift @@ -12,10 +12,12 @@ 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 @AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner var body: some View { @@ -24,10 +26,12 @@ struct ProfileImage: View { let uiImage = UIImage(data: data) { clipProfileImage(Image(uiImage: uiImage), size: size, radius: radius) } 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)) } } } @@ -51,6 +55,29 @@ private let radiusFactor = (1 - squareToCircleRatio) / 50 } } + +extension Color { + func asAnotherColorFromSecondary(_ theme: AppTheme) -> Color { + return self + } + + 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 + } + } +} + struct ProfileImage_Previews: PreviewProvider { static var previews: some View { ProfileImage(imageStr: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAKHGlDQ1BJQ0MgUHJvZmlsZQAASImFVgdUVNcWve9Nb7QZeu9NehtAem/Sq6gMQ28OQxWxgAQjEFFEREARNFQFg1KjiIhiIQgoYA9IEFBisCAq6OQNJNH4//r/zDpz9ttzz7n73ffWmg0A6QCDxYqD+QCIT0hmezlYywQEBsngngEYCAIy0AC6DGYSy8rDwxUg8Xf9d7wbAxC33tHgzvrP3/9nCISFJzEBgIIRTGey2MkILkawT1oyi4tnEUxjI6IQvMLFkauYqxjQQtewwuoaHy8bBNMBwJMZDHYkAERbhJdJZUYic4hhCNZOCItOQDB3vjkzioFwxLsIXhcRl5IOAImrRzs+fivCk7QRrIL0shAcwNUW+tX8yH/tFfrPXgxG5D84Pi6F+dc9ck+HHJ7g641UMSQlQATQBHEgBaQDGcACbLAVYaIRJhx5Dv+9j77aZ4OsZIFtSEc0iARRIBnpt/9qlvfqpGSQBhjImnCEcUU+NtxnujZy4fbqVEiU/wuXdQyA9S0cDqfzC+e2F4DzyLkSB79wyi0A8KoBcL2GmcJOXePQ3C8MIAJeQAOiQArIAxXuWwMMgSmwBHbAGbgDHxAINgMmojceUZUGMkEWyAX54AA4DMpAJTgJ6sAZ0ALawQVwGVwDt8AQGAUPwQSYBi/AAngHliEIwkEUiAqJQtKQIqQO6UJ0yByyg1whLygQCoEioQQoBcqE9kD5UBFUBlVB9dBPUCd0GboBDUP3oUloDnoNfYRRMBmmwZKwEqwF02Er2AX2gTfBkXAinAHnwPvhUrgaPg23wZfhW/AoPAG/gBdRAEVCCaFkURooOsoG5Y4KQkWg2KidqDxUCaoa1YTqQvWj7qAmUPOoD2gsmoqWQWugTdGOaF80E52I3okuQJeh69Bt6D70HfQkegH9GUPBSGDUMSYYJ0wAJhKThsnFlGBqMK2Yq5hRzDTmHRaLFcIqY42wjthAbAx2O7YAewzbjO3BDmOnsIs4HE4Up44zw7njGLhkXC7uKO407hJuBDeNe48n4aXxunh7fBA+AZ+NL8E34LvxI/gZ/DKBj6BIMCG4E8II2wiFhFOELsJtwjRhmchPVCaaEX2IMcQsYimxiXiV+Ij4hkQiyZGMSZ6kaNJuUinpLOk6aZL0gSxAViPbkIPJKeT95FpyD/k++Q2FQlGiWFKCKMmU/ZR6yhXKE8p7HiqPJo8TTxjPLp5ynjaeEZ6XvAReRV4r3s28GbwlvOd4b/PO8xH4lPhs+Bh8O/nK+Tr5xvkW+an8Ovzu/PH8BfwN/Df4ZwVwAkoCdgJhAjkCJwWuCExRUVR5qg2VSd1DPUW9Sp2mYWnKNCdaDC2fdoY2SFsQFBDUF/QTTBcsF7woOCGEElISchKKEyoUahEaE/ooLClsJRwuvE+4SXhEeElEXMRSJFwkT6RZZFTko6iMqJ1orOhB0XbRx2JoMTUxT7E0seNiV8XmxWnipuJM8TzxFvEHErCEmoSXxHaJkxIDEouSUpIOkizJo5JXJOelhKQspWKkiqW6peakqdLm0tHSxdKXpJ/LCMpYycTJlMr0ySzISsg6yqbIVskOyi7LKcv5ymXLNcs9lifK0+Uj5Ivle+UXFKQV3BQyFRoVHigSFOmKUYpHFPsVl5SUlfyV9iq1K80qiyg7KWcoNyo/UqGoWKgkqlSr3FXFqtJVY1WPqQ6pwWoGalFq5Wq31WF1Q/Vo9WPqw+sw64zXJayrXjeuQdaw0kjVaNSY1BTSdNXM1mzXfKmloBWkdVCrX+uztoF2nPYp7Yc6AjrOOtk6XTqvddV0mbrlunf1KHr2erv0OvRe6avrh+sf179nQDVwM9hr0GvwydDIkG3YZDhnpGAUYlRhNE6n0T3oBfTrxhhja+NdxheMP5gYmiSbtJj8YaphGmvaYDq7Xnl9+PpT66fM5MwYZlVmE+Yy5iHmJ8wnLGQtGBbVFk8t5S3DLGssZ6xUrWKsTlu9tNa2Zlu3Wi/ZmNjssOmxRdk62ObZDtoJ2Pnaldk9sZezj7RvtF9wMHDY7tDjiHF0cTzoOO4k6cR0qndacDZy3uHc50J28XYpc3nqqubKdu1yg92c3Q65PdqguCFhQ7s7cHdyP+T+2EPZI9HjZ0+sp4dnueczLx2vTK9+b6r3Fu8G73c+1j6FPg99VXxTfHv9eP2C/er9lvxt/Yv8JwK0AnYE3AoUC4wO7AjCBfkF1QQtbrTbeHjjdLBBcG7w2CblTembbmwW2xy3+eIW3i2MLedCMCH+IQ0hKwx3RjVjMdQptCJ0gWnDPMJ8EWYZVhw2F24WXhQ+E2EWURQxG2kWeShyLsoiqiRqPtomuiz6VYxjTGXMUqx7bG0sJ84/rjkeHx8S35kgkBCb0LdVamv61mGWOiuXNZFokng4cYHtwq5JgpI2JXUk05A/0oEUlZTvUiZTzVPLU9+n+aWdS+dPT0gf2Ka2bd+2mQz7jB+3o7czt/dmymZmZU7usNpRtRPaGbqzd5f8rpxd07sddtdlEbNis37J1s4uyn67x39PV45kzu6cqe8cvmvM5cll547vNd1b+T36++jvB/fp7Tu673NeWN7NfO38kvyVAmbBzR90fij9gbM/Yv9goWHh8QPYAwkHxg5aHKwr4i/KKJo65HaorVimOK/47eEth2+U6JdUHiEeSTkyUepa2nFU4eiBoytlUWWj5dblzRUSFfsqlo6FHRs5bnm8qVKyMr/y44noE/eqHKraqpWqS05iT6aefHbK71T/j/Qf62vEavJrPtUm1E7UedX11RvV1zdINBQ2wo0pjXOng08PnbE909Gk0VTVLNScfxacTTn7/KeQn8ZaXFp6z9HPNZ1XPF/RSm3Na4PatrUttEe1T3QEdgx3Onf2dpl2tf6s+XPtBdkL5RcFLxZ2E7tzujmXMi4t9rB65i9HXp7q3dL78ErAlbt9nn2DV12uXr9mf+1Kv1X/petm1y/cMLnReZN+s/2W4a22AYOB1l8MfmkdNBxsu210u2PIeKhreP1w94jFyOU7tneu3XW6e2t0w+jwmO/YvfHg8Yl7Yfdm78fdf/Ug9cHyw92PMI/yHvM9Lnki8aT6V9VfmycMJy5O2k4OPPV++nCKOfXit6TfVqZznlGelcxIz9TP6s5emLOfG3q+8fn0C9aL5fnc3/l/r3ip8vL8H5Z/DCwELEy/Yr/ivC54I/qm9q3+295Fj8Un7+LfLS/lvRd9X/eB/qH/o//HmeW0FdxK6SfVT12fXT4/4sRzOCwGm7FqBVBIwhERALyuBYASCAB1CPEPG9f8119+BvrK2fyNwVndL5jhvubRVsMQgCakeCFp04OsQ1LJEgAe5NodqT6WANbT+yf/iqQIPd21PXgaAcDJcjivtwJAQHLFgcNZ9uBwPlUgYhHf1z37f7V9g9e8ITewiP88wfWIYET6HPg21nzjV2fybQVcxfrg2/onng/F50lD/ccAAAA4ZVhJZk1NACoAAAAIAAGHaQAEAAAAAQAAABoAAAAAAAKgAgAEAAAAAQAAABigAwAEAAAAAQAAABgAAAAAwf1XlwAAAaNJREFUSA3FlT1LA0EQQBN/gYUYRTksJZVgEbCR/D+7QMr8ABtttBBCsLGzsLG2sxaxED/ie4d77u0dyaE5HHjczn7MzO7M7nU6/yXz+bwLhzCCjTQO+rZhDH3opuNLdRYN4RHe4RIKJ7R34Ro+4AEGSw2mE1iUwT18gpI74WvkGlccu4XNdH0jnYU7cAUacidn37qR23cOxc4aGU0nYUAn7iSWEHkz46w0ocdQu1X6B/AMQZ5o7KfBqNOfwRH8JB7FajGhnmcpKvQe3MEbvILiDm5gPXaCHnZr4vvFGMoEKudKn8YvQIOOe+YzCPop7dwJ3zRfJ7GDuso4YJGRa0yZgg4tUaNXdGrbuZWKKxzYYEJc2xp9AUUjGt8KC2jvgYadF8+10vJyDnNLXwbdiWUZi0fUK01Eoc+AZhCLZVzK4Vq6sDUdz+0dEcbbTTIOJmAyTVhx/WmvrExbv2jtPhWLKodjCtefZiEeZeVZWWSndgwj6fVf3XON8Qwq15++uoqrfYVrow6dGBpCq79ME291jaB0/Q2CPncyht/99MNO/vr9AqW/CGi8sJqbAAAAAElFTkSuQmCC", size: 63) 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/ViewModifiers.swift b/apps/ios/Shared/Views/Helpers/ViewModifiers.swift new file mode 100644 index 0000000000..7e2655f4f7 --- /dev/null +++ b/apps/ios/Shared/Views/Helpers/ViewModifiers.swift @@ -0,0 +1,19 @@ +// +// 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: @autoclosure () -> Bool, transform: (Self) -> Content) -> some View { + if condition() { + transform(self) + } else { + self + } + } +} 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..ec2ce883c5 100644 --- a/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift +++ b/apps/ios/Shared/Views/Migration/MigrateFromDevice.swift @@ -53,6 +53,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 @@ -177,6 +178,7 @@ struct MigrateFromDevice: View { List { Section {} header: { Text("Stopping chat") + .foregroundColor(theme.colors.secondary) } } progressView() @@ -188,14 +190,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 +218,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 +238,7 @@ struct MigrateFromDevice: View { List { Section {} header: { Text("Archiving database") + .foregroundColor(theme.colors.secondary) } } progressView() @@ -246,10 +253,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 +270,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 +293,7 @@ struct MigrateFromDevice: View { List { Section {} header: { Text("Creating archive link") + .foregroundColor(theme.colors.secondary) } } progressView() @@ -293,13 +304,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 +318,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 +334,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 +346,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 +393,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 +403,7 @@ struct MigrateFromDevice: View { Text(title) .font(.system(size: 54)) .bold() - .foregroundColor(.accentColor) + .foregroundColor(primaryColor) Text(description) .font(.title3) @@ -398,7 +412,7 @@ struct MigrateFromDevice: View { Circle() .trim(from: 0, to: CGFloat(value)) .stroke( - Color.accentColor, + primaryColor, style: StrokeStyle(lineWidth: 27) ) .rotationEffect(.degrees(180)) @@ -590,6 +604,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 +627,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 { diff --git a/apps/ios/Shared/Views/Migration/MigrateToDevice.swift b/apps/ios/Shared/Views/Migration/MigrateToDevice.swift index e290537b46..107785e336 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) } } @@ -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..5e6d44f686 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()) } } diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index 4b272f4caa..5c8a88bd8f 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? @@ -70,7 +71,7 @@ struct AddGroupView: View { 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 +96,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 +105,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 +146,7 @@ struct AddGroupView: View { profile.image = nil } } + .modifier(ThemedBackground(grouped: true)) } func groupNameTextField() -> some View { @@ -156,7 +159,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) diff --git a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift index c3452ce18d..3be1095bfd 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatMenuButton.swift @@ -10,6 +10,7 @@ import SwiftUI enum NewChatMenuOption: Identifiable { case newContact + case scanPaste case newGroup var id: Self { self } @@ -25,6 +26,11 @@ struct NewChatMenuButton: View { } label: { Text("Add contact") } + Button { + newChatMenuOption = .scanPaste + } label: { + Text("Scan / Paste link") + } Button { newChatMenuOption = .newGroup } label: { @@ -39,6 +45,7 @@ struct NewChatMenuButton: View { .sheet(item: $newChatMenuOption) { opt in switch opt { case .newContact: NewChatView(selection: .invite) + case .scanPaste: NewChatView(selection: .connect, showQRCodeScanner: true) case .newGroup: AddGroupView() } } diff --git a/apps/ios/Shared/Views/NewChat/NewChatView.swift b/apps/ios/Shared/Views/NewChat/NewChatView.swift index 4b1f72345a..df50bf9df2 100644 --- a/apps/ios/Shared/Views/NewChat/NewChatView.swift +++ b/apps/ios/Shared/Views/NewChat/NewChatView.swift @@ -10,6 +10,7 @@ import SwiftUI import SimpleXChat import CodeScanner import AVFoundation +import SimpleXChat struct SomeAlert: Identifiable { var alert: Alert @@ -37,6 +38,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 @@ -89,7 +91,7 @@ struct NewChatView: View { .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) @@ -108,7 +110,7 @@ struct NewChatView: View { } ) } - .background(Color(.systemGroupedBackground)) + .modifier(ThemedBackground(grouped: true)) .onChange(of: invitationUsed) { used in if used && !(m.showingInvitation?.connChatUsed ?? true) { m.markShowingInvitationUsed() @@ -202,6 +204,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 @@ -209,7 +212,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)) @@ -220,6 +223,7 @@ private struct InviteView: View { IncognitoToggle(incognitoEnabled: $incognitoDefault) } footer: { sharedProfileInfo(incognitoDefault) + .foregroundColor(theme.colors.secondary) } } .onChange(of: incognitoDefault) { incognito in @@ -256,7 +260,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( @@ -279,6 +283,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? @@ -286,10 +291,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) } } @@ -483,6 +488,7 @@ func strHasSingleSimplexLink(_ str: String) -> FormattedText? { } struct IncognitoToggle: View { + @EnvironmentObject var theme: AppTheme @Binding var incognitoEnabled: Bool @State private var showIncognitoSheet = false @@ -490,13 +496,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 { diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift index 0ee6baa765..a62f609833 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,10 +65,12 @@ 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) { @@ -78,6 +83,7 @@ struct CreateProfile: View { 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 +95,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) 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 86e14b74f7..74390c97e1 100644 --- a/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift +++ b/apps/ios/Shared/Views/Onboarding/WhatsNewView.swift @@ -444,6 +444,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 @@ -455,7 +456,7 @@ struct WhatsNewView: View { VStack(alignment: .leading, spacing: 16) { Text("New in \(v.version)") .font(.title) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .frame(maxWidth: .infinity) .padding(.vertical) ForEach(v.features, id: \.icon) { f in @@ -499,7 +500,7 @@ struct WhatsNewView: View { HStack(alignment: .center, spacing: 4) { Image(systemName: icon) .symbolRenderingMode(.monochrome) - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .frame(minWidth: 30, alignment: .center) Text(title).font(.title3).bold() } diff --git a/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift b/apps/ios/Shared/Views/RemoteAccess/ConnectDesktopView.swift index c749e09ca8..30d200b6e3 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 @@ -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,18 +179,19 @@ struct ConnectDesktopView: View { } } .navigationTitle("Connect to desktop") + .modifier(ThemedBackground(grouped: true)) } private func connectingDesktopView(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> some View { ZStack { List { - Section("Connecting to desktop") { + Section(header: Text("Connecting to desktop").foregroundColor(theme.colors.secondary)) { ctrlDeviceNameText(session, rc) ctrlDeviceVersionText(session) } if let sessCode = session.sessionCode { - Section("Session code") { + Section(header: Text("Session code").foregroundColor(theme.colors.secondary)) { sessionCodeText(sessCode) } } @@ -202,14 +204,15 @@ struct ConnectDesktopView: View { ProgressView().scaleEffect(2) } + .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() @@ -219,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 { @@ -246,6 +250,7 @@ struct ConnectDesktopView: View { } } .navigationTitle("Found desktop") + .modifier(ThemedBackground(grouped: true)) if compatible && connectRemoteViaMulticastAuto { v.onAppear { confirmKnownDesktop(rc) } @@ -256,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) @@ -275,6 +280,7 @@ struct ConnectDesktopView: View { } } .navigationTitle("Verify connection") + .modifier(ThemedBackground(grouped: true)) } private func ctrlDeviceNameText(_ session: RemoteCtrlSession, _ rc: RemoteCtrlInfo?) -> Text { @@ -296,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) } } @@ -312,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 { @@ -333,15 +341,15 @@ struct ConnectDesktopView: View { } } } - + private func scanDesctopAddressView() -> some View { - Section("Scan QR code from desktop") { + 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 ?? "" @@ -354,7 +362,7 @@ struct ConnectDesktopView: View { Text(sessionAddress).lineLimit(1) Spacer() Image(systemName: "multiply.circle.fill") - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) .onTapGesture { sessionAddress = "" } } } @@ -369,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) } @@ -380,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 { @@ -389,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 e8bb6c6444..6dda7bf799 100644 --- a/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AdvancedNetworkSettings.swift @@ -24,6 +24,7 @@ enum NetworkSettingsAlert: Identifiable { } struct AdvancedNetworkSettings: View { + @EnvironmentObject var theme: AppTheme @State private var netCfg = NetCfg.defaults @State private var currentNetCfg = NetCfg.defaults @State private var cfgLoaded = false @@ -69,10 +70,11 @@ struct AdvancedNetworkSettings: View { Text("TCP_KEEPINTVL") Text("TCP_KEEPCNT") } - .foregroundColor(.secondary) + .foregroundColor(theme.colors.secondary) } } header: { Text("") + .foregroundColor(theme.colors.secondary) } footer: { HStack { Button { diff --git a/apps/ios/Shared/Views/UserSettings/AppSettings.swift b/apps/ios/Shared/Views/UserSettings/AppSettings.swift index 299c96626a..8c68d70526 100644 --- a/apps/ios/Shared/Views/UserSettings/AppSettings.swift +++ b/apps/ios/Shared/Views/UserSettings/AppSettings.swift @@ -44,6 +44,11 @@ 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 { 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) } } public static var current: AppSettings { @@ -69,6 +74,11 @@ 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.float(forKey: DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) + c.uiColorScheme = currentThemeDefault.get() + c.uiDarkColorScheme = systemDarkThemeDefault.get() + c.uiCurrentThemeIds = currentThemeIdsDefault.get() + c.uiThemes = themeOverridesDefault.get() return c } } diff --git a/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift b/apps/ios/Shared/Views/UserSettings/AppearanceSettings.swift index b91d2c9369..18cf5fa199 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,12 +25,30 @@ 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 + @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{ List { @@ -39,15 +62,111 @@ 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 { + 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) { _ in + 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 +180,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 +290,737 @@ struct AppearanceSettings: View { } } +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 +1031,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 +1111,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..e55ff5cfb6 100644 --- a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift +++ b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift @@ -10,6 +10,7 @@ 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 @Environment(\.colorScheme) var colorScheme @@ -23,24 +24,26 @@ 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") { + settingsRow("internaldrive", color: theme.colors.secondary) { Toggle("Confirm database upgrades", isOn: $confirmDatabaseUpgrades) } - 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) } } } 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 6d849479e5..b07f0f6a13 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift @@ -29,8 +29,10 @@ private enum NetworkAlert: Identifiable { struct NetworkAndServers: View { @EnvironmentObject var m: ChatModel + @EnvironmentObject var theme: AppTheme @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false - @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = true + @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false + @AppStorage(DEFAULT_SHOW_SUBSCRIPTION_PERCENTAGE) private var showSubscriptionPercentage = false @State private var cfgLoaded = false @State private var currentNetCfg = NetCfg.defaults @State private var netCfg = NetCfg.defaults @@ -47,6 +49,7 @@ struct NetworkAndServers: View { NavigationLink { ProtocolServersView(serverProtocol: .smp) .navigationTitle("Your SMP servers") + .modifier(ThemedBackground(grouped: true)) } label: { Text("SMP servers") } @@ -54,10 +57,13 @@ struct NetworkAndServers: View { NavigationLink { ProtocolServersView(serverProtocol: .xftp) .navigationTitle("Your XFTP servers") + .modifier(ThemedBackground(grouped: true)) } label: { Text("XFTP servers") } + Toggle("Subscription percentage", isOn: $showSubscriptionPercentage) + Picker("Use .onion hosts", selection: $onionHosts) { ForEach(OnionHosts.values, id: \.self) { Text($0.text) } } @@ -73,13 +79,16 @@ struct NetworkAndServers: View { NavigationLink { AdvancedNetworkSettings() .navigationTitle("Network settings") + .modifier(ThemedBackground(grouped: true)) } label: { Text("Advanced network settings") } } header: { Text("Messages & files") + .foregroundColor(theme.colors.secondary) } footer: { Text("Using .onion hosts requires compatible VPN provider.") + .foregroundColor(theme.colors.secondary) } Section { @@ -97,6 +106,7 @@ struct NetworkAndServers: View { 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.") @@ -104,18 +114,20 @@ struct NetworkAndServers: View { Text("Show → on messages sent via private routing.") } } + .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() 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 01f31d66b4..879ee301f2 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -11,6 +11,7 @@ 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 @@ -44,34 +45,35 @@ 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("network") { + settingsRow("network", color: theme.colors.secondary) { Toggle("Send link previews", isOn: $useLinkPreviews) } - 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 @@ -80,7 +82,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]) @@ -95,48 +97,54 @@ struct PrivacySettings: View { } } header: { Text("Chats") + .foregroundColor(theme.colors.secondary) } Section { - settingsRow("lock.doc") { + settingsRow("lock.doc", color: theme.colors.secondary) { Toggle("Encrypt local files", isOn: $encryptLocalFiles) .onChange(of: encryptLocalFiles) { setEncryptLocalFiles($0) } } - settingsRow("photo") { + settingsRow("photo", color: theme.colors.secondary) { Toggle("Auto-accept images", isOn: $autoAcceptImages) .onChange(of: autoAcceptImages) { privacyAcceptImagesGroupDefault.set($0) } } - settingsRow("network.badge.shield.half.filled") { + 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") { + 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) { @@ -332,6 +340,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 @@ -411,12 +420,12 @@ struct SimplexLockView: View { } 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 { diff --git a/apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift b/apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift index 6702ab7ce8..f4e5459613 100644 --- a/apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift +++ b/apps/ios/Shared/Views/UserSettings/ProtocolServerView.swift @@ -11,9 +11,11 @@ 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 + @State var serverEnabled: Bool @State private var showTestFailure = false @State private var testing = false @State private var testFailure: ProtocolTestFailure? @@ -49,7 +51,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 +77,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 +86,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 +96,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 +113,10 @@ struct ProtocolServerView: View { Spacer() showTestStatus(server: serverToEdit) } - Toggle("Use for new connections", isOn: $serverToEdit.enabled) + Toggle("Use for new connections", isOn: $serverEnabled) + .onChange(of: serverEnabled) { enabled in + serverToEdit.enabled = enabled ? .enabled : .disabled + } } } } @@ -179,7 +185,8 @@ struct ProtocolServerView_Previews: PreviewProvider { ProtocolServerView( serverProtocol: .smp, server: Binding.constant(ServerCfg.sampleData.custom), - serverToEdit: ServerCfg.sampleData.custom + serverToEdit: ServerCfg.sampleData.custom, + serverEnabled: true ) } } diff --git a/apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift b/apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift index b9163d4bad..857bab69fb 100644 --- a/apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift +++ b/apps/ios/Shared/Views/UserSettings/ProtocolServersView.swift @@ -14,6 +14,7 @@ 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] = [] @@ -67,8 +68,10 @@ struct ProtocolServersView: View { } } header: { Text("\(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) } @@ -94,6 +97,7 @@ struct ProtocolServersView: View { } .sheet(isPresented: $showScanProtoServer) { ScanProtocolServer(servers: $servers) + .modifier(ThemedBackground(grouped: true)) } .modifier(BackButton(disabled: Binding.constant(false)) { if saveDisabled { @@ -159,7 +163,7 @@ struct ProtocolServersView: View { } private var allServersDisabled: Bool { - servers.allSatisfy { !$0.enabled } + servers.allSatisfy { $0.enabled != .enabled } } private func protocolServerView(_ server: Binding) -> some View { @@ -168,9 +172,11 @@ struct ProtocolServersView: View { ProtocolServerView( serverProtocol: serverProtocol, server: server, - serverToEdit: srv + serverToEdit: srv, + serverEnabled: srv.enabled == .enabled ) .navigationBarTitle(srv.preset ? "Preset server" : "Your server") + .modifier(ThemedBackground(grouped: true)) .navigationBarTitleDisplayMode(.large) } label: { let address = parseServerAddress(srv.server) @@ -181,8 +187,8 @@ struct ProtocolServersView: View { invalidServer() } else if !uniqueAddress(srv, address) { Image(systemName: "exclamationmark.circle").foregroundColor(.red) - } else if !srv.enabled { - Image(systemName: "slash.circle").foregroundColor(.secondary) + } else if srv.enabled != .enabled { + Image(systemName: "slash.circle").foregroundColor(theme.colors.secondary) } else { showTestStatus(server: srv) } @@ -194,10 +200,10 @@ struct ProtocolServersView: View { .padding(.trailing, 4) let v = Text(address?.hostnames.first ?? srv.server).lineLimit(1) - if srv.enabled { + if srv.enabled == .enabled { v } else { - v.foregroundColor(.secondary) + v.foregroundColor(theme.colors.secondary) } } } @@ -235,7 +241,7 @@ struct ProtocolServersView: View { private func addAllPresets() { for srv in presetServers { if !hasPreset(srv) { - servers.append(ServerCfg(server: srv, preset: true, tested: nil, enabled: true)) + servers.append(ServerCfg(server: srv, preset: true, tested: nil, enabled: .enabled)) } } } @@ -260,7 +266,7 @@ 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 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)) } +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 @@ -175,11 +242,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) @@ -189,7 +257,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") } } @@ -197,39 +265,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() @@ -241,24 +313,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) @@ -266,8 +341,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) } @@ -275,30 +351,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 { @@ -307,12 +386,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) @@ -324,27 +403,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 @@ -356,8 +439,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") @@ -388,13 +472,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 @@ -403,10 +487,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) @@ -414,7 +498,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) @@ -425,7 +509,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..a22a10cd9c 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() @@ -110,6 +111,7 @@ struct UserAddressView: View { createAddressButton() } footer: { Text("Create an address to let people connect with you.") + .foregroundColor(theme.colors.secondary) } Section { @@ -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 5da7c8e877..13b9b2b097 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 @@ -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) @@ -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/bg.xcloc/Localized Contents/bg.xliff b/apps/ios/SimpleX Localizations/bg.xcloc/Localized Contents/bg.xliff index 673d6668f9..7ac1db1557 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 @@
- +
@@ -557,9 +557,8 @@ Повече за SimpleX адреса No comment provided by engineer. - - Accent color - Основен цвят + + Accent No comment provided by engineer. @@ -583,6 +582,14 @@ Приеми инкогнито accept contact request via notification + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + 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. Добавете адрес към вашия профил, така че вашите контакти да могат да го споделят с други хора. Актуализацията на профила ще бъде изпратена до вашите контакти. @@ -623,6 +630,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 +667,10 @@ Разширени мрежови настройки No comment provided by engineer. + + Advanced settings + No comment provided by engineer. + All app data is deleted. Всички данни от приложението бяха изтрити. @@ -663,6 +686,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 +710,10 @@ Всички нови съобщения от %@ ще бъдат скрити! No comment provided by engineer. + + All users + No comment provided by engineer. + All your contacts will remain connected. Всички ваши контакти ще останат свързани. @@ -881,6 +912,10 @@ Приложи No comment provided by engineer. + + Apply to + No comment provided by engineer. + Archive and upload Архивиране и качване @@ -956,6 +991,10 @@ Назад No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address Грешен адрес на настолното устройство @@ -981,6 +1020,10 @@ По-добри съобщения No comment provided by engineer. + + Black + No comment provided by engineer. + Block Блокирай @@ -1091,6 +1134,10 @@ Няма достъп до Keychain за запазване на паролата за базата данни No comment provided by engineer. + + Cannot forward message + No comment provided by engineer. + Cannot receive file Файлът не може да бъде получен @@ -1161,6 +1208,10 @@ Архив на чата No comment provided by engineer. + + Chat colors + No comment provided by engineer. + Chat console Конзола @@ -1206,6 +1257,10 @@ Чат настройки No comment provided by engineer. + + Chat theme + No comment provided by engineer. + Chats Чатове @@ -1236,6 +1291,18 @@ Избери от библиотеката 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 Изчисти @@ -1261,9 +1328,8 @@ Изчисти проверката No comment provided by engineer. - - Colors - Цветове + + Color mode No comment provided by engineer. @@ -1276,6 +1342,10 @@ Сравнете кодовете за сигурност с вашите контакти. No comment provided by engineer. + + Completed + No comment provided by engineer. + Configure ICE servers Конфигурирай ICE сървъри @@ -1384,16 +1454,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… Свързване със сървъра… @@ -1439,6 +1521,18 @@ 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. + + + Connections subscribed + No comment provided by engineer. + Contact allows Контактът позволява @@ -1492,7 +1586,11 @@ This is your own one-time link! Copy Копирай - chat item action + No comment provided by engineer. + + + Copy error + No comment provided by engineer. Core version: v%@ @@ -1569,6 +1667,10 @@ This is your own one-time link! Създай своя профил No comment provided by engineer. + + Created + No comment provided by engineer. + Created at Създаден на @@ -1604,6 +1706,10 @@ This is your own one-time link! Текуща парола… No comment provided by engineer. + + Current user + No comment provided by engineer. + Currently maximum supported file size is %@. В момента максималният поддържан размер на файла е %@. @@ -1614,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 в базата данни @@ -1923,6 +2037,10 @@ This cannot be undone! Изтрий потребителския профил? No comment provided by engineer. + + Deleted + No comment provided by engineer. + Deleted at Изтрито на @@ -1933,6 +2051,10 @@ This cannot be undone! Изтрито на: %@ copied message info + + Deletion errors + No comment provided by engineer. + Delivery Доставка @@ -1972,6 +2094,14 @@ This cannot be undone! Destination server error: %@ snd error text + + Detailed statistics + No comment provided by engineer. + + + Details + No comment provided by engineer. + Develop Разработване @@ -2125,6 +2255,10 @@ This cannot be undone! Изтегли chat item action + + Download errors + No comment provided by engineer. + Download failed Неуспешно изтегляне @@ -2135,6 +2269,14 @@ This cannot be undone! Свали файл server test step + + Downloaded + No comment provided by engineer. + + + Downloaded files + No comment provided by engineer. + Downloading archive Архива се изтегля @@ -2505,6 +2647,10 @@ This cannot be undone! Грешка при експортиране на чат базата данни No comment provided by engineer. + + Error exporting theme: %@ + No comment provided by engineer. + Error importing chat database Грешка при импортиране на чат базата данни @@ -2530,11 +2676,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 Грешка при запазване на %@ сървъра @@ -2653,7 +2811,8 @@ This cannot be undone! Error: %@ Грешка: %@ - snd error text + file error text + snd error text Error: URL is invalid @@ -2665,6 +2824,10 @@ This cannot be undone! Грешка: няма файл с база данни No comment provided by engineer. + + Errors + No comment provided by engineer. + Even when disabled in the conversation. Дори когато е деактивиран в разговора. @@ -2690,6 +2853,10 @@ This cannot be undone! Грешка при експортиране: No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. Експортиран архив на базата данни. @@ -2725,6 +2892,26 @@ This cannot be undone! Любим No comment provided by engineer. + + 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. Файлът ще бъде изтрит от сървърите. @@ -2909,6 +3096,14 @@ Error: %2$@ GIF файлове и стикери No comment provided by engineer. + + Good afternoon! + message preview + + + Good morning! + message preview + Group Група @@ -3189,6 +3384,10 @@ Error: %2$@ Неуспешно импортиране No comment provided by engineer. + + Import theme + No comment provided by engineer. + Importing archive Импортиране на архив @@ -3311,6 +3510,10 @@ Error: %2$@ Интерфейс No comment provided by engineer. + + Interface colors + No comment provided by engineer. + Invalid QR code Невалиден QR код @@ -3654,6 +3857,10 @@ This is your link for group %@! Член No comment provided by engineer. + + Member inactive + item status text + Member role will be changed to "%@". All group members will be notified. Ролята на члена ще бъде променена на "%@". Всички членове на групата ще бъдат уведомени. @@ -3669,6 +3876,10 @@ This is your link for group %@! Членът ще бъде премахнат от групата - това не може да бъде отменено! No comment provided by engineer. + + Menus + No comment provided by engineer. + Message delivery error Грешка при доставката на съобщението @@ -3688,6 +3899,14 @@ This is your link for group %@! Чернова на съобщение 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. @@ -3720,6 +3939,18 @@ This is your link for group %@! Източникът на съобщението остава скрит. No comment provided by engineer. + + Message status + No comment provided by engineer. + + + Message status: %@ + copied message info + + + Message subscriptions + No comment provided by engineer. + Message text Текст на съобщението @@ -3745,6 +3976,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. Съобщенията, файловете и разговорите са защитени чрез **криптиране от край до край** с перфектна секретност при препращане, правдоподобно опровержение и възстановяване при взлом. @@ -3979,6 +4218,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 Няма филтрирани чатове @@ -3994,6 +4237,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 Няма мрежова връзка @@ -4178,6 +4425,10 @@ This is your link for group %@! Отвори миграцията към друго устройство authentication reason + + Open server settings + No comment provided by engineer. + Open user profiles Отвори потребителските профили @@ -4283,6 +4534,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. Хората могат да се свържат с вас само чрез ликовете, които споделяте. @@ -4308,6 +4563,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. Моля, проверете дали сте използвали правилния линк или поискайте вашия контакт, за да ви изпрати друг. @@ -4405,6 +4665,10 @@ Error: %@ Визуализация No comment provided by engineer. + + Previously connected servers + No comment provided by engineer. + Privacy & security Поверителност и сигурност @@ -4467,6 +4731,10 @@ Error: %@ Профилна парола No comment provided by engineer. + + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. Актуализацията на профила ще бъде изпратена до вашите контакти. @@ -4546,6 +4814,14 @@ Enable in *Network & servers* settings. Време за изчакване на протокола за KB No comment provided by engineer. + + Proxied + No comment provided by engineer. + + + Proxied servers + No comment provided by engineer. + Push notifications Push известия @@ -4611,6 +4887,10 @@ Enable in *Network & servers* settings. Потвърждениeто за доставка е деактивирано No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at Получено в @@ -4631,6 +4911,18 @@ Enable in *Network & servers* settings. Получено съобщение 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. Получаващият адрес ще бъде променен към друг сървър. Промяната на адреса ще завърши, след като подателят е онлайн. @@ -4661,11 +4953,31 @@ Enable in *Network & servers* settings. Получателите виждат актуализации, докато ги въвеждате. 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? Повторно свърване със сървърите? @@ -4716,6 +5028,10 @@ Enable in *Network & servers* settings. Премахване No comment provided by engineer. + + Remove image + No comment provided by engineer. + Remove member Острани член @@ -4786,16 +5102,32 @@ Enable in *Network & servers* settings. Нулиране 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 Рестартирайте приложението, за да създадете нов чат профил @@ -4866,6 +5198,10 @@ Enable in *Network & servers* settings. Стартиране на чат No comment provided by engineer. + + SMP server + No comment provided by engineer. + SMP servers SMP сървъри @@ -4980,6 +5316,14 @@ Enable in *Network & servers* settings. Запазено съобщение message info title + + Scale + No comment provided by engineer. + + + Scan / Paste link + No comment provided by engineer. + Scan QR code Сканирай QR код @@ -5020,11 +5364,19 @@ Enable in *Network & servers* settings. Търсене или поставяне на 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 Оценка на сигурността @@ -5040,6 +5392,10 @@ Enable in *Network & servers* settings. Избери No comment provided by engineer. + + Selected chat preferences prohibit this message. + No comment provided by engineer. + Self-destruct Самоунищожение @@ -5090,6 +5446,10 @@ Enable in *Network & servers* settings. Изпрати изчезващо съобщение No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews Изпрати визуализация на линковете @@ -5198,6 +5558,10 @@ Enable in *Network & servers* settings. Изпратено на: %@ copied message info + + Sent directly + No comment provided by engineer. + Sent file event Събитие за изпратен файл @@ -5208,11 +5572,31 @@ Enable in *Network & servers* settings. Изпратено съобщение 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. @@ -5232,6 +5616,10 @@ Enable in *Network & servers* settings. Тестът на сървъра е неуспешен! No comment provided by engineer. + + Server type + No comment provided by engineer. + Server version is incompatible with network settings. srv error text @@ -5241,6 +5629,14 @@ Enable in *Network & servers* settings. Сървъри 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 Код на сесията @@ -5256,6 +5652,10 @@ Enable in *Network & servers* settings. Задай име на контакт… No comment provided by engineer. + + Set default theme + No comment provided by engineer. + Set group preferences Задай групови настройки @@ -5374,6 +5774,10 @@ Enable in *Network & servers* settings. Покажи: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address SimpleX Адрес @@ -5449,6 +5853,10 @@ Enable in *Network & servers* settings. Опростен режим инкогнито No comment provided by engineer. + + Size + No comment provided by engineer. + Skip Пропускане @@ -5494,6 +5902,14 @@ Enable in *Network & servers* settings. Започни миграция No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop Спри @@ -5559,6 +5975,22 @@ Enable in *Network & servers* settings. Изпрати No comment provided by engineer. + + Subscribed + No comment provided by engineer. + + + Subscription errors + No comment provided by engineer. + + + Subscription percentage + No comment provided by engineer. + + + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat Подкрепете SimpleX Chat @@ -5639,6 +6071,10 @@ Enable in *Network & servers* settings. Докосни за започване на нов чат No comment provided by engineer. + + Temporary file error + No comment provided by engineer. + Test failed at step %@. Тестът е неуспешен на стъпка %@. @@ -5775,9 +6211,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. @@ -5845,11 +6280,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: За да задавате въпроси и да получавате актуализации: @@ -5916,11 +6359,19 @@ You will be prompted to complete authentication before this feature is enabled.< Избор на инкогнито при свързване. 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: %@). Опит за свързване със сървъра, използван за получаване на съобщения от този контакт (грешка: %@). @@ -6112,6 +6563,10 @@ To connect, please ask your contact to create another connection link and check Актуализирай и отвори чата No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed Неуспешно качване @@ -6122,6 +6577,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 Архивът се качва @@ -6195,6 +6658,10 @@ To connect, please ask your contact to create another connection link and check Потребителски профил No comment provided by engineer. + + User selection + No comment provided by engineer. + Using .onion hosts requires compatible VPN provider. Използването на .onion хостове изисква съвместим VPN доставчик. @@ -6330,6 +6797,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 Внимание: стартирането на чата на множество устройства не се поддържа и ще доведе до неуспешно изпращане на съобщения @@ -6432,11 +6907,19 @@ To connect, please ask your contact to create another connection link and check 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 server + No comment provided by engineer. + XFTP servers XFTP сървъри @@ -6519,6 +7002,10 @@ 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. Можете да приемате обаждания от заключен екран, без идентификация на устройство и приложението. @@ -6925,6 +7412,10 @@ SimpleX сървърите не могат да видят вашия профи и %lld други събития No comment provided by engineer. + + attempts + No comment provided by engineer. + audio call (not e2e encrypted) аудио разговор (не е e2e криптиран) @@ -6958,7 +7449,7 @@ SimpleX сървърите не могат да видят вашия профи blocked by admin блокиран от админ - marked deleted chat item preview text + blocked chat item bold @@ -7115,6 +7606,10 @@ SimpleX сървърите не могат да видят вашия профи дни time unit + + decryption errors + No comment provided by engineer. + default (%@) по подразбиране (%@) @@ -7165,6 +7660,10 @@ SimpleX сървърите не могат да видят вашия профи дублирано съобщение integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted e2e криптиран @@ -7245,6 +7744,10 @@ SimpleX сървърите не могат да видят вашия профи събитие се случи No comment provided by engineer. + + expired + No comment provided by engineer. + forwarded препратено @@ -7275,6 +7778,10 @@ SimpleX сървърите не могат да видят вашия профи iOS Keychain ще се използва за сигурно съхраняване на паролата, след като рестартирате приложението или промените паролата - това ще позволи получаването на push известия. No comment provided by engineer. + + inactive + No comment provided by engineer. + incognito via contact address link инкогнито чрез линк с адрес за контакт @@ -7452,6 +7959,14 @@ SimpleX сървърите не могат да видят вашия профи включено group pref value + + other + No comment provided by engineer. + + + other errors + No comment provided by engineer. + owner собственик @@ -7759,7 +8274,7 @@ last received msg: %2$@
- +
@@ -7796,7 +8311,7 @@ last received msg: %2$@
- +
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/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index 24c71a3487..392b9cf70d 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 @@
- +
@@ -539,9 +539,8 @@ O SimpleX adrese No comment provided by engineer. - - Accent color - Zbarvení + + Accent No comment provided by engineer. @@ -565,6 +564,14 @@ Přijmout inkognito accept contact request via notification + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + 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. Přidejte adresu do svého profilu, aby ji vaše kontakty mohly sdílet s dalšími lidmi. Aktualizace profilu bude zaslána vašim kontaktům. @@ -604,6 +611,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 +647,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 +666,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 +688,10 @@ All new messages from %@ will be hidden! No comment provided by engineer. + + All users + No comment provided by engineer. + All your contacts will remain connected. Všechny vaše kontakty zůstanou připojeny. @@ -853,6 +884,10 @@ Apply No comment provided by engineer. + + Apply to + No comment provided by engineer. + Archive and upload No comment provided by engineer. @@ -926,6 +961,10 @@ Zpět No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address No comment provided by engineer. @@ -949,6 +988,10 @@ Lepší zprávy No comment provided by engineer. + + Black + No comment provided by engineer. + Block No comment provided by engineer. @@ -1050,6 +1093,10 @@ 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 @@ -1119,6 +1166,10 @@ Chat se archivuje No comment provided by engineer. + + Chat colors + No comment provided by engineer. + Chat console Konzola pro chat @@ -1162,6 +1213,10 @@ Předvolby chatu No comment provided by engineer. + + Chat theme + No comment provided by engineer. + Chats Chaty @@ -1191,6 +1246,18 @@ 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 @@ -1215,9 +1282,8 @@ Zrušte ověření No comment provided by engineer. - - Colors - Barvy + + Color mode No comment provided by engineer. @@ -1230,6 +1296,10 @@ 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 @@ -1326,14 +1396,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… @@ -1377,6 +1459,18 @@ 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. + + + Connections subscribed + No comment provided by engineer. + Contact allows Kontakt povolil @@ -1430,7 +1524,11 @@ This is your own one-time link! Copy Kopírovat - chat item action + No comment provided by engineer. + + + Copy error + No comment provided by engineer. Core version: v%@ @@ -1503,6 +1601,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. @@ -1534,6 +1636,10 @@ This is your own one-time link! Aktuální přístupová fráze… No comment provided by engineer. + + Current user + No comment provided by engineer. + Currently maximum supported file size is %@. Aktuálně maximální podporovaná velikost souboru je %@. @@ -1544,11 +1650,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 @@ -1848,6 +1962,10 @@ This cannot be undone! Smazat uživatelský profil? No comment provided by engineer. + + Deleted + No comment provided by engineer. + Deleted at Smazáno v @@ -1858,6 +1976,10 @@ This cannot be undone! Smazáno v: %@ copied message info + + Deletion errors + No comment provided by engineer. + Delivery Doručenka @@ -1894,6 +2016,14 @@ This cannot be undone! Destination server error: %@ snd error text + + Detailed statistics + No comment provided by engineer. + + + Details + No comment provided by engineer. + Develop Vyvinout @@ -2043,6 +2173,10 @@ This cannot be undone! Download chat item action + + Download errors + No comment provided by engineer. + Download failed No comment provided by engineer. @@ -2052,6 +2186,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. @@ -2408,6 +2550,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 @@ -2432,11 +2578,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ů %@ @@ -2551,7 +2709,8 @@ This cannot be undone! Error: %@ Chyba: %@ - snd error text + file error text + snd error text Error: URL is invalid @@ -2563,6 +2722,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. @@ -2587,6 +2750,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. @@ -2620,6 +2787,26 @@ This cannot be undone! Oblíbené No comment provided by engineer. + + 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ů. @@ -2795,6 +2982,14 @@ Error: %2$@ GIFy a nálepky No comment provided by engineer. + + Good afternoon! + message preview + + + Good morning! + message preview + Group Skupina @@ -3069,6 +3264,10 @@ Error: %2$@ Import failed No comment provided by engineer. + + Import theme + No comment provided by engineer. + Importing archive No comment provided by engineer. @@ -3185,6 +3384,10 @@ Error: %2$@ Rozhranní No comment provided by engineer. + + Interface colors + No comment provided by engineer. + Invalid QR code No comment provided by engineer. @@ -3511,6 +3714,10 @@ This is your link for group %@! Č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. @@ -3526,6 +3733,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 @@ -3545,6 +3756,14 @@ This is your link for group %@! 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. @@ -3576,6 +3795,18 @@ This is your link for group %@! Message source remains private. No comment provided by engineer. + + Message status + No comment provided by engineer. + + + Message status: %@ + copied message info + + + Message subscriptions + No comment provided by engineer. + Message text Text zprávy @@ -3599,6 +3830,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. @@ -3820,6 +4059,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 @@ -3835,6 +4078,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. @@ -4014,6 +4261,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 @@ -4109,6 +4360,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. @@ -4133,6 +4388,11 @@ This is your link for group %@! 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ý. @@ -4227,6 +4487,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í @@ -4285,6 +4549,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. @@ -4363,6 +4631,14 @@ Enable in *Network & servers* settings. Č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í @@ -4425,6 +4701,10 @@ Enable in *Network & servers* settings. 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 @@ -4445,6 +4725,18 @@ Enable in *Network & servers* settings. 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. @@ -4473,11 +4765,31 @@ Enable in *Network & servers* settings. 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? @@ -4528,6 +4840,10 @@ Enable in *Network & servers* settings. Odstranit No comment provided by engineer. + + Remove image + No comment provided by engineer. + Remove member Odstranit člena @@ -4593,16 +4909,32 @@ Enable in *Network & servers* settings. Obnovit 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 @@ -4672,6 +5004,10 @@ Enable in *Network & servers* settings. Spustit chat No comment provided by engineer. + + SMP server + No comment provided by engineer. + SMP servers SMP servery @@ -4782,6 +5118,14 @@ Enable in *Network & servers* settings. 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 @@ -4819,11 +5163,19 @@ Enable in *Network & servers* settings. 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 @@ -4839,6 +5191,10 @@ Enable in *Network & servers* settings. Vybrat No comment provided by engineer. + + Selected chat preferences prohibit this message. + No comment provided by engineer. + Self-destruct Sebedestrukce @@ -4889,6 +5245,10 @@ Enable in *Network & servers* settings. 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ů @@ -4996,6 +5356,10 @@ Enable in *Network & servers* settings. Posláno v: % @ copied message info + + Sent directly + No comment provided by engineer. + Sent file event Odeslaná událost souboru @@ -5006,11 +5370,31 @@ Enable in *Network & servers* settings. 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. @@ -5030,6 +5414,10 @@ Enable in *Network & servers* settings. 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 @@ -5039,6 +5427,14 @@ Enable in *Network & servers* settings. 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. @@ -5053,6 +5449,10 @@ Enable in *Network & servers* settings. 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 @@ -5167,6 +5567,10 @@ Enable in *Network & servers* settings. Zobrazit: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address SimpleX Adresa @@ -5240,6 +5644,10 @@ Enable in *Network & servers* settings. Zjednodušený inkognito režim No comment provided by engineer. + + Size + No comment provided by engineer. + Skip Přeskočit @@ -5283,6 +5691,14 @@ Enable in *Network & servers* settings. Zahájit přenesení No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop Zastavit @@ -5346,6 +5762,22 @@ Enable in *Network & servers* settings. Odeslat No comment provided by engineer. + + Subscribed + No comment provided by engineer. + + + Subscription errors + No comment provided by engineer. + + + Subscription percentage + No comment provided by engineer. + + + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat Podpořte SimpleX Chat @@ -5423,6 +5855,10 @@ Enable in *Network & servers* settings. Klepnutím na zahájíte nový chat No comment provided by engineer. + + Temporary file error + No comment provided by engineer. + Test failed at step %@. Test selhal v kroku %@. @@ -5557,9 +5993,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. @@ -5621,11 +6056,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: @@ -5691,11 +6134,19 @@ Před zapnutím této funkce budete vyzváni k dokončení ověření. Změnit inkognito režim při připojení. 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: %@). @@ -5878,6 +6329,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu 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. @@ -5887,6 +6342,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. @@ -5956,6 +6419,10 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu Profil uživatele No comment provided by engineer. + + User selection + No comment provided by engineer. + Using .onion hosts requires compatible VPN provider. Použití hostitelů .onion vyžaduje kompatibilního poskytovatele VPN. @@ -6082,6 +6549,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. @@ -6176,11 +6651,19 @@ Chcete-li se připojit, požádejte svůj kontakt o vytvoření dalšího odkazu 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 server + No comment provided by engineer. + XFTP servers XFTP servery @@ -6254,6 +6737,10 @@ 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. @@ -6648,6 +7135,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) @@ -6677,7 +7168,7 @@ Servery SimpleX nevidí váš profil. blocked by admin - marked deleted chat item preview text + blocked chat item bold @@ -6833,6 +7324,10 @@ Servery SimpleX nevidí váš profil. dní time unit + + decryption errors + No comment provided by engineer. + default (%@) výchozí (%@) @@ -6882,6 +7377,10 @@ Servery SimpleX nevidí váš profil. duplicitní zpráva integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted e2e šifrované @@ -6961,6 +7460,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. @@ -6990,6 +7493,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 @@ -7166,6 +7673,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 @@ -7457,7 +7972,7 @@ last received msg: %2$@
- +
@@ -7493,7 +8008,7 @@ last received msg: %2$@
- +
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 4889950d33..cc2773d5e4 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 @@
- +
@@ -557,9 +557,8 @@ Über die SimpleX-Adresse No comment provided by engineer. - - Accent color - Akzentfarbe + + Accent No comment provided by engineer. @@ -583,6 +582,14 @@ Inkognito akzeptieren accept contact request via notification + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + 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. @@ -623,6 +630,18 @@ Begrüßungsmeldung hinzufügen 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 Adresse @@ -648,6 +667,10 @@ Erweiterte Netzwerkeinstellungen No comment provided by engineer. + + Advanced settings + No comment provided by engineer. + All app data is deleted. Werden die App-Daten komplett gelöscht. @@ -663,6 +686,10 @@ Alle Daten werden gelöscht, sobald dieser eingegeben wird. No comment provided by engineer. + + All data is private to your device. + No comment provided by engineer. + All group members will remain connected. Alle Gruppenmitglieder bleiben verbunden. @@ -683,6 +710,10 @@ Von %@ werden alle neuen Nachrichten ausgeblendet! No comment provided by engineer. + + All users + No comment provided by engineer. + All your contacts will remain connected. Alle Ihre Kontakte bleiben verbunden. @@ -883,6 +914,10 @@ Anwenden No comment provided by engineer. + + Apply to + No comment provided by engineer. + Archive and upload Archivieren und Hochladen @@ -958,6 +993,10 @@ Zurück No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address Falsche Desktop-Adresse @@ -983,6 +1022,10 @@ Verbesserungen bei Nachrichten No comment provided by engineer. + + Black + No comment provided by engineer. + Block Blockieren @@ -1093,6 +1136,10 @@ Die App kann nicht auf den Schlüsselbund zugreifen, um das Datenbank-Passwort zu speichern No comment provided by engineer. + + Cannot forward message + No comment provided by engineer. + Cannot receive file Datei kann nicht empfangen werden @@ -1164,6 +1211,10 @@ Datenbank Archiv No comment provided by engineer. + + Chat colors + No comment provided by engineer. + Chat console Chat-Konsole @@ -1209,6 +1260,10 @@ Chat-Präferenzen No comment provided by engineer. + + Chat theme + No comment provided by engineer. + Chats Chats @@ -1239,6 +1294,18 @@ Aus dem Fotoalbum auswählen 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 Löschen @@ -1264,9 +1331,8 @@ Überprüfung zurücknehmen No comment provided by engineer. - - Colors - Farben + + Color mode No comment provided by engineer. @@ -1279,6 +1345,10 @@ Vergleichen Sie die Sicherheitscodes mit Ihren Kontakten. No comment provided by engineer. + + Completed + No comment provided by engineer. + Configure ICE servers ICE-Server konfigurieren @@ -1388,16 +1458,28 @@ Das ist Ihr eigener Einmal-Link! Mit %@ verbinden No comment provided by engineer. + + Connected + No comment provided by engineer. + Connected desktop Verbundener Desktop No comment provided by engineer. + + Connected servers + No comment provided by engineer. + Connected to desktop Mit dem Desktop verbunden No comment provided by engineer. + + Connecting + No comment provided by engineer. + Connecting to server… Mit dem Server verbinden… @@ -1443,6 +1525,18 @@ Das ist Ihr eigener Einmal-Link! Verbindungszeitüberschreitung No comment provided by engineer. + + Connection with desktop stopped + No comment provided by engineer. + + + Connections + No comment provided by engineer. + + + Connections subscribed + No comment provided by engineer. + Contact allows Der Kontakt erlaubt @@ -1496,7 +1590,11 @@ Das ist Ihr eigener Einmal-Link! Copy Kopieren - chat item action + No comment provided by engineer. + + + Copy error + No comment provided by engineer. Core version: v%@ @@ -1573,6 +1671,10 @@ Das ist Ihr eigener Einmal-Link! Erstellen Sie Ihr Profil No comment provided by engineer. + + Created + No comment provided by engineer. + Created at Erstellt um @@ -1608,6 +1710,10 @@ Das ist Ihr eigener Einmal-Link! Aktuelles Passwort… No comment provided by engineer. + + Current user + No comment provided by engineer. + Currently maximum supported file size is %@. Die derzeit maximal unterstützte Dateigröße beträgt %@. @@ -1618,11 +1724,19 @@ Das ist Ihr eigener Einmal-Link! Zeit anpassen No comment provided by engineer. + + Customize theme + No comment provided by engineer. + Dark Dunkel No comment provided by engineer. + + Dark mode colors + No comment provided by engineer. + Database ID Datenbank-ID @@ -1927,6 +2041,10 @@ Das kann nicht rückgängig gemacht werden! Benutzerprofil löschen? No comment provided by engineer. + + Deleted + No comment provided by engineer. + Deleted at Gelöscht um @@ -1937,6 +2055,10 @@ Das kann nicht rückgängig gemacht werden! Gelöscht um: %@ copied message info + + Deletion errors + No comment provided by engineer. + Delivery Zustellung @@ -1977,6 +2099,14 @@ Das kann nicht rückgängig gemacht werden! Zielserver-Fehler: %@ snd error text + + Detailed statistics + No comment provided by engineer. + + + Details + No comment provided by engineer. + Develop Entwicklung @@ -2132,6 +2262,10 @@ Das kann nicht rückgängig gemacht werden! Herunterladen chat item action + + Download errors + No comment provided by engineer. + Download failed Herunterladen fehlgeschlagen @@ -2142,6 +2276,14 @@ Das kann nicht rückgängig gemacht werden! Datei herunterladen server test step + + Downloaded + No comment provided by engineer. + + + Downloaded files + No comment provided by engineer. + Downloading archive Archiv wird heruntergeladen @@ -2512,6 +2654,10 @@ Das kann nicht rückgängig gemacht werden! Fehler beim Exportieren der Chat-Datenbank No comment provided by engineer. + + Error exporting theme: %@ + No comment provided by engineer. + Error importing chat database Fehler beim Importieren der Chat-Datenbank @@ -2537,11 +2683,23 @@ Das kann nicht rückgängig gemacht werden! Fehler beim Empfangen der Datei No comment provided by engineer. + + Error reconnecting server + No comment provided by engineer. + + + Error reconnecting servers + No comment provided by engineer. + Error removing member Fehler beim Entfernen des Mitglieds No comment provided by engineer. + + Error resetting statistics + No comment provided by engineer. + Error saving %@ servers Fehler beim Speichern der %@-Server @@ -2660,7 +2818,8 @@ Das kann nicht rückgängig gemacht werden! Error: %@ Fehler: %@ - snd error text + file error text + snd error text Error: URL is invalid @@ -2672,6 +2831,10 @@ Das kann nicht rückgängig gemacht werden! Fehler: Keine Datenbankdatei No comment provided by engineer. + + Errors + No comment provided by engineer. + Even when disabled in the conversation. Auch wenn sie im Chat deaktiviert sind. @@ -2697,6 +2860,10 @@ Das kann nicht rückgängig gemacht werden! Fehler beim Export: No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. Exportiertes Datenbankarchiv. @@ -2732,6 +2899,26 @@ Das kann nicht rückgängig gemacht werden! Favorit No comment provided by engineer. + + 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. Die Datei wird von den Servern gelöscht. @@ -2921,6 +3108,14 @@ Fehler: %2$@ GIFs und Sticker No comment provided by engineer. + + Good afternoon! + message preview + + + Good morning! + message preview + Group Gruppe @@ -3201,6 +3396,10 @@ Fehler: %2$@ Import ist fehlgeschlagen No comment provided by engineer. + + Import theme + No comment provided by engineer. + Importing archive Archiv wird importiert @@ -3323,6 +3522,10 @@ Fehler: %2$@ Schnittstelle No comment provided by engineer. + + Interface colors + No comment provided by engineer. + Invalid QR code Ungültiger QR-Code @@ -3666,6 +3869,10 @@ Das ist Ihr Link für die Gruppe %@! Mitglied No comment provided by engineer. + + Member inactive + 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. @@ -3681,6 +3888,10 @@ Das ist Ihr Link für die Gruppe %@! Das Mitglied wird aus der Gruppe entfernt - dies kann nicht rückgängig gemacht werden! No comment provided by engineer. + + Menus + No comment provided by engineer. + Message delivery error Fehler bei der Nachrichtenzustellung @@ -3701,6 +3912,14 @@ Das ist Ihr Link für die Gruppe %@! Nachrichtenentwurf 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. @@ -3735,6 +3954,18 @@ Das ist Ihr Link für die Gruppe %@! Die Nachrichtenquelle bleibt privat. No comment provided by engineer. + + Message status + No comment provided by engineer. + + + Message status: %@ + copied message info + + + Message subscriptions + No comment provided by engineer. + Message text Nachrichtentext @@ -3760,6 +3991,14 @@ Das ist Ihr Link für die Gruppe %@! Die Nachrichten von %@ werden angezeigt! 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. Nachrichten, Dateien und Anrufe sind durch **Ende-zu-Ende-Verschlüsselung** mit Perfect Forward Secrecy, Ablehnung und Einbruchs-Wiederherstellung geschützt. @@ -3995,6 +4234,10 @@ 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. + item status description + No filtered chats Keine gefilterten Chats @@ -4010,6 +4253,10 @@ Das ist Ihr Link für die Gruppe %@! Kein Nachrichtenverlauf No comment provided by engineer. + + No info, try to reload + No comment provided by engineer. + No network connection Keine Netzwerkverbindung @@ -4194,6 +4441,10 @@ Das ist Ihr Link für die Gruppe %@! Migration auf ein anderes Gerät öffnen authentication reason + + Open server settings + No comment provided by engineer. + Open user profiles Benutzerprofile öffnen @@ -4299,6 +4550,10 @@ Das ist Ihr Link für die Gruppe %@! Fügen Sie den erhaltenen Link ein No comment provided by engineer. + + Pending + 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. @@ -4324,6 +4579,11 @@ Das ist Ihr Link für die Gruppe %@! 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. + 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. @@ -4421,6 +4681,10 @@ Fehler: %@ Vorschau No comment provided by engineer. + + Previously connected servers + No comment provided by engineer. + Privacy & security Datenschutz & Sicherheit @@ -4486,6 +4750,10 @@ Fehler: %@ Passwort für Profil No comment provided by engineer. + + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. Profil-Aktualisierung wird an Ihre Kontakte gesendet. @@ -4568,6 +4836,14 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Protokollzeitüberschreitung pro kB No comment provided by engineer. + + Proxied + No comment provided by engineer. + + + Proxied servers + No comment provided by engineer. + Push notifications Push-Benachrichtigungen @@ -4633,6 +4909,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Bestätigungen sind deaktiviert No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at Empfangen um @@ -4653,6 +4933,18 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Empfangene Nachricht 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. Die Empfängeradresse wird auf einen anderen Server geändert. Der Adresswechsel wird abgeschlossen, wenn der Absender wieder online ist. @@ -4683,11 +4975,31 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Die Empfänger sehen Nachrichtenaktualisierungen, während Sie sie eingeben. No comment provided by engineer. + + Reconnect + 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 + 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? Die Server neu verbinden? @@ -4738,6 +5050,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Entfernen No comment provided by engineer. + + Remove image + No comment provided by engineer. + Remove member Mitglied entfernen @@ -4808,16 +5124,32 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Zurücksetzen No comment provided by engineer. + + Reset all statistics + No comment provided by engineer. + + + Reset all statistics? + No comment provided by engineer. + Reset colors Farben zurücksetzen No comment provided by engineer. + + Reset to app theme + No comment provided by engineer. + Reset to defaults Auf Voreinstellungen zurücksetzen No comment provided by engineer. + + Reset to user theme + 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 @@ -4888,6 +5220,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Chat starten No comment provided by engineer. + + SMP server + No comment provided by engineer. + SMP servers SMP-Server @@ -5003,6 +5339,14 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Gespeicherte Nachricht message info title + + Scale + No comment provided by engineer. + + + Scan / Paste link + No comment provided by engineer. + Scan QR code QR-Code scannen @@ -5043,11 +5387,19 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Suchen oder fügen Sie den SimpleX-Link ein No comment provided by engineer. + + Secondary + No comment provided by engineer. + Secure queue Sichere Warteschlange server test step + + Secured + No comment provided by engineer. + Security assessment Sicherheits-Gutachten @@ -5063,6 +5415,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Auswählen No comment provided by engineer. + + Selected chat preferences prohibit this message. + No comment provided by engineer. + Self-destruct Selbstzerstörung @@ -5113,6 +5469,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Verschwindende Nachricht senden No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews Link-Vorschau senden @@ -5223,6 +5583,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Gesendet um: %@ copied message info + + Sent directly + No comment provided by engineer. + Sent file event Datei-Ereignis wurde gesendet @@ -5233,11 +5597,31 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Gesendete Nachricht message info title + + Sent messages + 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 + 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. Die Server-Adresse ist nicht mit den Netzwerk-Einstellungen kompatibel. @@ -5258,6 +5642,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Server Test ist fehlgeschlagen! No comment provided by engineer. + + Server type + No comment provided by engineer. + Server version is incompatible with network settings. Die Server-Version ist nicht mit den Netzwerk-Einstellungen kompatibel. @@ -5268,6 +5656,14 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Server 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 Sitzungscode @@ -5283,6 +5679,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Kontaktname festlegen… No comment provided by engineer. + + Set default theme + No comment provided by engineer. + Set group preferences Gruppen-Präferenzen einstellen @@ -5403,6 +5803,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Anzeigen: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address SimpleX-Adresse @@ -5478,6 +5882,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Vereinfachter Inkognito-Modus No comment provided by engineer. + + Size + No comment provided by engineer. + Skip Überspringen @@ -5523,6 +5931,14 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Starten Sie die Migration No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop Beenden @@ -5588,6 +6004,22 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Bestätigen No comment provided by engineer. + + Subscribed + No comment provided by engineer. + + + Subscription errors + No comment provided by engineer. + + + Subscription percentage + No comment provided by engineer. + + + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat Unterstützung von SimpleX Chat @@ -5668,6 +6100,10 @@ Aktivieren Sie es in den *Netzwerk & Server* Einstellungen. Zum Starten eines neuen Chats tippen No comment provided by engineer. + + Temporary file error + No comment provided by engineer. + Test failed at step %@. Der Test ist beim Schritt %@ fehlgeschlagen. @@ -5805,9 +6241,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 - Design + + Themes No comment provided by engineer. @@ -5875,11 +6310,19 @@ 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. + 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 + No comment provided by engineer. + To ask any questions and to receive updates: Um Fragen zu stellen und aktuelle Informationen zu erhalten: @@ -5947,11 +6390,19 @@ Sie werden aufgefordert, die Authentifizierung abzuschließen, bevor diese Funkt Inkognito beim Verbinden einschalten. No comment provided by engineer. + + Total + No comment provided by engineer. + Transport isolation 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: %@). 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: %@). @@ -6144,6 +6595,10 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Aktualisieren und den Chat öffnen No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed Hochladen fehlgeschlagen @@ -6154,6 +6609,14 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Datei hochladen server test step + + Uploaded + No comment provided by engineer. + + + Uploaded files + No comment provided by engineer. + Uploading archive Archiv wird hochgeladen @@ -6229,6 +6692,10 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Benutzerprofil No comment provided by engineer. + + User selection + No comment provided by engineer. + Using .onion hosts requires compatible VPN provider. Für die Nutzung von .onion-Hosts sind kompatible VPN-Anbieter erforderlich. @@ -6364,6 +6831,14 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s Auf das Video warten 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 Warnung: Das Starten des Chats auf mehreren Geräten wird nicht unterstützt und wird zu Fehlern bei der Nachrichtenübermittlung führen @@ -6469,11 +6944,19 @@ Bitten Sie Ihren Kontakt darum einen weiteren Verbindungs-Link zu erzeugen, um s 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. + file error text + Wrong passphrase! Falsches Passwort! No comment provided by engineer. + + XFTP server + No comment provided by engineer. + XFTP servers XFTP-Server @@ -6556,6 +7039,10 @@ 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. + 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. @@ -6962,6 +7449,10 @@ SimpleX-Server können Ihr Profil nicht einsehen. und %lld weitere Ereignisse No comment provided by engineer. + + attempts + No comment provided by engineer. + audio call (not e2e encrypted) Audioanruf (nicht E2E verschlüsselt) @@ -6995,7 +7486,7 @@ SimpleX-Server können Ihr Profil nicht einsehen. blocked by admin wurde vom Administrator blockiert - marked deleted chat item preview text + blocked chat item bold @@ -7152,6 +7643,10 @@ SimpleX-Server können Ihr Profil nicht einsehen. Tage time unit + + decryption errors + No comment provided by engineer. + default (%@) Voreinstellung (%@) @@ -7202,6 +7697,10 @@ SimpleX-Server können Ihr Profil nicht einsehen. Doppelte Nachricht integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted E2E-verschlüsselt @@ -7282,6 +7781,10 @@ SimpleX-Server können Ihr Profil nicht einsehen. event happened No comment provided by engineer. + + expired + No comment provided by engineer. + forwarded weitergeleitet @@ -7312,6 +7815,10 @@ 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 + No comment provided by engineer. + incognito via contact address link Inkognito über einen Kontaktadressen-Link @@ -7489,6 +7996,14 @@ SimpleX-Server können Ihr Profil nicht einsehen. Ein group pref value + + other + No comment provided by engineer. + + + other errors + No comment provided by engineer. + owner Eigentümer @@ -7799,7 +8314,7 @@ last received msg: %2$@
- +
@@ -7836,7 +8351,7 @@ last received msg: %2$@
- +
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/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index aca1aefb11..08e1255199 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 @@
- +
@@ -557,9 +557,9 @@ About SimpleX address No comment provided by engineer. - - Accent color - Accent color + + Accent + Accent No comment provided by engineer. @@ -583,6 +583,16 @@ Accept incognito accept contact request via notification + + Acknowledged + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + Acknowledgement errors + 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. Add address to your profile, so that your contacts can share it with other people. Profile update will be sent to your contacts. @@ -623,6 +633,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 +673,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 +693,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 +718,11 @@ All new messages from %@ will be hidden! No comment provided by engineer. + + All users + All users + No comment provided by engineer. + All your contacts will remain connected. All your contacts will remain connected. @@ -883,6 +923,11 @@ Apply No comment provided by engineer. + + Apply to + Apply to + No comment provided by engineer. + Archive and upload Archive and upload @@ -958,6 +1003,11 @@ Back No comment provided by engineer. + + Background + Background + No comment provided by engineer. + Bad desktop address Bad desktop address @@ -983,6 +1033,11 @@ Better messages No comment provided by engineer. + + Black + Black + No comment provided by engineer. + Block Block @@ -1093,6 +1148,11 @@ 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 @@ -1164,6 +1224,11 @@ Chat archive No comment provided by engineer. + + Chat colors + Chat colors + No comment provided by engineer. + Chat console Chat console @@ -1209,6 +1274,11 @@ Chat preferences No comment provided by engineer. + + Chat theme + Chat theme + No comment provided by engineer. + Chats Chats @@ -1239,6 +1309,21 @@ 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 @@ -1264,9 +1349,9 @@ Clear verification No comment provided by engineer. - - Colors - Colors + + Color mode + Color mode No comment provided by engineer. @@ -1279,6 +1364,11 @@ Compare security codes with your contacts. No comment provided by engineer. + + Completed + Completed + No comment provided by engineer. + Configure ICE servers Configure ICE servers @@ -1388,16 +1478,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… @@ -1443,6 +1548,21 @@ 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. + + + Connections subscribed + Connections subscribed + No comment provided by engineer. + Contact allows Contact allows @@ -1496,7 +1616,12 @@ This is your own one-time link! Copy Copy - chat item action + No comment provided by engineer. + + + Copy error + Copy error + No comment provided by engineer. Core version: v%@ @@ -1573,6 +1698,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 @@ -1608,6 +1738,11 @@ This is your own one-time link! Current passphrase… No comment provided by engineer. + + Current user + Current user + No comment provided by engineer. + Currently maximum supported file size is %@. Currently maximum supported file size is %@. @@ -1618,11 +1753,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 @@ -1928,6 +2073,11 @@ This cannot be undone! Delete user profile? No comment provided by engineer. + + Deleted + Deleted + No comment provided by engineer. + Deleted at Deleted at @@ -1938,6 +2088,11 @@ This cannot be undone! Deleted at: %@ copied message info + + Deletion errors + Deletion errors + No comment provided by engineer. + Delivery Delivery @@ -1978,6 +2133,16 @@ This cannot be undone! Destination server error: %@ snd error text + + Detailed statistics + Detailed statistics + No comment provided by engineer. + + + Details + Details + No comment provided by engineer. + Develop Develop @@ -2133,6 +2298,11 @@ This cannot be undone! Download chat item action + + Download errors + Download errors + No comment provided by engineer. + Download failed Download failed @@ -2143,6 +2313,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 @@ -2513,6 +2693,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 @@ -2538,11 +2723,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 @@ -2661,7 +2861,8 @@ This cannot be undone! Error: %@ Error: %@ - snd error text + file error text + snd error text Error: URL is invalid @@ -2673,6 +2874,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. @@ -2698,6 +2904,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. @@ -2733,6 +2944,31 @@ This cannot be undone! Favorite No comment provided by engineer. + + 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. @@ -2922,6 +3158,16 @@ Error: %2$@ GIFs and stickers No comment provided by engineer. + + Good afternoon! + Good afternoon! + message preview + + + Good morning! + Good morning! + message preview + Group Group @@ -3202,6 +3448,11 @@ Error: %2$@ Import failed No comment provided by engineer. + + Import theme + Import theme + No comment provided by engineer. + Importing archive Importing archive @@ -3324,6 +3575,11 @@ Error: %2$@ Interface No comment provided by engineer. + + Interface colors + Interface colors + No comment provided by engineer. + Invalid QR code Invalid QR code @@ -3667,6 +3923,11 @@ This is your link for group %@! 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. @@ -3682,6 +3943,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 @@ -3702,6 +3968,16 @@ This is your link for group %@! 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 @@ -3737,6 +4013,21 @@ This is your link for group %@! 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 subscriptions + Message subscriptions + No comment provided by engineer. + Message text Message text @@ -3762,6 +4053,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. @@ -3997,6 +4298,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 @@ -4012,6 +4318,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 @@ -4196,6 +4507,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 @@ -4301,6 +4617,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. @@ -4326,6 +4647,13 @@ This is your link for group %@! 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. @@ -4423,6 +4751,11 @@ Error: %@ Preview No comment provided by engineer. + + Previously connected servers + Previously connected servers + No comment provided by engineer. + Privacy & security Privacy & security @@ -4488,6 +4821,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. @@ -4570,6 +4908,16 @@ Enable in *Network & servers* settings. 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 @@ -4635,6 +4983,11 @@ Enable in *Network & servers* settings. Receipts are disabled No comment provided by engineer. + + Receive errors + Receive errors + No comment provided by engineer. + Received at Received at @@ -4655,6 +5008,21 @@ Enable in *Network & servers* settings. 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. @@ -4685,11 +5053,36 @@ Enable in *Network & servers* settings. 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? @@ -4740,6 +5133,11 @@ Enable in *Network & servers* settings. Remove No comment provided by engineer. + + Remove image + Remove image + No comment provided by engineer. + Remove member Remove member @@ -4810,16 +5208,36 @@ Enable in *Network & servers* settings. Reset 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 @@ -4890,6 +5308,11 @@ Enable in *Network & servers* settings. Run chat No comment provided by engineer. + + SMP server + SMP server + No comment provided by engineer. + SMP servers SMP servers @@ -5005,6 +5428,16 @@ Enable in *Network & servers* settings. 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 @@ -5045,11 +5478,21 @@ Enable in *Network & servers* settings. 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 @@ -5065,6 +5508,11 @@ Enable in *Network & servers* settings. Select No comment provided by engineer. + + Selected chat preferences prohibit this message. + Selected chat preferences prohibit this message. + No comment provided by engineer. + Self-destruct Self-destruct @@ -5115,6 +5563,11 @@ Enable in *Network & servers* settings. Send disappearing message No comment provided by engineer. + + Send errors + Send errors + No comment provided by engineer. + Send link previews Send link previews @@ -5225,6 +5678,11 @@ Enable in *Network & servers* settings. Sent at: %@ copied message info + + Sent directly + Sent directly + No comment provided by engineer. + Sent file event Sent file event @@ -5235,11 +5693,36 @@ Enable in *Network & servers* settings. 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. @@ -5260,6 +5743,11 @@ Enable in *Network & servers* settings. 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. @@ -5270,6 +5758,16 @@ Enable in *Network & servers* settings. 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 @@ -5285,6 +5783,11 @@ Enable in *Network & servers* settings. 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 @@ -5405,6 +5908,11 @@ Enable in *Network & servers* settings. Show: No comment provided by engineer. + + SimpleX + SimpleX + No comment provided by engineer. + SimpleX Address SimpleX Address @@ -5480,6 +5988,11 @@ Enable in *Network & servers* settings. Simplified incognito mode No comment provided by engineer. + + Size + Size + No comment provided by engineer. + Skip Skip @@ -5525,6 +6038,16 @@ Enable in *Network & servers* settings. 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 @@ -5590,6 +6113,26 @@ Enable in *Network & servers* settings. Submit No comment provided by engineer. + + Subscribed + Subscribed + No comment provided by engineer. + + + Subscription errors + Subscription errors + No comment provided by engineer. + + + Subscription percentage + Subscription percentage + No comment provided by engineer. + + + Subscriptions ignored + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat Support SimpleX Chat @@ -5670,6 +6213,11 @@ Enable in *Network & servers* settings. Tap to start a new chat No comment provided by engineer. + + Temporary file error + Temporary file error + No comment provided by engineer. + Test failed at step %@. Test failed at step %@. @@ -5807,9 +6355,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. @@ -5877,11 +6425,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: @@ -5949,11 +6507,21 @@ You will be prompted to complete authentication before this feature is enabled.< Toggle incognito when connecting. 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: %@). @@ -6146,6 +6714,11 @@ To connect, please ask your contact to create another connection link and check Upgrade and open chat No comment provided by engineer. + + Upload errors + Upload errors + No comment provided by engineer. + Upload failed Upload failed @@ -6156,6 +6729,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 @@ -6231,6 +6814,11 @@ To connect, please ask your contact to create another connection link and check User profile No comment provided by engineer. + + User selection + User selection + No comment provided by engineer. + Using .onion hosts requires compatible VPN provider. Using .onion hosts requires compatible VPN provider. @@ -6366,6 +6954,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 @@ -6471,11 +7069,21 @@ To connect, please ask your contact to create another connection link and check 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 server + XFTP server + No comment provided by engineer. + XFTP servers XFTP servers @@ -6558,6 +7166,11 @@ 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. @@ -6964,6 +7577,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) @@ -6997,7 +7615,7 @@ SimpleX servers cannot see your profile. blocked by admin blocked by admin - marked deleted chat item preview text + blocked chat item bold @@ -7154,6 +7772,11 @@ SimpleX servers cannot see your profile. days time unit + + decryption errors + decryption errors + No comment provided by engineer. + default (%@) default (%@) @@ -7204,6 +7827,11 @@ SimpleX servers cannot see your profile. duplicate message integrity error chat item + + duplicates + duplicates + No comment provided by engineer. + e2e encrypted e2e encrypted @@ -7284,6 +7912,11 @@ SimpleX servers cannot see your profile. event happened No comment provided by engineer. + + expired + expired + No comment provided by engineer. + forwarded forwarded @@ -7314,6 +7947,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 @@ -7491,6 +8129,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 @@ -7804,7 +8452,7 @@ last received msg: %2$@
- +
@@ -7841,7 +8489,7 @@ last received msg: %2$@
- +
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 d76630ab3f..3e37e6c6d5 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 @@
- +
@@ -557,9 +557,8 @@ Acerca de la dirección SimpleX No comment provided by engineer. - - Accent color - Color + + Accent No comment provided by engineer. @@ -583,6 +582,14 @@ Aceptar incógnito accept contact request via notification + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + 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. @@ -623,6 +630,18 @@ Añadir mensaje de bienvenida 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 Dirección @@ -648,6 +667,10 @@ Configuración avanzada de red No comment provided by engineer. + + Advanced settings + No comment provided by engineer. + All app data is deleted. Todos los datos de la aplicación se eliminarán. @@ -663,6 +686,10 @@ Al introducirlo todos los datos son eliminados. No comment provided by engineer. + + All data is private to your device. + No comment provided by engineer. + All group members will remain connected. Todos los miembros del grupo permanecerán conectados. @@ -683,6 +710,10 @@ ¡Los mensajes nuevos de %@ estarán ocultos! No comment provided by engineer. + + All users + No comment provided by engineer. + All your contacts will remain connected. Todos tus contactos permanecerán conectados. @@ -883,6 +914,10 @@ Aplicar No comment provided by engineer. + + Apply to + No comment provided by engineer. + Archive and upload Archivar y subir @@ -958,6 +993,10 @@ Volver No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address Dirección ordenador incorrecta @@ -983,6 +1022,10 @@ Mensajes mejorados No comment provided by engineer. + + Black + No comment provided by engineer. + Block Bloquear @@ -1093,6 +1136,10 @@ Keychain inaccesible para guardar la contraseña de la base de datos No comment provided by engineer. + + Cannot forward message + No comment provided by engineer. + Cannot receive file No se puede recibir el archivo @@ -1164,6 +1211,10 @@ Archivo del chat No comment provided by engineer. + + Chat colors + No comment provided by engineer. + Chat console Consola de Chat @@ -1209,6 +1260,10 @@ Preferencias de Chat No comment provided by engineer. + + Chat theme + No comment provided by engineer. + Chats Chats @@ -1239,6 +1294,18 @@ Elige de la biblioteca 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 Vaciar @@ -1264,9 +1331,8 @@ Eliminar verificación No comment provided by engineer. - - Colors - Colores + + Color mode No comment provided by engineer. @@ -1279,6 +1345,10 @@ Compara los códigos de seguridad con tus contactos. No comment provided by engineer. + + Completed + No comment provided by engineer. + Configure ICE servers Configure servidores ICE @@ -1388,16 +1458,28 @@ This is your own one-time link! Conectar con %@ No comment provided by engineer. + + Connected + No comment provided by engineer. + Connected desktop Ordenador conectado No comment provided by engineer. + + Connected servers + No comment provided by engineer. + Connected to desktop Conectado con ordenador No comment provided by engineer. + + Connecting + No comment provided by engineer. + Connecting to server… Conectando con el servidor… @@ -1443,6 +1525,18 @@ This is your own one-time link! Tiempo de conexión expirado No comment provided by engineer. + + Connection with desktop stopped + No comment provided by engineer. + + + Connections + No comment provided by engineer. + + + Connections subscribed + No comment provided by engineer. + Contact allows El contacto permite @@ -1496,7 +1590,11 @@ This is your own one-time link! Copy Copiar - chat item action + No comment provided by engineer. + + + Copy error + No comment provided by engineer. Core version: v%@ @@ -1573,6 +1671,10 @@ This is your own one-time link! Crea tu perfil No comment provided by engineer. + + Created + No comment provided by engineer. + Created at Creado @@ -1608,6 +1710,10 @@ This is your own one-time link! Contraseña actual… No comment provided by engineer. + + Current user + No comment provided by engineer. + Currently maximum supported file size is %@. El tamaño máximo de archivo admitido es %@. @@ -1618,11 +1724,19 @@ This is your own one-time link! Tiempo personalizado No comment provided by engineer. + + Customize theme + No comment provided by engineer. + Dark Oscuro No comment provided by engineer. + + Dark mode colors + No comment provided by engineer. + Database ID ID base de datos @@ -1927,6 +2041,10 @@ This cannot be undone! ¿Eliminar perfil de usuario? No comment provided by engineer. + + Deleted + No comment provided by engineer. + Deleted at Eliminado @@ -1937,6 +2055,10 @@ This cannot be undone! Eliminado: %@ copied message info + + Deletion errors + No comment provided by engineer. + Delivery Entrega @@ -1977,6 +2099,14 @@ This cannot be undone! Error del servidor de destino: %@ snd error text + + Detailed statistics + No comment provided by engineer. + + + Details + No comment provided by engineer. + Develop Desarrollo @@ -2132,6 +2262,10 @@ This cannot be undone! Descargar chat item action + + Download errors + No comment provided by engineer. + Download failed Descarga fallida @@ -2142,6 +2276,14 @@ This cannot be undone! Descargar archivo server test step + + Downloaded + No comment provided by engineer. + + + Downloaded files + No comment provided by engineer. + Downloading archive Descargando archivo @@ -2512,6 +2654,10 @@ This cannot be undone! Error al exportar base de datos No comment provided by engineer. + + Error exporting theme: %@ + No comment provided by engineer. + Error importing chat database Error al importar base de datos @@ -2537,11 +2683,23 @@ This cannot be undone! Error al recibir archivo No comment provided by engineer. + + Error reconnecting server + No comment provided by engineer. + + + Error reconnecting servers + No comment provided by engineer. + Error removing member Error al eliminar miembro No comment provided by engineer. + + Error resetting statistics + No comment provided by engineer. + Error saving %@ servers Error al guardar servidores %@ @@ -2660,7 +2818,8 @@ This cannot be undone! Error: %@ Error: %@ - snd error text + file error text + snd error text Error: URL is invalid @@ -2672,6 +2831,10 @@ This cannot be undone! Error: sin archivo de base de datos No comment provided by engineer. + + Errors + No comment provided by engineer. + Even when disabled in the conversation. Incluso si está desactivado para la conversación. @@ -2697,6 +2860,10 @@ This cannot be undone! Error al exportar: No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. Archivo de base de datos exportado. @@ -2732,6 +2899,26 @@ This cannot be undone! Favoritos No comment provided by engineer. + + 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. El archivo será eliminado de los servidores. @@ -2921,6 +3108,14 @@ Error: %2$@ GIFs y stickers No comment provided by engineer. + + Good afternoon! + message preview + + + Good morning! + message preview + Group Grupo @@ -3201,6 +3396,10 @@ Error: %2$@ Error de importación No comment provided by engineer. + + Import theme + No comment provided by engineer. + Importing archive Importando archivo @@ -3323,6 +3522,10 @@ Error: %2$@ Interfaz No comment provided by engineer. + + Interface colors + No comment provided by engineer. + Invalid QR code Código QR no válido @@ -3666,6 +3869,10 @@ This is your link for group %@! Miembro No comment provided by engineer. + + Member inactive + 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. @@ -3681,6 +3888,10 @@ This is your link for group %@! El miembro será expulsado del grupo. ¡No podrá deshacerse! No comment provided by engineer. + + Menus + No comment provided by engineer. + Message delivery error Error en la entrega del mensaje @@ -3701,6 +3912,14 @@ This is your link for group %@! Borrador de mensaje 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. @@ -3735,6 +3954,18 @@ This is your link for group %@! El autor del mensaje se mantiene privado. No comment provided by engineer. + + Message status + No comment provided by engineer. + + + Message status: %@ + copied message info + + + Message subscriptions + No comment provided by engineer. + Message text Contacto y texto @@ -3760,6 +3991,14 @@ This is your link for group %@! ¡Los mensajes de %@ serán mostrados! 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. 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. @@ -3995,6 +4234,10 @@ This is your link for group %@! ¡Sin dispositivo token! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + item status description + No filtered chats Sin chats filtrados @@ -4010,6 +4253,10 @@ This is your link for group %@! Sin historial No comment provided by engineer. + + No info, try to reload + No comment provided by engineer. + No network connection Sin conexión de red @@ -4194,6 +4441,10 @@ This is your link for group %@! Abrir menú migración a otro dispositivo authentication reason + + Open server settings + No comment provided by engineer. + Open user profiles Abrir perfil de usuario @@ -4299,6 +4550,10 @@ This is your link for group %@! Pegar el enlace recibido No comment provided by engineer. + + Pending + 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. @@ -4324,6 +4579,11 @@ This is your link for group %@! 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. + 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. @@ -4421,6 +4681,10 @@ Error: %@ Vista previa No comment provided by engineer. + + Previously connected servers + No comment provided by engineer. + Privacy & security Privacidad y Seguridad @@ -4486,6 +4750,10 @@ Error: %@ Contraseña del perfil No comment provided by engineer. + + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. La actualización del perfil se enviará a tus contactos. @@ -4568,6 +4836,14 @@ Actívalo en ajustes de *Servidores y Redes*. Límite de espera del protocolo por KB No comment provided by engineer. + + Proxied + No comment provided by engineer. + + + Proxied servers + No comment provided by engineer. + Push notifications Notificaciones automáticas @@ -4633,6 +4909,10 @@ Actívalo en ajustes de *Servidores y Redes*. Las confirmaciones están desactivadas No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at Recibido a las @@ -4653,6 +4933,18 @@ Actívalo en ajustes de *Servidores y Redes*. Mensaje entrante 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. La dirección de recepción pasará a otro servidor. El cambio se completará cuando el remitente esté en línea. @@ -4683,11 +4975,31 @@ Actívalo en ajustes de *Servidores y Redes*. Los destinatarios ven actualizarse mientras escribes. No comment provided by engineer. + + Reconnect + 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 + 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? ¿Reconectar servidores? @@ -4738,6 +5050,10 @@ Actívalo en ajustes de *Servidores y Redes*. Eliminar No comment provided by engineer. + + Remove image + No comment provided by engineer. + Remove member Expulsar miembro @@ -4808,16 +5124,32 @@ Actívalo en ajustes de *Servidores y Redes*. Restablecer No comment provided by engineer. + + Reset all statistics + No comment provided by engineer. + + + Reset all statistics? + No comment provided by engineer. + Reset colors Restablecer colores No comment provided by engineer. + + Reset to app theme + No comment provided by engineer. + Reset to defaults Restablecer valores predetarminados No comment provided by engineer. + + Reset to user theme + No comment provided by engineer. + Restart the app to create a new chat profile Reinicia la aplicación para crear un perfil nuevo @@ -4888,6 +5220,10 @@ Actívalo en ajustes de *Servidores y Redes*. Ejecutar chat No comment provided by engineer. + + SMP server + No comment provided by engineer. + SMP servers Servidores SMP @@ -5003,6 +5339,14 @@ Actívalo en ajustes de *Servidores y Redes*. Mensaje guardado message info title + + Scale + No comment provided by engineer. + + + Scan / Paste link + No comment provided by engineer. + Scan QR code Escanear código QR @@ -5043,11 +5387,19 @@ Actívalo en ajustes de *Servidores y Redes*. Buscar o pegar enlace SimpleX No comment provided by engineer. + + Secondary + No comment provided by engineer. + Secure queue Cola segura server test step + + Secured + No comment provided by engineer. + Security assessment Evaluación de la seguridad @@ -5063,6 +5415,10 @@ Actívalo en ajustes de *Servidores y Redes*. Seleccionar No comment provided by engineer. + + Selected chat preferences prohibit this message. + No comment provided by engineer. + Self-destruct Autodestrucción @@ -5113,6 +5469,10 @@ Actívalo en ajustes de *Servidores y Redes*. Enviar mensaje temporal No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews Enviar previsualizacion de enlaces @@ -5223,6 +5583,10 @@ Actívalo en ajustes de *Servidores y Redes*. Enviado: %@ copied message info + + Sent directly + No comment provided by engineer. + Sent file event Evento de archivo enviado @@ -5233,11 +5597,31 @@ Actívalo en ajustes de *Servidores y Redes*. Mensaje saliente message info title + + Sent messages + 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 + 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. La dirección del servidor es incompatible con la configuración de la red. @@ -5258,6 +5642,10 @@ Actívalo en ajustes de *Servidores y Redes*. ¡Error en prueba del servidor! No comment provided by engineer. + + Server type + 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. @@ -5268,6 +5656,14 @@ Actívalo en ajustes de *Servidores y Redes*. Servidores 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 Código de sesión @@ -5283,6 +5679,10 @@ Actívalo en ajustes de *Servidores y Redes*. Escribe el nombre del contacto… No comment provided by engineer. + + Set default theme + No comment provided by engineer. + Set group preferences Establecer preferencias de grupo @@ -5403,6 +5803,10 @@ Actívalo en ajustes de *Servidores y Redes*. Mostrar: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address Dirección SimpleX @@ -5478,6 +5882,10 @@ Actívalo en ajustes de *Servidores y Redes*. Modo incógnito simplificado No comment provided by engineer. + + Size + No comment provided by engineer. + Skip Omitir @@ -5523,6 +5931,14 @@ Actívalo en ajustes de *Servidores y Redes*. Iniciar migración No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop Parar @@ -5588,6 +6004,22 @@ Actívalo en ajustes de *Servidores y Redes*. Enviar No comment provided by engineer. + + Subscribed + No comment provided by engineer. + + + Subscription errors + No comment provided by engineer. + + + Subscription percentage + No comment provided by engineer. + + + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat Soporte SimpleX Chat @@ -5668,6 +6100,10 @@ Actívalo en ajustes de *Servidores y Redes*. Pulsa para iniciar chat nuevo No comment provided by engineer. + + Temporary file error + No comment provided by engineer. + Test failed at step %@. La prueba ha fallado en el paso %@. @@ -5805,9 +6241,8 @@ 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 No comment provided by engineer. @@ -5875,11 +6310,19 @@ 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. + 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 + No comment provided by engineer. + To ask any questions and to receive updates: Para consultar cualquier duda y recibir actualizaciones: @@ -5947,11 +6390,19 @@ Se te pedirá que completes la autenticación antes de activar esta función.
Activa incógnito al conectar. No comment provided by engineer. + + Total + No comment provided by engineer. + Transport isolation Aislamiento de transporte 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: %@). Intentando conectar con el servidor usado para recibir mensajes de este contacto (error: %@). @@ -6145,6 +6596,10 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Actualizar y abrir Chat No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed Error de subida @@ -6155,6 +6610,14 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Subir archivo server test step + + Uploaded + No comment provided by engineer. + + + Uploaded files + No comment provided by engineer. + Uploading archive Subiendo archivo @@ -6230,6 +6693,10 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb Perfil de usuario No comment provided by engineer. + + User selection + No comment provided by engineer. + Using .onion hosts requires compatible VPN provider. Usar hosts .onion requiere un proveedor VPN compatible. @@ -6365,6 +6832,14 @@ 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 + 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 Atención: el inicio del chat en varios dispositivos es incompatible y provocará fallos en la entrega de mensajes @@ -6470,11 +6945,19 @@ Para conectarte, pide a tu contacto que cree otro enlace de conexión y comprueb 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. + file error text + Wrong passphrase! ¡Contraseña incorrecta! No comment provided by engineer. + + XFTP server + No comment provided by engineer. + XFTP servers Servidores XFTP @@ -6557,6 +7040,10 @@ 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 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. @@ -6963,6 +7450,10 @@ Los servidores de SimpleX no pueden ver tu perfil. y %lld evento(s) más No comment provided by engineer. + + attempts + No comment provided by engineer. + audio call (not e2e encrypted) llamada (sin cifrar) @@ -6996,7 +7487,7 @@ Los servidores de SimpleX no pueden ver tu perfil. blocked by admin bloqueado por administrador - marked deleted chat item preview text + blocked chat item bold @@ -7153,6 +7644,10 @@ Los servidores de SimpleX no pueden ver tu perfil. días time unit + + decryption errors + No comment provided by engineer. + default (%@) predeterminado (%@) @@ -7203,6 +7698,10 @@ Los servidores de SimpleX no pueden ver tu perfil. mensaje duplicado integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted cifrado de extremo a extremo @@ -7283,6 +7782,10 @@ Los servidores de SimpleX no pueden ver tu perfil. evento ocurrido No comment provided by engineer. + + expired + No comment provided by engineer. + forwarded reenviado @@ -7313,6 +7816,10 @@ 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 + No comment provided by engineer. + incognito via contact address link en modo incógnito mediante enlace de dirección del contacto @@ -7490,6 +7997,14 @@ Los servidores de SimpleX no pueden ver tu perfil. Activado group pref value + + other + No comment provided by engineer. + + + other errors + No comment provided by engineer. + owner propietario @@ -7800,7 +8315,7 @@ last received msg: %2$@
- +
@@ -7837,7 +8352,7 @@ last received msg: %2$@
- +
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 77f9e29871..71e0f0796a 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 @@
- +
@@ -534,9 +534,8 @@ Tietoja SimpleX osoitteesta No comment provided by engineer. - - Accent color - Korostusväri + + Accent No comment provided by engineer. @@ -560,6 +559,14 @@ Hyväksy tuntematon accept contact request via notification + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + 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. Lisää osoite profiiliisi, jotta kontaktisi voivat jakaa sen muiden kanssa. Profiilipäivitys lähetetään kontakteillesi. @@ -599,6 +606,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 +642,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 +661,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 +683,10 @@ All new messages from %@ will be hidden! No comment provided by engineer. + + All users + No comment provided by engineer. + All your contacts will remain connected. Kaikki kontaktisi pysyvät yhteydessä. @@ -847,6 +878,10 @@ Apply No comment provided by engineer. + + Apply to + No comment provided by engineer. + Archive and upload No comment provided by engineer. @@ -920,6 +955,10 @@ Takaisin No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address No comment provided by engineer. @@ -943,6 +982,10 @@ Parempia viestejä No comment provided by engineer. + + Black + No comment provided by engineer. + Block No comment provided by engineer. @@ -1043,6 +1086,10 @@ 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 @@ -1112,6 +1159,10 @@ Chat-arkisto No comment provided by engineer. + + Chat colors + No comment provided by engineer. + Chat console Chat-konsoli @@ -1155,6 +1206,10 @@ Chat-asetukset No comment provided by engineer. + + Chat theme + No comment provided by engineer. + Chats Keskustelut @@ -1184,6 +1239,18 @@ 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ä @@ -1208,9 +1275,8 @@ Tyhjennä vahvistus No comment provided by engineer. - - Colors - Värit + + Color mode No comment provided by engineer. @@ -1223,6 +1289,10 @@ Vertaa turvakoodeja kontaktiesi kanssa. No comment provided by engineer. + + Completed + No comment provided by engineer. + Configure ICE servers Määritä ICE-palvelimet @@ -1319,14 +1389,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… @@ -1370,6 +1452,18 @@ 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. + + + Connections subscribed + No comment provided by engineer. + Contact allows Kontakti sallii @@ -1423,7 +1517,11 @@ This is your own one-time link! Copy Kopioi - chat item action + No comment provided by engineer. + + + Copy error + No comment provided by engineer. Core version: v%@ @@ -1496,6 +1594,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. @@ -1527,6 +1629,10 @@ This is your own one-time link! Nykyinen tunnuslause… No comment provided by engineer. + + Current user + No comment provided by engineer. + Currently maximum supported file size is %@. Nykyinen tuettu enimmäistiedostokoko on %@. @@ -1537,11 +1643,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 @@ -1841,6 +1955,10 @@ This cannot be undone! Poista käyttäjäprofiili? No comment provided by engineer. + + Deleted + No comment provided by engineer. + Deleted at Poistettu klo @@ -1851,6 +1969,10 @@ This cannot be undone! Poistettu klo: %@ copied message info + + Deletion errors + No comment provided by engineer. + Delivery Toimitus @@ -1887,6 +2009,14 @@ This cannot be undone! Destination server error: %@ snd error text + + Detailed statistics + No comment provided by engineer. + + + Details + No comment provided by engineer. + Develop Kehitä @@ -2036,6 +2166,10 @@ This cannot be undone! Download chat item action + + Download errors + No comment provided by engineer. + Download failed No comment provided by engineer. @@ -2045,6 +2179,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. @@ -2399,6 +2541,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 @@ -2423,11 +2569,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 @@ -2541,7 +2699,8 @@ This cannot be undone! Error: %@ Virhe: %@ - snd error text + file error text + snd error text Error: URL is invalid @@ -2553,6 +2712,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. @@ -2577,6 +2740,10 @@ This cannot be undone! Vientivirhe: No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. Viety tietokanta-arkisto. @@ -2610,6 +2777,26 @@ This cannot be undone! Suosikki No comment provided by engineer. + + 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. @@ -2785,6 +2972,14 @@ Error: %2$@ GIFit ja tarrat No comment provided by engineer. + + Good afternoon! + message preview + + + Good morning! + message preview + Group Ryhmä @@ -3059,6 +3254,10 @@ Error: %2$@ Import failed No comment provided by engineer. + + Import theme + No comment provided by engineer. + Importing archive No comment provided by engineer. @@ -3175,6 +3374,10 @@ Error: %2$@ Käyttöliittymä No comment provided by engineer. + + Interface colors + No comment provided by engineer. + Invalid QR code No comment provided by engineer. @@ -3501,6 +3704,10 @@ This is your link for group %@! 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. @@ -3516,6 +3723,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 @@ -3535,6 +3746,14 @@ This is your link for group %@! 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. @@ -3566,6 +3785,18 @@ This is your link for group %@! Message source remains private. No comment provided by engineer. + + Message status + No comment provided by engineer. + + + Message status: %@ + copied message info + + + Message subscriptions + No comment provided by engineer. + Message text Viestin teksti @@ -3589,6 +3820,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. @@ -3809,6 +4048,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 @@ -3824,6 +4067,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. @@ -4002,6 +4249,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 @@ -4097,6 +4348,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. @@ -4121,6 +4376,11 @@ This is your link for group %@! 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. @@ -4215,6 +4475,10 @@ Error: %@ Esikatselu No comment provided by engineer. + + Previously connected servers + No comment provided by engineer. + Privacy & security Yksityisyys ja turvallisuus @@ -4273,6 +4537,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. @@ -4351,6 +4619,14 @@ Enable in *Network & servers* settings. 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 @@ -4413,6 +4689,10 @@ Enable in *Network & servers* settings. Kuittaukset pois käytöstä No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at Vastaanotettu klo @@ -4433,6 +4713,18 @@ Enable in *Network & servers* settings. 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. @@ -4461,11 +4753,31 @@ Enable in *Network & servers* settings. 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? @@ -4516,6 +4828,10 @@ Enable in *Network & servers* settings. Poista No comment provided by engineer. + + Remove image + No comment provided by engineer. + Remove member Poista jäsen @@ -4581,16 +4897,32 @@ Enable in *Network & servers* settings. Oletustilaan 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 @@ -4660,6 +4992,10 @@ Enable in *Network & servers* settings. Käynnistä chat No comment provided by engineer. + + SMP server + No comment provided by engineer. + SMP servers SMP-palvelimet @@ -4770,6 +5106,14 @@ Enable in *Network & servers* settings. 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 @@ -4807,11 +5151,19 @@ Enable in *Network & servers* settings. 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 @@ -4827,6 +5179,10 @@ Enable in *Network & servers* settings. Valitse No comment provided by engineer. + + Selected chat preferences prohibit this message. + No comment provided by engineer. + Self-destruct Itsetuho @@ -4876,6 +5232,10 @@ Enable in *Network & servers* settings. Lähetä katoava viesti No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews Lähetä linkkien esikatselu @@ -4983,6 +5343,10 @@ Enable in *Network & servers* settings. Lähetetty klo: %@ copied message info + + Sent directly + No comment provided by engineer. + Sent file event Lähetetty tiedosto tapahtuma @@ -4993,11 +5357,31 @@ Enable in *Network & servers* settings. 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. @@ -5017,6 +5401,10 @@ Enable in *Network & servers* settings. 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 @@ -5026,6 +5414,14 @@ Enable in *Network & servers* settings. 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. @@ -5040,6 +5436,10 @@ Enable in *Network & servers* settings. Aseta kontaktin nimi… No comment provided by engineer. + + Set default theme + No comment provided by engineer. + Set group preferences Aseta ryhmän asetukset @@ -5154,6 +5554,10 @@ Enable in *Network & servers* settings. Näytä: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address SimpleX-osoite @@ -5226,6 +5630,10 @@ Enable in *Network & servers* settings. Simplified incognito mode No comment provided by engineer. + + Size + No comment provided by engineer. + Skip Ohita @@ -5269,6 +5677,14 @@ Enable in *Network & servers* settings. Aloita siirto No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop Lopeta @@ -5332,6 +5748,22 @@ Enable in *Network & servers* settings. Lähetä No comment provided by engineer. + + Subscribed + No comment provided by engineer. + + + Subscription errors + No comment provided by engineer. + + + Subscription percentage + No comment provided by engineer. + + + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat SimpleX Chat tuki @@ -5409,6 +5841,10 @@ Enable in *Network & servers* settings. Aloita uusi keskustelu napauttamalla No comment provided by engineer. + + Temporary file error + No comment provided by engineer. + Test failed at step %@. Testi epäonnistui vaiheessa %@. @@ -5543,9 +5979,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. @@ -5607,11 +6042,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ä: @@ -5676,11 +6119,19 @@ Sinua kehotetaan suorittamaan todennus loppuun, ennen kuin tämä ominaisuus ote Toggle incognito when connecting. 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: %@). @@ -5863,6 +6314,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Päivitä ja avaa keskustelu No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed No comment provided by engineer. @@ -5872,6 +6327,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. @@ -5941,6 +6404,10 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja Käyttäjäprofiili No comment provided by engineer. + + User selection + No comment provided by engineer. + Using .onion hosts requires compatible VPN provider. .onion-isäntien käyttäminen vaatii yhteensopivan VPN-palveluntarjoajan. @@ -6067,6 +6534,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. @@ -6161,11 +6636,19 @@ Jos haluat muodostaa yhteyden, pyydä kontaktiasi luomaan toinen yhteyslinkki ja 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 server + No comment provided by engineer. + XFTP servers XFTP-palvelimet @@ -6239,6 +6722,10 @@ 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. @@ -6633,6 +7120,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) @@ -6662,7 +7153,7 @@ SimpleX-palvelimet eivät näe profiiliasi. blocked by admin - marked deleted chat item preview text + blocked chat item bold @@ -6817,6 +7308,10 @@ SimpleX-palvelimet eivät näe profiiliasi. päivää time unit + + decryption errors + No comment provided by engineer. + default (%@) oletusarvo (%@) @@ -6866,6 +7361,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 @@ -6946,6 +7445,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. @@ -6975,6 +7478,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 @@ -7151,6 +7658,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 @@ -7441,7 +7956,7 @@ last received msg: %2$@
- +
@@ -7477,7 +7992,7 @@ last received msg: %2$@
- +
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 7e6c0d113d..4f618f26b0 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 @@
- +
@@ -557,9 +557,8 @@ À propos de l'adresse SimpleX No comment provided by engineer. - - Accent color - Couleur principale + + Accent No comment provided by engineer. @@ -583,6 +582,14 @@ Accepter en incognito accept contact request via notification + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + 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. @@ -623,6 +630,18 @@ Ajouter un message d'accueil 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 Adresse @@ -648,6 +667,10 @@ Paramètres réseau avancés No comment provided by engineer. + + Advanced settings + No comment provided by engineer. + All app data is deleted. Toutes les données de l'application sont supprimées. @@ -663,6 +686,10 @@ Toutes les données sont effacées lorsqu'il est saisi. No comment provided by engineer. + + All data is private to your device. + No comment provided by engineer. + All group members will remain connected. Tous les membres du groupe resteront connectés. @@ -683,6 +710,10 @@ Tous les nouveaux messages de %@ seront cachés ! No comment provided by engineer. + + All users + No comment provided by engineer. + All your contacts will remain connected. Tous vos contacts resteront connectés. @@ -883,6 +914,10 @@ Appliquer No comment provided by engineer. + + Apply to + No comment provided by engineer. + Archive and upload Archiver et transférer @@ -958,6 +993,10 @@ Retour No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address Mauvaise adresse de bureau @@ -983,6 +1022,10 @@ Meilleurs messages No comment provided by engineer. + + Black + No comment provided by engineer. + Block Bloquer @@ -1093,6 +1136,10 @@ 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 + No comment provided by engineer. + Cannot receive file Impossible de recevoir le fichier @@ -1164,6 +1211,10 @@ Archives du chat No comment provided by engineer. + + Chat colors + No comment provided by engineer. + Chat console Console du chat @@ -1209,6 +1260,10 @@ Préférences de chat No comment provided by engineer. + + Chat theme + No comment provided by engineer. + Chats Discussions @@ -1239,6 +1294,18 @@ Choisir dans la photothèque 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 Effacer @@ -1264,9 +1331,8 @@ Retirer la vérification No comment provided by engineer. - - Colors - Couleurs + + Color mode No comment provided by engineer. @@ -1279,6 +1345,10 @@ Comparez les codes de sécurité avec vos contacts. No comment provided by engineer. + + Completed + No comment provided by engineer. + Configure ICE servers Configurer les serveurs ICE @@ -1388,16 +1458,28 @@ Il s'agit de votre propre lien unique ! Se connecter avec %@ No comment provided by engineer. + + Connected + No comment provided by engineer. + Connected desktop Bureau connecté No comment provided by engineer. + + Connected servers + No comment provided by engineer. + Connected to desktop Connecté au bureau No comment provided by engineer. + + Connecting + No comment provided by engineer. + Connecting to server… Connexion au serveur… @@ -1443,6 +1525,18 @@ Il s'agit de votre propre lien unique ! Délai de connexion No comment provided by engineer. + + Connection with desktop stopped + No comment provided by engineer. + + + Connections + No comment provided by engineer. + + + Connections subscribed + No comment provided by engineer. + Contact allows Votre contact autorise @@ -1496,7 +1590,11 @@ Il s'agit de votre propre lien unique ! Copy Copier - chat item action + No comment provided by engineer. + + + Copy error + No comment provided by engineer. Core version: v%@ @@ -1573,6 +1671,10 @@ Il s'agit de votre propre lien unique ! Créez votre profil No comment provided by engineer. + + Created + No comment provided by engineer. + Created at Créé à @@ -1608,6 +1710,10 @@ Il s'agit de votre propre lien unique ! Phrase secrète actuelle… No comment provided by engineer. + + Current user + No comment provided by engineer. + Currently maximum supported file size is %@. Actuellement, la taille maximale des fichiers supportés est de %@. @@ -1618,11 +1724,19 @@ Il s'agit de votre propre lien unique ! Délai personnalisé No comment provided by engineer. + + Customize theme + No comment provided by engineer. + Dark Sombre No comment provided by engineer. + + Dark mode colors + No comment provided by engineer. + Database ID ID de base de données @@ -1927,6 +2041,10 @@ Cette opération ne peut être annulée ! Supprimer le profil utilisateur ? No comment provided by engineer. + + Deleted + No comment provided by engineer. + Deleted at Supprimé à @@ -1937,6 +2055,10 @@ Cette opération ne peut être annulée ! Supprimé à : %@ copied message info + + Deletion errors + No comment provided by engineer. + Delivery Distribution @@ -1977,6 +2099,14 @@ Cette opération ne peut être annulée ! Erreur du serveur de destination : %@ snd error text + + Detailed statistics + No comment provided by engineer. + + + Details + No comment provided by engineer. + Develop Développer @@ -2132,6 +2262,10 @@ Cette opération ne peut être annulée ! Télécharger chat item action + + Download errors + No comment provided by engineer. + Download failed Échec du téléchargement @@ -2142,6 +2276,14 @@ Cette opération ne peut être annulée ! Télécharger le fichier server test step + + Downloaded + No comment provided by engineer. + + + Downloaded files + No comment provided by engineer. + Downloading archive Téléchargement de l'archive @@ -2512,6 +2654,10 @@ 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: %@ + No comment provided by engineer. + Error importing chat database Erreur lors de l'importation de la base de données du chat @@ -2537,11 +2683,23 @@ Cette opération ne peut être annulée ! Erreur lors de la réception du fichier No comment provided by engineer. + + Error reconnecting server + No comment provided by engineer. + + + Error reconnecting servers + No comment provided by engineer. + Error removing member Erreur lors de la suppression d'un membre No comment provided by engineer. + + Error resetting statistics + No comment provided by engineer. + Error saving %@ servers Erreur lors de la sauvegarde des serveurs %@ @@ -2660,7 +2818,8 @@ Cette opération ne peut être annulée ! Error: %@ Erreur : %@ - snd error text + file error text + snd error text Error: URL is invalid @@ -2672,6 +2831,10 @@ Cette opération ne peut être annulée ! Erreur : pas de fichier de base de données No comment provided by engineer. + + Errors + No comment provided by engineer. + Even when disabled in the conversation. Même s'il est désactivé dans la conversation. @@ -2697,6 +2860,10 @@ Cette opération ne peut être annulée ! Erreur lors de l'exportation : No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. Archive de la base de données exportée. @@ -2732,6 +2899,26 @@ Cette opération ne peut être annulée ! Favoris No comment provided by engineer. + + 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. Le fichier sera supprimé des serveurs. @@ -2921,6 +3108,14 @@ Erreur : %2$@ GIFs et stickers No comment provided by engineer. + + Good afternoon! + message preview + + + Good morning! + message preview + Group Groupe @@ -3201,6 +3396,10 @@ Erreur : %2$@ Échec de l'importation No comment provided by engineer. + + Import theme + No comment provided by engineer. + Importing archive Importation de l'archive @@ -3323,6 +3522,10 @@ Erreur : %2$@ Interface No comment provided by engineer. + + Interface colors + No comment provided by engineer. + Invalid QR code Code QR invalide @@ -3666,6 +3869,10 @@ Voici votre lien pour le groupe %@ ! Membre No comment provided by engineer. + + Member inactive + 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. @@ -3681,6 +3888,10 @@ Voici votre lien pour le groupe %@ ! Ce membre sera retiré du groupe - impossible de revenir en arrière ! No comment provided by engineer. + + Menus + No comment provided by engineer. + Message delivery error Erreur de distribution du message @@ -3701,6 +3912,14 @@ Voici votre lien pour le groupe %@ ! Brouillon de message 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. @@ -3735,6 +3954,18 @@ Voici votre lien pour le groupe %@ ! La source du message reste privée. No comment provided by engineer. + + Message status + No comment provided by engineer. + + + Message status: %@ + copied message info + + + Message subscriptions + No comment provided by engineer. + Message text Texte du message @@ -3760,6 +3991,14 @@ Voici votre lien pour le groupe %@ ! Les messages de %@ seront affichés ! 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. 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. @@ -3995,6 +4234,10 @@ 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. + item status description + No filtered chats Aucune discussion filtrés @@ -4010,6 +4253,10 @@ Voici votre lien pour le groupe %@ ! Aucun historique No comment provided by engineer. + + No info, try to reload + No comment provided by engineer. + No network connection Pas de connexion au réseau @@ -4194,6 +4441,10 @@ Voici votre lien pour le groupe %@ ! Ouvrir le transfert vers un autre appareil authentication reason + + Open server settings + No comment provided by engineer. + Open user profiles Ouvrir les profils d'utilisateurs @@ -4299,6 +4550,10 @@ Voici votre lien pour le groupe %@ ! Collez le lien que vous avez reçu No comment provided by engineer. + + Pending + 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. @@ -4324,6 +4579,11 @@ Voici votre lien pour le groupe %@ ! 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. + 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. @@ -4421,6 +4681,10 @@ Erreur : %@ Aperçu No comment provided by engineer. + + Previously connected servers + No comment provided by engineer. + Privacy & security Vie privée et sécurité @@ -4486,6 +4750,10 @@ Erreur : %@ Mot de passe de profil No comment provided by engineer. + + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. La mise à jour du profil sera envoyée à vos contacts. @@ -4568,6 +4836,14 @@ Activez-le dans les paramètres *Réseau et serveurs*. Délai d'attente du protocole par KB No comment provided by engineer. + + Proxied + No comment provided by engineer. + + + Proxied servers + No comment provided by engineer. + Push notifications Notifications push @@ -4633,6 +4909,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Les accusés de réception sont désactivés No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at Reçu à @@ -4653,6 +4933,18 @@ Activez-le dans les paramètres *Réseau et serveurs*. Message reçu 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. 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. @@ -4683,11 +4975,31 @@ Activez-le dans les paramètres *Réseau et serveurs*. Les destinataires voient les mises à jour au fur et à mesure que vous leur écrivez. No comment provided by engineer. + + Reconnect + 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 + 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? Reconnecter les serveurs ? @@ -4738,6 +5050,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Supprimer No comment provided by engineer. + + Remove image + No comment provided by engineer. + Remove member Retirer le membre @@ -4808,16 +5124,32 @@ Activez-le dans les paramètres *Réseau et serveurs*. Réinitialisation No comment provided by engineer. + + Reset all statistics + No comment provided by engineer. + + + Reset all statistics? + No comment provided by engineer. + Reset colors Réinitialisation des couleurs No comment provided by engineer. + + Reset to app theme + No comment provided by engineer. + Reset to defaults Réinitialisation des valeurs par défaut No comment provided by engineer. + + Reset to user theme + 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 @@ -4888,6 +5220,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Exécuter le chat No comment provided by engineer. + + SMP server + No comment provided by engineer. + SMP servers Serveurs SMP @@ -5003,6 +5339,14 @@ Activez-le dans les paramètres *Réseau et serveurs*. Message enregistré message info title + + Scale + No comment provided by engineer. + + + Scan / Paste link + No comment provided by engineer. + Scan QR code Scanner un code QR @@ -5043,11 +5387,19 @@ Activez-le dans les paramètres *Réseau et serveurs*. Rechercher ou coller un lien SimpleX No comment provided by engineer. + + Secondary + No comment provided by engineer. + Secure queue File d'attente sécurisée server test step + + Secured + No comment provided by engineer. + Security assessment Évaluation de sécurité @@ -5063,6 +5415,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Choisir No comment provided by engineer. + + Selected chat preferences prohibit this message. + No comment provided by engineer. + Self-destruct Autodestruction @@ -5113,6 +5469,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Envoyer un message éphémère No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews Envoi d'aperçus de liens @@ -5223,6 +5583,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Envoyé le : %@ copied message info + + Sent directly + No comment provided by engineer. + Sent file event Événement de fichier envoyé @@ -5233,11 +5597,31 @@ Activez-le dans les paramètres *Réseau et serveurs*. Message envoyé message info title + + Sent messages + 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 + 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. L'adresse du serveur est incompatible avec les paramètres du réseau. @@ -5258,6 +5642,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Échec du test du serveur ! No comment provided by engineer. + + Server type + 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. @@ -5268,6 +5656,14 @@ Activez-le dans les paramètres *Réseau et serveurs*. Serveurs 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 Code de session @@ -5283,6 +5679,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Définir le nom du contact… No comment provided by engineer. + + Set default theme + No comment provided by engineer. + Set group preferences Définir les préférences du groupe @@ -5403,6 +5803,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Afficher : No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address Adresse SimpleX @@ -5478,6 +5882,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Mode incognito simplifié No comment provided by engineer. + + Size + No comment provided by engineer. + Skip Passer @@ -5523,6 +5931,14 @@ Activez-le dans les paramètres *Réseau et serveurs*. Démarrer la migration No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop Arrêter @@ -5588,6 +6004,22 @@ Activez-le dans les paramètres *Réseau et serveurs*. Soumettre No comment provided by engineer. + + Subscribed + No comment provided by engineer. + + + Subscription errors + No comment provided by engineer. + + + Subscription percentage + No comment provided by engineer. + + + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat Supporter SimpleX Chat @@ -5668,6 +6100,10 @@ Activez-le dans les paramètres *Réseau et serveurs*. Appuyez ici pour démarrer une nouvelle discussion No comment provided by engineer. + + Temporary file error + No comment provided by engineer. + Test failed at step %@. Échec du test à l'étape %@. @@ -5805,9 +6241,8 @@ 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 No comment provided by engineer. @@ -5875,11 +6310,19 @@ 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. + 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 + 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 : @@ -5947,11 +6390,19 @@ Vous serez invité à confirmer l'authentification avant que cette fonction ne s Basculer en mode incognito lors de la connexion. No comment provided by engineer. + + Total + No comment provided by engineer. + Transport isolation Transport isolé 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: %@). Tentative de connexion au serveur utilisé pour recevoir les messages de ce contact (erreur : %@). @@ -6144,6 +6595,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Mettre à niveau et ouvrir le chat No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed Échec de l'envoi @@ -6154,6 +6609,14 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Transférer le fichier server test step + + Uploaded + No comment provided by engineer. + + + Uploaded files + No comment provided by engineer. + Uploading archive Envoi de l'archive @@ -6229,6 +6692,10 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien Profil d'utilisateur No comment provided by engineer. + + User selection + No comment provided by engineer. + Using .onion hosts requires compatible VPN provider. L'utilisation des hôtes .onion nécessite un fournisseur VPN compatible. @@ -6364,6 +6831,14 @@ 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 + 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 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 @@ -6469,11 +6944,19 @@ Pour vous connecter, veuillez demander à votre contact de créer un autre lien 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. + file error text + Wrong passphrase! Mauvaise phrase secrète ! No comment provided by engineer. + + XFTP server + No comment provided by engineer. + XFTP servers Serveurs XFTP @@ -6556,6 +7039,10 @@ 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. + 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. @@ -6962,6 +7449,10 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. et %lld autres événements No comment provided by engineer. + + attempts + No comment provided by engineer. + audio call (not e2e encrypted) appel audio (sans chiffrement) @@ -6995,7 +7486,7 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. blocked by admin bloqué par l'administrateur - marked deleted chat item preview text + blocked chat item bold @@ -7152,6 +7643,10 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. jours time unit + + decryption errors + No comment provided by engineer. + default (%@) défaut (%@) @@ -7202,6 +7697,10 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. message dupliqué integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted chiffré de bout en bout @@ -7282,6 +7781,10 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. event happened No comment provided by engineer. + + expired + No comment provided by engineer. + forwarded transféré @@ -7312,6 +7815,10 @@ 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 + No comment provided by engineer. + incognito via contact address link mode incognito via le lien d'adresse du contact @@ -7489,6 +7996,14 @@ Les serveurs SimpleX ne peuvent pas voir votre profil. on group pref value + + other + No comment provided by engineer. + + + other errors + No comment provided by engineer. + owner propriétaire @@ -7799,7 +8314,7 @@ last received msg: %2$@
- +
@@ -7836,7 +8351,7 @@ last received msg: %2$@
- +
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/hu.xcloc/Localized Contents/hu.xliff b/apps/ios/SimpleX Localizations/hu.xcloc/Localized Contents/hu.xliff index 13594c8efa..ecc0610115 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 @@
- +
@@ -557,9 +557,8 @@ A SimpleX azonosítóról No comment provided by engineer. - - Accent color - Kiemelő szín + + Accent No comment provided by engineer. @@ -583,6 +582,14 @@ Fogadás inkognítóban accept contact request via notification + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + 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. @@ -623,6 +630,18 @@ Üdvözlő üzenet hozzáadása 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 Cím @@ -648,6 +667,10 @@ Speciális hálózati beállítások No comment provided by engineer. + + Advanced settings + No comment provided by engineer. + All app data is deleted. Minden alkalmazásadat törölve. @@ -663,6 +686,10 @@ 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. + No comment provided by engineer. + All group members will remain connected. Minden csoporttag kapcsolódva marad. @@ -683,6 +710,10 @@ Minden új üzenet elrejtésre kerül tőle: %@! No comment provided by engineer. + + All users + No comment provided by engineer. + All your contacts will remain connected. Minden ismerős kapcsolódva marad. @@ -883,6 +914,10 @@ Alkalmaz No comment provided by engineer. + + Apply to + No comment provided by engineer. + Archive and upload Archiválás és feltöltés @@ -958,6 +993,10 @@ Vissza No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address Hibás számítógép azonosító @@ -983,6 +1022,10 @@ Jobb üzenetek No comment provided by engineer. + + Black + No comment provided by engineer. + Block Blokkolás @@ -1093,6 +1136,10 @@ Nem lehet hozzáférni a kulcstartóhoz az adatbázis jelszavának mentéséhez No comment provided by engineer. + + Cannot forward message + No comment provided by engineer. + Cannot receive file Nem lehet fogadni a fájlt @@ -1164,6 +1211,10 @@ Csevegési archívum No comment provided by engineer. + + Chat colors + No comment provided by engineer. + Chat console Csevegési konzol @@ -1209,6 +1260,10 @@ Csevegési beállítások No comment provided by engineer. + + Chat theme + No comment provided by engineer. + Chats Csevegések @@ -1239,6 +1294,18 @@ Választás a könyvtárból 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 Kiürítés @@ -1264,9 +1331,8 @@ Hitelesítés törlése No comment provided by engineer. - - Colors - Színek + + Color mode No comment provided by engineer. @@ -1279,6 +1345,10 @@ Biztonsági kódok összehasonlítása az ismerősökkel. No comment provided by engineer. + + Completed + No comment provided by engineer. + Configure ICE servers ICE kiszolgálók beállítása @@ -1388,16 +1458,28 @@ Ez az egyszer használatos hivatkozása! Kapcsolódás ezzel: %@ No comment provided by engineer. + + Connected + No comment provided by engineer. + Connected desktop Csatlakoztatott számítógép No comment provided by engineer. + + Connected servers + No comment provided by engineer. + Connected to desktop Kapcsolódva a számítógéphez No comment provided by engineer. + + Connecting + No comment provided by engineer. + Connecting to server… Kapcsolódás a kiszolgálóhoz… @@ -1443,6 +1525,18 @@ Ez az egyszer használatos hivatkozása! Kapcsolat időtúllépés No comment provided by engineer. + + Connection with desktop stopped + No comment provided by engineer. + + + Connections + No comment provided by engineer. + + + Connections subscribed + No comment provided by engineer. + Contact allows Ismerős engedélyezi @@ -1496,7 +1590,11 @@ Ez az egyszer használatos hivatkozása! Copy Másolás - chat item action + No comment provided by engineer. + + + Copy error + No comment provided by engineer. Core version: v%@ @@ -1573,6 +1671,10 @@ Ez az egyszer használatos hivatkozása! Saját profil létrehozása No comment provided by engineer. + + Created + No comment provided by engineer. + Created at Létrehozva ekkor: @@ -1608,6 +1710,10 @@ Ez az egyszer használatos hivatkozása! Jelenlegi jelmondat… No comment provided by engineer. + + Current user + No comment provided by engineer. + Currently maximum supported file size is %@. Jelenleg a maximális támogatott fájlméret %@. @@ -1618,11 +1724,19 @@ Ez az egyszer használatos hivatkozása! Személyreszabott idő No comment provided by engineer. + + Customize theme + No comment provided by engineer. + Dark Sötét No comment provided by engineer. + + Dark mode colors + No comment provided by engineer. + Database ID Adatbázis ID @@ -1927,6 +2041,10 @@ Ez a művelet nem vonható vissza! Felhasználói profil törlése? No comment provided by engineer. + + Deleted + No comment provided by engineer. + Deleted at Törölve ekkor: @@ -1937,6 +2055,10 @@ Ez a művelet nem vonható vissza! Törölve ekkor: %@ copied message info + + Deletion errors + No comment provided by engineer. + Delivery Kézbesítés @@ -1977,6 +2099,14 @@ Ez a művelet nem vonható vissza! Célkiszolgáló hiba: %@ snd error text + + Detailed statistics + No comment provided by engineer. + + + Details + No comment provided by engineer. + Develop Fejlesztés @@ -2132,6 +2262,10 @@ Ez a művelet nem vonható vissza! Letöltés chat item action + + Download errors + No comment provided by engineer. + Download failed Sikertelen letöltés @@ -2142,6 +2276,14 @@ Ez a művelet nem vonható vissza! Fájl letöltése server test step + + Downloaded + No comment provided by engineer. + + + Downloaded files + No comment provided by engineer. + Downloading archive Archívum letöltése @@ -2512,6 +2654,10 @@ Ez a művelet nem vonható vissza! Hiba a csevegési adatbázis exportálásakor No comment provided by engineer. + + Error exporting theme: %@ + No comment provided by engineer. + Error importing chat database Hiba a csevegési adatbázis importálásakor @@ -2537,11 +2683,23 @@ Ez a művelet nem vonható vissza! Hiba a fájl fogadásakor No comment provided by engineer. + + Error reconnecting server + No comment provided by engineer. + + + Error reconnecting servers + No comment provided by engineer. + Error removing member Hiba a tag eltávolításakor No comment provided by engineer. + + Error resetting statistics + No comment provided by engineer. + Error saving %@ servers Hiba történt a %@ kiszolgálók mentése közben @@ -2660,7 +2818,8 @@ Ez a művelet nem vonható vissza! Error: %@ Hiba: %@ - snd error text + file error text + snd error text Error: URL is invalid @@ -2672,6 +2831,10 @@ Ez a művelet nem vonható vissza! Hiba: nincs adatbázis fájl No comment provided by engineer. + + Errors + No comment provided by engineer. + Even when disabled in the conversation. Akkor is, ha le van tiltva a beszélgetésben. @@ -2697,6 +2860,10 @@ Ez a művelet nem vonható vissza! Exportálási hiba: No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. Exportált adatbázis-archívum. @@ -2732,6 +2899,26 @@ Ez a művelet nem vonható vissza! Kedvenc No comment provided by engineer. + + 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. A fájl törölve lesz a kiszolgálóról. @@ -2921,6 +3108,14 @@ Hiba: %2$@ GIF-ek és matricák No comment provided by engineer. + + Good afternoon! + message preview + + + Good morning! + message preview + Group Csoport @@ -3201,6 +3396,10 @@ Hiba: %2$@ Sikertelen importálás No comment provided by engineer. + + Import theme + No comment provided by engineer. + Importing archive Archívum importálása @@ -3323,6 +3522,10 @@ Hiba: %2$@ Felület No comment provided by engineer. + + Interface colors + No comment provided by engineer. + Invalid QR code Érvénytelen QR-kód @@ -3666,6 +3869,10 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Tag No comment provided by engineer. + + Member inactive + 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. @@ -3681,6 +3888,10 @@ 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 + No comment provided by engineer. + Message delivery error Üzenetkézbesítési hiba @@ -3701,6 +3912,14 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Üzenetvázlat 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. @@ -3735,6 +3954,18 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Az üzenet forrása titokban marad. No comment provided by engineer. + + Message status + No comment provided by engineer. + + + Message status: %@ + copied message info + + + Message subscriptions + No comment provided by engineer. + Message text Üzenet szövege @@ -3760,6 +3991,14 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! A(z) %@ által írt üzenetek megjelennek! 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. 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. @@ -3995,6 +4234,10 @@ 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. + item status description + No filtered chats Nincsenek szűrt csevegések @@ -4010,6 +4253,10 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Nincsenek előzmények No comment provided by engineer. + + No info, try to reload + No comment provided by engineer. + No network connection Nincs hálózati kapcsolat @@ -4194,6 +4441,10 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Átköltöztetés megkezdése egy másik eszközre authentication reason + + Open server settings + No comment provided by engineer. + Open user profiles Felhasználói profilok megnyitása @@ -4299,6 +4550,10 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! Fogadott hivatkozás beillesztése No comment provided by engineer. + + Pending + 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. @@ -4324,6 +4579,11 @@ Ez az ön hivatkozása a(z) %@ csoporthoz! 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. + 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. @@ -4421,6 +4681,10 @@ Hiba: %@ Előnézet No comment provided by engineer. + + Previously connected servers + No comment provided by engineer. + Privacy & security Adatvédelem és biztonság @@ -4486,6 +4750,10 @@ Hiba: %@ Profiljelszó No comment provided by engineer. + + Profile theme + 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. @@ -4568,6 +4836,14 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Protokoll időkorlát KB-onként No comment provided by engineer. + + Proxied + No comment provided by engineer. + + + Proxied servers + No comment provided by engineer. + Push notifications Push értesítések @@ -4633,6 +4909,10 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Üzenet kézbesítési jelentés letiltva No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at Fogadva ekkor: @@ -4653,6 +4933,18 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Fogadott üzenet 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. 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. @@ -4683,11 +4975,31 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< A címzettek a beírás közben látják a frissítéseket. No comment provided by engineer. + + Reconnect + 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 + 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? Újrakapcsolódás a kiszolgálókhoz? @@ -4738,6 +5050,10 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Eltávolítás No comment provided by engineer. + + Remove image + No comment provided by engineer. + Remove member Eltávolítás @@ -4808,16 +5124,32 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Alaphelyzetbe állítás No comment provided by engineer. + + Reset all statistics + No comment provided by engineer. + + + Reset all statistics? + No comment provided by engineer. + Reset colors Színek alaphelyzetbe állítása No comment provided by engineer. + + Reset to app theme + No comment provided by engineer. + Reset to defaults Alaphelyzetbe állítás No comment provided by engineer. + + Reset to user theme + 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 @@ -4888,6 +5220,10 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Csevegési szolgáltatás indítása No comment provided by engineer. + + SMP server + No comment provided by engineer. + SMP servers Üzenetküldő (SMP) kiszolgálók @@ -5003,6 +5339,14 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Mentett üzenet message info title + + Scale + No comment provided by engineer. + + + Scan / Paste link + No comment provided by engineer. + Scan QR code QR-kód beolvasása @@ -5043,11 +5387,19 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Keresés, vagy SimpleX hivatkozás beillesztése No comment provided by engineer. + + Secondary + No comment provided by engineer. + Secure queue Biztonságos várólista server test step + + Secured + No comment provided by engineer. + Security assessment Biztonsági kiértékelés @@ -5063,6 +5415,10 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Választás No comment provided by engineer. + + Selected chat preferences prohibit this message. + No comment provided by engineer. + Self-destruct Önmegsemmisítés @@ -5113,6 +5469,10 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Eltűnő üzenet küldése No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews Hivatkozás előnézetek küldése @@ -5223,6 +5583,10 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Elküldve ekkor: %@ copied message info + + Sent directly + No comment provided by engineer. + Sent file event Elküldött fájl esemény @@ -5233,11 +5597,31 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Elküldött üzenet message info title + + Sent messages + 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 + 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. A kiszolgáló címe nem kompatibilis a hálózati beállításokkal. @@ -5258,6 +5642,10 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Sikertelen kiszolgáló-teszt! No comment provided by engineer. + + Server type + 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. @@ -5268,6 +5656,14 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Kiszolgálók 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 Munkamenet kód @@ -5283,6 +5679,10 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Ismerős nevének beállítása… No comment provided by engineer. + + Set default theme + No comment provided by engineer. + Set group preferences Csoportbeállítások megadása @@ -5403,6 +5803,10 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Megjelenítés: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address SimpleX azonosító @@ -5478,6 +5882,10 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Egyszerűsített inkognító mód No comment provided by engineer. + + Size + No comment provided by engineer. + Skip Kihagyás @@ -5523,6 +5931,14 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Átköltöztetés indítása No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop Megállítás @@ -5588,6 +6004,22 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Elküldés No comment provided by engineer. + + Subscribed + No comment provided by engineer. + + + Subscription errors + No comment provided by engineer. + + + Subscription percentage + No comment provided by engineer. + + + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat Támogassa a SimpleX Chatet @@ -5668,6 +6100,10 @@ Engedélyezze a beállításokban a *Hálózat és kiszolgálók* menüpontban.< Koppintson az új csevegés indításához No comment provided by engineer. + + Temporary file error + No comment provided by engineer. + Test failed at step %@. A teszt sikertelen volt a(z) %@ lépésnél. @@ -5805,9 +6241,8 @@ 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 No comment provided by engineer. @@ -5875,11 +6310,19 @@ 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. + 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 + 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: @@ -5947,11 +6390,19 @@ 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. + + Total + No comment provided by engineer. + Transport isolation Kapcsolat izolációs mód 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: %@). 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: %@). @@ -6144,6 +6595,10 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol A csevegés frissítése és megnyitása No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed Sikertelen feltöltés @@ -6154,6 +6609,14 @@ 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 + No comment provided by engineer. + + + Uploaded files + No comment provided by engineer. + Uploading archive Archívum feltöltése @@ -6229,6 +6692,10 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Felhasználói profil No comment provided by engineer. + + User selection + 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. @@ -6364,6 +6831,14 @@ 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 + 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 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 @@ -6469,11 +6944,19 @@ A kapcsolódáshoz kérje meg ismerősét, hogy hozzon létre egy másik kapcsol Rossz 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. + file error text + Wrong passphrase! Téves jelmondat! No comment provided by engineer. + + XFTP server + No comment provided by engineer. + XFTP servers XFTP kiszolgálók @@ -6556,6 +7039,10 @@ 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. + 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. @@ -6962,6 +7449,10 @@ A SimpleX kiszolgálók nem látjhatják profilját. és %lld további esemény No comment provided by engineer. + + attempts + No comment provided by engineer. + audio call (not e2e encrypted) hanghívás (nem e2e titkosított) @@ -6995,7 +7486,7 @@ A SimpleX kiszolgálók nem látjhatják profilját. blocked by admin letiltva az admin által - marked deleted chat item preview text + blocked chat item bold @@ -7152,6 +7643,10 @@ A SimpleX kiszolgálók nem látjhatják profilját. nap time unit + + decryption errors + No comment provided by engineer. + default (%@) alapértelmezett (%@) @@ -7202,6 +7697,10 @@ A SimpleX kiszolgálók nem látjhatják profilját. duplikálódott üzenet integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted e2e titkosított @@ -7282,6 +7781,10 @@ A SimpleX kiszolgálók nem látjhatják profilját. esemény történt No comment provided by engineer. + + expired + No comment provided by engineer. + forwarded továbbított @@ -7312,6 +7815,10 @@ A SimpleX kiszolgálók nem látjhatják profilját. 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. No comment provided by engineer. + + inactive + No comment provided by engineer. + incognito via contact address link inkognitó a kapcsolattartási hivatkozáson keresztül @@ -7489,6 +7996,14 @@ A SimpleX kiszolgálók nem látjhatják profilját. be group pref value + + other + No comment provided by engineer. + + + other errors + No comment provided by engineer. + owner tulajdonos @@ -7799,7 +8314,7 @@ last received msg: %2$@
- +
@@ -7836,7 +8351,7 @@ last received msg: %2$@
- +
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 b6fd18f551..8d311a803c 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 @@
- +
@@ -557,9 +557,8 @@ Info sull'indirizzo SimpleX No comment provided by engineer. - - Accent color - Colore principale + + Accent No comment provided by engineer. @@ -583,6 +582,14 @@ Accetta in incognito accept contact request via notification + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + 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. @@ -623,6 +630,18 @@ Aggiungi messaggio di benvenuto 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 Indirizzo @@ -648,6 +667,10 @@ Impostazioni di rete avanzate No comment provided by engineer. + + Advanced settings + No comment provided by engineer. + All app data is deleted. Tutti i dati dell'app vengono eliminati. @@ -663,6 +686,10 @@ Tutti i dati vengono cancellati quando inserito. No comment provided by engineer. + + All data is private to your device. + No comment provided by engineer. + All group members will remain connected. Tutti i membri del gruppo resteranno connessi. @@ -683,6 +710,10 @@ Tutti i nuovi messaggi da %@ verrranno nascosti! No comment provided by engineer. + + All users + No comment provided by engineer. + All your contacts will remain connected. Tutti i tuoi contatti resteranno connessi. @@ -883,6 +914,10 @@ Applica No comment provided by engineer. + + Apply to + No comment provided by engineer. + Archive and upload Archivia e carica @@ -958,6 +993,10 @@ Indietro No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address Indirizzo desktop errato @@ -983,6 +1022,10 @@ Messaggi migliorati No comment provided by engineer. + + Black + No comment provided by engineer. + Block Blocca @@ -1093,6 +1136,10 @@ Impossibile accedere al portachiavi per salvare la password del database No comment provided by engineer. + + Cannot forward message + No comment provided by engineer. + Cannot receive file Impossibile ricevere il file @@ -1164,6 +1211,10 @@ Archivio chat No comment provided by engineer. + + Chat colors + No comment provided by engineer. + Chat console Console della chat @@ -1209,6 +1260,10 @@ Preferenze della chat No comment provided by engineer. + + Chat theme + No comment provided by engineer. + Chats Chat @@ -1239,6 +1294,18 @@ Scegli dalla libreria 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 Svuota @@ -1264,9 +1331,8 @@ Annulla la verifica No comment provided by engineer. - - Colors - Colori + + Color mode No comment provided by engineer. @@ -1279,6 +1345,10 @@ Confronta i codici di sicurezza con i tuoi contatti. No comment provided by engineer. + + Completed + No comment provided by engineer. + Configure ICE servers Configura server ICE @@ -1388,16 +1458,28 @@ Questo è il tuo link una tantum! Connettersi con %@ No comment provided by engineer. + + Connected + No comment provided by engineer. + Connected desktop Desktop connesso No comment provided by engineer. + + Connected servers + No comment provided by engineer. + Connected to desktop Connesso al desktop No comment provided by engineer. + + Connecting + No comment provided by engineer. + Connecting to server… Connessione al server… @@ -1443,6 +1525,18 @@ Questo è il tuo link una tantum! Connessione scaduta No comment provided by engineer. + + Connection with desktop stopped + No comment provided by engineer. + + + Connections + No comment provided by engineer. + + + Connections subscribed + No comment provided by engineer. + Contact allows Il contatto lo consente @@ -1496,7 +1590,11 @@ Questo è il tuo link una tantum! Copy Copia - chat item action + No comment provided by engineer. + + + Copy error + No comment provided by engineer. Core version: v%@ @@ -1573,6 +1671,10 @@ Questo è il tuo link una tantum! Crea il tuo profilo No comment provided by engineer. + + Created + No comment provided by engineer. + Created at Creato il @@ -1608,6 +1710,10 @@ Questo è il tuo link una tantum! Password attuale… No comment provided by engineer. + + Current user + No comment provided by engineer. + Currently maximum supported file size is %@. Attualmente la dimensione massima supportata è di %@. @@ -1618,11 +1724,19 @@ Questo è il tuo link una tantum! Tempo personalizzato No comment provided by engineer. + + Customize theme + No comment provided by engineer. + Dark Scuro No comment provided by engineer. + + Dark mode colors + No comment provided by engineer. + Database ID ID database @@ -1927,6 +2041,10 @@ Non è reversibile! Eliminare il profilo utente? No comment provided by engineer. + + Deleted + No comment provided by engineer. + Deleted at Eliminato il @@ -1937,6 +2055,10 @@ Non è reversibile! Eliminato il: %@ copied message info + + Deletion errors + No comment provided by engineer. + Delivery Consegna @@ -1977,6 +2099,14 @@ Non è reversibile! Errore del server di destinazione: %@ snd error text + + Detailed statistics + No comment provided by engineer. + + + Details + No comment provided by engineer. + Develop Sviluppa @@ -2132,6 +2262,10 @@ Non è reversibile! Scarica chat item action + + Download errors + No comment provided by engineer. + Download failed Scaricamento fallito @@ -2142,6 +2276,14 @@ Non è reversibile! Scarica file server test step + + Downloaded + No comment provided by engineer. + + + Downloaded files + No comment provided by engineer. + Downloading archive Scaricamento archivio @@ -2512,6 +2654,10 @@ Non è reversibile! Errore nell'esportazione del database della chat No comment provided by engineer. + + Error exporting theme: %@ + No comment provided by engineer. + Error importing chat database Errore nell'importazione del database della chat @@ -2537,11 +2683,23 @@ Non è reversibile! Errore nella ricezione del 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 Errore nella rimozione del membro No comment provided by engineer. + + Error resetting statistics + No comment provided by engineer. + Error saving %@ servers Errore nel salvataggio dei server %@ @@ -2660,7 +2818,8 @@ Non è reversibile! Error: %@ Errore: %@ - snd error text + file error text + snd error text Error: URL is invalid @@ -2672,6 +2831,10 @@ Non è reversibile! Errore: nessun file di database No comment provided by engineer. + + Errors + No comment provided by engineer. + Even when disabled in the conversation. Anche quando disattivato nella conversazione. @@ -2697,6 +2860,10 @@ Non è reversibile! Errore di esportazione: No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. Archivio database esportato. @@ -2732,6 +2899,26 @@ Non è reversibile! Preferito No comment provided by engineer. + + 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. Il file verrà eliminato dai server. @@ -2921,6 +3108,14 @@ Errore: %2$@ GIF e adesivi No comment provided by engineer. + + Good afternoon! + message preview + + + Good morning! + message preview + Group Gruppo @@ -3201,6 +3396,10 @@ Errore: %2$@ Importazione fallita No comment provided by engineer. + + Import theme + No comment provided by engineer. + Importing archive Importazione archivio @@ -3323,6 +3522,10 @@ Errore: %2$@ Interfaccia No comment provided by engineer. + + Interface colors + No comment provided by engineer. + Invalid QR code Codice QR non valido @@ -3666,6 +3869,10 @@ Questo è il tuo link per il gruppo %@! Membro No comment provided by engineer. + + Member inactive + 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. @@ -3681,6 +3888,10 @@ Questo è il tuo link per il gruppo %@! Il membro verrà rimosso dal gruppo, non è reversibile! No comment provided by engineer. + + Menus + No comment provided by engineer. + Message delivery error Errore di recapito del messaggio @@ -3701,6 +3912,14 @@ Questo è il tuo link per il gruppo %@! Bozza dei messaggi 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. @@ -3735,6 +3954,18 @@ Questo è il tuo link per il gruppo %@! La fonte del messaggio resta privata. No comment provided by engineer. + + Message status + No comment provided by engineer. + + + Message status: %@ + copied message info + + + Message subscriptions + No comment provided by engineer. + Message text Testo del messaggio @@ -3760,6 +3991,14 @@ Questo è il tuo link per il gruppo %@! I messaggi da %@ verranno mostrati! 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. I messaggi, i file e le chiamate sono protetti da **crittografia end-to-end** con perfect forward secrecy, ripudio e recupero da intrusione. @@ -3995,6 +4234,10 @@ 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. + item status description + No filtered chats Nessuna chat filtrata @@ -4010,6 +4253,10 @@ Questo è il tuo link per il gruppo %@! Nessuna cronologia No comment provided by engineer. + + No info, try to reload + No comment provided by engineer. + No network connection Nessuna connessione di rete @@ -4194,6 +4441,10 @@ Questo è il tuo link per il gruppo %@! Apri migrazione ad un altro dispositivo authentication reason + + Open server settings + No comment provided by engineer. + Open user profiles Apri i profili utente @@ -4299,6 +4550,10 @@ Questo è il tuo link per il gruppo %@! Incolla il link che hai ricevuto No comment provided by engineer. + + Pending + 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. @@ -4324,6 +4579,11 @@ Questo è il tuo link per il gruppo %@! 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. + 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. @@ -4421,6 +4681,10 @@ Errore: %@ Anteprima No comment provided by engineer. + + Previously connected servers + No comment provided by engineer. + Privacy & security Privacy e sicurezza @@ -4486,6 +4750,10 @@ Errore: %@ Password del profilo No comment provided by engineer. + + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. L'aggiornamento del profilo verrà inviato ai tuoi contatti. @@ -4568,6 +4836,14 @@ Attivalo nelle impostazioni *Rete e server*. Scadenza del protocollo per KB No comment provided by engineer. + + Proxied + No comment provided by engineer. + + + Proxied servers + No comment provided by engineer. + Push notifications Notifiche push @@ -4633,6 +4909,10 @@ Attivalo nelle impostazioni *Rete e server*. Le ricevute sono disattivate No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at Ricevuto il @@ -4653,6 +4933,18 @@ Attivalo nelle impostazioni *Rete e server*. Messaggio ricevuto 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. L'indirizzo di ricezione verrà cambiato in un server diverso. La modifica dell'indirizzo verrà completata dopo che il mittente sarà in linea. @@ -4683,11 +4975,31 @@ Attivalo nelle impostazioni *Rete e server*. I destinatari vedono gli aggiornamenti mentre li digiti. No comment provided by engineer. + + Reconnect + 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 + 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? Riconnettere i server? @@ -4738,6 +5050,10 @@ Attivalo nelle impostazioni *Rete e server*. Rimuovi No comment provided by engineer. + + Remove image + No comment provided by engineer. + Remove member Rimuovi membro @@ -4808,16 +5124,32 @@ Attivalo nelle impostazioni *Rete e server*. Ripristina No comment provided by engineer. + + Reset all statistics + No comment provided by engineer. + + + Reset all statistics? + No comment provided by engineer. + Reset colors Ripristina i colori No comment provided by engineer. + + Reset to app theme + No comment provided by engineer. + Reset to defaults Ripristina i predefiniti No comment provided by engineer. + + Reset to user theme + No comment provided by engineer. + Restart the app to create a new chat profile Riavvia l'app per creare un nuovo profilo di chat @@ -4888,6 +5220,10 @@ Attivalo nelle impostazioni *Rete e server*. Avvia chat No comment provided by engineer. + + SMP server + No comment provided by engineer. + SMP servers Server SMP @@ -5003,6 +5339,14 @@ Attivalo nelle impostazioni *Rete e server*. Messaggio salvato message info title + + Scale + No comment provided by engineer. + + + Scan / Paste link + No comment provided by engineer. + Scan QR code Scansiona codice QR @@ -5043,11 +5387,19 @@ Attivalo nelle impostazioni *Rete e server*. Cerca o incolla un link SimpleX No comment provided by engineer. + + Secondary + No comment provided by engineer. + Secure queue Coda sicura server test step + + Secured + No comment provided by engineer. + Security assessment Valutazione della sicurezza @@ -5063,6 +5415,10 @@ Attivalo nelle impostazioni *Rete e server*. Seleziona No comment provided by engineer. + + Selected chat preferences prohibit this message. + No comment provided by engineer. + Self-destruct Autodistruzione @@ -5113,6 +5469,10 @@ Attivalo nelle impostazioni *Rete e server*. Invia messaggio a tempo No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews Invia anteprime dei link @@ -5223,6 +5583,10 @@ Attivalo nelle impostazioni *Rete e server*. Inviato il: %@ copied message info + + Sent directly + No comment provided by engineer. + Sent file event Evento file inviato @@ -5233,11 +5597,31 @@ Attivalo nelle impostazioni *Rete e server*. Messaggio inviato message info title + + Sent messages + 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 + 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. L'indirizzo del server non è compatibile con le impostazioni di rete. @@ -5258,6 +5642,10 @@ Attivalo nelle impostazioni *Rete e server*. Test del server fallito! No comment provided by engineer. + + Server type + No comment provided by engineer. + Server version is incompatible with network settings. La versione del server non è compatibile con le impostazioni di rete. @@ -5268,6 +5656,14 @@ Attivalo nelle impostazioni *Rete e server*. Server 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 Codice di sessione @@ -5283,6 +5679,10 @@ Attivalo nelle impostazioni *Rete e server*. Imposta nome del contatto… No comment provided by engineer. + + Set default theme + No comment provided by engineer. + Set group preferences Imposta le preferenze del gruppo @@ -5403,6 +5803,10 @@ Attivalo nelle impostazioni *Rete e server*. Mostra: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address Indirizzo SimpleX @@ -5478,6 +5882,10 @@ Attivalo nelle impostazioni *Rete e server*. Modalità incognito semplificata No comment provided by engineer. + + Size + No comment provided by engineer. + Skip Salta @@ -5523,6 +5931,14 @@ Attivalo nelle impostazioni *Rete e server*. Avvia la migrazione No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop Ferma @@ -5588,6 +6004,22 @@ Attivalo nelle impostazioni *Rete e server*. Invia No comment provided by engineer. + + Subscribed + No comment provided by engineer. + + + Subscription errors + No comment provided by engineer. + + + Subscription percentage + No comment provided by engineer. + + + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat Supporta SimpleX Chat @@ -5668,6 +6100,10 @@ Attivalo nelle impostazioni *Rete e server*. Tocca per iniziare una chat No comment provided by engineer. + + Temporary file error + No comment provided by engineer. + Test failed at step %@. Test fallito al passo %@. @@ -5805,9 +6241,8 @@ 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 No comment provided by engineer. @@ -5875,11 +6310,19 @@ 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. + 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 + No comment provided by engineer. + To ask any questions and to receive updates: Per porre domande e ricevere aggiornamenti: @@ -5947,11 +6390,19 @@ Ti verrà chiesto di completare l'autenticazione prima di attivare questa funzio Attiva/disattiva l'incognito quando ti colleghi. No comment provided by engineer. + + Total + No comment provided by engineer. + Transport isolation Isolamento del trasporto 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: %@). Tentativo di connessione al server usato per ricevere messaggi da questo contatto (errore: %@). @@ -6144,6 +6595,10 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Aggiorna e apri chat No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed Invio fallito @@ -6154,6 +6609,14 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Invia file server test step + + Uploaded + No comment provided by engineer. + + + Uploaded files + No comment provided by engineer. + Uploading archive Invio dell'archivio @@ -6229,6 +6692,10 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e Profilo utente No comment provided by engineer. + + User selection + No comment provided by engineer. + Using .onion hosts requires compatible VPN provider. L'uso di host .onion richiede un fornitore di VPN compatibile. @@ -6364,6 +6831,14 @@ 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 + 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 Attenzione: avviare la chat su più dispositivi non è supportato e provocherà problemi di recapito dei messaggi @@ -6469,11 +6944,19 @@ Per connetterti, chiedi al tuo contatto di creare un altro link di connessione e 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. + file error text + Wrong passphrase! Password sbagliata! No comment provided by engineer. + + XFTP server + No comment provided by engineer. + XFTP servers Server XFTP @@ -6556,6 +7039,10 @@ 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. + 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. @@ -6962,6 +7449,10 @@ I server di SimpleX non possono vedere il tuo profilo. e altri %lld eventi No comment provided by engineer. + + attempts + No comment provided by engineer. + audio call (not e2e encrypted) chiamata audio (non crittografata e2e) @@ -6995,7 +7486,7 @@ I server di SimpleX non possono vedere il tuo profilo. blocked by admin bloccato dall'amministratore - marked deleted chat item preview text + blocked chat item bold @@ -7152,6 +7643,10 @@ I server di SimpleX non possono vedere il tuo profilo. giorni time unit + + decryption errors + No comment provided by engineer. + default (%@) predefinito (%@) @@ -7202,6 +7697,10 @@ I server di SimpleX non possono vedere il tuo profilo. messaggio duplicato integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted crittografato e2e @@ -7282,6 +7781,10 @@ I server di SimpleX non possono vedere il tuo profilo. evento accaduto No comment provided by engineer. + + expired + No comment provided by engineer. + forwarded inoltrato @@ -7312,6 +7815,10 @@ 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 + No comment provided by engineer. + incognito via contact address link incognito via link indirizzo del contatto @@ -7489,6 +7996,14 @@ I server di SimpleX non possono vedere il tuo profilo. on group pref value + + other + No comment provided by engineer. + + + other errors + No comment provided by engineer. + owner proprietario @@ -7799,7 +8314,7 @@ last received msg: %2$@
- +
@@ -7836,7 +8351,7 @@ last received msg: %2$@
- +
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 395fcb9326..43f0f8f6cd 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 @@
- +
@@ -551,9 +551,8 @@ SimpleXアドレスについて No comment provided by engineer. - - Accent color - アクセントカラー + + Accent No comment provided by engineer. @@ -577,6 +576,14 @@ シークレットモードで承諾 accept contact request via notification + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + 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. プロフィールにアドレスを追加し、連絡先があなたのアドレスを他の人と共有できるようにします。プロフィールの更新は連絡先に送信されます。 @@ -616,6 +623,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 +659,10 @@ ネットワーク詳細設定 No comment provided by engineer. + + Advanced settings + No comment provided by engineer. + All app data is deleted. すべてのアプリデータが削除されます。 @@ -655,6 +678,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 +700,10 @@ All new messages from %@ will be hidden! No comment provided by engineer. + + All users + No comment provided by engineer. + All your contacts will remain connected. あなたの連絡先が繋がったまま継続します。 @@ -870,6 +901,10 @@ Apply No comment provided by engineer. + + Apply to + No comment provided by engineer. + Archive and upload No comment provided by engineer. @@ -943,6 +978,10 @@ 戻る No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address No comment provided by engineer. @@ -966,6 +1005,10 @@ より良いメッセージ No comment provided by engineer. + + Black + No comment provided by engineer. + Block No comment provided by engineer. @@ -1067,6 +1110,10 @@ データベースのパスワードを保存するためのキーチェーンにアクセスできません No comment provided by engineer. + + Cannot forward message + No comment provided by engineer. + Cannot receive file ファイル受信ができません @@ -1136,6 +1183,10 @@ チャットのアーカイブ No comment provided by engineer. + + Chat colors + No comment provided by engineer. + Chat console チャットのコンソール @@ -1179,6 +1230,10 @@ チャット設定 No comment provided by engineer. + + Chat theme + No comment provided by engineer. + Chats チャット @@ -1208,6 +1263,18 @@ ライブラリから選択 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 消す @@ -1232,9 +1299,8 @@ 検証を消す No comment provided by engineer. - - Colors - + + Color mode No comment provided by engineer. @@ -1247,6 +1313,10 @@ 連絡先とセキュリティコードを確認する。 No comment provided by engineer. + + Completed + No comment provided by engineer. + Configure ICE servers ICEサーバを設定 @@ -1343,14 +1413,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… サーバーに接続中… @@ -1394,6 +1476,18 @@ 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. + + + Connections subscribed + No comment provided by engineer. + Contact allows 連絡先の許可 @@ -1447,7 +1541,11 @@ This is your own one-time link! Copy コピー - chat item action + No comment provided by engineer. + + + Copy error + No comment provided by engineer. Core version: v%@ @@ -1520,6 +1618,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. @@ -1551,6 +1653,10 @@ This is your own one-time link! 現在の暗証フレーズ… No comment provided by engineer. + + Current user + No comment provided by engineer. + Currently maximum supported file size is %@. 現在サポートされている最大ファイルサイズは %@. @@ -1561,11 +1667,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 @@ -1865,6 +1979,10 @@ This cannot be undone! ユーザープロフィールを削除しますか? No comment provided by engineer. + + Deleted + No comment provided by engineer. + Deleted at 削除完了 @@ -1875,6 +1993,10 @@ This cannot be undone! 削除完了: %@ copied message info + + Deletion errors + No comment provided by engineer. + Delivery 配信 @@ -1911,6 +2033,14 @@ This cannot be undone! Destination server error: %@ snd error text + + Detailed statistics + No comment provided by engineer. + + + Details + No comment provided by engineer. + Develop 開発 @@ -2060,6 +2190,10 @@ This cannot be undone! Download chat item action + + Download errors + No comment provided by engineer. + Download failed No comment provided by engineer. @@ -2069,6 +2203,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. @@ -2424,6 +2566,10 @@ This cannot be undone! チャットデータベースのエキスポートにエラー発生 No comment provided by engineer. + + Error exporting theme: %@ + No comment provided by engineer. + Error importing chat database チャットデータベースのインポートにエラー発生 @@ -2448,11 +2594,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 %@ サーバの保存エラー @@ -2566,7 +2724,8 @@ This cannot be undone! Error: %@ エラー : %@ - snd error text + file error text + snd error text Error: URL is invalid @@ -2578,6 +2737,10 @@ This cannot be undone! エラー: データベースが存在しません No comment provided by engineer. + + Errors + No comment provided by engineer. + Even when disabled in the conversation. 会話中に無効になっている場合でも。 @@ -2602,6 +2765,10 @@ This cannot be undone! エクスポートエラー: No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. データベースのアーカイブをエクスポートします。 @@ -2635,6 +2802,26 @@ This cannot be undone! お気に入り No comment provided by engineer. + + 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. ファイルはサーバーから削除されます。 @@ -2810,6 +2997,14 @@ Error: %2$@ GIFとステッカー No comment provided by engineer. + + Good afternoon! + message preview + + + Good morning! + message preview + Group グループ @@ -3084,6 +3279,10 @@ Error: %2$@ Import failed No comment provided by engineer. + + Import theme + No comment provided by engineer. + Importing archive No comment provided by engineer. @@ -3200,6 +3399,10 @@ Error: %2$@ インターフェース No comment provided by engineer. + + Interface colors + No comment provided by engineer. + Invalid QR code No comment provided by engineer. @@ -3526,6 +3729,10 @@ This is your link for group %@! メンバー No comment provided by engineer. + + Member inactive + item status text + Member role will be changed to "%@". All group members will be notified. メンバーの役割が "%@" に変更されます。 グループメンバー全員に通知されます。 @@ -3541,6 +3748,10 @@ This is your link for group %@! メンバーをグループから除名する (※元に戻せません※)! No comment provided by engineer. + + Menus + No comment provided by engineer. + Message delivery error メッセージ送信エラー @@ -3559,6 +3770,14 @@ This is your link for group %@! メッセージの下書き 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. @@ -3590,6 +3809,18 @@ This is your link for group %@! Message source remains private. No comment provided by engineer. + + Message status + No comment provided by engineer. + + + Message status: %@ + copied message info + + + Message subscriptions + No comment provided by engineer. + Message text メッセージ内容 @@ -3613,6 +3844,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. @@ -3834,6 +4073,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 フィルタされたチャットはありません @@ -3849,6 +4092,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. @@ -4028,6 +4275,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 ユーザープロフィールを開く @@ -4123,6 +4374,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. あなたと繋がることができるのは、あなたからリンクを頂いた方のみです。 @@ -4147,6 +4402,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. リンクが正しいかどうかご確認ください。または、連絡相手にもう一度リンクをお求めください。 @@ -4241,6 +4501,10 @@ Error: %@ プレビュー No comment provided by engineer. + + Previously connected servers + No comment provided by engineer. + Privacy & security プライバシーとセキュリティ @@ -4299,6 +4563,10 @@ Error: %@ プロフィールのパスワード No comment provided by engineer. + + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. 連絡先にプロフィール更新のお知らせが届きます。 @@ -4377,6 +4645,14 @@ Enable in *Network & servers* settings. KB あたりのプロトコル タイムアウト No comment provided by engineer. + + Proxied + No comment provided by engineer. + + + Proxied servers + No comment provided by engineer. + Push notifications プッシュ通知 @@ -4438,6 +4714,10 @@ Enable in *Network & servers* settings. Receipts are disabled No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at 受信 @@ -4458,6 +4738,18 @@ Enable in *Network & servers* settings. 受信したメッセージ 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でなければ機能しません。アドレス変更が完了すると、会話にメッセージが出ます。連絡相手 (またはグループのメンバー) からメッセージを受信できないかをご確認ください。 @@ -4486,11 +4778,31 @@ Enable in *Network & servers* settings. 受信者には、入力時に更新内容が表示されます。 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? サーバーに再接続しますか? @@ -4541,6 +4853,10 @@ Enable in *Network & servers* settings. 削除 No comment provided by engineer. + + Remove image + No comment provided by engineer. + Remove member メンバーを除名する @@ -4606,16 +4922,32 @@ Enable in *Network & servers* settings. 戻す 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 新しいチャットプロファイルを作成するためにアプリを再起動する @@ -4685,6 +5017,10 @@ Enable in *Network & servers* settings. チャット起動 No comment provided by engineer. + + SMP server + No comment provided by engineer. + SMP servers SMPサーバ @@ -4795,6 +5131,14 @@ Enable in *Network & servers* settings. Saved message message info title + + Scale + No comment provided by engineer. + + + Scan / Paste link + No comment provided by engineer. + Scan QR code QRコードを読み込む @@ -4832,11 +5176,19 @@ Enable in *Network & servers* settings. 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 セキュリティ評価 @@ -4852,6 +5204,10 @@ Enable in *Network & servers* settings. 選択 No comment provided by engineer. + + Selected chat preferences prohibit this message. + No comment provided by engineer. + Self-destruct 自己破壊 @@ -4901,6 +5257,10 @@ Enable in *Network & servers* settings. 消えるメッセージを送信 No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews リンクのプレビューを送信 @@ -5001,6 +5361,10 @@ Enable in *Network & servers* settings. 送信日時: %@ copied message info + + Sent directly + No comment provided by engineer. + Sent file event 送信済みファイルイベント @@ -5011,11 +5375,31 @@ Enable in *Network & servers* settings. 送信 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. @@ -5035,6 +5419,10 @@ Enable in *Network & servers* settings. サーバテスト失敗! No comment provided by engineer. + + Server type + No comment provided by engineer. + Server version is incompatible with network settings. srv error text @@ -5044,6 +5432,14 @@ Enable in *Network & servers* settings. サーバ 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. @@ -5058,6 +5454,10 @@ Enable in *Network & servers* settings. 連絡先の名前を設定… No comment provided by engineer. + + Set default theme + No comment provided by engineer. + Set group preferences グループの設定を行う @@ -5172,6 +5572,10 @@ Enable in *Network & servers* settings. 表示する: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address SimpleXアドレス @@ -5245,6 +5649,10 @@ Enable in *Network & servers* settings. シークレットモードの簡素化 No comment provided by engineer. + + Size + No comment provided by engineer. + Skip スキップ @@ -5288,6 +5696,14 @@ Enable in *Network & servers* settings. 移行の開始 No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop 停止 @@ -5351,6 +5767,22 @@ Enable in *Network & servers* settings. 送信 No comment provided by engineer. + + Subscribed + No comment provided by engineer. + + + Subscription errors + No comment provided by engineer. + + + Subscription percentage + No comment provided by engineer. + + + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat Simplex Chatを支援 @@ -5428,6 +5860,10 @@ Enable in *Network & servers* settings. タップして新しいチャットを始める No comment provided by engineer. + + Temporary file error + No comment provided by engineer. + Test failed at step %@. テストはステップ %@ で失敗しました。 @@ -5562,9 +5998,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. @@ -5625,11 +6060,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: 質問や最新情報を受け取るには: @@ -5694,11 +6137,19 @@ You will be prompted to complete authentication before this feature is enabled.< Toggle incognito when connecting. 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: %@). この連絡先からのメッセージの受信に使用されるサーバーに接続しようとしています (エラー: %@)。 @@ -5881,6 +6332,10 @@ To connect, please ask your contact to create another connection link and check アップグレードしてチャットを開く No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed No comment provided by engineer. @@ -5890,6 +6345,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. @@ -5959,6 +6422,10 @@ To connect, please ask your contact to create another connection link and check ユーザープロフィール No comment provided by engineer. + + User selection + No comment provided by engineer. + Using .onion hosts requires compatible VPN provider. .onionホストを使用するには、互換性のあるVPNプロバイダーが必要です。 @@ -6085,6 +6552,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. @@ -6179,11 +6654,19 @@ To connect, please ask your contact to create another connection link and check 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 server + No comment provided by engineer. + XFTP servers XFTPサーバ @@ -6257,6 +6740,10 @@ 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. デバイスやアプリの認証を行わずに、ロック画面から通話を受けることができます。 @@ -6651,6 +7138,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 and %lld other events No comment provided by engineer. + + attempts + No comment provided by engineer. + audio call (not e2e encrypted) 音声通話 (エンドツーエンド暗号化なし) @@ -6680,7 +7171,7 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 blocked by admin - marked deleted chat item preview text + blocked chat item bold @@ -6835,6 +7326,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 time unit + + decryption errors + No comment provided by engineer. + default (%@) デフォルト (%@) @@ -6884,6 +7379,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 重複メッセージ integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted エンドツーエンド暗号化済み @@ -6964,6 +7463,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 イベント発生 No comment provided by engineer. + + expired + No comment provided by engineer. + forwarded No comment provided by engineer. @@ -6993,6 +7496,10 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 iOS キーチェーンは、アプリを再起動するかパスフレーズを変更した後にパスフレーズを安全に保存するために使用され、プッシュ通知を受信できるようになります。 No comment provided by engineer. + + inactive + No comment provided by engineer. + incognito via contact address link 連絡先リンク経由でシークレットモード @@ -7169,6 +7676,14 @@ SimpleX サーバーはあなたのプロファイルを参照できません。 オン group pref value + + other + No comment provided by engineer. + + + other errors + No comment provided by engineer. + owner オーナー @@ -7459,7 +7974,7 @@ last received msg: %2$@
- +
@@ -7495,7 +8010,7 @@ last received msg: %2$@
- +
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/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index e4ee887f4b..fdfd49c3d3 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 @@
- +
@@ -557,9 +557,8 @@ Over SimpleX adres No comment provided by engineer. - - Accent color - Accent kleur + + Accent No comment provided by engineer. @@ -583,6 +582,14 @@ Accepteer incognito accept contact request via notification + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + 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. @@ -623,6 +630,18 @@ Welkomst bericht toevoegen 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 +667,10 @@ Geavanceerde netwerk instellingen No comment provided by engineer. + + Advanced settings + No comment provided by engineer. + All app data is deleted. Alle app-gegevens worden verwijderd. @@ -663,6 +686,10 @@ Alle gegevens worden bij het invoeren gewist. No comment provided by engineer. + + All data is private to your device. + No comment provided by engineer. + All group members will remain connected. Alle groepsleden blijven verbonden. @@ -683,6 +710,10 @@ Alle nieuwe berichten van %@ worden verborgen! No comment provided by engineer. + + All users + No comment provided by engineer. + All your contacts will remain connected. Al uw contacten blijven verbonden. @@ -883,6 +914,10 @@ Toepassen No comment provided by engineer. + + Apply to + No comment provided by engineer. + Archive and upload Archiveren en uploaden @@ -958,6 +993,10 @@ Terug No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address Onjuist desktopadres @@ -983,6 +1022,10 @@ Betere berichten No comment provided by engineer. + + Black + No comment provided by engineer. + Block Blokkeren @@ -1093,6 +1136,10 @@ Geen toegang tot de keychain om database wachtwoord op te slaan No comment provided by engineer. + + Cannot forward message + No comment provided by engineer. + Cannot receive file Kan bestand niet ontvangen @@ -1164,6 +1211,10 @@ Gesprek archief No comment provided by engineer. + + Chat colors + No comment provided by engineer. + Chat console Chat console @@ -1209,6 +1260,10 @@ Gesprek voorkeuren No comment provided by engineer. + + Chat theme + No comment provided by engineer. + Chats Gesprekken @@ -1239,6 +1294,18 @@ Kies uit bibliotheek 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 Wissen @@ -1264,9 +1331,8 @@ Verwijderd verificatie No comment provided by engineer. - - Colors - Kleuren + + Color mode No comment provided by engineer. @@ -1279,6 +1345,10 @@ Vergelijk beveiligingscodes met je contacten. No comment provided by engineer. + + Completed + No comment provided by engineer. + Configure ICE servers ICE servers configureren @@ -1388,16 +1458,28 @@ Dit is uw eigen eenmalige link! Verbonden met %@ No comment provided by engineer. + + Connected + No comment provided by engineer. + Connected desktop Verbonden desktop No comment provided by engineer. + + Connected servers + No comment provided by engineer. + Connected to desktop Verbonden met desktop No comment provided by engineer. + + Connecting + No comment provided by engineer. + Connecting to server… Verbinden met de server… @@ -1443,6 +1525,18 @@ Dit is uw eigen eenmalige link! Timeout verbinding No comment provided by engineer. + + Connection with desktop stopped + No comment provided by engineer. + + + Connections + No comment provided by engineer. + + + Connections subscribed + No comment provided by engineer. + Contact allows Contact maakt het mogelijk @@ -1496,7 +1590,11 @@ Dit is uw eigen eenmalige link! Copy Kopiëren - chat item action + No comment provided by engineer. + + + Copy error + No comment provided by engineer. Core version: v%@ @@ -1573,6 +1671,10 @@ Dit is uw eigen eenmalige link! Maak je profiel aan No comment provided by engineer. + + Created + No comment provided by engineer. + Created at Gemaakt op @@ -1608,6 +1710,10 @@ Dit is uw eigen eenmalige link! Huidige wachtwoord… No comment provided by engineer. + + Current user + No comment provided by engineer. + Currently maximum supported file size is %@. De momenteel maximaal ondersteunde bestandsgrootte is %@. @@ -1618,11 +1724,19 @@ Dit is uw eigen eenmalige link! Aangepaste tijd No comment provided by engineer. + + Customize theme + No comment provided by engineer. + Dark Donker No comment provided by engineer. + + Dark mode colors + No comment provided by engineer. + Database ID Database-ID @@ -1927,6 +2041,10 @@ Dit kan niet ongedaan gemaakt worden! Gebruikers profiel verwijderen? No comment provided by engineer. + + Deleted + No comment provided by engineer. + Deleted at Verwijderd om @@ -1937,6 +2055,10 @@ Dit kan niet ongedaan gemaakt worden! Verwijderd om: %@ copied message info + + Deletion errors + No comment provided by engineer. + Delivery Bezorging @@ -1977,6 +2099,14 @@ Dit kan niet ongedaan gemaakt worden! Bestemmingsserverfout: %@ snd error text + + Detailed statistics + No comment provided by engineer. + + + Details + No comment provided by engineer. + Develop Ontwikkelen @@ -2132,6 +2262,10 @@ Dit kan niet ongedaan gemaakt worden! Downloaden chat item action + + Download errors + No comment provided by engineer. + Download failed Download mislukt @@ -2142,6 +2276,14 @@ Dit kan niet ongedaan gemaakt worden! Download bestand server test step + + Downloaded + No comment provided by engineer. + + + Downloaded files + No comment provided by engineer. + Downloading archive Archief downloaden @@ -2512,6 +2654,10 @@ Dit kan niet ongedaan gemaakt worden! Fout bij het exporteren van de chat database No comment provided by engineer. + + Error exporting theme: %@ + No comment provided by engineer. + Error importing chat database Fout bij het importeren van de chat database @@ -2537,11 +2683,23 @@ Dit kan niet ongedaan gemaakt worden! Fout bij ontvangen van bestand No comment provided by engineer. + + Error reconnecting server + No comment provided by engineer. + + + Error reconnecting servers + No comment provided by engineer. + Error removing member Fout bij verwijderen van lid No comment provided by engineer. + + Error resetting statistics + No comment provided by engineer. + Error saving %@ servers Fout bij opslaan van %@ servers @@ -2660,7 +2818,8 @@ Dit kan niet ongedaan gemaakt worden! Error: %@ Fout: %@ - snd error text + file error text + snd error text Error: URL is invalid @@ -2672,6 +2831,10 @@ Dit kan niet ongedaan gemaakt worden! Fout: geen database bestand No comment provided by engineer. + + Errors + No comment provided by engineer. + Even when disabled in the conversation. Zelfs wanneer uitgeschakeld in het gesprek. @@ -2697,6 +2860,10 @@ Dit kan niet ongedaan gemaakt worden! Exportfout: No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. Geëxporteerd database archief. @@ -2732,6 +2899,26 @@ Dit kan niet ongedaan gemaakt worden! Favoriet No comment provided by engineer. + + 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. Het bestand wordt van de servers verwijderd. @@ -2921,6 +3108,14 @@ Fout: %2$@ GIF's en stickers No comment provided by engineer. + + Good afternoon! + message preview + + + Good morning! + message preview + Group Groep @@ -3201,6 +3396,10 @@ Fout: %2$@ Importeren is mislukt No comment provided by engineer. + + Import theme + No comment provided by engineer. + Importing archive Archief importeren @@ -3323,6 +3522,10 @@ Fout: %2$@ Interface No comment provided by engineer. + + Interface colors + No comment provided by engineer. + Invalid QR code Ongeldige QR-code @@ -3666,6 +3869,10 @@ Dit is jouw link voor groep %@! Lid No comment provided by engineer. + + Member inactive + 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. @@ -3681,6 +3888,10 @@ Dit is jouw link voor groep %@! Lid wordt uit de groep verwijderd, dit kan niet ongedaan worden gemaakt! No comment provided by engineer. + + Menus + No comment provided by engineer. + Message delivery error Fout bij bezorging van bericht @@ -3701,6 +3912,14 @@ Dit is jouw link voor groep %@! Concept bericht 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. @@ -3735,6 +3954,18 @@ Dit is jouw link voor groep %@! Berichtbron blijft privé. No comment provided by engineer. + + Message status + No comment provided by engineer. + + + Message status: %@ + copied message info + + + Message subscriptions + No comment provided by engineer. + Message text Bericht tekst @@ -3760,6 +3991,14 @@ Dit is jouw link voor groep %@! Berichten van %@ worden getoond! 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. Berichten, bestanden en oproepen worden beschermd door **end-to-end codering** met perfecte voorwaartse geheimhouding, afwijzing en inbraakherstel. @@ -3995,6 +4234,10 @@ Dit is jouw link voor groep %@! Geen apparaattoken! No comment provided by engineer. + + No direct connection yet, message is forwarded by admin. + item status description + No filtered chats Geen gefilterde gesprekken @@ -4010,6 +4253,10 @@ Dit is jouw link voor groep %@! Geen geschiedenis No comment provided by engineer. + + No info, try to reload + No comment provided by engineer. + No network connection Geen netwerkverbinding @@ -4194,6 +4441,10 @@ Dit is jouw link voor groep %@! Open de migratie naar een ander apparaat authentication reason + + Open server settings + No comment provided by engineer. + Open user profiles Gebruikers profielen openen @@ -4299,6 +4550,10 @@ Dit is jouw link voor groep %@! Plak de link die je hebt ontvangen No comment provided by engineer. + + Pending + 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. @@ -4324,6 +4579,11 @@ Dit is jouw link voor groep %@! 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. + 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. @@ -4421,6 +4681,10 @@ Fout: %@ Voorbeeld No comment provided by engineer. + + Previously connected servers + No comment provided by engineer. + Privacy & security Privacy en beveiliging @@ -4486,6 +4750,10 @@ Fout: %@ Profiel wachtwoord No comment provided by engineer. + + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. Profiel update wordt naar uw contacten verzonden. @@ -4568,6 +4836,14 @@ Schakel dit in in *Netwerk en servers*-instellingen. 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 meldingen @@ -4633,6 +4909,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Bevestigingen zijn uitgeschakeld No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at Ontvangen op @@ -4653,6 +4933,18 @@ Schakel dit in in *Netwerk en servers*-instellingen. Ontvangen bericht 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. Het ontvangstadres wordt gewijzigd naar een andere server. Adres wijziging wordt voltooid nadat de afzender online is. @@ -4683,11 +4975,31 @@ Schakel dit in in *Netwerk en servers*-instellingen. Ontvangers zien updates terwijl u ze typt. No comment provided by engineer. + + Reconnect + 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 + 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? Servers opnieuw verbinden? @@ -4738,6 +5050,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Verwijderen No comment provided by engineer. + + Remove image + No comment provided by engineer. + Remove member Lid verwijderen @@ -4808,16 +5124,32 @@ Schakel dit in in *Netwerk en servers*-instellingen. Resetten No comment provided by engineer. + + Reset all statistics + No comment provided by engineer. + + + Reset all statistics? + No comment provided by engineer. + Reset colors Kleuren resetten No comment provided by engineer. + + Reset to app theme + No comment provided by engineer. + Reset to defaults Resetten naar standaardwaarden No comment provided by engineer. + + Reset to user theme + 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 @@ -4888,6 +5220,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Chat uitvoeren No comment provided by engineer. + + SMP server + No comment provided by engineer. + SMP servers SMP servers @@ -5003,6 +5339,14 @@ Schakel dit in in *Netwerk en servers*-instellingen. Opgeslagen bericht message info title + + Scale + No comment provided by engineer. + + + Scan / Paste link + No comment provided by engineer. + Scan QR code Scan QR-code @@ -5043,11 +5387,19 @@ Schakel dit in in *Netwerk en servers*-instellingen. Zoeken of plak een SimpleX link No comment provided by engineer. + + Secondary + No comment provided by engineer. + Secure queue Veilige wachtrij server test step + + Secured + No comment provided by engineer. + Security assessment Beveiligingsbeoordeling @@ -5063,6 +5415,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Selecteer No comment provided by engineer. + + Selected chat preferences prohibit this message. + No comment provided by engineer. + Self-destruct Zelfvernietiging @@ -5113,6 +5469,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Stuur een verdwijnend bericht No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews Link voorbeelden verzenden @@ -5223,6 +5583,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Verzonden op: %@ copied message info + + Sent directly + No comment provided by engineer. + Sent file event Verzonden bestandsgebeurtenis @@ -5233,11 +5597,31 @@ Schakel dit in in *Netwerk en servers*-instellingen. Verzonden bericht message info title + + Sent messages + 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 + 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. Serveradres is niet compatibel met netwerkinstellingen. @@ -5258,6 +5642,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Servertest mislukt! No comment provided by engineer. + + Server type + No comment provided by engineer. + Server version is incompatible with network settings. Serverversie is incompatibel met netwerkinstellingen. @@ -5268,6 +5656,14 @@ Schakel dit in in *Netwerk en servers*-instellingen. 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 Sessie code @@ -5283,6 +5679,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Contactnaam instellen… No comment provided by engineer. + + Set default theme + No comment provided by engineer. + Set group preferences Groep voorkeuren instellen @@ -5403,6 +5803,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Toon: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address SimpleX adres @@ -5478,6 +5882,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Vereenvoudigde incognitomodus No comment provided by engineer. + + Size + No comment provided by engineer. + Skip Overslaan @@ -5523,6 +5931,14 @@ Schakel dit in in *Netwerk en servers*-instellingen. Start migratie No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop Stop @@ -5588,6 +6004,22 @@ Schakel dit in in *Netwerk en servers*-instellingen. Indienen No comment provided by engineer. + + Subscribed + No comment provided by engineer. + + + Subscription errors + No comment provided by engineer. + + + Subscription percentage + No comment provided by engineer. + + + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat Ondersteuning van SimpleX Chat @@ -5668,6 +6100,10 @@ Schakel dit in in *Netwerk en servers*-instellingen. Tik om een nieuw gesprek te starten No comment provided by engineer. + + Temporary file error + No comment provided by engineer. + Test failed at step %@. Test mislukt bij stap %@. @@ -5805,9 +6241,8 @@ 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 No comment provided by engineer. @@ -5875,11 +6310,19 @@ 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. + 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 + No comment provided by engineer. + To ask any questions and to receive updates: Om vragen te stellen en updates te ontvangen: @@ -5947,11 +6390,19 @@ U wordt gevraagd de authenticatie te voltooien voordat deze functie wordt ingesc Schakel incognito in tijdens het verbinden. No comment provided by engineer. + + Total + No comment provided by engineer. + Transport isolation 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: %@). Proberen verbinding te maken met de server die wordt gebruikt om berichten van dit contact te ontvangen (fout: %@). @@ -6144,6 +6595,10 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Upgrade en open chat No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed Upload mislukt @@ -6154,6 +6609,14 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Upload bestand server test step + + Uploaded + No comment provided by engineer. + + + Uploaded files + No comment provided by engineer. + Uploading archive Archief uploaden @@ -6229,6 +6692,10 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak Gebruikers profiel No comment provided by engineer. + + User selection + No comment provided by engineer. + Using .onion hosts requires compatible VPN provider. Het gebruik van .onion-hosts vereist een compatibele VPN-provider. @@ -6364,6 +6831,14 @@ 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 + 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 Waarschuwing: het starten van de chat op meerdere apparaten wordt niet ondersteund en zal leiden tot mislukte bezorging van berichten @@ -6469,11 +6944,19 @@ Om verbinding te maken, vraagt u uw contact om een andere verbinding link te mak 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. + file error text + Wrong passphrase! Verkeerd wachtwoord! No comment provided by engineer. + + XFTP server + No comment provided by engineer. + XFTP servers XFTP servers @@ -6556,6 +7039,10 @@ 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. + 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. @@ -6962,6 +7449,10 @@ SimpleX servers kunnen uw profiel niet zien. en %lld andere gebeurtenissen No comment provided by engineer. + + attempts + No comment provided by engineer. + audio call (not e2e encrypted) audio oproep (niet e2e versleuteld) @@ -6995,7 +7486,7 @@ SimpleX servers kunnen uw profiel niet zien. blocked by admin geblokkeerd door beheerder - marked deleted chat item preview text + blocked chat item bold @@ -7152,6 +7643,10 @@ SimpleX servers kunnen uw profiel niet zien. dagen time unit + + decryption errors + No comment provided by engineer. + default (%@) standaard (%@) @@ -7202,6 +7697,10 @@ SimpleX servers kunnen uw profiel niet zien. dubbel bericht integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted e2e versleuteld @@ -7282,6 +7781,10 @@ SimpleX servers kunnen uw profiel niet zien. gebeurtenis gebeurd No comment provided by engineer. + + expired + No comment provided by engineer. + forwarded doorgestuurd @@ -7312,6 +7815,10 @@ 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 + No comment provided by engineer. + incognito via contact address link incognito via contact adres link @@ -7489,6 +7996,14 @@ SimpleX servers kunnen uw profiel niet zien. aan group pref value + + other + No comment provided by engineer. + + + other errors + No comment provided by engineer. + owner Eigenaar @@ -7799,7 +8314,7 @@ last received msg: %2$@
- +
@@ -7836,7 +8351,7 @@ last received msg: %2$@
- +
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 3a165f8c43..9da2b6bd82 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 @@
- +
@@ -557,9 +557,8 @@ O adresie SimpleX No comment provided by engineer. - - Accent color - Kolor akcentu + + Accent No comment provided by engineer. @@ -583,6 +582,14 @@ Akceptuj incognito accept contact request via notification + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + 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. @@ -623,6 +630,18 @@ Dodaj wiadomość powitalną 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 +667,10 @@ Zaawansowane ustawienia sieci No comment provided by engineer. + + Advanced settings + No comment provided by engineer. + All app data is deleted. Wszystkie dane aplikacji są usunięte. @@ -663,6 +686,10 @@ Wszystkie dane są usuwane po jego wprowadzeniu. No comment provided by engineer. + + All data is private to your device. + No comment provided by engineer. + All group members will remain connected. Wszyscy członkowie grupy pozostaną połączeni. @@ -683,6 +710,10 @@ Wszystkie nowe wiadomości z %@ zostaną ukryte! No comment provided by engineer. + + All users + No comment provided by engineer. + All your contacts will remain connected. Wszystkie Twoje kontakty pozostaną połączone. @@ -883,6 +914,10 @@ Zastosuj No comment provided by engineer. + + Apply to + No comment provided by engineer. + Archive and upload Archiwizuj i prześlij @@ -958,6 +993,10 @@ Wstecz No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address Zły adres komputera @@ -983,6 +1022,10 @@ Lepsze wiadomości No comment provided by engineer. + + Black + No comment provided by engineer. + Block Zablokuj @@ -1093,6 +1136,10 @@ 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 + No comment provided by engineer. + Cannot receive file Nie można odebrać pliku @@ -1164,6 +1211,10 @@ Archiwum czatu No comment provided by engineer. + + Chat colors + No comment provided by engineer. + Chat console Konsola czatu @@ -1209,6 +1260,10 @@ Preferencje czatu No comment provided by engineer. + + Chat theme + No comment provided by engineer. + Chats Czaty @@ -1239,6 +1294,18 @@ Wybierz z biblioteki 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 Wyczyść @@ -1264,9 +1331,8 @@ Wyczyść weryfikację No comment provided by engineer. - - Colors - Kolory + + Color mode No comment provided by engineer. @@ -1279,6 +1345,10 @@ Porównaj kody bezpieczeństwa ze swoimi kontaktami. No comment provided by engineer. + + Completed + No comment provided by engineer. + Configure ICE servers Skonfiguruj serwery ICE @@ -1388,16 +1458,28 @@ To jest twój jednorazowy link! Połącz z %@ No comment provided by engineer. + + Connected + No comment provided by engineer. + Connected desktop Połączony komputer No comment provided by engineer. + + Connected servers + No comment provided by engineer. + Connected to desktop Połączony do komputera No comment provided by engineer. + + Connecting + No comment provided by engineer. + Connecting to server… Łączenie z serwerem… @@ -1443,6 +1525,18 @@ To jest twój jednorazowy link! Czas połączenia minął No comment provided by engineer. + + Connection with desktop stopped + No comment provided by engineer. + + + Connections + No comment provided by engineer. + + + Connections subscribed + No comment provided by engineer. + Contact allows Kontakt pozwala @@ -1496,7 +1590,11 @@ To jest twój jednorazowy link! Copy Kopiuj - chat item action + No comment provided by engineer. + + + Copy error + No comment provided by engineer. Core version: v%@ @@ -1573,6 +1671,10 @@ To jest twój jednorazowy link! Utwórz swój profil No comment provided by engineer. + + Created + No comment provided by engineer. + Created at Utworzony o @@ -1608,6 +1710,10 @@ To jest twój jednorazowy link! Obecne hasło… No comment provided by engineer. + + Current user + No comment provided by engineer. + Currently maximum supported file size is %@. Obecnie maksymalna obsługiwana wielkość pliku wynosi %@. @@ -1618,11 +1724,19 @@ To jest twój jednorazowy link! Niestandardowy czas No comment provided by engineer. + + Customize theme + No comment provided by engineer. + Dark Ciemny No comment provided by engineer. + + Dark mode colors + No comment provided by engineer. + Database ID ID bazy danych @@ -1927,6 +2041,10 @@ To nie może być cofnięte! Usunąć profil użytkownika? No comment provided by engineer. + + Deleted + No comment provided by engineer. + Deleted at Usunięto o @@ -1937,6 +2055,10 @@ To nie może być cofnięte! Usunięto o: %@ copied message info + + Deletion errors + No comment provided by engineer. + Delivery Dostarczenie @@ -1977,6 +2099,14 @@ To nie może być cofnięte! Błąd docelowego serwera: %@ snd error text + + Detailed statistics + No comment provided by engineer. + + + Details + No comment provided by engineer. + Develop Deweloperskie @@ -2132,6 +2262,10 @@ To nie może być cofnięte! Pobierz chat item action + + Download errors + No comment provided by engineer. + Download failed Pobieranie nie udane @@ -2142,6 +2276,14 @@ To nie może być cofnięte! Pobierz plik server test step + + Downloaded + No comment provided by engineer. + + + Downloaded files + No comment provided by engineer. + Downloading archive Pobieranie archiwum @@ -2512,6 +2654,10 @@ To nie może być cofnięte! Błąd eksportu bazy danych czatu No comment provided by engineer. + + Error exporting theme: %@ + No comment provided by engineer. + Error importing chat database Błąd importu bazy danych czatu @@ -2537,11 +2683,23 @@ To nie może być cofnięte! Błąd odbioru pliku No comment provided by engineer. + + Error reconnecting server + No comment provided by engineer. + + + Error reconnecting servers + No comment provided by engineer. + Error removing member Błąd usuwania członka No comment provided by engineer. + + Error resetting statistics + No comment provided by engineer. + Error saving %@ servers Błąd zapisu %@ serwerów @@ -2660,7 +2818,8 @@ To nie może być cofnięte! Error: %@ Błąd: %@ - snd error text + file error text + snd error text Error: URL is invalid @@ -2672,6 +2831,10 @@ To nie może być cofnięte! Błąd: brak pliku bazy danych No comment provided by engineer. + + Errors + No comment provided by engineer. + Even when disabled in the conversation. Nawet po wyłączeniu w rozmowie. @@ -2697,6 +2860,10 @@ To nie może być cofnięte! Błąd eksportu: No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. Wyeksportowane archiwum bazy danych. @@ -2732,6 +2899,26 @@ To nie może być cofnięte! Ulubione No comment provided by engineer. + + 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. Plik zostanie usunięty z serwerów. @@ -2921,6 +3108,14 @@ Błąd: %2$@ GIF-y i naklejki No comment provided by engineer. + + Good afternoon! + message preview + + + Good morning! + message preview + Group Grupa @@ -3201,6 +3396,10 @@ Błąd: %2$@ Import nie udał się No comment provided by engineer. + + Import theme + No comment provided by engineer. + Importing archive Importowanie archiwum @@ -3323,6 +3522,10 @@ Błąd: %2$@ Interfejs No comment provided by engineer. + + Interface colors + No comment provided by engineer. + Invalid QR code Nieprawidłowy kod QR @@ -3666,6 +3869,10 @@ To jest twój link do grupy %@! Członek No comment provided by engineer. + + Member inactive + 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. @@ -3681,6 +3888,10 @@ 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 + No comment provided by engineer. + Message delivery error Błąd dostarczenia wiadomości @@ -3701,6 +3912,14 @@ To jest twój link do grupy %@! Wersja robocza wiadomości 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. @@ -3735,6 +3954,18 @@ To jest twój link do grupy %@! Źródło wiadomości pozostaje prywatne. No comment provided by engineer. + + Message status + No comment provided by engineer. + + + Message status: %@ + copied message info + + + Message subscriptions + No comment provided by engineer. + Message text Tekst wiadomości @@ -3760,6 +3991,14 @@ To jest twój link do grupy %@! Wiadomości od %@ zostaną pokazane! 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. Wiadomości, pliki i połączenia są chronione przez **szyfrowanie end-to-end** z doskonałym utajnianiem z wyprzedzeniem i odzyskiem po złamaniu. @@ -3995,6 +4234,10 @@ 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. + item status description + No filtered chats Brak filtrowanych czatów @@ -4010,6 +4253,10 @@ To jest twój link do grupy %@! Brak historii No comment provided by engineer. + + No info, try to reload + No comment provided by engineer. + No network connection Brak połączenia z siecią @@ -4194,6 +4441,10 @@ To jest twój link do grupy %@! Otwórz migrację na innym urządzeniu authentication reason + + Open server settings + No comment provided by engineer. + Open user profiles Otwórz profile użytkownika @@ -4299,6 +4550,10 @@ To jest twój link do grupy %@! Wklej link, który otrzymałeś No comment provided by engineer. + + Pending + 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. @@ -4324,6 +4579,11 @@ To jest twój link do grupy %@! 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. + 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. @@ -4421,6 +4681,10 @@ Błąd: %@ Podgląd No comment provided by engineer. + + Previously connected servers + No comment provided by engineer. + Privacy & security Prywatność i bezpieczeństwo @@ -4486,6 +4750,10 @@ Błąd: %@ Hasło profilu No comment provided by engineer. + + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. Aktualizacja profilu zostanie wysłana do Twoich kontaktów. @@ -4568,6 +4836,14 @@ Włącz w ustawianiach *Sieć i serwery* . Limit czasu protokołu na KB No comment provided by engineer. + + Proxied + No comment provided by engineer. + + + Proxied servers + No comment provided by engineer. + Push notifications Powiadomienia push @@ -4633,6 +4909,10 @@ Włącz w ustawianiach *Sieć i serwery* . Potwierdzenia są wyłączone No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at Otrzymane o @@ -4653,6 +4933,18 @@ Włącz w ustawianiach *Sieć i serwery* . Otrzymano wiadomość 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. Adres odbiorczy zostanie zmieniony na inny serwer. Zmiana adresu zostanie zakończona gdy nadawca będzie online. @@ -4683,11 +4975,31 @@ Włącz w ustawianiach *Sieć i serwery* . Odbiorcy widzą aktualizacje podczas ich wpisywania. No comment provided by engineer. + + Reconnect + 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 + 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? Ponownie połączyć serwery? @@ -4738,6 +5050,10 @@ Włącz w ustawianiach *Sieć i serwery* . Usuń No comment provided by engineer. + + Remove image + No comment provided by engineer. + Remove member Usuń członka @@ -4808,16 +5124,32 @@ Włącz w ustawianiach *Sieć i serwery* . Resetuj No comment provided by engineer. + + Reset all statistics + No comment provided by engineer. + + + Reset all statistics? + No comment provided by engineer. + Reset colors Resetuj kolory No comment provided by engineer. + + Reset to app theme + No comment provided by engineer. + Reset to defaults Przywróć wartości domyślne No comment provided by engineer. + + Reset to user theme + No comment provided by engineer. + Restart the app to create a new chat profile Uruchom ponownie aplikację, aby utworzyć nowy profil czatu @@ -4888,6 +5220,10 @@ Włącz w ustawianiach *Sieć i serwery* . Uruchom czat No comment provided by engineer. + + SMP server + No comment provided by engineer. + SMP servers Serwery SMP @@ -5003,6 +5339,14 @@ Włącz w ustawianiach *Sieć i serwery* . Zachowano wiadomość message info title + + Scale + No comment provided by engineer. + + + Scan / Paste link + No comment provided by engineer. + Scan QR code Zeskanuj kod QR @@ -5043,11 +5387,19 @@ Włącz w ustawianiach *Sieć i serwery* . Wyszukaj lub wklej link SimpleX No comment provided by engineer. + + Secondary + No comment provided by engineer. + Secure queue Bezpieczna kolejka server test step + + Secured + No comment provided by engineer. + Security assessment Ocena bezpieczeństwa @@ -5063,6 +5415,10 @@ Włącz w ustawianiach *Sieć i serwery* . Wybierz No comment provided by engineer. + + Selected chat preferences prohibit this message. + No comment provided by engineer. + Self-destruct Samozniszczenie @@ -5113,6 +5469,10 @@ Włącz w ustawianiach *Sieć i serwery* . Wyślij znikającą wiadomość No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews Wyślij podgląd linku @@ -5223,6 +5583,10 @@ Włącz w ustawianiach *Sieć i serwery* . Wysłano o: %@ copied message info + + Sent directly + No comment provided by engineer. + Sent file event Wyślij zdarzenie pliku @@ -5233,11 +5597,31 @@ Włącz w ustawianiach *Sieć i serwery* . Wyślij wiadomość message info title + + Sent messages + 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 + 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. Adres serwera jest niekompatybilny z ustawieniami sieciowymi. @@ -5258,6 +5642,10 @@ Włącz w ustawianiach *Sieć i serwery* . Test serwera nie powiódł się! No comment provided by engineer. + + Server type + No comment provided by engineer. + Server version is incompatible with network settings. Wersja serwera jest niekompatybilna z ustawieniami sieciowymi. @@ -5268,6 +5656,14 @@ Włącz w ustawianiach *Sieć i serwery* . Serwery 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 Kod sesji @@ -5283,6 +5679,10 @@ Włącz w ustawianiach *Sieć i serwery* . Ustaw nazwę kontaktu… No comment provided by engineer. + + Set default theme + No comment provided by engineer. + Set group preferences Ustaw preferencje grupy @@ -5403,6 +5803,10 @@ Włącz w ustawianiach *Sieć i serwery* . Pokaż: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address Adres SimpleX @@ -5478,6 +5882,10 @@ Włącz w ustawianiach *Sieć i serwery* . Uproszczony tryb incognito No comment provided by engineer. + + Size + No comment provided by engineer. + Skip Pomiń @@ -5523,6 +5931,14 @@ Włącz w ustawianiach *Sieć i serwery* . Rozpocznij migrację No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop Zatrzymaj @@ -5588,6 +6004,22 @@ Włącz w ustawianiach *Sieć i serwery* . Zatwierdź No comment provided by engineer. + + Subscribed + No comment provided by engineer. + + + Subscription errors + No comment provided by engineer. + + + Subscription percentage + No comment provided by engineer. + + + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat Wspieraj SimpleX Chat @@ -5668,6 +6100,10 @@ Włącz w ustawianiach *Sieć i serwery* . Dotknij, aby rozpocząć nowy czat No comment provided by engineer. + + Temporary file error + No comment provided by engineer. + Test failed at step %@. Test nie powiódł się na etapie %@. @@ -5805,9 +6241,8 @@ 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 No comment provided by engineer. @@ -5875,11 +6310,19 @@ 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. + 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 + No comment provided by engineer. + To ask any questions and to receive updates: Aby zadać wszelkie pytania i otrzymywać aktualizacje: @@ -5947,11 +6390,19 @@ Przed włączeniem tej funkcji zostanie wyświetlony monit uwierzytelniania.Przełącz incognito przy połączeniu. No comment provided by engineer. + + Total + No comment provided by engineer. + Transport isolation Izolacja 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: %@). Próbowanie połączenia z serwerem używanym do odbierania wiadomości od tego kontaktu (błąd: %@). @@ -6144,6 +6595,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Zaktualizuj i otwórz czat No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed Wgrywanie nie udane @@ -6154,6 +6609,14 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Prześlij plik server test step + + Uploaded + No comment provided by engineer. + + + Uploaded files + No comment provided by engineer. + Uploading archive Wgrywanie archiwum @@ -6229,6 +6692,10 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Profil użytkownika No comment provided by engineer. + + User selection + No comment provided by engineer. + Using .onion hosts requires compatible VPN provider. Używanie hostów .onion wymaga kompatybilnego dostawcy VPN. @@ -6364,6 +6831,14 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc Oczekiwanie na film 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 Ostrzeżenie: rozpoczęcie czatu na wielu urządzeniach nie jest wspierane i spowoduje niepowodzenia dostarczania wiadomości @@ -6469,11 +6944,19 @@ Aby się połączyć, poproś Twój kontakt o utworzenie kolejnego linku połąc 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. + file error text + Wrong passphrase! Nieprawidłowe hasło! No comment provided by engineer. + + XFTP server + No comment provided by engineer. + XFTP servers Serwery XFTP @@ -6556,6 +7039,10 @@ 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. + 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. @@ -6962,6 +7449,10 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. i %lld innych wydarzeń No comment provided by engineer. + + attempts + No comment provided by engineer. + audio call (not e2e encrypted) połączenie audio (nie szyfrowane e2e) @@ -6995,7 +7486,7 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. blocked by admin zablokowany przez admina - marked deleted chat item preview text + blocked chat item bold @@ -7152,6 +7643,10 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. dni time unit + + decryption errors + No comment provided by engineer. + default (%@) domyślne (%@) @@ -7202,6 +7697,10 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. zduplikowana wiadomość integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted zaszyfrowany e2e @@ -7282,6 +7781,10 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. nowe wydarzenie No comment provided by engineer. + + expired + No comment provided by engineer. + forwarded przekazane dalej @@ -7312,6 +7815,10 @@ 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 + No comment provided by engineer. + incognito via contact address link incognito poprzez link adresu kontaktowego @@ -7489,6 +7996,14 @@ Serwery SimpleX nie mogą zobaczyć Twojego profilu. włączone group pref value + + other + No comment provided by engineer. + + + other errors + No comment provided by engineer. + owner właściciel @@ -7799,7 +8314,7 @@ last received msg: %2$@
- +
@@ -7836,7 +8351,7 @@ last received msg: %2$@
- +
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/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 55c22354a9..db7f5bfb9e 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 @@
- +
@@ -557,9 +557,8 @@ Об адресе SimpleX No comment provided by engineer. - - Accent color - Основной цвет + + Accent No comment provided by engineer. @@ -583,6 +582,14 @@ Принять инкогнито accept contact request via notification + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + 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. Добавьте адрес в свой профиль, чтобы Ваши контакты могли поделиться им. Профиль будет отправлен Вашим контактам. @@ -623,6 +630,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 +667,10 @@ Настройки сети No comment provided by engineer. + + Advanced settings + No comment provided by engineer. + All app data is deleted. Все данные приложения будут удалены. @@ -663,6 +686,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 +710,10 @@ Все новые сообщения от %@ будут скрыты! No comment provided by engineer. + + All users + No comment provided by engineer. + All your contacts will remain connected. Все контакты, которые соединились через этот адрес, сохранятся. @@ -883,6 +914,10 @@ Применить No comment provided by engineer. + + Apply to + No comment provided by engineer. + Archive and upload Архивировать и загрузить @@ -958,6 +993,10 @@ Назад No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address Неверный адрес компьютера @@ -983,6 +1022,10 @@ Улучшенные сообщения No comment provided by engineer. + + Black + No comment provided by engineer. + Block Заблокировать @@ -1093,6 +1136,10 @@ Ошибка доступа к Keychain при сохранении пароля No comment provided by engineer. + + Cannot forward message + No comment provided by engineer. + Cannot receive file Невозможно получить файл @@ -1164,6 +1211,10 @@ Архив чата No comment provided by engineer. + + Chat colors + No comment provided by engineer. + Chat console Консоль @@ -1209,6 +1260,10 @@ Предпочтения No comment provided by engineer. + + Chat theme + No comment provided by engineer. + Chats Чаты @@ -1239,6 +1294,18 @@ Выбрать из библиотеки 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 Очистить @@ -1264,9 +1331,8 @@ Сбросить подтверждение No comment provided by engineer. - - Colors - Цвета + + Color mode No comment provided by engineer. @@ -1279,6 +1345,10 @@ Сравните код безопасности с Вашими контактами. No comment provided by engineer. + + Completed + No comment provided by engineer. + Configure ICE servers Настройка ICE серверов @@ -1388,16 +1458,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… Устанавливается соединение с сервером… @@ -1443,6 +1525,18 @@ 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. + + + Connections subscribed + No comment provided by engineer. + Contact allows Контакт разрешает @@ -1496,7 +1590,11 @@ This is your own one-time link! Copy Скопировать - chat item action + No comment provided by engineer. + + + Copy error + No comment provided by engineer. Core version: v%@ @@ -1573,6 +1671,10 @@ This is your own one-time link! Создать профиль No comment provided by engineer. + + Created + No comment provided by engineer. + Created at Создано @@ -1608,6 +1710,10 @@ This is your own one-time link! Текущий пароль… No comment provided by engineer. + + Current user + No comment provided by engineer. + Currently maximum supported file size is %@. Максимальный размер файла - %@. @@ -1618,11 +1724,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 базы данных @@ -1927,6 +2041,10 @@ This cannot be undone! Удалить профиль пользователя? No comment provided by engineer. + + Deleted + No comment provided by engineer. + Deleted at Удалено @@ -1937,6 +2055,10 @@ This cannot be undone! Удалено: %@ copied message info + + Deletion errors + No comment provided by engineer. + Delivery Доставка @@ -1977,6 +2099,14 @@ This cannot be undone! Ошибка сервера получателя: %@ snd error text + + Detailed statistics + No comment provided by engineer. + + + Details + No comment provided by engineer. + Develop Для разработчиков @@ -2132,6 +2262,10 @@ This cannot be undone! Загрузить chat item action + + Download errors + No comment provided by engineer. + Download failed Ошибка загрузки @@ -2142,6 +2276,14 @@ This cannot be undone! Загрузка файла server test step + + Downloaded + No comment provided by engineer. + + + Downloaded files + No comment provided by engineer. + Downloading archive Загрузка архива @@ -2512,6 +2654,10 @@ This cannot be undone! Ошибка при экспорте архива чата No comment provided by engineer. + + Error exporting theme: %@ + No comment provided by engineer. + Error importing chat database Ошибка при импорте архива чата @@ -2537,11 +2683,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 Ошибка при сохранении %@ серверов @@ -2660,7 +2818,8 @@ This cannot be undone! Error: %@ Ошибка: %@ - snd error text + file error text + snd error text Error: URL is invalid @@ -2672,6 +2831,10 @@ This cannot be undone! Ошибка: данные чата не найдены No comment provided by engineer. + + Errors + No comment provided by engineer. + Even when disabled in the conversation. Даже когда они выключены в разговоре. @@ -2697,6 +2860,10 @@ This cannot be undone! Ошибка при экспорте: No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. Архив чата экспортирован. @@ -2732,6 +2899,26 @@ This cannot be undone! Избранный No comment provided by engineer. + + 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. Файл будет удалён с серверов. @@ -2921,6 +3108,14 @@ Error: %2$@ ГИФ файлы и стикеры No comment provided by engineer. + + Good afternoon! + message preview + + + Good morning! + message preview + Group Группа @@ -3201,6 +3396,10 @@ Error: %2$@ Ошибка импорта No comment provided by engineer. + + Import theme + No comment provided by engineer. + Importing archive Импорт архива @@ -3323,6 +3522,10 @@ Error: %2$@ Интерфейс No comment provided by engineer. + + Interface colors + No comment provided by engineer. + Invalid QR code Неверный QR код @@ -3666,6 +3869,10 @@ This is your link for group %@! Член группы No comment provided by engineer. + + Member inactive + item status text + Member role will be changed to "%@". All group members will be notified. Роль члена группы будет изменена на "%@". Все члены группы получат сообщение. @@ -3681,6 +3888,10 @@ This is your link for group %@! Член группы будет удален - это действие нельзя отменить! No comment provided by engineer. + + Menus + No comment provided by engineer. + Message delivery error Ошибка доставки сообщения @@ -3701,6 +3912,14 @@ This is your link for group %@! Черновик сообщения 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. @@ -3735,6 +3954,18 @@ This is your link for group %@! Источник сообщения остаётся конфиденциальным. No comment provided by engineer. + + Message status + No comment provided by engineer. + + + Message status: %@ + copied message info + + + Message subscriptions + No comment provided by engineer. + Message text Текст сообщения @@ -3760,6 +3991,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. Сообщения, файлы и звонки защищены **end-to-end шифрованием** с прямой секретностью (PFS), правдоподобным отрицанием и восстановлением от взлома. @@ -3995,6 +4234,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 Нет отфильтрованных разговоров @@ -4010,6 +4253,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 Нет интернет-соединения @@ -4194,6 +4441,10 @@ This is your link for group %@! Открытие миграции на другое устройство authentication reason + + Open server settings + No comment provided by engineer. + Open user profiles Открыть профили пользователя @@ -4299,6 +4550,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. С Вами можно соединиться только через созданные Вами ссылки. @@ -4324,6 +4579,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. Пожалуйста, проверьте, что Вы использовали правильную ссылку или попросите, чтобы Ваш контакт отправил Вам другую ссылку. @@ -4421,6 +4681,10 @@ Error: %@ Просмотр No comment provided by engineer. + + Previously connected servers + No comment provided by engineer. + Privacy & security Конфиденциальность @@ -4486,6 +4750,10 @@ Error: %@ Пароль профиля No comment provided by engineer. + + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. Обновлённый профиль будет отправлен Вашим контактам. @@ -4568,6 +4836,14 @@ Enable in *Network & servers* settings. Таймаут протокола на KB No comment provided by engineer. + + Proxied + No comment provided by engineer. + + + Proxied servers + No comment provided by engineer. + Push notifications Доставка уведомлений @@ -4633,6 +4909,10 @@ Enable in *Network & servers* settings. Отчёты о доставке выключены No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at Получено @@ -4653,6 +4933,18 @@ Enable in *Network & servers* settings. Полученное сообщение 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. Адрес получения сообщений будет перемещён на другой сервер. Изменение адреса завершится после того как отправитель будет онлайн. @@ -4683,11 +4975,31 @@ Enable in *Network & servers* settings. Получатели видят их в то время как Вы их набираете. 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? Переподключить серверы? @@ -4738,6 +5050,10 @@ Enable in *Network & servers* settings. Удалить No comment provided by engineer. + + Remove image + No comment provided by engineer. + Remove member Удалить члена группы @@ -4808,16 +5124,32 @@ Enable in *Network & servers* settings. Сбросить 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 Перезапустите приложение, чтобы создать новый профиль. @@ -4888,6 +5220,10 @@ Enable in *Network & servers* settings. Запустить chat No comment provided by engineer. + + SMP server + No comment provided by engineer. + SMP servers SMP серверы @@ -5003,6 +5339,14 @@ Enable in *Network & servers* settings. Сохраненное сообщение message info title + + Scale + No comment provided by engineer. + + + Scan / Paste link + No comment provided by engineer. + Scan QR code Сканировать QR код @@ -5043,11 +5387,19 @@ Enable in *Network & servers* settings. Искать или вставьте ссылку 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 Аудит безопасности @@ -5063,6 +5415,10 @@ Enable in *Network & servers* settings. Выбрать No comment provided by engineer. + + Selected chat preferences prohibit this message. + No comment provided by engineer. + Self-destruct Самоуничтожение @@ -5113,6 +5469,10 @@ Enable in *Network & servers* settings. Отправить исчезающее сообщение No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews Отправлять картинки ссылок @@ -5223,6 +5583,10 @@ Enable in *Network & servers* settings. Отправлено: %@ copied message info + + Sent directly + No comment provided by engineer. + Sent file event Отправка файла @@ -5233,11 +5597,31 @@ Enable in *Network & servers* settings. Отправленное сообщение 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. Адрес сервера несовместим с настройками сети. @@ -5258,6 +5642,10 @@ Enable in *Network & servers* settings. Ошибка теста сервера! No comment provided by engineer. + + Server type + No comment provided by engineer. + Server version is incompatible with network settings. Версия сервера несовместима с настройками сети. @@ -5268,6 +5656,14 @@ Enable in *Network & servers* settings. Серверы 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 Код сессии @@ -5283,6 +5679,10 @@ Enable in *Network & servers* settings. Имя контакта… No comment provided by engineer. + + Set default theme + No comment provided by engineer. + Set group preferences Предпочтения группы @@ -5403,6 +5803,10 @@ Enable in *Network & servers* settings. Показать: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address Адрес SimpleX @@ -5478,6 +5882,10 @@ Enable in *Network & servers* settings. Упрощенный режим Инкогнито No comment provided by engineer. + + Size + No comment provided by engineer. + Skip Пропустить @@ -5523,6 +5931,14 @@ Enable in *Network & servers* settings. Запустить перемещение данных No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop Остановить @@ -5588,6 +6004,22 @@ Enable in *Network & servers* settings. Продолжить No comment provided by engineer. + + Subscribed + No comment provided by engineer. + + + Subscription errors + No comment provided by engineer. + + + Subscription percentage + No comment provided by engineer. + + + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat Поддержать SimpleX Chat @@ -5668,6 +6100,10 @@ Enable in *Network & servers* settings. Нажмите, чтобы начать чат No comment provided by engineer. + + Temporary file error + No comment provided by engineer. + Test failed at step %@. Ошибка теста на шаге %@. @@ -5805,9 +6241,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. @@ -5875,11 +6310,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: Чтобы задать вопросы и получать уведомления о новых версиях, @@ -5947,11 +6390,19 @@ You will be prompted to complete authentication before this feature is enabled.< Установите режим Инкогнито при соединении. 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: %@). Устанавливается соединение с сервером, через который Вы получаете сообщения от этого контакта (ошибка: %@). @@ -6144,6 +6595,10 @@ To connect, please ask your contact to create another connection link and check Обновить и открыть чат No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed Ошибка загрузки @@ -6154,6 +6609,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 Загрузка архива @@ -6229,6 +6692,10 @@ To connect, please ask your contact to create another connection link and check Профиль чата No comment provided by engineer. + + User selection + No comment provided by engineer. + Using .onion hosts requires compatible VPN provider. Для использования .onion хостов требуется совместимый VPN провайдер. @@ -6364,6 +6831,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 Внимание: запуск чата на нескольких устройствах не поддерживается и приведет к сбоям доставки сообщений @@ -6469,11 +6944,19 @@ To connect, please ask your contact to create another connection link and check Неверный ключ или неизвестное соединение - скорее всего, это соединение удалено. 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 server + No comment provided by engineer. + XFTP servers XFTP серверы @@ -6556,6 +7039,10 @@ 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. Вы можете принимать звонки на экране блокировки, без аутентификации. @@ -6962,6 +7449,10 @@ SimpleX серверы не могут получить доступ к Ваше и %lld других событий No comment provided by engineer. + + attempts + No comment provided by engineer. + audio call (not e2e encrypted) аудиозвонок (не e2e зашифрованный) @@ -6995,7 +7486,7 @@ SimpleX серверы не могут получить доступ к Ваше blocked by admin заблокировано администратором - marked deleted chat item preview text + blocked chat item bold @@ -7152,6 +7643,10 @@ SimpleX серверы не могут получить доступ к Ваше дней time unit + + decryption errors + No comment provided by engineer. + default (%@) по умолчанию (%@) @@ -7202,6 +7697,10 @@ SimpleX серверы не могут получить доступ к Ваше повторное сообщение integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted e2e зашифровано @@ -7282,6 +7781,10 @@ SimpleX серверы не могут получить доступ к Ваше событие произошло No comment provided by engineer. + + expired + No comment provided by engineer. + forwarded переслано @@ -7312,6 +7815,10 @@ SimpleX серверы не могут получить доступ к Ваше Пароль базы данных будет безопасно сохранен в iOS Keychain после запуска чата или изменения пароля - это позволит получать мгновенные уведомления. No comment provided by engineer. + + inactive + No comment provided by engineer. + incognito via contact address link инкогнито через ссылку-контакт @@ -7489,6 +7996,14 @@ SimpleX серверы не могут получить доступ к Ваше да group pref value + + other + No comment provided by engineer. + + + other errors + No comment provided by engineer. + owner владелец @@ -7799,7 +8314,7 @@ last received msg: %2$@
- +
@@ -7836,7 +8351,7 @@ last received msg: %2$@
- +
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 4f4f41aa38..a71061d578 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 @@
- +
@@ -527,9 +527,8 @@ เกี่ยวกับที่อยู่ SimpleX No comment provided by engineer. - - Accent color - สีเน้น + + Accent No comment provided by engineer. @@ -552,6 +551,14 @@ ยอมรับโหมดไม่ระบุตัวตน accept contact request via notification + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + 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,6 +598,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 +634,10 @@ การตั้งค่าระบบเครือข่ายขั้นสูง No comment provided by engineer. + + Advanced settings + No comment provided by engineer. + All app data is deleted. ข้อมูลแอปทั้งหมดถูกลบแล้ว. @@ -630,6 +653,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 +675,10 @@ All new messages from %@ will be hidden! No comment provided by engineer. + + All users + No comment provided by engineer. + All your contacts will remain connected. ผู้ติดต่อทั้งหมดของคุณจะยังคงเชื่อมต่ออยู่. @@ -839,6 +870,10 @@ Apply No comment provided by engineer. + + Apply to + No comment provided by engineer. + Archive and upload No comment provided by engineer. @@ -912,6 +947,10 @@ กลับ No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address No comment provided by engineer. @@ -935,6 +974,10 @@ ข้อความที่ดีขึ้น No comment provided by engineer. + + Black + No comment provided by engineer. + Block No comment provided by engineer. @@ -1035,6 +1078,10 @@ ไม่สามารถเข้าถึง keychain เพื่อบันทึกรหัสผ่านฐานข้อมูล No comment provided by engineer. + + Cannot forward message + No comment provided by engineer. + Cannot receive file ไม่สามารถรับไฟล์ได้ @@ -1104,6 +1151,10 @@ ที่เก็บแชทถาวร No comment provided by engineer. + + Chat colors + No comment provided by engineer. + Chat console คอนโซลแชท @@ -1147,6 +1198,10 @@ ค่ากําหนดในการแชท No comment provided by engineer. + + Chat theme + No comment provided by engineer. + Chats แชท @@ -1176,6 +1231,18 @@ เลือกจากอัลบั้ม 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 ลบ @@ -1200,9 +1267,8 @@ ล้างการยืนยัน No comment provided by engineer. - - Colors - สี + + Color mode No comment provided by engineer. @@ -1215,6 +1281,10 @@ เปรียบเทียบรหัสความปลอดภัยกับผู้ติดต่อของคุณ No comment provided by engineer. + + Completed + No comment provided by engineer. + Configure ICE servers กำหนดค่าเซิร์ฟเวอร์ ICE @@ -1309,14 +1379,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… กำลังเชื่อมต่อกับเซิร์ฟเวอร์… @@ -1360,6 +1442,18 @@ 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. + + + Connections subscribed + No comment provided by engineer. + Contact allows ผู้ติดต่ออนุญาต @@ -1413,7 +1507,11 @@ This is your own one-time link! Copy คัดลอก - chat item action + No comment provided by engineer. + + + Copy error + No comment provided by engineer. Core version: v%@ @@ -1485,6 +1583,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. @@ -1516,6 +1618,10 @@ This is your own one-time link! รหัสผ่านปัจจุบัน… No comment provided by engineer. + + Current user + No comment provided by engineer. + Currently maximum supported file size is %@. ขนาดไฟล์ที่รองรับสูงสุดในปัจจุบันคือ %@ @@ -1526,11 +1632,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 ฐานข้อมูล @@ -1830,6 +1944,10 @@ This cannot be undone! ลบโปรไฟล์ผู้ใช้? No comment provided by engineer. + + Deleted + No comment provided by engineer. + Deleted at ลบที่ @@ -1840,6 +1958,10 @@ This cannot be undone! ลบที่: %@ copied message info + + Deletion errors + No comment provided by engineer. + Delivery No comment provided by engineer. @@ -1875,6 +1997,14 @@ This cannot be undone! Destination server error: %@ snd error text + + Detailed statistics + No comment provided by engineer. + + + Details + No comment provided by engineer. + Develop พัฒนา @@ -2023,6 +2153,10 @@ This cannot be undone! Download chat item action + + Download errors + No comment provided by engineer. + Download failed No comment provided by engineer. @@ -2032,6 +2166,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. @@ -2384,6 +2526,10 @@ This cannot be undone! เกิดข้อผิดพลาดในการส่งออกฐานข้อมูลแชท No comment provided by engineer. + + Error exporting theme: %@ + No comment provided by engineer. + Error importing chat database เกิดข้อผิดพลาดในการนำเข้าฐานข้อมูลแชท @@ -2408,11 +2554,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 เกิดข้อผิดพลาดในการบันทึกเซิร์ฟเวอร์ %@ @@ -2526,7 +2684,8 @@ This cannot be undone! Error: %@ ข้อผิดพลาด: % @ - snd error text + file error text + snd error text Error: URL is invalid @@ -2538,6 +2697,10 @@ This cannot be undone! เกิดข้อผิดพลาด: ไม่มีแฟ้มฐานข้อมูล No comment provided by engineer. + + Errors + No comment provided by engineer. + Even when disabled in the conversation. แม้ในขณะที่ปิดใช้งานในการสนทนา @@ -2562,6 +2725,10 @@ This cannot be undone! ข้อผิดพลาดในการส่งออก: No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. ที่เก็บถาวรฐานข้อมูลที่ส่งออก @@ -2595,6 +2762,26 @@ This cannot be undone! ที่ชอบ No comment provided by engineer. + + 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. ไฟล์จะถูกลบออกจากเซิร์ฟเวอร์ @@ -2770,6 +2957,14 @@ Error: %2$@ GIFs และสติกเกอร์ No comment provided by engineer. + + Good afternoon! + message preview + + + Good morning! + message preview + Group กลุ่ม @@ -3044,6 +3239,10 @@ Error: %2$@ Import failed No comment provided by engineer. + + Import theme + No comment provided by engineer. + Importing archive No comment provided by engineer. @@ -3159,6 +3358,10 @@ Error: %2$@ อินเตอร์เฟซ No comment provided by engineer. + + Interface colors + No comment provided by engineer. + Invalid QR code No comment provided by engineer. @@ -3484,6 +3687,10 @@ This is your link for group %@! สมาชิก No comment provided by engineer. + + Member inactive + item status text + Member role will be changed to "%@". All group members will be notified. บทบาทของสมาชิกจะถูกเปลี่ยนเป็น "%@" สมาชิกกลุ่มทั้งหมดจะได้รับแจ้ง @@ -3499,6 +3706,10 @@ This is your link for group %@! สมาชิกจะถูกลบออกจากกลุ่ม - ไม่สามารถยกเลิกได้! No comment provided by engineer. + + Menus + No comment provided by engineer. + Message delivery error ข้อผิดพลาดในการส่งข้อความ @@ -3518,6 +3729,14 @@ This is your link for group %@! ร่างข้อความ 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. @@ -3549,6 +3768,18 @@ This is your link for group %@! Message source remains private. No comment provided by engineer. + + Message status + No comment provided by engineer. + + + Message status: %@ + copied message info + + + Message subscriptions + No comment provided by engineer. + Message text ข้อความ @@ -3572,6 +3803,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. @@ -3790,6 +4029,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 ไม่มีการกรองการแชท @@ -3805,6 +4048,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. @@ -3983,6 +4230,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 เปิดโปรไฟล์ผู้ใช้ @@ -4078,6 +4329,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. ผู้คนสามารถเชื่อมต่อกับคุณผ่านลิงก์ที่คุณแบ่งปันเท่านั้น @@ -4102,6 +4357,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. โปรดตรวจสอบว่าคุณใช้ลิงก์ที่ถูกต้องหรือขอให้ผู้ติดต่อของคุณส่งลิงก์ใหม่ให้คุณ @@ -4196,6 +4456,10 @@ Error: %@ ดูตัวอย่าง No comment provided by engineer. + + Previously connected servers + No comment provided by engineer. + Privacy & security ความเป็นส่วนตัวและความปลอดภัย @@ -4254,6 +4518,10 @@ Error: %@ รหัสผ่านโปรไฟล์ No comment provided by engineer. + + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. การอัปเดตโปรไฟล์จะถูกส่งไปยังผู้ติดต่อของคุณ @@ -4332,6 +4600,14 @@ Enable in *Network & servers* settings. การหมดเวลาของโปรโตคอลต่อ KB No comment provided by engineer. + + Proxied + No comment provided by engineer. + + + Proxied servers + No comment provided by engineer. + Push notifications การแจ้งเตือนแบบทันที @@ -4393,6 +4669,10 @@ Enable in *Network & servers* settings. Receipts are disabled No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at ได้รับเมื่อ @@ -4413,6 +4693,18 @@ Enable in *Network & servers* settings. ได้รับข้อความ 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. ที่อยู่ผู้รับจะถูกเปลี่ยนเป็นเซิร์ฟเวอร์อื่น การเปลี่ยนแปลงที่อยู่จะเสร็จสมบูรณ์หลังจากที่ผู้ส่งออนไลน์ @@ -4441,11 +4733,31 @@ Enable in *Network & servers* settings. ผู้รับจะเห็นการอัปเดตเมื่อคุณพิมพ์ 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? เชื่อมต่อเซิร์ฟเวอร์อีกครั้งหรือไม่? @@ -4495,6 +4807,10 @@ Enable in *Network & servers* settings. ลบ No comment provided by engineer. + + Remove image + No comment provided by engineer. + Remove member ลบสมาชิกออก @@ -4560,16 +4876,32 @@ Enable in *Network & servers* settings. รีเซ็ต 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 รีสตาร์ทแอปเพื่อสร้างโปรไฟล์แชทใหม่ @@ -4639,6 +4971,10 @@ Enable in *Network & servers* settings. เรียกใช้แชท No comment provided by engineer. + + SMP server + No comment provided by engineer. + SMP servers เซิร์ฟเวอร์ SMP @@ -4749,6 +5085,14 @@ Enable in *Network & servers* settings. Saved message message info title + + Scale + No comment provided by engineer. + + + Scan / Paste link + No comment provided by engineer. + Scan QR code สแกนคิวอาร์โค้ด @@ -4786,11 +5130,19 @@ Enable in *Network & servers* settings. 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 การประเมินความปลอดภัย @@ -4806,6 +5158,10 @@ Enable in *Network & servers* settings. เลือก No comment provided by engineer. + + Selected chat preferences prohibit this message. + No comment provided by engineer. + Self-destruct ทําลายตัวเอง @@ -4855,6 +5211,10 @@ Enable in *Network & servers* settings. ส่งข้อความแบบที่หายไป No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews ส่งตัวอย่างลิงก์ @@ -4960,6 +5320,10 @@ Enable in *Network & servers* settings. ส่งเมื่อ: %@ copied message info + + Sent directly + No comment provided by engineer. + Sent file event เหตุการณ์ไฟล์ที่ส่ง @@ -4970,11 +5334,31 @@ Enable in *Network & servers* settings. ข้อความที่ส่งแล้ว 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. @@ -4994,6 +5378,10 @@ Enable in *Network & servers* settings. การทดสอบเซิร์ฟเวอร์ล้มเหลว! No comment provided by engineer. + + Server type + No comment provided by engineer. + Server version is incompatible with network settings. srv error text @@ -5003,6 +5391,14 @@ Enable in *Network & servers* settings. เซิร์ฟเวอร์ 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. @@ -5017,6 +5413,10 @@ Enable in *Network & servers* settings. ตั้งชื่อผู้ติดต่อ… No comment provided by engineer. + + Set default theme + No comment provided by engineer. + Set group preferences ตั้งค่าการกําหนดลักษณะกลุ่ม @@ -5130,6 +5530,10 @@ Enable in *Network & servers* settings. แสดง: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address ที่อยู่ SimpleX @@ -5202,6 +5606,10 @@ Enable in *Network & servers* settings. Simplified incognito mode No comment provided by engineer. + + Size + No comment provided by engineer. + Skip ข้าม @@ -5244,6 +5652,14 @@ Enable in *Network & servers* settings. เริ่มการย้ายข้อมูล No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop หยุด @@ -5307,6 +5723,22 @@ Enable in *Network & servers* settings. ส่ง No comment provided by engineer. + + Subscribed + No comment provided by engineer. + + + Subscription errors + No comment provided by engineer. + + + Subscription percentage + No comment provided by engineer. + + + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat สนับสนุน SimpleX แชท @@ -5384,6 +5816,10 @@ Enable in *Network & servers* settings. แตะเพื่อเริ่มแชทใหม่ No comment provided by engineer. + + Temporary file error + No comment provided by engineer. + Test failed at step %@. การทดสอบล้มเหลวในขั้นตอน %@ @@ -5519,9 +5955,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. @@ -5581,11 +6016,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: หากต้องการถามคำถามและรับการอัปเดต: @@ -5650,11 +6093,19 @@ You will be prompted to complete authentication before this feature is enabled.< Toggle incognito when connecting. 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: %@). กำลังพยายามเชื่อมต่อกับเซิร์ฟเวอร์ที่ใช้รับข้อความจากผู้ติดต่อนี้ (ข้อผิดพลาด: %@) @@ -5837,6 +6288,10 @@ To connect, please ask your contact to create another connection link and check อัปเกรดและเปิดการแชท No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed No comment provided by engineer. @@ -5846,6 +6301,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. @@ -5913,6 +6376,10 @@ To connect, please ask your contact to create another connection link and check โปรไฟล์ผู้ใช้ No comment provided by engineer. + + User selection + No comment provided by engineer. + Using .onion hosts requires compatible VPN provider. การใช้โฮสต์ .onion ต้องการผู้ให้บริการ VPN ที่เข้ากันได้ @@ -6039,6 +6506,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. @@ -6133,11 +6608,19 @@ To connect, please ask your contact to create another connection link and check 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 server + No comment provided by engineer. + XFTP servers เซิร์ฟเวอร์ XFTP @@ -6211,6 +6694,10 @@ 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. คุณสามารถรับสายจากหน้าจอล็อกโดยไม่ต้องมีการตรวจสอบสิทธิ์อุปกรณ์และแอป @@ -6603,6 +7090,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 จากต้นจนจบ) @@ -6632,7 +7123,7 @@ SimpleX servers cannot see your profile. blocked by admin - marked deleted chat item preview text + blocked chat item bold @@ -6787,6 +7278,10 @@ SimpleX servers cannot see your profile. วัน time unit + + decryption errors + No comment provided by engineer. + default (%@) ค่าเริ่มต้น (%@) @@ -6835,6 +7330,10 @@ SimpleX servers cannot see your profile. ข้อความที่ซ้ำกัน integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted encrypted จากต้นจนจบ @@ -6914,6 +7413,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. @@ -6943,6 +7446,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 ไม่ระบุตัวตนผ่านลิงค์ที่อยู่ติดต่อ @@ -7119,6 +7626,14 @@ SimpleX servers cannot see your profile. เปิด group pref value + + other + No comment provided by engineer. + + + other errors + No comment provided by engineer. + owner เจ้าของ @@ -7409,7 +7924,7 @@ last received msg: %2$@
- +
@@ -7445,7 +7960,7 @@ last received msg: %2$@
- +
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 4a6418c596..66f24d5bb7 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 @@
- +
@@ -557,9 +557,8 @@ SimpleX Chat adresi hakkında No comment provided by engineer. - - Accent color - Vurgu rengi + + Accent No comment provided by engineer. @@ -583,6 +582,14 @@ Takma adla kabul et accept contact request via notification + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + 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. Kişilerinizin başkalarıyla paylaşabilmesi için profilinize adres ekleyin. Profil güncellemesi kişilerinize gönderilecek. @@ -623,6 +630,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 +667,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 +686,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 +710,10 @@ %@ 'den gelen bütün yeni mesajlar saklı olacak! No comment provided by engineer. + + All users + No comment provided by engineer. + All your contacts will remain connected. Konuştuğun kişilerin tümü bağlı kalacaktır. @@ -883,6 +914,10 @@ Uygula No comment provided by engineer. + + Apply to + No comment provided by engineer. + Archive and upload Arşivle ve yükle @@ -958,6 +993,10 @@ Geri No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address Kötü bilgisayar adresi @@ -983,6 +1022,10 @@ Daha iyi mesajlar No comment provided by engineer. + + Black + No comment provided by engineer. + Block Engelle @@ -1093,6 +1136,10 @@ 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 @@ -1164,6 +1211,10 @@ Sohbet arşivi No comment provided by engineer. + + Chat colors + No comment provided by engineer. + Chat console Sohbet konsolu @@ -1209,6 +1260,10 @@ Sohbet tercihleri No comment provided by engineer. + + Chat theme + No comment provided by engineer. + Chats Sohbetler @@ -1239,6 +1294,18 @@ 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 @@ -1264,9 +1331,8 @@ Doğrulamayı temizle No comment provided by engineer. - - Colors - Renkler + + Color mode No comment provided by engineer. @@ -1279,6 +1345,10 @@ 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 @@ -1388,16 +1458,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… @@ -1443,6 +1525,18 @@ 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. + + + Connections subscribed + No comment provided by engineer. + Contact allows Kişi izin veriyor @@ -1496,7 +1590,11 @@ Bu senin kendi tek kullanımlık bağlantın! Copy Kopyala - chat item action + No comment provided by engineer. + + + Copy error + No comment provided by engineer. Core version: v%@ @@ -1573,6 +1671,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 @@ -1608,6 +1710,10 @@ Bu senin kendi tek kullanımlık bağlantın! Şu anki parola… No comment provided by engineer. + + Current user + No comment provided by engineer. + Currently maximum supported file size is %@. Şu anki maksimum desteklenen dosya boyutu %@ kadardır. @@ -1618,11 +1724,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 @@ -1927,6 +2041,10 @@ Bu geri alınamaz! Kullanıcı profili silinsin mi? No comment provided by engineer. + + Deleted + No comment provided by engineer. + Deleted at de silindi @@ -1937,6 +2055,10 @@ Bu geri alınamaz! %@ de silindi copied message info + + Deletion errors + No comment provided by engineer. + Delivery Teslimat @@ -1977,6 +2099,14 @@ Bu geri alınamaz! Hedef sunucu hatası: %@ snd error text + + Detailed statistics + No comment provided by engineer. + + + Details + No comment provided by engineer. + Develop Geliştir @@ -2132,6 +2262,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 @@ -2142,6 +2276,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 @@ -2512,6 +2654,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 @@ -2537,11 +2683,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 @@ -2660,7 +2818,8 @@ Bu geri alınamaz! Error: %@ Hata: %@ - snd error text + file error text + snd error text Error: URL is invalid @@ -2672,6 +2831,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. @@ -2697,6 +2860,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. @@ -2732,6 +2899,26 @@ Bu geri alınamaz! Favori No comment provided by engineer. + + 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. @@ -2921,6 +3108,14 @@ Hata: %2$@ GİFler ve çıkartmalar No comment provided by engineer. + + Good afternoon! + message preview + + + Good morning! + message preview + Group Grup @@ -3201,6 +3396,10 @@ Hata: %2$@ İç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 @@ -3323,6 +3522,10 @@ Hata: %2$@ Arayüz No comment provided by engineer. + + Interface colors + No comment provided by engineer. + Invalid QR code Geçersiz QR kodu @@ -3666,6 +3869,10 @@ Bu senin grup için bağlantın %@! 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. @@ -3681,6 +3888,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ı @@ -3701,6 +3912,14 @@ Bu senin grup için bağlantın %@! 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 No comment provided by engineer. @@ -3735,6 +3954,18 @@ Bu senin grup için bağlantın %@! Mesaj kaynağı gizli kalır. No comment provided by engineer. + + Message status + No comment provided by engineer. + + + Message status: %@ + copied message info + + + Message subscriptions + No comment provided by engineer. + Message text Mesaj yazısı @@ -3760,6 +3991,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. @@ -3995,6 +4234,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 @@ -4010,6 +4253,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 @@ -4194,6 +4441,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ç @@ -4299,6 +4550,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. @@ -4324,6 +4579,11 @@ Bu senin grup için bağlantın %@! 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. @@ -4421,6 +4681,10 @@ Hata: %@ Ön izleme No comment provided by engineer. + + Previously connected servers + No comment provided by engineer. + Privacy & security Gizlilik & güvenlik @@ -4486,6 +4750,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. @@ -4568,6 +4836,14 @@ Enable in *Network & servers* settings. 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 @@ -4633,6 +4909,10 @@ Enable in *Network & servers* settings. 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ı @@ -4653,6 +4933,18 @@ Enable in *Network & servers* settings. 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. @@ -4683,11 +4975,31 @@ Enable in *Network & servers* settings. 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ı? @@ -4738,6 +5050,10 @@ Enable in *Network & servers* settings. Sil No comment provided by engineer. + + Remove image + No comment provided by engineer. + Remove member Kişiyi sil @@ -4808,16 +5124,32 @@ Enable in *Network & servers* settings. Sıfırla 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 @@ -4888,6 +5220,10 @@ Enable in *Network & servers* settings. Sohbeti çalıştır No comment provided by engineer. + + SMP server + No comment provided by engineer. + SMP servers SMP sunucuları @@ -5003,6 +5339,14 @@ Enable in *Network & servers* settings. 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 @@ -5043,11 +5387,19 @@ Enable in *Network & servers* settings. 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 @@ -5063,6 +5415,10 @@ Enable in *Network & servers* settings. Seç No comment provided by engineer. + + Selected chat preferences prohibit this message. + No comment provided by engineer. + Self-destruct Kendi kendini imha @@ -5113,6 +5469,10 @@ Enable in *Network & servers* settings. 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 @@ -5223,6 +5583,10 @@ Enable in *Network & servers* settings. Şuradan gönderildi: %@ copied message info + + Sent directly + No comment provided by engineer. + Sent file event Dosya etkinliği gönderildi @@ -5233,11 +5597,31 @@ Enable in *Network & servers* settings. 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. @@ -5258,6 +5642,10 @@ Enable in *Network & servers* settings. 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. @@ -5268,6 +5656,14 @@ Enable in *Network & servers* settings. 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 @@ -5283,6 +5679,10 @@ Enable in *Network & servers* settings. Kişi adı gir… No comment provided by engineer. + + Set default theme + No comment provided by engineer. + Set group preferences Grup tercihlerini ayarla @@ -5403,6 +5803,10 @@ Enable in *Network & servers* settings. Göster: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address SimpleX Adresi @@ -5478,6 +5882,10 @@ Enable in *Network & servers* settings. Basitleştirilmiş gizli mod No comment provided by engineer. + + Size + No comment provided by engineer. + Skip Atla @@ -5523,6 +5931,14 @@ Enable in *Network & servers* settings. 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 @@ -5588,6 +6004,22 @@ Enable in *Network & servers* settings. Gönder No comment provided by engineer. + + Subscribed + No comment provided by engineer. + + + Subscription errors + No comment provided by engineer. + + + Subscription percentage + No comment provided by engineer. + + + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat SimpleX Chat'e destek ol @@ -5668,6 +6100,10 @@ Enable in *Network & servers* settings. Yeni bir sohbet başlatmak için tıkla No comment provided by engineer. + + Temporary file error + No comment provided by engineer. + Test failed at step %@. Test %@ adımında başarısız oldu. @@ -5805,9 +6241,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. @@ -5875,11 +6310,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: @@ -5947,11 +6390,19 @@ Bu özellik etkinleştirilmeden önce kimlik doğrulamayı tamamlamanız istenec Bağlanırken gizli moda geçiş yap. 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: %@). @@ -6144,6 +6595,10 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste 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 @@ -6154,6 +6609,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 @@ -6229,6 +6692,10 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste Kullanıcı profili No comment provided by engineer. + + User selection + 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. @@ -6364,6 +6831,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 @@ -6469,11 +6944,19 @@ Bağlanmak için lütfen kişinizden başka bir bağlantı oluşturmasını iste 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 server + No comment provided by engineer. + XFTP servers XFTP sunucuları @@ -6556,6 +7039,10 @@ 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. @@ -6962,6 +7449,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) @@ -6995,7 +7486,7 @@ SimpleX sunucuları profilinizi göremez. blocked by admin yönetici tarafından engellendi - marked deleted chat item preview text + blocked chat item bold @@ -7152,6 +7643,10 @@ SimpleX sunucuları profilinizi göremez. gün time unit + + decryption errors + No comment provided by engineer. + default (%@) varsayılan (%@) @@ -7202,6 +7697,10 @@ SimpleX sunucuları profilinizi göremez. yinelenen mesaj integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted uçtan uca şifrelenmiş @@ -7282,6 +7781,10 @@ SimpleX sunucuları profilinizi göremez. etkinlik yaşandı No comment provided by engineer. + + expired + No comment provided by engineer. + forwarded iletildi @@ -7312,6 +7815,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 @@ -7489,6 +7996,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 @@ -7799,7 +8314,7 @@ last received msg: %2$@
- +
@@ -7836,7 +8351,7 @@ last received msg: %2$@
- +
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 258c42007a..bf0b5718f7 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 @@
- +
@@ -557,9 +557,8 @@ Про адресу SimpleX No comment provided by engineer. - - Accent color - Акцентний колір + + Accent No comment provided by engineer. @@ -583,6 +582,14 @@ Прийняти інкогніто accept contact request via notification + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + 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. Додайте адресу до свого профілю, щоб ваші контакти могли поділитися нею з іншими людьми. Повідомлення про оновлення профілю буде надіслано вашим контактам. @@ -623,6 +630,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 +667,10 @@ Розширені налаштування мережі No comment provided by engineer. + + Advanced settings + No comment provided by engineer. + All app data is deleted. Всі дані програми видаляються. @@ -663,6 +686,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 +710,10 @@ Всі нові повідомлення від %@ будуть приховані! No comment provided by engineer. + + All users + No comment provided by engineer. + All your contacts will remain connected. Всі ваші контакти залишаться на зв'язку. @@ -883,6 +914,10 @@ Подати заявку No comment provided by engineer. + + Apply to + No comment provided by engineer. + Archive and upload Архівування та завантаження @@ -958,6 +993,10 @@ Назад No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address Неправильна адреса робочого столу @@ -983,6 +1022,10 @@ Кращі повідомлення No comment provided by engineer. + + Black + No comment provided by engineer. + Block Блокувати @@ -1093,6 +1136,10 @@ Не вдається отримати доступ до зв'язки ключів для збереження пароля до бази даних No comment provided by engineer. + + Cannot forward message + No comment provided by engineer. + Cannot receive file Не вдається отримати файл @@ -1164,6 +1211,10 @@ Архів чату No comment provided by engineer. + + Chat colors + No comment provided by engineer. + Chat console Консоль чату @@ -1209,6 +1260,10 @@ Налаштування чату No comment provided by engineer. + + Chat theme + No comment provided by engineer. + Chats Чати @@ -1239,6 +1294,18 @@ Виберіть з бібліотеки 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 Чисто @@ -1264,9 +1331,8 @@ Очистити перевірку No comment provided by engineer. - - Colors - Кольори + + Color mode No comment provided by engineer. @@ -1279,6 +1345,10 @@ Порівняйте коди безпеки зі своїми контактами. No comment provided by engineer. + + Completed + No comment provided by engineer. + Configure ICE servers Налаштування серверів ICE @@ -1388,16 +1458,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… Підключення до сервера… @@ -1443,6 +1525,18 @@ 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. + + + Connections subscribed + No comment provided by engineer. + Contact allows Контакт дозволяє @@ -1496,7 +1590,11 @@ This is your own one-time link! Copy Копіювати - chat item action + No comment provided by engineer. + + + Copy error + No comment provided by engineer. Core version: v%@ @@ -1573,6 +1671,10 @@ This is your own one-time link! Створіть свій профіль No comment provided by engineer. + + Created + No comment provided by engineer. + Created at Створено за адресою @@ -1608,6 +1710,10 @@ This is your own one-time link! Поточна парольна фраза… No comment provided by engineer. + + Current user + No comment provided by engineer. + Currently maximum supported file size is %@. Наразі максимальний підтримуваний розмір файлу - %@. @@ -1618,11 +1724,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 Ідентифікатор бази даних @@ -1927,6 +2041,10 @@ This cannot be undone! Видалити профіль користувача? No comment provided by engineer. + + Deleted + No comment provided by engineer. + Deleted at Видалено за @@ -1937,6 +2055,10 @@ This cannot be undone! Видалено за: %@ copied message info + + Deletion errors + No comment provided by engineer. + Delivery Доставка @@ -1977,6 +2099,14 @@ This cannot be undone! Помилка сервера призначення: %@ snd error text + + Detailed statistics + No comment provided by engineer. + + + Details + No comment provided by engineer. + Develop Розробник @@ -2132,6 +2262,10 @@ This cannot be undone! Завантажити chat item action + + Download errors + No comment provided by engineer. + Download failed Не вдалося завантажити @@ -2142,6 +2276,14 @@ This cannot be undone! Завантажити файл server test step + + Downloaded + No comment provided by engineer. + + + Downloaded files + No comment provided by engineer. + Downloading archive Завантажити архів @@ -2512,6 +2654,10 @@ This cannot be undone! Помилка експорту бази даних чату No comment provided by engineer. + + Error exporting theme: %@ + No comment provided by engineer. + Error importing chat database Помилка імпорту бази даних чату @@ -2537,11 +2683,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 Помилка збереження %@ серверів @@ -2660,7 +2818,8 @@ This cannot be undone! Error: %@ Помилка: %@ - snd error text + file error text + snd error text Error: URL is invalid @@ -2672,6 +2831,10 @@ This cannot be undone! Помилка: немає файлу бази даних No comment provided by engineer. + + Errors + No comment provided by engineer. + Even when disabled in the conversation. Навіть коли вимкнений у розмові. @@ -2697,6 +2860,10 @@ This cannot be undone! Помилка експорту: No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. Експортований архів бази даних. @@ -2732,6 +2899,26 @@ This cannot be undone! Улюблений No comment provided by engineer. + + 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. Файл буде видалено з серверів. @@ -2921,6 +3108,14 @@ Error: %2$@ GIF-файли та наклейки No comment provided by engineer. + + Good afternoon! + message preview + + + Good morning! + message preview + Group Група @@ -3201,6 +3396,10 @@ Error: %2$@ Не вдалося імпортувати No comment provided by engineer. + + Import theme + No comment provided by engineer. + Importing archive Імпорт архіву @@ -3323,6 +3522,10 @@ Error: %2$@ Інтерфейс No comment provided by engineer. + + Interface colors + No comment provided by engineer. + Invalid QR code Неправильний QR-код @@ -3666,6 +3869,10 @@ This is your link for group %@! Учасник No comment provided by engineer. + + Member inactive + item status text + Member role will be changed to "%@". All group members will be notified. Роль учасника буде змінено на "%@". Всі учасники групи будуть повідомлені про це. @@ -3681,6 +3888,10 @@ This is your link for group %@! Учасник буде видалений з групи - це неможливо скасувати! No comment provided by engineer. + + Menus + No comment provided by engineer. + Message delivery error Помилка доставки повідомлення @@ -3701,6 +3912,14 @@ This is your link for group %@! Чернетка повідомлення 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. @@ -3735,6 +3954,18 @@ This is your link for group %@! Джерело повідомлення залишається приватним. No comment provided by engineer. + + Message status + No comment provided by engineer. + + + Message status: %@ + copied message info + + + Message subscriptions + No comment provided by engineer. + Message text Текст повідомлення @@ -3760,6 +3991,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. Повідомлення, файли та дзвінки захищені **наскрізним шифруванням** з ідеальною секретністю переадресації, відмовою та відновленням після злому. @@ -3995,6 +4234,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 Немає фільтрованих чатів @@ -4010,6 +4253,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 Немає підключення до мережі @@ -4194,6 +4441,10 @@ This is your link for group %@! Відкрита міграція на інший пристрій authentication reason + + Open server settings + No comment provided by engineer. + Open user profiles Відкрити профілі користувачів @@ -4299,6 +4550,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. Люди можуть зв'язатися з вами лише за посиланнями, якими ви ділитеся. @@ -4324,6 +4579,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. Будь ласка, перевірте, чи ви скористалися правильним посиланням, або попросіть контактну особу надіслати вам інше. @@ -4421,6 +4681,10 @@ Error: %@ Попередній перегляд No comment provided by engineer. + + Previously connected servers + No comment provided by engineer. + Privacy & security Конфіденційність і безпека @@ -4486,6 +4750,10 @@ Error: %@ Пароль до профілю No comment provided by engineer. + + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. Оновлення профілю буде надіслано вашим контактам. @@ -4568,6 +4836,14 @@ Enable in *Network & servers* settings. Тайм-аут протоколу на КБ No comment provided by engineer. + + Proxied + No comment provided by engineer. + + + Proxied servers + No comment provided by engineer. + Push notifications Push-повідомлення @@ -4633,6 +4909,10 @@ Enable in *Network & servers* settings. Підтвердження виключені No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at Отримано за @@ -4653,6 +4933,18 @@ Enable in *Network & servers* settings. Отримано повідомлення 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. Адреса отримувача буде змінена на інший сервер. Зміна адреси завершиться після того, як відправник з'явиться в мережі. @@ -4683,11 +4975,31 @@ Enable in *Network & servers* settings. Одержувачі бачать оновлення, коли ви їх вводите. 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? Перепідключити сервери? @@ -4738,6 +5050,10 @@ Enable in *Network & servers* settings. Видалити No comment provided by engineer. + + Remove image + No comment provided by engineer. + Remove member Видалити учасника @@ -4808,16 +5124,32 @@ Enable in *Network & servers* settings. Перезавантаження 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 Перезапустіть програму, щоб створити новий профіль чату @@ -4888,6 +5220,10 @@ Enable in *Network & servers* settings. Запустити чат No comment provided by engineer. + + SMP server + No comment provided by engineer. + SMP servers Сервери SMP @@ -5003,6 +5339,14 @@ Enable in *Network & servers* settings. Збережене повідомлення message info title + + Scale + No comment provided by engineer. + + + Scan / Paste link + No comment provided by engineer. + Scan QR code Відскануйте QR-код @@ -5043,11 +5387,19 @@ Enable in *Network & servers* settings. Знайдіть або вставте посилання 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 Оцінка безпеки @@ -5063,6 +5415,10 @@ Enable in *Network & servers* settings. Виберіть No comment provided by engineer. + + Selected chat preferences prohibit this message. + No comment provided by engineer. + Self-destruct Самознищення @@ -5113,6 +5469,10 @@ Enable in *Network & servers* settings. Надіслати зникаюче повідомлення No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews Надіслати попередній перегляд за посиланням @@ -5223,6 +5583,10 @@ Enable in *Network & servers* settings. Надіслано за: %@ copied message info + + Sent directly + No comment provided by engineer. + Sent file event Подія надісланого файлу @@ -5233,11 +5597,31 @@ Enable in *Network & servers* settings. Надіслано повідомлення 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. Адреса сервера несумісна з налаштуваннями мережі. @@ -5258,6 +5642,10 @@ Enable in *Network & servers* settings. Тест сервера завершився невдало! No comment provided by engineer. + + Server type + No comment provided by engineer. + Server version is incompatible with network settings. Серверна версія несумісна з мережевими налаштуваннями. @@ -5268,6 +5656,14 @@ Enable in *Network & servers* settings. Сервери 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 Код сесії @@ -5283,6 +5679,10 @@ Enable in *Network & servers* settings. Встановити ім'я контакту… No comment provided by engineer. + + Set default theme + No comment provided by engineer. + Set group preferences Встановіть налаштування групи @@ -5403,6 +5803,10 @@ Enable in *Network & servers* settings. Показати: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address Адреса SimpleX @@ -5478,6 +5882,10 @@ Enable in *Network & servers* settings. Спрощений режим інкогніто No comment provided by engineer. + + Size + No comment provided by engineer. + Skip Пропустити @@ -5523,6 +5931,14 @@ Enable in *Network & servers* settings. Почати міграцію No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop Зупинити @@ -5588,6 +6004,22 @@ Enable in *Network & servers* settings. Надіслати No comment provided by engineer. + + Subscribed + No comment provided by engineer. + + + Subscription errors + No comment provided by engineer. + + + Subscription percentage + No comment provided by engineer. + + + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat Підтримка чату SimpleX @@ -5668,6 +6100,10 @@ Enable in *Network & servers* settings. Натисніть, щоб почати новий чат No comment provided by engineer. + + Temporary file error + No comment provided by engineer. + Test failed at step %@. Тест завершився невдало на кроці %@. @@ -5805,9 +6241,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. @@ -5875,11 +6310,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: Задати будь-які питання та отримувати новини: @@ -5947,11 +6390,19 @@ You will be prompted to complete authentication before this feature is enabled.< Увімкніть інкогніто при підключенні. 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: %@). Спроба з'єднатися з сервером, який використовується для отримання повідомлень від цього контакту (помилка: %@). @@ -6144,6 +6595,10 @@ To connect, please ask your contact to create another connection link and check Оновлення та відкритий чат No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed Не вдалося завантфжити @@ -6154,6 +6609,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 Завантаження архіву @@ -6229,6 +6692,10 @@ To connect, please ask your contact to create another connection link and check Профіль користувача No comment provided by engineer. + + User selection + No comment provided by engineer. + Using .onion hosts requires compatible VPN provider. Для використання хостів .onion потрібен сумісний VPN-провайдер. @@ -6364,6 +6831,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 Попередження: запуск чату на декількох пристроях не підтримується і може призвести до збоїв у доставці повідомлень @@ -6469,11 +6944,19 @@ To connect, please ask your contact to create another connection link and check Неправильний ключ або невідоме з'єднання - швидше за все, це з'єднання видалено. 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 server + No comment provided by engineer. + XFTP servers Сервери XFTP @@ -6556,6 +7039,10 @@ 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. Ви можете приймати дзвінки з екрана блокування без автентифікації пристрою та програми. @@ -6962,6 +7449,10 @@ SimpleX servers cannot see your profile. та %lld інших подій No comment provided by engineer. + + attempts + No comment provided by engineer. + audio call (not e2e encrypted) аудіовиклик (без шифрування e2e) @@ -6995,7 +7486,7 @@ SimpleX servers cannot see your profile. blocked by admin заблоковано адміністратором - marked deleted chat item preview text + blocked chat item bold @@ -7152,6 +7643,10 @@ SimpleX servers cannot see your profile. днів time unit + + decryption errors + No comment provided by engineer. + default (%@) за замовчуванням (%@) @@ -7202,6 +7697,10 @@ SimpleX servers cannot see your profile. дублююче повідомлення integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted e2e зашифрований @@ -7282,6 +7781,10 @@ SimpleX servers cannot see your profile. відбулася подія No comment provided by engineer. + + expired + No comment provided by engineer. + forwarded переслано @@ -7312,6 +7815,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 інкогніто за посиланням на контактну адресу @@ -7489,6 +7996,14 @@ SimpleX servers cannot see your profile. увімкненo group pref value + + other + No comment provided by engineer. + + + other errors + No comment provided by engineer. + owner власник @@ -7799,7 +8314,7 @@ last received msg: %2$@
- +
@@ -7836,7 +8351,7 @@ last received msg: %2$@
- +
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 9808c6fbcd..5d1852b0b4 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 @@
- +
@@ -545,9 +545,8 @@ 关于 SimpleX 地址 No comment provided by engineer. - - Accent color - 色调 + + Accent No comment provided by engineer. @@ -571,6 +570,14 @@ 接受隐身聊天 accept contact request via notification + + Acknowledged + No comment provided by engineer. + + + Acknowledgement errors + 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. 将地址添加到您的个人资料,以便您的联系人可以与其他人共享。个人资料更新将发送给您的联系人。 @@ -611,6 +618,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 地址 @@ -636,6 +655,10 @@ 高级网络设置 No comment provided by engineer. + + Advanced settings + No comment provided by engineer. + All app data is deleted. 已删除所有应用程序数据。 @@ -651,6 +674,10 @@ 所有数据在输入后将被删除。 No comment provided by engineer. + + All data is private to your device. + No comment provided by engineer. + All group members will remain connected. 所有群组成员将保持连接。 @@ -670,6 +697,10 @@ All new messages from %@ will be hidden! No comment provided by engineer. + + All users + No comment provided by engineer. + All your contacts will remain connected. 所有联系人会保持连接。 @@ -868,6 +899,10 @@ 应用 No comment provided by engineer. + + Apply to + No comment provided by engineer. + Archive and upload 存档和上传 @@ -943,6 +978,10 @@ 返回 No comment provided by engineer. + + Background + No comment provided by engineer. + Bad desktop address 糟糕的桌面地址 @@ -968,6 +1007,10 @@ 更好的消息 No comment provided by engineer. + + Black + No comment provided by engineer. + Block 封禁 @@ -1078,6 +1121,10 @@ 无法访问钥匙串以保存数据库密码 No comment provided by engineer. + + Cannot forward message + No comment provided by engineer. + Cannot receive file 无法接收文件 @@ -1148,6 +1195,10 @@ 聊天档案 No comment provided by engineer. + + Chat colors + No comment provided by engineer. + Chat console 聊天控制台 @@ -1193,6 +1244,10 @@ 聊天偏好设置 No comment provided by engineer. + + Chat theme + No comment provided by engineer. + Chats 聊天 @@ -1222,6 +1277,18 @@ 从库中选择 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 清除 @@ -1247,9 +1314,8 @@ 清除验证 No comment provided by engineer. - - Colors - 颜色 + + Color mode No comment provided by engineer. @@ -1262,6 +1328,10 @@ 与您的联系人比较安全码。 No comment provided by engineer. + + Completed + No comment provided by engineer. + Configure ICE servers 配置 ICE 服务器 @@ -1364,16 +1434,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… 连接服务器中…… @@ -1419,6 +1501,18 @@ 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. + + + Connections subscribed + No comment provided by engineer. + Contact allows 联系人允许 @@ -1472,7 +1566,11 @@ This is your own one-time link! Copy 复制 - chat item action + No comment provided by engineer. + + + Copy error + No comment provided by engineer. Core version: v%@ @@ -1548,6 +1646,10 @@ This is your own one-time link! 创建您的资料 No comment provided by engineer. + + Created + No comment provided by engineer. + Created at 创建于 @@ -1582,6 +1684,10 @@ This is your own one-time link! 现有密码…… No comment provided by engineer. + + Current user + No comment provided by engineer. + Currently maximum supported file size is %@. 目前支持的最大文件大小为 %@。 @@ -1592,11 +1698,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 @@ -1898,6 +2012,10 @@ This cannot be undone! 删除用户资料? No comment provided by engineer. + + Deleted + No comment provided by engineer. + Deleted at 已删除于 @@ -1908,6 +2026,10 @@ This cannot be undone! 已删除于:%@ copied message info + + Deletion errors + No comment provided by engineer. + Delivery 传送 @@ -1946,6 +2068,14 @@ This cannot be undone! Destination server error: %@ snd error text + + Detailed statistics + No comment provided by engineer. + + + Details + No comment provided by engineer. + Develop 开发 @@ -2099,6 +2229,10 @@ This cannot be undone! 下载 chat item action + + Download errors + No comment provided by engineer. + Download failed 下载失败了 @@ -2109,6 +2243,14 @@ This cannot be undone! 下载文件 server test step + + Downloaded + No comment provided by engineer. + + + Downloaded files + No comment provided by engineer. + Downloading archive 正在下载存档 @@ -2476,6 +2618,10 @@ This cannot be undone! 导出聊天数据库错误 No comment provided by engineer. + + Error exporting theme: %@ + No comment provided by engineer. + Error importing chat database 导入聊天数据库错误 @@ -2500,11 +2646,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 保存 %@ 服务器错误 @@ -2622,7 +2780,8 @@ This cannot be undone! Error: %@ 错误: %@ - snd error text + file error text + snd error text Error: URL is invalid @@ -2634,6 +2793,10 @@ This cannot be undone! 错误:没有数据库文件 No comment provided by engineer. + + Errors + No comment provided by engineer. + Even when disabled in the conversation. 即使在对话中被禁用。 @@ -2659,6 +2822,10 @@ This cannot be undone! 导出错误: No comment provided by engineer. + + Export theme + No comment provided by engineer. + Exported database archive. 导出数据库归档。 @@ -2694,6 +2861,26 @@ This cannot be undone! 最喜欢 No comment provided by engineer. + + 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. 文件将从服务器中删除。 @@ -2878,6 +3065,14 @@ Error: %2$@ GIF 和贴纸 No comment provided by engineer. + + Good afternoon! + message preview + + + Good morning! + message preview + Group 群组 @@ -3156,6 +3351,10 @@ Error: %2$@ 导入失败了 No comment provided by engineer. + + Import theme + No comment provided by engineer. + Importing archive 正在导入存档 @@ -3278,6 +3477,10 @@ Error: %2$@ 界面 No comment provided by engineer. + + Interface colors + No comment provided by engineer. + Invalid QR code 无效的二维码 @@ -3616,6 +3819,10 @@ This is your link for group %@! 成员 No comment provided by engineer. + + Member inactive + item status text + Member role will be changed to "%@". All group members will be notified. 成员角色将更改为 "%@"。所有群成员将收到通知。 @@ -3631,6 +3838,10 @@ This is your link for group %@! 成员将被移出群组——此操作无法撤消! No comment provided by engineer. + + Menus + No comment provided by engineer. + Message delivery error 消息传递错误 @@ -3650,6 +3861,14 @@ This is your link for group %@! 消息草稿 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. @@ -3682,6 +3901,18 @@ This is your link for group %@! 消息来源保持私密。 No comment provided by engineer. + + Message status + No comment provided by engineer. + + + Message status: %@ + copied message info + + + Message subscriptions + No comment provided by engineer. + Message text 消息正文 @@ -3706,6 +3937,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. @@ -3938,6 +4177,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 无过滤聊天 @@ -3953,6 +4196,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 无网络连接 @@ -4136,6 +4383,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 打开用户个人资料 @@ -4239,6 +4490,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. 人们只能通过您共享的链接与您建立联系。 @@ -4264,6 +4519,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. 请检查您使用的链接是否正确,或者让您的联系人给您发送另一个链接。 @@ -4359,6 +4619,10 @@ Error: %@ 预览 No comment provided by engineer. + + Previously connected servers + No comment provided by engineer. + Privacy & security 隐私和安全 @@ -4420,6 +4684,10 @@ Error: %@ 个人资料密码 No comment provided by engineer. + + Profile theme + No comment provided by engineer. + Profile update will be sent to your contacts. 个人资料更新将被发送给您的联系人。 @@ -4498,6 +4766,14 @@ Enable in *Network & servers* settings. 每 KB 协议超时 No comment provided by engineer. + + Proxied + No comment provided by engineer. + + + Proxied servers + No comment provided by engineer. + Push notifications 推送通知 @@ -4562,6 +4838,10 @@ Enable in *Network & servers* settings. 回执已禁用 No comment provided by engineer. + + Receive errors + No comment provided by engineer. + Received at 已收到于 @@ -4582,6 +4862,18 @@ Enable in *Network & servers* settings. 收到的信息 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. 接收地址将变更到不同的服务器。地址更改将在发件人上线后完成。 @@ -4611,11 +4903,31 @@ Enable in *Network & servers* settings. 对方会在您键入时看到更新。 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? 是否重新连接服务器? @@ -4666,6 +4978,10 @@ Enable in *Network & servers* settings. 移除 No comment provided by engineer. + + Remove image + No comment provided by engineer. + Remove member 删除成员 @@ -4736,16 +5052,32 @@ Enable in *Network & servers* settings. 重置 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 重新启动应用程序以创建新的聊天资料 @@ -4816,6 +5148,10 @@ Enable in *Network & servers* settings. 运行聊天程序 No comment provided by engineer. + + SMP server + No comment provided by engineer. + SMP servers SMP 服务器 @@ -4930,6 +5266,14 @@ Enable in *Network & servers* settings. 已保存的消息 message info title + + Scale + No comment provided by engineer. + + + Scan / Paste link + No comment provided by engineer. + Scan QR code 扫描二维码 @@ -4970,11 +5314,19 @@ Enable in *Network & servers* settings. 搜索或粘贴 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 安全评估 @@ -4990,6 +5342,10 @@ Enable in *Network & servers* settings. 选择 No comment provided by engineer. + + Selected chat preferences prohibit this message. + No comment provided by engineer. + Self-destruct 自毁 @@ -5040,6 +5396,10 @@ Enable in *Network & servers* settings. 发送限时消息中 No comment provided by engineer. + + Send errors + No comment provided by engineer. + Send link previews 发送链接预览 @@ -5148,6 +5508,10 @@ Enable in *Network & servers* settings. 已发送于:%@ copied message info + + Sent directly + No comment provided by engineer. + Sent file event 已发送文件项目 @@ -5158,11 +5522,31 @@ Enable in *Network & servers* settings. 已发信息 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. @@ -5182,6 +5566,10 @@ Enable in *Network & servers* settings. 服务器测试失败! No comment provided by engineer. + + Server type + No comment provided by engineer. + Server version is incompatible with network settings. srv error text @@ -5191,6 +5579,14 @@ Enable in *Network & servers* settings. 服务器 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 会话码 @@ -5206,6 +5602,10 @@ Enable in *Network & servers* settings. 设置联系人姓名…… No comment provided by engineer. + + Set default theme + No comment provided by engineer. + Set group preferences 设置群组偏好设置 @@ -5324,6 +5724,10 @@ Enable in *Network & servers* settings. 显示: No comment provided by engineer. + + SimpleX + No comment provided by engineer. + SimpleX Address SimpleX 地址 @@ -5399,6 +5803,10 @@ Enable in *Network & servers* settings. 简化的隐身模式 No comment provided by engineer. + + Size + No comment provided by engineer. + Skip 跳过 @@ -5444,6 +5852,14 @@ Enable in *Network & servers* settings. 开始迁移 No comment provided by engineer. + + Starting from %@. + No comment provided by engineer. + + + Statistics + No comment provided by engineer. + Stop 停止 @@ -5509,6 +5925,22 @@ Enable in *Network & servers* settings. 提交 No comment provided by engineer. + + Subscribed + No comment provided by engineer. + + + Subscription errors + No comment provided by engineer. + + + Subscription percentage + No comment provided by engineer. + + + Subscriptions ignored + No comment provided by engineer. + Support SimpleX Chat 支持 SimpleX Chat @@ -5589,6 +6021,10 @@ Enable in *Network & servers* settings. 点击开始一个新聊天 No comment provided by engineer. + + Temporary file error + No comment provided by engineer. + Test failed at step %@. 在步骤 %@ 上测试失败。 @@ -5725,9 +6161,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. @@ -5795,11 +6230,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: 要提出任何问题并接收更新,请: @@ -5866,11 +6309,19 @@ You will be prompted to complete authentication before this feature is enabled.< 在连接时切换隐身模式。 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: %@). 正在尝试连接到用于从该联系人接收消息的服务器(错误:%@)。 @@ -6061,6 +6512,10 @@ To connect, please ask your contact to create another connection link and check 升级并打开聊天 No comment provided by engineer. + + Upload errors + No comment provided by engineer. + Upload failed 上传失败了 @@ -6071,6 +6526,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 正在上传存档 @@ -6143,6 +6606,10 @@ To connect, please ask your contact to create another connection link and check 用户资料 No comment provided by engineer. + + User selection + No comment provided by engineer. + Using .onion hosts requires compatible VPN provider. 使用 .onion 主机需要兼容的 VPN 提供商。 @@ -6277,6 +6744,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 警告:不支持在多部设备上启动聊天,这么做会导致消息传送失败。 @@ -6379,11 +6854,19 @@ To connect, please ask your contact to create another connection link and check 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 server + No comment provided by engineer. + XFTP servers XFTP 服务器 @@ -6459,6 +6942,10 @@ 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. 您可以从锁屏上接听电话,无需设备和应用程序的认证。 @@ -6860,6 +7347,10 @@ SimpleX 服务器无法看到您的资料。 and %lld other events No comment provided by engineer. + + attempts + No comment provided by engineer. + audio call (not e2e encrypted) 语音通话(非端到端加密) @@ -6893,7 +7384,7 @@ SimpleX 服务器无法看到您的资料。 blocked by admin 由管理员封禁 - marked deleted chat item preview text + blocked chat item bold @@ -7049,6 +7540,10 @@ SimpleX 服务器无法看到您的资料。 time unit + + decryption errors + No comment provided by engineer. + default (%@) 默认 (%@) @@ -7099,6 +7594,10 @@ SimpleX 服务器无法看到您的资料。 重复的消息 integrity error chat item + + duplicates + No comment provided by engineer. + e2e encrypted 端到端加密 @@ -7179,6 +7678,10 @@ SimpleX 服务器无法看到您的资料。 发生的事 No comment provided by engineer. + + expired + No comment provided by engineer. + forwarded 已转发 @@ -7209,6 +7712,10 @@ SimpleX 服务器无法看到您的资料。 在您重启应用或改变密码后,iOS钥匙串将被用来安全地存储密码——它将允许接收推送通知。 No comment provided by engineer. + + inactive + No comment provided by engineer. + incognito via contact address link 通过联系人地址链接隐身聊天 @@ -7385,6 +7892,14 @@ SimpleX 服务器无法看到您的资料。 开启 group pref value + + other + No comment provided by engineer. + + + other errors + No comment provided by engineer. + owner 群主 @@ -7687,7 +8202,7 @@ last received msg: %2$@
- +
@@ -7723,7 +8238,7 @@ last received msg: %2$@
- +
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 NSE/NotificationService.swift b/apps/ios/SimpleX NSE/NotificationService.swift index 12094c7053..764415b1aa 100644 --- a/apps/ios/SimpleX NSE/NotificationService.swift +++ b/apps/ios/SimpleX NSE/NotificationService.swift @@ -439,8 +439,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() @@ -660,14 +659,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 } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index 8907938f52..9bd72ff5a9 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -18,7 +18,6 @@ 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 */; }; @@ -178,25 +177,38 @@ 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 */; }; + CE984D4B2C36C5D500E3AEFF /* ChatItemClipShape.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.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 */; }; - E5D68D3F2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D68D3A2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a */; }; - E5D68D402C22D78C00CBA347 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D68D3B2C22D78C00CBA347 /* libffi.a */; }; - E5D68D412C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D68D3C2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a */; }; - E5D68D422C22D78C00CBA347 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D68D3D2C22D78C00CBA347 /* libgmp.a */; }; - E5D68D432C22D78C00CBA347 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5D68D3E2C22D78C00CBA347 /* libgmpxx.a */; }; + E50581002C3DDD7F009C3F71 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E50580FB2C3DDD7F009C3F71 /* libffi.a */; }; + E50581012C3DDD7F009C3F71 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E50580FC2C3DDD7F009C3F71 /* libgmp.a */; }; + E50581022C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E50580FD2C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN-ghc9.6.3.a */; }; + E50581032C3DDD7F009C3F71 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E50580FE2C3DDD7F009C3F71 /* libgmpxx.a */; }; + E50581042C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E50580FF2C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN.a */; }; + E50581062C3DDD9D009C3F71 /* Yams in Frameworks */ = {isa = PBXBuildFile; productRef = E50581052C3DDD9D009C3F71 /* Yams */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -267,7 +279,6 @@ 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 = ""; }; @@ -475,23 +486,34 @@ 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 = ""; }; + CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatItemClipShape.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; }; - E5D68D3A2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a"; sourceTree = ""; }; - E5D68D3B2C22D78C00CBA347 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; - E5D68D3C2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a"; sourceTree = ""; }; - E5D68D3D2C22D78C00CBA347 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; - E5D68D3E2C22D78C00CBA347 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + E50580FB2C3DDD7F009C3F71 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = ""; }; + E50580FC2C3DDD7F009C3F71 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = ""; }; + E50580FD2C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN-ghc9.6.3.a"; sourceTree = ""; }; + E50580FE2C3DDD7F009C3F71 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; + E50580FF2C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN.a"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -500,6 +522,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 +552,14 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - E5D68D412C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a in Frameworks */, + E50581032C3DDD7F009C3F71 /* libgmpxx.a in Frameworks */, 5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */, + E50581002C3DDD7F009C3F71 /* libffi.a in Frameworks */, + E50581012C3DDD7F009C3F71 /* libgmp.a in Frameworks */, 5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */, - E5D68D3F2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a in Frameworks */, - E5D68D422C22D78C00CBA347 /* libgmp.a in Frameworks */, - E5D68D402C22D78C00CBA347 /* libffi.a in Frameworks */, - E5D68D432C22D78C00CBA347 /* libgmpxx.a in Frameworks */, + E50581022C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN-ghc9.6.3.a in Frameworks */, + E50581062C3DDD9D009C3F71 /* Yams in Frameworks */, + E50581042C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -591,6 +615,7 @@ 5CE4407127ADB1D0007B033A /* Emoji.swift */, 5CADE79B292131E900072E13 /* ContactPreferencesView.swift */, 5CBE6C11294487F7002D9531 /* VerifyCodeView.swift */, + CEEA861C2C2ABCB50084E1EA /* ReverseList.swift */, 5CBE6C132944CC12002D9531 /* ScanCodeView.swift */, 64C06EB42A0A4A7C00792D4D /* ChatItemInfoView.swift */, 648679AA2BC96A74006456E7 /* ChatItemForwardingView.swift */, @@ -601,11 +626,11 @@ 5C764E5C279C70B7000C6508 /* Libraries */ = { isa = PBXGroup; children = ( - E5D68D3B2C22D78C00CBA347 /* libffi.a */, - E5D68D3D2C22D78C00CBA347 /* libgmp.a */, - E5D68D3E2C22D78C00CBA347 /* libgmpxx.a */, - E5D68D3C2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c-ghc9.6.3.a */, - E5D68D3A2C22D78C00CBA347 /* libHSsimplex-chat-5.8.1.0-GEbUSGuGADZH0bnStuks0c.a */, + E50580FB2C3DDD7F009C3F71 /* libffi.a */, + E50580FC2C3DDD7F009C3F71 /* libgmp.a */, + E50580FE2C3DDD7F009C3F71 /* libgmpxx.a */, + E50580FD2C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN-ghc9.6.3.a */, + E50580FF2C3DDD7F009C3F71 /* libHSsimplex-chat-6.0.0.0-IhofDzGnTMcDdW5i3Fb7xN.a */, ); path = Libraries; sourceTree = ""; @@ -651,7 +676,6 @@ 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */, 646BB38D283FDB6D001CE359 /* LocalAuthenticationUtils.swift */, 5C6BA666289BD954009B8ECC /* DismissSheets.swift */, - 5C00164328A26FBC0094D739 /* ContextMenu.swift */, 5CA7DFC229302AF000F7FDDE /* AppSheet.swift */, 18415A7F0F189D87DEFEABCA /* PressedButtonStyle.swift */, 5CCB939B297EFCB100399E78 /* NavStackCompat.swift */, @@ -660,6 +684,10 @@ 64C3B0202A0D359700E19930 /* CustomTimePicker.swift */, 5CEBD7452A5C0A8F00665FE2 /* KeyboardPadding.swift */, 8C05382D2B39887E006436DC /* VideoUtils.swift */, + 8C7F8F0D2C19C0C100D16888 /* ViewModifiers.swift */, + 8C74C3ED2C1B942300039E77 /* ChatWallpaper.swift */, + 8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */, + CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */, ); path = Helpers; sourceTree = ""; @@ -684,6 +712,7 @@ 5CA059C2279559F40002BEB4 /* Shared */ = { isa = PBXGroup; children = ( + 8C74C3E92C1B909200039E77 /* Theme */, 5CA059C3279559F40002BEB4 /* SimpleXApp.swift */, 5C36027227F47AD5009F19D9 /* AppDelegate.swift */, 5CA059C4279559F40002BEB4 /* ContentView.swift */, @@ -802,6 +831,7 @@ 5C13730A28156D2700F43030 /* ContactConnectionView.swift */, 5C10D88728EED12E00E58BF0 /* ContactConnectionInfo.swift */, 18415835CBD939A9ABDC108A /* UserPicker.swift */, + 64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */, ); path = ChatList; sourceTree = ""; @@ -820,6 +850,7 @@ 5CE2BA692845308900EC33A6 /* SimpleXChat */ = { isa = PBXGroup; children = ( + 8C86EBE32C0DAE3700E12243 /* Theme */, 5CDCAD5228186F9500503DA2 /* AppGroup.swift */, 5CDCAD7228188CFF00503DA2 /* ChatTypes.swift */, 5CDCAD7428188D2900503DA2 /* APITypes.swift */, @@ -913,6 +944,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 +962,16 @@ path = Migration; sourceTree = ""; }; + 8C86EBE32C0DAE3700E12243 /* Theme */ = { + isa = PBXGroup; + children = ( + 8C7E3CE32C0DEAC400BFF63A /* ThemeTypes.swift */, + 8C852B072C1086D100BA61E8 /* Color.swift */, + 8C804B1D2C11F966007A63C8 /* ChatWallpaperTypes.swift */, + ); + path = Theme; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -960,6 +1010,7 @@ D77B92DB2952372200A5A1CC /* SwiftyGif */, D7F0E33829964E7E0068AF69 /* LZString */, D7197A1729AE89660055C05A /* WebRTC */, + 8C8118712C220B5B00E6FC94 /* Yams */, ); productName = "SimpleX (iOS)"; productReference = 5CA059CA279559F40002BEB4 /* SimpleX.app */; @@ -1016,6 +1067,7 @@ ); name = SimpleXChat; packageProductDependencies = ( + E50581052C3DDD9D009C3F71 /* Yams */, ); productName = SimpleXChat; productReference = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; @@ -1080,6 +1132,7 @@ D77B92DA2952372200A5A1CC /* XCRemoteSwiftPackageReference "SwiftyGif" */, D7F0E33729964E7D0068AF69 /* XCRemoteSwiftPackageReference "lzstring-swift" */, D7197A1629AE89660055C05A /* XCRemoteSwiftPackageReference "WebRTC" */, + 8C73C1162C21E17B00892670 /* XCRemoteSwiftPackageReference "Yams" */, ); productRefGroup = 5CA059CB279559F40002BEB4 /* Products */; projectDirPath = ""; @@ -1134,6 +1187,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 */, @@ -1152,6 +1206,7 @@ 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 */, @@ -1176,7 +1231,7 @@ 5C7505A827B6D34800BE3227 /* ChatInfoToolbar.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 +1239,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,6 +1264,7 @@ 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 */, @@ -1225,6 +1282,7 @@ 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,6 +1292,7 @@ 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 */, @@ -1256,6 +1315,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 */, @@ -1309,6 +1369,7 @@ 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 */, @@ -1317,7 +1378,9 @@ 5CE2BA8F284533A300EC33A6 /* APITypes.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 */, 5CE2BA8E284533A300EC33A6 /* API.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1552,7 +1615,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 225; + CURRENT_PROJECT_VERSION = 227; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1577,7 +1640,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES_THIN; - MARKETING_VERSION = 5.8.1; + MARKETING_VERSION = 6.0; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1601,7 +1664,7 @@ CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 225; + CURRENT_PROJECT_VERSION = 227; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; @@ -1626,7 +1689,7 @@ "@executable_path/Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.8.1; + MARKETING_VERSION = 6.0; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app; PRODUCT_NAME = SimpleX; SDKROOT = iphoneos; @@ -1687,7 +1750,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 225; + CURRENT_PROJECT_VERSION = 227; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; GCC_OPTIMIZATION_LEVEL = s; @@ -1702,7 +1765,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.8.1; + MARKETING_VERSION = 6.0; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1724,7 +1787,7 @@ CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 225; + CURRENT_PROJECT_VERSION = 227; DEVELOPMENT_TEAM = 5NN7GUYB6T; ENABLE_BITCODE = NO; ENABLE_CODE_COVERAGE = NO; @@ -1739,7 +1802,7 @@ "@executable_path/../../Frameworks", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.8.1; + MARKETING_VERSION = 6.0; PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE"; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -1761,7 +1824,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 225; + CURRENT_PROJECT_VERSION = 227; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1787,7 +1850,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.8.1; + MARKETING_VERSION = 6.0; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -1812,7 +1875,7 @@ CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES; CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 225; + CURRENT_PROJECT_VERSION = 227; DEFINES_MODULE = YES; DEVELOPMENT_TEAM = 5NN7GUYB6T; DYLIB_COMPATIBILITY_VERSION = 1; @@ -1838,7 +1901,7 @@ "$(PROJECT_DIR)/Libraries/sim", ); LLVM_LTO = YES; - MARKETING_VERSION = 5.8.1; + MARKETING_VERSION = 6.0; PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; @@ -1915,6 +1978,14 @@ minimumVersion = 2.0.0; }; }; + 8C73C1162C21E17B00892670 /* XCRemoteSwiftPackageReference "Yams" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/jpsim/Yams"; + requirement = { + kind = exactVersion; + version = 5.1.2; + }; + }; D7197A1629AE89660055C05A /* XCRemoteSwiftPackageReference "WebRTC" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/simplex-chat/WebRTC.git"; @@ -1947,6 +2018,11 @@ package = 5C8F01CB27A6F0D8007D2C8D /* XCRemoteSwiftPackageReference "CodeScanner" */; productName = CodeScanner; }; + 8C8118712C220B5B00E6FC94 /* Yams */ = { + isa = XCSwiftPackageProductDependency; + package = 8C73C1162C21E17B00892670 /* XCRemoteSwiftPackageReference "Yams" */; + productName = Yams; + }; D7197A1729AE89660055C05A /* WebRTC */ = { isa = XCSwiftPackageProductDependency; package = D7197A1629AE89660055C05A /* XCRemoteSwiftPackageReference "WebRTC" */; @@ -1962,6 +2038,11 @@ package = D7F0E33729964E7D0068AF69 /* XCRemoteSwiftPackageReference "lzstring-swift" */; productName = LZString; }; + E50581052C3DDD9D009C3F71 /* Yams */ = { + isa = XCSwiftPackageProductDependency; + package = 8C73C1162C21E17B00892670 /* XCRemoteSwiftPackageReference "Yams" */; + productName = Yams; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 5CA059BE279559F40002BEB4 /* Project object */; 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..d3e61c88f9 100644 --- a/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/apps/ios/SimpleX.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "e2611d1e91fd8071abc106776ba14ee2e395d2ad08a78e073381294abc10f115", "pins" : [ { "identity" : "codescanner", @@ -33,7 +34,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/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 7b0a0a6646..2d3663b9f8 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -8,9 +8,10 @@ import Foundation import SwiftUI +import Yams public let jsonDecoder = getJSONDecoder() -let jsonEncoder = getJSONEncoder() +public let jsonEncoder = getJSONEncoder() public enum ChatCommand { case showActiveUser @@ -29,8 +30,7 @@ public enum ChatCommand { 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) @@ -78,6 +78,7 @@ 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) @@ -106,6 +107,8 @@ public enum ChatCommand { 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) @@ -122,6 +125,7 @@ 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) @@ -142,6 +146,8 @@ public enum ChatCommand { case apiStandaloneFileInfo(url: String) // misc case showVersion + case getAgentServersSummary(userId: Int64) + case resetAgentServersStats case string(String) public var cmdString: String { @@ -169,8 +175,7 @@ public enum ChatCommand { 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))" @@ -226,6 +231,7 @@ 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)" @@ -268,6 +274,8 @@ public enum ChatCommand { 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)" @@ -301,6 +309,8 @@ 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 .getAgentServersSummary(userId): return "/get servers summary \(userId)" + case .resetAgentServersStats: return "/reset servers stats" case let .string(str): return str } } @@ -325,8 +335,7 @@ public enum ChatCommand { 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" @@ -375,6 +384,7 @@ 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" @@ -402,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" @@ -435,6 +447,8 @@ public enum ChatCommand { case .apiDownloadStandaloneFile: return "apiDownloadStandaloneFile" case .apiStandaloneFileInfo: return "apiStandaloneFileInfo" case .showVersion: return "showVersion" + case .getAgentServersSummary: return "getAgentServersSummary" + case .resetAgentServersStats: return "resetAgentServersStats" case .string: return "console command" } } @@ -663,6 +677,8 @@ public enum ChatResponse: Decodable, Error { // misc case versionInfo(versionInfo: CoreVersionInfo, chatMigrations: [UpMigration], agentMigrations: [UpMigration]) case cmdOk(user: UserRef?) + 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 archiveImported(archiveErrors: [ArchiveError]) @@ -821,6 +837,8 @@ public enum ChatResponse: Decodable, Error { case .contactPQEnabled: return "contactPQEnabled" case .versionInfo: return "versionInfo" case .cmdOk: return "cmdOk" + case .agentServersSummary: return "agentServersSummary" + case .agentSubsSummary: return "agentSubsSummary" case .chatCmdError: return "chatCmdError" case .chatError: return "chatError" case .archiveImported: return "archiveImported" @@ -849,7 +867,7 @@ public enum ChatResponse: Decodable, Error { 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(rcvMsgInfo) } else { "none" } + 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))") @@ -984,6 +1002,8 @@ public enum ChatResponse: Decodable, Error { 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 .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 .archiveImported(archiveErrors): return String(describing: archiveErrors) @@ -1010,20 +1030,20 @@ public func chatError(_ chatResponse: ChatResponse) -> ChatErrorType? { } } -public enum ConnectionPlan: Decodable { +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 @@ -1032,7 +1052,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 @@ -1040,13 +1060,13 @@ 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) @@ -1109,13 +1129,13 @@ public struct ServerCfg: Identifiable, Equatable, Codable { public var server: String public var preset: Bool public var tested: Bool? - public var enabled: Bool + public var enabled: ServerEnabled var createdAt = Date() // public var sendEnabled: Bool // can we potentially want to prevent sending on the servers we use to receive? // Even if we don't see the use case, it's probably better to allow it in the model // In any case, "trusted/known" servers are out of scope of this change - public init(server: String, preset: Bool, tested: Bool?, enabled: Bool) { + public init(server: String, preset: Bool, tested: Bool?, enabled: ServerEnabled) { self.server = server self.preset = preset self.tested = tested @@ -1128,7 +1148,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: .enabled) public var isEmpty: Bool { server.trimmingCharacters(in: .whitespaces) == "" @@ -1145,19 +1165,19 @@ public struct ServerCfg: Identifiable, Equatable, Codable { server: "smp://abcd@smp8.simplex.im", preset: true, tested: true, - enabled: true + enabled: .enabled ), custom: ServerCfg( server: "smp://abcd@smp9.simplex.im", preset: false, tested: false, - enabled: false + enabled: .disabled ), untested: ServerCfg( server: "smp://abcd@smp10.simplex.im", preset: false, tested: nil, - enabled: true + enabled: .enabled ) ) @@ -1169,6 +1189,12 @@ public struct ServerCfg: Identifiable, Equatable, Codable { } } +public enum ServerEnabled: String, Codable { + case disabled + case enabled + case known +} + public enum ProtocolTestStep: String, Decodable, Equatable { case connect case disconnect @@ -1198,8 +1224,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 @@ -1262,7 +1288,7 @@ 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 @@ -1308,18 +1334,18 @@ public struct NetCfg: Codable, Equatable { 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 SocksMode: String, Codable { +public enum SocksMode: String, Codable, Hashable { case always = "always" case onion = "onion" } -public enum SMPProxyMode: String, Codable { +public enum SMPProxyMode: String, Codable, Hashable { case always = "always" case unknown = "unknown" case unprotected = "unprotected" @@ -1339,7 +1365,7 @@ public enum SMPProxyMode: String, Codable { public static let values: [SMPProxyMode] = [.always, .unknown, .unprotected, .never] } -public enum SMPProxyFallback: String, Codable { +public enum SMPProxyFallback: String, Codable, Hashable { case allow = "allow" case allowProtected = "allowProtected" case prohibit = "prohibit" @@ -1357,7 +1383,7 @@ public enum SMPProxyFallback: String, Codable { public static let values: [SMPProxyFallback] = [.allow, .allowProtected, .prohibit] } -public enum OnionHosts: String, Identifiable { +public enum OnionHosts: String, Identifiable, Hashable { case no case prefer case require @@ -1391,7 +1417,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 @@ -1407,7 +1433,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 @@ -1415,7 +1441,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 @@ -1453,12 +1479,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 @@ -1472,13 +1498,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 @@ -1488,7 +1514,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] @@ -1504,30 +1530,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 } @@ -1551,7 +1577,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? @@ -1565,7 +1591,7 @@ public struct UserContactLink: Decodable { } } -public struct AutoAccept: Codable { +public struct AutoAccept: Codable, Hashable { public var acceptIncognito: Bool public var autoReply: MsgContent? @@ -1587,7 +1613,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 @@ -1601,12 +1627,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 @@ -1620,7 +1646,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" @@ -1638,7 +1664,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 @@ -1656,7 +1682,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? @@ -1666,7 +1692,7 @@ public struct RemoteCtrlInfo: Decodable { } } -public enum RemoteCtrlSessionState: Decodable { +public enum RemoteCtrlSessionState: Decodable, Hashable { case starting case searching case connecting @@ -1681,17 +1707,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 @@ -1713,7 +1739,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) @@ -1722,7 +1748,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) @@ -1801,7 +1827,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) @@ -1861,7 +1887,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) @@ -1869,12 +1895,12 @@ public enum DatabaseError: Decodable { case errorOpen(sqliteError: SQLiteError) } -public enum SQLiteError: Decodable { +public enum SQLiteError: Decodable, Hashable { case errorNotADatabase case error(String) } -public enum AgentErrorType: Decodable { +public enum AgentErrorType: Decodable, Hashable { case CMD(cmdErr: CommandErrorType) case CONN(connErr: ConnectionErrorType) case SMP(smpErr: ProtocolErrorType) @@ -1888,7 +1914,7 @@ public enum AgentErrorType: Decodable { case INACTIVE } -public enum CommandErrorType: Decodable { +public enum CommandErrorType: Decodable, Hashable { case PROHIBITED case SYNTAX case NO_CONN @@ -1896,7 +1922,7 @@ public enum CommandErrorType: Decodable { case LARGE } -public enum ConnectionErrorType: Decodable { +public enum ConnectionErrorType: Decodable, Hashable { case NOT_FOUND case DUPLICATE case SIMPLEX @@ -1904,7 +1930,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 @@ -1913,7 +1939,7 @@ public enum BrokerErrorType: Decodable { case TIMEOUT } -public enum ProtocolErrorType: Decodable { +public enum ProtocolErrorType: Decodable, Hashable { case BLOCK case SESSION case CMD(cmdErr: ProtocolCommandError) @@ -1924,7 +1950,7 @@ public enum ProtocolErrorType: Decodable { case INTERNAL } -public enum XFTPErrorType: Decodable { +public enum XFTPErrorType: Decodable, Hashable { case BLOCK case SESSION case CMD(cmdErr: ProtocolCommandError) @@ -1941,7 +1967,7 @@ public enum XFTPErrorType: Decodable { case INTERNAL } -public enum RCErrorType: Decodable { +public enum RCErrorType: Decodable, Hashable { case `internal`(internalErr: String) case identity case noLocalAddress @@ -1959,7 +1985,7 @@ public enum RCErrorType: Decodable { case syntax(syntaxErr: String) } -public enum ProtocolCommandError: Decodable { +public enum ProtocolCommandError: Decodable, Hashable { case UNKNOWN case SYNTAX case PROHIBITED @@ -1968,7 +1994,7 @@ public enum ProtocolCommandError: Decodable { case NO_ENTITY } -public enum ProtocolTransportError: Decodable { +public enum ProtocolTransportError: Decodable, Hashable { case badBlock case largeMsg case badSession @@ -1976,14 +2002,14 @@ public enum ProtocolTransportError: Decodable { 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 @@ -1992,12 +2018,12 @@ public enum SMPAgentError: Decodable { case A_QUEUE(queueErr: String) } -public enum ArchiveError: Decodable { +public enum ArchiveError: Decodable, Hashable { case `import`(chatError: ChatError) case importFile(file: String, chatError: ChatError) } -public enum RemoteCtrlError: Decodable { +public enum RemoteCtrlError: Decodable, Hashable { case inactive case badState case busy @@ -2011,14 +2037,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? @@ -2050,7 +2076,7 @@ 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 @@ -2071,6 +2097,11 @@ public struct AppSettings: Codable, Equatable { public var androidCallOnLockScreen: AppSettingsLockScreenCalls? = nil public var iosCallKitEnabled: Bool? = nil public var iosCallKitCallsInRecents: Bool? = nil + public var uiProfileImageCornerRadius: Float? = nil + public var uiColorScheme: String? = nil + public var uiDarkColorScheme: String? = nil + public var uiCurrentThemeIds: [String: String]? = nil + public var uiThemes: [ThemeOverrides]? = nil public func prepareForExport() -> AppSettings { var empty = AppSettings() @@ -2095,6 +2126,11 @@ 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 } return empty } @@ -2119,12 +2155,17 @@ 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]? ) } } -public enum AppSettingsNotificationMode: String, Codable { +public enum AppSettingsNotificationMode: String, Codable, Hashable { case off case periodic case instant @@ -2152,13 +2193,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 @@ -2168,7 +2209,7 @@ public struct UserNetworkInfo: Codable, Equatable { } } -public enum UserNetworkType: String, Codable { +public enum UserNetworkType: String, Codable, Hashable { case none case cellular case wifi @@ -2186,7 +2227,7 @@ public enum UserNetworkType: String, Codable { } } -public struct RcvMsgInfo: Codable { +public struct RcvMsgInfo: Codable, Hashable { var msgId: Int64 var msgDeliveryId: Int64 var msgDeliveryStatus: String @@ -2194,7 +2235,7 @@ public struct RcvMsgInfo: Codable { var agentMsgMeta: String } -public struct QueueInfo: Codable { +public struct QueueInfo: Codable, Hashable { var qiSnd: Bool var qiNtf: Bool var qiSub: QSub? @@ -2202,25 +2243,171 @@ public struct QueueInfo: Codable { var qiMsg: MsgInfo? } -public struct QSub: Codable { +public struct QSub: Codable, Hashable { var qSubThread: QSubThread var qDelivered: String? } -public enum QSubThread: String, Codable { +public enum QSubThread: String, Codable, Hashable { case noSub case subPending case subThread case prohibitSub } -public struct MsgInfo: Codable { +public struct MsgInfo: Codable, Hashable { var msgId: String var msgTs: Date var msgType: MsgType } -public enum MsgType: String, Codable { +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 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 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 +} diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 1f58ee2363..b84e4bb3a0 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) @@ -1370,7 +1373,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { } } - public enum ShowEnableVoiceMessagesAlert { + public enum ShowEnableVoiceMessagesAlert: Hashable { case userEnable case askContact case groupOwnerCan @@ -1443,7 +1446,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,7 +1463,7 @@ public enum ChatInfo: Identifiable, Decodable, NamedChat { ) } -public struct ChatData: Decodable, Identifiable { +public struct ChatData: Decodable, Identifiable, Hashable { public var chatInfo: ChatInfo public var chatItems: [ChatItem] public var chatStats: ChatStats @@ -1476,7 +1479,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 +1491,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,6 +1507,7 @@ public struct Contact: Identifiable, Decodable, NamedChat { var chatTs: Date? var contactGroupMemberId: Int64? var contactGrpInvSent: Bool + public var uiThemes: ThemeModeOverrides? public var id: ChatId { get { "@\(contactId)" } } public var apiId: Int64 { get { contactId } } @@ -1574,12 +1578,12 @@ public struct Contact: Identifiable, Decodable, NamedChat { ) } -public enum ContactStatus: String, Decodable { +public enum ContactStatus: String, Decodable, Hashable { case active = "active" case deleted = "deleted" } -public struct ContactRef: Decodable, Equatable { +public struct ContactRef: Decodable, Equatable, Hashable { var contactId: Int64 public var agentConnId: String var connId: Int64 @@ -1588,12 +1592,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 @@ -1607,11 +1611,12 @@ public struct Connection: Decodable { 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, authErrCounter + case connId, agentConnId, peerChatVRange, connStatus, connLevel, viaGroupLink, customUserProfileId, connectionCode, pqSupport, pqEncryption, pqSndEnabled, pqRcvEnabled, authErrCounter, quotaErrCounter } public var id: ChatId { get { ":\(connId)" } } @@ -1620,6 +1625,10 @@ public struct Connection: Decodable { authErrCounter >= 10 // authErrDisableCount in core } + public var connInactive: Bool { + quotaErrCounter >= 5 // quotaErrInactiveCount in core + } + public var connPQEnabled: Bool { pqSndEnabled == true && pqRcvEnabled == true } @@ -1633,11 +1642,12 @@ public struct Connection: Decodable { viaGroupLink: false, pqSupport: false, pqEncryption: false, - authErrCounter: 0 + 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 @@ -1651,7 +1661,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 @@ -1661,7 +1671,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? @@ -1679,7 +1689,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 @@ -1708,7 +1718,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 @@ -1798,7 +1808,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" @@ -1822,7 +1832,7 @@ public enum ConnStatus: String, Decodable { } } -public struct Group: Decodable { +public struct Group: Decodable, Hashable { public var groupInfo: GroupInfo public var members: [GroupMember] @@ -1832,7 +1842,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 @@ -1843,6 +1853,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 } } @@ -1878,12 +1889,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 @@ -1905,7 +1916,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 @@ -2037,21 +2048,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" @@ -2085,7 +2096,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" @@ -2093,7 +2104,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" @@ -2142,7 +2153,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 @@ -2175,18 +2186,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) @@ -2217,12 +2228,12 @@ 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 AChatItem: Decodable, Hashable { public var chatInfo: ChatInfo public var chatItem: ChatItem @@ -2234,19 +2245,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 @@ -2596,7 +2607,7 @@ public struct ChatItem: Identifiable, Decodable { } } -public enum CIMergeCategory { +public enum CIMergeCategory: Hashable { case memberConnected case rcvGroupEvent case sndGroupEvent @@ -2605,7 +2616,7 @@ public enum CIMergeCategory { case chatFeature } -public enum CIDirection: Decodable { +public enum CIDirection: Decodable, Hashable { case directSnd case directRcv case groupSnd @@ -2627,7 +2638,7 @@ public enum CIDirection: Decodable { } } -public struct CIMeta: Decodable { +public struct CIMeta: Decodable, Hashable { public var itemId: Int64 public var itemTs: Date var itemText: String @@ -2653,8 +2664,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 { @@ -2690,7 +2701,7 @@ public struct CIMeta: Decodable { } } -public struct CITimed: Decodable { +public struct CITimed: Decodable, Hashable { public var ttl: Int public var deleteAt: Date? } @@ -2717,7 +2728,7 @@ 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) @@ -2742,7 +2753,7 @@ public enum CIStatus: Decodable { } } - 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) @@ -2754,7 +2765,7 @@ public enum CIStatus: Decodable { case .sndErrorAuth: return ("multiply", .red) case .sndError: return ("multiply", .red) case .sndWarning: return ("exclamationmark.triangle.fill", .orange) - case .rcvNew: return ("circlebadge.fill", Color.accentColor) + case .rcvNew: return ("circlebadge.fill", primaryColor) case .rcvRead: return nil case .invalid: return ("questionmark", metaColor) } @@ -2787,7 +2798,7 @@ public enum CIStatus: Decodable { } } -public enum SndError: Decodable { +public enum SndError: Decodable, Hashable { case auth case quota case expired @@ -2809,7 +2820,7 @@ public enum SndError: Decodable { } } -public enum SrvError: Decodable, Equatable { +public enum SrvError: Decodable, Hashable { case host case version case other(srvError: String) @@ -2831,17 +2842,73 @@ public enum SrvError: Decodable, Equatable { } } -public enum MsgReceiptStatus: String, Decodable { +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?) @@ -2857,12 +2924,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?) @@ -2882,7 +2949,7 @@ public enum CIForwardedFrom: Decodable { } } -public enum CIDeleteMode: String, Decodable { +public enum CIDeleteMode: String, Decodable, Hashable { case cidmBroadcast = "broadcast" case cidmInternal = "internal" } @@ -2891,7 +2958,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 @@ -3027,7 +3094,7 @@ public enum CIContent: Decodable, ItemContent { } } -public enum MsgDecryptError: String, Decodable { +public enum MsgDecryptError: String, Decodable, Hashable { case ratchetHeader case tooManySkipped case ratchetEarlier @@ -3045,7 +3112,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 @@ -3083,13 +3150,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) @@ -3110,9 +3177,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 = "😀" @@ -3153,7 +3227,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 @@ -3221,7 +3295,7 @@ public struct CIFile: Decodable { } } -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? @@ -3268,22 +3342,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( @@ -3311,13 +3391,13 @@ 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 @@ -3355,7 +3435,7 @@ public enum CIFileStatus: Decodable, Equatable { } } -public enum FileError: Decodable, Equatable { +public enum FileError: Decodable, Equatable, Hashable { case auth case noFile case relay(srvError: SrvError) @@ -3380,7 +3460,7 @@ public enum FileError: Decodable, Equatable { } } -public enum MsgContent: Equatable { +public enum MsgContent: Equatable, Hashable { case text(String) case link(text: String, preview: LinkPreview) case image(text: String, image: String) @@ -3547,7 +3627,7 @@ extension MsgContent: Encodable { } } -public struct FormattedText: Decodable { +public struct FormattedText: Decodable, Hashable { public var text: String public var format: Format? @@ -3556,7 +3636,7 @@ public struct FormattedText: Decodable { } } -public enum Format: Decodable, Equatable { +public enum Format: Decodable, Equatable, Hashable { case bold case italic case strikeThrough @@ -3578,7 +3658,7 @@ public enum Format: Decodable, Equatable { } } -public enum SimplexLinkType: String, Decodable { +public enum SimplexLinkType: String, Decodable, Hashable { case contact case invitation case group @@ -3592,7 +3672,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" @@ -3619,7 +3699,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 @@ -3634,7 +3714,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" @@ -3643,22 +3723,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 @@ -3690,7 +3770,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 @@ -3707,7 +3787,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 @@ -3723,18 +3803,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) @@ -3763,7 +3843,7 @@ public enum RcvDirectEvent: Decodable { } } -public enum RcvGroupEvent: Decodable { +public enum RcvGroupEvent: Decodable, Hashable { case memberAdded(groupMemberId: Int64, profile: Profile) case memberConnected case memberLeft @@ -3819,7 +3899,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) @@ -3847,7 +3927,7 @@ public enum SndGroupEvent: Decodable { } } -public enum RcvConnEvent: Decodable { +public enum RcvConnEvent: Decodable, Hashable { case switchQueue(phase: SwitchPhase) case ratchetSync(syncStatus: RatchetSyncState) case verificationCodeReset @@ -3884,7 +3964,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) @@ -3921,14 +4001,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 @@ -3978,13 +4058,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]? @@ -3992,8 +4072,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/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/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/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/SimplexApp.kt index 95bba8e8a2..7d0312a43a 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 @@ -179,6 +179,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() 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 69ad8defbf..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,7 +73,8 @@ 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 @@ -83,12 +85,26 @@ object NtfManager { } } + 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) + } + } + fun displayNotification(user: UserLike, chatId: String, displayName: String, msgText: String, image: String? = null, actions: List = emptyList()) { 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/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..f49196c64a 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 @@ -29,6 +29,7 @@ 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.* @@ -107,7 +108,7 @@ 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 + editText.textSize = textStyle.value.fontSize.value * appPrefs.fontScale.get() val drawable = androidAppContext.getDrawable(R.drawable.send_msg_view_background)!! DrawableCompat.setTint(drawable, tintColor.toArgb()) editText.background = drawable @@ -135,7 +136,7 @@ actual fun PlatformTextField( editText }) { it.setTextColor(textColor.toArgb()) - it.textSize = textStyle.value.fontSize.value + it.textSize = textStyle.value.fontSize.value * appPrefs.fontScale.get() DrawableCompat.setTint(it.background, tintColor.toArgb()) it.isFocusable = composeState.value.preview !is ComposePreview.VoicePreview it.isFocusableInTouchMode = it.isFocusable 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 d6af35432d..8d4ab3206a 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 @@ -554,7 +554,7 @@ fun CallPermissionsView(pipActive: Boolean, hasVideo: Boolean, cancel: () -> Uni } } else { ColumnWithScrollBar(Modifier.fillMaxSize()) { - Spacer(Modifier.height(AppBarHeight)) + Spacer(Modifier.height(AppBarHeight * fontSizeSqrtMultiplier)) AppBarTitle(stringResource(MR.strings.permissions_required)) Spacer(Modifier.weight(1f)) 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 7cb5c77f6e..9601152773 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 @@ -137,6 +137,9 @@ fun AppearanceScope.AppearanceLayout( } } + SectionDividerSpaced(maxBottomPadding = true) + FontScaleSection() + SectionBottomSpacer() } } 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..bd35594ac0 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 @@ -18,6 +18,7 @@ import androidx.compose.ui.graphics.graphicsLayer 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 @@ -36,6 +37,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, @@ -333,38 +335,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 0ebe77e524..795baece0c 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 @@ -52,6 +52,7 @@ object ChatModel { val chatDbStatus = mutableStateOf(null) val ctrlInitInProgress = mutableStateOf(false) val dbMigrationInProgress = mutableStateOf(false) + val incompleteInitializedDbRemoved = mutableStateOf(false) val chats = mutableStateListOf() // map of connections network statuses, key is agent connection id val networkStatuses = mutableStateMapOf() @@ -1171,18 +1172,22 @@ data class Connection( val pqSndEnabled: Boolean? = null, val pqRcvEnabled: Boolean? = null, val connectionStats: ConnectionStats? = null, - val authErrCounter: Int + 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, authErrCounter = 0) + 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) } } @@ -2351,6 +2356,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() @@ -3465,7 +3512,7 @@ data class ChatItemVersion( @Serializable data class MemberDeliveryStatus( val groupMemberId: Long, - val memberDeliveryStatus: CIStatus, + val memberDeliveryStatus: GroupSndStatus, val sentViaProxy: Boolean? ) 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 c55ea0a871..7633b0c808 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 @@ -127,6 +127,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( @@ -176,6 +177,12 @@ class AppPreferences { val selfDestruct = mkBoolPreference(SHARED_PREFS_SELF_DESTRUCT, false) val selfDestructDisplayName = mkStrPreference(SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME, null) + // 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 = { @@ -191,6 +198,8 @@ class AppPreferences { }, 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) @@ -332,6 +341,7 @@ 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" @@ -361,6 +371,7 @@ 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_SELF_DESTRUCT = "LocalAuthenticationSelfDestruct" private const val SHARED_PREFS_SELF_DESTRUCT_DISPLAY_NAME = "LocalAuthenticationSelfDestructDisplayName" @@ -371,6 +382,8 @@ class AppPreferences { 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" @@ -404,6 +417,18 @@ object ChatController { fun hasChatCtrl() = ctrl != -1L && ctrl != 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) { @@ -491,11 +516,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) @@ -558,20 +587,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 } } @@ -909,6 +942,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) { @@ -2347,6 +2388,8 @@ object ChatController { notify() } else if (chatModel.upsertChatItem(rh, cInfo, cItem)) { notify() + } else if (cItem.content is CIContent.RcvCall && cItem.content.status == CICallStatus.Missed) { + notify() } } @@ -2564,6 +2607,8 @@ 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() @@ -2635,6 +2680,8 @@ sealed class CC { class ApiStandaloneFileInfo(val url: String): CC() // misc class ShowVersion(): CC() + class ResetAgentServersStats(): CC() + class GetAgentServersSummary(val userId: Long): CC() val cmdString: String get() = when (this) { is Console -> cmd @@ -2711,6 +2758,8 @@ 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" @@ -2791,6 +2840,8 @@ sealed class CC { is ApiDownloadStandaloneFile -> "/_download $userId $url ${file.filePath}" is ApiStandaloneFileInfo -> "/_download info $url" is ShowVersion -> "/version" + is ResetAgentServersStats -> "/reset servers stats" + is GetAgentServersSummary -> "/get servers summary $userId" } val cmdType: String get() = when (this) { @@ -2851,6 +2902,8 @@ 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" @@ -2920,6 +2973,8 @@ sealed class CC { is ApiDownloadStandaloneFile -> "apiDownloadStandaloneFile" is ApiStandaloneFileInfo -> "apiStandaloneFileInfo" is ShowVersion -> "showVersion" + is ResetAgentServersStats -> "resetAgentServersStats" + is GetAgentServersSummary -> "getAgentServersSummary" } class ItemRange(val from: Long, val to: Long) @@ -3018,7 +3073,7 @@ data class ServerCfg( val server: String, val preset: Boolean, val tested: Boolean? = null, - val enabled: Boolean + val enabled: ServerEnabled ) { @Transient private val createdAt: Date = Date() @@ -3032,7 +3087,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 = ServerEnabled.Enabled) class SampleData( val preset: ServerCfg, @@ -3046,26 +3101,33 @@ data class ServerCfg( server = "smp://abcd@smp8.simplex.im", preset = true, tested = true, - enabled = true + enabled = ServerEnabled.Enabled ), custom = ServerCfg( remoteHostId = null, server = "smp://abcd@smp9.simplex.im", preset = false, tested = false, - enabled = false + enabled = ServerEnabled.Disabled ), untested = ServerCfg( remoteHostId = null, server = "smp://abcd@smp10.simplex.im", preset = false, tested = null, - enabled = true + enabled = ServerEnabled.Enabled ) ) } } +@Serializable +enum class ServerEnabled { + @SerialName("disabled") Disabled, + @SerialName("enabled") Enabled, + @SerialName("known") Known; +} + @Serializable enum class ProtocolTestStep { @SerialName("connect") Connect, @@ -3183,7 +3245,7 @@ data class NetCfg( val tcpKeepAlive: KeepAliveOpts?, val smpPingInterval: Long, // microseconds val smpPingCount: Int, - val logTLSErrors: Boolean = false + val logTLSErrors: Boolean = false, ) { val useSocksProxy: Boolean get() = socksProxy != null val enableKeepAlive: Boolean get() = tcpKeepAlive != null @@ -3408,6 +3470,154 @@ 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 + ) + } +} + +@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() @@ -4419,6 +4629,7 @@ sealed class CR { @Serializable @SerialName("chatError") class ChatRespError(val user_: UserRef?, val chatError: ChatError): CR() @Serializable @SerialName("archiveImported") class ArchiveImported(val archiveErrors: List): CR() @Serializable @SerialName("appSettings") class AppSettingsR(val appSettings: AppSettings): 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() @@ -4578,6 +4789,7 @@ sealed class CR { is ContactPQAllowed -> "contactPQAllowed" is ContactPQEnabled -> "contactPQEnabled" is VersionInfo -> "versionInfo" + is AgentServersSummary -> "agentServersSummary" is CmdOk -> "cmdOk" is ChatCmdError -> "chatCmdError" is ChatRespError -> "chatError" @@ -4756,6 +4968,7 @@ sealed class CR { 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 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 })}" 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 d6b8901042..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 @@ -88,8 +88,23 @@ 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.apiSetAppFilePaths( appFilesDir.absolutePath, 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/ui/theme/Theme.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/ui/theme/Theme.kt index 45f656a011..fb922fe52b 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,6 +6,8 @@ import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.* +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.model.ChatController.appPrefs @@ -32,7 +34,6 @@ enum class DefaultTheme { val mode: DefaultThemeMode get() = if (this == LIGHT) DefaultThemeMode.LIGHT else DefaultThemeMode.DARK - // Call it only with base theme, not SYSTEM fun hasChangedAnyColor(overrides: ThemeOverrides?): Boolean { if (overrides == null) return false return overrides.colors != ThemeColors() || @@ -778,6 +779,7 @@ fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) { typography = Typography, shapes = Shapes, 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. @@ -792,6 +794,7 @@ fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) { LocalContentColor provides MaterialTheme.colors.onBackground, LocalAppColors provides rememberedAppColors, LocalAppWallpaper provides rememberedWallpaper, + LocalDensity provides density, content = content) } ) 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 c23a6ad00e..797d8e66b8 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 @@ -330,7 +330,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools } @Composable - fun MemberDeliveryStatusView(member: GroupMember, status: CIStatus, sentViaProxy: Boolean?) { + fun MemberDeliveryStatusView(member: GroupMember, status: GroupSndStatus, sentViaProxy: Boolean?) { SectionItemView( padding = PaddingValues(horizontal = 0.dp) ) { @@ -355,7 +355,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools ) } } - val statusIcon = status.statusIcon(MaterialTheme.colors.primary, 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) { @@ -367,20 +367,11 @@ 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 + ) } } } @@ -520,7 +511,7 @@ 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 -> Triple(mem, mds.memberDeliveryStatus, mds.sentViaProxy) 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 af9d77b1d0..ec34a73d31 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 @@ -821,9 +821,9 @@ fun ChatInfoToolbar( 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() } } @@ -837,9 +837,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 @@ -865,7 +865,7 @@ 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) + Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(18.dp * fontSizeSqrtMultiplier).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary) } data class CIListState(val scrolled: Boolean, val itemCount: Int, val keyboardState: KeyboardState) @@ -1283,7 +1283,7 @@ val MEMBER_IMAGE_SIZE: Dp = 38.dp @Composable fun MemberImage(member: GroupMember) { - ProfileImage(MEMBER_IMAGE_SIZE, member.memberProfile.image, backgroundColor = MaterialTheme.colors.background) + ProfileImage(MEMBER_IMAGE_SIZE * fontSizeSqrtMultiplier, member.memberProfile.image, backgroundColor = MaterialTheme.colors.background) } @Composable 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 24533247ba..a0e1bf8107 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,10 +13,12 @@ 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 @@ -884,7 +886,7 @@ fun ComposeView( && !nextSendGrpInv.value IconButton( attachmentClicked, - Modifier.padding(bottom = if (appPlatform.isAndroid) 0.dp else 7.dp), + Modifier.padding(bottom = if (appPlatform.isAndroid) 0.dp else with(LocalDensity.current) { 7.sp.toDp() }), enabled = attachmentEnabled ) { Icon( 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 779371a07c..192f1b005f 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 @@ -15,10 +15,12 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.* import chat.simplex.common.model.* +import chat.simplex.common.model.ChatController.appPrefs import chat.simplex.common.ui.theme.* import chat.simplex.common.views.chat.item.ItemAction import chat.simplex.common.views.helpers.* @@ -99,7 +101,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 with(LocalDensity.current) { 5.sp.toDp() } * fontSizeSqrtMultiplier)) { val sendButtonSize = remember { Animatable(36f) } val sendButtonAlpha = remember { Animatable(1f) } val scope = rememberCoroutineScope() 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 b60c1f2496..c91bc3bcfc 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 @@ -390,6 +390,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, @@ -412,8 +422,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, 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 2799904b71..98b9ca729f 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 @@ -358,13 +358,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() @@ -401,6 +394,13 @@ 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) 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..13e88c3b2f 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 @@ -35,7 +35,10 @@ 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 +import kotlin.time.Duration.Companion.seconds @Composable fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerformLA: (Boolean) -> Unit, stopped: Boolean) { @@ -69,7 +72,7 @@ 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 = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(scaffoldState.drawerState, userPickerState, stopped)} }, scaffoldState = scaffoldState, drawerContent = { tryOrShowError("Settings", error = { ErrorSettingsView() }) { @@ -88,7 +91,7 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf if (newChatSheetState.value.isVisible()) hideNewChatSheet(true) else showNewChatSheet() } }, - 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 +101,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(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), Modifier.size(24.dp * fontSizeSqrtMultiplier)) } } } @@ -181,8 +184,10 @@ 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) { barButtons.add { IconButton(onClick = { @@ -200,6 +205,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 +225,33 @@ 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( + serversSummary = serversSummary, + 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 +259,54 @@ private fun ChatListToolbar(searchInList: State, drawerState: Dr onSearchValueChanged = {}, buttons = barButtons ) - Divider(Modifier.padding(top = AppBarHeight)) + Divider(Modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier)) +} + +@Composable +fun SubscriptionStatusIndicator(serversSummary: MutableState, click: (() -> Unit)) { + var subs by remember { mutableStateOf(SMPServerSubs.newSMPServerSubs) } + var sess by remember { mutableStateOf(ServerSessions.newServerSessions) } + var timer: Job? by remember { mutableStateOf(null) } + + val fetchInterval: Duration = 1.seconds + + val scope = rememberCoroutineScope() + + fun setServersSummary() { + withBGApi { + serversSummary.value = chatModel.controller.getAgentServersSummary(chatModel.remoteHostId()) + + serversSummary.value?.let { + subs = it.allUsersSMP.smpTotals.subs + sess = it.allUsersSMP.smpTotals.sessions + } + } + } + + LaunchedEffect(Unit) { + setServersSummary() + timer = timer ?: scope.launch { + while (true) { + delay(fetchInterval.inWholeMilliseconds) + setServersSummary() + } + } + } + + fun stopTimer() { + timer?.cancel() + timer = null + } + + DisposableEffect(Unit) { + onDispose { + stopTimer() + } + } + + SimpleButtonFrame(click = click) { + SubscriptionStatusIndicatorView(subs = subs, sess = sess) + } } @Composable @@ -250,7 +316,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) { @@ -289,32 +355,17 @@ 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) - ) - } -} - -@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) + .size(sp16) ) } } @@ -338,7 +389,7 @@ private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState Row(verticalAlignment = Alignment.CenterVertically) { 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), null, Modifier.padding(horizontal = DEFAULT_PADDING_HALF).size(24.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 +408,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.size > 0) { + ToggleFilterEnabledButton() } + Spacer(Modifier.width(padding)) } val focusManager = LocalFocusManager.current val keyboardState = getKeyboardState() 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..0e215724f7 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 @@ -47,10 +47,11 @@ fun ChatPreviewView( @Composable fun inactiveIcon() { + val sp18 = with(LocalDensity.current) { 18.sp.toDp() } 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(sp18).background(MaterialTheme.colors.background, CircleShape), tint = MaterialTheme.colors.secondary ) } @@ -87,10 +88,11 @@ 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) + val sp19 = with(LocalDensity.current) { 19.sp.toDp() } + Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(sp19).padding(end = 3.dp, top = 1.dp), 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 +117,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 +169,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 } @@ -220,10 +223,11 @@ fun ChatPreviewView( @Composable fun progressView() { + val sp15 = with(LocalDensity.current) { 15.sp.toDp() } CircularProgressIndicator( Modifier .padding(horizontal = 2.dp) - .size(15.dp), + .size(sp15), color = MaterialTheme.colors.secondary, strokeWidth = 1.5.dp ) @@ -231,6 +235,7 @@ fun ChatPreviewView( @Composable fun chatStatusImage() { + val sp19 = with(LocalDensity.current) { 19.sp.toDp() } if (cInfo is ChatInfo.Direct) { if (cInfo.contact.active && cInfo.contact.activeConn != null) { val descr = contactNetworkStatus?.statusString @@ -244,7 +249,7 @@ fun ChatPreviewView( contentDescription = descr, tint = MaterialTheme.colors.secondary, modifier = Modifier - .size(19.dp) + .size(sp19) ) else -> @@ -266,7 +271,7 @@ fun ChatPreviewView( Row { Box(contentAlignment = Alignment.BottomEnd) { - ChatInfoImage(cInfo, size = 72.dp) + ChatInfoImage(cInfo, size = 72.dp * fontSizeSqrtMultiplier) Box(Modifier.padding(end = 6.dp, bottom = 6.dp)) { chatPreviewImageOverlayIcon() } @@ -295,9 +300,13 @@ fun ChatPreviewView( ) val n = chat.chatStats.unreadCount val showNtfsIcon = !chat.chatInfo.ntfsEnabled && (chat.chatInfo is ChatInfo.Direct || chat.chatInfo is ChatInfo.Group) + val sp17 = with(LocalDensity.current) { 17.sp.toDp() } + val sp21 = with(LocalDensity.current) { 21.sp.toDp() } + val sp23 = with(LocalDensity.current) { 23.sp.toDp() } + val sp46 = with(LocalDensity.current) { 46.sp.toDp() } if (n > 0 || chat.chatStats.unreadChat) { Box( - Modifier.padding(top = 24.dp), + Modifier.padding(top = sp23, end = with(LocalDensity.current) { 3.sp.toDp() }), contentAlignment = Alignment.Center ) { Text( @@ -313,7 +322,7 @@ fun ChatPreviewView( } } else if (showNtfsIcon) { Box( - Modifier.padding(top = 24.dp), + Modifier.padding(top = sp21), contentAlignment = Alignment.Center ) { Icon( @@ -323,12 +332,12 @@ fun ChatPreviewView( modifier = Modifier .padding(horizontal = 3.dp) .padding(vertical = 1.dp) - .size(17.dp) + .size(sp17) ) } } else if (chat.chatInfo.chatSettings?.favorite == true) { Box( - Modifier.padding(top = 24.dp), + Modifier.padding(top = sp21), contentAlignment = Alignment.Center ) { Icon( @@ -338,12 +347,12 @@ fun ChatPreviewView( modifier = Modifier .padding(horizontal = 3.dp) .padding(vertical = 1.dp) - .size(17.dp) + .size(sp17) ) } } Box( - Modifier.padding(top = 50.dp), + Modifier.padding(top = sp46), contentAlignment = Alignment.Center ) { chatStatusImage() @@ -355,12 +364,13 @@ fun ChatPreviewView( @Composable fun IncognitoIcon(incognito: Boolean) { if (incognito) { + val sp21 = with(LocalDensity.current) { 21.sp.toDp() } Icon( painterResource(MR.images.ic_theater_comedy), contentDescription = null, tint = MaterialTheme.colors.secondary, modifier = Modifier - .size(21.dp) + .size(sp21) ) } } 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..5a3f09919e --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ServersSummaryView.kt @@ -0,0 +1,979 @@ +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.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.ProtocolServersView +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.datetime.Instant +import numOrDash +import java.text.DecimalFormat +import kotlin.math.floor +import kotlin.math.roundToInt + +enum class SubscriptionColorType { + ACTIVE, ACTIVE_SOCKS_PROXY, DISCONNECTED, ACTIVE_DISCONNECTED +} + +data class SubscriptionStatus( + val color: SubscriptionColorType, + val variableValue: Float, + val opacity: Float, + val statusPercent: Float +) + +fun subscriptionStatusColorAndPercentage( + online: Boolean, + socksProxy: String?, + subs: SMPServerSubs, + sess: ServerSessions +): 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, 1f, 0f) + val activeSubsRounded = roundedToQuarter(subs.shareOfActive) + + return if (online && subs.total > 0) { + if (subs.ssActive == 0) { + if (sess.ssConnected == 0) + noConnColorAndPercent + else + SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) + } else { // ssActive > 0 + if (sess.ssConnected == 0) + // This would mean implementation error + SubscriptionStatus(SubscriptionColorType.ACTIVE_DISCONNECTED, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) + else + SubscriptionStatus(activeColor, activeSubsRounded, subs.shareOfActive, subs.shareOfActive) + } + } else noConnColorAndPercent +} + +@Composable +private fun SubscriptionStatusIndicatorPercentage(percentageText: String) { + Text( + percentageText, + color = MaterialTheme.colors.secondary, + fontSize = 12.sp, + style = MaterialTheme.typography.caption + ) +} + +@Composable +fun SubscriptionStatusIndicatorView(subs: SMPServerSubs, sess: ServerSessions, leadingPercentage: Boolean = false) { + val netCfg = rememberUpdatedState(chatModel.controller.getNetCfg()) + val statusColorAndPercentage = subscriptionStatusColorAndPercentage(chatModel.networkInfo.value.online, netCfg.value.socksProxy, subs, sess) + 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, sess = srvSumm.sessionsOrNew, 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) + } + 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) + ) + } + } +} + +@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) + } + 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) { + SectionDividerSpaced() + XFTPStatsView(stats = summary.stats, rh = rh, statsStartedAt = statsStartedAt) + } + + if (summary.sessions != null) { + SectionDividerSpaced() + 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)) + } + } + + if (summary.stats != null) { + SectionDividerSpaced() + SMPStatsView(stats = summary.stats, remoteHostInfo = rh, statsStartedAt = statsStartedAt) + } + + if (summary.subs != null) { + SectionDividerSpaced() + SMPSubscriptionsSection(subs = summary.subs, summary = summary, rh = rh) + } + + if (summary.sessions != null) { + SectionDividerSpaced() + ServerSessionsView(summary.sessions) + } + } + + SectionBottomSpacer() +} + +@Composable +fun ModalData.SMPServerSummaryView( + rh: RemoteHostInfo?, + close: () -> Unit, + summary: SMPServerSummary, + statsStartedAt: Instant +) { + ModalView( + close = close + ) { + ColumnWithScrollBar( + Modifier.fillMaxSize(), + ) { + Box(contentAlignment = Alignment.Center) { + 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) { + Column( + 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() + + LaunchedEffect(Unit) { + if (chatModel.users.count { u -> u.user.activeUser || !u.user.hidden } == 1 + ) { + selectedUserCategory.value = PresentedUserCategory.CURRENT_USER + } else { + showUserSelection = true + } + } + + Column( + Modifier.fillMaxSize(), + ) { + Box(contentAlignment = Alignment.Center) { + 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.values()[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 -> + ColumnWithScrollBar( + 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() + 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() + } + + 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() + + 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() + + SectionView { + ReconnectAllServersButton(rh) + ResetStatisticsButton(rh) + } + + 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?) { + SectionItemView(click = { resetStatisticsAlert(rh) }) { + Text( + stringResource(MR.strings.servers_info_reset_stats), + color = MaterialTheme.colors.primary + ) + } +} + +private fun resetStatisticsAlert(rh: RemoteHostInfo?) { + 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 = { + withBGApi { + val success = controller.resetAgentServersStats(rh?.remoteHostId) + + if (!success) { + AlertManager.shared.showAlertMsg( + title = generalGetString(MR.strings.servers_info_modal_error_title), + text = generalGetString(MR.strings.servers_info_reset_stats_alert_error_title) + ) + } + } + } + ) +} 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..8199a3f2d6 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 @@ -309,7 +309,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, @@ -354,7 +354,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), @@ -395,7 +395,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 +409,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 +419,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 +429,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 +439,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 +449,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 +459,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/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 ca0a6f2f93..fa43048e9e 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,6 +18,7 @@ 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.ui.theme.* @@ -450,7 +451,9 @@ private fun stopChat(m: ChatModel, progressIndicator: MutableState? = n platform.androidChatStopped() // close chat view for desktop chatModel.chatId.value = null - ModalManager.end.closeModals() + if (appPlatform.isDesktop) { + ModalManager.end.closeModals() + } onStop?.invoke() } catch (e: Error) { m.chatRunning.value = true @@ -492,6 +495,7 @@ fun deleteChatDatabaseFilesAndState() { wallpapersDir.deleteRecursively() wallpapersDir.mkdirs() DatabaseUtils.ksDatabasePassword.remove() + appPrefs.newDatabaseInitialized.set(false) controller.appPrefs.storeDBPassphrase.set(true) controller.ctrl = null @@ -500,6 +504,7 @@ fun deleteChatDatabaseFilesAndState() { chatModel.chatItems.clear() chatModel.chats.clear() chatModel.users.clear() + ntfManager.cancelAllNotifications() } private fun exportArchive( 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 b26a047b0e..d92dccddc2 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 @@ -22,15 +22,13 @@ fun CloseSheetBar(close: (() -> Unit)?, showClose: Boolean = true, tintColor: Co Column( Modifier .fillMaxWidth() - .heightIn(min = AppBarHeight) - .padding(horizontal = AppBarHorizontalPadding), + .heightIn(min = AppBarHeight * fontSizeSqrtMultiplier) + .padding(horizontal = AppBarHorizontalPadding) ) { Row( - Modifier - .padding(top = 4.dp), // Like in DefaultAppBar content = { Row( - Modifier.fillMaxWidth().height(TextFieldDefaults.MinHeight), + Modifier.fillMaxWidth().height(AppBarHeight * fontSizeSqrtMultiplier), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { 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/DefaultTopAppBar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt index 577411c7e3..3d6d242832 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 @@ -44,10 +45,10 @@ 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 ) } } @@ -84,7 +85,7 @@ private fun TopAppBar( Box( modifier .fillMaxWidth() - .height(AppBarHeight) + .height(AppBarHeight * fontSizeSqrtMultiplier) .background(backgroundColor) .padding(horizontal = 4.dp), contentAlignment = Alignment.CenterStart, 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 6ee60c4596..4acb18561a 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 @@ -8,12 +8,14 @@ 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( @@ -89,7 +91,7 @@ class ModalManager(private val placement: ModalPlacement? = null) { 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())) } } @@ -100,6 +102,9 @@ class ModalManager(private val placement: ModalPlacement? = null) { fun hasModalsOpen() = modalCount.value > 0 + val hasModalsOpen: Boolean + @Composable get () = remember { modalCount }.value > 0 + fun closeModal() { if (modalViews.isNotEmpty()) { if (modalViews.lastOrNull()?.first == false) modalViews.removeAt(modalViews.lastIndex) 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 94affad0e7..d3a3a0ff9c 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 @@ -276,8 +276,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) @@ -287,6 +287,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/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/Utils.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/Utils.kt index 884551f600..ccef86c343 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 @@ -519,6 +520,15 @@ 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 DisposableEffectOnGone(always: () -> Unit = {}, whenDispose: () -> Unit = {}, whenGone: () -> Unit) { DisposableEffect(Unit) { 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..3cc5468bbb 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 @@ -17,6 +17,7 @@ import androidx.compose.ui.text.font.FontWeight 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.ChatController.getNetCfg import chat.simplex.common.model.ChatController.startChat import chat.simplex.common.model.ChatController.startChatWithTemporaryDatabase @@ -38,6 +39,7 @@ import kotlinx.serialization.* import java.io.File import java.net.URLEncoder import kotlin.math.max +import kotlin.math.sqrt @Serializable data class MigrationFileLinkData( @@ -426,7 +428,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 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..9faee4532a 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 @@ -44,6 +44,11 @@ fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = close) } }, + scanPaste = { + closeNewChatSheet(false) + ModalManager.center.closeModals() + ModalManager.center.showModalCloseable { close -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = true, close = close) } + }, createGroup = { closeNewChatSheet(false) ModalManager.center.closeModals() @@ -55,15 +60,17 @@ fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow, stopped: Boolean, addContact: () -> Unit, + scanPaste: () -> Unit, createGroup: () -> Unit, closeNewChatSheet: (animated: Boolean) -> Unit, ) { @@ -102,7 +109,7 @@ private fun NewChatSheetLayout( verticalArrangement = Arrangement.Bottom, horizontalAlignment = Alignment.End ) { - val actions = remember { listOf(addContact, createGroup) } + val actions = remember { listOf(addContact, scanPaste, createGroup) } val backgroundColor = if (isInDarkTheme()) blendARGB(MaterialTheme.colors.primary, Color.Black, 0.7F) else @@ -118,11 +125,11 @@ private fun NewChatSheetLayout( Box(contentAlignment = Alignment.CenterEnd) { Button( actions[index], - shape = RoundedCornerShape(21.dp), + shape = RoundedCornerShape(21.dp * fontSizeSqrtMultiplier), colors = ButtonDefaults.textButtonColors(backgroundColor = backgroundColor), elevation = null, contentPadding = PaddingValues(horizontal = DEFAULT_PADDING_HALF, vertical = DEFAULT_PADDING_HALF), - modifier = Modifier.height(42.dp) + modifier = Modifier.height(42.dp * fontSizeSqrtMultiplier) ) { Text( stringResource(titles[index]), @@ -133,7 +140,7 @@ private fun NewChatSheetLayout( Icon( painterResource(icons[index]), stringResource(titles[index]), - Modifier.size(42.dp), + Modifier.size(42.dp * fontSizeSqrtMultiplier), tint = if (isInDarkTheme()) MaterialTheme.colors.primary else MaterialTheme.colors.primary ) } @@ -145,7 +152,7 @@ private fun NewChatSheetLayout( } FloatingActionButton( onClick = { if (!stopped) closeNewChatSheet(true) }, - Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING), + Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING).size(AppBarHeight * fontSizeSqrtMultiplier), elevation = FloatingActionButtonDefaults.elevation( defaultElevation = 0.dp, pressedElevation = 0.dp, @@ -157,11 +164,11 @@ private fun NewChatSheetLayout( ) { Icon( painterResource(MR.images.ic_edit_filled), stringResource(MR.strings.add_contact_or_create_group), - Modifier.graphicsLayer { alpha = 1 - animatedFloat.value } + Modifier.graphicsLayer { alpha = 1 - animatedFloat.value }.size(24.dp * fontSizeSqrtMultiplier) ) Icon( painterResource(MR.images.ic_close), stringResource(MR.strings.add_contact_or_create_group), - Modifier.graphicsLayer { alpha = animatedFloat.value } + Modifier.graphicsLayer { alpha = animatedFloat.value }.size(24.dp * fontSizeSqrtMultiplier) ) } } @@ -264,6 +271,7 @@ private fun PreviewNewChatSheet() { MutableStateFlow(AnimatedViewState.VISIBLE), stopped = false, addContact = {}, + scanPaste = {}, createGroup = {}, closeNewChatSheet = {}, ) 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..a877e123b3 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 @@ -140,7 +140,7 @@ fun ModalData.NewChatView(rh: RemoteHostInfo?, selection: NewChatOption, showQRC } } - HorizontalPager(state = pagerState, Modifier.fillMaxSize(), verticalAlignment = Alignment.Top) { index -> + HorizontalPager(state = pagerState, Modifier.fillMaxSize(), verticalAlignment = Alignment.Top, userScrollEnabled = appPlatform.isAndroid) { index -> // LALAL SCROLLBAR DOESN'T WORK ColumnWithScrollBar( Modifier 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 84fe333ba8..5fda4b7d1d 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 @@ -19,6 +19,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.* import androidx.compose.ui.graphics.* 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 @@ -82,6 +83,66 @@ object AppearanceScope { } } + @Composable + 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, + ) + ) + } + } + } + } + @Composable fun ChatThemePreview( theme: DefaultTheme, @@ -225,8 +286,8 @@ object AppearanceScope { } if (appPlatform.isDesktop) { - val itemWidth = (DEFAULT_START_MODAL_WIDTH - DEFAULT_PADDING * 2 - DEFAULT_PADDING_HALF * 3) / 4 - val itemHeight = (DEFAULT_START_MODAL_WIDTH - DEFAULT_PADDING * 2) / 4 + 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), 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..c60e8e6bf4 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 @@ -18,6 +18,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 +37,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) { 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 61c8e1b75f..e7033e88f4 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 @@ -42,6 +42,7 @@ 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 networkShowSubscriptionPercentage: MutableState = remember { mutableStateOf(chatModel.controller.appPrefs.networkShowSubscriptionPercentage.get()) } val developerTools = chatModel.controller.appPrefs.developerTools.get() val onionHosts = remember { mutableStateOf(netCfg.onionHosts) } val sessionMode = remember { mutableStateOf(netCfg.sessionMode) } @@ -53,6 +54,7 @@ fun NetworkAndServersView() { currentRemoteHost = currentRemoteHost, developerTools = developerTools, networkUseSocksProxy = networkUseSocksProxy, + networkShowSubscriptionPercentage = networkShowSubscriptionPercentage, onionHosts = onionHosts, sessionMode = sessionMode, smpProxyMode = smpProxyMode, @@ -117,6 +119,9 @@ fun NetworkAndServersView() { ) } }, + toggleNetworkShowSubscriptionPercentage = { enable -> + networkShowSubscriptionPercentage.value = enable + }, useOnion = { if (onionHosts.value == it) return@NetworkAndServersLayout val prevValue = onionHosts.value @@ -230,12 +235,14 @@ fun NetworkAndServersView() { currentRemoteHost: RemoteHostInfo?, developerTools: Boolean, networkUseSocksProxy: MutableState, + networkShowSubscriptionPercentage: MutableState, onionHosts: MutableState, sessionMode: MutableState, smpProxyMode: MutableState, smpProxyFallback: MutableState, proxyPort: State, toggleSocksProxy: (Boolean) -> Unit, + toggleNetworkShowSubscriptionPercentage: (Boolean) -> Unit, useOnion: (OnionHosts) -> Unit, updateSessionMode: (TransportSessionMode) -> Unit, updateSMPProxyMode: (SMPProxyMode) -> Unit, @@ -256,6 +263,7 @@ fun NetworkAndServersView() { SettingsActionItem(painterResource(MR.images.ic_dns), stringResource(MR.strings.xftp_servers), { ModalManager.start.showCustomModal { close -> ProtocolServersView(m, m.remoteHostId, ServerProtocol.XFTP, close) } }) if (currentRemoteHost == null) { + SettingsPreferenceItem(painterResource(MR.images.ic_radiowaves_up_forward_4_bar),stringResource(MR.strings.subscription_percentage), chatModel.controller.appPrefs.networkShowSubscriptionPercentage) UseSocksProxySwitch(networkUseSocksProxy, proxyPort, toggleSocksProxy, showModal, chatModel.controller.appPrefs.networkProxyHostPort, false) UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion) if (developerTools) { @@ -677,8 +685,10 @@ fun PreviewNetworkAndServersLayout() { currentRemoteHost = null, developerTools = true, networkUseSocksProxy = remember { mutableStateOf(true) }, + networkShowSubscriptionPercentage = remember { mutableStateOf(false) }, proxyPort = remember { mutableStateOf(9050) }, toggleSocksProxy = {}, + toggleNetworkShowSubscriptionPercentage = {}, onionHosts = remember { mutableStateOf(OnionHosts.PREFER) }, sessionMode = remember { mutableStateOf(TransportSessionMode.User) }, smpProxyMode = remember { mutableStateOf(SMPProxyMode.Never) }, 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..a7b3896ce2 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 @@ -175,8 +175,10 @@ 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)) } + val enabled = rememberUpdatedState(server.enabled == ServerEnabled.Enabled) + PreferenceToggle(stringResource(MR.strings.smp_servers_use_server_for_new_conn), enabled.value) { enable -> + onUpdate(server.copy(enabled = if (enable) ServerEnabled.Enabled else ServerEnabled.Disabled)) + } 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..bbf3e07c93 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 @@ -34,7 +34,7 @@ fun ModalData.ProtocolServersView(m: ChatModel, rhId: Long?, serverProtocol: Ser val currServers = remember(rhId) { mutableStateOf(servers) } val testing = rememberSaveable(rhId) { mutableStateOf(false) } val serversUnchanged = remember(servers) { derivedStateOf { servers == currServers.value || testing.value } } - val allServersDisabled = remember { derivedStateOf { servers.none { it.enabled } } } + val allServersDisabled = remember { derivedStateOf { servers.none { it.enabled == ServerEnabled.Enabled } } } val saveDisabled = remember(servers) { derivedStateOf { servers.isEmpty() || @@ -250,12 +250,12 @@ private fun ProtocolServerView(serverProtocol: ServerProtocol, srv: ServerCfg, s val address = parseServerAddress(srv.server) when { address == null || !address.valid || address.serverProtocol != serverProtocol || !uniqueAddress(srv, address, servers) -> InvalidServer() - !srv.enabled -> Icon(painterResource(MR.images.ic_do_not_disturb_on), null, tint = MaterialTheme.colors.secondary) + srv.enabled != ServerEnabled.Enabled -> Icon(painterResource(MR.images.ic_do_not_disturb_on), null, tint = MaterialTheme.colors.secondary) else -> ShowTestStatus(srv) } Spacer(Modifier.padding(horizontal = 4.dp)) val text = address?.hostnames?.firstOrNull() ?: srv.server - if (srv.enabled) { + if (srv.enabled == ServerEnabled.Enabled) { Text(text, color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.onBackground, maxLines = 1) } else { Text(text, maxLines = 1, color = MaterialTheme.colors.secondary) @@ -292,7 +292,7 @@ private fun addAllPresets(rhId: Long?, presetServers: List, servers: Lis 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(ServerCfg(remoteHostId = rhId, srv, preset = true, tested = null, enabled = ServerEnabled.Enabled)) } } return toAdd @@ -319,7 +319,7 @@ private suspend fun testServers(testing: MutableState, servers: List): List { val copy = ArrayList(servers) for ((index, server) in servers.withIndex()) { - if (server.enabled) { + if (server.enabled == ServerEnabled.Enabled) { copy.removeAt(index) copy.add(index, server.copy(tested = null)) } @@ -331,7 +331,7 @@ private suspend fun runServersTest(servers: List, m: ChatModel, onUpd val fs: MutableMap = mutableMapOf() val updatedServers = ArrayList(servers) for ((index, server) in servers.withIndex()) { - if (server.enabled) { + if (server.enabled == ServerEnabled.Enabled) { interruptIfCancelled() val (updatedServer, f) = testServerConnection(server, m) updatedServers.removeAt(index) 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..c1951341a4 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 @@ -7,6 +7,7 @@ import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.unit.dp import chat.simplex.common.model.ServerAddress.Companion.parseServerAddress import chat.simplex.common.model.ServerCfg +import chat.simplex.common.model.ServerEnabled import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.helpers.* import chat.simplex.common.views.newchat.QRCodeScanner @@ -25,7 +26,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, ServerEnabled.Enabled)) } 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/SettingsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/SettingsView.kt index 0b5033aca3..cef457de2e 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 @@ -174,11 +174,14 @@ fun SettingsLayout( Box( Modifier .fillMaxWidth() + .height(AppBarHeight * fontSizeSqrtMultiplier) .background(MaterialTheme.colors.background) .background(if (isInDarkTheme()) ToolbarDark else ToolbarLight) - .padding(start = 4.dp, top = 8.dp) + .padding(start = 4.dp, top = 8.dp), + contentAlignment = Alignment.CenterStart ) { - NavigationButtonBack(closeSettings) + val sp24 = with(LocalDensity.current) { 24.sp.toDp() } + NavigationButtonBack(closeSettings, height = sp24) } } } 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..862fb052e1 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 @@ -367,6 +367,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/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 996ecb11da..e2d10ae5ae 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -324,6 +324,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 @@ -617,6 +622,7 @@ New chat Add contact + Scan / Paste link One-time invitation link 1-time link SimpleX address @@ -685,6 +691,7 @@ The servers for new connections of your current chat profile Save servers? XFTP servers + Subscription percentage Install SimpleX Chat for terminal Star on GitHub Contribute @@ -1459,6 +1466,8 @@ Messages from %s will be shown! Blocked by admin blocked + disabled + inactive MEMBER Role Change role @@ -1604,6 +1613,8 @@ Wallpaper background Wallpaper accent Remove image + Font size + Zoom Good afternoon! @@ -2075,4 +2086,85 @@ WiFi Wired ethernet Other + + + Servers info + Files + No info, try to reload + Showing info for + All users + Current user + Transport sessions + Connected + Connecting + Errors + Statistics + Messages sent + Messages received + Details + Starting from %s.\nAll data is private to your device. + Messages subscriptions + Connections subscribed + 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/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_person_off.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_person_off.svg new file mode 100644 index 0000000000..a583950023 --- /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_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..db1cb48d53 --- /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..6f9a3211dd --- /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..1962851519 --- /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..ff7a146284 --- /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/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt b/apps/multiplatform/common/src/desktopMain/kotlin/chat/simplex/common/DesktopApp.kt index 4128982f78..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 @@ -14,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 @@ -26,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() @@ -195,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 3913c0dc9b..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 @@ -16,7 +16,7 @@ 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 { @@ -45,14 +45,14 @@ 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 } @@ -60,7 +60,7 @@ object NtfManager { fun cancelNotificationsForChat(chatId: ChatId) { withBGApi { prevNtfsMutex.withLock { - val ntf = prevNtfs.firstOrNull { it.first == chatId } + val ntf = prevNtfs.firstOrNull { (userChat) -> userChat.second == chatId } if (ntf != null) { prevNtfs.remove(ntf) /*try { @@ -74,6 +74,16 @@ object NtfManager { } } + 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()}") } } withBGApi { @@ -95,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, @@ -123,7 +134,7 @@ object NtfManager { try { withBGApi { prevNtfsMutex.withLock { - prevNtfs.add(chatId to builder.toast()) + prevNtfs.add(Pair(userId, chatId) to builder.toast()) } } } catch (e: Throwable) { 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 33a3ae2578..905e25566b 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 @@ -18,6 +18,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() {} 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 669dd1949d..9f7f613835 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,18 +3,29 @@ package chat.simplex.common.views.usersettings import SectionBottomSpacer import SectionDividerSpaced import SectionView +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.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.* +import chat.simplex.common.ui.theme.DEFAULT_PADDING import chat.simplex.common.views.helpers.* 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) { @@ -55,6 +66,56 @@ fun AppearanceScope.AppearanceLayout( SectionDividerSpaced(maxTopPadding = true) ProfileImageSection() + SectionDividerSpaced(maxBottomPadding = true) + FontScaleSection() + + SectionDividerSpaced(maxBottomPadding = 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/gradle.properties b/apps/multiplatform/gradle.properties index ecf038c8a5..2923c39742 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.8.1 -android.version_code=221 +android.version_name=6.0-beta.0 +android.version_code=225 -desktop.version_name=5.8.1 -desktop.version_code=54 +desktop.version_name=6.0-beta.0 +desktop.version_code=56 kotlin.version=1.9.23 gradle.plugin.version=8.2.0 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/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/cabal.project b/cabal.project index 9c06c2d169..724d598c20 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: e2d0d975e8924941e3666c34b026d61532ffd75b + tag: d39100f7ea9cb2ea79df723d0040f484f58ae058 source-repository-package type: git 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 fcbe5d0446..06850e76d2 100644 --- a/docs/DOWNLOADS.md +++ b/docs/DOWNLOADS.md @@ -1,10 +1,10 @@ --- 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.8. @@ -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/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/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-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/package.yaml b/package.yaml index 3e2392d858..e1ea646e58 100644 --- a/package.yaml +++ b/package.yaml @@ -1,5 +1,5 @@ name: simplex-chat -version: 5.8.1.0 +version: 6.0.0.0 #synopsis: #description: homepage: https://github.com/simplex-chat/simplex-chat#readme diff --git a/scripts/desktop/build-lib-linux.sh b/scripts/desktop/build-lib-linux.sh index c27b672e40..da645c6e86 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 -flink-rts - 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/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..15c6f22d28 --- /dev/null +++ b/scripts/flatpak/chat.simplex.simplex.metainfo.xml @@ -0,0 +1,93 @@ + + + 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/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/nix/sha256map.nix b/scripts/nix/sha256map.nix index f6f306e817..14bce2c849 100644 --- a/scripts/nix/sha256map.nix +++ b/scripts/nix/sha256map.nix @@ -1,5 +1,5 @@ { - "https://github.com/simplex-chat/simplexmq.git"."e2d0d975e8924941e3666c34b026d61532ffd75b" = "12lmpf8a90fjkrs4g6r4cbiszbx0fvj9bgy1l0js4jycb5nh9kbc"; + "https://github.com/simplex-chat/simplexmq.git"."d39100f7ea9cb2ea79df723d0040f484f58ae058" = "0j1kkavlgv86grqsdrq2a71nnjf9pn2farq5f8gz9f3ygh1f80cl"; "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 6b4429cb28..1c409936e0 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.8.1.0 +version: 6.0.0.0 category: Web, System, Services, Cryptography homepage: https://github.com/simplex-chat/simplex-chat#readme author: simplex.chat @@ -160,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 diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index c46eed70e1..786f065ae0 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -47,7 +47,6 @@ import qualified Data.List.NonEmpty as L import Data.Map.Strict (Map) import qualified Data.Map.Strict as M import Data.Maybe (catMaybes, fromMaybe, isJust, isNothing, listToMaybe, mapMaybe, maybeToList) -import Data.Ord (Down (..)) import qualified Data.Set as S import Data.Text (Text) import qualified Data.Text as T @@ -71,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 @@ -84,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) @@ -95,7 +94,7 @@ 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, getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, getNetworkConfig', ipAddressProtected, withLockMap) +import Simplex.Messaging.Agent.Client (SubInfo (..), agentClientStore, getAgentQueuesInfo, getAgentWorkersDetails, getAgentWorkersSummary, getNetworkConfig', ipAddressProtected, withLockMap) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig (..), InitialAgentServers (..), createAgentStore, defaultAgentConfig) import Simplex.Messaging.Agent.Lock (withLock) import Simplex.Messaging.Agent.Protocol @@ -113,7 +112,7 @@ 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, XFTPServer, userProtocol) +import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), EntityId, ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, 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 @@ -176,12 +175,11 @@ defaultChatConfig = _defaultSMPServers :: NonEmpty SMPServerWithAuth _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" + [ "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" ] _defaultNtfServers :: [NtfServer] @@ -369,7 +367,7 @@ activeAgentServers ChatConfig {defaultServers} p = fromMaybe (cfgServers p defaultServers) . nonEmpty . map (\ServerCfg {server} -> server) - . filter (\ServerCfg {enabled} -> enabled) + . filter (\ServerCfg {enabled} -> enabled == SEEnabled) cfgServers :: UserProtocol p => SProtocolType p -> (DefaultAgentServers -> NonEmpty (ProtoServerWithAuth p)) cfgServers p DefaultAgentServers {smp, xftp} = case p of @@ -1315,7 +1313,7 @@ processChatCommand' vr = \case 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} + toServerCfg server = ServerCfg {server, preset = True, tested = Nothing, enabled = SEEnabled} GetUserProtoServers aProtocol -> withUser $ \User {userId} -> processChatCommand $ APIGetUserProtoServers userId aProtocol APISetUserProtoServers userId (APSC p (ProtoServersConfig servers)) -> withUserId userId $ \user -> withServerProtocol p $ do @@ -2253,17 +2251,24 @@ processChatCommand' vr = \case CLUserContact ucId -> "UserContact " <> show ucId CLFile fId -> "File " <> show fId DebugEvent event -> toView event >> ok_ + GetAgentServersSummary userId -> withUserId userId $ \user -> do + agentServersSummary <- lift $ withAgent' getAgentServersSummary + users <- withStore' getUsers + smpServers <- getUserServers user SPSMP + xftpServers <- getUserServers user SPXFTP + let presentedServersSummary = toPresentedServersSummary agentServersSummary users user smpServers xftpServers + pure $ CRAgentServersSummary user presentedServersSummary + where + getUserServers :: forall p. (ProtocolTypeI p, UserProtocol p) => User -> SProtocolType p -> CM [ProtocolServer p] + getUserServers users protocol = do + ChatConfig {defaultServers} <- asks config + let defServers = cfgServers protocol defaultServers + servers <- map (\ServerCfg {server} -> server) <$> withStore' (`getProtocolServers` users) + let srvs = if null servers then L.toList defServers else servers + pure $ map protoServer srvs + 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_ - GetAgentMsgCounts -> lift $ do - counts <- map (first decodeLatin1) <$> withAgent' getMsgCounts - let allMsgs = foldl' (\(ts, ds) (_, (t, d)) -> (ts + t, ds + d)) (0, 0) counts - pure CRAgentMsgCounts {msgCounts = ("all", allMsgs) : sortOn (Down . snd) (filter (\(_, (_, d)) -> d /= 0) counts)} GetAgentSubs -> lift $ summary <$> withAgent' getAgentSubscriptions where summary SubscriptionsInfo {activeSubscriptions, pendingSubscriptions, removedSubscriptions} = @@ -2781,14 +2786,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, groupSndResult) <- 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 + withStore' $ \db -> do + let GroupSndResult {sentTo, pending, forwarded} = groupSndResult + 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 @@ -4569,7 +4579,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = continued <- continueSending connEntity conn sentMsgDeliveryEvent conn msgId checkSndInlineFTComplete conn msgId - updateGroupItemStatus gInfo m conn msgId (CISSndSent SSPComplete) (Just $ isJust proxy) + 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) @@ -4610,15 +4620,15 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = continued <- continueSending connEntity conn when continued $ sendPendingGroupMessages user m conn MWARN msgId err -> do - withStore' $ \db -> updateGroupItemErrorStatus db msgId (groupMemberId' m) (CISSndWarning $ agentSndError err) + 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) (CISSndError $ agentSndError 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) processConnMERR connEntity conn err MERRS msgIds err -> do - let newStatus = CISSndError $ agentSndError 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 () @@ -4629,7 +4639,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 @@ -6285,7 +6295,7 @@ 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) Nothing + updateGroupItemStatus gInfo m conn agentMsgId (GSSRcvd msgRcptStatus) Nothing updateDirectItemsStatus :: Contact -> Connection -> [AgentMsgId] -> CIStatus 'MDSnd -> CM () updateDirectItemsStatus ct conn msgIds newStatus = do @@ -6309,20 +6319,20 @@ 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 -> Maybe Bool -> CM () + 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 () @@ -6773,7 +6783,7 @@ deliverMessagesB msgReqs = do -- 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, [GroupMember]) +sendGroupMessage :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> CM (SndMessage, GroupSndResult) sendGroupMessage user gInfo members chatMsgEvent = do when shouldSendProfileUpdate $ sendProfileUpdate `catchChatError` (\e -> toView (CRChatError (Just user) e)) @@ -6795,12 +6805,18 @@ 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]) +data GroupSndResult = GroupSndResult + { sentTo :: [GroupMember], + pending :: [GroupMember], + forwarded :: [GroupMember] + } + +sendGroupMessage' :: MsgEncodingI e => User -> GroupInfo -> [GroupMember] -> ChatMsgEvent e -> CM (SndMessage, GroupSndResult) 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 + (toSend, pending, forwarded, _, dups) = foldr addMember ([], [], [], S.empty, 0 :: Int) recipientMembers -- 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" @@ -6808,8 +6824,13 @@ sendGroupMessage' user GroupInfo {groupId} members chatMsgEvent = do 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 gsr = + GroupSndResult + { sentTo = filterSent delivered toSend fst, + pending = filterSent stored pending id, + forwarded + } + pure (msg, gsr) where shuffleMembers :: [GroupMember] -> IO [GroupMember] shuffleMembers ms = do @@ -6817,12 +6838,13 @@ 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 + addMember m acc@(toSend, pending, forwarded, !mIds, !dups) = case memberSendAction chatMsgEvent members m of Just a - | mId `S.member` mIds -> (toSend, pending, mIds, dups + 1) + | mId `S.member` mIds -> (toSend, pending, forwarded, mIds, dups + 1) | otherwise -> case a of - MSASend conn -> ((m, conn) : toSend, pending, mIds', dups) - MSAPending -> (toSend, m : pending, mIds', dups) + MSASend conn -> ((m, conn) : toSend, pending, forwarded, mIds', dups) + MSAPending -> (toSend, m : pending, forwarded, mIds', dups) + MSAForwarded -> (toSend, pending, m : forwarded, mIds', dups) Nothing -> acc where mId = groupMemberId' m @@ -6830,7 +6852,7 @@ sendGroupMessage' user GroupInfo {groupId} members chatMsgEvent = do filterSent :: [Either ChatError a] -> [mem] -> (mem -> GroupMember) -> [GroupMember] filterSent rs ms mem = [mem m | (Right _, m) <- zip rs ms] -data MemberSendAction = MSASend Connection | MSAPending +data MemberSendAction = MSASend Connection | MSAPending | MSAForwarded memberSendAction :: ChatMsgEvent e -> [GroupMember] -> GroupMember -> Maybe MemberSendAction memberSendAction chatMsgEvent members m@GroupMember {invitedByGroupMemberId} = case memberConn m of @@ -6842,7 +6864,7 @@ memberSendAction chatMsgEvent members m@GroupMember {invitedByGroupMemberId} = c | otherwise -> pendingOrForwarded where pendingOrForwarded - | forwardSupported && isForwardedGroupMsg chatMsgEvent = Nothing + | forwardSupported && isForwardedGroupMsg chatMsgEvent = Just MSAForwarded | isXGrpMsgForward chatMsgEvent = Nothing | otherwise = Just MSAPending where @@ -6867,6 +6889,7 @@ sendGroupMemberMessage user m@GroupMember {groupMemberId} chatMsgEvent groupId i messageMember SndMessage {msgId, msgBody} = forM_ (memberSendAction chatMsgEvent [m] m) $ \case MSASend 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 () @@ -7611,13 +7634,12 @@ chatCommandP = ("/version" <|> "/v") $> ShowVersion, "/debug locks" $> DebugLocks, "/debug event " *> (DebugEvent <$> jsonP), - "/get stats" $> GetAgentStats, - "/reset stats" $> ResetAgentStats, + "/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 msgs" $> GetAgentMsgCounts, "/get queues" $> GetAgentQueuesInfo, "//" *> (CustomChatCommand <$> A.takeByteString) ] @@ -7755,7 +7777,7 @@ 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} + toServerCfg server = ServerCfg {server, preset = False, tested = Nothing, enabled = SEEnabled} rcCtrlAddressP = RCCtrlAddress <$> ("addr=" *> strP) <*> (" iface=" *> (jsonP <|> text1P)) text1P = safeDecodeUtf8 <$> A.takeTill (== ' ') char_ = optional . A.char 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 04f360cc3e..fdb07b0bd9 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -60,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 @@ -68,14 +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, AgentQueuesInfo (..), AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, UserNetworkInfo) +import Simplex.Messaging.Agent.Client (AgentLocks, ServerQueueInfo, AgentQueuesInfo (..), AgentWorkersDetails (..), AgentWorkersSummary (..), ProtocolTestFailure, UserNetworkInfo) import Simplex.Messaging.Agent.Env.SQLite (AgentConfig, NetworkConfig) 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.DB (SlowQueryStats (..)) import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB -import Simplex.Messaging.Client (SMPProxyMode (..), SMPProxyFallback (..)) +import Simplex.Messaging.Client (SMPProxyFallback (..), SMPProxyMode (..)) import qualified Simplex.Messaging.Crypto as C import Simplex.Messaging.Crypto.File (CryptoFile (..)) import qualified Simplex.Messaging.Crypto.File as CF @@ -84,7 +85,6 @@ 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, XFTPServer, XFTPServerWithAuth, userProtocol) -import Simplex.Messaging.Server.QueueStore.QueueInfo import Simplex.Messaging.TMap (TMap) import Simplex.Messaging.Transport (TLS, simplexMQVersion) import Simplex.Messaging.Transport.Client (SocksProxy, TransportHost) @@ -505,13 +505,12 @@ data ChatCommand | ShowVersion | DebugLocks | DebugEvent ChatResponse - | GetAgentStats - | ResetAgentStats + | GetAgentServersSummary UserId + | ResetAgentServersStats | GetAgentSubs | GetAgentSubsDetails | GetAgentWorkers | GetAgentWorkersDetails - | GetAgentMsgCounts | GetAgentQueuesInfo | -- The parser will return this command for strings that start from "//". -- This command should be processed in preCmdHook @@ -577,7 +576,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 :: QueueInfo} + | 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} @@ -756,12 +755,11 @@ data ChatResponse | CRSQLResult {rows :: [Text]} | CRSlowSQLQueries {chatQueries :: [SlowSQLQuery], agentQueries :: [SlowSQLQuery]} | CRDebugLocks {chatLockName :: Maybe String, chatEntityLocks :: Map String String, agentLocks :: AgentLocks} - | CRAgentStats {agentStats :: [[String]]} + | 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} - | CRAgentMsgCounts {msgCounts :: [(Text, (Int, Int))]} | CRAgentQueuesInfo {agentQueuesInfo :: AgentQueuesInfo} | CRContactDisabled {user :: User, contact :: Contact} | CRConnectionDisabled {connectionEntity :: ConnectionEntity} diff --git a/src/Simplex/Chat/Markdown.hs b/src/Simplex/Chat/Markdown.hs index d7c6d31fc8..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 @@ -146,7 +145,7 @@ parseMarkdown s = fromRight (unmarked s) $ A.parseOnly (markdownP <* A.endOfInpu isSimplexLink :: Format -> Bool isSimplexLink = \case - SimplexLink {} -> True; + SimplexLink {} -> True _ -> False markdownP :: Parser Markdown @@ -223,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 9e0eccbbde..e59429b7ea 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -873,7 +873,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 @@ -884,9 +884,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 @@ -903,6 +903,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 @@ -1176,7 +1217,7 @@ mkItemVersion ChatItem {content, meta} = version <$> ciMsgContent content data MemberDeliveryStatus = MemberDeliveryStatus { groupMemberId :: GroupMemberId, - memberDeliveryStatus :: CIStatus 'MDSnd, + memberDeliveryStatus :: GroupSndStatus, sentViaProxy :: Maybe Bool } deriving (Eq, Show) @@ -1234,6 +1275,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) 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/Protocol.hs b/src/Simplex/Chat/Protocol.hs index 8c5a9e1905..d611760fbf 100644 --- a/src/Simplex/Chat/Protocol.hs +++ b/src/Simplex/Chat/Protocol.hs @@ -46,14 +46,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: diff --git a/src/Simplex/Chat/Stats.hs b/src/Simplex/Chat/Stats.hs new file mode 100644 index 0000000000..f14353f0e0 --- /dev/null +++ b/src/Simplex/Chat/Stats.hs @@ -0,0 +1,294 @@ +{-# 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.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, + currentUserSMP :: SMPServersSummary, + currentUserXFTP :: XFTPServersSummary + } + 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) + +-- 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 -> [SMPServer] -> [XFTPServer] -> PresentedServersSummary +toPresentedServersSummary agentSummary users currentUser userSMPSrvs userXFTPSrvs = do + let (userSMPSrvsSumms, allSMPSrvsSumms) = accSMPSrvsSummaries + (userSMPTotals, allSMPTotals) = (accSMPTotals userSMPSrvsSumms, accSMPTotals allSMPSrvsSumms) + (userSMPCurr, userSMPPrev, userSMPProx) = smpSummsIntoCategories userSMPSrvsSumms + (allSMPCurr, allSMPPrev, allSMPProx) = smpSummsIntoCategories allSMPSrvsSumms + (userXFTPSrvsSumms, allXFTPSrvsSumms) = accXFTPSrvsSummaries + (userXFTPTotals, allXFTPTotals) = (accXFTPTotals userXFTPSrvsSumms, accXFTPTotals allXFTPSrvsSumms) + (userXFTPCurr, userXFTPPrev) = xftpSummsIntoCategories userXFTPSrvsSumms + (allXFTPCurr, allXFTPPrev) = xftpSummsIntoCategories allXFTPSrvsSumms + PresentedServersSummary + { statsStartedAt, + allUsersSMP = + SMPServersSummary + { smpTotals = allSMPTotals, + currentlyUsedSMPServers = allSMPCurr, + previouslyUsedSMPServers = allSMPPrev, + onlyProxiedSMPServers = allSMPProx + }, + allUsersXFTP = + XFTPServersSummary + { xftpTotals = allXFTPTotals, + currentlyUsedXFTPServers = allXFTPCurr, + previouslyUsedXFTPServers = allXFTPPrev + }, + currentUserSMP = + SMPServersSummary + { smpTotals = userSMPTotals, + currentlyUsedSMPServers = userSMPCurr, + previouslyUsedSMPServers = userSMPPrev, + onlyProxiedSMPServers = userSMPProx + }, + currentUserXFTP = + XFTPServersSummary + { xftpTotals = userXFTPTotals, + currentlyUsedXFTPServers = userXFTPCurr, + previouslyUsedXFTPServers = userXFTPPrev + } + } + where + AgentServersSummary {statsStartedAt, smpServersSessions, smpServersSubs, smpServersStats, xftpServersSessions, xftpServersStats, xftpRcvInProgress, xftpSndInProgress, xftpDelInProgress} = 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 + } + smpSummsIntoCategories :: Map SMPServer SMPServerSummary -> ([SMPServerSummary], [SMPServerSummary], [SMPServerSummary]) + smpSummsIntoCategories = foldr partitionSummary ([], [], []) + where + partitionSummary 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 = foldr partitionSummary ([], []) + where + partitionSummary srvSumm (curr, prev) + | isCurrentlyUsed srvSumm = (srvSumm : curr, prev) + | otherwise = (curr, srvSumm : prev) + isCurrentlyUsed XFTPServerSummary {sessions, rcvInProgress, sndInProgress, delInProgress} = + isJust sessions || rcvInProgress || sndInProgress || delInProgress + 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 :: + (a -> SMPServerSummary -> SMPServerSummary) -> + (UserId, SMPServer) -> + a -> + (Map SMPServer SMPServerSummary, Map SMPServer SMPServerSummary) -> + (Map SMPServer SMPServerSummary, Map SMPServer SMPServerSummary) + addServerData addData (userId, srv) d (userSumms, allUsersSumms) = (userSumms', allUsersSumms') + where + userSumms' + | userId == aUserId currentUser = alterSumms newUserSummary userSumms + | otherwise = userSumms + allUsersSumms' + | countUserInAll userId = alterSumms newSummary allUsersSumms + | otherwise = allUsersSumms + alterSumms n = M.alter (Just . addData d . fromMaybe n) srv + newUserSummary = (newSummary :: SMPServerSummary) {known = Just $ srv `elem` userSMPSrvs} + newSummary = + 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 :: + (a -> XFTPServerSummary -> XFTPServerSummary) -> + (UserId, XFTPServer) -> + a -> + (Map XFTPServer XFTPServerSummary, Map XFTPServer XFTPServerSummary) -> + (Map XFTPServer XFTPServerSummary, Map XFTPServer XFTPServerSummary) + addServerData addData (userId, srv) d (userSumms, allUsersSumms) = (userSumms', allUsersSumms') + where + userSumms' + | userId == aUserId currentUser = alterSumms newUserSummary userSumms + | otherwise = userSumms + allUsersSumms' + | countUserInAll userId = alterSumms newSummary allUsersSumms + | otherwise = allUsersSumms + alterSumms n = M.alter (Just . addData d . fromMaybe n) srv + newUserSummary = (newSummary :: XFTPServerSummary) {known = Just $ srv `elem` userXFTPSrvs} + newSummary = + XFTPServerSummary + { xftpServer = srv, + known = Nothing, + sessions = Nothing, + stats = Nothing, + rcvInProgress = srv `elem` xftpRcvInProgress, + sndInProgress = srv `elem` xftpSndInProgress, + delInProgress = srv `elem` xftpDelInProgress + } + 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} + 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 ''PresentedServersSummary) diff --git a/src/Simplex/Chat/Store/Messages.hs b/src/Simplex/Chat/Store/Messages.hs index 853d34995a..5d9f5a7619 100644 --- a/src/Simplex/Chat/Store/Messages.hs +++ b/src/Simplex/Chat/Store/Messages.hs @@ -2531,14 +2531,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 @@ -2551,7 +2551,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 @@ -2589,7 +2589,7 @@ getGroupSndStatuses db itemId = memStatus (groupMemberId, memberDeliveryStatus, sentViaProxy) = MemberDeliveryStatus {groupMemberId, memberDeliveryStatus, sentViaProxy} -getGroupSndStatusCounts :: DB.Connection -> ChatItemId -> IO [(CIStatus 'MDSnd, Int)] +getGroupSndStatusCounts :: DB.Connection -> ChatItemId -> IO [(GroupSndStatus, Int)] getGroupSndStatusCounts db itemId = DB.query db diff --git a/src/Simplex/Chat/Store/Profiles.hs b/src/Simplex/Chat/Store/Profiles.hs index be06ea8878..a740389c6a 100644 --- a/src/Simplex/Chat/Store/Profiles.hs +++ b/src/Simplex/Chat/Store/Profiles.hs @@ -523,9 +523,10 @@ getProtocolServers db User {userId} = (userId, decodeLatin1 $ strEncode protocol) where protocol = protocolTypeI @p - toServerCfg :: (NonEmpty TransportHost, String, C.KeyHash, Maybe Text, Bool, Maybe Bool, Bool) -> ServerCfg p - toServerCfg (host, port, keyHash, auth_, preset, tested, enabled) = + toServerCfg :: (NonEmpty TransportHost, String, C.KeyHash, Maybe Text, Bool, Maybe Bool, Int) -> ServerCfg p + toServerCfg (host, port, keyHash, auth_, preset, tested, enabledInt) = let server = ProtoServerWithAuth (ProtocolServer protocol host port keyHash) (BasicAuth . encodeUtf8 <$> auth_) + enabled = toServerEnabled enabledInt in ServerCfg {server, preset, tested, enabled} overwriteProtocolServers :: forall p. ProtocolTypeI p => DB.Connection -> User -> [ServerCfg p] -> ExceptT StoreError IO () @@ -542,7 +543,7 @@ overwriteProtocolServers db User {userId} servers = (protocol, host, port, key_hash, basic_auth, preset, tested, enabled, user_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?) |] - ((protocol, host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_) :. (preset, tested, enabled, userId, currentTs, currentTs)) + ((protocol, host, port, keyHash, safeDecodeUtf8 . unBasicAuth <$> auth_) :. (preset, tested, fromServerEnabled enabled, userId, currentTs, currentTs)) pure $ Right () where protocol = decodeLatin1 $ strEncode $ protocolTypeI @p diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index 106c4b3373..07c9afa2e2 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -1632,10 +1632,42 @@ data ServerCfg p = ServerCfg { server :: ProtoServerWithAuth p, preset :: Bool, tested :: Maybe Bool, - enabled :: Bool + enabled :: ServerEnabled } deriving (Show) +data ServerEnabled + = SEDisabled + | SEEnabled + | -- server is marked as known, but it's not in the list of configured servers; + -- e.g., it may be added via an unknown server dialogue and user didn't manually configure it, + -- meaning server wasn't tested (or at least such option wasn't presented in UI) + -- and it may be inoperable for user due to server password + SEKnown + deriving (Eq, Show) + +pattern DBSEDisabled :: Int +pattern DBSEDisabled = 0 + +pattern DBSEEnabled :: Int +pattern DBSEEnabled = 1 + +pattern DBSEKnown :: Int +pattern DBSEKnown = 2 + +toServerEnabled :: Int -> ServerEnabled +toServerEnabled = \case + DBSEDisabled -> SEDisabled + DBSEEnabled -> SEEnabled + DBSEKnown -> SEKnown + _ -> SEDisabled + +fromServerEnabled :: ServerEnabled -> Int +fromServerEnabled = \case + SEDisabled -> DBSEDisabled + SEEnabled -> DBSEEnabled + SEKnown -> DBSEKnown + data ChatVersion instance VersionScope ChatVersion @@ -1764,6 +1796,8 @@ $(JQ.deriveJSON defaultJSON ''ContactRef) $(JQ.deriveJSON defaultJSON ''NoteFolder) +$(JQ.deriveJSON (enumJSON $ dropPrefix "SE") ''ServerEnabled) + instance ProtocolTypeI p => ToJSON (ServerCfg p) where toEncoding = $(JQ.mkToEncoding defaultJSON ''ServerCfg) toJSON = $(JQ.mkToJSON 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 ef445c5a7c..cc5290aa69 100644 --- a/src/Simplex/Chat/Types/UITheme.hs +++ b/src/Simplex/Chat/Types/UITheme.hs @@ -18,6 +18,7 @@ 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 (decodeJSON, encodeJSON) data UITheme = UITheme { themeId :: Text, diff --git a/src/Simplex/Chat/Types/Util.hs b/src/Simplex/Chat/Types/Util.hs index e19d48caba..47edf8eaf8 100644 --- a/src/Simplex/Chat/Types/Util.hs +++ b/src/Simplex/Chat/Types/Util.hs @@ -2,26 +2,15 @@ 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.Encoding.String -import Simplex.Messaging.Util (safeDecodeUtf8) - -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 diff --git a/src/Simplex/Chat/View.hs b/src/Simplex/Chat/View.hs index d1f3625e18..04c06103ea 100644 --- a/src/Simplex/Chat/View.hs +++ b/src/Simplex/Chat/View.hs @@ -54,7 +54,6 @@ 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.Protocol -import Simplex.Messaging.Agent.Protocol (AgentErrorType (RCP)) import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..)) import Simplex.Messaging.Client (SMPProxyFallback, SMPProxyMode (..)) import qualified Simplex.Messaging.Crypto as C @@ -366,7 +365,7 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe "chat entity locks: " <> viewJSON chatEntityLocks, "agent locks: " <> viewJSON agentLocks ] - CRAgentStats stats -> map (plain . intercalate ",") stats + 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) @@ -384,7 +383,6 @@ responseToView hu@(currentRH, user_) ChatConfig {logLevel, showReactions, showRe [ "agent workers details:", viewJSON agentWorkersDetails -- this would be huge, but copypastable when has its own line ] - CRAgentMsgCounts {msgCounts} -> ["received messages (total, duplicates):", viewJSON msgCounts] CRAgentQueuesInfo {agentQueuesInfo} -> [ "agent queues info:", plain . LB.unpack $ J.encode agentQueuesInfo diff --git a/tests/ChatClient.hs b/tests/ChatClient.hs index efca493002..ca5b92e04e 100644 --- a/tests/ChatClient.hs +++ b/tests/ChatClient.hs @@ -36,7 +36,7 @@ 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 @@ -48,10 +48,11 @@ 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) @@ -135,6 +136,14 @@ testAgentCfg = { 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 testCfg = defaultChatConfig @@ -144,6 +153,9 @@ testCfg = tbqSize = 16 } +testCfgSlow :: ChatConfig +testCfgSlow = testCfg {agentConfig = testAgentCfgSlow} + testAgentCfgVPrev :: AgentConfig testAgentCfgVPrev = testAgentCfg @@ -427,11 +439,11 @@ smpServerCfg = serverStatsLogFile = "tests/smp-server-stats.daily.log", serverStatsBackupFile = Nothing, smpServerVRange = supportedServerSMPRelayVRange, - transportConfig = defaultTransportServerConfig, + transportConfig = defaultTransportServerConfig {alpn = Just supportedSMPHandshakes}, smpHandshakeTimeout = 1000000, controlPort = Nothing, smpAgentCfg = defaultSMPClientAgentConfig, - allowSMPProxy = False, + allowSMPProxy = True, serverClientConcurrency = 16, information = Nothing } @@ -473,7 +485,7 @@ xftpServerConfig = 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 8345a90b0c..aaf91af910 100644 --- a/tests/ChatTests/Direct.hs +++ b/tests/ChatTests/Direct.hs @@ -63,11 +63,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 @@ -842,41 +853,38 @@ testTestXFTPServer = 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" @@ -885,143 +893,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}} @@ -2463,7 +2361,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!" diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 009a26cf0b..6f1ba20246 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -90,6 +90,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 @@ -2527,7 +2528,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" @@ -2630,7 +2631,7 @@ testPlanGroupLinkConnecting tmp = do bob ##> ("/c " <> gLink) bob <## "group link: connecting" where - cfg = testCfgGroupLinkViaContact + cfg = mkCfgGroupLinkViaContact testCfgSlow testPlanGroupLinkLeaveRejoin :: HasCallStack => FilePath -> IO () testPlanGroupLinkLeaveRejoin = @@ -3228,6 +3229,52 @@ 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 + 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" @@ -3253,7 +3300,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!" diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index c1388072ab..a3352c7f4f 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 @@ -705,6 +707,49 @@ testPlanAddressConnecting tmp = do alice ##> "/ac bob" alice <## "bob (Bob): accepting contact request..." 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) @@ -1050,9 +1095,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") diff --git a/tests/MarkdownTests.hs b/tests/MarkdownTests.hs index a279201bea..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" diff --git a/tests/ProtocolTests.hs b/tests/ProtocolTests.hs index c4dfa7da25..0bc801e824 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 @@ -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==\"}}" @@ -254,16 +255,16 @@ decodeChatMessageTest = describe "Chat message encoding/decoding" $ do "{\"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-8\",\"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/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/_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