mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2024-12-17 17:20:21 +01:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e6831d46f | |||
| 2f730d54e9 | |||
| fe0013c4a9 | |||
| 5261886b31 | |||
| 93ab3076d4 | |||
| 3b88ddbd4f | |||
| d6dc35738e | |||
| 5b3aba9db2 | |||
| 981cbb8bf9 | |||
| fc3ae49214 | |||
| 25fb099c44 | |||
| 40e93cc61e | |||
| c22d23750f | |||
| 2539255957 | |||
| 3ea8279451 | |||
| 0cb568d206 | |||
| 351cfcbcbc | |||
| 691cd489ea | |||
| bccbb9900f | |||
| e002f33c53 | |||
| 6407d5de63 | |||
| 9cba96082d | |||
| 2089fd8539 | |||
| 31c4ff2705 | |||
| fe20a43232 | |||
| 087519c39d |
@@ -5,12 +5,22 @@ on:
|
||||
branches:
|
||||
- master
|
||||
- stable
|
||||
- users
|
||||
tags:
|
||||
- "v*"
|
||||
- "!*-fdroid"
|
||||
- "!*-armv7a"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- "apps/ios"
|
||||
- "apps/multiplatform"
|
||||
- "blog"
|
||||
- "docs"
|
||||
- "fastlane"
|
||||
- "images"
|
||||
- "packages"
|
||||
- "website"
|
||||
- "README.md"
|
||||
- "PRIVACY.md"
|
||||
|
||||
jobs:
|
||||
prepare-release:
|
||||
|
||||
@@ -15,16 +15,11 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
|
||||
logger.debug("AppDelegate: didFinishLaunchingWithOptions")
|
||||
application.registerForRemoteNotifications()
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(pasteboardChanged), name: UIPasteboard.changedNotification, object: nil)
|
||||
removePasscodesIfReinstalled()
|
||||
prepareForLaunch()
|
||||
return true
|
||||
}
|
||||
|
||||
@objc func pasteboardChanged() {
|
||||
ChatModel.shared.pasteboardHasStrings = UIPasteboard.general.hasStrings
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
||||
let token = deviceToken.map { String(format: "%02hhx", $0) }.joined()
|
||||
logger.debug("AppDelegate: didRegisterForRemoteNotificationsWithDeviceToken \(token)")
|
||||
|
||||
@@ -183,7 +183,6 @@ final class ChatModel: ObservableObject {
|
||||
@Published var stopPreviousRecPlay: URL? = nil // coordinates currently playing source
|
||||
@Published var draft: ComposeState?
|
||||
@Published var draftChatId: String?
|
||||
@Published var pasteboardHasStrings: Bool = UIPasteboard.general.hasStrings
|
||||
@Published var networkInfo = UserNetworkInfo(networkType: .other, online: true)
|
||||
|
||||
var messageDelivery: Dictionary<Int64, () -> Void> = [:]
|
||||
|
||||
@@ -407,12 +407,18 @@ struct ServersSummaryView: View {
|
||||
|
||||
struct SubscriptionStatusIndicatorView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var subs: SMPServerSubs
|
||||
var hasSess: Bool
|
||||
|
||||
var body: some View {
|
||||
let onionHosts = networkUseOnionHostsGroupDefault.get()
|
||||
let (color, variableValue, opacity, _) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, hasSess)
|
||||
let (color, variableValue, opacity, _) = subscriptionStatusColorAndPercentage(
|
||||
online: m.networkInfo.online,
|
||||
usesProxy: networkUseOnionHostsGroupDefault.get() != .no || groupDefaults.string(forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY) != nil,
|
||||
subs: subs,
|
||||
hasSess: hasSess,
|
||||
primaryColor: theme.colors.primary
|
||||
)
|
||||
if #available(iOS 16.0, *) {
|
||||
Image(systemName: "dot.radiowaves.up.forward", variableValue: variableValue)
|
||||
.foregroundColor(color)
|
||||
@@ -425,26 +431,32 @@ struct SubscriptionStatusIndicatorView: View {
|
||||
|
||||
struct SubscriptionStatusPercentageView: View {
|
||||
@EnvironmentObject var m: ChatModel
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var subs: SMPServerSubs
|
||||
var hasSess: Bool
|
||||
|
||||
var body: some View {
|
||||
let onionHosts = networkUseOnionHostsGroupDefault.get()
|
||||
let (_, _, _, statusPercent) = subscriptionStatusColorAndPercentage(m.networkInfo.online, onionHosts, subs, hasSess)
|
||||
let (_, _, _, statusPercent) = subscriptionStatusColorAndPercentage(
|
||||
online: m.networkInfo.online,
|
||||
usesProxy: networkUseOnionHostsGroupDefault.get() != .no || groupDefaults.string(forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY) != nil,
|
||||
subs: subs,
|
||||
hasSess: hasSess,
|
||||
primaryColor: theme.colors.primary
|
||||
)
|
||||
Text(verbatim: "\(Int(floor(statusPercent * 100)))%")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
func subscriptionStatusColorAndPercentage(_ online: Bool, _ onionHosts: OnionHosts, _ subs: SMPServerSubs, _ hasSess: Bool) -> (Color, Double, Double, Double) {
|
||||
func subscriptionStatusColorAndPercentage(online: Bool, usesProxy: Bool, subs: SMPServerSubs, hasSess: Bool, primaryColor: Color) -> (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 activeColor: Color = usesProxy ? .indigo : primaryColor
|
||||
let noConnColorAndPercent: (Color, Double, Double, Double) = (Color(uiColor: .tertiaryLabel), 1, 1, 0)
|
||||
let activeSubsRounded = roundedToQuarter(subs.shareOfActive)
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ private enum MigrationFromState: Equatable {
|
||||
}
|
||||
|
||||
private enum MigrateFromDeviceViewAlert: Identifiable {
|
||||
case finishMigration(_ fileId: Int64, _ ctrl: chat_ctrl)
|
||||
case deleteChat(_ title: LocalizedStringKey = "Delete chat profile?", _ text: LocalizedStringKey = "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.")
|
||||
case startChat(_ title: LocalizedStringKey = "Start chat?", _ text: LocalizedStringKey = "Warning: starting chat on multiple devices is not supported and will cause message delivery failures")
|
||||
|
||||
@@ -38,6 +39,7 @@ private enum MigrateFromDeviceViewAlert: Identifiable {
|
||||
|
||||
var id: String {
|
||||
switch self {
|
||||
case .finishMigration: return "finishMigration"
|
||||
case let .deleteChat(title, text): return "\(title) \(text)"
|
||||
case let .startChat(title, text): return "\(title) \(text)"
|
||||
|
||||
@@ -138,6 +140,15 @@ struct MigrateFromDevice: View {
|
||||
}
|
||||
.alert(item: $alert) { alert in
|
||||
switch alert {
|
||||
case let .finishMigration(fileId, ctrl):
|
||||
return Alert(
|
||||
title: Text("Remove archive?"),
|
||||
message: Text("The uploaded database archive will be permanently removed from the servers."),
|
||||
primaryButton: .destructive(Text("Continue")) {
|
||||
finishMigration(fileId, ctrl)
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
case let .startChat(title, text):
|
||||
return Alert(
|
||||
title: Text(title),
|
||||
@@ -318,7 +329,7 @@ struct MigrateFromDevice: View {
|
||||
Text("Cancel migration").foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
Button(action: { finishMigration(fileId, ctrl) }) {
|
||||
Button(action: { alert = .finishMigration(fileId, ctrl) }) {
|
||||
settingsRow("checkmark", color: theme.colors.secondary) {
|
||||
Text("Finalize migration").foregroundColor(theme.colors.primary)
|
||||
}
|
||||
@@ -523,9 +534,15 @@ struct MigrateFromDevice: View {
|
||||
}
|
||||
case let .sndStandaloneFileComplete(_, fileTransferMeta, rcvURIs):
|
||||
let cfg = getNetCfg()
|
||||
let proxy: NetworkProxy? = if cfg.socksProxy == nil {
|
||||
nil
|
||||
} else {
|
||||
networkProxyDefault.get()
|
||||
}
|
||||
let data = MigrationFileLinkData.init(
|
||||
networkConfig: MigrationFileLinkData.NetworkConfig(
|
||||
socksProxy: cfg.socksProxy,
|
||||
networkProxy: proxy,
|
||||
hostMode: cfg.hostMode,
|
||||
requiredHostMode: cfg.requiredHostMode
|
||||
)
|
||||
|
||||
@@ -93,7 +93,6 @@ 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?
|
||||
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
|
||||
@State private var alert: MigrateToDeviceViewAlert?
|
||||
@@ -102,6 +101,7 @@ struct MigrateToDevice: View {
|
||||
// Prevent from hiding the view until migration is finished or app deleted
|
||||
@State private var backDisabled: Bool = false
|
||||
@State private var showQRCodeScanner: Bool = true
|
||||
@State private var pasteboardHasStrings = UIPasteboard.general.hasStrings
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
@@ -197,10 +197,8 @@ struct MigrateToDevice: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
if developerTools {
|
||||
Section(header: Text("Or paste archive link").foregroundColor(theme.colors.secondary)) {
|
||||
pasteLinkView()
|
||||
}
|
||||
Section(header: Text("Or paste archive link").foregroundColor(theme.colors.secondary)) {
|
||||
pasteLinkView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,7 +216,7 @@ struct MigrateToDevice: View {
|
||||
} label: {
|
||||
Text("Tap to paste link")
|
||||
}
|
||||
.disabled(!ChatModel.shared.pasteboardHasStrings)
|
||||
.disabled(!pasteboardHasStrings)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
|
||||
@@ -487,6 +485,9 @@ struct MigrateToDevice: View {
|
||||
do {
|
||||
if !hasChatCtrl() {
|
||||
chatInitControllerRemovingDatabases()
|
||||
} else if ChatModel.shared.chatRunning == true {
|
||||
// cannot delete storage if chat is running
|
||||
try await apiStopChat()
|
||||
}
|
||||
try await apiDeleteStorage()
|
||||
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
|
||||
@@ -556,11 +557,22 @@ struct MigrateToDevice: View {
|
||||
do {
|
||||
try? FileManager.default.removeItem(at: getMigrationTempFilesDirectory())
|
||||
MigrationToDeviceState.save(nil)
|
||||
appSettings.importIntoApp()
|
||||
try SimpleX.startChat(refreshInvitations: true)
|
||||
AlertManager.shared.showAlertMsg(title: "Chat migrated!", message: "Finalize migration on another device.")
|
||||
try ObjC.catchException {
|
||||
appSettings.importIntoApp()
|
||||
}
|
||||
do {
|
||||
try SimpleX.startChat(refreshInvitations: true)
|
||||
AlertManager.shared.showAlertMsg(title: "Chat migrated!", message: "Finalize migration on another device.")
|
||||
} catch let error {
|
||||
AlertManager.shared.showAlert(Alert(title: Text("Error starting chat"), message: Text(responseError(error))))
|
||||
}
|
||||
} catch let error {
|
||||
AlertManager.shared.showAlert(Alert(title: Text("Error starting chat"), message: Text(responseError(error))))
|
||||
logger.error("Error importing settings: \(error.localizedDescription)")
|
||||
AlertManager.shared.showAlert(
|
||||
Alert(
|
||||
title: Text("Error migrating settings"),
|
||||
message: Text ("Not all settings were migrated. Repeat migration if you need them.") + Text("\n\n") + Text(responseError(error)))
|
||||
)
|
||||
}
|
||||
hideView()
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ func chatContactType(chat: Chat) -> ContactType {
|
||||
case .contactRequest:
|
||||
return .request
|
||||
case let .direct(contact):
|
||||
if contact.activeConn == nil && contact.profile.contactLink != nil {
|
||||
if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active {
|
||||
return .card
|
||||
} else if contact.chatDeleted {
|
||||
return .chatDeleted
|
||||
|
||||
@@ -296,6 +296,7 @@ private struct ConnectView: View {
|
||||
@Binding var pastedLink: String
|
||||
@Binding var alert: NewChatViewAlert?
|
||||
@State private var sheet: PlanAndConnectActionSheet?
|
||||
@State private var pasteboardHasStrings = UIPasteboard.general.hasStrings
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
@@ -332,7 +333,7 @@ private struct ConnectView: View {
|
||||
} label: {
|
||||
Text("Tap to paste link")
|
||||
}
|
||||
.disabled(!ChatModel.shared.pasteboardHasStrings)
|
||||
.disabled(!pasteboardHasStrings)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
} else {
|
||||
linkTextView(pastedLink)
|
||||
|
||||
@@ -36,6 +36,10 @@ struct AdvancedNetworkSettings: View {
|
||||
@State private var showSettingsAlert: NetworkSettingsAlert?
|
||||
@State private var onionHosts: OnionHosts = .no
|
||||
@State private var showSaveDialog = false
|
||||
@State private var netProxy = networkProxyDefault.get()
|
||||
@State private var currentNetProxy = networkProxyDefault.get()
|
||||
@State private var useNetProxy = false
|
||||
@State private var netProxyAuth = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
@@ -102,6 +106,76 @@ struct AdvancedNetworkSettings: View {
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
|
||||
Section {
|
||||
Toggle("Use SOCKS proxy", isOn: $useNetProxy)
|
||||
Group {
|
||||
TextField("IP address", text: $netProxy.host)
|
||||
TextField(
|
||||
"Port",
|
||||
text: Binding(
|
||||
get: { netProxy.port > 0 ? "\(netProxy.port)" : "" },
|
||||
set: { s in
|
||||
netProxy.port = if let port = Int(s), port > 0 {
|
||||
port
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
Toggle("Proxy requires password", isOn: $netProxyAuth)
|
||||
if netProxyAuth {
|
||||
TextField("Username", text: $netProxy.username)
|
||||
PassphraseField(
|
||||
key: $netProxy.password,
|
||||
placeholder: "Password",
|
||||
valid: NetworkProxy.validCredential(netProxy.password)
|
||||
)
|
||||
}
|
||||
}
|
||||
.if(!useNetProxy) { $0.foregroundColor(theme.colors.secondary) }
|
||||
.disabled(!useNetProxy)
|
||||
} header: {
|
||||
HStack {
|
||||
Text("SOCKS proxy").foregroundColor(theme.colors.secondary)
|
||||
if useNetProxy && !netProxy.valid {
|
||||
Spacer()
|
||||
Image(systemName: "exclamationmark.circle.fill").foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
} footer: {
|
||||
if netProxyAuth {
|
||||
Text("Your credentials may be sent unencrypted.")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} else {
|
||||
Text("Do not use credentials with proxy.")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
.onChange(of: useNetProxy) { useNetProxy in
|
||||
netCfg.socksProxy = useNetProxy && currentNetProxy.valid
|
||||
? currentNetProxy.toProxyString()
|
||||
: nil
|
||||
netProxy = currentNetProxy
|
||||
netProxyAuth = netProxy.username != "" || netProxy.password != ""
|
||||
}
|
||||
.onChange(of: netProxyAuth) { netProxyAuth in
|
||||
if netProxyAuth {
|
||||
netProxy.auth = currentNetProxy.auth
|
||||
netProxy.username = currentNetProxy.username
|
||||
netProxy.password = currentNetProxy.password
|
||||
} else {
|
||||
netProxy.auth = .username
|
||||
netProxy.username = ""
|
||||
netProxy.password = ""
|
||||
}
|
||||
}
|
||||
.onChange(of: netProxy) { netProxy in
|
||||
netCfg.socksProxy = useNetProxy && netProxy.valid
|
||||
? netProxy.toProxyString()
|
||||
: nil
|
||||
}
|
||||
|
||||
Section {
|
||||
Picker("Use .onion hosts", selection: $onionHosts) {
|
||||
ForEach(OnionHosts.values, id: \.self) { Text($0.text) }
|
||||
@@ -156,19 +230,19 @@ struct AdvancedNetworkSettings: View {
|
||||
|
||||
Section {
|
||||
Button("Reset to defaults") {
|
||||
updateNetCfgView(NetCfg.defaults)
|
||||
updateNetCfgView(NetCfg.defaults, NetworkProxy.def)
|
||||
}
|
||||
.disabled(netCfg == NetCfg.defaults)
|
||||
|
||||
Button("Set timeouts for proxy/VPN") {
|
||||
updateNetCfgView(netCfg.withProxyTimeouts)
|
||||
updateNetCfgView(netCfg.withProxyTimeouts, netProxy)
|
||||
}
|
||||
.disabled(netCfg.hasProxyTimeouts)
|
||||
|
||||
Button("Save and reconnect") {
|
||||
showSettingsAlert = .update
|
||||
}
|
||||
.disabled(netCfg == currentNetCfg)
|
||||
.disabled(netCfg == currentNetCfg || (useNetProxy && !netProxy.valid))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,7 +256,8 @@ struct AdvancedNetworkSettings: View {
|
||||
if cfgLoaded { return }
|
||||
cfgLoaded = true
|
||||
currentNetCfg = getNetCfg()
|
||||
updateNetCfgView(currentNetCfg)
|
||||
currentNetProxy = networkProxyDefault.get()
|
||||
updateNetCfgView(currentNetCfg, currentNetProxy)
|
||||
}
|
||||
.alert(item: $showSettingsAlert) { a in
|
||||
switch a {
|
||||
@@ -206,7 +281,7 @@ struct AdvancedNetworkSettings: View {
|
||||
if netCfg == currentNetCfg {
|
||||
dismiss()
|
||||
cfgLoaded = false
|
||||
} else {
|
||||
} else if !useNetProxy || netProxy.valid {
|
||||
showSaveDialog = true
|
||||
}
|
||||
})
|
||||
@@ -221,18 +296,26 @@ struct AdvancedNetworkSettings: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func updateNetCfgView(_ cfg: NetCfg) {
|
||||
private func updateNetCfgView(_ cfg: NetCfg, _ proxy: NetworkProxy) {
|
||||
netCfg = cfg
|
||||
netProxy = proxy
|
||||
onionHosts = OnionHosts(netCfg: netCfg)
|
||||
enableKeepAlive = netCfg.enableKeepAlive
|
||||
keepAliveOpts = netCfg.tcpKeepAlive ?? KeepAliveOpts.defaults
|
||||
useNetProxy = netCfg.socksProxy != nil
|
||||
netProxyAuth = switch netProxy.auth {
|
||||
case .username: netProxy.username != "" || netProxy.password != ""
|
||||
case .isolate: false
|
||||
}
|
||||
}
|
||||
|
||||
private func saveNetCfg() -> Bool {
|
||||
do {
|
||||
try setNetworkConfig(netCfg)
|
||||
currentNetCfg = netCfg
|
||||
setNetCfg(netCfg)
|
||||
setNetCfg(netCfg, networkProxy: useNetProxy ? netProxy : nil)
|
||||
currentNetProxy = netProxy
|
||||
networkProxyDefault.set(netProxy)
|
||||
return true
|
||||
} catch let error {
|
||||
let err = responseError(error)
|
||||
|
||||
@@ -19,9 +19,15 @@ extension AppSettings {
|
||||
val.hostMode = .publicHost
|
||||
val.requiredHostMode = true
|
||||
}
|
||||
val.socksProxy = nil
|
||||
setNetCfg(val)
|
||||
if val.socksProxy != nil {
|
||||
val.socksProxy = networkProxy?.toProxyString()
|
||||
setNetCfg(val, networkProxy: networkProxy)
|
||||
} else {
|
||||
val.socksProxy = nil
|
||||
setNetCfg(val, networkProxy: nil)
|
||||
}
|
||||
}
|
||||
if let val = networkProxy { networkProxyDefault.set(val) }
|
||||
if let val = privacyEncryptLocalFiles { privacyEncryptLocalFilesGroupDefault.set(val) }
|
||||
if let val = privacyAskToApproveRelays { privacyAskToApproveRelaysGroupDefault.set(val) }
|
||||
if let val = privacyAcceptImages {
|
||||
@@ -52,10 +58,10 @@ extension AppSettings {
|
||||
profileImageCornerRadiusGroupDefault.set(val)
|
||||
def.setValue(val, forKey: DEFAULT_PROFILE_IMAGE_CORNER_RADIUS)
|
||||
}
|
||||
if let val = uiColorScheme { def.setValue(val, forKey: DEFAULT_CURRENT_THEME) }
|
||||
if let val = uiDarkColorScheme { def.setValue(val, forKey: DEFAULT_SYSTEM_DARK_THEME) }
|
||||
if let val = uiCurrentThemeIds { def.setValue(val, forKey: DEFAULT_CURRENT_THEME_IDS) }
|
||||
if let val = uiThemes { def.setValue(val.skipDuplicates(), forKey: DEFAULT_THEME_OVERRIDES) }
|
||||
if let val = uiColorScheme { currentThemeDefault.set(val) }
|
||||
if let val = uiDarkColorScheme { systemDarkThemeDefault.set(val) }
|
||||
if let val = uiCurrentThemeIds { currentThemeIdsDefault.set(val) }
|
||||
if let val = uiThemes { themeOverridesDefault.set(val.skipDuplicates()) }
|
||||
if let val = oneHandUI { groupDefaults.setValue(val, forKey: GROUP_DEFAULT_ONE_HAND_UI) }
|
||||
}
|
||||
|
||||
@@ -63,6 +69,7 @@ extension AppSettings {
|
||||
let def = UserDefaults.standard
|
||||
var c = AppSettings.defaults
|
||||
c.networkConfig = getNetCfg()
|
||||
c.networkProxy = networkProxyDefault.get()
|
||||
c.privacyEncryptLocalFiles = privacyEncryptLocalFilesGroupDefault.get()
|
||||
c.privacyAskToApproveRelays = privacyAskToApproveRelaysGroupDefault.get()
|
||||
c.privacyAcceptImages = privacyAcceptImagesGroupDefault.get()
|
||||
|
||||
@@ -73,6 +73,8 @@ let DEFAULT_SYSTEM_DARK_THEME = "systemDarkTheme"
|
||||
let DEFAULT_CURRENT_THEME_IDS = "currentThemeIds"
|
||||
let DEFAULT_THEME_OVERRIDES = "themeOverrides"
|
||||
|
||||
let DEFAULT_NETWORK_PROXY = "networkProxy"
|
||||
|
||||
let ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN = "androidCallOnLockScreen"
|
||||
|
||||
let appDefaults: [String: Any] = [
|
||||
@@ -245,6 +247,7 @@ public class CodableDefault<T: Codable> {
|
||||
}
|
||||
}
|
||||
|
||||
let networkProxyDefault: CodableDefault<NetworkProxy> = CodableDefault(defaults: UserDefaults.standard, forKey: DEFAULT_NETWORK_PROXY, withDefault: NetworkProxy.def)
|
||||
|
||||
struct SettingsView: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
|
||||
@@ -177,6 +177,8 @@
|
||||
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 */; };
|
||||
8C01E9C12C8EFC33008A4B0A /* objc.m in Sources */ = {isa = PBXBuildFile; fileRef = 8C01E9C02C8EFC33008A4B0A /* objc.m */; };
|
||||
8C01E9C22C8EFF8F008A4B0A /* objc.h in Headers */ = {isa = PBXBuildFile; fileRef = 8C01E9BF2C8EFBB6008A4B0A /* objc.h */; };
|
||||
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 */; };
|
||||
@@ -214,11 +216,11 @@
|
||||
D77B92DC2952372200A5A1CC /* SwiftyGif in Frameworks */ = {isa = PBXBuildFile; productRef = D77B92DB2952372200A5A1CC /* SwiftyGif */; };
|
||||
D7F0E33929964E7E0068AF69 /* LZString in Frameworks */ = {isa = PBXBuildFile; productRef = D7F0E33829964E7E0068AF69 /* LZString */; };
|
||||
E51CC1E62C62085600DB91FE /* OneHandUICard.swift in Sources */ = {isa = PBXBuildFile; fileRef = E51CC1E52C62085600DB91FE /* OneHandUICard.swift */; };
|
||||
E5BD844D2C8220D0008C24D1 /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BD84482C8220D0008C24D1 /* libffi.a */; };
|
||||
E5BD844E2C8220D0008C24D1 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BD84492C8220D0008C24D1 /* libgmpxx.a */; };
|
||||
E5BD844F2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BD844A2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob-ghc9.6.3.a */; };
|
||||
E5BD84502C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BD844B2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob.a */; };
|
||||
E5BD84512C8220D0008C24D1 /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5BD844C2C8220D0008C24D1 /* libgmp.a */; };
|
||||
E5CC47842CA31C3A00551ACF /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5CC477F2CA31C3900551ACF /* libgmpxx.a */; };
|
||||
E5CC47852CA31C3A00551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5CC47802CA31C3900551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP.a */; };
|
||||
E5CC47862CA31C3A00551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP-ghc9.6.3.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5CC47812CA31C3900551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP-ghc9.6.3.a */; };
|
||||
E5CC47872CA31C3A00551ACF /* libgmp.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5CC47822CA31C3A00551ACF /* libgmp.a */; };
|
||||
E5CC47882CA31C3A00551ACF /* libffi.a in Frameworks */ = {isa = PBXBuildFile; fileRef = E5CC47832CA31C3A00551ACF /* libffi.a */; };
|
||||
E5DCF8DB2C56FAC1007928CC /* SimpleXChat.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5CE2BA682845308900EC33A6 /* SimpleXChat.framework */; };
|
||||
E5DCF9712C590272007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF96F2C590272007928CC /* Localizable.strings */; };
|
||||
E5DCF9842C5902CE007928CC /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E5DCF9822C5902CE007928CC /* Localizable.strings */; };
|
||||
@@ -516,6 +518,8 @@
|
||||
64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = "<group>"; };
|
||||
64EEB0F62C353F1C00972D62 /* ServersSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServersSummaryView.swift; sourceTree = "<group>"; };
|
||||
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = "<group>"; };
|
||||
8C01E9BF2C8EFBB6008A4B0A /* objc.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = objc.h; sourceTree = "<group>"; };
|
||||
8C01E9C02C8EFC33008A4B0A /* objc.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = objc.m; sourceTree = "<group>"; };
|
||||
8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
|
||||
8C74C3EB2C1B92A900039E77 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
|
||||
8C74C3ED2C1B942300039E77 /* ChatWallpaper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatWallpaper.swift; sourceTree = "<group>"; };
|
||||
@@ -550,11 +554,11 @@
|
||||
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||
D7AA2C3429A936B400737B40 /* MediaEncryption.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = MediaEncryption.playground; path = Shared/MediaEncryption.playground; sourceTree = SOURCE_ROOT; xcLanguageSpecificationIdentifier = xcode.lang.swift; };
|
||||
E51CC1E52C62085600DB91FE /* OneHandUICard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OneHandUICard.swift; sourceTree = "<group>"; };
|
||||
E5BD84482C8220D0008C24D1 /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
E5BD84492C8220D0008C24D1 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
E5BD844A2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
E5BD844B2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob.a"; sourceTree = "<group>"; };
|
||||
E5BD844C2C8220D0008C24D1 /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
E5CC477F2CA31C3900551ACF /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = "<group>"; };
|
||||
E5CC47802CA31C3900551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP.a"; sourceTree = "<group>"; };
|
||||
E5CC47812CA31C3900551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP-ghc9.6.3.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP-ghc9.6.3.a"; sourceTree = "<group>"; };
|
||||
E5CC47822CA31C3A00551ACF /* libgmp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmp.a; sourceTree = "<group>"; };
|
||||
E5CC47832CA31C3A00551ACF /* libffi.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libffi.a; sourceTree = "<group>"; };
|
||||
E5DCF9702C590272007928CC /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
E5DCF9722C590274007928CC /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
E5DCF9732C590275007928CC /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = "<group>"; };
|
||||
@@ -645,14 +649,14 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E5CC47852CA31C3A00551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP.a in Frameworks */,
|
||||
E5CC47872CA31C3A00551ACF /* libgmp.a in Frameworks */,
|
||||
E5CC47842CA31C3A00551ACF /* libgmpxx.a in Frameworks */,
|
||||
E5CC47882CA31C3A00551ACF /* libffi.a in Frameworks */,
|
||||
5CE2BA93284534B000EC33A6 /* libiconv.tbd in Frameworks */,
|
||||
E5BD844E2C8220D0008C24D1 /* libgmpxx.a in Frameworks */,
|
||||
E5BD84512C8220D0008C24D1 /* libgmp.a in Frameworks */,
|
||||
E5CC47862CA31C3A00551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP-ghc9.6.3.a in Frameworks */,
|
||||
5CE2BA94284534BB00EC33A6 /* libz.tbd in Frameworks */,
|
||||
CE38A29C2C3FCD72005ED185 /* SwiftyGif in Frameworks */,
|
||||
E5BD84502C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob.a in Frameworks */,
|
||||
E5BD844D2C8220D0008C24D1 /* libffi.a in Frameworks */,
|
||||
E5BD844F2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob-ghc9.6.3.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -729,11 +733,11 @@
|
||||
5C764E5C279C70B7000C6508 /* Libraries */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E5BD84482C8220D0008C24D1 /* libffi.a */,
|
||||
E5BD844C2C8220D0008C24D1 /* libgmp.a */,
|
||||
E5BD84492C8220D0008C24D1 /* libgmpxx.a */,
|
||||
E5BD844A2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob-ghc9.6.3.a */,
|
||||
E5BD844B2C8220D0008C24D1 /* libHSsimplex-chat-6.0.4.0-2x1D8vVukGZOGJwEVzeob.a */,
|
||||
E5CC47832CA31C3A00551ACF /* libffi.a */,
|
||||
E5CC47822CA31C3A00551ACF /* libgmp.a */,
|
||||
E5CC477F2CA31C3900551ACF /* libgmpxx.a */,
|
||||
E5CC47812CA31C3900551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP-ghc9.6.3.a */,
|
||||
E5CC47802CA31C3900551ACF /* libHSsimplex-chat-6.0.5.0-3qcee2iGFVOIynW0cRTIaP.a */,
|
||||
);
|
||||
path = Libraries;
|
||||
sourceTree = "<group>";
|
||||
@@ -976,6 +980,8 @@
|
||||
5CE2BA96284537A800EC33A6 /* dummy.m */,
|
||||
5CD67B8D2B0E858A00C510B1 /* hs_init.h */,
|
||||
5CD67B8E2B0E858A00C510B1 /* hs_init.c */,
|
||||
8C01E9BF2C8EFBB6008A4B0A /* objc.h */,
|
||||
8C01E9C02C8EFC33008A4B0A /* objc.m */,
|
||||
);
|
||||
path = SimpleXChat;
|
||||
sourceTree = "<group>";
|
||||
@@ -1113,6 +1119,7 @@
|
||||
files = (
|
||||
5CE2BA77284530BF00EC33A6 /* SimpleXChat.h in Headers */,
|
||||
5CD67B8F2B0E858A00C510B1 /* hs_init.h in Headers */,
|
||||
8C01E9C22C8EFF8F008A4B0A /* objc.h in Headers */,
|
||||
5CE2BA952845354B00EC33A6 /* SimpleX.h in Headers */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -1539,6 +1546,7 @@
|
||||
8C74C3E72C1B901900039E77 /* Color.swift in Sources */,
|
||||
5CD67B902B0E858A00C510B1 /* hs_init.c in Sources */,
|
||||
5CE2BA91284533A300EC33A6 /* Notifications.swift in Sources */,
|
||||
8C01E9C12C8EFC33008A4B0A /* objc.m in Sources */,
|
||||
5CE2BA79284530CC00EC33A6 /* SimpleXChat.docc in Sources */,
|
||||
5CE2BA90284533A300EC33A6 /* JSON.swift in Sources */,
|
||||
5CE2BA8B284533A300EC33A6 /* ChatTypes.swift in Sources */,
|
||||
@@ -1879,7 +1887,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 239;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -1904,7 +1912,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES_THIN;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.0.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1928,7 +1936,7 @@
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX (iOS).entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 239;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -1953,7 +1961,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.0.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.app;
|
||||
PRODUCT_NAME = SimpleX;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1969,11 +1977,11 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 239;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.0.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -1989,11 +1997,11 @@
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 239;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.0.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.Tests-iOS";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2014,7 +2022,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 239;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
GCC_OPTIMIZATION_LEVEL = s;
|
||||
@@ -2029,7 +2037,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.0.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -2051,7 +2059,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX NSE/SimpleX NSE.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 239;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_CODE_COVERAGE = NO;
|
||||
@@ -2066,7 +2074,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.0.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-NSE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
@@ -2088,7 +2096,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 239;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2114,7 +2122,7 @@
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.0.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2139,7 +2147,7 @@
|
||||
CLANG_TIDY_BUGPRONE_REDUNDANT_BRANCH_CONDITION = YES;
|
||||
CLANG_TIDY_MISC_REDUNDANT_EXPRESSION = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 239;
|
||||
DEFINES_MODULE = YES;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
DYLIB_COMPATIBILITY_VERSION = 1;
|
||||
@@ -2165,7 +2173,7 @@
|
||||
"$(PROJECT_DIR)/Libraries/sim",
|
||||
);
|
||||
LLVM_LTO = YES;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.0.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = chat.simplex.SimpleXChat;
|
||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2190,7 +2198,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 239;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -2205,7 +2213,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.0.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
@@ -2224,7 +2232,7 @@
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CODE_SIGN_ENTITLEMENTS = "SimpleX SE/SimpleX SE.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 236;
|
||||
CURRENT_PROJECT_VERSION = 239;
|
||||
DEVELOPMENT_TEAM = 5NN7GUYB6T;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
@@ -2239,7 +2247,7 @@
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 6.0.4;
|
||||
MARKETING_VERSION = 6.0.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "chat.simplex.app.SimpleX-SE";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = iphoneos;
|
||||
|
||||
+1
-2
@@ -23,8 +23,7 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/kirualex/SwiftyGif",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "7c50eb60ca4b90043c6ad719d595803488496212"
|
||||
"revision" : "5e8619335d394901379c9add5c4c1c2f420b3800"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
import Network
|
||||
|
||||
public let jsonDecoder = getJSONDecoder()
|
||||
public let jsonEncoder = getJSONEncoder()
|
||||
@@ -1476,6 +1477,63 @@ public struct KeepAliveOpts: Codable, Equatable {
|
||||
public static let defaults: KeepAliveOpts = KeepAliveOpts(keepIdle: 30, keepIntvl: 15, keepCnt: 4)
|
||||
}
|
||||
|
||||
public struct NetworkProxy: Equatable, Codable {
|
||||
public var host: String = ""
|
||||
public var port: Int = 0
|
||||
public var auth: NetworkProxyAuth = .username
|
||||
public var username: String = ""
|
||||
public var password: String = ""
|
||||
|
||||
public static var def: NetworkProxy {
|
||||
NetworkProxy()
|
||||
}
|
||||
|
||||
public var valid: Bool {
|
||||
let hostOk = switch NWEndpoint.Host(host) {
|
||||
case .ipv4: true
|
||||
case .ipv6: true
|
||||
default: false
|
||||
}
|
||||
return hostOk &&
|
||||
port > 0 && port <= 65535 &&
|
||||
NetworkProxy.validCredential(username) && NetworkProxy.validCredential(password)
|
||||
}
|
||||
|
||||
public static func validCredential(_ s: String) -> Bool {
|
||||
!s.contains(":") && !s.contains("@")
|
||||
}
|
||||
|
||||
public func toProxyString() -> String? {
|
||||
if !valid { return nil }
|
||||
var res = ""
|
||||
switch auth {
|
||||
case .username:
|
||||
let usernameTrimmed = username.trimmingCharacters(in: .whitespaces)
|
||||
let passwordTrimmed = password.trimmingCharacters(in: .whitespaces)
|
||||
if usernameTrimmed != "" || passwordTrimmed != "" {
|
||||
res += usernameTrimmed + ":" + passwordTrimmed + "@"
|
||||
} else {
|
||||
res += "@"
|
||||
}
|
||||
case .isolate: ()
|
||||
}
|
||||
if host != "" {
|
||||
if host.contains(":") {
|
||||
res += "[\(host.trimmingCharacters(in: [" ", "[", "]"]))]"
|
||||
} else {
|
||||
res += host.trimmingCharacters(in: .whitespaces)
|
||||
}
|
||||
}
|
||||
res += ":\(port)"
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
public enum NetworkProxyAuth: String, Codable {
|
||||
case username
|
||||
case isolate
|
||||
}
|
||||
|
||||
public enum NetworkStatus: Decodable, Equatable {
|
||||
case unknown
|
||||
case connected
|
||||
@@ -2099,11 +2157,13 @@ public struct MigrationFileLinkData: Codable {
|
||||
|
||||
public struct NetworkConfig: Codable {
|
||||
let socksProxy: String?
|
||||
let networkProxy: NetworkProxy?
|
||||
let hostMode: HostMode?
|
||||
let requiredHostMode: Bool?
|
||||
|
||||
public init(socksProxy: String?, hostMode: HostMode?, requiredHostMode: Bool?) {
|
||||
public init(socksProxy: String?, networkProxy: NetworkProxy?, hostMode: HostMode?, requiredHostMode: Bool?) {
|
||||
self.socksProxy = socksProxy
|
||||
self.networkProxy = networkProxy
|
||||
self.hostMode = hostMode
|
||||
self.requiredHostMode = requiredHostMode
|
||||
}
|
||||
@@ -2112,6 +2172,7 @@ public struct MigrationFileLinkData: Codable {
|
||||
return if let hostMode, let requiredHostMode {
|
||||
NetworkConfig(
|
||||
socksProxy: nil,
|
||||
networkProxy: nil,
|
||||
hostMode: hostMode == .onionViaSocks ? .onionHost : hostMode,
|
||||
requiredHostMode: requiredHostMode
|
||||
)
|
||||
@@ -2131,6 +2192,7 @@ public struct MigrationFileLinkData: Codable {
|
||||
|
||||
public struct AppSettings: Codable, Equatable {
|
||||
public var networkConfig: NetCfg? = nil
|
||||
public var networkProxy: NetworkProxy? = nil
|
||||
public var privacyEncryptLocalFiles: Bool? = nil
|
||||
public var privacyAskToApproveRelays: Bool? = nil
|
||||
public var privacyAcceptImages: Bool? = nil
|
||||
@@ -2162,6 +2224,7 @@ public struct AppSettings: Codable, Equatable {
|
||||
var empty = AppSettings()
|
||||
let def = AppSettings.defaults
|
||||
if networkConfig != def.networkConfig { empty.networkConfig = networkConfig }
|
||||
if networkProxy != def.networkProxy { empty.networkProxy = networkProxy }
|
||||
if privacyEncryptLocalFiles != def.privacyEncryptLocalFiles { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles }
|
||||
if privacyAskToApproveRelays != def.privacyAskToApproveRelays { empty.privacyAskToApproveRelays = privacyAskToApproveRelays }
|
||||
if privacyAcceptImages != def.privacyAcceptImages { empty.privacyAcceptImages = privacyAcceptImages }
|
||||
@@ -2194,6 +2257,7 @@ public struct AppSettings: Codable, Equatable {
|
||||
public static var defaults: AppSettings {
|
||||
AppSettings (
|
||||
networkConfig: NetCfg.defaults,
|
||||
networkProxy: NetworkProxy.def,
|
||||
privacyEncryptLocalFiles: true,
|
||||
privacyAskToApproveRelays: true,
|
||||
privacyAcceptImages: true,
|
||||
|
||||
@@ -35,6 +35,7 @@ public let GROUP_DEFAULT_PRIVACY_ASK_TO_APPROVE_RELAYS = "privacyAskToApproveRel
|
||||
// replaces DEFAULT_PROFILE_IMAGE_CORNER_RADIUS
|
||||
public let GROUP_DEFAULT_PROFILE_IMAGE_CORNER_RADIUS = "profileImageCornerRadius"
|
||||
let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount"
|
||||
public let GROUP_DEFAULT_NETWORK_SOCKS_PROXY = "networkSocksProxy"
|
||||
let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts"
|
||||
let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode"
|
||||
let GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE = "networkSMPProxyMode"
|
||||
@@ -327,6 +328,7 @@ public class Default<T> {
|
||||
}
|
||||
|
||||
public func getNetCfg() -> NetCfg {
|
||||
let socksProxy = groupDefaults.string(forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY)
|
||||
let onionHosts = networkUseOnionHostsGroupDefault.get()
|
||||
let (hostMode, requiredHostMode) = onionHosts.hostMode
|
||||
let sessionMode = networkSessionModeGroupDefault.get()
|
||||
@@ -349,6 +351,7 @@ public func getNetCfg() -> NetCfg {
|
||||
tcpKeepAlive = nil
|
||||
}
|
||||
return NetCfg(
|
||||
socksProxy: socksProxy,
|
||||
hostMode: hostMode,
|
||||
requiredHostMode: requiredHostMode,
|
||||
sessionMode: sessionMode,
|
||||
@@ -365,11 +368,13 @@ public func getNetCfg() -> NetCfg {
|
||||
)
|
||||
}
|
||||
|
||||
public func setNetCfg(_ cfg: NetCfg) {
|
||||
public func setNetCfg(_ cfg: NetCfg, networkProxy: NetworkProxy?) {
|
||||
networkUseOnionHostsGroupDefault.set(OnionHosts(netCfg: cfg))
|
||||
networkSessionModeGroupDefault.set(cfg.sessionMode)
|
||||
networkSMPProxyModeGroupDefault.set(cfg.smpProxyMode)
|
||||
networkSMPProxyFallbackGroupDefault.set(cfg.smpProxyFallback)
|
||||
let socksProxy = networkProxy?.toProxyString()
|
||||
groupDefaults.set(socksProxy, forKey: GROUP_DEFAULT_NETWORK_SOCKS_PROXY)
|
||||
groupDefaults.set(cfg.tcpConnectTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT)
|
||||
groupDefaults.set(cfg.tcpTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT)
|
||||
groupDefaults.set(cfg.tcpTimeoutPerKb, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB)
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#define SimpleX_h
|
||||
|
||||
#include "hs_init.h"
|
||||
#include "objc.h"
|
||||
|
||||
extern void hs_init(int argc, char **argv[]);
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// objc.h
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Stanislav Dmitrenko on 09.09.2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef objc_h
|
||||
#define objc_h
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface ObjC : NSObject
|
||||
|
||||
+ (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* objc_h */
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// objc.m
|
||||
// SimpleXChat
|
||||
//
|
||||
// Created by Stanislav Dmitrenko on 09.09.2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
#import "objc.h"
|
||||
|
||||
@implementation ObjC
|
||||
|
||||
// https://stackoverflow.com/a/36454808
|
||||
+ (BOOL)catchException:(void(^)(void))tryBlock error:(__autoreleasing NSError **)error {
|
||||
@try {
|
||||
tryBlock();
|
||||
return YES;
|
||||
}
|
||||
@catch (NSException *exception) {
|
||||
*error = [[NSError alloc] initWithDomain: exception.name code: 0 userInfo: exception.userInfo];
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -15,7 +15,7 @@ android {
|
||||
namespace = "chat.simplex.app"
|
||||
minSdk = 26
|
||||
//noinspection OldTargetApi
|
||||
targetSdk = 33
|
||||
targetSdk = 34
|
||||
// !!!
|
||||
// skip version code after release to F-Droid, as it uses two version codes
|
||||
versionCode = (extra["android.version_code"] as String).toInt()
|
||||
@@ -126,29 +126,29 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation(project(":common"))
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.core:core-ktx:1.13.1")
|
||||
//implementation("androidx.compose.ui:ui:${rootProject.extra["compose.version"] as String}")
|
||||
//implementation("androidx.compose.material:material:$compose_version")
|
||||
//implementation("androidx.compose.ui:ui-tooling-preview:$compose_version")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
|
||||
implementation("androidx.lifecycle:lifecycle-process:2.7.0")
|
||||
implementation("androidx.activity:activity-compose:1.8.2")
|
||||
val workVersion = "2.9.0"
|
||||
implementation("androidx.appcompat:appcompat:1.7.0")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.4")
|
||||
implementation("androidx.lifecycle:lifecycle-process:2.8.4")
|
||||
implementation("androidx.activity:activity-compose:1.9.1")
|
||||
val workVersion = "2.9.1"
|
||||
implementation("androidx.work:work-runtime-ktx:$workVersion")
|
||||
implementation("androidx.work:work-multiprocess:$workVersion")
|
||||
|
||||
implementation("com.jakewharton:process-phoenix:2.2.0")
|
||||
implementation("com.jakewharton:process-phoenix:3.0.0")
|
||||
|
||||
//Camera Permission
|
||||
implementation("com.google.accompanist:accompanist-permissions:0.23.0")
|
||||
implementation("com.google.accompanist:accompanist-permissions:0.34.0")
|
||||
|
||||
//implementation("androidx.compose.material:material-icons-extended:$compose_version")
|
||||
//implementation("androidx.compose.ui:ui-util:$compose_version")
|
||||
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.2.1")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
|
||||
//androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version")
|
||||
debugImplementation("androidx.compose.ui:ui-tooling:1.6.4")
|
||||
}
|
||||
|
||||
@@ -21,6 +21,12 @@
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
|
||||
|
||||
<!-- Requirements that allows to specify foreground service types -->
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_REMOTE_MESSAGING" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_CAMERA" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
|
||||
<application
|
||||
android:name="SimplexApp"
|
||||
android:allowBackup="false"
|
||||
@@ -133,7 +139,9 @@
|
||||
android:name=".SimplexService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:stopWithTask="false"></service>
|
||||
android:stopWithTask="false"
|
||||
android:foregroundServiceType="remoteMessaging"
|
||||
/>
|
||||
|
||||
<!-- SimplexService restart on reboot -->
|
||||
|
||||
@@ -141,7 +149,9 @@
|
||||
android:name=".CallService"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
android:stopWithTask="false"/>
|
||||
android:stopWithTask="false"
|
||||
android:foregroundServiceType="mediaPlayback|microphone|camera|remoteMessaging"
|
||||
/>
|
||||
|
||||
<receiver
|
||||
android:name=".CallService$CallActionReceiver"
|
||||
|
||||
@@ -2,17 +2,18 @@ package chat.simplex.app
|
||||
|
||||
import android.app.*
|
||||
import android.content.*
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.*
|
||||
import androidx.compose.ui.graphics.asAndroidBitmap
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import chat.simplex.app.model.NtfManager.EndCallAction
|
||||
import chat.simplex.app.views.call.CallActivity
|
||||
import chat.simplex.common.model.NotificationPreviewMode
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.call.CallState
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.datetime.Instant
|
||||
@@ -34,7 +35,7 @@ class CallService: Service() {
|
||||
} else {
|
||||
Log.d(TAG, "null intent. Probably restarted by the system.")
|
||||
}
|
||||
startForeground(CALL_SERVICE_ID, serviceNotification)
|
||||
ServiceCompat.startForeground(this, CALL_SERVICE_ID, createNotificationIfNeeded(), foregroundServiceType())
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
@@ -42,8 +43,7 @@ class CallService: Service() {
|
||||
super.onCreate()
|
||||
Log.d(TAG, "Call service created")
|
||||
notificationManager = createNotificationChannel()
|
||||
updateNotification()
|
||||
startForeground(CALL_SERVICE_ID, serviceNotification)
|
||||
ServiceCompat.startForeground(this, CALL_SERVICE_ID, updateNotification(), foregroundServiceType())
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@@ -69,7 +69,14 @@ class CallService: Service() {
|
||||
}
|
||||
}
|
||||
|
||||
fun updateNotification() {
|
||||
private fun createNotificationIfNeeded(): Notification {
|
||||
val ntf = serviceNotification
|
||||
if (ntf != null) return ntf
|
||||
|
||||
return updateNotification()
|
||||
}
|
||||
|
||||
fun updateNotification(): Notification {
|
||||
val call = chatModel.activeCall.value
|
||||
val previewMode = appPreferences.notificationPreviewMode.get()
|
||||
val title = if (previewMode == NotificationPreviewMode.HIDDEN.name)
|
||||
@@ -83,8 +90,31 @@ class CallService: Service() {
|
||||
else
|
||||
base64ToBitmap(image).asAndroidBitmap()
|
||||
|
||||
serviceNotification = createNotification(title, text, largeIcon, call?.connectedAt)
|
||||
startForeground(CALL_SERVICE_ID, serviceNotification)
|
||||
val ntf = createNotification(title, text, largeIcon, call?.connectedAt)
|
||||
serviceNotification = ntf
|
||||
ServiceCompat.startForeground(this, CALL_SERVICE_ID, ntf, foregroundServiceType())
|
||||
return ntf
|
||||
}
|
||||
|
||||
private fun foregroundServiceType(): Int {
|
||||
val call = chatModel.activeCall.value
|
||||
return if (call == null) {
|
||||
if (Build.VERSION.SDK_INT >= 34) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else if (Build.VERSION.SDK_INT >= 30) {
|
||||
if (call.supportsVideo()) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA
|
||||
} else {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE
|
||||
}
|
||||
} else if (Build.VERSION.SDK_INT >= 29) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotificationChannel(): NotificationManager? {
|
||||
|
||||
@@ -54,7 +54,7 @@ class MainActivity: FragmentActivity() {
|
||||
SimplexApp.context.schedulePeriodicWakeUp()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
processIntent(intent)
|
||||
processExternalIntent(intent)
|
||||
|
||||
@@ -5,9 +5,8 @@ import android.util.Log
|
||||
import androidx.work.*
|
||||
import chat.simplex.app.SimplexService.Companion.showPassphraseNotification
|
||||
import chat.simplex.common.model.ChatController
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.helpers.DBMigrationResult
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.platform.initChatControllerOnStart
|
||||
import chat.simplex.common.views.helpers.DatabaseUtils
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.Date
|
||||
@@ -30,12 +29,12 @@ object MessagesFetcherWorker {
|
||||
.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(SimplexApp.context).enqueueUniqueWork(UNIQUE_WORK_TAG, ExistingWorkPolicy.REPLACE, periodicWorkRequest)
|
||||
SimplexApp.context.getWorkManagerInstance().enqueueUniqueWork(UNIQUE_WORK_TAG, ExistingWorkPolicy.REPLACE, periodicWorkRequest)
|
||||
}
|
||||
|
||||
fun cancelAll() {
|
||||
Log.d(TAG, "Worker: canceled all tasks")
|
||||
WorkManager.getInstance(SimplexApp.context).cancelUniqueWork(UNIQUE_WORK_TAG)
|
||||
SimplexApp.context.getWorkManagerInstance().cancelUniqueWork(UNIQUE_WORK_TAG)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import chat.simplex.common.platform.Log
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.os.*
|
||||
import android.view.View
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -120,7 +119,10 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
* */
|
||||
if (chatModel.chatRunning.value != false &&
|
||||
chatModel.controller.appPrefs.onboardingStage.get() == OnboardingStage.OnboardingComplete &&
|
||||
appPrefs.notificationsMode.get() == NotificationsMode.SERVICE
|
||||
appPrefs.notificationsMode.get() == NotificationsMode.SERVICE &&
|
||||
// New installation passes all checks above and tries to start the service which is not needed at all
|
||||
// because preferred notification type is not yet chosen. So, check that the user has initialized db already
|
||||
appPrefs.newDatabaseInitialized.get()
|
||||
) {
|
||||
SimplexService.start()
|
||||
}
|
||||
@@ -161,7 +163,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
.addTag(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC)
|
||||
.build()
|
||||
Log.d(TAG, "ServiceStartWorker: Scheduling period work every ${SimplexService.SERVICE_START_WORKER_INTERVAL_MINUTES} minutes")
|
||||
WorkManager.getInstance(context)?.enqueueUniquePeriodicWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC, workPolicy, work)
|
||||
getWorkManagerInstance().enqueueUniquePeriodicWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC, workPolicy, work)
|
||||
}
|
||||
|
||||
fun schedulePeriodicWakeUp() = CoroutineScope(Dispatchers.Default).launch {
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
||||
import android.app.*
|
||||
import android.content.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.net.Uri
|
||||
import android.os.*
|
||||
import android.os.SystemClock
|
||||
@@ -15,8 +16,10 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.work.*
|
||||
import chat.simplex.app.model.NtfManager
|
||||
import chat.simplex.common.AppLock
|
||||
import chat.simplex.common.helpers.requiresIgnoringBattery
|
||||
import chat.simplex.common.model.ChatController
|
||||
@@ -52,18 +55,15 @@ class SimplexService: Service() {
|
||||
} else {
|
||||
Log.d(TAG, "null intent. Probably restarted by the system.")
|
||||
}
|
||||
startForeground(SIMPLEX_SERVICE_ID, serviceNotification)
|
||||
ServiceCompat.startForeground(this, SIMPLEX_SERVICE_ID, createNotificationIfNeeded(), foregroundServiceType())
|
||||
return START_STICKY // to restart if killed
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(TAG, "Simplex service created")
|
||||
val title = generalGetString(MR.strings.simplex_service_notification_title)
|
||||
val text = generalGetString(MR.strings.simplex_service_notification_text)
|
||||
notificationManager = createNotificationChannel()
|
||||
serviceNotification = createNotification(title, text)
|
||||
startForeground(SIMPLEX_SERVICE_ID, serviceNotification)
|
||||
createNotificationIfNeeded()
|
||||
ServiceCompat.startForeground(this, SIMPLEX_SERVICE_ID, createNotificationIfNeeded(), foregroundServiceType())
|
||||
/**
|
||||
* The reason [stopAfterStart] exists is because when the service is not called [startForeground] yet, and
|
||||
* we call [stopSelf] on the same service, [ForegroundServiceDidNotStartInTimeException] will be thrown.
|
||||
@@ -103,6 +103,26 @@ class SimplexService: Service() {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun createNotificationIfNeeded(): Notification {
|
||||
val ntf = serviceNotification
|
||||
if (ntf != null) return ntf
|
||||
|
||||
val title = generalGetString(MR.strings.simplex_service_notification_title)
|
||||
val text = generalGetString(MR.strings.simplex_service_notification_text)
|
||||
notificationManager = createNotificationChannel()
|
||||
val newNtf = createNotification(title, text)
|
||||
serviceNotification = newNtf
|
||||
return newNtf
|
||||
}
|
||||
|
||||
private fun foregroundServiceType(): Int {
|
||||
return if (Build.VERSION.SDK_INT >= 34) {
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
private fun startService() {
|
||||
Log.d(TAG, "SimplexService startService")
|
||||
if (wakeLock != null || isCheckingNewMessages) return
|
||||
@@ -272,7 +292,7 @@ class SimplexService: Service() {
|
||||
|
||||
fun scheduleStart(context: Context) {
|
||||
Log.d(TAG, "Enqueuing work to start subscriber service")
|
||||
val workManager = WorkManager.getInstance(context)
|
||||
val workManager = context.getWorkManagerInstance()
|
||||
val startServiceRequest = OneTimeWorkRequest.Builder(ServiceStartWorker::class.java).build()
|
||||
workManager.enqueueUniqueWork(WORK_NAME_ONCE, ExistingWorkPolicy.KEEP, startServiceRequest) // Unique avoids races!
|
||||
}
|
||||
@@ -292,6 +312,10 @@ class SimplexService: Service() {
|
||||
}
|
||||
|
||||
private suspend fun serviceAction(action: Action) {
|
||||
if (!NtfManager.areNotificationsEnabledInSystem()) {
|
||||
Log.d(TAG, "SimplexService serviceAction: ${action.name}. Notifications are not enabled in OS yet, not starting service")
|
||||
return
|
||||
}
|
||||
Log.d(TAG, "SimplexService serviceAction: ${action.name}")
|
||||
withContext(Dispatchers.IO) {
|
||||
Intent(androidAppContext, SimplexService::class.java).also {
|
||||
|
||||
+3
-1
@@ -53,7 +53,7 @@ object NtfManager {
|
||||
private val msgNtfTimeoutMs = 30000L
|
||||
|
||||
init {
|
||||
if (manager.areNotificationsEnabled()) createNtfChannelsMaybeShowAlert()
|
||||
if (areNotificationsEnabledInSystem()) createNtfChannelsMaybeShowAlert()
|
||||
}
|
||||
|
||||
private fun callNotificationChannel(channelId: String, channelName: String): NotificationChannel {
|
||||
@@ -287,6 +287,8 @@ object NtfManager {
|
||||
}
|
||||
}
|
||||
|
||||
fun areNotificationsEnabledInSystem() = manager.areNotificationsEnabled()
|
||||
|
||||
/**
|
||||
* This function creates notifications channels. On Android 13+ calling it for the first time will trigger system alert,
|
||||
* The alert asks a user to allow or disallow to show notifications for the app. That's why it should be called only when the user
|
||||
|
||||
+5
-5
@@ -120,6 +120,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
|
||||
return grantedAudio && grantedCamera
|
||||
}
|
||||
|
||||
@Deprecated("Was deprecated in OS")
|
||||
override fun onBackPressed() {
|
||||
if (isOnLockScreenNow()) {
|
||||
super.onBackPressed()
|
||||
@@ -139,6 +140,7 @@ class CallActivity: ComponentActivity(), ServiceConnection {
|
||||
}
|
||||
|
||||
override fun onUserLeaveHint() {
|
||||
super.onUserLeaveHint()
|
||||
// On Android 12+ PiP is enabled automatically when a user hides the app
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R && callSupportsVideo() && platform.androidPictureInPictureAllowed()) {
|
||||
enterPictureInPictureMode()
|
||||
@@ -248,6 +250,9 @@ fun CallActivityView() {
|
||||
)
|
||||
if (permissionsState.allPermissionsGranted) {
|
||||
ActiveCallView()
|
||||
LaunchedEffect(Unit) {
|
||||
activity.startServiceAndBind()
|
||||
}
|
||||
} else {
|
||||
CallPermissionsView(remember { m.activeCallViewIsCollapsed }.value, callSupportsVideo()) {
|
||||
withBGApi { chatModel.callManager.endCall(call) }
|
||||
@@ -285,11 +290,6 @@ fun CallActivityView() {
|
||||
AlertManager.shared.showInView()
|
||||
}
|
||||
}
|
||||
LaunchedEffect(call == null) {
|
||||
if (call != null) {
|
||||
activity.startServiceAndBind()
|
||||
}
|
||||
}
|
||||
LaunchedEffect(invitation, call, switchingCall, showCallView) {
|
||||
if (!switchingCall && invitation == null && (!showCallView || call == null)) {
|
||||
Log.d(TAG, "CallActivityView: finishing activity")
|
||||
|
||||
@@ -2,6 +2,5 @@
|
||||
<resources>
|
||||
<color name="black">#FF000000</color>
|
||||
<color name="white">#FFFFFFFF</color>
|
||||
<color name="highOrLowLight">#8b8786</color>
|
||||
<color name="window_background_dark">#121212</color>
|
||||
</resources>
|
||||
@@ -61,8 +61,8 @@ kotlin {
|
||||
val androidMain by getting {
|
||||
kotlin.srcDir("build/generated/moko/androidMain/src")
|
||||
dependencies {
|
||||
implementation("androidx.activity:activity-compose:1.8.2")
|
||||
val workVersion = "2.9.0"
|
||||
implementation("androidx.activity:activity-compose:1.9.1")
|
||||
val workVersion = "2.9.1"
|
||||
implementation("androidx.work:work-runtime-ktx:$workVersion")
|
||||
implementation("com.google.accompanist:accompanist-insets:0.30.1")
|
||||
|
||||
@@ -78,31 +78,36 @@ kotlin {
|
||||
//Camera Permission
|
||||
implementation("com.google.accompanist:accompanist-permissions:0.34.0")
|
||||
|
||||
implementation("androidx.webkit:webkit:1.10.0")
|
||||
implementation("androidx.webkit:webkit:1.11.0")
|
||||
|
||||
// GIFs support
|
||||
implementation("io.coil-kt:coil-compose:2.6.0")
|
||||
implementation("io.coil-kt:coil-gif:2.6.0")
|
||||
|
||||
implementation("com.jakewharton:process-phoenix:2.2.0")
|
||||
implementation("com.jakewharton:process-phoenix:3.0.0")
|
||||
|
||||
val cameraXVersion = "1.3.2"
|
||||
val cameraXVersion = "1.3.4"
|
||||
implementation("androidx.camera:camera-core:${cameraXVersion}")
|
||||
implementation("androidx.camera:camera-camera2:${cameraXVersion}")
|
||||
implementation("androidx.camera:camera-lifecycle:${cameraXVersion}")
|
||||
implementation("androidx.camera:camera-view:${cameraXVersion}")
|
||||
|
||||
// Calls lifecycle listener
|
||||
implementation("androidx.lifecycle:lifecycle-process:2.4.1")
|
||||
implementation("androidx.lifecycle:lifecycle-process:2.8.4")
|
||||
}
|
||||
}
|
||||
val desktopMain by getting {
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.8.0")
|
||||
implementation("com.github.Dansoftowner:jSystemThemeDetector:3.8")
|
||||
implementation("com.github.Dansoftowner:jSystemThemeDetector:3.8") {
|
||||
exclude("net.java.dev.jna")
|
||||
}
|
||||
// For jSystemThemeDetector only
|
||||
implementation("net.java.dev.jna:jna-platform:5.14.0")
|
||||
implementation("com.sshtools:two-slices:0.9.0-SNAPSHOT")
|
||||
implementation("org.slf4j:slf4j-simple:2.0.12")
|
||||
implementation("uk.co.caprica:vlcj:4.8.2")
|
||||
implementation("uk.co.caprica:vlcj:4.8.3")
|
||||
implementation("net.java.dev.jna:jna:5.14.0")
|
||||
implementation("com.github.NanoHttpd.nanohttpd:nanohttpd:efb2ebf85a")
|
||||
implementation("com.github.NanoHttpd.nanohttpd:nanohttpd-websocket:efb2ebf85a")
|
||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||
@@ -119,8 +124,8 @@ android {
|
||||
defaultConfig {
|
||||
minSdk = 26
|
||||
}
|
||||
testOptions.targetSdk = 33
|
||||
lint.targetSdk = 33
|
||||
testOptions.targetSdk = 34
|
||||
lint.targetSdk = 34
|
||||
val isAndroid = gradle.startParameter.taskNames.find {
|
||||
val lower = it.lowercase()
|
||||
lower.contains("release") || lower.startsWith("assemble") || lower.startsWith("install")
|
||||
|
||||
+15
@@ -6,6 +6,8 @@ import android.net.LocalServerSocket
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.WorkManager
|
||||
import java.io.*
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
@@ -72,3 +74,16 @@ fun initHaskell() {
|
||||
|
||||
initHS()
|
||||
}
|
||||
|
||||
fun Context.getWorkManagerInstance(): WorkManager {
|
||||
// https://github.com/OneSignal/OneSignal-Android-SDK/pull/2052/files
|
||||
// https://github.com/OneSignal/OneSignal-Android-SDK/issues/1672
|
||||
if (!WorkManager.isInitialized()) {
|
||||
try {
|
||||
WorkManager.initialize(this, Configuration.Builder().build())
|
||||
} catch (e: IllegalStateException) {
|
||||
Log.e(TAG, "Error initializing WorkManager: ${e.stackTraceToString()}")
|
||||
}
|
||||
}
|
||||
return WorkManager.getInstance(this)
|
||||
}
|
||||
|
||||
+1
-2
@@ -2,7 +2,6 @@ package chat.simplex.common.views.usersettings
|
||||
|
||||
import SectionView
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.work.WorkManager
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -33,7 +32,7 @@ fun restartApp() {
|
||||
}
|
||||
|
||||
private fun shutdownApp() {
|
||||
WorkManager.getInstance(androidAppContext).cancelAllWork()
|
||||
androidAppContext.getWorkManagerInstance().cancelAllWork()
|
||||
platform.androidServiceSafeStop()
|
||||
Runtime.getRuntime().exit(0)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
|
||||
<solid android:color="@color/highOrLowLight" />
|
||||
<solid android:color="#8b8786" />
|
||||
<size android:width="1dp" />
|
||||
</shape>
|
||||
|
||||
+3
-1
@@ -411,7 +411,9 @@ object ChatModel {
|
||||
// remove from current chat
|
||||
if (chatId.value == cInfo.id) {
|
||||
chatItems.removeAll {
|
||||
val remove = it.id == cItem.id
|
||||
// We delete taking into account meta.createdAt to make sure we will not be in situation when two items with the same id will be deleted
|
||||
// (it can happen if already deleted chat item in backend still in the list and new one came with the same (re-used) chat item id)
|
||||
val remove = it.id == cItem.id && it.meta.createdAt == cItem.meta.createdAt
|
||||
if (remove) { AudioPlayer.stop(it) }
|
||||
remove
|
||||
}
|
||||
|
||||
+71
-21
@@ -129,7 +129,22 @@ class AppPreferences {
|
||||
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 _networkProxy = mkStrPreference(SHARED_PREFS_NETWORK_PROXY_HOST_PORT, json.encodeToString(NetworkProxy()))
|
||||
val networkProxy: SharedPreference<NetworkProxy> = SharedPreference(
|
||||
get = fun(): NetworkProxy {
|
||||
val value = _networkProxy.get() ?: return NetworkProxy()
|
||||
return try {
|
||||
if (value.startsWith("{")) {
|
||||
json.decodeFromString(value)
|
||||
} else {
|
||||
NetworkProxy(host = value.substringBefore(":").ifBlank { "localhost" }, port = value.substringAfter(":").toIntOrNull() ?: 9050)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
NetworkProxy()
|
||||
}
|
||||
},
|
||||
set = fun(proxy: NetworkProxy) { _networkProxy.set(json.encodeToString(proxy)) }
|
||||
)
|
||||
private val _networkSessionMode = mkStrPreference(SHARED_PREFS_NETWORK_SESSION_MODE, TransportSessionMode.default.name)
|
||||
val networkSessionMode: SharedPreference<TransportSessionMode> = SharedPreference(
|
||||
get = fun(): TransportSessionMode {
|
||||
@@ -531,7 +546,7 @@ object ChatController {
|
||||
suspend fun startChatWithTemporaryDatabase(ctrl: ChatCtrl, netCfg: NetCfg): User? {
|
||||
Log.d(TAG, "startChatWithTemporaryDatabase")
|
||||
val migrationActiveUser = apiGetActiveUser(null, ctrl) ?: apiCreateActiveUser(null, Profile(displayName = "Temp", fullName = ""), ctrl = ctrl)
|
||||
if (!apiSetNetworkConfig(netCfg, ctrl)) {
|
||||
if (!apiSetNetworkConfig(netCfg, ctrl = ctrl)) {
|
||||
Log.e(TAG, "Error setting network config, stopping migration")
|
||||
return null
|
||||
}
|
||||
@@ -976,16 +991,18 @@ object ChatController {
|
||||
throw Exception("failed to set chat item TTL: ${r.responseType} ${r.details}")
|
||||
}
|
||||
|
||||
suspend fun apiSetNetworkConfig(cfg: NetCfg, ctrl: ChatCtrl? = null): Boolean {
|
||||
suspend fun apiSetNetworkConfig(cfg: NetCfg, showAlertOnError: Boolean = true, ctrl: ChatCtrl? = null): Boolean {
|
||||
val r = sendCmd(null, CC.APISetNetworkConfig(cfg), ctrl)
|
||||
return when (r) {
|
||||
is CR.CmdOk -> true
|
||||
else -> {
|
||||
Log.e(TAG, "apiSetNetworkConfig bad response: ${r.responseType} ${r.details}")
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.error_setting_network_config),
|
||||
"${r.responseType}: ${r.details}"
|
||||
)
|
||||
if (showAlertOnError) {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.error_setting_network_config),
|
||||
"${r.responseType}: ${r.details}"
|
||||
)
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -2745,13 +2762,9 @@ object ChatController {
|
||||
|
||||
fun getNetCfg(): NetCfg {
|
||||
val useSocksProxy = appPrefs.networkUseSocksProxy.get()
|
||||
val proxyHostPort = appPrefs.networkProxyHostPort.get()
|
||||
val networkProxy = appPrefs.networkProxy.get()
|
||||
val socksProxy = if (useSocksProxy) {
|
||||
if (proxyHostPort?.startsWith("localhost:") == true) {
|
||||
proxyHostPort.removePrefix("localhost")
|
||||
} else {
|
||||
proxyHostPort ?: ":9050"
|
||||
}
|
||||
networkProxy.toProxyString()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@@ -2793,7 +2806,7 @@ object ChatController {
|
||||
}
|
||||
|
||||
/**
|
||||
* [AppPreferences.networkProxyHostPort] is not changed here, use appPrefs to set it
|
||||
* [AppPreferences.networkProxy] is not changed here, use appPrefs to set it
|
||||
* */
|
||||
fun setNetCfg(cfg: NetCfg) {
|
||||
appPrefs.networkUseSocksProxy.set(cfg.useSocksProxy)
|
||||
@@ -3529,13 +3542,8 @@ data class NetCfg(
|
||||
val useSocksProxy: Boolean get() = socksProxy != null
|
||||
val enableKeepAlive: Boolean get() = tcpKeepAlive != null
|
||||
|
||||
fun withHostPort(hostPort: String?, default: String? = ":9050"): NetCfg {
|
||||
val socksProxy = if (hostPort?.startsWith("localhost:") == true) {
|
||||
hostPort.removePrefix("localhost")
|
||||
} else {
|
||||
hostPort ?: default
|
||||
}
|
||||
return copy(socksProxy = socksProxy)
|
||||
fun withProxy(proxy: NetworkProxy?, default: String? = ":9050"): NetCfg {
|
||||
return copy(socksProxy = proxy?.toProxyString() ?: default)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -3577,6 +3585,39 @@ data class NetCfg(
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class NetworkProxy(
|
||||
val username: String = "",
|
||||
val password: String = "",
|
||||
val auth: NetworkProxyAuth = NetworkProxyAuth.ISOLATE,
|
||||
val host: String = "localhost",
|
||||
val port: Int = 9050
|
||||
) {
|
||||
fun toProxyString(): String {
|
||||
var res = ""
|
||||
if (auth == NetworkProxyAuth.USERNAME && (username.isNotBlank() || password.isNotBlank())) {
|
||||
res += username.trim() + ":" + password.trim() + "@"
|
||||
} else if (auth == NetworkProxyAuth.USERNAME) {
|
||||
res += "@"
|
||||
}
|
||||
if (host != "localhost") {
|
||||
res += if (host.contains(':')) "[${host.trim(' ', '[', ']')}]" else host.trim()
|
||||
}
|
||||
if (port != 9050 || res.isEmpty()) {
|
||||
res += ":$port"
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
enum class NetworkProxyAuth {
|
||||
@SerialName("isolate")
|
||||
ISOLATE,
|
||||
@SerialName("username")
|
||||
USERNAME,
|
||||
}
|
||||
|
||||
enum class OnionHosts {
|
||||
NEVER, PREFER, REQUIRED
|
||||
}
|
||||
@@ -5316,6 +5357,7 @@ abstract class TerminalItem {
|
||||
val date: Instant = Clock.System.now()
|
||||
abstract val label: String
|
||||
abstract val details: String
|
||||
val createdAtNanos: Long = System.nanoTime()
|
||||
|
||||
class Cmd(override val id: Long, override val remoteHostId: Long?, val cmd: CC): TerminalItem() {
|
||||
override val label get() = "> ${cmd.cmdString}"
|
||||
@@ -6138,6 +6180,7 @@ enum class NotificationsMode() {
|
||||
@Serializable
|
||||
data class AppSettings(
|
||||
var networkConfig: NetCfg? = null,
|
||||
var networkProxy: NetworkProxy? = null,
|
||||
var privacyEncryptLocalFiles: Boolean? = null,
|
||||
var privacyAskToApproveRelays: Boolean? = null,
|
||||
var privacyAcceptImages: Boolean? = null,
|
||||
@@ -6169,6 +6212,7 @@ data class AppSettings(
|
||||
val empty = AppSettings()
|
||||
val def = defaults
|
||||
if (networkConfig != def.networkConfig) { empty.networkConfig = networkConfig }
|
||||
if (networkProxy != def.networkProxy) { empty.networkProxy = networkProxy }
|
||||
if (privacyEncryptLocalFiles != def.privacyEncryptLocalFiles) { empty.privacyEncryptLocalFiles = privacyEncryptLocalFiles }
|
||||
if (privacyAskToApproveRelays != def.privacyAskToApproveRelays) { empty.privacyAskToApproveRelays = privacyAskToApproveRelays }
|
||||
if (privacyAcceptImages != def.privacyAcceptImages) { empty.privacyAcceptImages = privacyAcceptImages }
|
||||
@@ -6206,8 +6250,12 @@ data class AppSettings(
|
||||
if (net.hostMode == HostMode.Onion) {
|
||||
net = net.copy(hostMode = HostMode.Public, requiredHostMode = true)
|
||||
}
|
||||
if (net.socksProxy != null) {
|
||||
net = net.copy(socksProxy = networkProxy?.toProxyString())
|
||||
}
|
||||
setNetCfg(net)
|
||||
}
|
||||
networkProxy?.let { def.networkProxy.set(it) }
|
||||
privacyEncryptLocalFiles?.let { def.privacyEncryptLocalFiles.set(it) }
|
||||
privacyAskToApproveRelays?.let { def.privacyAskToApproveRelays.set(it) }
|
||||
privacyAcceptImages?.let { def.privacyAcceptImages.set(it) }
|
||||
@@ -6240,6 +6288,7 @@ data class AppSettings(
|
||||
val defaults: AppSettings
|
||||
get() = AppSettings(
|
||||
networkConfig = NetCfg.defaults,
|
||||
networkProxy = null,
|
||||
privacyEncryptLocalFiles = true,
|
||||
privacyAskToApproveRelays = true,
|
||||
privacyAcceptImages = true,
|
||||
@@ -6273,6 +6322,7 @@ data class AppSettings(
|
||||
val def = appPreferences
|
||||
return defaults.copy(
|
||||
networkConfig = getNetCfg(),
|
||||
networkProxy = def.networkProxy.get(),
|
||||
privacyEncryptLocalFiles = def.privacyEncryptLocalFiles.get(),
|
||||
privacyAskToApproveRelays = def.privacyAskToApproveRelays.get(),
|
||||
privacyAcceptImages = def.privacyAcceptImages.get(),
|
||||
|
||||
+1
-1
@@ -132,7 +132,7 @@ fun TerminalLog() {
|
||||
}
|
||||
val clipboard = LocalClipboardManager.current
|
||||
LazyColumnWithScrollBar(reverseLayout = true) {
|
||||
items(reversedTerminalItems) { item ->
|
||||
items(reversedTerminalItems, key = { item -> item.id to item.createdAtNanos }) { item ->
|
||||
val rhId = item.remoteHostId
|
||||
val rhIdStr = if (rhId == null) "" else "$rhId "
|
||||
Text(
|
||||
|
||||
+8
-2
@@ -105,6 +105,7 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> {
|
||||
val perChatTheme = remember(chatInfo, CurrentColors.value.base) { if (chatInfo is ChatInfo.Direct) chatInfo.contact.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else null }
|
||||
val overrides = if (perChatTheme != null) ThemeManager.currentColors(null, perChatTheme, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) else null
|
||||
val fullDeleteAllowed = remember(chatInfo) { chatInfo.featureEnabled(ChatFeature.FullDelete) }
|
||||
SimpleXThemeOverride(overrides ?: CurrentColors.collectAsState().value) {
|
||||
ChatLayout(
|
||||
remoteHostId = remoteHostId,
|
||||
@@ -142,10 +143,15 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
|
||||
chatInfo = chatInfo,
|
||||
deleteItems = { canDeleteForAll ->
|
||||
val itemIds = selectedChatItems.value
|
||||
val questionText =
|
||||
if (!canDeleteForAll || fullDeleteAllowed || chatInfo is ChatInfo.Local)
|
||||
generalGetString(MR.strings.delete_messages_cannot_be_undone_warning)
|
||||
else
|
||||
generalGetString(MR.strings.delete_messages_mark_deleted_warning)
|
||||
if (itemIds != null) {
|
||||
deleteMessagesAlertDialog(
|
||||
itemIds.sorted(),
|
||||
generalGetString(if (itemIds.size == 1) MR.strings.delete_message_mark_deleted_warning else MR.strings.delete_messages_mark_deleted_warning),
|
||||
questionText = questionText,
|
||||
forAll = canDeleteForAll,
|
||||
deleteMessages = { ids, forAll ->
|
||||
deleteMessages(chatRh, chatInfo, ids, forAll, moderate = false) {
|
||||
@@ -998,7 +1004,7 @@ fun BoxWithConstraintsScope.ChatItemsList(
|
||||
}
|
||||
)
|
||||
LazyColumnWithScrollBar(Modifier.align(Alignment.BottomCenter), state = listState, reverseLayout = true) {
|
||||
itemsIndexed(reversedChatItems, key = { _, item -> item.id }) { i, cItem ->
|
||||
itemsIndexed(reversedChatItems, key = { _, item -> item.id to item.meta.createdAt.toEpochMilliseconds() }) { i, cItem ->
|
||||
CompositionLocalProvider(
|
||||
// Makes horizontal and vertical scrolling to coexist nicely.
|
||||
// With default touchSlop when you scroll LazyColumn, you can unintentionally open reply view
|
||||
|
||||
+5
-6
@@ -40,7 +40,7 @@ import kotlinx.coroutines.launch
|
||||
const val SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20
|
||||
|
||||
@Composable
|
||||
fun GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit, close: () -> Unit, onSearchClicked: () -> Unit) {
|
||||
fun ModalData.GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: String, groupLink: String?, groupLinkMemberRole: GroupMemberRole?, onGroupLinkUpdated: (Pair<String, GroupMemberRole>?) -> Unit, close: () -> Unit, onSearchClicked: () -> Unit) {
|
||||
BackHandler(onBack = close)
|
||||
// TODO derivedStateOf?
|
||||
val chat = chatModel.chats.value.firstOrNull { ch -> ch.id == chatId && ch.remoteHostId == rhId }
|
||||
@@ -249,9 +249,8 @@ fun AddGroupMembersButton(
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
fun GroupChatInfoLayout(
|
||||
fun ModalData.GroupChatInfoLayout(
|
||||
chat: Chat,
|
||||
groupInfo: GroupInfo,
|
||||
currentUser: User,
|
||||
@@ -272,12 +271,12 @@ fun GroupChatInfoLayout(
|
||||
close: () -> Unit = { ModalManager.closeAllModalsEverywhere()},
|
||||
onSearchClicked: () -> Unit
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val listState = remember { appBarHandler.listState }
|
||||
val scope = rememberCoroutineScope()
|
||||
KeyChangeEffect(chat.id) {
|
||||
scope.launch { listState.scrollToItem(0) }
|
||||
}
|
||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue()) }
|
||||
val searchText = remember { stateGetOrPut("searchText") { TextFieldValue() } }
|
||||
val filteredMembers = remember(members) {
|
||||
derivedStateOf {
|
||||
val s = searchText.value.text.trim().lowercase()
|
||||
@@ -674,7 +673,7 @@ private fun SearchRowView(
|
||||
@Composable
|
||||
fun PreviewGroupChatInfoLayout() {
|
||||
SimpleXTheme {
|
||||
GroupChatInfoLayout(
|
||||
ModalData().GroupChatInfoLayout(
|
||||
chat = Chat(
|
||||
remoteHostId = null,
|
||||
chatInfo = ChatInfo.Direct.sampleData,
|
||||
|
||||
+2
-2
@@ -135,7 +135,7 @@ fun ChatItemView(
|
||||
}
|
||||
|
||||
fun deleteMessageQuestionText(): String {
|
||||
return if (!sent || fullDeleteAllowed) {
|
||||
return if (!sent || fullDeleteAllowed || cInfo is ChatInfo.Local) {
|
||||
generalGetString(MR.strings.delete_message_cannot_be_undone_warning)
|
||||
} else {
|
||||
generalGetString(MR.strings.delete_message_mark_deleted_warning)
|
||||
@@ -637,7 +637,7 @@ fun DeleteItemAction(
|
||||
}
|
||||
deleteMessagesAlertDialog(
|
||||
itemIds,
|
||||
generalGetString(if (itemIds.size == 1) MR.strings.delete_message_mark_deleted_warning else MR.strings.delete_messages_mark_deleted_warning),
|
||||
generalGetString(MR.strings.delete_messages_cannot_be_undone_warning),
|
||||
forAll = false,
|
||||
deleteMessages = { ids, _ -> deleteMessages(ids) }
|
||||
)
|
||||
|
||||
+1
-1
@@ -186,7 +186,7 @@ fun ErrorChatListItem() {
|
||||
|
||||
suspend fun directChatAction(rhId: Long?, contact: Contact, chatModel: ChatModel) {
|
||||
when {
|
||||
contact.activeConn == null && contact.profile.contactLink != null -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close = null, openChat = true)
|
||||
contact.activeConn == null && contact.profile.contactLink != null && contact.active -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close = null, openChat = true)
|
||||
else -> openChat(rhId, ChatInfo.Direct(contact), chatModel)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -356,7 +356,8 @@ sealed class WallpaperType {
|
||||
private fun drawToBitmap(image: ImageBitmap, imageScale: Float, tint: Color, size: Size, density: Float, layoutDirection: LayoutDirection): ImageBitmap {
|
||||
val quality = if (appPlatform.isAndroid) FilterQuality.High else FilterQuality.Low
|
||||
val drawScope = CanvasDrawScope()
|
||||
val bitmap = ImageBitmap(size.width.toInt(), size.height.toInt())
|
||||
// Don't allow to make zero size because it crashes the app when reducing a size of a window on desktop
|
||||
val bitmap = ImageBitmap(size.width.toInt().coerceAtLeast(1), size.height.toInt().coerceAtLeast(1))
|
||||
val canvas = Canvas(bitmap)
|
||||
drawScope.draw(
|
||||
density = Density(density),
|
||||
|
||||
-6
@@ -71,12 +71,6 @@ fun SearchTextField(
|
||||
}
|
||||
}
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
if (searchText.value.text.isNotEmpty()) onValueChange("")
|
||||
}
|
||||
}
|
||||
|
||||
val colors = TextFieldDefaults.textFieldColors(
|
||||
backgroundColor = Color.Unspecified,
|
||||
textColor = MaterialTheme.colors.onBackground,
|
||||
|
||||
+19
-7
@@ -4,7 +4,6 @@ import SectionBottomSpacer
|
||||
import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
@@ -17,6 +16,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,7 +38,6 @@ import kotlinx.serialization.*
|
||||
import java.io.File
|
||||
import java.net.URLEncoder
|
||||
import kotlin.math.max
|
||||
import kotlin.math.sqrt
|
||||
|
||||
@Serializable
|
||||
data class MigrationFileLinkData(
|
||||
@@ -46,16 +45,20 @@ data class MigrationFileLinkData(
|
||||
) {
|
||||
@Serializable
|
||||
data class NetworkConfig(
|
||||
val socksProxy: String?,
|
||||
// Legacy. Remove in 2025
|
||||
@SerialName("socksProxy")
|
||||
val legacySocksProxy: String?,
|
||||
val networkProxy: NetworkProxy?,
|
||||
val hostMode: HostMode?,
|
||||
val requiredHostMode: Boolean?
|
||||
) {
|
||||
fun hasOnionConfigured(): Boolean = socksProxy != null || hostMode == HostMode.Onion
|
||||
fun hasProxyConfigured(): Boolean = networkProxy != null || legacySocksProxy != null || hostMode == HostMode.Onion
|
||||
|
||||
fun transformToPlatformSupported(): NetworkConfig {
|
||||
return if (hostMode != null && requiredHostMode != null) {
|
||||
NetworkConfig(
|
||||
socksProxy = if (hostMode == HostMode.Onion) socksProxy ?: NetCfg.proxyDefaults.socksProxy else socksProxy,
|
||||
legacySocksProxy = if (hostMode == HostMode.Onion) legacySocksProxy ?: NetCfg.proxyDefaults.socksProxy else legacySocksProxy,
|
||||
networkProxy = if (hostMode == HostMode.Onion) networkProxy ?: NetworkProxy() else networkProxy,
|
||||
hostMode = if (hostMode == HostMode.Onion) HostMode.OnionViaSocks else hostMode,
|
||||
requiredHostMode = requiredHostMode
|
||||
)
|
||||
@@ -349,7 +352,15 @@ private fun MutableState<MigrationFromState>.LinkShownView(fileId: Long, link: S
|
||||
text = stringResource(MR.strings.migrate_from_device_finalize_migration),
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
click = {
|
||||
finishMigration(fileId, ctrl)
|
||||
AlertManager.shared.showAlertDialog(
|
||||
title = generalGetString(MR.strings.migrate_from_device_remove_archive_question),
|
||||
text = generalGetString(MR.strings.migrate_from_device_uploaded_archive_will_be_removed),
|
||||
confirmText = generalGetString(MR.strings.continue_to_next_step),
|
||||
destructive = true,
|
||||
onConfirm = {
|
||||
finishMigration(fileId, ctrl)
|
||||
}
|
||||
)
|
||||
}
|
||||
) {}
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_archive_will_be_deleted))
|
||||
@@ -562,7 +573,8 @@ private fun MutableState<MigrationFromState>.startUploading(
|
||||
val cfg = getNetCfg()
|
||||
val data = MigrationFileLinkData(
|
||||
networkConfig = MigrationFileLinkData.NetworkConfig(
|
||||
socksProxy = cfg.socksProxy,
|
||||
legacySocksProxy = null,
|
||||
networkProxy = if (appPrefs.networkUseSocksProxy.get()) appPrefs.networkProxy.get() else null,
|
||||
hostMode = cfg.hostMode,
|
||||
requiredHostMode = cfg.requiredHostMode
|
||||
)
|
||||
|
||||
+98
-87
@@ -5,16 +5,15 @@ import SectionItemView
|
||||
import SectionSpacer
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.platform.LocalClipboardManager
|
||||
import chat.simplex.common.model.*
|
||||
import chat.simplex.common.model.AppPreferences.Companion.SHARED_PREFS_MIGRATION_TO_STAGE
|
||||
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.ChatCtrl
|
||||
@@ -41,10 +40,10 @@ import kotlin.math.max
|
||||
|
||||
@Serializable
|
||||
sealed class MigrationToDeviceState {
|
||||
@Serializable @SerialName("onion") data class Onion(val link: String, val socksProxy: String?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationToDeviceState()
|
||||
@Serializable @SerialName("downloadProgress") data class DownloadProgress(val link: String, val archiveName: String, val netCfg: NetCfg): MigrationToDeviceState()
|
||||
@Serializable @SerialName("archiveImport") data class ArchiveImport(val archiveName: String, val netCfg: NetCfg): MigrationToDeviceState()
|
||||
@Serializable @SerialName("passphrase") data class Passphrase(val netCfg: NetCfg): MigrationToDeviceState()
|
||||
@Serializable @SerialName("onion") data class Onion(val link: String, val socksProxy: String?, val networkProxy: NetworkProxy?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationToDeviceState()
|
||||
@Serializable @SerialName("downloadProgress") data class DownloadProgress(val link: String, val archiveName: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToDeviceState()
|
||||
@Serializable @SerialName("archiveImport") data class ArchiveImport(val archiveName: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToDeviceState()
|
||||
@Serializable @SerialName("passphrase") data class Passphrase(val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToDeviceState()
|
||||
|
||||
companion object {
|
||||
// Here we check whether it's needed to show migration process after app restart or not
|
||||
@@ -66,10 +65,10 @@ sealed class MigrationToDeviceState {
|
||||
null
|
||||
} else {
|
||||
val archivePath = File(getMigrationTempFilesDirectory(), state.archiveName)
|
||||
MigrationToState.ArchiveImportFailed(archivePath.absolutePath, state.netCfg)
|
||||
MigrationToState.ArchiveImportFailed(archivePath.absolutePath, state.netCfg, state.networkProxy)
|
||||
}
|
||||
}
|
||||
is Passphrase -> MigrationToState.Passphrase("", state.netCfg)
|
||||
is Passphrase -> MigrationToState.Passphrase("", state.netCfg, state.networkProxy)
|
||||
}
|
||||
if (initial == null) {
|
||||
settings.remove(SHARED_PREFS_MIGRATION_TO_STAGE)
|
||||
@@ -91,16 +90,24 @@ sealed class MigrationToDeviceState {
|
||||
@Serializable
|
||||
sealed class MigrationToState {
|
||||
@Serializable object PasteOrScanLink: MigrationToState()
|
||||
@Serializable data class Onion(val link: String, val socksProxy: String?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationToState()
|
||||
@Serializable data class DatabaseInit(val link: String, val netCfg: NetCfg): MigrationToState()
|
||||
@Serializable data class LinkDownloading(val link: String, val ctrl: ChatCtrl, val user: User, val archivePath: String, val netCfg: NetCfg): MigrationToState()
|
||||
@Serializable data class DownloadProgress(val downloadedBytes: Long, val totalBytes: Long, val fileId: Long, val link: String, val archivePath: String, val netCfg: NetCfg, val ctrl: ChatCtrl?): MigrationToState()
|
||||
@Serializable data class DownloadFailed(val totalBytes: Long, val link: String, val archivePath: String, val netCfg: NetCfg): MigrationToState()
|
||||
@Serializable data class ArchiveImport(val archivePath: String, val netCfg: NetCfg): MigrationToState()
|
||||
@Serializable data class ArchiveImportFailed(val archivePath: String, val netCfg: NetCfg): MigrationToState()
|
||||
@Serializable data class Passphrase(val passphrase: String, val netCfg: NetCfg): MigrationToState()
|
||||
@Serializable data class MigrationConfirmation(val status: DBMigrationResult, val passphrase: String, val useKeychain: Boolean, val netCfg: NetCfg): MigrationToState()
|
||||
@Serializable data class Migration(val passphrase: String, val confirmation: chat.simplex.common.views.helpers.MigrationConfirmation, val useKeychain: Boolean, val netCfg: NetCfg): MigrationToState()
|
||||
@Serializable data class Onion(
|
||||
val link: String,
|
||||
// Legacy, remove in 2025
|
||||
@SerialName("socksProxy")
|
||||
val legacySocksProxy: String?,
|
||||
val networkProxy: NetworkProxy?,
|
||||
val hostMode: HostMode,
|
||||
val requiredHostMode: Boolean
|
||||
): MigrationToState()
|
||||
@Serializable data class DatabaseInit(val link: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
|
||||
@Serializable data class LinkDownloading(val link: String, val ctrl: ChatCtrl, val user: User, val archivePath: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
|
||||
@Serializable data class DownloadProgress(val downloadedBytes: Long, val totalBytes: Long, val fileId: Long, val link: String, val archivePath: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?, val ctrl: ChatCtrl?): MigrationToState()
|
||||
@Serializable data class DownloadFailed(val totalBytes: Long, val link: String, val archivePath: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
|
||||
@Serializable data class ArchiveImport(val archivePath: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
|
||||
@Serializable data class ArchiveImportFailed(val archivePath: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
|
||||
@Serializable data class Passphrase(val passphrase: String, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
|
||||
@Serializable data class MigrationConfirmation(val status: DBMigrationResult, val passphrase: String, val useKeychain: Boolean, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
|
||||
@Serializable data class Migration(val passphrase: String, val confirmation: chat.simplex.common.views.helpers.MigrationConfirmation, val useKeychain: Boolean, val netCfg: NetCfg, val networkProxy: NetworkProxy?): MigrationToState()
|
||||
}
|
||||
|
||||
private var MutableState<MigrationToState?>.state: MigrationToState?
|
||||
@@ -175,16 +182,16 @@ private fun ModalData.SectionByState(
|
||||
when (val s = migrationState.value) {
|
||||
null -> {}
|
||||
is MigrationToState.PasteOrScanLink -> migrationState.PasteOrScanLinkView()
|
||||
is MigrationToState.Onion -> OnionView(s.link, s.socksProxy, s.hostMode, s.requiredHostMode, migrationState)
|
||||
is MigrationToState.DatabaseInit -> migrationState.DatabaseInitView(s.link, tempDatabaseFile, s.netCfg)
|
||||
is MigrationToState.LinkDownloading -> migrationState.LinkDownloadingView(s.link, s.ctrl, s.user, s.archivePath, tempDatabaseFile, chatReceiver, s.netCfg)
|
||||
is MigrationToState.Onion -> OnionView(s.link, s.legacySocksProxy, s.networkProxy, s.hostMode, s.requiredHostMode, migrationState)
|
||||
is MigrationToState.DatabaseInit -> migrationState.DatabaseInitView(s.link, tempDatabaseFile, s.netCfg, s.networkProxy)
|
||||
is MigrationToState.LinkDownloading -> migrationState.LinkDownloadingView(s.link, s.ctrl, s.user, s.archivePath, tempDatabaseFile, chatReceiver, s.netCfg, s.networkProxy)
|
||||
is MigrationToState.DownloadProgress -> DownloadProgressView(s.downloadedBytes, totalBytes = s.totalBytes)
|
||||
is MigrationToState.DownloadFailed -> migrationState.DownloadFailedView(s.link, chatReceiver.value, s.archivePath, s.netCfg)
|
||||
is MigrationToState.ArchiveImport -> migrationState.ArchiveImportView(s.archivePath, s.netCfg)
|
||||
is MigrationToState.ArchiveImportFailed -> migrationState.ArchiveImportFailedView(s.archivePath, s.netCfg)
|
||||
is MigrationToState.Passphrase -> migrationState.PassphraseEnteringView(currentKey = s.passphrase, s.netCfg)
|
||||
is MigrationToState.MigrationConfirmation -> migrationState.MigrationConfirmationView(s.status, s.passphrase, s.useKeychain, s.netCfg)
|
||||
is MigrationToState.Migration -> MigrationView(s.passphrase, s.confirmation, s.useKeychain, s.netCfg, close)
|
||||
is MigrationToState.DownloadFailed -> migrationState.DownloadFailedView(s.link, chatReceiver.value, s.archivePath, s.netCfg, s.networkProxy)
|
||||
is MigrationToState.ArchiveImport -> migrationState.ArchiveImportView(s.archivePath, s.netCfg, s.networkProxy)
|
||||
is MigrationToState.ArchiveImportFailed -> migrationState.ArchiveImportFailedView(s.archivePath, s.netCfg, s.networkProxy)
|
||||
is MigrationToState.Passphrase -> migrationState.PassphraseEnteringView(currentKey = s.passphrase, s.netCfg, s.networkProxy)
|
||||
is MigrationToState.MigrationConfirmation -> migrationState.MigrationConfirmationView(s.status, s.passphrase, s.useKeychain, s.netCfg, s.networkProxy)
|
||||
is MigrationToState.Migration -> MigrationView(s.passphrase, s.confirmation, s.useKeychain, s.netCfg, s.networkProxy, close)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,10 +206,8 @@ private fun MutableState<MigrationToState?>.PasteOrScanLinkView() {
|
||||
SectionSpacer()
|
||||
}
|
||||
|
||||
if (appPlatform.isDesktop || appPreferences.developerTools.get()) {
|
||||
SectionView(stringResource(if (appPlatform.isAndroid) MR.strings.or_paste_archive_link else MR.strings.paste_archive_link).uppercase()) {
|
||||
PasteLinkView()
|
||||
}
|
||||
SectionView(stringResource(if (appPlatform.isAndroid) MR.strings.or_paste_archive_link else MR.strings.paste_archive_link).uppercase()) {
|
||||
PasteLinkView()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,21 +223,24 @@ private fun MutableState<MigrationToState?>.PasteLinkView() {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: HostMode, requiredHostMode: Boolean, state: MutableState<MigrationToState?>) {
|
||||
private fun ModalData.OnionView(link: String, legacyLinkSocksProxy: String?, linkNetworkProxy: NetworkProxy?, hostMode: HostMode, requiredHostMode: Boolean, state: MutableState<MigrationToState?>) {
|
||||
val onionHosts = remember { stateGetOrPut("onionHosts") {
|
||||
getNetCfg().copy(socksProxy = socksProxy, hostMode = hostMode, requiredHostMode = requiredHostMode).onionHosts
|
||||
getNetCfg().copy(socksProxy = linkNetworkProxy?.toProxyString() ?: legacyLinkSocksProxy, hostMode = hostMode, requiredHostMode = requiredHostMode).onionHosts
|
||||
} }
|
||||
val networkUseSocksProxy = remember { stateGetOrPut("networkUseSocksProxy") { socksProxy != null } }
|
||||
val networkUseSocksProxy = remember { stateGetOrPut("networkUseSocksProxy") { linkNetworkProxy != null || legacyLinkSocksProxy != null } }
|
||||
val sessionMode = remember { stateGetOrPut("sessionMode") { TransportSessionMode.User} }
|
||||
val networkProxyHostPort = remember { stateGetOrPut("networkHostProxyPort") {
|
||||
var proxy = (socksProxy ?: chatModel.controller.appPrefs.networkProxyHostPort.get())
|
||||
if (proxy?.startsWith(":") == true) proxy = "localhost$proxy"
|
||||
proxy
|
||||
}
|
||||
val networkProxy = remember { stateGetOrPut("networkProxy") {
|
||||
linkNetworkProxy
|
||||
?: if (legacyLinkSocksProxy != null) {
|
||||
NetworkProxy(host = legacyLinkSocksProxy.substringBefore(":").ifBlank { "localhost" }, port = legacyLinkSocksProxy.substringAfter(":").toIntOrNull() ?: 9050)
|
||||
} else {
|
||||
appPrefs.networkProxy.get()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val netCfg = rememberSaveable(stateSaver = serializableSaver()) {
|
||||
mutableStateOf(getNetCfg().withOnionHosts(onionHosts.value).copy(socksProxy = socksProxy, sessionMode = sessionMode.value))
|
||||
mutableStateOf(getNetCfg().withOnionHosts(onionHosts.value).copy(socksProxy = linkNetworkProxy?.toProxyString() ?: legacyLinkSocksProxy, sessionMode = sessionMode.value))
|
||||
}
|
||||
|
||||
SectionView(stringResource(MR.strings.migrate_to_device_confirm_network_settings).uppercase()) {
|
||||
@@ -243,12 +251,12 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos
|
||||
click = {
|
||||
val updated = netCfg.value
|
||||
.withOnionHosts(onionHosts.value)
|
||||
.withHostPort(if (networkUseSocksProxy.value) networkProxyHostPort.value else null, null)
|
||||
.withProxy(if (networkUseSocksProxy.value) networkProxy.value else null, null)
|
||||
.copy(
|
||||
sessionMode = sessionMode.value
|
||||
)
|
||||
withBGApi {
|
||||
state.value = MigrationToState.DatabaseInit(link, updated)
|
||||
state.value = MigrationToState.DatabaseInit(link, updated, if (networkUseSocksProxy.value) networkProxy.value else null)
|
||||
}
|
||||
}
|
||||
){}
|
||||
@@ -257,8 +265,8 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos
|
||||
|
||||
SectionSpacer()
|
||||
|
||||
val networkProxyHostPortPref = SharedPreference(get = { networkProxyHostPort.value }, set = {
|
||||
networkProxyHostPort.value = it
|
||||
val networkProxyPref = SharedPreference(get = { networkProxy.value }, set = {
|
||||
networkProxy.value = it
|
||||
})
|
||||
SectionView(stringResource(MR.strings.network_settings_title).uppercase()) {
|
||||
OnionRelatedLayout(
|
||||
@@ -266,13 +274,10 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos
|
||||
networkUseSocksProxy,
|
||||
onionHosts,
|
||||
sessionMode,
|
||||
networkProxyHostPortPref,
|
||||
networkProxyPref,
|
||||
toggleSocksProxy = { enable ->
|
||||
networkUseSocksProxy.value = enable
|
||||
},
|
||||
useOnion = {
|
||||
onionHosts.value = it
|
||||
},
|
||||
updateSessionMode = {
|
||||
sessionMode.value = it
|
||||
}
|
||||
@@ -281,13 +286,13 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationToState?>.DatabaseInitView(link: String, tempDatabaseFile: File, netCfg: NetCfg) {
|
||||
private fun MutableState<MigrationToState?>.DatabaseInitView(link: String, tempDatabaseFile: File, netCfg: NetCfg, networkProxy: NetworkProxy?) {
|
||||
Box {
|
||||
SectionView(stringResource(MR.strings.migrate_to_device_database_init).uppercase()) {}
|
||||
ProgressView()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
prepareDatabase(link, tempDatabaseFile, netCfg)
|
||||
prepareDatabase(link, tempDatabaseFile, netCfg, networkProxy)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -299,14 +304,15 @@ private fun MutableState<MigrationToState?>.LinkDownloadingView(
|
||||
archivePath: String,
|
||||
tempDatabaseFile: File,
|
||||
chatReceiver: MutableState<MigrationToChatReceiver?>,
|
||||
netCfg: NetCfg
|
||||
netCfg: NetCfg,
|
||||
networkProxy: NetworkProxy?
|
||||
) {
|
||||
Box {
|
||||
SectionView(stringResource(MR.strings.migrate_to_device_downloading_details).uppercase()) {}
|
||||
ProgressView()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
startDownloading(0, ctrl, user, tempDatabaseFile, chatReceiver, link, archivePath, netCfg)
|
||||
startDownloading(0, ctrl, user, tempDatabaseFile, chatReceiver, link, archivePath, netCfg, networkProxy)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,14 +327,14 @@ private fun DownloadProgressView(downloadedBytes: Long, totalBytes: Long) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationToState?>.DownloadFailedView(link: String, chatReceiver: MigrationToChatReceiver?, archivePath: String, netCfg: NetCfg) {
|
||||
private fun MutableState<MigrationToState?>.DownloadFailedView(link: String, chatReceiver: MigrationToChatReceiver?, archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
|
||||
SectionView(stringResource(MR.strings.migrate_to_device_download_failed).uppercase()) {
|
||||
SettingsActionItemWithContent(
|
||||
icon = painterResource(MR.images.ic_download),
|
||||
text = stringResource(MR.strings.migrate_to_device_repeat_download),
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
click = {
|
||||
state = MigrationToState.DatabaseInit(link, netCfg)
|
||||
state = MigrationToState.DatabaseInit(link, netCfg, networkProxy)
|
||||
}
|
||||
) {}
|
||||
SectionTextFooter(stringResource(MR.strings.migrate_to_device_try_again))
|
||||
@@ -341,25 +347,25 @@ private fun MutableState<MigrationToState?>.DownloadFailedView(link: String, cha
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationToState?>.ArchiveImportView(archivePath: String, netCfg: NetCfg) {
|
||||
private fun MutableState<MigrationToState?>.ArchiveImportView(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
|
||||
Box {
|
||||
SectionView(stringResource(MR.strings.migrate_to_device_importing_archive).uppercase()) {}
|
||||
ProgressView()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
importArchive(archivePath, netCfg)
|
||||
importArchive(archivePath, netCfg, networkProxy)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationToState?>.ArchiveImportFailedView(archivePath: String, netCfg: NetCfg) {
|
||||
private fun MutableState<MigrationToState?>.ArchiveImportFailedView(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
|
||||
SectionView(stringResource(MR.strings.migrate_to_device_import_failed).uppercase()) {
|
||||
SettingsActionItemWithContent(
|
||||
icon = painterResource(MR.images.ic_download),
|
||||
text = stringResource(MR.strings.migrate_to_device_repeat_import),
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
click = {
|
||||
state = MigrationToState.ArchiveImport(archivePath, netCfg)
|
||||
state = MigrationToState.ArchiveImport(archivePath, netCfg, networkProxy)
|
||||
}
|
||||
) {}
|
||||
SectionTextFooter(stringResource(MR.strings.migrate_to_device_try_again))
|
||||
@@ -367,7 +373,7 @@ private fun MutableState<MigrationToState?>.ArchiveImportFailedView(archivePath:
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationToState?>.PassphraseEnteringView(currentKey: String, netCfg: NetCfg) {
|
||||
private fun MutableState<MigrationToState?>.PassphraseEnteringView(currentKey: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
|
||||
val currentKey = rememberSaveable { mutableStateOf(currentKey) }
|
||||
val verifyingPassphrase = rememberSaveable { mutableStateOf(false) }
|
||||
val useKeychain = rememberSaveable { mutableStateOf(appPreferences.storeDBPassphrase.get()) }
|
||||
@@ -397,9 +403,9 @@ private fun MutableState<MigrationToState?>.PassphraseEnteringView(currentKey: S
|
||||
val (status, _) = chatInitTemporaryDatabase(dbAbsolutePrefixPath, key = currentKey.value, confirmation = MigrationConfirmation.YesUp)
|
||||
val success = status == DBMigrationResult.OK || status == DBMigrationResult.InvalidConfirmation
|
||||
if (success) {
|
||||
state = MigrationToState.Migration(currentKey.value, MigrationConfirmation.YesUp, useKeychain.value, netCfg)
|
||||
state = MigrationToState.Migration(currentKey.value, MigrationConfirmation.YesUp, useKeychain.value, netCfg, networkProxy)
|
||||
} else if (status is DBMigrationResult.ErrorMigration) {
|
||||
state = MigrationToState.MigrationConfirmation(status, currentKey.value, useKeychain.value, netCfg)
|
||||
state = MigrationToState.MigrationConfirmation(status, currentKey.value, useKeychain.value, netCfg, networkProxy)
|
||||
} else {
|
||||
showErrorOnMigrationIfNeeded(status)
|
||||
}
|
||||
@@ -416,7 +422,7 @@ private fun MutableState<MigrationToState?>.PassphraseEnteringView(currentKey: S
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MutableState<MigrationToState?>.MigrationConfirmationView(status: DBMigrationResult, passphrase: String, useKeychain: Boolean, netCfg: NetCfg) {
|
||||
private fun MutableState<MigrationToState?>.MigrationConfirmationView(status: DBMigrationResult, passphrase: String, useKeychain: Boolean, netCfg: NetCfg, networkProxy: NetworkProxy?) {
|
||||
data class Tuple4<A,B,C,D>(val a: A, val b: B, val c: C, val d: D)
|
||||
val (header: String, button: String?, footer: String, confirmation: MigrationConfirmation?) = when (status) {
|
||||
is DBMigrationResult.ErrorMigration -> when (val err = status.migrationError) {
|
||||
@@ -451,7 +457,7 @@ private fun MutableState<MigrationToState?>.MigrationConfirmationView(status: DB
|
||||
text = button,
|
||||
textColor = MaterialTheme.colors.primary,
|
||||
click = {
|
||||
state = MigrationToState.Migration(passphrase, confirmation, useKeychain, netCfg)
|
||||
state = MigrationToState.Migration(passphrase, confirmation, useKeychain, netCfg, networkProxy)
|
||||
}
|
||||
) {}
|
||||
}
|
||||
@@ -460,13 +466,13 @@ private fun MutableState<MigrationToState?>.MigrationConfirmationView(status: DB
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MigrationView(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, close: () -> Unit) {
|
||||
private fun MigrationView(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, networkProxy: NetworkProxy?, close: () -> Unit) {
|
||||
Box {
|
||||
SectionView(stringResource(MR.strings.migrate_to_device_migrating).uppercase()) {}
|
||||
ProgressView()
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
startChat(passphrase, confirmation, useKeychain, netCfg, close)
|
||||
startChat(passphrase, confirmation, useKeychain, netCfg, networkProxy, close)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,19 +484,21 @@ private fun ProgressView() {
|
||||
private suspend fun MutableState<MigrationToState?>.checkUserLink(link: String) {
|
||||
if (strHasSimplexFileLink(link.trim())) {
|
||||
val data = MigrationFileLinkData.readFromLink(link)
|
||||
val hasOnionConfigured = data?.networkConfig?.hasOnionConfigured() ?: false
|
||||
val hasProxyConfigured = data?.networkConfig?.hasProxyConfigured() ?: false
|
||||
val networkConfig = data?.networkConfig?.transformToPlatformSupported()
|
||||
// If any of iOS or Android had onion enabled, show onion screen
|
||||
if (hasOnionConfigured && networkConfig?.hostMode != null && networkConfig.requiredHostMode != null) {
|
||||
state = MigrationToState.Onion(link.trim(), networkConfig.socksProxy, networkConfig.hostMode, networkConfig.requiredHostMode)
|
||||
MigrationToDeviceState.save(MigrationToDeviceState.Onion(link.trim(), networkConfig.socksProxy, networkConfig.hostMode, networkConfig.requiredHostMode))
|
||||
if (hasProxyConfigured && networkConfig?.hostMode != null && networkConfig.requiredHostMode != null) {
|
||||
state = MigrationToState.Onion(link.trim(), networkConfig.legacySocksProxy, networkConfig.networkProxy, networkConfig.hostMode, networkConfig.requiredHostMode)
|
||||
MigrationToDeviceState.save(MigrationToDeviceState.Onion(link.trim(), networkConfig.legacySocksProxy, networkConfig.networkProxy, networkConfig.hostMode, networkConfig.requiredHostMode))
|
||||
} else {
|
||||
val current = getNetCfg()
|
||||
state = MigrationToState.DatabaseInit(link.trim(), current.copy(
|
||||
socksProxy = networkConfig?.socksProxy,
|
||||
socksProxy = null,
|
||||
hostMode = networkConfig?.hostMode ?: current.hostMode,
|
||||
requiredHostMode = networkConfig?.requiredHostMode ?: current.requiredHostMode
|
||||
))
|
||||
),
|
||||
networkProxy = null
|
||||
)
|
||||
}
|
||||
} else {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
@@ -504,6 +512,7 @@ private fun MutableState<MigrationToState?>.prepareDatabase(
|
||||
link: String,
|
||||
tempDatabaseFile: File,
|
||||
netCfg: NetCfg,
|
||||
networkProxy: NetworkProxy?
|
||||
) {
|
||||
withLongRunningApi {
|
||||
val ctrlAndUser = initTemporaryDatabase(tempDatabaseFile, netCfg)
|
||||
@@ -515,7 +524,7 @@ private fun MutableState<MigrationToState?>.prepareDatabase(
|
||||
}
|
||||
|
||||
val (ctrl, user) = ctrlAndUser
|
||||
state = MigrationToState.LinkDownloading(link, ctrl, user, archivePath(), netCfg)
|
||||
state = MigrationToState.LinkDownloading(link, ctrl, user, archivePath(), netCfg, networkProxy)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,13 +537,14 @@ private fun MutableState<MigrationToState?>.startDownloading(
|
||||
link: String,
|
||||
archivePath: String,
|
||||
netCfg: NetCfg,
|
||||
networkProxy: NetworkProxy?
|
||||
) {
|
||||
withBGApi {
|
||||
chatReceiver.value = MigrationToChatReceiver(ctrl, tempDatabaseFile) { msg ->
|
||||
when (msg) {
|
||||
is CR.RcvFileProgressXFTP -> {
|
||||
state = MigrationToState.DownloadProgress(msg.receivedSize, msg.totalSize, msg.rcvFileTransfer.fileId, link, archivePath, netCfg, ctrl)
|
||||
MigrationToDeviceState.save(MigrationToDeviceState.DownloadProgress(link, File(archivePath).name, netCfg))
|
||||
state = MigrationToState.DownloadProgress(msg.receivedSize, msg.totalSize, msg.rcvFileTransfer.fileId, link, archivePath, netCfg, networkProxy, ctrl)
|
||||
MigrationToDeviceState.save(MigrationToDeviceState.DownloadProgress(link, File(archivePath).name, netCfg, networkProxy))
|
||||
}
|
||||
is CR.RcvStandaloneFileComplete -> {
|
||||
delay(500)
|
||||
@@ -542,8 +552,8 @@ private fun MutableState<MigrationToState?>.startDownloading(
|
||||
if (state == null) {
|
||||
MigrationToDeviceState.save(null)
|
||||
} else {
|
||||
state = MigrationToState.ArchiveImport(archivePath, netCfg)
|
||||
MigrationToDeviceState.save(MigrationToDeviceState.ArchiveImport(File(archivePath).name, netCfg))
|
||||
state = MigrationToState.ArchiveImport(archivePath, netCfg, networkProxy)
|
||||
MigrationToDeviceState.save(MigrationToDeviceState.ArchiveImport(File(archivePath).name, netCfg, networkProxy))
|
||||
}
|
||||
}
|
||||
is CR.RcvFileError -> {
|
||||
@@ -551,7 +561,7 @@ private fun MutableState<MigrationToState?>.startDownloading(
|
||||
generalGetString(MR.strings.migrate_to_device_download_failed),
|
||||
generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid)
|
||||
)
|
||||
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg)
|
||||
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy)
|
||||
}
|
||||
is CR.ChatRespError -> {
|
||||
if (msg.chatError is ChatError.ChatErrorChat && msg.chatError.errorType is ChatErrorType.NoRcvFileUser) {
|
||||
@@ -559,9 +569,9 @@ private fun MutableState<MigrationToState?>.startDownloading(
|
||||
generalGetString(MR.strings.migrate_to_device_download_failed),
|
||||
generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid)
|
||||
)
|
||||
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg)
|
||||
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy)
|
||||
} else {
|
||||
Log.d(TAG, "unsupported error: ${msg.responseType}")
|
||||
Log.d(TAG, "unsupported error: ${msg.responseType}, ${json.encodeToString(msg.chatError)}")
|
||||
}
|
||||
}
|
||||
else -> Log.d(TAG, "unsupported event: ${msg.responseType}")
|
||||
@@ -571,7 +581,7 @@ private fun MutableState<MigrationToState?>.startDownloading(
|
||||
|
||||
val (res, error) = controller.downloadStandaloneFile(user, link, CryptoFile.plain(File(archivePath).path), ctrl)
|
||||
if (res == null) {
|
||||
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg)
|
||||
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg, networkProxy)
|
||||
AlertManager.shared.showAlertMsg(
|
||||
generalGetString(MR.strings.migrate_to_device_error_downloading_archive),
|
||||
error
|
||||
@@ -580,7 +590,7 @@ private fun MutableState<MigrationToState?>.startDownloading(
|
||||
}
|
||||
}
|
||||
|
||||
private fun MutableState<MigrationToState?>.importArchive(archivePath: String, netCfg: NetCfg) {
|
||||
private fun MutableState<MigrationToState?>.importArchive(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
|
||||
withLongRunningApi {
|
||||
try {
|
||||
if (ChatController.ctrl == null || ChatController.ctrl == -1L) {
|
||||
@@ -594,14 +604,14 @@ private fun MutableState<MigrationToState?>.importArchive(archivePath: String, n
|
||||
if (archiveErrors.isNotEmpty()) {
|
||||
showArchiveImportedWithErrorsAlert(archiveErrors)
|
||||
}
|
||||
state = MigrationToState.Passphrase("", netCfg)
|
||||
MigrationToDeviceState.save(MigrationToDeviceState.Passphrase(netCfg))
|
||||
state = MigrationToState.Passphrase("", netCfg, networkProxy)
|
||||
MigrationToDeviceState.save(MigrationToDeviceState.Passphrase(netCfg, networkProxy))
|
||||
} catch (e: Exception) {
|
||||
state = MigrationToState.ArchiveImportFailed(archivePath, netCfg)
|
||||
state = MigrationToState.ArchiveImportFailed(archivePath, netCfg, networkProxy)
|
||||
AlertManager.shared.showAlertMsg (generalGetString(MR.strings.error_importing_database), e.stackTraceToString())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
state = MigrationToState.ArchiveImportFailed(archivePath, netCfg)
|
||||
state = MigrationToState.ArchiveImportFailed(archivePath, netCfg, networkProxy)
|
||||
AlertManager.shared.showAlertMsg (generalGetString(MR.strings.error_deleting_database), e.stackTraceToString())
|
||||
}
|
||||
}
|
||||
@@ -611,7 +621,7 @@ private suspend fun stopArchiveDownloading(fileId: Long, ctrl: ChatCtrl) {
|
||||
controller.apiCancelFile(null, fileId, ctrl)
|
||||
}
|
||||
|
||||
private fun startChat(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, close: () -> Unit) {
|
||||
private fun startChat(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, networkProxy: NetworkProxy?, close: () -> Unit) {
|
||||
if (useKeychain) {
|
||||
ksDatabasePassword.set(passphrase)
|
||||
} else {
|
||||
@@ -623,7 +633,8 @@ private fun startChat(passphrase: String, confirmation: MigrationConfirmation, u
|
||||
try {
|
||||
initChatController(useKey = passphrase, confirmMigrations = confirmation) { CompletableDeferred(false) }
|
||||
val appSettings = controller.apiGetAppSettings(AppSettings.current.prepareForExport()).copy(
|
||||
networkConfig = netCfg
|
||||
networkConfig = netCfg,
|
||||
networkProxy = networkProxy
|
||||
)
|
||||
finishMigration(appSettings, close)
|
||||
} catch (e: Exception) {
|
||||
|
||||
+25
-9
@@ -36,11 +36,13 @@ import chat.simplex.common.views.chatlist.ScrollDirection
|
||||
import chat.simplex.common.views.contacts.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
import chat.simplex.res.MR
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import java.net.URI
|
||||
|
||||
@Composable
|
||||
fun NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) {
|
||||
fun ModalData.NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) {
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
val keyboardState by getKeyboardState()
|
||||
val showToolbarInOneHandUI = remember { derivedStateOf { keyboardState == KeyboardState.Closed && oneHandUI.value } }
|
||||
@@ -97,7 +99,7 @@ fun chatContactType(chat: Chat): ContactType {
|
||||
val contact = cInfo.contact
|
||||
|
||||
when {
|
||||
contact.activeConn == null && contact.profile.contactLink != null -> ContactType.CARD
|
||||
contact.activeConn == null && contact.profile.contactLink != null && contact.active -> ContactType.CARD
|
||||
contact.chatDeleted -> ContactType.CHAT_DELETED
|
||||
contact.contactStatus == ContactStatus.Active -> ContactType.RECENT
|
||||
else -> ContactType.UNLISTED
|
||||
@@ -111,10 +113,8 @@ private fun filterContactTypes(c: List<Chat>, contactTypes: List<ContactType>):
|
||||
return c.filter { chat -> contactTypes.contains(chatContactType(chat)) }
|
||||
}
|
||||
|
||||
private var lazyListState = 0 to 0
|
||||
|
||||
@Composable
|
||||
private fun NewChatSheetLayout(
|
||||
private fun ModalData.NewChatSheetLayout(
|
||||
rh: RemoteHostInfo?,
|
||||
addContact: () -> Unit,
|
||||
scanPaste: () -> Unit,
|
||||
@@ -122,7 +122,23 @@ private fun NewChatSheetLayout(
|
||||
close: () -> Unit,
|
||||
) {
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
||||
val listState = remember { appBarHandler.listState }
|
||||
// This is workaround of an issue when position of a list is not restored (when going back to that screen) when a header exists.
|
||||
// Upon returning back, this code returns correct index and position if number of items is the same
|
||||
LaunchedEffect(Unit) {
|
||||
val prevIndex = listState.firstVisibleItemIndex
|
||||
val prevOffset = listState.firstVisibleItemScrollOffset
|
||||
val total = listState.layoutInfo.totalItemsCount
|
||||
if (prevIndex == 0 && prevOffset == 0) return@LaunchedEffect
|
||||
snapshotFlow { listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset }
|
||||
.filter { it == 0 to 0 }
|
||||
.collect {
|
||||
if (total <= listState.layoutInfo.totalItemsCount) {
|
||||
listState.scrollToItem(prevIndex, prevOffset)
|
||||
}
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
||||
val searchShowingSimplexLink = remember { mutableStateOf(false) }
|
||||
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
|
||||
@@ -536,7 +552,7 @@ private fun contactTypesSearchTargets(baseContactTypes: List<ContactType>, searc
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats: () -> Unit, close: () -> Unit) {
|
||||
private fun ModalData.DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats: () -> Unit, close: () -> Unit) {
|
||||
val oneHandUI = remember { appPrefs.oneHandUI.state }
|
||||
val keyboardState by getKeyboardState()
|
||||
val showToolbarInOneHandUI = remember { derivedStateOf { keyboardState == KeyboardState.Closed && oneHandUI.value } }
|
||||
@@ -558,7 +574,7 @@ private fun DeletedContactsView(rh: RemoteHostInfo?, closeDeletedChats: () -> Un
|
||||
}
|
||||
}
|
||||
) { contentPadding ->
|
||||
val listState = rememberLazyListState(lazyListState.first, lazyListState.second)
|
||||
val listState = remember { appBarHandler.listState }
|
||||
val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
|
||||
val searchShowingSimplexLink = remember { mutableStateOf(false) }
|
||||
val searchChatFilteredBySimplexLink = remember { mutableStateOf<String?>(null) }
|
||||
@@ -723,6 +739,6 @@ fun ActionButton(
|
||||
@Composable
|
||||
private fun PreviewNewChatSheet() {
|
||||
SimpleXTheme {
|
||||
NewChatSheet(rh = null, close = {})
|
||||
ModalData().NewChatSheet(rh = null, close = {})
|
||||
}
|
||||
}
|
||||
|
||||
+3
-29
@@ -1,10 +1,10 @@
|
||||
package chat.simplex.common.views.usersettings
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionCustomFooter
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionItemWithValue
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import SectionViewSelectableCards
|
||||
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||
@@ -40,12 +40,10 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
|
||||
val currentCfg = remember { stateGetOrPut("currentCfg") { controller.getNetCfg() } }
|
||||
val currentCfgVal = currentCfg.value // used only on initialization
|
||||
|
||||
val onionHosts = remember { mutableStateOf(currentCfgVal.onionHosts) }
|
||||
val sessionMode = remember { mutableStateOf(currentCfgVal.sessionMode) }
|
||||
val smpProxyMode = remember { mutableStateOf(currentCfgVal.smpProxyMode) }
|
||||
val smpProxyFallback = remember { mutableStateOf(currentCfgVal.smpProxyFallback) }
|
||||
|
||||
val networkUseSocksProxy: MutableState<Boolean> = remember { mutableStateOf(currentCfgVal.useSocksProxy) }
|
||||
val networkTCPConnectTimeout = remember { mutableStateOf(currentCfgVal.tcpConnectTimeout) }
|
||||
val networkTCPTimeout = remember { mutableStateOf(currentCfgVal.tcpTimeout) }
|
||||
val networkTCPTimeoutPerKb = remember { mutableStateOf(currentCfgVal.tcpTimeoutPerKb) }
|
||||
@@ -90,11 +88,10 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
|
||||
tcpKeepAlive = tcpKeepAlive,
|
||||
smpPingInterval = networkSMPPingInterval.value,
|
||||
smpPingCount = networkSMPPingCount.value
|
||||
).withOnionHosts(onionHosts.value)
|
||||
).withOnionHosts(currentCfg.value.onionHosts)
|
||||
}
|
||||
|
||||
fun updateView(cfg: NetCfg) {
|
||||
onionHosts.value = cfg.onionHosts
|
||||
sessionMode.value = cfg.sessionMode
|
||||
smpProxyMode.value = cfg.smpProxyMode
|
||||
smpProxyFallback.value = cfg.smpProxyFallback
|
||||
@@ -148,10 +145,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
|
||||
) {
|
||||
AdvancedNetworkSettingsLayout(
|
||||
currentRemoteHost = currentRemoteHost,
|
||||
networkUseSocksProxy = networkUseSocksProxy,
|
||||
developerTools = developerTools,
|
||||
onionHosts = onionHosts,
|
||||
useOnion = { onionHosts.value = it; currentCfg.value = currentCfg.value.withOnionHosts(it) },
|
||||
sessionMode = sessionMode,
|
||||
smpProxyMode = smpProxyMode,
|
||||
smpProxyFallback = smpProxyFallback,
|
||||
@@ -183,10 +177,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
|
||||
|
||||
@Composable fun AdvancedNetworkSettingsLayout(
|
||||
currentRemoteHost: RemoteHostInfo?,
|
||||
networkUseSocksProxy: State<Boolean>,
|
||||
developerTools: Boolean,
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
useOnion: (OnionHosts) -> Unit,
|
||||
sessionMode: MutableState<TransportSessionMode>,
|
||||
smpProxyMode: MutableState<SMPProxyMode>,
|
||||
smpProxyFallback: MutableState<SMPProxyFallback>,
|
||||
@@ -223,21 +214,7 @@ fun ModalData.AdvancedNetworkSettingsView(showModal: (ModalData.() -> Unit) -> U
|
||||
SMPProxyFallbackPicker(smpProxyFallback, showModal, updateSMPProxyFallback, enabled = remember { derivedStateOf { smpProxyMode.value != SMPProxyMode.Never } })
|
||||
SettingsPreferenceItem(painterResource(MR.images.ic_arrow_forward), stringResource(MR.strings.private_routing_show_message_status), chatModel.controller.appPrefs.showSentViaProxy)
|
||||
}
|
||||
SectionCustomFooter {
|
||||
Text(stringResource(MR.strings.private_routing_explanation))
|
||||
}
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
}
|
||||
|
||||
if (currentRemoteHost == null && networkUseSocksProxy.value) {
|
||||
SectionView(stringResource(MR.strings.network_socks_proxy).uppercase()) {
|
||||
UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion)
|
||||
SectionCustomFooter {
|
||||
Column {
|
||||
Text(annotatedStringResource(MR.strings.disable_onion_hosts_when_not_supported))
|
||||
}
|
||||
}
|
||||
}
|
||||
SectionTextFooter(stringResource(MR.strings.private_routing_explanation))
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
}
|
||||
|
||||
@@ -562,7 +539,6 @@ fun PreviewAdvancedNetworkSettingsLayout() {
|
||||
SimpleXTheme {
|
||||
AdvancedNetworkSettingsLayout(
|
||||
currentRemoteHost = null,
|
||||
networkUseSocksProxy = remember { mutableStateOf(false) },
|
||||
developerTools = false,
|
||||
sessionMode = remember { mutableStateOf(TransportSessionMode.User) },
|
||||
smpProxyMode = remember { mutableStateOf(SMPProxyMode.Never) },
|
||||
@@ -577,8 +553,6 @@ fun PreviewAdvancedNetworkSettingsLayout() {
|
||||
networkTCPKeepIdle = remember { mutableStateOf(10) },
|
||||
networkTCPKeepIntvl = remember { mutableStateOf(10) },
|
||||
networkTCPKeepCnt = remember { mutableStateOf(10) },
|
||||
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
|
||||
useOnion = {},
|
||||
updateSessionMode = {},
|
||||
updateSMPProxyMode = {},
|
||||
updateSMPProxyFallback = {},
|
||||
|
||||
+189
-96
@@ -1,10 +1,10 @@
|
||||
package chat.simplex.common.views.usersettings
|
||||
|
||||
import SectionBottomSpacer
|
||||
import SectionCustomFooter
|
||||
import SectionDividerSpaced
|
||||
import SectionItemView
|
||||
import SectionItemWithValue
|
||||
import SectionTextFooter
|
||||
import SectionView
|
||||
import SectionViewSelectable
|
||||
import TextIconSpaced
|
||||
@@ -37,10 +37,11 @@ fun NetworkAndServersView() {
|
||||
val netCfg = remember { chatModel.controller.getNetCfg() }
|
||||
val networkUseSocksProxy: MutableState<Boolean> = remember { mutableStateOf(netCfg.useSocksProxy) }
|
||||
|
||||
val proxyPort = remember { derivedStateOf { chatModel.controller.appPrefs.networkProxyHostPort.state.value?.split(":")?.lastOrNull()?.toIntOrNull() ?: 9050 } }
|
||||
val proxyPort = remember { derivedStateOf { appPrefs.networkProxy.state.value.port } }
|
||||
NetworkAndServersLayout(
|
||||
currentRemoteHost = currentRemoteHost,
|
||||
networkUseSocksProxy = networkUseSocksProxy,
|
||||
onionHosts = remember { mutableStateOf(netCfg.onionHosts) },
|
||||
toggleSocksProxy = { enable ->
|
||||
val def = NetCfg.defaults
|
||||
val proxyDef = NetCfg.proxyDefaults
|
||||
@@ -51,7 +52,7 @@ fun NetworkAndServersView() {
|
||||
confirmText = generalGetString(MR.strings.confirm_verb),
|
||||
onConfirm = {
|
||||
withBGApi {
|
||||
var conf = controller.getNetCfg().withHostPort(controller.appPrefs.networkProxyHostPort.get())
|
||||
var conf = controller.getNetCfg().withProxy(controller.appPrefs.networkProxy.get())
|
||||
if (conf.tcpConnectTimeout == def.tcpConnectTimeout) {
|
||||
conf = conf.copy(tcpConnectTimeout = proxyDef.tcpConnectTimeout)
|
||||
}
|
||||
@@ -104,6 +105,7 @@ fun NetworkAndServersView() {
|
||||
@Composable fun NetworkAndServersLayout(
|
||||
currentRemoteHost: RemoteHostInfo?,
|
||||
networkUseSocksProxy: MutableState<Boolean>,
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
toggleSocksProxy: (Boolean) -> Unit,
|
||||
) {
|
||||
val m = chatModel
|
||||
@@ -120,14 +122,10 @@ fun NetworkAndServersView() {
|
||||
|
||||
if (currentRemoteHost == null) {
|
||||
UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy)
|
||||
SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, appPrefs.networkProxyHostPort, false, it) }})
|
||||
SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, appPrefs.networkProxy, onionHosts, sessionMode = appPrefs.networkSessionMode.get(), false, it) }})
|
||||
SettingsActionItem(painterResource(MR.images.ic_cable), stringResource(MR.strings.network_settings), { ModalManager.start.showCustomModal { AdvancedNetworkSettingsView(showModal, it) } })
|
||||
if (networkUseSocksProxy.value) {
|
||||
SectionCustomFooter {
|
||||
Column {
|
||||
Text(annotatedStringResource(MR.strings.socks_proxy_setting_limitations))
|
||||
}
|
||||
}
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.socks_proxy_setting_limitations))
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
} else {
|
||||
SectionDividerSpaced()
|
||||
@@ -158,16 +156,14 @@ fun NetworkAndServersView() {
|
||||
networkUseSocksProxy: MutableState<Boolean>,
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
sessionMode: MutableState<TransportSessionMode>,
|
||||
networkProxyHostPort: SharedPreference<String?>,
|
||||
networkProxy: SharedPreference<NetworkProxy>,
|
||||
toggleSocksProxy: (Boolean) -> Unit,
|
||||
useOnion: (OnionHosts) -> Unit,
|
||||
updateSessionMode: (TransportSessionMode) -> Unit,
|
||||
) {
|
||||
val showModal = { it: @Composable ModalData.() -> Unit -> ModalManager.fullscreen.showModal(content = it) }
|
||||
val showCustomModal = { it: @Composable (close: () -> Unit) -> Unit -> ModalManager.fullscreen.showCustomModal { close -> it(close) }}
|
||||
UseSocksProxySwitch(networkUseSocksProxy, toggleSocksProxy)
|
||||
SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, networkProxyHostPort, true, it) } })
|
||||
UseOnionHosts(onionHosts, networkUseSocksProxy, showModal, useOnion)
|
||||
SettingsActionItem(painterResource(MR.images.ic_settings_ethernet), stringResource(MR.strings.network_socks_proxy_settings), { showCustomModal { SocksProxySettings(networkUseSocksProxy.value, networkProxy, onionHosts, sessionMode.value, true, it) } })
|
||||
if (developerTools) {
|
||||
SessionModePicker(sessionMode, showModal, updateSessionMode)
|
||||
}
|
||||
@@ -205,46 +201,98 @@ fun UseSocksProxySwitch(
|
||||
@Composable
|
||||
fun SocksProxySettings(
|
||||
networkUseSocksProxy: Boolean,
|
||||
networkProxyHostPort: SharedPreference<String?> = appPrefs.networkProxyHostPort,
|
||||
networkProxy: SharedPreference<NetworkProxy>,
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
sessionMode: TransportSessionMode,
|
||||
migration: Boolean,
|
||||
close: () -> Unit
|
||||
) {
|
||||
val defaultHostPort = remember { "localhost:9050" }
|
||||
val hostPortSaved by remember { networkProxyHostPort.state }
|
||||
val networkProxySaved by remember { networkProxy.state }
|
||||
val onionHostsSaved = remember { mutableStateOf(onionHosts.value) }
|
||||
|
||||
val usernameUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(networkProxySaved.username))
|
||||
}
|
||||
val passwordUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(networkProxySaved.password))
|
||||
}
|
||||
val hostUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(hostPortSaved?.split(":")?.firstOrNull() ?: "localhost"))
|
||||
mutableStateOf(TextFieldValue(networkProxySaved.host))
|
||||
}
|
||||
val portUnsaved = rememberSaveable(stateSaver = TextFieldValue.Saver) {
|
||||
mutableStateOf(TextFieldValue(hostPortSaved?.split(":")?.lastOrNull() ?: "9050"))
|
||||
mutableStateOf(TextFieldValue(networkProxySaved.port.toString()))
|
||||
}
|
||||
val save = {
|
||||
val oldValue = networkProxyHostPort.get()
|
||||
networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text)
|
||||
if (networkUseSocksProxy && !migration) {
|
||||
withBGApi {
|
||||
if (!controller.apiSetNetworkConfig(controller.getNetCfg())) {
|
||||
networkProxyHostPort.set(oldValue)
|
||||
}
|
||||
val proxyAuthRandomUnsaved = rememberSaveable { mutableStateOf(networkProxySaved.auth == NetworkProxyAuth.ISOLATE) }
|
||||
LaunchedEffect(proxyAuthRandomUnsaved.value) {
|
||||
if (!proxyAuthRandomUnsaved.value && onionHosts.value != OnionHosts.NEVER) {
|
||||
onionHosts.value = OnionHosts.NEVER
|
||||
}
|
||||
}
|
||||
val proxyAuthModeUnsaved = remember(proxyAuthRandomUnsaved.value, usernameUnsaved.value.text, passwordUnsaved.value.text) {
|
||||
derivedStateOf {
|
||||
if (proxyAuthRandomUnsaved.value) {
|
||||
NetworkProxyAuth.ISOLATE
|
||||
} else {
|
||||
NetworkProxyAuth.USERNAME
|
||||
}
|
||||
}
|
||||
}
|
||||
val saveAndClose = {
|
||||
val oldValue = networkProxyHostPort.get()
|
||||
networkProxyHostPort.set(hostUnsaved.value.text + ":" + portUnsaved.value.text)
|
||||
|
||||
val save: (Boolean) -> Unit = { closeOnSuccess ->
|
||||
val oldValue = networkProxy.get()
|
||||
usernameUnsaved.value = usernameUnsaved.value.copy(if (proxyAuthModeUnsaved.value == NetworkProxyAuth.USERNAME) usernameUnsaved.value.text.trim() else "")
|
||||
passwordUnsaved.value = passwordUnsaved.value.copy(if (proxyAuthModeUnsaved.value == NetworkProxyAuth.USERNAME) passwordUnsaved.value.text.trim() else "")
|
||||
hostUnsaved.value = hostUnsaved.value.copy(hostUnsaved.value.text.trim())
|
||||
portUnsaved.value = portUnsaved.value.copy(portUnsaved.value.text.trim())
|
||||
|
||||
networkProxy.set(
|
||||
NetworkProxy(
|
||||
username = usernameUnsaved.value.text,
|
||||
password = passwordUnsaved.value.text,
|
||||
host = hostUnsaved.value.text,
|
||||
port = portUnsaved.value.text.toIntOrNull() ?: 9050,
|
||||
auth = proxyAuthModeUnsaved.value
|
||||
)
|
||||
)
|
||||
val oldCfg = controller.getNetCfg()
|
||||
val cfg = oldCfg.withOnionHosts(onionHosts.value)
|
||||
val oldOnionHosts = onionHostsSaved.value
|
||||
onionHostsSaved.value = onionHosts.value
|
||||
|
||||
if (!migration) {
|
||||
controller.setNetCfg(cfg)
|
||||
}
|
||||
if (networkUseSocksProxy && !migration) {
|
||||
withBGApi {
|
||||
if (controller.apiSetNetworkConfig(controller.getNetCfg())) {
|
||||
close()
|
||||
if (controller.apiSetNetworkConfig(cfg, showAlertOnError = false)) {
|
||||
onionHosts.value = cfg.onionHosts
|
||||
onionHostsSaved.value = onionHosts.value
|
||||
if (closeOnSuccess) {
|
||||
close()
|
||||
}
|
||||
} else {
|
||||
networkProxyHostPort.set(oldValue)
|
||||
controller.setNetCfg(oldCfg)
|
||||
networkProxy.set(oldValue)
|
||||
onionHostsSaved.value = oldOnionHosts
|
||||
showWrongProxyConfigAlert()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val saveDisabled = hostPortSaved == (hostUnsaved.value.text + ":" + portUnsaved.value.text) ||
|
||||
remember { derivedStateOf { !validHost(hostUnsaved.value.text) } }.value ||
|
||||
remember { derivedStateOf { !validPort(portUnsaved.value.text) } }.value
|
||||
val resetDisabled = hostUnsaved.value.text + ":" + portUnsaved.value.text == defaultHostPort
|
||||
val saveDisabled =
|
||||
(
|
||||
networkProxySaved.username == usernameUnsaved.value.text.trim() &&
|
||||
networkProxySaved.password == passwordUnsaved.value.text.trim() &&
|
||||
networkProxySaved.host == hostUnsaved.value.text.trim() &&
|
||||
networkProxySaved.port.toString() == portUnsaved.value.text.trim() &&
|
||||
networkProxySaved.auth == proxyAuthModeUnsaved.value &&
|
||||
onionHosts.value == onionHostsSaved.value
|
||||
) ||
|
||||
!validCredential(usernameUnsaved.value.text) ||
|
||||
!validCredential(passwordUnsaved.value.text) ||
|
||||
!validHost(hostUnsaved.value.text) ||
|
||||
!validPort(portUnsaved.value.text)
|
||||
val resetDisabled = hostUnsaved.value.text.trim() == "localhost" && portUnsaved.value.text.trim() == "9050" && proxyAuthRandomUnsaved.value && onionHosts.value == NetCfg.defaults.onionHosts
|
||||
ModalView(
|
||||
close = {
|
||||
if (saveDisabled) {
|
||||
@@ -252,7 +300,7 @@ fun SocksProxySettings(
|
||||
} else {
|
||||
showUnsavedSocksHostPortAlert(
|
||||
confirmText = generalGetString(if (networkUseSocksProxy && !migration) MR.strings.network_options_save_and_reconnect else MR.strings.network_options_save),
|
||||
save = saveAndClose,
|
||||
save = { save(true) },
|
||||
close = close
|
||||
)
|
||||
}
|
||||
@@ -263,38 +311,78 @@ fun SocksProxySettings(
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
AppBarTitle(generalGetString(MR.strings.network_socks_proxy_settings))
|
||||
SectionView(contentPadding = PaddingValues(horizontal = DEFAULT_PADDING)) {
|
||||
DefaultConfigurableTextField(
|
||||
hostUnsaved,
|
||||
stringResource(MR.strings.host_verb),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isValid = ::validHost,
|
||||
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
|
||||
keyboardType = KeyboardType.Text,
|
||||
)
|
||||
DefaultConfigurableTextField(
|
||||
portUnsaved,
|
||||
stringResource(MR.strings.port_verb),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isValid = ::validPort,
|
||||
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done); save() }),
|
||||
keyboardType = KeyboardType.Number,
|
||||
)
|
||||
SectionView(stringResource(MR.strings.network_socks_proxy).uppercase()) {
|
||||
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
|
||||
DefaultConfigurableTextField(
|
||||
hostUnsaved,
|
||||
stringResource(MR.strings.host_verb),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isValid = ::validHost,
|
||||
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
|
||||
keyboardType = KeyboardType.Text,
|
||||
)
|
||||
DefaultConfigurableTextField(
|
||||
portUnsaved,
|
||||
stringResource(MR.strings.port_verb),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isValid = ::validPort,
|
||||
keyboardActions = KeyboardActions(onDone = { defaultKeyboardAction(ImeAction.Done); save(false) }),
|
||||
keyboardType = KeyboardType.Number,
|
||||
)
|
||||
}
|
||||
|
||||
UseOnionHosts(onionHosts, rememberUpdatedState(networkUseSocksProxy && proxyAuthRandomUnsaved.value)) {
|
||||
onionHosts.value = it
|
||||
}
|
||||
SectionTextFooter(annotatedStringResource(MR.strings.disable_onion_hosts_when_not_supported))
|
||||
}
|
||||
|
||||
SectionDividerSpaced(maxBottomPadding = false)
|
||||
SectionDividerSpaced(maxTopPadding = true)
|
||||
|
||||
SectionView(stringResource(MR.strings.network_proxy_auth).uppercase()) {
|
||||
PreferenceToggle(
|
||||
stringResource(MR.strings.network_proxy_random_credentials),
|
||||
checked = proxyAuthRandomUnsaved.value,
|
||||
onChange = { proxyAuthRandomUnsaved.value = it }
|
||||
)
|
||||
if (!proxyAuthRandomUnsaved.value) {
|
||||
Column(Modifier.padding(horizontal = DEFAULT_PADDING)) {
|
||||
DefaultConfigurableTextField(
|
||||
usernameUnsaved,
|
||||
stringResource(MR.strings.network_proxy_username),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isValid = ::validCredential,
|
||||
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
|
||||
keyboardType = KeyboardType.Text,
|
||||
)
|
||||
DefaultConfigurableTextField(
|
||||
passwordUnsaved,
|
||||
stringResource(MR.strings.network_proxy_password),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
isValid = ::validCredential,
|
||||
keyboardActions = KeyboardActions(onNext = { defaultKeyboardAction(ImeAction.Next) }),
|
||||
keyboardType = KeyboardType.Password,
|
||||
)
|
||||
}
|
||||
}
|
||||
SectionTextFooter(proxyAuthFooter(usernameUnsaved.value.text, passwordUnsaved.value.text, proxyAuthModeUnsaved.value, sessionMode))
|
||||
}
|
||||
|
||||
SectionDividerSpaced(maxBottomPadding = false, maxTopPadding = true)
|
||||
|
||||
SectionView {
|
||||
SectionItemView({
|
||||
val newHost = defaultHostPort.split(":").first()
|
||||
val newPort = defaultHostPort.split(":").last()
|
||||
hostUnsaved.value = hostUnsaved.value.copy(newHost, TextRange(newHost.length))
|
||||
portUnsaved.value = portUnsaved.value.copy(newPort, TextRange(newPort.length))
|
||||
hostUnsaved.value = hostUnsaved.value.copy("localhost", TextRange(9))
|
||||
portUnsaved.value = portUnsaved.value.copy("9050", TextRange(4))
|
||||
usernameUnsaved.value = TextFieldValue()
|
||||
passwordUnsaved.value = TextFieldValue()
|
||||
proxyAuthRandomUnsaved.value = true
|
||||
onionHosts.value = NetCfg.defaults.onionHosts
|
||||
}, disabled = resetDisabled) {
|
||||
Text(stringResource(MR.strings.network_options_reset_to_defaults), color = if (resetDisabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
|
||||
}
|
||||
SectionItemView(
|
||||
click = { if (networkUseSocksProxy && !migration) showUpdateNetworkSettingsDialog { save() } else save() },
|
||||
click = { if (networkUseSocksProxy && !migration) showUpdateNetworkSettingsDialog { save(false) } else save(false) },
|
||||
disabled = saveDisabled
|
||||
) {
|
||||
Text(stringResource(if (networkUseSocksProxy && !migration) MR.strings.network_options_save_and_reconnect else MR.strings.network_options_save), color = if (saveDisabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary)
|
||||
@@ -305,6 +393,12 @@ fun SocksProxySettings(
|
||||
}
|
||||
}
|
||||
|
||||
private fun proxyAuthFooter(username: String, password: String, auth: NetworkProxyAuth, sessionMode: TransportSessionMode): String = when {
|
||||
auth == NetworkProxyAuth.ISOLATE -> generalGetString(if (sessionMode == TransportSessionMode.User) MR.strings.network_proxy_auth_mode_isolate_by_auth_user else MR.strings.network_proxy_auth_mode_isolate_by_auth_entity)
|
||||
username.isBlank() && password.isBlank() -> generalGetString(MR.strings.network_proxy_auth_mode_no_auth)
|
||||
else -> generalGetString(MR.strings.network_proxy_auth_mode_username_password)
|
||||
}
|
||||
|
||||
private fun showUnsavedSocksHostPortAlert(confirmText: String, save: () -> Unit, close: () -> Unit) {
|
||||
AlertManager.shared.showAlertDialogStacked(
|
||||
title = generalGetString(MR.strings.update_network_settings_question),
|
||||
@@ -319,7 +413,6 @@ private fun showUnsavedSocksHostPortAlert(confirmText: String, save: () -> Unit,
|
||||
fun UseOnionHosts(
|
||||
onionHosts: MutableState<OnionHosts>,
|
||||
enabled: State<Boolean>,
|
||||
showModal: (@Composable ModalData.() -> Unit) -> Unit,
|
||||
useOnion: (OnionHosts) -> Unit,
|
||||
) {
|
||||
val values = remember {
|
||||
@@ -331,36 +424,29 @@ fun UseOnionHosts(
|
||||
}
|
||||
}
|
||||
}
|
||||
val onSelected = {
|
||||
showModal {
|
||||
ColumnWithScrollBar(
|
||||
Modifier.fillMaxWidth(),
|
||||
) {
|
||||
AppBarTitle(stringResource(MR.strings.network_use_onion_hosts))
|
||||
SectionViewSelectable(null, onionHosts, values, useOnion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (enabled.value) {
|
||||
SectionItemWithValue(
|
||||
generalGetString(MR.strings.network_use_onion_hosts),
|
||||
onionHosts,
|
||||
values,
|
||||
icon = painterResource(MR.images.ic_security),
|
||||
enabled = enabled,
|
||||
onSelected = onSelected
|
||||
)
|
||||
} else {
|
||||
// In reality, when socks proxy is disabled, this option acts like NEVER regardless of what was chosen before
|
||||
SectionItemWithValue(
|
||||
generalGetString(MR.strings.network_use_onion_hosts),
|
||||
remember { mutableStateOf(OnionHosts.NEVER) },
|
||||
listOf(ValueTitleDesc(OnionHosts.NEVER, generalGetString(MR.strings.network_use_onion_hosts_no), AnnotatedString(generalGetString(MR.strings.network_use_onion_hosts_no_desc)))),
|
||||
icon = painterResource(MR.images.ic_security),
|
||||
enabled = enabled,
|
||||
onSelected = {}
|
||||
)
|
||||
Column {
|
||||
if (enabled.value) {
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(MR.strings.network_use_onion_hosts),
|
||||
values.map { it.value to it.title },
|
||||
onionHosts,
|
||||
icon = painterResource(MR.images.ic_security),
|
||||
enabled = enabled,
|
||||
onSelected = useOnion
|
||||
)
|
||||
} else {
|
||||
// In reality, when socks proxy is disabled, this option acts like NEVER regardless of what was chosen before
|
||||
ExposedDropDownSettingRow(
|
||||
generalGetString(MR.strings.network_use_onion_hosts),
|
||||
listOf(OnionHosts.NEVER to generalGetString(MR.strings.network_use_onion_hosts_no)),
|
||||
remember { mutableStateOf(OnionHosts.NEVER) },
|
||||
icon = painterResource(MR.images.ic_security),
|
||||
enabled = enabled,
|
||||
onSelected = {}
|
||||
)
|
||||
}
|
||||
SectionTextFooter(values.first { it.value == onionHosts.value }.description)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,12 +484,8 @@ fun SessionModePicker(
|
||||
)
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/106223
|
||||
private fun validHost(s: String): Boolean {
|
||||
val validIp = Regex("^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])[.]){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$")
|
||||
val validHostname = Regex("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])[.])*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$");
|
||||
return s.matches(validIp) || s.matches(validHostname)
|
||||
}
|
||||
private fun validHost(s: String): Boolean =
|
||||
!s.contains('@')
|
||||
|
||||
// https://ihateregex.io/expr/port/
|
||||
fun validPort(s: String): Boolean {
|
||||
@@ -411,6 +493,16 @@ fun validPort(s: String): Boolean {
|
||||
return s.isNotBlank() && s.matches(validPort)
|
||||
}
|
||||
|
||||
private fun validCredential(s: String): Boolean =
|
||||
!s.contains(':') && !s.contains('@')
|
||||
|
||||
fun showWrongProxyConfigAlert() {
|
||||
AlertManager.shared.showAlertMsg(
|
||||
title = generalGetString(MR.strings.network_proxy_incorrect_config_title),
|
||||
text = generalGetString(MR.strings.network_proxy_incorrect_config_desc),
|
||||
)
|
||||
}
|
||||
|
||||
fun showUpdateNetworkSettingsDialog(
|
||||
title: String,
|
||||
startsWith: String = "",
|
||||
@@ -435,6 +527,7 @@ fun PreviewNetworkAndServersLayout() {
|
||||
NetworkAndServersLayout(
|
||||
currentRemoteHost = null,
|
||||
networkUseSocksProxy = remember { mutableStateOf(true) },
|
||||
onionHosts = remember { mutableStateOf(OnionHosts.PREFER) },
|
||||
toggleSocksProxy = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
<string name="messages_section_description">ينطبق هذا الإعداد على الرسائل الموجودة في ملف تعريف الدردشة الحالي الخاص بك</string>
|
||||
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">منصة الرسائل والتطبيقات تحمي خصوصيتك وأمنك.</string>
|
||||
<string name="profile_is_only_shared_with_your_contacts">يتم مشاركة ملف التعريف مع جهات اتصالك فقط.</string>
|
||||
<string name="member_role_will_be_changed_with_notification">سيتم تغيير الدور إلى \"%s\". سيتم إبلاغ كل فرد في المجموعة.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">سيتم تغيير الدور إلى \"%s\". سيستلم العضو دعوة جديدة.</string>
|
||||
<string name="member_role_will_be_changed_with_notification">سيتم تغيير الدور إلى "%s". سيتم إبلاغ كل فرد في المجموعة.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">سيتم تغيير الدور إلى "%s". سيستلم العضو دعوة جديدة.</string>
|
||||
<string name="smp_servers_per_user">خوادم الاتصالات الجديدة لملف تعريف الدردشة الحالي الخاص بك</string>
|
||||
<string name="switch_receiving_address_desc">سيتم تغيير عنوان الاستلام إلى خادم مختلف. سيتم إكمال تغيير العنوان بعد اتصال المرسل بالإنترنت.</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">هذا الرابط ليس رابط اتصال صالح!</string>
|
||||
@@ -767,7 +767,7 @@
|
||||
<string name="no_contacts_selected">لم تٌحدد جهات اتصال</string>
|
||||
<string name="v4_6_group_moderation_descr">يمكّن للمشرف الآن:
|
||||
\n- حذف رسائل الأعضاء.
|
||||
\n- تعطيل الأعضاء (دور \"المراقب\")</string>
|
||||
\n- تعطيل الأعضاء (دور "المراقب")</string>
|
||||
<string name="settings_notifications_mode_title">خدمة الإشعار</string>
|
||||
<string name="chat_preferences_off">غير مفعّل</string>`
|
||||
<string name="chat_preferences_on">مفعل</string>
|
||||
@@ -1076,7 +1076,7 @@
|
||||
<string name="stop_file__action">إيقاف الملف</string>
|
||||
<string name="stop_snd_file__title">التوقف عن إرسال الملف؟</string>
|
||||
<string name="icon_descr_address">عنوان SimpleX</string>
|
||||
<string name="disable_onion_hosts_when_not_supported"><![CDATA[اضبط <i>استخدم مضيفي .onion</i> إلى \"لا\" إذا كان وكيل SOCKS لا يدعمها.]]></string>
|
||||
<string name="disable_onion_hosts_when_not_supported"><![CDATA[اضبط <i>استخدم مضيفي .onion</i> إلى "لا" إذا كان وكيل SOCKS لا يدعمها.]]></string>
|
||||
<string name="share_with_contacts">مشاركة مع جهات الاتصال</string>
|
||||
<string name="shutdown_alert_question">إيقاف التشغيل؟</string>
|
||||
<string name="network_socks_proxy_settings">إعدادات وكيل SOCKS</string>
|
||||
@@ -1406,7 +1406,7 @@
|
||||
<string name="connected_desktop">سطح المكتب متصل</string>
|
||||
<string name="multicast_connect_automatically">اتصل تلقائيًا</string>
|
||||
<string name="desktop_address">عنوان سطح المكتب</string>
|
||||
<string name="marked_deleted_items_description">وضّع علامة \"محذوفة\" على %d من الرسائل</string>
|
||||
<string name="marked_deleted_items_description">وضّع علامة "محذوفة" على %d من الرسائل</string>
|
||||
<string name="discover_on_network">اكتشف عبر الشبكة المحلية</string>
|
||||
<string name="connect_plan_connect_via_link">اتصل عبر الرابط؟</string>
|
||||
<string name="connect_plan_connect_to_yourself">اتصل بنفسك؟</string>
|
||||
|
||||
@@ -318,6 +318,7 @@
|
||||
<string name="delete_message__question">Delete message?</string>
|
||||
<string name="delete_messages__question">Delete %d messages?</string>
|
||||
<string name="delete_message_cannot_be_undone_warning">Message will be deleted - this cannot be undone!</string>
|
||||
<string name="delete_messages_cannot_be_undone_warning">Messages will be deleted - this cannot be undone!</string>
|
||||
<string name="delete_message_mark_deleted_warning">Message will be marked for deletion. The recipient(s) will be able to reveal this message.</string>
|
||||
<string name="delete_messages_mark_deleted_warning">Messages will be marked for deletion. The recipient(s) will be able to reveal these messages.</string>
|
||||
<string name="delete_member_message__question">Delete member message?</string>
|
||||
@@ -764,7 +765,17 @@
|
||||
<string name="network_socks_proxy">SOCKS proxy</string>
|
||||
<string name="network_socks_proxy_settings">SOCKS proxy settings</string>
|
||||
<string name="network_socks_toggle_use_socks_proxy">Use SOCKS proxy</string>
|
||||
<string name="network_proxy_auth">Proxy authentication</string>
|
||||
<string name="network_proxy_random_credentials">Use random credentials</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_user">Use different proxy credentials for each profile.</string>
|
||||
<string name="network_proxy_auth_mode_isolate_by_auth_entity">Use different proxy credentials for each connection.</string>
|
||||
<string name="network_proxy_auth_mode_no_auth">Do not use credentials with proxy.</string>
|
||||
<string name="network_proxy_auth_mode_username_password">Your credentials may be sent unencrypted.</string>
|
||||
<string name="network_proxy_username">Username</string>
|
||||
<string name="network_proxy_password">Password</string>
|
||||
<string name="network_proxy_port">port %d</string>
|
||||
<string name="network_proxy_incorrect_config_title">Error saving proxy</string>
|
||||
<string name="network_proxy_incorrect_config_desc">Make sure proxy configuration is correct.</string>
|
||||
<string name="host_verb">Host</string>
|
||||
<string name="port_verb">Port</string>
|
||||
<string name="network_enable_socks">Use SOCKS proxy?</string>
|
||||
@@ -1556,8 +1567,8 @@
|
||||
<string name="change_verb">Change</string>
|
||||
<string name="switch_verb">Switch</string>
|
||||
<string name="change_member_role_question">Change group role?</string>
|
||||
<string name="member_role_will_be_changed_with_notification">The role will be changed to \"%s\". Everyone in the group will be notified.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">The role will be changed to \"%s\". The member will receive a new invitation.</string>
|
||||
<string name="member_role_will_be_changed_with_notification">The role will be changed to "%s". Everyone in the group will be notified.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">The role will be changed to "%s". The member will receive a new invitation.</string>
|
||||
<string name="connect_via_member_address_alert_title">Connect directly?</string>
|
||||
<string name="connect_via_member_address_alert_desc">Сonnection request will be sent to this group member.</string>
|
||||
<string name="error_removing_member">Error removing member</string>
|
||||
@@ -1893,7 +1904,7 @@
|
||||
<string name="v4_6_audio_video_calls">Audio and video calls</string>
|
||||
<string name="v4_6_audio_video_calls_descr">Support bluetooth and other improvements.</string>
|
||||
<string name="v4_6_group_moderation">Group moderation</string>
|
||||
<string name="v4_6_group_moderation_descr">Now admins can:\n- delete members\' messages.\n- disable members (\"observer\" role)</string>
|
||||
<string name="v4_6_group_moderation_descr">Now admins can:\n- delete members\' messages.\n- disable members ("observer" role)</string>
|
||||
<string name="v4_6_group_welcome_message">Group welcome message</string>
|
||||
<string name="v4_6_group_welcome_message_descr">Set the message shown to new members!</string>
|
||||
<string name="v4_6_reduced_battery_usage">Further reduced battery usage</string>
|
||||
@@ -2174,6 +2185,8 @@
|
||||
<string name="migrate_from_device_creating_archive_link">Creating archive link</string>
|
||||
<string name="migrate_from_device_cancel_migration">Cancel migration</string>
|
||||
<string name="migrate_from_device_finalize_migration">Finalize migration</string>
|
||||
<string name="migrate_from_device_remove_archive_question">Remove archive?</string>
|
||||
<string name="migrate_from_device_uploaded_archive_will_be_removed">The uploaded database archive will be permanently removed from the servers.</string>
|
||||
<string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Choose <i>Migrate from another device</i> on the new device and scan QR code.]]></string>
|
||||
<string name="migrate_from_device_or_share_this_file_link">Or securely share this file link</string>
|
||||
<string name="migrate_from_device_delete_database_from_device">Delete database from this device</string>
|
||||
|
||||
@@ -482,7 +482,7 @@
|
||||
<string name="sending_delivery_receipts_will_be_enabled_all_profiles">Изпращането на потвърждениe за доставка ще бъде активирано за всички контакти във всички видими чат профили.</string>
|
||||
<string name="send_receipts">Изпращане на потвърждениe за доставка</string>
|
||||
<string name="you_can_enable_delivery_receipts_later">Можете да активирате по-късно през Настройки</string>
|
||||
<string name="you_can_enable_delivery_receipts_later_alert">Можете да ги активирате по-късно през настройките за \"Поверителност и сигурност\" на приложението.</string>
|
||||
<string name="you_can_enable_delivery_receipts_later_alert">Можете да ги активирате по-късно през настройките за "Поверителност и сигурност" на приложението.</string>
|
||||
<string name="database_downgrade_warning">Предупреждение: Може да загубите някои данни!</string>
|
||||
<string name="enter_correct_passphrase">Въведи правилна парола.</string>
|
||||
<string name="feature_enabled_for_you">активирано за вас</string>
|
||||
@@ -517,7 +517,7 @@
|
||||
<string name="snd_conn_event_ratchet_sync_agreed">криптирането е съгласувано за %s</string>
|
||||
<string name="button_edit_group_profile">Редактирай групов профил</string>
|
||||
<string name="share_text_disappears_at">Изчезва в: %s</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Ролята ще бъде променена на \"%s\". Членът ще получи нова покана.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Ролята ще бъде променена на "%s". Членът ще получи нова покана.</string>
|
||||
<string name="conn_level_desc_direct">директна</string>
|
||||
<string name="renegotiate_encryption">Предоговори криптирането</string>
|
||||
<string name="group_display_name_field">Въведи име на групата:</string>
|
||||
@@ -918,7 +918,7 @@
|
||||
<string name="member_will_be_removed_from_group_cannot_be_undone">Членът ще бъде премахнат от групата - това не може да бъде отменено!</string>
|
||||
<string name="item_info_no_text">няма текст</string>
|
||||
<string name="button_remove_member">Острани член</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Ролята ще бъде променена на \"%s\". Всички в групата ще бъдат уведомени.</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Ролята ще бъде променена на "%s". Всички в групата ще бъдат уведомени.</string>
|
||||
<string name="users_delete_data_only">Само данни за локален профил</string>
|
||||
<string name="users_delete_with_connections">Профилни и сървърни връзки</string>
|
||||
<string name="user_mute">Без звук</string>
|
||||
@@ -936,7 +936,7 @@
|
||||
<string name="v4_6_hidden_chat_profiles_descr">Защитете чат профилите с парола!</string>
|
||||
<string name="v4_6_group_moderation_descr">Сега администраторите могат:
|
||||
\n- да изтриват съобщения на членове.
|
||||
\n- да деактивират членове (роля \"наблюдател\")</string>
|
||||
\n- да деактивират членове (роля "наблюдател")</string>
|
||||
<string name="v4_6_reduced_battery_usage_descr">Очаквайте скоро още подобрения!</string>
|
||||
<string name="v5_0_polish_interface">Полски интерфейс</string>
|
||||
<string name="v5_1_message_reactions">Реакции на съобщения</string>
|
||||
@@ -1155,7 +1155,7 @@
|
||||
<string name="this_link_is_not_a_valid_connection_link">Този линк не е валиден линк за връзка!</string>
|
||||
<string name="your_chat_profiles">Вашите чат профили</string>
|
||||
<string name="smp_servers_per_user">Сървърите за нови връзки на текущия ви чат профил</string>
|
||||
<string name="to_reveal_profile_enter_password">За да покажете скрития профил, въведете пълната парола в полето за търсене на страницата \"Вашите чат профили\".</string>
|
||||
<string name="to_reveal_profile_enter_password">За да покажете скрития профил, въведете пълната парола в полето за търсене на страницата "Вашите чат профили".</string>
|
||||
<string name="profile_is_only_shared_with_your_contacts">Профилът се споделя само с вашите контакти.</string>
|
||||
<string name="delete_files_and_media_desc">Това действие не може да бъде отменено - всички получени и изпратени файлове и медия ще бъдат изтрити. Снимките с ниска разделителна способност ще бъдат запазени.</string>
|
||||
<string name="tap_to_activate_profile">Докосни за активиране на профил.</string>
|
||||
@@ -1646,7 +1646,7 @@
|
||||
<string name="v5_6_quantum_resistant_encryption">Квантово устойчиво криптиране</string>
|
||||
<string name="v5_6_app_data_migration">Миграция на данните от приложението</string>
|
||||
<string name="v5_6_app_data_migration_descr">Мигрирайте към друго устройство чрез QR код.</string>
|
||||
<string name="v5_6_picture_in_picture_calls">Обаждания \"картина в картина\"</string>
|
||||
<string name="v5_6_picture_in_picture_calls">Обаждания "картина в картина"</string>
|
||||
<string name="v5_6_safer_groups">По-безопасни групи</string>
|
||||
<string name="v5_6_safer_groups_descr">Администраторите могат да блокират член за всички.</string>
|
||||
<string name="migrate_to_device_downloading_details">Подробности за линка се изтеглят</string>
|
||||
|
||||
@@ -462,7 +462,7 @@
|
||||
<string name="rcv_group_event_member_left">odešel</string>
|
||||
<string name="clear_contacts_selection_button">Vyčistit</string>
|
||||
<string name="switch_verb">Přepnout</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Role bude změněna na \"%s\". Všichni ve skupině budou informováni.</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Role bude změněna na "%s". Všichni ve skupině budou informováni.</string>
|
||||
<string name="error_removing_member">Chyba při odebrání člena</string>
|
||||
<string name="error_saving_group_profile">Chyba při ukládání profilu skupiny</string>
|
||||
<string name="network_option_seconds_label">vteřiny</string>
|
||||
@@ -857,7 +857,7 @@
|
||||
<string name="role_in_group">Role</string>
|
||||
<string name="change_role">Změnit roli</string>
|
||||
<string name="change_verb">Změnit</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Role bude změněna na \"%s\". Člen obdrží novou pozvánku.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Role bude změněna na "%s". Člen obdrží novou pozvánku.</string>
|
||||
<string name="error_changing_role">Chyba při změně role</string>
|
||||
<string name="conn_level_desc_direct">přímo</string>
|
||||
<string name="sending_via">Odesíláno přes</string>
|
||||
@@ -972,7 +972,7 @@
|
||||
<string name="v4_6_reduced_battery_usage_descr">Další vylepšení již brzy!</string>
|
||||
<string name="v4_6_group_moderation_descr">Nyní mohou správci:
|
||||
\n- mazat zprávy členů.
|
||||
\n- zakázat členy (role \"pozorovatel\")</string>
|
||||
\n- zakázat členy (role "pozorovatel")</string>
|
||||
<string name="save_profile_password">Uložit heslo profilu</string>
|
||||
<string name="user_mute">Ztlumit</string>
|
||||
<string name="v4_6_hidden_chat_profiles_descr">Chraňte své chat profily heslem!</string>
|
||||
|
||||
@@ -792,8 +792,8 @@
|
||||
<string name="change_verb">Ändern</string>
|
||||
<string name="switch_verb">Wechseln</string>
|
||||
<string name="change_member_role_question">Die Mitgliederrolle ändern?</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Die Mitgliederrolle wird auf \"%s\" geändert. Alle Mitglieder der Gruppe werden benachrichtigt.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Die Mitgliederrolle wird auf \"%s\" geändert. Das Mitglied wird eine neue Einladung erhalten.</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Die Mitgliederrolle wird auf "%s" geändert. Alle Mitglieder der Gruppe werden benachrichtigt.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Die Mitgliederrolle wird auf "%s" geändert. Das Mitglied wird eine neue Einladung erhalten.</string>
|
||||
<string name="error_removing_member">Fehler beim Entfernen des Mitglieds</string>
|
||||
<string name="error_changing_role">Fehler beim Ändern der Rolle</string>
|
||||
<string name="info_row_group">Gruppe</string>
|
||||
@@ -1074,7 +1074,7 @@
|
||||
<string name="you_will_still_receive_calls_and_ntfs">Sie können Anrufe und Benachrichtigungen auch von stummgeschalteten Profilen empfangen, solange diese aktiv sind.</string>
|
||||
<string name="group_welcome_title">Begrüßungsmeldung</string>
|
||||
<string name="you_can_hide_or_mute_user_profile">Sie können ein Benutzerprofil verbergen oder stummschalten – für das Menü gedrückt halten.</string>
|
||||
<string name="to_reveal_profile_enter_password">Geben Sie ein vollständiges Passwort in das Suchfeld auf der Seite \"Ihre Chat-Profile\" ein, um Ihr verborgenes Profil zu sehen.</string>
|
||||
<string name="to_reveal_profile_enter_password">Geben Sie ein vollständiges Passwort in das Suchfeld auf der Seite "Ihre Chat-Profile" ein, um Ihr verborgenes Profil zu sehen.</string>
|
||||
<string name="invalid_migration_confirmation">Migrations-Bestätigung ungültig</string>
|
||||
<string name="upgrade_and_open_chat">Aktualisieren und den Chat öffnen</string>
|
||||
<string name="confirm_database_upgrades">Datenbank-Aktualisierungen bestätigen</string>
|
||||
@@ -1151,7 +1151,7 @@
|
||||
<string name="network_socks_toggle_use_socks_proxy">SOCKS-Proxy nutzen</string>
|
||||
<string name="la_lock_mode">SimpleX-Sperrmodus</string>
|
||||
<string name="lock_not_enabled">SimpleX-Sperre ist nicht aktiviert!</string>
|
||||
<string name="disable_onion_hosts_when_not_supported"><![CDATA[Setzen Sie <i>Verwende .onion-Hosts</i> auf \"Nein\", wenn der SOCKS-Proxy sie nicht unterstützt.]]></string>
|
||||
<string name="disable_onion_hosts_when_not_supported"><![CDATA[Setzen Sie <i>Verwende .onion-Hosts</i> auf "Nein", wenn der SOCKS-Proxy sie nicht unterstützt.]]></string>
|
||||
<string name="submit_passcode">Bestätigen</string>
|
||||
<string name="la_mode_system">System</string>
|
||||
<string name="la_could_not_be_verified">Sie können nicht überprüft werden – bitte versuchen Sie es nochmal.</string>
|
||||
|
||||
@@ -726,7 +726,7 @@
|
||||
<string name="icon_descr_sent_msg_status_unauthorized_send">envío no autorizado</string>
|
||||
<string name="set_contact_name">Escribe un nombre para el contacto</string>
|
||||
<string name="unknown_error">Error desconocido</string>
|
||||
<string name="member_role_will_be_changed_with_notification">El rol del miembro cambiará a \"%s\" y se notificará al grupo.</string>
|
||||
<string name="member_role_will_be_changed_with_notification">El rol del miembro cambiará a "%s" y se notificará al grupo.</string>
|
||||
<string name="v4_2_security_assessment_desc">La seguridad de SimpleX Chat ha sido auditada por Trail of Bits.</string>
|
||||
<string name="v4_4_disappearing_messages_desc">Los mensajes enviados se eliminarán una vez transcurrido el tiempo establecido.</string>
|
||||
<string name="ntf_channel_messages">Mensajes de chat SimpleX</string>
|
||||
@@ -814,7 +814,7 @@
|
||||
<string name="update_database_passphrase">Actualizar contraseña base de datos</string>
|
||||
<string name="group_invitation_tap_to_join_incognito">Pulsa para unirte en modo incógnito</string>
|
||||
<string name="switch_verb">Cambiar</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">El rol del miembro cambiará a \"%s\" y recibirá una invitación nueva.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">El rol del miembro cambiará a "%s" y recibirá una invitación nueva.</string>
|
||||
<string name="update_network_settings_confirmation">Actualizar</string>
|
||||
<string name="update_network_settings_question">¿Actualizar la configuración de red\?</string>
|
||||
<string name="trying_to_connect_to_server_to_receive_messages">Intentando conectar con el servidor para recibir mensajes de este contacto.</string>
|
||||
@@ -992,7 +992,7 @@
|
||||
<string name="you_will_still_receive_calls_and_ntfs">Seguirás recibiendo llamadas y notificaciones de los perfiles silenciados cuando estén activos.</string>
|
||||
<string name="v4_6_group_moderation_descr">Ahora los administradores pueden:
|
||||
\n- eliminar mensajes de los miembros.
|
||||
\n- desactivar el rol miembro (a rol \"observador\")</string>
|
||||
\n- desactivar el rol miembro (a rol "observador")</string>
|
||||
<string name="to_reveal_profile_enter_password">Para hacer visible tu perfil oculto, introduce la contraseña completa en el campo de búsqueda del menú Mis perfiles.</string>
|
||||
<string name="database_upgrade">Actualización de la base de datos</string>
|
||||
<string name="database_downgrade">Volviendo a versión anterior de la base de datos</string>
|
||||
|
||||
@@ -757,7 +757,7 @@
|
||||
<string name="v4_6_reduced_battery_usage_descr">Lisää parannuksia on tulossa pian!</string>
|
||||
<string name="v4_6_group_moderation_descr">Nyt järjestelmänvalvojat voivat:
|
||||
\n- poistaa jäsenten viestit.
|
||||
\n- poista jäsenet käytöstä (\"tarkkailija\" rooli)</string>
|
||||
\n- poista jäsenet käytöstä ("tarkkailija" rooli)</string>
|
||||
<string name="save_and_notify_group_members">Tallenna ja ilmoita ryhmän jäsenille</string>
|
||||
<string name="stop_chat_confirmation">Lopeta</string>
|
||||
<string name="stop_chat_to_export_import_or_delete_chat_database">Pysäytä keskustelut viedäksesi, tuodaksesi tai poistaaksesi keskustelujen tietokannan. Et voi vastaanottaa ja lähettää viestejä, kun keskustelut on pysäytetty.</string>
|
||||
@@ -1108,8 +1108,8 @@
|
||||
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Viestintä- ja sovellusalusta, joka suojaa yksityisyyttäsi ja tietoturvaasi.</string>
|
||||
<string name="icon_descr_video_call">videopuhelu</string>
|
||||
<string name="group_info_section_title_num_members"> %1$s JÄSENET</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Rooli muuttuu muotoon \"%s\". Kaikille ryhmän jäsenille ilmoitetaan asiasta.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Rooli muuttuu muotoon \"%s\". Jäsen saa uuden kutsun.</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Rooli muuttuu muotoon "%s". Kaikille ryhmän jäsenille ilmoitetaan asiasta.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Rooli muuttuu muotoon "%s". Jäsen saa uuden kutsun.</string>
|
||||
<string name="voice_prohibited_in_this_chat">Ääniviestit ovat kiellettyjä tässä keskustelussa.</string>
|
||||
<string name="v4_3_voice_messages">Ääniviestit</string>
|
||||
<string name="trying_to_connect_to_server_to_receive_messages">Yritetään muodostaa yhteys palvelimeen, jota käytetään viestien vastaanottamiseen tältä kontaktilta.</string>
|
||||
|
||||
@@ -972,7 +972,7 @@
|
||||
<string name="v4_6_hidden_chat_profiles">Profils de chat cachés</string>
|
||||
<string name="v4_6_group_moderation_descr">Désormais, les administrateurs peuvent :
|
||||
\n- supprimer les messages des membres.
|
||||
\n- désactiver des membres (rôle \"observateur\")</string>
|
||||
\n- désactiver des membres (rôle "observateur")</string>
|
||||
<string name="save_welcome_message_question">Enregistrer le message d\'accueil ?</string>
|
||||
<string name="v4_6_group_welcome_message_descr">Choisissez un message à l\'attention des nouveaux membres !</string>
|
||||
<string name="hide_profile">Masquer le profil</string>
|
||||
|
||||
@@ -822,8 +822,8 @@
|
||||
<string name="theme_system">Sistema</string>
|
||||
<string name="network_option_tcp_connection_timeout">Scadenza connessione TCP</string>
|
||||
<string name="group_is_decentralized">Completamente decentralizzato: visibile solo ai membri.</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Il ruolo verrà cambiato in \"%s\". Tutti i membri del gruppo riceveranno una notifica.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Il ruolo verrà cambiato in \"%s\". Il membro riceverà un nuovo invito.</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Il ruolo verrà cambiato in "%s". Tutti i membri del gruppo riceveranno una notifica.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Il ruolo verrà cambiato in "%s". Il membro riceverà un nuovo invito.</string>
|
||||
<string name="update_network_settings_confirmation">Aggiorna</string>
|
||||
<string name="update_network_settings_question">Aggiornare le impostazioni di rete\?</string>
|
||||
<string name="updating_settings_will_reconnect_client_to_all_servers">L\'aggiornamento delle impostazioni riconnetterà il client a tutti i server.</string>
|
||||
@@ -978,11 +978,11 @@
|
||||
<string name="v4_6_chinese_spanish_interface_descr">Grazie agli utenti – contribuite via Weblate!</string>
|
||||
<string name="hidden_profile_password">Password del profilo nascosta</string>
|
||||
<string name="save_profile_password">Salva la password del profilo</string>
|
||||
<string name="to_reveal_profile_enter_password">Per rivelare il tuo profilo nascosto, inserisci una password completa in un campo di ricerca nella pagina \"I tuoi profili di chat\".</string>
|
||||
<string name="to_reveal_profile_enter_password">Per rivelare il tuo profilo nascosto, inserisci una password completa in un campo di ricerca nella pagina "I tuoi profili di chat".</string>
|
||||
<string name="password_to_show">Password per mostrare</string>
|
||||
<string name="v4_6_group_moderation_descr">Ora gli amministratori possono:
|
||||
\n- eliminare i messaggi dei membri.
|
||||
\n- disattivare i membri (ruolo \"osservatore\")</string>
|
||||
\n- disattivare i membri (ruolo "osservatore")</string>
|
||||
<string name="hide_profile">Nascondi il profilo</string>
|
||||
<string name="confirm_password">Conferma password</string>
|
||||
<string name="error_updating_user_privacy">Errore nell\'aggiornamento della privacy dell\'utente</string>
|
||||
|
||||
@@ -1015,7 +1015,7 @@
|
||||
<string name="group_invitation_tap_to_join">הקישו כדי להצטרף</string>
|
||||
<string name="network_option_tcp_connection_timeout">תום זמן חיבור TCP</string>
|
||||
<string name="periodic_notifications_desc">האפליקציה בודקת הודעות חדשות מעת לעת - היא משתמשת בכמה אחוזים מהסוללה ביום. האפליקציה לא משתמשת בהתראות דחיפה - נתונים מהמכשיר שלך לא נשלחים לשרתים.</string>
|
||||
<string name="member_role_will_be_changed_with_notification">התפקיד ישתנה ל־\"%s\". כל חברי הקבוצה יקבלו הודעה על כך.</string>
|
||||
<string name="member_role_will_be_changed_with_notification">התפקיד ישתנה ל־"%s". כל חברי הקבוצה יקבלו הודעה על כך.</string>
|
||||
<string name="to_connect_via_link_title">כדי להתחבר באמצעות קישור</string>
|
||||
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">פלטפורמת ההודעות והיישומים המגנה על הפרטיות והאבטחה שלך.</string>
|
||||
<string name="alert_text_msg_bad_id">המזהה של ההודעה הבאה שגוי (קטן או שווה להודעה הקודמת).
|
||||
@@ -1027,7 +1027,7 @@
|
||||
<string name="thank_you_for_installing_simplex">תודה שהתקנתם את SimpleX Chat!</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">קישור זה אינו קישור חיבור תקין!</string>
|
||||
<string name="theme_colors_section_title">צבעי ערכת נושא</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">התפקיד ישתנה ל־\"%s\". החבר יקבל הזמנה חדשה.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">התפקיד ישתנה ל־"%s". החבר יקבל הזמנה חדשה.</string>
|
||||
<string name="smp_servers_per_user">השרתים לחיבורים חדשים של פרופיל הצ׳אט הנוכחי שלך</string>
|
||||
<string name="first_platform_without_user_ids">הפלטפורמה הראשונה ללא כל מזהי משתמש - פרטית בעיצובה.</string>
|
||||
<string name="next_generation_of_private_messaging">הדור הבא של תקשורת פרטית</string>
|
||||
@@ -1836,7 +1836,7 @@
|
||||
<string name="color_mode_system">מערכת</string>
|
||||
<string name="error_showing_desktop_notification">שגיאה בהצגת התראה, צור קשר עם המפתחים</string>
|
||||
<string name="smp_proxy_error_connecting">שגיאה בהתחברות לשרת %1$s, אנא נסה מאוחר יותר</string>
|
||||
<string name="message_forwarded_desc">אין עדיין חיבור ישיר, ההודעה תעובר ע\"י מנהל.</string>
|
||||
<string name="message_forwarded_desc">אין עדיין חיבור ישיר, ההודעה תעובר ע"י מנהל.</string>
|
||||
<string name="member_inactive_title">חבר לא פעיל</string>
|
||||
<string name="xftp_servers_other">שרתי XFTP אחרים</string>
|
||||
<string name="subscription_percentage">הראה אחוזים</string>
|
||||
@@ -1920,11 +1920,11 @@
|
||||
<string name="servers_info_files_tab">קבצים</string>
|
||||
<string name="servers_info_missing">אין מידע, נסה לרענן</string>
|
||||
<string name="servers_info">מידע על השרתים</string>
|
||||
<string name="servers_info_detailed_statistics_received_total">התקבל סה\"כ</string>
|
||||
<string name="servers_info_detailed_statistics_received_total">התקבל סה"כ</string>
|
||||
<string name="servers_info_detailed_statistics_receive_errors">התקבלו שגיאות</string>
|
||||
<string name="reconnect">התחבר מחדש</string>
|
||||
<string name="servers_info_detailed_statistics_sent_messages_header">שלח הודעות</string>
|
||||
<string name="servers_info_detailed_statistics_sent_messages_total">נשלח בסה\"כ</string>
|
||||
<string name="servers_info_detailed_statistics_sent_messages_total">נשלח בסה"כ</string>
|
||||
<string name="smp_server">שרת SMP</string>
|
||||
<string name="xftp_server">שרת XFTP</string>
|
||||
<string name="privacy_media_blur_radius_soft">חלש</string>
|
||||
|
||||
@@ -634,7 +634,7 @@
|
||||
<string name="messages_section_title">메시지</string>
|
||||
<string name="messages_section_description">이 설정은 현재 내 프로필의 메시지에 적용되어요.</string>
|
||||
<string name="member_info_section_title_member">멤버</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">역할이 \"%s\"(으)로 변경되고, 회원은 새로운 초대를 받게 될 거예요.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">역할이 "%s"(으)로 변경되고, 회원은 새로운 초대를 받게 될 거예요.</string>
|
||||
<string name="message_deletion_prohibited">이 채팅에서는 메시지 영구 삭제가 허용되지 않았어요.</string>
|
||||
<string name="leave_group_button">나가기</string>
|
||||
<string name="large_file">큰 파일!</string>
|
||||
@@ -675,11 +675,11 @@
|
||||
<string name="leave_group_question">그룹에서 나갈까요\?</string>
|
||||
<string name="mtr_error_no_down_migration">데이터베이스 버전이 앱보다 최신이지만, 다음에 대한 다운 마이그레이션 없음: %s</string>
|
||||
<string name="member_will_be_removed_from_group_cannot_be_undone">멤버가 그룹에서 제거되어요. 이 작업은 되돌릴 수 없어요!</string>
|
||||
<string name="member_role_will_be_changed_with_notification">역할이 \"%s\"(으)로 변경되어요. 그룹의 모든 멤버에게 알림이 전송됩니다.</string>
|
||||
<string name="member_role_will_be_changed_with_notification">역할이 "%s"(으)로 변경되어요. 그룹의 모든 멤버에게 알림이 전송됩니다.</string>
|
||||
<string name="network_options_reset_to_defaults">기본값으로 재설정</string>
|
||||
<string name="notification_preview_mode_message">메시지 내용</string>
|
||||
<string name="notification_preview_mode_message_desc">대화 상대 이름 및 메시지 표시</string>
|
||||
<string name="only_you_can_delete_messages">나만 메시지를 영구 삭제할 수 있어요(대화 상대는 \"삭제됨\" 표시만 할 수 있음).</string>
|
||||
<string name="only_you_can_delete_messages">나만 메시지를 영구 삭제할 수 있어요(대화 상대는 "삭제됨" 표시만 할 수 있음).</string>
|
||||
<string name="profile_will_be_sent_to_contact_sending_link">이 링크를 보낸 상대에게 프로필이 전송될 거예요.</string>
|
||||
<string name="receiving_files_not_yet_supported">파일 수신은 아직 지원되지 않아요.</string>
|
||||
<string name="please_check_correct_link_and_maybe_ask_for_a_new_one">올바른 링크를 사용했는지 확인하거나 상대에게 다른 링크를 보내달라고 말해 주세요</string>
|
||||
@@ -714,7 +714,7 @@
|
||||
<string name="only_your_contact_can_send_voice">대화 상대만 음성 메시지를 보낼 수 있어요.</string>
|
||||
<string name="prohibit_message_deletion">메시지 영구 삭제 허용되지 않음.</string>
|
||||
<string name="prohibit_sending_voice">음성 메시지 허용되지 않음.</string>
|
||||
<string name="only_your_contact_can_delete">상대만 메시지를 영구 삭제할 수 있어요(나는 \"삭제됨\"으로 표시만 할 수 있음).</string>
|
||||
<string name="only_your_contact_can_delete">상대만 메시지를 영구 삭제할 수 있어요(나는 "삭제됨"으로 표시만 할 수 있음).</string>
|
||||
<string name="only_group_owners_can_enable_voice">그룹 소유자만 음성 메시지를 사용 가능하도록 설정할 수 있어요.</string>
|
||||
<string name="one_time_link">일회성 초대 링크</string>
|
||||
<string name="paste_button">붙여넣기</string>
|
||||
|
||||
@@ -600,7 +600,7 @@
|
||||
<string name="invite_prohibited">Nepavyko pakviesti kontakto!</string>
|
||||
<string name="v4_5_transport_isolation_descr">Pagal pokalbių profilį (numatytieji nustatymai) arba pagal ryšį (BETA).</string>
|
||||
<string name="onboarding_notifications_mode_service_desc"><![CDATA[<b>Naudoja daugiau baterijos</b>! Fono paslauga veikia visada - pranešimai rodomi, kai tik atsiranda žinučių.]]></string>
|
||||
<string name="cannot_access_keychain">Negalima pasiekti \"Keystore\", kad išsaugotumėte duomenų bazės slaptažodį</string>
|
||||
<string name="cannot_access_keychain">Negalima pasiekti "Keystore", kad išsaugotumėte duomenų bazės slaptažodį</string>
|
||||
<string name="icon_descr_cancel_file_preview">Atšaukti failo peržiūrą</string>
|
||||
<string name="icon_descr_cancel_image_preview">Atšaukti vaizdo peržiūrą</string>
|
||||
<string name="share_text_database_id">Duomenų bazės ID: %d</string>
|
||||
@@ -1484,7 +1484,7 @@
|
||||
<string name="no_info_on_delivery">Nėra pristatymo informacijos</string>
|
||||
<string name="v4_6_group_moderation_descr">Dabar administratoriai gali:
|
||||
\n- ištrinti narių žinutes.
|
||||
\n- išjungti narius (\"stebėtojas\" rolė)</string>
|
||||
\n- išjungti narius ("stebėtojas" rolė)</string>
|
||||
<string name="chat_preferences_on">įj.</string>
|
||||
<string name="unfavorite_chat">Nebemėgti</string>
|
||||
<string name="updating_settings_will_reconnect_client_to_all_servers">Nustatymų atnaujinimas perjungs klientą iš naujo prie visų serverių.</string>
|
||||
|
||||
@@ -280,7 +280,7 @@
|
||||
<string name="custom_time_unit_weeks">ആഴ്ചകൾ</string>
|
||||
<string name="icon_descr_sent_msg_status_unauthorized_send">അനധികൃത അയക്കുക</string>
|
||||
<string name="switch_receiving_address_question">സ്വീകരിക്കുന്ന വിലാസം മാറണോ\?</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">കര്ത്തവ്യം \"%s\" ആയി മാറ്റും. അംഗത്തിന് പുതിയ ക്ഷണം ലഭിക്കും.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">കര്ത്തവ്യം "%s" ആയി മാറ്റും. അംഗത്തിന് പുതിയ ക്ഷണം ലഭിക്കും.</string>
|
||||
<string name="la_lock_mode_system">സംവിധാനം പ്രാമാണീകരണം</string>
|
||||
<string name="skip_inviting_button">അംഗങ്ങളെ ക്ഷണിക്കുന്നത് ഒഴിവാക്കുക</string>
|
||||
<string name="save_welcome_message_question">സ്വാഗത സന്ദേശം സംരക്ഷിക്കണോ\?</string>
|
||||
|
||||
@@ -767,7 +767,7 @@
|
||||
<string name="group_info_section_title_num_members">%1$s LEDEN</string>
|
||||
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">U kunt een link of een QR-code delen. Iedereen kan lid worden van de groep. U verliest geen leden van de groep als u deze later verwijdert.</string>
|
||||
<string name="switch_verb">Wijzig</string>
|
||||
<string name="member_role_will_be_changed_with_notification">De rol wordt gewijzigd in \"%s\". Iedereen in de groep wordt op de hoogte gebracht.</string>
|
||||
<string name="member_role_will_be_changed_with_notification">De rol wordt gewijzigd in "%s". Iedereen in de groep wordt op de hoogte gebracht.</string>
|
||||
<string name="receiving_via">Ontvang via</string>
|
||||
<string name="save_group_profile">Groep profiel opslaan</string>
|
||||
<string name="group_is_decentralized">Volledig gedecentraliseerd – alleen zichtbaar voor leden.</string>
|
||||
@@ -800,7 +800,7 @@
|
||||
<string name="button_remove_member">Lid verwijderen</string>
|
||||
<string name="role_in_group">Rol</string>
|
||||
<string name="button_send_direct_message">Direct bericht sturen</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">De rol wordt gewijzigd in \"%s\". De gebruiker ontvangt een nieuwe uitnodiging.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">De rol wordt gewijzigd in "%s". De gebruiker ontvangt een nieuwe uitnodiging.</string>
|
||||
<string name="sending_via">Verzenden via</string>
|
||||
<string name="conn_stats_section_title_servers">SERVERS</string>
|
||||
<string name="network_options_reset_to_defaults">Resetten naar standaardwaarden</string>
|
||||
@@ -973,7 +973,7 @@
|
||||
<string name="make_profile_private">Profiel privé maken!</string>
|
||||
<string name="v4_6_group_moderation_descr">Nu kunnen beheerders:
|
||||
\n- berichten van leden verwijderen.
|
||||
\n- schakel leden uit (\"waarnemer\" rol)</string>
|
||||
\n- schakel leden uit ("waarnemer" rol)</string>
|
||||
<string name="v4_6_hidden_chat_profiles_descr">Bescherm je chat profielen met een wachtwoord!</string>
|
||||
<string name="password_to_show">Wachtwoord om weer te geven</string>
|
||||
<string name="save_and_update_group_profile">Groep profiel opslaan en bijwerken</string>
|
||||
|
||||
@@ -739,7 +739,7 @@
|
||||
<string name="switch_verb">Przełącz</string>
|
||||
<string name="switch_receiving_address">Zmień adres odbioru</string>
|
||||
<string name="group_is_decentralized">W pełni zdecentralizowana – widoczna tylko dla członków.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Rola zostanie zmieniona na \"%s\". Członek otrzyma nowe zaproszenie.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Rola zostanie zmieniona na "%s". Członek otrzyma nowe zaproszenie.</string>
|
||||
<string name="group_welcome_title">Wiadomość powitalna</string>
|
||||
<string name="users_delete_question">Usunąć profil czatu\?</string>
|
||||
<string name="users_delete_profile_for">Usuń profil czatu dla</string>
|
||||
@@ -944,7 +944,7 @@
|
||||
<string name="failed_to_parse_chat_title">Nie udało się załadować czatu</string>
|
||||
<string name="v4_6_group_moderation_descr">Teraz administratorzy mogą:
|
||||
\n- usuwać wiadomości członków.
|
||||
\n- wyłączyć członków (rola \"obserwatora\")</string>
|
||||
\n- wyłączyć członków (rola "obserwatora")</string>
|
||||
<string name="from_gallery_button">Z Galerii</string>
|
||||
<string name="gallery_image_button">Obraz</string>
|
||||
<string name="gallery_video_button">Wideo</string>
|
||||
@@ -993,7 +993,7 @@
|
||||
<string name="smp_servers_test_some_failed">Niektóre serwery nie przeszły testu:</string>
|
||||
<string name="thank_you_for_installing_simplex">Dziękujemy za zainstalowanie SimpleX Chat!</string>
|
||||
<string name="moderate_message_will_be_marked_warning">Wiadomość zostanie oznaczona jako zmoderowana dla wszystkich członków.</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Rola zostanie zmieniona na \"%s\". Wszyscy w grupie zostaną powiadomieni.</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Rola zostanie zmieniona na "%s". Wszyscy w grupie zostaną powiadomieni.</string>
|
||||
<string name="delete_files_and_media_desc">Tego działania nie można cofnąć - wszystkie odebrane i wysłane pliki oraz media zostaną usunięte. Obrazy o niskiej rozdzielczości pozostaną.</string>
|
||||
<string name="switch_receiving_address_desc">Adres odbiorczy zostanie zmieniony na inny serwer. Zmiana adresu zostanie zakończona gdy nadawca będzie online.</string>
|
||||
<string name="this_link_is_not_a_valid_connection_link">Ten link nie jest prawidłowym linkiem połączenia!</string>
|
||||
|
||||
@@ -695,14 +695,14 @@
|
||||
<string name="chat_preferences_no">não</string>
|
||||
<string name="v4_6_group_moderation_descr">Agora administradores podem:
|
||||
\n- excluir mensagens de membros.
|
||||
\n- desativar membros (cargo de \"observador\")</string>
|
||||
\n- desativar membros (cargo de "observador")</string>
|
||||
<string name="v4_6_group_moderation">Moderação do grupo</string>
|
||||
<string name="v4_6_group_welcome_message">Mensagem de boas-vindas do grupo</string>
|
||||
<string name="database_downgrade">Desatualizar banco de dados</string>
|
||||
<string name="mtr_error_different">migração diferente no aplicativo/banco de dados: %s / %s</string>
|
||||
<string name="invite_to_group_button">Convidar para o grupo</string>
|
||||
<string name="no_contacts_to_add">Sem contatos para adicionar</string>
|
||||
<string name="member_role_will_be_changed_with_notification">O cargo será alterada para \"%s\". Todos no grupo serão notificados.</string>
|
||||
<string name="member_role_will_be_changed_with_notification">O cargo será alterada para "%s". Todos no grupo serão notificados.</string>
|
||||
<string name="user_mute">Mutar</string>
|
||||
<string name="only_you_can_send_voice">Somente você pode enviar mensagens de voz.</string>
|
||||
<string name="only_your_contact_can_send_voice">Somente seu contato pode enviar mensagens de voz.</string>
|
||||
@@ -813,7 +813,7 @@
|
||||
<string name="settings_section_title_experimenta">EXPERIMENTAL</string>
|
||||
<string name="snd_conn_event_switch_queue_phase_completed">você alterou o endereço</string>
|
||||
<string name="database_upgrade">Atualização do banco de dados</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">O cargo será alterado para \"%s\". O membro receberá um novo convite.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">O cargo será alterado para "%s". O membro receberá um novo convite.</string>
|
||||
<string name="only_group_owners_can_change_prefs">Somente os proprietários do grupo podem alterar as preferências do grupo.</string>
|
||||
<string name="button_add_welcome_message">Adicionar mensagem de boas-vindas</string>
|
||||
<string name="group_welcome_title">Mensagem de boas-vindas</string>
|
||||
|
||||
@@ -631,7 +631,7 @@
|
||||
<string name="chat_preferences_on">ligado</string>
|
||||
<string name="v4_6_group_moderation_descr">Agora os administradores podem:
|
||||
\n- eliminar mensagens de membros.
|
||||
\n- desativar membros (função de \"observador\")</string>
|
||||
\n- desativar membros (função de "observador")</string>
|
||||
<string name="simplex_link_group">Ligação do grupo SimpleX</string>
|
||||
<string name="images_limit_desc">Apenas 10 imagens podem ser enviadas ao mesmo tempo</string>
|
||||
<string name="videos_limit_desc">Apenas 10 vídeos podem ser enviados ao mesmo tempo</string>
|
||||
|
||||
@@ -796,8 +796,8 @@
|
||||
<string name="change_verb">Поменять</string>
|
||||
<string name="switch_verb">Переключить</string>
|
||||
<string name="change_member_role_question">Поменять роль в группе?</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Роль будет изменена на \"%s\". Все в группе получат сообщение.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Роль будет изменена на \"%s\". Будет отправлено новое приглашение.</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Роль будет изменена на "%s". Все в группе получат сообщение.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Роль будет изменена на "%s". Будет отправлено новое приглашение.</string>
|
||||
<string name="error_removing_member">Ошибка при удалении члена группы</string>
|
||||
<string name="error_changing_role">Ошибка при изменении роли</string>
|
||||
<string name="info_row_group">Группа</string>
|
||||
@@ -964,7 +964,7 @@
|
||||
<string name="v4_4_disappearing_messages">Исчезающие сообщения</string>
|
||||
<string name="v4_4_disappearing_messages_desc">Отправленные сообщения будут удалены через заданное время.</string>
|
||||
<string name="v4_3_improved_server_configuration">Улучшенная конфигурация серверов</string>
|
||||
<string name="v4_4_live_messages">\"Живые\" сообщения</string>
|
||||
<string name="v4_4_live_messages">"Живые" сообщения</string>
|
||||
<string name="v4_4_live_messages_desc">Получатели видят их в то время как Вы их набираете.</string>
|
||||
<string name="v4_4_verify_connection_security">Проверить безопасность соединения</string>
|
||||
<string name="v4_4_verify_connection_security_desc">Сравните код безопасности с Вашими контактами.</string>
|
||||
@@ -1065,7 +1065,7 @@
|
||||
<string name="v4_6_reduced_battery_usage_descr">Дополнительные улучшения скоро!</string>
|
||||
<string name="v4_6_group_moderation_descr">Теперь админы могут:
|
||||
\n- удалять сообщения членов.
|
||||
\n- приостанавливать членов (роль \"наблюдатель\")</string>
|
||||
\n- приостанавливать членов (роль "наблюдатель")</string>
|
||||
<string name="v4_6_hidden_chat_profiles_descr">Защитите Ваши профили чата паролем!</string>
|
||||
<string name="user_unhide">Раскрыть</string>
|
||||
<string name="v4_6_audio_video_calls_descr">Поддержка bluetooth и другие улучшения.</string>
|
||||
|
||||
@@ -742,7 +742,7 @@
|
||||
<string name="v4_6_reduced_battery_usage_descr">การปรับปรุงเพิ่มเติมกำลังจะมาเร็ว ๆ นี้!</string>
|
||||
<string name="v4_6_group_moderation_descr">ขณะนี้ผู้ดูแลระบบสามารถ:
|
||||
\n- ลบข้อความของสมาชิก
|
||||
\n- ปิดการใช้งานสมาชิก (บทบาท \"ผู้สังเกตการณ์\")</string>
|
||||
\n- ปิดการใช้งานสมาชิก (บทบาท "ผู้สังเกตการณ์")</string>
|
||||
<string name="v5_1_message_reactions">ปฏิกิริยาต่อข้อความ</string>
|
||||
<string name="custom_time_unit_minutes">นาที</string>
|
||||
<string name="custom_time_unit_months">เดือน</string>
|
||||
@@ -1020,7 +1020,7 @@
|
||||
<string name="v5_0_polish_interface_descr">ขอบคุณผู้ใช้ – มีส่วนร่วมผ่าน Weblate!</string>
|
||||
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">แพลตฟอร์มการส่งข้อความและแอปพลิเคชันที่ปกป้องความเป็นส่วนตัวและความปลอดภัยของคุณ</string>
|
||||
<string name="next_generation_of_private_messaging">การส่งข้อความส่วนตัวรุ่นต่อไป</string>
|
||||
<string name="member_role_will_be_changed_with_notification">บทบาทจะถูกเปลี่ยนเป็น \"%s\" ทุกคนในกลุ่มจะได้รับแจ้ง</string>
|
||||
<string name="member_role_will_be_changed_with_notification">บทบาทจะถูกเปลี่ยนเป็น "%s" ทุกคนในกลุ่มจะได้รับแจ้ง</string>
|
||||
<string name="delete_files_and_media_desc">การดำเนินการนี้ไม่สามารถยกเลิกได้ ไฟล์และสื่อที่ได้รับและส่งทั้งหมดจะถูกลบ รูปภาพความละเอียดต่ำจะยังคงอยู่</string>
|
||||
<string name="to_reveal_profile_enter_password">หากต้องการเปิดเผยโปรไฟล์ที่ซ่อนอยู่ของคุณ ให้ป้อนรหัสผ่านแบบเต็มในช่องค้นหาในหน้าโปรไฟล์แชทของคุณ</string>
|
||||
<string name="network_session_mode_transport_isolation">การแยกการขนส่ง</string>
|
||||
@@ -1198,7 +1198,7 @@
|
||||
<string name="button_welcome_message">ข้อความต้อนรับ</string>
|
||||
<string name="you_can_share_group_link_anybody_will_be_able_to_connect">คุณสามารถแชร์ลิงก์หรือคิวอาร์โค้ดได้ ทุกคนจะสามารถเข้าร่วมกลุ่มได้ คุณจะไม่สูญเสียสมาชิกของกลุ่มหากคุณลบในภายหลัง</string>
|
||||
<string name="you_can_share_this_address_with_your_contacts">คุณสามารถแบ่งปันที่อยู่นี้กับผู้ติดต่อของคุณเพื่อให้พวกเขาเชื่อมต่อกับ %s</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">บทบาทจะถูกเปลี่ยนเป็น \"%s\" สมาชิกจะได้รับคำเชิญใหม่</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">บทบาทจะถูกเปลี่ยนเป็น "%s" สมาชิกจะได้รับคำเชิญใหม่</string>
|
||||
<string name="group_welcome_title">ข้อความต้อนรับ</string>
|
||||
<string name="group_main_profile_sent">โปรไฟล์การแชทของคุณจะถูกส่งไปยังสมาชิกในกลุ่ม</string>
|
||||
<string name="update_network_settings_confirmation">อัปเดต</string>
|
||||
|
||||
@@ -138,14 +138,14 @@
|
||||
<string name="save_and_notify_contact">Kaydet ve kişiyi bilgilendir</string>
|
||||
<string name="save_passphrase_in_keychain">Parolayı, Keystore\'a kaydet.</string>
|
||||
<string name="icon_descr_audio_on">Ses açık</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Yetki, \"%s\" olarak değiştirelecek. Gruptaki herkes bilgilendirilecek.</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Yetki, "%s" olarak değiştirelecek. Gruptaki herkes bilgilendirilecek.</string>
|
||||
<string name="calls_prohibited_with_this_contact">Sesli/görüntülü aramalar yasaktır.</string>
|
||||
<string name="la_auth_failed">Kimlik doğrulama başarısız</string>
|
||||
<string name="icon_descr_address">SimpleX Adresi</string>
|
||||
<string name="moderate_message_will_be_deleted_warning">Mesaj, tüm üyeler için silinecek.</string>
|
||||
<string name="the_messaging_and_app_platform_protecting_your_privacy_and_security">Gizliliğinizi ve güvenliğinizi koruyan mesajlaşma ve uygulama platformu.</string>
|
||||
<string name="settings_section_title_incognito">Gizlilik kipi</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Yetki, \"%s\" olarak değiştirilecek. Üye, yeni bir davet alacak.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Yetki, "%s" olarak değiştirilecek. Üye, yeni bir davet alacak.</string>
|
||||
<string name="role_in_group">Yetki</string>
|
||||
<string name="allow_voice_messages_question">Sesli mesajlara izin verilsin mi?</string>
|
||||
<string name="users_add">Profil ekle</string>
|
||||
@@ -360,7 +360,7 @@
|
||||
<string name="ttl_hours">%d saat</string>
|
||||
<string name="v4_6_group_moderation_descr">Yeni yöneticiler artık:
|
||||
\n- üyelerin mesajlarını silebilir.
|
||||
\n- üyeleri etkisizleştirebilir (\"gözlemci\" yetkisi verir)</string>
|
||||
\n- üyeleri etkisizleştirebilir ("gözlemci" yetkisi verir)</string>
|
||||
<string name="receipts_section_contacts">Konuşmalar</string>
|
||||
<string name="create_group_link">Grup bağlantısı oluştur</string>
|
||||
<string name="error_accepting_contact_request">Konuşma isteğini onaylarken hata oluştu</string>
|
||||
|
||||
@@ -726,8 +726,8 @@
|
||||
<string name="share_text_disappears_at">Зникає о: %s</string>
|
||||
<string name="item_info_current">(поточне)</string>
|
||||
<string name="button_remove_member">Вилучити учасника</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Роль буде змінено на \"%s\". Всі учасники групи будуть сповіщені.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Роль буде змінено на \"%s\". Учасник отримає нове запрошення.</string>
|
||||
<string name="member_role_will_be_changed_with_notification">Роль буде змінено на "%s". Всі учасники групи будуть сповіщені.</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">Роль буде змінено на "%s". Учасник отримає нове запрошення.</string>
|
||||
<string name="info_row_group">Група</string>
|
||||
<string name="group_welcome_title">Ласкаво просимо</string>
|
||||
<string name="group_profile_is_stored_on_members_devices">Профіль групи зберігається на пристроях учасників, а не на серверах.</string>
|
||||
|
||||
@@ -1327,7 +1327,7 @@
|
||||
<string name="enable_receipts_all">启用</string>
|
||||
<string name="send_receipts_disabled_alert_msg">该群组成员超过 %1$d ,未发送送达回执。</string>
|
||||
<string name="fix_connection_question">修复连接?</string>
|
||||
<string name="v5_2_message_delivery_receipts_descr">我们错过的第二个\"√\"!✅</string>
|
||||
<string name="v5_2_message_delivery_receipts_descr">我们错过的第二个"√"!✅</string>
|
||||
<string name="setup_database_passphrase">设定数据库密码</string>
|
||||
<string name="receipts_groups_title_disable">为群组禁用回执吗?</string>
|
||||
<string name="rcv_group_event_3_members_connected">%s、%s 和 %s 已连接</string>
|
||||
|
||||
@@ -599,8 +599,8 @@
|
||||
<string name="button_create_group_link">建立連結</string>
|
||||
<string name="delete_link_question">刪除連結?</string>
|
||||
<string name="button_remove_member">移除成員</string>
|
||||
<string name="member_role_will_be_changed_with_notification">成員的身份會修改為 \"%s\"。所有在群組內的成員都接收到通知。</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">成員的身份會修改為 \"%s\"。該成員將接收到新的邀請。</string>
|
||||
<string name="member_role_will_be_changed_with_notification">成員的身份會修改為 "%s"。所有在群組內的成員都接收到通知。</string>
|
||||
<string name="member_role_will_be_changed_with_invitation">成員的身份會修改為 "%s"。該成員將接收到新的邀請。</string>
|
||||
<string name="network_status">網路狀態</string>
|
||||
<string name="network_options_reset_to_defaults">重置為預設值</string>
|
||||
<string name="incognito_info_share">當你與某人分享已啟用匿名聊天模式的個人檔案時,此個人檔案將用於他們邀請你參加的群組。</string>
|
||||
|
||||
@@ -34,6 +34,9 @@ var useWorker = false;
|
||||
var isDesktop = false;
|
||||
var localizedState = "";
|
||||
var localizedDescription = "";
|
||||
// When one side of a call sends candidates tot fast (until local & remote descriptions are set), that candidates
|
||||
// will be stored here and then set when the call will be ready to process them
|
||||
let afterCallInitializedCandidates = [];
|
||||
const processCommand = (function () {
|
||||
const defaultIceServers = [
|
||||
{ urls: ["stuns:stun.simplex.im:443"] },
|
||||
@@ -234,6 +237,8 @@ const processCommand = (function () {
|
||||
const pc = activeCall.connection;
|
||||
const offer = await pc.createOffer();
|
||||
await pc.setLocalDescription(offer);
|
||||
addIceCandidates(pc, afterCallInitializedCandidates);
|
||||
afterCallInitializedCandidates = [];
|
||||
// for debugging, returning the command for callee to use
|
||||
// resp = {
|
||||
// type: "offer",
|
||||
@@ -272,6 +277,8 @@ const processCommand = (function () {
|
||||
const answer = await pc.createAnswer();
|
||||
await pc.setLocalDescription(answer);
|
||||
addIceCandidates(pc, remoteIceCandidates);
|
||||
addIceCandidates(pc, afterCallInitializedCandidates);
|
||||
afterCallInitializedCandidates = [];
|
||||
// same as command for caller to use
|
||||
resp = {
|
||||
type: "answer",
|
||||
@@ -297,17 +304,20 @@ const processCommand = (function () {
|
||||
// console.log("answer remoteIceCandidates", JSON.stringify(remoteIceCandidates))
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(answer));
|
||||
addIceCandidates(pc, remoteIceCandidates);
|
||||
addIceCandidates(pc, afterCallInitializedCandidates);
|
||||
afterCallInitializedCandidates = [];
|
||||
resp = { type: "ok" };
|
||||
}
|
||||
break;
|
||||
case "ice":
|
||||
const remoteIceCandidates = parse(command.iceCandidates);
|
||||
if (pc) {
|
||||
const remoteIceCandidates = parse(command.iceCandidates);
|
||||
addIceCandidates(pc, remoteIceCandidates);
|
||||
resp = { type: "ok" };
|
||||
}
|
||||
else {
|
||||
resp = { type: "error", message: "ice: call not started" };
|
||||
afterCallInitializedCandidates.push(...remoteIceCandidates);
|
||||
resp = { type: "error", message: "ice: call not started yet, will add candidates later" };
|
||||
}
|
||||
break;
|
||||
case "media":
|
||||
|
||||
@@ -18,7 +18,6 @@ kotlin {
|
||||
dependencies {
|
||||
implementation(project(":common"))
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation("net.java.dev.jna:jna:5.14.0")
|
||||
}
|
||||
}
|
||||
val jvmTest by getting
|
||||
|
||||
@@ -26,11 +26,11 @@ android.enableJetifier=true
|
||||
kotlin.mpp.androidSourceSetLayoutVersion=2
|
||||
kotlin.jvm.target=11
|
||||
|
||||
android.version_name=6.0.4
|
||||
android.version_code=237
|
||||
android.version_name=6.0.5
|
||||
android.version_code=241
|
||||
|
||||
desktop.version_name=6.0.4
|
||||
desktop.version_code=65
|
||||
desktop.version_name=6.0.5
|
||||
desktop.version_code=68
|
||||
|
||||
kotlin.version=1.9.23
|
||||
gradle.plugin.version=8.2.0
|
||||
|
||||
@@ -23,7 +23,7 @@ import Simplex.Chat
|
||||
import Simplex.Chat.Controller
|
||||
import Simplex.Chat.Core
|
||||
import Simplex.Chat.Options
|
||||
import Simplex.Messaging.Transport.Server (runTCPServer)
|
||||
import Simplex.Messaging.Transport.Server (runLocalTCPServer)
|
||||
import Simplex.Messaging.Util (raceAny_)
|
||||
import UnliftIO.Exception
|
||||
import UnliftIO.STM
|
||||
@@ -68,7 +68,7 @@ newChatServerClient qSize = do
|
||||
runChatServer :: ChatServerConfig -> ChatController -> IO ()
|
||||
runChatServer ChatServerConfig {chatPort, clientQSize} cc = do
|
||||
started <- newEmptyTMVarIO
|
||||
runTCPServer started chatPort $ \sock -> do
|
||||
runLocalTCPServer started chatPort $ \sock -> do
|
||||
ws <- liftIO $ getConnection sock
|
||||
c <- atomically $ newChatServerClient clientQSize
|
||||
putStrLn "client connected"
|
||||
|
||||
@@ -66,7 +66,7 @@ This service continues running when the app is switched off, and it is restarted
|
||||
|
||||
So, for Android we can now deliver instant message notifications without compromising users' privacy in any way. The app version 1.5 that includes private instant notifications is now available on [Play Store](https://play.google.com/store/apps/details?id=chat.simplex.app), in our [F-Droid repo](https://app.simplex.chat/) and via direct [APK](https://github.com/simplex-chat/simplex-chat/releases/latest/download/simplex.apk) downloads!
|
||||
|
||||
Please let us what needs to be improved - it's only the first version of instant notifications for Android!
|
||||
Please let us know what needs to be improved - it's only the first version of instant notifications for Android!
|
||||
|
||||
## Our iOS approach has one trade-off
|
||||
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: d559a66145cf7b4cd367c09974ed1ce8393940b2
|
||||
tag: 4268b90763c58358809a3ea7dd8bc7d78eeb3077
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
||||
Generated
+82
-29
@@ -156,11 +156,11 @@
|
||||
"ghc98X": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696643148,
|
||||
"narHash": "sha256-E02DfgISH7EvvNAu0BHiPvl1E5FGMDi0pWdNZtIBC9I=",
|
||||
"lastModified": 1715066704,
|
||||
"narHash": "sha256-F0EVR8x/fcpj1st+hz96Wdsz5uwVIOziGKAwRxLOYJw=",
|
||||
"ref": "ghc-9.8",
|
||||
"rev": "443e870d977b1ab6fc05f47a9a17bc49296adbd6",
|
||||
"revCount": 61642,
|
||||
"rev": "78a253543d466ac511a1664a3e6aff032ca684d5",
|
||||
"revCount": 61757,
|
||||
"submodules": true,
|
||||
"type": "git",
|
||||
"url": "https://gitlab.haskell.org/ghc/ghc"
|
||||
@@ -175,11 +175,11 @@
|
||||
"ghc99": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1697054644,
|
||||
"narHash": "sha256-kKarOuXUaAH3QWv7ASx+gGFMHaHKe0pK5Zu37ky2AL4=",
|
||||
"lastModified": 1726585445,
|
||||
"narHash": "sha256-IdwQBex4boY6s0Plj5+ixf36rfYSUyMdTWrztKvZH30=",
|
||||
"ref": "refs/heads/master",
|
||||
"rev": "f383a242c76f90bcca8a4d7ee001dcb49c172a9a",
|
||||
"revCount": 62040,
|
||||
"rev": "7fd9e5e29ab54eb406880077463e8552e2ddd39a",
|
||||
"revCount": 67238,
|
||||
"submodules": true,
|
||||
"type": "git",
|
||||
"url": "https://gitlab.haskell.org/ghc/ghc"
|
||||
@@ -225,6 +225,8 @@
|
||||
"hls-2.2": "hls-2.2",
|
||||
"hls-2.3": "hls-2.3",
|
||||
"hls-2.4": "hls-2.4",
|
||||
"hls-2.5": "hls-2.5",
|
||||
"hls-2.6": "hls-2.6",
|
||||
"hpc-coveralls": "hpc-coveralls",
|
||||
"hydra": "hydra",
|
||||
"iserv-proxy": "iserv-proxy",
|
||||
@@ -238,16 +240,17 @@
|
||||
"nixpkgs-2205": "nixpkgs-2205",
|
||||
"nixpkgs-2211": "nixpkgs-2211",
|
||||
"nixpkgs-2305": "nixpkgs-2305",
|
||||
"nixpkgs-2311": "nixpkgs-2311",
|
||||
"nixpkgs-unstable": "nixpkgs-unstable",
|
||||
"old-ghc-nix": "old-ghc-nix",
|
||||
"stackage": "stackage"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1701163700,
|
||||
"narHash": "sha256-sOrewUS3LnzV09nGr7+3R6Q6zsgU4smJc61QsHq+4DE=",
|
||||
"lastModified": 1705833500,
|
||||
"narHash": "sha256-rUIr6JNbCedt1g4gVYVvE9t0oFU6FUspCA0DS5cA8Bg=",
|
||||
"owner": "input-output-hk",
|
||||
"repo": "haskell.nix",
|
||||
"rev": "2808bfe3e62e9eb4ee8974cd623a00e1611f302b",
|
||||
"rev": "d0c35e75cbbc6858770af42ac32b0b85495fbd71",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -328,16 +331,50 @@
|
||||
"hls-2.4": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696939266,
|
||||
"narHash": "sha256-VOMf5+kyOeOmfXTHlv4LNFJuDGa7G3pDnOxtzYR40IU=",
|
||||
"lastModified": 1699862708,
|
||||
"narHash": "sha256-YHXSkdz53zd0fYGIYOgLt6HrA0eaRJi9mXVqDgmvrjk=",
|
||||
"owner": "haskell",
|
||||
"repo": "haskell-language-server",
|
||||
"rev": "362fdd1293efb4b82410b676ab1273479f6d17ee",
|
||||
"rev": "54507ef7e85fa8e9d0eb9a669832a3287ffccd57",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "haskell",
|
||||
"ref": "2.4.0.0",
|
||||
"ref": "2.4.0.1",
|
||||
"repo": "haskell-language-server",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"hls-2.5": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1701080174,
|
||||
"narHash": "sha256-fyiR9TaHGJIIR0UmcCb73Xv9TJq3ht2ioxQ2mT7kVdc=",
|
||||
"owner": "haskell",
|
||||
"repo": "haskell-language-server",
|
||||
"rev": "27f8c3d3892e38edaef5bea3870161815c4d014c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "haskell",
|
||||
"ref": "2.5.0.0",
|
||||
"repo": "haskell-language-server",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"hls-2.6": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1705325287,
|
||||
"narHash": "sha256-+P87oLdlPyMw8Mgoul7HMWdEvWP/fNlo8jyNtwME8E8=",
|
||||
"owner": "haskell",
|
||||
"repo": "haskell-language-server",
|
||||
"rev": "6e0b342fa0327e628610f2711f8c3e4eaaa08b1e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "haskell",
|
||||
"ref": "2.6.0.0",
|
||||
"repo": "haskell-language-server",
|
||||
"type": "github"
|
||||
}
|
||||
@@ -384,11 +421,11 @@
|
||||
"iserv-proxy": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1691634696,
|
||||
"narHash": "sha256-MZH2NznKC/gbgBu8NgIibtSUZeJ00HTLJ0PlWKCBHb0=",
|
||||
"lastModified": 1707968597,
|
||||
"narHash": "sha256-C53NqToxl+n9s1pQ0iLtiH6P5vX3rM+NW/mFt4Ykpsk=",
|
||||
"ref": "hkm/remote-iserv",
|
||||
"rev": "43a979272d9addc29fbffc2e8542c5d96e993d73",
|
||||
"revCount": 14,
|
||||
"rev": "1b7f8aeb37bbc7c00f04e44d9379aa15a4409e8b",
|
||||
"revCount": 18,
|
||||
"type": "git",
|
||||
"url": "https://gitlab.haskell.org/hamishmack/iserv-proxy.git"
|
||||
},
|
||||
@@ -552,11 +589,11 @@
|
||||
},
|
||||
"nixpkgs-2305": {
|
||||
"locked": {
|
||||
"lastModified": 1695416179,
|
||||
"narHash": "sha256-610o1+pwbSu+QuF3GE0NU5xQdTHM3t9wyYhB9l94Cd8=",
|
||||
"lastModified": 1705033721,
|
||||
"narHash": "sha256-K5eJHmL1/kev6WuqyqqbS1cdNnSidIZ3jeqJ7GbrYnQ=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "715d72e967ec1dd5ecc71290ee072bcaf5181ed6",
|
||||
"rev": "a1982c92d8980a0114372973cbdfe0a307f1bdea",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -566,6 +603,22 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-2311": {
|
||||
"locked": {
|
||||
"lastModified": 1719957072,
|
||||
"narHash": "sha256-gvFhEf5nszouwLAkT9nWsDzocUTqLWHuL++dvNjMp9I=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "7144d6241f02d171d25fba3edeaf15e0f2592105",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-23.11-darwin",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-lib": {
|
||||
"locked": {
|
||||
"dir": "lib",
|
||||
@@ -602,17 +655,17 @@
|
||||
},
|
||||
"nixpkgs-unstable": {
|
||||
"locked": {
|
||||
"lastModified": 1695318763,
|
||||
"narHash": "sha256-FHVPDRP2AfvsxAdc+AsgFJevMz5VBmnZglFUMlxBkcY=",
|
||||
"lastModified": 1694822471,
|
||||
"narHash": "sha256-6fSDCj++lZVMZlyqOe9SIOL8tYSBz1bI8acwovRwoX8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e12483116b3b51a185a33a272bf351e357ba9a99",
|
||||
"rev": "47585496bcb13fb72e4a90daeea2f434e2501998",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "47585496bcb13fb72e4a90daeea2f434e2501998",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
@@ -664,11 +717,11 @@
|
||||
"stackage": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1699834215,
|
||||
"narHash": "sha256-g/JKy0BCvJaxPuYDl3QVc4OY8cFEomgG+hW/eEV470M=",
|
||||
"lastModified": 1726532152,
|
||||
"narHash": "sha256-LRXbVY3M2S8uQWdwd2zZrsnVPEvt2GxaHGoy8EFFdJA=",
|
||||
"owner": "input-output-hk",
|
||||
"repo": "stackage.nix",
|
||||
"rev": "47aacd04abcce6bad57f43cbbbd133538380248e",
|
||||
"rev": "c77b3530cebad603812cb111c6f64968c2d2337d",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
name: simplex-chat
|
||||
version: 6.0.4.0
|
||||
version: 6.0.5.0
|
||||
#synopsis:
|
||||
#description:
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
|
||||
@@ -219,6 +219,9 @@ var useWorker = false
|
||||
var isDesktop = false
|
||||
var localizedState = ""
|
||||
var localizedDescription = ""
|
||||
// When one side of a call sends candidates tot fast (until local & remote descriptions are set), that candidates
|
||||
// will be stored here and then set when the call will be ready to process them
|
||||
let afterCallInitializedCandidates: RTCIceCandidateInit[] = []
|
||||
|
||||
const processCommand = (function () {
|
||||
type RTCRtpSenderWithEncryption = RTCRtpSender & {
|
||||
@@ -445,6 +448,8 @@ const processCommand = (function () {
|
||||
const pc = activeCall.connection
|
||||
const offer = await pc.createOffer()
|
||||
await pc.setLocalDescription(offer)
|
||||
addIceCandidates(pc, afterCallInitializedCandidates)
|
||||
afterCallInitializedCandidates = []
|
||||
// for debugging, returning the command for callee to use
|
||||
// resp = {
|
||||
// type: "offer",
|
||||
@@ -481,6 +486,8 @@ const processCommand = (function () {
|
||||
const answer = await pc.createAnswer()
|
||||
await pc.setLocalDescription(answer)
|
||||
addIceCandidates(pc, remoteIceCandidates)
|
||||
addIceCandidates(pc, afterCallInitializedCandidates)
|
||||
afterCallInitializedCandidates = []
|
||||
// same as command for caller to use
|
||||
resp = {
|
||||
type: "answer",
|
||||
@@ -503,16 +510,19 @@ const processCommand = (function () {
|
||||
// console.log("answer remoteIceCandidates", JSON.stringify(remoteIceCandidates))
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(answer))
|
||||
addIceCandidates(pc, remoteIceCandidates)
|
||||
addIceCandidates(pc, afterCallInitializedCandidates)
|
||||
afterCallInitializedCandidates = []
|
||||
resp = {type: "ok"}
|
||||
}
|
||||
break
|
||||
case "ice":
|
||||
const remoteIceCandidates: RTCIceCandidateInit[] = parse(command.iceCandidates)
|
||||
if (pc) {
|
||||
const remoteIceCandidates: RTCIceCandidateInit[] = parse(command.iceCandidates)
|
||||
addIceCandidates(pc, remoteIceCandidates)
|
||||
resp = {type: "ok"}
|
||||
} else {
|
||||
resp = {type: "error", message: "ice: call not started"}
|
||||
afterCallInitializedCandidates.push(...remoteIceCandidates)
|
||||
resp = {type: "error", message: "ice: call not started yet, will add candidates later"}
|
||||
}
|
||||
break
|
||||
case "media":
|
||||
|
||||
@@ -38,6 +38,41 @@
|
||||
</description>
|
||||
|
||||
<releases>
|
||||
<release version="6.0.4" date="2024-08-31">
|
||||
<url type="details">https://simplex.chat/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.html</url>
|
||||
<description>
|
||||
<p>New in v6.0.1-4:</p>
|
||||
<ul>
|
||||
<li>reduce traffic and battery usage on unstable networks.</li>
|
||||
<li>only offer the next versions for update, based on SerVer ordering.</li>
|
||||
<li>UI fixes.</li>
|
||||
<li>reduce app memory usage and start time.</li>
|
||||
<li>faster sending of files to groups.</li>
|
||||
<li>fix rare delivery bug.</li>
|
||||
</ul>
|
||||
<p>New in v6.0:</p>
|
||||
<p>New chat experience:</p>
|
||||
<ul>
|
||||
<li>connect to your friends faster.</li>
|
||||
<li>archive contacts to chat later.</li>
|
||||
<li>delete up to 20 messages at once.</li>
|
||||
<li>increase font size.</li>
|
||||
</ul>
|
||||
<p>New media options:</p>
|
||||
<ul>
|
||||
<li>play from the chat list.</li>
|
||||
<li>blur for better privacy.</li>
|
||||
</ul>
|
||||
<p>Private routing:</p>
|
||||
<ul>
|
||||
<li>it protects your IP address and connections and is now enabled by default.</li>
|
||||
</ul>
|
||||
<p>Connection and servers information:</p>
|
||||
<ul>
|
||||
<li>to control your network status and usage.</li>
|
||||
</ul>
|
||||
</description>
|
||||
</release>
|
||||
<release version="6.0.3" date="2024-08-24">
|
||||
<url type="details">https://simplex.chat/blog/20240814-simplex-chat-vision-funding-v6-private-routing-new-user-experience.html</url>
|
||||
<description>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."d559a66145cf7b4cd367c09974ed1ce8393940b2" = "1jav7jmriims6vlkxg8gmal03f9mbgrwc8v6g0rp95ivkx8gfjyw";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."4268b90763c58358809a3ea7dd8bc7d78eeb3077" = "0w444jbxxi5hgipf35xniwmffnpg4qb46sz112hrwxyf5syfi2k5";
|
||||
"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";
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ cabal-version: 1.12
|
||||
-- see: https://github.com/sol/hpack
|
||||
|
||||
name: simplex-chat
|
||||
version: 6.0.4.0
|
||||
version: 6.0.5.0
|
||||
category: Web, System, Services, Cryptography
|
||||
homepage: https://github.com/simplex-chat/simplex-chat#readme
|
||||
author: simplex.chat
|
||||
|
||||
+13
-8
@@ -105,7 +105,7 @@ import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation (..), Migrati
|
||||
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.Migrations as Migrations
|
||||
import Simplex.Messaging.Client (NetworkConfig (..), ProxyClientError (..), SocksMode (SMAlways), defaultNetworkConfig)
|
||||
import Simplex.Messaging.Client (NetworkConfig (..), ProxyClientError (..), SocksMode (SMAlways), defaultNetworkConfig, textToHostMode)
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.File (CryptoFile (..), CryptoFileArgs (..))
|
||||
import qualified Simplex.Messaging.Crypto.File as CF
|
||||
@@ -114,12 +114,12 @@ 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 (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, XFTPServer, userProtocol)
|
||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth (..), AProtocolType (..), ErrorType (..), MsgBody, MsgFlags (..), NtfServer, ProtoServerWithAuth (..), ProtocolServer, ProtocolType (..), ProtocolTypeI (..), SProtocolType (..), SubscriptionMode (..), UserProtocol, XFTPServer, userProtocol)
|
||||
import qualified Simplex.Messaging.Protocol as SMP
|
||||
import Simplex.Messaging.ServiceScheme (ServiceScheme (..))
|
||||
import qualified Simplex.Messaging.TMap as TM
|
||||
import Simplex.Messaging.Transport (TransportError (..))
|
||||
import Simplex.Messaging.Transport.Client (defaultSocksProxy)
|
||||
import Simplex.Messaging.Transport.Client (defaultSocksProxyWithAuth)
|
||||
import Simplex.Messaging.Util
|
||||
import Simplex.Messaging.Version
|
||||
import Simplex.RemoteControl.Invitation (RCInvitation (..), RCSignedInvitation (..))
|
||||
@@ -341,11 +341,11 @@ newChatController
|
||||
userServers user' = useServers config protocol <$> withTransaction chatStore (`getProtocolServers` user')
|
||||
|
||||
updateNetworkConfig :: NetworkConfig -> SimpleNetCfg -> NetworkConfig
|
||||
updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, smpProxyMode_, smpProxyFallback_, tcpTimeout_, logTLSErrors} =
|
||||
updateNetworkConfig cfg SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, tcpTimeout_, logTLSErrors} =
|
||||
let cfg1 = maybe cfg (\smpProxyMode -> cfg {smpProxyMode}) smpProxyMode_
|
||||
cfg2 = maybe cfg1 (\smpProxyFallback -> cfg1 {smpProxyFallback}) smpProxyFallback_
|
||||
cfg3 = maybe cfg2 (\tcpTimeout -> cfg2 {tcpTimeout, tcpConnectTimeout = (tcpTimeout * 3) `div` 2}) tcpTimeout_
|
||||
in cfg3 {socksProxy, socksMode, logTLSErrors}
|
||||
in cfg3 {socksProxy, socksMode, hostMode, requiredHostMode, logTLSErrors}
|
||||
|
||||
withChatLock :: String -> CM a -> CM a
|
||||
withChatLock name action = asks chatLock >>= \l -> withLock l name action
|
||||
@@ -2763,7 +2763,10 @@ processChatCommand' vr = \case
|
||||
filesInfo <- withFastStore' (`getUserFileInfo` user)
|
||||
cancelFilesInProgress user filesInfo
|
||||
deleteFilesLocally filesInfo
|
||||
withAgent $ \a -> deleteUser a (aUserId user) delSMPQueues
|
||||
withAgent (\a -> deleteUser a (aUserId user) delSMPQueues)
|
||||
`catchChatError` \case
|
||||
e@(ChatErrorAgent NO_USER _) -> toView $ CRChatError (Just user) e
|
||||
e -> throwError e
|
||||
withFastStore' (`deleteUserRecord` user)
|
||||
when (activeUser user) $ chatWriteVar currentUser Nothing
|
||||
ok_
|
||||
@@ -8049,14 +8052,16 @@ chatCommandP =
|
||||
<|> ("yes" $> TMEEnableKeepTTL)
|
||||
<|> ("no" $> TMEDisableKeepTTL)
|
||||
netCfgP = do
|
||||
socksProxy <- "socks=" *> ("off" $> Nothing <|> "on" $> Just defaultSocksProxy <|> Just <$> strP)
|
||||
socksProxy <- "socks=" *> ("off" $> Nothing <|> "on" $> Just defaultSocksProxyWithAuth <|> Just <$> strP)
|
||||
socksMode <- " socks-mode=" *> strP <|> pure SMAlways
|
||||
hostMode <- " host-mode=" *> (textToHostMode . safeDecodeUtf8 <$?> A.takeTill (== ' ')) <|> pure (defaultHostMode socksProxy)
|
||||
requiredHostMode <- " required-host-mode" *> onOffP <|> pure False
|
||||
smpProxyMode_ <- optional $ " smp-proxy=" *> strP
|
||||
smpProxyFallback_ <- optional $ " smp-proxy-fallback=" *> strP
|
||||
t_ <- optional $ " timeout=" *> A.decimal
|
||||
logTLSErrors <- " log=" *> onOffP <|> pure False
|
||||
let tcpTimeout_ = (1000000 *) <$> t_
|
||||
pure $ SimpleNetCfg {socksProxy, socksMode, smpProxyMode_, smpProxyFallback_, tcpTimeout_, logTLSErrors}
|
||||
pure $ SimpleNetCfg {socksProxy, socksMode, hostMode, requiredHostMode, smpProxyMode_, smpProxyFallback_, tcpTimeout_, logTLSErrors}
|
||||
dbKeyP = nonEmptyKey <$?> strP
|
||||
nonEmptyKey k@(DBEncryptionKey s) = if BA.null s then Left "empty key" else Right k
|
||||
dbEncryptionConfig currentKey newKey = DBEncryptionConfig {currentKey, newKey, keepKey = Just False}
|
||||
|
||||
@@ -76,7 +76,7 @@ import Simplex.Messaging.Agent.Protocol
|
||||
import Simplex.Messaging.Agent.Store.SQLite (MigrationConfirmation, SQLiteStore, UpMigration, withTransaction, withTransactionPriority)
|
||||
import Simplex.Messaging.Agent.Store.SQLite.DB (SlowQueryStats (..))
|
||||
import qualified Simplex.Messaging.Agent.Store.SQLite.DB as DB
|
||||
import Simplex.Messaging.Client (SMPProxyFallback (..), SMPProxyMode (..), SocksMode (..))
|
||||
import Simplex.Messaging.Client (HostMode (..), SMPProxyFallback (..), SMPProxyMode (..), SocksMode (..))
|
||||
import qualified Simplex.Messaging.Crypto as C
|
||||
import Simplex.Messaging.Crypto.File (CryptoFile (..))
|
||||
import qualified Simplex.Messaging.Crypto.File as CF
|
||||
@@ -87,7 +87,7 @@ import Simplex.Messaging.Parsers (defaultJSON, dropPrefix, enumJSON, parseAll, p
|
||||
import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), CorrId, NtfServer, ProtocolType (..), ProtocolTypeI, QueueId, SMPMsgMeta (..), SProtocolType, SubscriptionMode (..), UserProtocol, XFTPServer, userProtocol)
|
||||
import Simplex.Messaging.TMap (TMap)
|
||||
import Simplex.Messaging.Transport (TLS, simplexMQVersion)
|
||||
import Simplex.Messaging.Transport.Client (SocksProxy, TransportHost)
|
||||
import Simplex.Messaging.Transport.Client (SocksProxyWithAuth, TransportHost)
|
||||
import Simplex.Messaging.Util (allFinally, catchAllErrors, catchAllErrors', tryAllErrors, tryAllErrors', (<$$>))
|
||||
import Simplex.RemoteControl.Client
|
||||
import Simplex.RemoteControl.Invitation (RCSignedInvitation, RCVerifiedInvitation)
|
||||
@@ -974,8 +974,10 @@ data AppFilePathsConfig = AppFilePathsConfig
|
||||
deriving (Show)
|
||||
|
||||
data SimpleNetCfg = SimpleNetCfg
|
||||
{ socksProxy :: Maybe SocksProxy,
|
||||
{ socksProxy :: Maybe SocksProxyWithAuth,
|
||||
socksMode :: SocksMode,
|
||||
hostMode :: HostMode,
|
||||
requiredHostMode :: Bool,
|
||||
smpProxyMode_ :: Maybe SMPProxyMode,
|
||||
smpProxyFallback_ :: Maybe SMPProxyFallback,
|
||||
tcpTimeout_ :: Maybe Int,
|
||||
@@ -984,7 +986,7 @@ data SimpleNetCfg = SimpleNetCfg
|
||||
deriving (Show)
|
||||
|
||||
defaultSimpleNetCfg :: SimpleNetCfg
|
||||
defaultSimpleNetCfg = SimpleNetCfg Nothing SMAlways Nothing Nothing Nothing False
|
||||
defaultSimpleNetCfg = SimpleNetCfg Nothing SMAlways HMOnionViaSocks True Nothing Nothing Nothing False
|
||||
|
||||
data ContactSubStatus = ContactSubStatus
|
||||
{ contact :: Contact,
|
||||
|
||||
@@ -13,6 +13,7 @@ module Simplex.Chat.Options
|
||||
coreChatOptsP,
|
||||
getChatOpts,
|
||||
protocolServersP,
|
||||
defaultHostMode,
|
||||
)
|
||||
where
|
||||
|
||||
@@ -20,6 +21,7 @@ import Control.Logger.Simple (LogLevel (..))
|
||||
import qualified Data.Attoparsec.ByteString.Char8 as A
|
||||
import Data.ByteArray (ScrubbedBytes)
|
||||
import qualified Data.ByteString.Char8 as B
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Text (Text)
|
||||
import qualified Data.Text as T
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
@@ -27,11 +29,11 @@ import Numeric.Natural (Natural)
|
||||
import Options.Applicative
|
||||
import Simplex.Chat.Controller (ChatLogLevel (..), SimpleNetCfg (..), updateStr, versionNumber, versionString)
|
||||
import Simplex.FileTransfer.Description (mb)
|
||||
import Simplex.Messaging.Client (SocksMode (..))
|
||||
import Simplex.Messaging.Client (HostMode (..), SocksMode (..), textToHostMode)
|
||||
import Simplex.Messaging.Encoding.String
|
||||
import Simplex.Messaging.Parsers (parseAll)
|
||||
import Simplex.Messaging.Protocol (ProtoServerWithAuth, ProtocolTypeI, SMPServerWithAuth, XFTPServerWithAuth)
|
||||
import Simplex.Messaging.Transport.Client (defaultSocksProxy)
|
||||
import Simplex.Messaging.Transport.Client (SocksProxyWithAuth (..), SocksAuth (..), defaultSocksProxyWithAuth)
|
||||
import System.FilePath (combine)
|
||||
|
||||
data ChatOpts = ChatOpts
|
||||
@@ -123,7 +125,7 @@ coreChatOptsP appDir defaultDbFileName = do
|
||||
<> value []
|
||||
)
|
||||
socksProxy <-
|
||||
flag' (Just defaultSocksProxy) (short 'x' <> help "Use local SOCKS5 proxy at :9050")
|
||||
flag' (Just defaultSocksProxyWithAuth) (short 'x' <> help "Use local SOCKS5 proxy at :9050")
|
||||
<|> option
|
||||
strParse
|
||||
( long "socks-proxy"
|
||||
@@ -139,6 +141,19 @@ coreChatOptsP appDir defaultDbFileName = do
|
||||
<> help "Use SOCKS5 proxy: always (default), onion (with onion-only relays)"
|
||||
<> value SMAlways
|
||||
)
|
||||
hostMode_ <-
|
||||
optional $
|
||||
option
|
||||
parseHostMode
|
||||
( long "host-mode"
|
||||
<> metavar "HOST_MODE"
|
||||
<> help "Preferred server host type: onion (when SOCKS proxy with isolate-by-auth is used), public"
|
||||
)
|
||||
requiredHostMode <-
|
||||
switch
|
||||
( long "required-host-mode"
|
||||
<> help "Refuse connection if preferred server host type is not available"
|
||||
)
|
||||
smpProxyMode_ <-
|
||||
optional $
|
||||
option
|
||||
@@ -226,7 +241,17 @@ coreChatOptsP appDir defaultDbFileName = do
|
||||
dbKey,
|
||||
smpServers,
|
||||
xftpServers,
|
||||
simpleNetCfg = SimpleNetCfg {socksProxy, socksMode, smpProxyMode_, smpProxyFallback_, tcpTimeout_ = Just $ useTcpTimeout socksProxy t, logTLSErrors},
|
||||
simpleNetCfg =
|
||||
SimpleNetCfg
|
||||
{ socksProxy,
|
||||
socksMode,
|
||||
hostMode = fromMaybe (defaultHostMode socksProxy) hostMode_,
|
||||
requiredHostMode,
|
||||
smpProxyMode_,
|
||||
smpProxyFallback_,
|
||||
tcpTimeout_ = Just $ useTcpTimeout socksProxy t,
|
||||
logTLSErrors
|
||||
},
|
||||
logLevel,
|
||||
logConnections = logConnections || logLevel <= CLLInfo,
|
||||
logServerHosts = logServerHosts || logLevel <= CLLInfo,
|
||||
@@ -240,6 +265,11 @@ coreChatOptsP appDir defaultDbFileName = do
|
||||
useTcpTimeout p t = 1000000 * if t > 0 then t else maybe 7 (const 15) p
|
||||
defaultDbFilePath = combine appDir defaultDbFileName
|
||||
|
||||
defaultHostMode :: Maybe SocksProxyWithAuth -> HostMode
|
||||
defaultHostMode = \case
|
||||
Just (SocksProxyWithAuth SocksIsolateByAuth _) -> HMOnionViaSocks;
|
||||
_ -> HMPublic
|
||||
|
||||
chatOptsP :: FilePath -> FilePath -> Parser ChatOpts
|
||||
chatOptsP appDir defaultDbFileName = do
|
||||
coreOptions <- coreChatOptsP appDir defaultDbFileName
|
||||
@@ -360,6 +390,9 @@ parseProtocolServers = eitherReader $ parseAll protocolServersP . B.pack
|
||||
strParse :: StrEncoding a => ReadM a
|
||||
strParse = eitherReader $ parseAll strP . encodeUtf8 . T.pack
|
||||
|
||||
parseHostMode :: ReadM HostMode
|
||||
parseHostMode = eitherReader $ textToHostMode . T.pack
|
||||
|
||||
parseServerPort :: ReadM (Maybe String)
|
||||
parseServerPort = eitherReader $ parseAll serverPortP . B.pack
|
||||
|
||||
|
||||
@@ -540,21 +540,21 @@ data ExtMsgContent = ExtMsgContent {content :: MsgContent, file :: Maybe FileInv
|
||||
$(JQ.deriveJSON defaultJSON ''QuotedMsg)
|
||||
|
||||
-- this limit reserves space for metadata in forwarded messages
|
||||
-- 15780 (limit used for fileChunkSize) - 161 (x.grp.msg.forward overhead) = 15619, round to 15610
|
||||
-- 15780 (limit used for fileChunkSize) - 161 (x.grp.msg.forward overhead) = 15619, - 16 for block encryption ("rounded" to 15602)
|
||||
maxEncodedMsgLength :: Int
|
||||
maxEncodedMsgLength = 15610
|
||||
maxEncodedMsgLength = 15602
|
||||
|
||||
-- maxEncodedMsgLength - 2222, see e2eEncUserMsgLength in agent
|
||||
maxCompressedMsgLength :: Int
|
||||
maxCompressedMsgLength = 13388
|
||||
maxCompressedMsgLength = 13380
|
||||
|
||||
-- maxEncodedMsgLength - delta between MSG and INFO + 100 (returned for forward overhead)
|
||||
-- delta between MSG and INFO = e2eEncUserMsgLength (no PQ) - e2eEncConnInfoLength (no PQ) = 1008
|
||||
maxEncodedInfoLength :: Int
|
||||
maxEncodedInfoLength = 14702
|
||||
maxEncodedInfoLength = 14694
|
||||
|
||||
maxCompressedInfoLength :: Int
|
||||
maxCompressedInfoLength = 10976 -- maxEncodedInfoLength - 3726, see e2eEncConnInfoLength in agent
|
||||
maxCompressedInfoLength = 10968 -- maxEncodedInfoLength - 3726, see e2eEncConnInfoLength in agent
|
||||
|
||||
data EncodedChatMessage = ECMEncoded ByteString | ECMLarge
|
||||
|
||||
|
||||
@@ -383,7 +383,6 @@ deleteUnusedIncognitoProfileById_ db User {userId} profileId =
|
||||
|]
|
||||
[":user_id" := userId, ":profile_id" := profileId]
|
||||
|
||||
|
||||
type ContactRow' = (ProfileId, ContactName, Maybe Int64, ContactName, Text, Maybe ImageData, Maybe ConnReqContact, LocalAlias, Bool, ContactStatus) :. (Maybe MsgFilter, Maybe Bool, Bool, Maybe Preferences, Preferences, UTCTime, UTCTime, Maybe UTCTime) :. (Maybe GroupMemberId, Bool, Maybe UIThemeEntityOverrides, Bool, Maybe CustomData)
|
||||
|
||||
type ContactRow = Only ContactId :. ContactRow'
|
||||
|
||||
Reference in New Issue
Block a user