diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift index 1c9df5fcbf..2f92f778c3 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIGroupInvitationView.swift @@ -21,6 +21,8 @@ struct CIGroupInvitationView: View { @State private var inProgress = false @State private var progressByTimeout = false + @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false + var body: some View { let action = !chatItem.chatDir.sent && groupInvitation.status == .pending let v = ZStack(alignment: .bottomTrailing) { @@ -43,7 +45,7 @@ struct CIGroupInvitationView: View { .foregroundColor(inProgress ? .secondary : chatIncognito ? .indigo : .accentColor) .font(.callout) + Text(" ") - + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false) + + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy) ) .overlay(DetermineWidth()) } @@ -51,7 +53,7 @@ struct CIGroupInvitationView: View { ( groupInvitationText() + Text(" ") - + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false) + + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy) ) .overlay(DetermineWidth()) } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift index 17b93930fe..24c2c07962 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIMetaView.swift @@ -17,6 +17,8 @@ struct CIMetaView: View { var showStatus = true var showEdited = true + @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false + var body: some View { if chatItem.isDeletedContent { chatItem.timestampText.font(.caption).foregroundColor(metaColor) @@ -27,24 +29,24 @@ struct CIMetaView: View { switch meta.itemStatus { case let .sndSent(sndProgress): switch sndProgress { - case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited) - case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited) + case .complete: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy) + case .partial: ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .sent, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy) } case let .sndRcvd(_, sndProgress): switch sndProgress { case .complete: ZStack { - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited) - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited) + ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy) + ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy) } case .partial: ZStack { - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited) - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited) + ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd1, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy) + ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: paleMetaColor, sent: .rcvd2, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy) } } default: - ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, showStatus: showStatus, showEdited: showEdited) + ciMetaText(meta, chatTTL: ttl, encrypted: encrypted, color: metaColor, showStatus: showStatus, showEdited: showEdited, showViaProxy: showSentViaProxy) } } } @@ -64,7 +66,8 @@ func ciMetaText( transparent: Bool = false, sent: SentCheckmark? = nil, showStatus: Bool = true, - showEdited: Bool = true + showEdited: Bool = true, + showViaProxy: Bool ) -> Text { var r = Text("") if showEdited, meta.itemEdited { @@ -78,6 +81,9 @@ func ciMetaText( } r = r + Text(" ") } + if showViaProxy, meta.sentViaProxy == true { + r = r + statusIconText("arrow.forward", color.opacity(0.67)).font(.caption2) + } if showStatus { if let (icon, statusColor) = meta.statusIcon(color) { let t = Text(Image(systemName: icon)).font(.caption2) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift index 3ad45d6987..da9d5e7d50 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIRcvDecryptionError.swift @@ -19,6 +19,8 @@ struct CIRcvDecryptionError: View { var chatItem: ChatItem @State private var alert: CIRcvDecryptionErrorAlert? + @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false + enum CIRcvDecryptionErrorAlert: Identifiable { case syncAllowedAlert(_ syncConnection: () -> Void) case syncNotSupportedContactAlert @@ -119,7 +121,7 @@ struct CIRcvDecryptionError: View { .foregroundColor(syncSupported ? .accentColor : .secondary) .font(.callout) + Text(" ") - + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true) + + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy) ) } .padding(.horizontal, 12) @@ -140,7 +142,7 @@ struct CIRcvDecryptionError: View { .foregroundColor(.red) .italic() + Text(" ") - + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true) + + ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy) } .padding(.horizontal, 12) CIMetaView(chat: chat, chatItem: chatItem) diff --git a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift index 95c3347f90..a1642769b3 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/FramedItemView.swift @@ -87,12 +87,17 @@ struct FramedItemView: View { .cornerRadius(18) .onPreferenceChange(DetermineWidth.Key.self) { msgWidth = $0 } - switch chatItem.meta.itemStatus { - case .sndErrorAuth: - v.onTapGesture { msgDeliveryError("Most likely this contact has deleted the connection with you.") } - case let .sndError(agentError): - v.onTapGesture { msgDeliveryError("Unexpected error: \(agentError)") } - default: v + if let (title, text) = chatItem.meta.itemStatus.statusInfo { + v.onTapGesture { + AlertManager.shared.showAlert( + Alert( + title: Text(title), + message: Text(text) + ) + ) + } + } else { + v } } @@ -157,13 +162,6 @@ struct FramedItemView: View { } } } - - private func msgDeliveryError(_ err: LocalizedStringKey) { - AlertManager.shared.showAlertMsg( - title: "Message delivery error", - message: err - ) - } @ViewBuilder func framedItemHeader(icon: String? = nil, caption: Text, pad: Bool = false) -> some View { let v = HStack(spacing: 6) { diff --git a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift index ccd7ac0a12..11e94cb2c9 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/MsgContentView.swift @@ -35,6 +35,8 @@ struct MsgContentView: View { @State private var typingIdx = 0 @State private var timer: Timer? + @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false + var body: some View { if meta?.isLive == true { msgContentView() @@ -81,7 +83,7 @@ struct MsgContentView: View { } private func reserveSpaceForMeta(_ mt: CIMeta) -> Text { - (rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true) + (rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift index 19aa261396..29bfdb6288 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemInfoView.swift @@ -383,7 +383,7 @@ struct ChatItemInfoView: View { let mss = membersStatuses(memberDeliveryStatuses) if !mss.isEmpty { ForEach(mss, id: \.0.groupMemberId) { memberStatus in - memberDeliveryStatusView(memberStatus.0, memberStatus.1) + memberDeliveryStatusView(memberStatus.0, memberStatus.1, memberStatus.2) } } else { Text("No delivery information") @@ -392,23 +392,27 @@ struct ChatItemInfoView: View { } } - private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, CIStatus)] { + private func membersStatuses(_ memberDeliveryStatuses: [MemberDeliveryStatus]) -> [(GroupMember, CIStatus, Bool?)] { memberDeliveryStatuses.compactMap({ mds in if let mem = chatModel.getGroupMember(mds.groupMemberId) { - return (mem.wrapped, mds.memberDeliveryStatus) + return (mem.wrapped, mds.memberDeliveryStatus, mds.sentViaProxy) } else { return nil } }) } - private func memberDeliveryStatusView(_ member: GroupMember, _ status: CIStatus) -> some View { + private func memberDeliveryStatusView(_ member: GroupMember, _ status: CIStatus, _ sentViaProxy: Bool?) -> some View { HStack{ ProfileImage(imageStr: member.image, size: 30) .padding(.trailing, 2) Text(member.chatViewName) .lineLimit(1) Spacer() + if sentViaProxy == true { + Image(systemName: "arrow.forward") + .foregroundColor(.secondary).opacity(0.67) + } let v = Group { if let (icon, statusColor) = status.statusIcon(Color.secondary) { switch status { diff --git a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift index fe8fd8b28e..4cfd5ae068 100644 --- a/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift +++ b/apps/ios/Shared/Views/ChatList/ChatPreviewView.swift @@ -240,14 +240,14 @@ struct ChatPreviewView: View { private func itemStatusMark(_ cItem: ChatItem) -> Text { switch cItem.meta.itemStatus { - case .sndErrorAuth: + case .sndErrorAuth, .sndError: return Text(Image(systemName: "multiply")) .font(.caption) .foregroundColor(.red) + Text(" ") - case .sndError: + case .sndWarning: return Text(Image(systemName: "exclamationmark.triangle.fill")) .font(.caption) - .foregroundColor(.yellow) + Text(" ") + .foregroundColor(.orange) + Text(" ") default: return Text("") } } diff --git a/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift b/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift index a6702b1821..01cb6ad2d3 100644 --- a/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift +++ b/apps/ios/Shared/Views/UserSettings/NetworkAndServers.swift @@ -12,12 +12,16 @@ import SimpleXChat private enum NetworkAlert: Identifiable { case updateOnionHosts(hosts: OnionHosts) case updateSessionMode(mode: TransportSessionMode) + case updateSMPProxyMode(proxyMode: SMPProxyMode) + case updateSMPProxyFallback(proxyFallback: SMPProxyFallback) case error(err: String) var id: String { switch self { case let .updateOnionHosts(hosts): return "updateOnionHosts \(hosts)" case let .updateSessionMode(mode): return "updateSessionMode \(mode)" + case let .updateSMPProxyMode(proxyMode): return "updateSMPProxyMode \(proxyMode)" + case let .updateSMPProxyFallback(proxyFallback): return "updateSMPProxyFallback \(proxyFallback)" case let .error(err): return "error \(err)" } } @@ -26,11 +30,14 @@ private enum NetworkAlert: Identifiable { struct NetworkAndServers: View { @EnvironmentObject var m: ChatModel @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false + @AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = true @State private var cfgLoaded = false @State private var currentNetCfg = NetCfg.defaults @State private var netCfg = NetCfg.defaults @State private var onionHosts: OnionHosts = .no @State private var sessionMode: TransportSessionMode = .user + @State private var proxyMode: SMPProxyMode = .never + @State private var proxyFallback: SMPProxyFallback = .allow @State private var alert: NetworkAlert? var body: some View { @@ -75,6 +82,30 @@ struct NetworkAndServers: View { Text("Using .onion hosts requires compatible VPN provider.") } + Section { + Picker("Private routing", selection: $proxyMode) { + ForEach(SMPProxyMode.values, id: \.self) { Text($0.text) } + } + .frame(height: 36) + + Picker("Allow downgrade", selection: $proxyFallback) { + ForEach(SMPProxyFallback.values, id: \.self) { Text($0.text) } + } + .disabled(proxyMode == .never) + .frame(height: 36) + + Toggle("Show message status", isOn: $showSentViaProxy) + } header: { + Text("Private message routing") + } footer: { + VStack(alignment: .leading) { + Text("To protect your IP address, private routing uses your SMP servers to deliver messages.") + if showSentViaProxy { + Text("Show → on messages sent via private routing.") + } + } + } + Section("Calls") { NavigationLink { RTCServers() @@ -99,14 +130,24 @@ struct NetworkAndServers: View { currentNetCfg = getNetCfg() resetNetCfgView() } - .onChange(of: onionHosts) { _ in - if onionHosts != OnionHosts(netCfg: currentNetCfg) { - alert = .updateOnionHosts(hosts: onionHosts) + .onChange(of: onionHosts) { hosts in + if hosts != OnionHosts(netCfg: currentNetCfg) { + alert = .updateOnionHosts(hosts: hosts) } } - .onChange(of: sessionMode) { _ in - if sessionMode != netCfg.sessionMode { - alert = .updateSessionMode(mode: sessionMode) + .onChange(of: sessionMode) { mode in + if mode != netCfg.sessionMode { + alert = .updateSessionMode(mode: mode) + } + } + .onChange(of: proxyMode) { mode in + if mode != netCfg.smpProxyMode { + alert = .updateSMPProxyMode(proxyMode: mode) + } + } + .onChange(of: proxyFallback) { fallbackMode in + if fallbackMode != netCfg.smpProxyFallback { + alert = .updateSMPProxyFallback(proxyFallback: fallbackMode) } } .alert(item: $alert) { a in @@ -137,6 +178,30 @@ struct NetworkAndServers: View { resetNetCfgView() } ) + case let .updateSMPProxyMode(proxyMode): + return Alert( + title: Text("Message routing mode"), + message: Text(proxyModeInfo(proxyMode)) + Text("\n") + Text("Updating this setting will re-connect the client to all servers."), + primaryButton: .default(Text("Ok")) { + netCfg.smpProxyMode = proxyMode + saveNetCfg() + }, + secondaryButton: .cancel() { + resetNetCfgView() + } + ) + case let .updateSMPProxyFallback(proxyFallback): + return Alert( + title: Text("Message routing fallback"), + message: Text(proxyFallbackInfo(proxyFallback)) + Text("\n") + Text("Updating this setting will re-connect the client to all servers."), + primaryButton: .default(Text("Ok")) { + netCfg.smpProxyFallback = proxyFallback + saveNetCfg() + }, + secondaryButton: .cancel() { + resetNetCfgView() + } + ) case let .error(err): return Alert( title: Text("Error updating settings"), @@ -166,6 +231,8 @@ struct NetworkAndServers: View { netCfg = currentNetCfg onionHosts = OnionHosts(netCfg: netCfg) sessionMode = netCfg.sessionMode + proxyMode = netCfg.smpProxyMode + proxyFallback = netCfg.smpProxyFallback } private func onionHostsInfo(_ hosts: OnionHosts) -> LocalizedStringKey { @@ -182,6 +249,23 @@ struct NetworkAndServers: View { case .entity: return "A separate TCP connection will be used **for each contact and group member**.\n**Please note**: if you have many connections, your battery and traffic consumption can be substantially higher and some connections may fail." } } + + private func proxyModeInfo(_ mode: SMPProxyMode) -> LocalizedStringKey { + switch mode { + case .always: return "Always use private routing." + case .unknown: return "Use private routing with unknown servers." + case .unprotected: return "Use private routing with unknown servers when IP address is not protected." + case .never: return "Do NOT use private routing." + } + } + + private func proxyFallbackInfo(_ proxyFallback: SMPProxyFallback) -> LocalizedStringKey { + switch proxyFallback { + case .allow: return "Send messages directly when your or destination server does not support 2-hop onion routing." + case .allowProtected: return "Send messages directly when IP address is protected and your or destination server does not support 2-hop onion routing." + case .prohibit: return "Do NOT send messages directly, even if your or destination server does not support 2-hop onion routing." + } + } } struct NetworkServersView_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index e532448a90..89582ab810 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -60,6 +60,7 @@ let DEFAULT_DEVICE_NAME_FOR_REMOTE_ACCESS = "deviceNameForRemoteAccess" let DEFAULT_CONFIRM_REMOTE_SESSIONS = "confirmRemoteSessions" let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST = "connectRemoteViaMulticast" let DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "connectRemoteViaMulticastAuto" +let DEFAULT_SHOW_SENT_VIA_RPOXY = "showSentViaProxy" let ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN = "androidCallOnLockScreen" @@ -99,6 +100,7 @@ let appDefaults: [String: Any] = [ DEFAULT_CONFIRM_REMOTE_SESSIONS: false, DEFAULT_CONNECT_REMOTE_VIA_MULTICAST: true, DEFAULT_CONNECT_REMOTE_VIA_MULTICAST_AUTO: true, + DEFAULT_SHOW_SENT_VIA_RPOXY: false, ANDROID_DEFAULT_CALL_ON_LOCK_SCREEN: AppSettingsLockScreenCalls.show.rawValue ] diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 3aa610e4af..6a4340ea40 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -1242,9 +1242,12 @@ public struct ServerAddress: Decodable { public struct NetCfg: Codable, Equatable { public var socksProxy: String? = nil + var socksMode: SocksMode = .always public var hostMode: HostMode = .publicHost public var requiredHostMode = true public var sessionMode: TransportSessionMode + public var smpProxyMode: SMPProxyMode = .never + public var smpProxyFallback: SMPProxyFallback = .allow public var tcpConnectTimeout: Int // microseconds public var tcpTimeout: Int // microseconds public var tcpTimeoutPerKb: Int // microseconds @@ -1289,6 +1292,49 @@ public enum HostMode: String, Codable { case publicHost = "public" } +public enum SocksMode: String, Codable { + case always = "always" + case onion = "onion" +} + +public enum SMPProxyMode: String, Codable { + case always = "always" + case unknown = "unknown" + case unprotected = "unprotected" + case never = "never" + + public var text: LocalizedStringKey { + switch self { + case .always: return "always" + case .unknown: return "unknown relays" + case .unprotected: return "unprotected" + case .never: return "never" + } + } + + public var id: SMPProxyMode { self } + + public static let values: [SMPProxyMode] = [.always, .unknown, .unprotected, .never] +} + +public enum SMPProxyFallback: String, Codable { + case allow = "allow" + case allowProtected = "allowProtected" + case prohibit = "prohibit" + + public var text: LocalizedStringKey { + switch self { + case .allow: return "yes" + case .allowProtected: return "when IP hidden" + case .prohibit: return "no" + } + } + + public var id: SMPProxyFallback { self } + + public static let values: [SMPProxyFallback] = [.allow, .allowProtected, .prohibit] +} + public enum OnionHosts: String, Identifiable { case no case prefer diff --git a/apps/ios/SimpleXChat/AppGroup.swift b/apps/ios/SimpleXChat/AppGroup.swift index 0511a8486c..118acae993 100644 --- a/apps/ios/SimpleXChat/AppGroup.swift +++ b/apps/ios/SimpleXChat/AppGroup.swift @@ -26,6 +26,8 @@ public let GROUP_DEFAULT_PRIVACY_ENCRYPT_LOCAL_FILES = "privacyEncryptLocalFiles let GROUP_DEFAULT_NTF_BADGE_COUNT = "ntgBadgeCount" let GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS = "networkUseOnionHosts" let GROUP_DEFAULT_NETWORK_SESSION_MODE = "networkSessionMode" +let GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE = "networkSMPProxyMode" +let GROUP_DEFAULT_NETWORK_SMP_PROXY_FALLBACK = "networkSMPProxyFallback" let GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT = "networkTCPConnectTimeout" let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT = "networkTCPTimeout" let GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB = "networkTCPTimeoutPerKb" @@ -53,6 +55,8 @@ public func registerGroupDefaults() { GROUP_DEFAULT_NTF_ENABLE_PERIODIC: false, GROUP_DEFAULT_NETWORK_USE_ONION_HOSTS: OnionHosts.no.rawValue, GROUP_DEFAULT_NETWORK_SESSION_MODE: TransportSessionMode.user.rawValue, + GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE: SMPProxyMode.never.rawValue, + GROUP_DEFAULT_NETWORK_SMP_PROXY_FALLBACK: SMPProxyFallback.allow.rawValue, GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT: NetCfg.defaults.tcpConnectTimeout, GROUP_DEFAULT_NETWORK_TCP_TIMEOUT: NetCfg.defaults.tcpTimeout, GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB: NetCfg.defaults.tcpTimeoutPerKb, @@ -191,6 +195,18 @@ public let networkSessionModeGroupDefault = EnumDefault( withDefault: .user ) +public let networkSMPProxyModeGroupDefault = EnumDefault( + defaults: groupDefaults, + forKey: GROUP_DEFAULT_NETWORK_SMP_PROXY_MODE, + withDefault: .never +) + +public let networkSMPProxyFallbackGroupDefault = EnumDefault( + defaults: groupDefaults, + forKey: GROUP_DEFAULT_NETWORK_SMP_PROXY_FALLBACK, + withDefault: .allow +) + public let storeDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_STORE_DB_PASSPHRASE) public let initialRandomDBPassphraseGroupDefault = BoolDefault(defaults: groupDefaults, forKey: GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE) @@ -275,6 +291,8 @@ public func getNetCfg() -> NetCfg { let onionHosts = networkUseOnionHostsGroupDefault.get() let (hostMode, requiredHostMode) = onionHosts.hostMode let sessionMode = networkSessionModeGroupDefault.get() + let smpProxyMode = networkSMPProxyModeGroupDefault.get() + let smpProxyFallback = networkSMPProxyFallbackGroupDefault.get() let tcpConnectTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT) let tcpTimeout = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT) let tcpTimeoutPerKb = groupDefaults.integer(forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB) @@ -295,6 +313,8 @@ public func getNetCfg() -> NetCfg { hostMode: hostMode, requiredHostMode: requiredHostMode, sessionMode: sessionMode, + smpProxyMode: smpProxyMode, + smpProxyFallback: smpProxyFallback, tcpConnectTimeout: tcpConnectTimeout, tcpTimeout: tcpTimeout, tcpTimeoutPerKb: tcpTimeoutPerKb, @@ -309,6 +329,8 @@ public func getNetCfg() -> NetCfg { public func setNetCfg(_ cfg: NetCfg) { networkUseOnionHostsGroupDefault.set(OnionHosts(netCfg: cfg)) networkSessionModeGroupDefault.set(cfg.sessionMode) + networkSMPProxyModeGroupDefault.set(cfg.smpProxyMode) + networkSMPProxyFallbackGroupDefault.set(cfg.smpProxyFallback) groupDefaults.set(cfg.tcpConnectTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_CONNECT_TIMEOUT) groupDefaults.set(cfg.tcpTimeout, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT) groupDefaults.set(cfg.tcpTimeoutPerKb, forKey: GROUP_DEFAULT_NETWORK_TCP_TIMEOUT_PER_KB) diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 24aca0dd18..09acd032a3 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -2621,6 +2621,7 @@ public struct CIMeta: Decodable { public var itemTs: Date var itemText: String public var itemStatus: CIStatus + public var sentViaProxy: Bool? public var createdAt: Date public var updatedAt: Date public var itemForwarded: CIForwardedFrom? @@ -2710,7 +2711,8 @@ public enum CIStatus: Decodable { case sndSent(sndProgress: SndCIStatusProgress) case sndRcvd(msgRcptStatus: MsgReceiptStatus, sndProgress: SndCIStatusProgress) case sndErrorAuth - case sndError(agentError: String) + case sndError(agentError: SndError) + case sndWarning(agentError: SndError) case rcvNew case rcvRead case invalid(text: String) @@ -2722,6 +2724,7 @@ public enum CIStatus: Decodable { case .sndRcvd: return "sndRcvd" case .sndErrorAuth: return "sndErrorAuth" case .sndError: return "sndError" + case .sndWarning: return "sndWarning" case .rcvNew: return "rcvNew" case .rcvRead: return "rcvRead" case .invalid: return "invalid" @@ -2738,7 +2741,8 @@ public enum CIStatus: Decodable { case .badMsgHash: return ("checkmark", .red) } case .sndErrorAuth: return ("multiply", .red) - case .sndError: return ("exclamationmark.triangle.fill", .yellow) + case .sndError: return ("multiply", .red) + case .sndWarning: return ("exclamationmark.triangle.fill", .orange) case .rcvNew: return ("circlebadge.fill", Color.accentColor) case .rcvRead: return nil case .invalid: return ("questionmark", metaColor) @@ -2756,7 +2760,11 @@ public enum CIStatus: Decodable { ) case let .sndError(agentError): return ( NSLocalizedString("Message delivery error", comment: "item status text"), - String.localizedStringWithFormat(NSLocalizedString("Unexpected error: %@", comment: "item status description"), agentError) + agentError.errorInfo + ) + case let .sndWarning(agentError): return ( + NSLocalizedString("Message delivery warning", comment: "item status text"), + agentError.errorInfo ) case .rcvNew: return nil case .rcvRead: return nil @@ -2768,6 +2776,42 @@ public enum CIStatus: Decodable { } } +public enum SndError: Decodable { + case auth + case quota + case expired + case relay(srvError: SrvError) + case proxy(proxyServer: String, srvError: SrvError) + case proxyRelay(proxyServer: String, srvError: SrvError) + case other(sndError: String) + + public var errorInfo: String { + switch self { + case .auth: NSLocalizedString("Wrong key or unknown connection - most likely this connection is deleted.", comment: "snd error text") + case .quota: NSLocalizedString("Capacity exceeded - recipient did not receive previously sent messages.", comment: "snd error text") + case .expired: NSLocalizedString("Network issues - message expired after many attempts to send it.", comment: "snd error text") + case let .relay(srvError): String.localizedStringWithFormat(NSLocalizedString("Destination server error: %@", comment: "snd error text"), srvError.errorInfo) + case let .proxy(proxyServer, srvError): String.localizedStringWithFormat(NSLocalizedString("Forwarding server: %@\nError: %@", comment: "snd error text"), proxyServer, srvError.errorInfo) + case let .proxyRelay(proxyServer, srvError): String.localizedStringWithFormat(NSLocalizedString("Forwarding server: %@\nDestination server error: %@", comment: "snd error text"), proxyServer, srvError.errorInfo) + case let .other(sndError): String.localizedStringWithFormat(NSLocalizedString("Error: %@", comment: "snd error text"), sndError) + } + } +} + +public enum SrvError: Decodable { + case host + case version + case other(srvError: String) + + public var errorInfo: String { + switch self { + case .host: NSLocalizedString("Server address is incompatible with network settings.", comment: "srv error text.") + case .version: NSLocalizedString("Server version is incompatible with network settings.", comment: "srv error text") + case let .other(srvError): srvError + } + } +} + public enum MsgReceiptStatus: String, Decodable { case ok case badMsgHash @@ -3886,4 +3930,5 @@ public struct ChatItemVersion: Decodable { public struct MemberDeliveryStatus: Decodable { public var groupMemberId: Int64 public var memberDeliveryStatus: CIStatus + public var sentViaProxy: Bool? } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 441e8e0186..4c82f2e7f7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -1900,6 +1900,7 @@ data class ChatItem ( ts: Instant = Clock.System.now(), text: String = "hello\nthere", status: CIStatus = CIStatus.SndNew(), + sentViaProxy: Boolean? = null, quotedItem: CIQuote? = null, file: CIFile? = null, itemForwarded: CIForwardedFrom? = null, @@ -1911,7 +1912,7 @@ data class ChatItem ( ) = ChatItem( chatDir = dir, - meta = CIMeta.getSample(id, ts, text, status, itemForwarded, itemDeleted, itemEdited, itemTimed, deletable, editable), + meta = CIMeta.getSample(id, ts, text, status, sentViaProxy, itemForwarded, itemDeleted, itemEdited, itemTimed, deletable, editable), content = CIContent.SndMsgContent(msgContent = MsgContent.MCText(text)), quotedItem = quotedItem, reactions = listOf(), @@ -1993,6 +1994,7 @@ data class ChatItem ( itemTs = Clock.System.now(), itemText = generalGetString(MR.strings.deleted_description), itemStatus = CIStatus.RcvRead(), + sentViaProxy = null, createdAt = Clock.System.now(), updatedAt = Clock.System.now(), itemForwarded = null, @@ -2016,6 +2018,7 @@ data class ChatItem ( itemTs = Clock.System.now(), itemText = "", itemStatus = CIStatus.RcvRead(), + sentViaProxy = null, createdAt = Clock.System.now(), updatedAt = Clock.System.now(), itemForwarded = null, @@ -2118,6 +2121,7 @@ data class CIMeta ( val itemTs: Instant, val itemText: String, val itemStatus: CIStatus, + val sentViaProxy: Boolean?, val createdAt: Instant, val updatedAt: Instant, val itemForwarded: CIForwardedFrom?, @@ -2144,7 +2148,7 @@ data class CIMeta ( companion object { fun getSample( - id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew(), + id: Long, ts: Instant, text: String, status: CIStatus = CIStatus.SndNew(), sentViaProxy: Boolean? = null, itemForwarded: CIForwardedFrom? = null, itemDeleted: CIDeleted? = null, itemEdited: Boolean = false, itemTimed: CITimed? = null, itemLive: Boolean = false, deletable: Boolean = true, editable: Boolean = true ): CIMeta = @@ -2153,6 +2157,7 @@ data class CIMeta ( itemTs = ts, itemText = text, itemStatus = status, + sentViaProxy = sentViaProxy, createdAt = ts, updatedAt = ts, itemForwarded = itemForwarded, @@ -2171,6 +2176,7 @@ data class CIMeta ( itemTs = Clock.System.now(), itemText = "invalid JSON", itemStatus = CIStatus.SndNew(), + sentViaProxy = null, createdAt = Clock.System.now(), updatedAt = Clock.System.now(), itemForwarded = null, @@ -2227,7 +2233,8 @@ sealed class CIStatus { @Serializable @SerialName("sndSent") class SndSent(val sndProgress: SndCIStatusProgress): CIStatus() @Serializable @SerialName("sndRcvd") class SndRcvd(val msgRcptStatus: MsgReceiptStatus, val sndProgress: SndCIStatusProgress): CIStatus() @Serializable @SerialName("sndErrorAuth") class SndErrorAuth: CIStatus() - @Serializable @SerialName("sndError") class SndError(val agentError: String): CIStatus() + @Serializable @SerialName("sndError") class CISSndError(val agentError: SndError): CIStatus() + @Serializable @SerialName("sndWarning") class SndWarning(val agentError: SndError): CIStatus() @Serializable @SerialName("rcvNew") class RcvNew: CIStatus() @Serializable @SerialName("rcvRead") class RcvRead: CIStatus() @Serializable @SerialName("invalid") class Invalid(val text: String): CIStatus() @@ -2251,7 +2258,8 @@ sealed class CIStatus { MsgReceiptStatus.BadMsgHash -> MR.images.ic_double_check to Color.Red } is SndErrorAuth -> MR.images.ic_close to Color.Red - is SndError -> MR.images.ic_warning_filled to WarningYellow + is CISSndError -> MR.images.ic_close to Color.Red + is SndWarning -> MR.images.ic_warning_filled to WarningOrange is RcvNew -> MR.images.ic_circle_filled to primaryColor is RcvRead -> null is CIStatus.Invalid -> MR.images.ic_question_mark to metaColor @@ -2262,13 +2270,48 @@ sealed class CIStatus { is SndSent -> null is SndRcvd -> null is SndErrorAuth -> generalGetString(MR.strings.message_delivery_error_title) to generalGetString(MR.strings.message_delivery_error_desc) - is SndError -> generalGetString(MR.strings.message_delivery_error_title) to (generalGetString(MR.strings.unknown_error) + ": $agentError") + is CISSndError -> generalGetString(MR.strings.message_delivery_error_title) to agentError.errorInfo + is SndWarning -> generalGetString(MR.strings.message_delivery_warning_title) to agentError.errorInfo is RcvNew -> null is RcvRead -> null is Invalid -> "Invalid status" to this.text } } +@Serializable +sealed class SndError { + @Serializable @SerialName("auth") class Auth: SndError() + @Serializable @SerialName("quota") class Quota: SndError() + @Serializable @SerialName("expired") class Expired: SndError() + @Serializable @SerialName("relay") class Relay(val srvError: SrvError): SndError() + @Serializable @SerialName("proxy") class Proxy(val proxyServer: String, val srvError: SrvError): SndError() + @Serializable @SerialName("proxyRelay") class ProxyRelay(val proxyServer: String, val srvError: SrvError): SndError() + @Serializable @SerialName("other") class Other(val sndError: String): SndError() + + val errorInfo: String get() = when (this) { + is SndError.Auth -> generalGetString(MR.strings.snd_error_auth) + is SndError.Quota -> generalGetString(MR.strings.snd_error_quota) + is SndError.Expired -> generalGetString(MR.strings.snd_error_expired) + is SndError.Relay -> generalGetString(MR.strings.snd_error_relay).format(srvError.errorInfo) + is SndError.Proxy -> generalGetString(MR.strings.snd_error_proxy).format(proxyServer, srvError.errorInfo) + is SndError.ProxyRelay -> generalGetString(MR.strings.snd_error_proxy_relay).format(proxyServer, srvError.errorInfo) + is SndError.Other -> generalGetString(MR.strings.ci_status_other_error).format(sndError) + } +} + +@Serializable +sealed class SrvError { + @Serializable @SerialName("host") class Host: SrvError() + @Serializable @SerialName("version") class Version: SrvError() + @Serializable @SerialName("other") class Other(val srvError: String): SrvError() + + val errorInfo: String get() = when (this) { + is SrvError.Host -> generalGetString(MR.strings.srv_error_host) + is SrvError.Version -> generalGetString(MR.strings.srv_error_version) + is SrvError.Other -> srvError + } +} + @Serializable enum class MsgReceiptStatus { @SerialName("ok") Ok, @@ -3354,7 +3397,8 @@ data class ChatItemVersion( @Serializable data class MemberDeliveryStatus( val groupMemberId: Long, - val memberDeliveryStatus: CIStatus + val memberDeliveryStatus: CIStatus, + val sentViaProxy: Boolean? ) enum class NotificationPreviewMode { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 281bcefd45..e548ac3e6e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -130,6 +130,8 @@ class AppPreferences { }, set = fun(mode: TransportSessionMode) { _networkSessionMode.set(mode.name) } ) + val networkSMPProxyMode = mkStrPreference(SHARED_PREFS_NETWORK_SMP_PROXY_MODE, SMPProxyMode.Never.name) + val networkSMPProxyFallback = mkStrPreference(SHARED_PREFS_NETWORK_SMP_PROXY_FALLBACK, SMPProxyFallback.Allow.name) val networkHostMode = mkStrPreference(SHARED_PREFS_NETWORK_HOST_MODE, HostMode.OnionViaSocks.name) val networkRequiredHostMode = mkBoolPreference(SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE, false) val networkTCPConnectTimeout = mkTimeoutPreference(SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT, NetCfg.defaults.tcpConnectTimeout, NetCfg.proxyDefaults.tcpConnectTimeout) @@ -185,6 +187,8 @@ class AppPreferences { val desktopWindowState = mkStrPreference(SHARED_PREFS_DESKTOP_WINDOW_STATE, null) + val showSentViaProxy = mkBoolPreference(SHARED_PREFS_SHOW_SENT_VIA_RPOXY, false) + val iosCallKitEnabled = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_ENABLED, true) val iosCallKitCallsInRecents = mkBoolPreference(SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS, false) @@ -306,6 +310,8 @@ class AppPreferences { private const val SHARED_PREFS_NETWORK_USE_SOCKS_PROXY = "NetworkUseSocksProxy" private const val SHARED_PREFS_NETWORK_PROXY_HOST_PORT = "NetworkProxyHostPort" private const val SHARED_PREFS_NETWORK_SESSION_MODE = "NetworkSessionMode" + private const val SHARED_PREFS_NETWORK_SMP_PROXY_MODE = "NetworkSMPProxyMode" + private const val SHARED_PREFS_NETWORK_SMP_PROXY_FALLBACK = "NetworkSMPProxyFallback" private const val SHARED_PREFS_NETWORK_HOST_MODE = "NetworkHostMode" private const val SHARED_PREFS_NETWORK_REQUIRED_HOST_MODE = "NetworkRequiredHostMode" private const val SHARED_PREFS_NETWORK_TCP_CONNECT_TIMEOUT = "NetworkTCPConnectTimeout" @@ -348,6 +354,7 @@ class AppPreferences { private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "ConnectRemoteViaMulticastAuto" private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast" private const val SHARED_PREFS_DESKTOP_WINDOW_STATE = "DesktopWindowState" + private const val SHARED_PREFS_SHOW_SENT_VIA_RPOXY = "showSentViaProxy" private const val SHARED_PREFS_IOS_CALL_KIT_ENABLED = "iOSCallKitEnabled" private const val SHARED_PREFS_IOS_CALL_KIT_CALLS_IN_RECENTS = "iOSCallKitCallsInRecents" @@ -2310,6 +2317,8 @@ object ChatController { val hostMode = HostMode.valueOf(appPrefs.networkHostMode.get()!!) val requiredHostMode = appPrefs.networkRequiredHostMode.get() val sessionMode = appPrefs.networkSessionMode.get() + val smpProxyMode = SMPProxyMode.valueOf(appPrefs.networkSMPProxyMode.get()!!) + val smpProxyFallback = SMPProxyFallback.valueOf(appPrefs.networkSMPProxyFallback.get()!!) val tcpConnectTimeout = appPrefs.networkTCPConnectTimeout.get() val tcpTimeout = appPrefs.networkTCPTimeout.get() val tcpTimeoutPerKb = appPrefs.networkTCPTimeoutPerKb.get() @@ -2330,6 +2339,8 @@ object ChatController { hostMode = hostMode, requiredHostMode = requiredHostMode, sessionMode = sessionMode, + smpProxyMode = smpProxyMode, + smpProxyFallback = smpProxyFallback, tcpConnectTimeout = tcpConnectTimeout, tcpTimeout = tcpTimeout, tcpTimeoutPerKb = tcpTimeoutPerKb, @@ -2348,6 +2359,8 @@ object ChatController { appPrefs.networkHostMode.set(cfg.hostMode.name) appPrefs.networkRequiredHostMode.set(cfg.requiredHostMode) appPrefs.networkSessionMode.set(cfg.sessionMode) + appPrefs.networkSMPProxyMode.set(cfg.smpProxyMode.name) + appPrefs.networkSMPProxyFallback.set(cfg.smpProxyFallback.name) appPrefs.networkTCPConnectTimeout.set(cfg.tcpConnectTimeout) appPrefs.networkTCPTimeout.set(cfg.tcpTimeout) appPrefs.networkTCPTimeoutPerKb.set(cfg.tcpTimeoutPerKb) @@ -3033,9 +3046,12 @@ data class ParsedServerAddress ( @Serializable data class NetCfg( val socksProxy: String?, + val socksMode: SocksMode = SocksMode.Always, val hostMode: HostMode, val requiredHostMode: Boolean, val sessionMode: TransportSessionMode, + val smpProxyMode: SMPProxyMode, + val smpProxyFallback: SMPProxyFallback, val tcpConnectTimeout: Long, // microseconds val tcpTimeout: Long, // microseconds val tcpTimeoutPerKb: Long, // microseconds @@ -3064,6 +3080,8 @@ data class NetCfg( hostMode = HostMode.OnionViaSocks, requiredHostMode = false, sessionMode = TransportSessionMode.User, + smpProxyMode = SMPProxyMode.Never, + smpProxyFallback = SMPProxyFallback.Allow, tcpConnectTimeout = 25_000_000, tcpTimeout = 15_000_000, tcpTimeoutPerKb = 10_000, @@ -3079,6 +3097,8 @@ data class NetCfg( hostMode = HostMode.OnionViaSocks, requiredHostMode = false, sessionMode = TransportSessionMode.User, + smpProxyMode = SMPProxyMode.Never, + smpProxyFallback = SMPProxyFallback.Allow, tcpConnectTimeout = 35_000_000, tcpTimeout = 20_000_000, tcpTimeoutPerKb = 15_000, @@ -3117,6 +3137,35 @@ enum class HostMode { @SerialName("public") Public; } +@Serializable +enum class SocksMode { + @SerialName("always") Always, + @SerialName("onion") Onion; +} + +@Serializable +enum class SMPProxyMode { + @SerialName("always") Always, + @SerialName("unknown") Unknown, + @SerialName("unprotected") Unprotected, + @SerialName("never") Never; + + companion object { + val default = Never + } +} + +@Serializable +enum class SMPProxyFallback { + @SerialName("allow") Allow, + @SerialName("allowProtected") AllowProtected, + @SerialName("prohibit") Prohibit; + + companion object { + val default = Allow + } +} + @Serializable enum class TransportSessionMode { @SerialName("user") User, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt index df8e535f82..e00592bce9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatItemInfoView.kt @@ -304,7 +304,7 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools } @Composable - fun MemberDeliveryStatusView(member: GroupMember, status: CIStatus) { + fun MemberDeliveryStatusView(member: GroupMember, status: CIStatus, sentViaProxy: Boolean?) { SectionItemView( padding = PaddingValues(horizontal = 0.dp) ) { @@ -317,6 +317,18 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools overflow = TextOverflow.Ellipsis ) Spacer(Modifier.fillMaxWidth().weight(1f)) + if (sentViaProxy == true) { + Box( + Modifier.size(36.dp), + contentAlignment = Alignment.Center + ) { + Icon( + painterResource(MR.images.ic_arrow_forward), + contentDescription = null, + tint = CurrentColors.value.colors.secondary + ) + } + } val statusIcon = status.statusIcon(MaterialTheme.colors.primary, CurrentColors.value.colors.secondary) var modifier = Modifier.size(36.dp).clip(RoundedCornerShape(20.dp)) val info = status.statusInto @@ -357,8 +369,8 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools if (mss.isNotEmpty()) { SectionView(padding = PaddingValues(horizontal = DEFAULT_PADDING)) { Text(stringResource(MR.strings.delivery), style = MaterialTheme.typography.h2, modifier = Modifier.padding(bottom = DEFAULT_PADDING)) - mss.forEach { (member, status) -> - MemberDeliveryStatusView(member, status) + mss.forEach { (member, status, sentViaProxy) -> + MemberDeliveryStatusView(member, status, sentViaProxy) } } } else { @@ -482,10 +494,10 @@ fun ChatItemInfoView(chatRh: Long?, ci: ChatItem, ciInfo: ChatItemInfo, devTools } } -private fun membersStatuses(chatModel: ChatModel, memberDeliveryStatuses: List): List> { +private fun membersStatuses(chatModel: ChatModel, memberDeliveryStatuses: List): List> { return memberDeliveryStatuses.mapNotNull { mds -> chatModel.getGroupMember(mds.groupMemberId)?.let { mem -> - mem to mds.memberDeliveryStatus + Triple(mem, mds.memberDeliveryStatus, mds.sentViaProxy) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index e6f90c1599..6c3a884a0e 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -479,6 +479,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: }, onComposed, developerTools = chatModel.controller.appPrefs.developerTools.get(), + showViaProxy = chatModel.controller.appPrefs.showSentViaProxy.get(), ) } is ChatInfo.ContactConnection -> { @@ -548,6 +549,7 @@ fun ChatLayout( onSearchValueChanged: (String) -> Unit, onComposed: suspend (chatId: String) -> Unit, developerTools: Boolean, + showViaProxy: Boolean ) { val scope = rememberCoroutineScope() val attachmentDisabled = remember { derivedStateOf { composeState.value.attachmentDisabled } } @@ -606,7 +608,7 @@ fun ChatLayout( useLinkPreviews, linkMode, showMemberInfo, loadPrevMessages, deleteMessage, deleteMessages, receiveFile, cancelFile, joinGroup, acceptCall, acceptFeature, openDirectChat, updateContactStats, updateMemberStats, syncContactConnection, syncMemberConnection, findModelChat, findModelMember, - setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, + setReaction, showItemDetails, markRead, setFloatingButton, onComposed, developerTools, showViaProxy, ) } } @@ -885,6 +887,7 @@ fun BoxWithConstraintsScope.ChatItemsList( setFloatingButton: (@Composable () -> Unit) -> Unit, onComposed: suspend (chatId: String) -> Unit, developerTools: Boolean, + showViaProxy: Boolean ) { val listState = rememberLazyListState() val scope = rememberCoroutineScope() @@ -975,7 +978,7 @@ fun BoxWithConstraintsScope.ChatItemsList( tryOrShowError("${cItem.id}ChatItem", error = { CIBrokenComposableView(if (cItem.chatDir.sent) Alignment.CenterEnd else Alignment.CenterStart) }) { - ChatItemView(chat.remoteHostId, chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools) + ChatItemView(chat.remoteHostId, chat.chatInfo, cItem, composeState, provider, useLinkPreviews = useLinkPreviews, linkMode = linkMode, revealed = revealed, range = range, deleteMessage = deleteMessage, deleteMessages = deleteMessages, receiveFile = receiveFile, cancelFile = cancelFile, joinGroup = joinGroup, acceptCall = acceptCall, acceptFeature = acceptFeature, openDirectChat = openDirectChat, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember, scrollToItem = scrollToItem, setReaction = setReaction, showItemDetails = showItemDetails, developerTools = developerTools, showViaProxy = showViaProxy) } } @@ -1543,6 +1546,7 @@ fun PreviewChatLayout() { onSearchValueChanged = {}, onComposed = {}, developerTools = false, + showViaProxy = false, ) } } @@ -1615,6 +1619,7 @@ fun PreviewGroupChatLayout() { onSearchValueChanged = {}, onComposed = {}, developerTools = false, + showViaProxy = false, ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CICallItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CICallItemView.kt index 5d3d5aa94a..74c6e38566 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CICallItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CICallItemView.kt @@ -47,7 +47,7 @@ fun CICallItemView( CICallStatus.Error -> {} } - CIMetaView(cItem, timedMessagesTTL, showStatus = false, showEdited = false) + CIMetaView(cItem, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt index e3008f36b3..a2338bb895 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIGroupInvitationView.kt @@ -144,7 +144,7 @@ fun CIGroupInvitationView( } } - CIMetaView(ci, timedMessagesTTL, showStatus = false, showEdited = false) + CIMetaView(ci, timedMessagesTTL, showStatus = false, showEdited = false, showViaProxy = false) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt index b40d8989e1..14fa6910da 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIMetaView.kt @@ -35,7 +35,8 @@ fun CIMetaView( blue = minOf(metaColor.red * 1.33F, 1F)) }, showStatus: Boolean = true, - showEdited: Boolean = true + showEdited: Boolean = true, + showViaProxy: Boolean ) { Row(Modifier.padding(start = 3.dp), verticalAlignment = Alignment.CenterVertically) { if (chatItem.isDeletedContent) { @@ -53,7 +54,8 @@ fun CIMetaView( metaColor, paleMetaColor, showStatus = showStatus, - showEdited = showEdited + showEdited = showEdited, + showViaProxy = showViaProxy ) } } @@ -68,7 +70,8 @@ private fun CIMetaText( color: Color, paleColor: Color, showStatus: Boolean = true, - showEdited: Boolean = true + showEdited: Boolean = true, + showViaProxy: Boolean ) { if (showEdited && meta.itemEdited) { StatusIconText(painterResource(MR.images.ic_edit), color) @@ -82,6 +85,9 @@ private fun CIMetaText( } Spacer(Modifier.width(4.dp)) } + if (showViaProxy && meta.sentViaProxy == true) { + Icon(painterResource(MR.images.ic_arrow_forward), null, Modifier.height(17.dp), tint = CurrentColors.value.colors.secondary) + } if (showStatus) { val statusIcon = meta.statusIcon(MaterialTheme.colors.primary, color, paleColor) if (statusIcon != null) { @@ -105,7 +111,14 @@ private fun CIMetaText( } // the conditions in this function should match CIMetaText -fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?, encrypted: Boolean?, showStatus: Boolean = true, showEdited: Boolean = true): String { +fun reserveSpaceForMeta( + meta: CIMeta, + chatTTL: Int?, + encrypted: Boolean?, + showStatus: Boolean = true, + showEdited: Boolean = true, + showViaProxy: Boolean = false +): String { val iconSpace = " " var res = "" if (showEdited && meta.itemEdited) res += iconSpace @@ -116,6 +129,9 @@ fun reserveSpaceForMeta(meta: CIMeta, chatTTL: Int?, encrypted: Boolean?, showSt res += shortTimeText(ttl) } } + if (showViaProxy && meta.sentViaProxy == true) { + res += iconSpace + } if (showStatus && (meta.statusIcon(CurrentColors.value.colors.secondary) != null || !meta.disappearing)) { res += iconSpace } @@ -137,7 +153,8 @@ fun PreviewCIMetaView() { chatItem = ChatItem.getSampleData( 1, CIDirection.DirectSnd(), Clock.System.now(), "hello" ), - null + null, + showViaProxy = false ) } @@ -149,7 +166,8 @@ fun PreviewCIMetaViewUnread() { 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.RcvNew() ), - null + null, + showViaProxy = false ) } @@ -159,9 +177,10 @@ fun PreviewCIMetaViewSendFailed() { CIMetaView( chatItem = ChatItem.getSampleData( 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", - status = CIStatus.SndError("CMD SYNTAX") + status = CIStatus.CISSndError(SndError.Other("CMD SYNTAX")) ), - null + null, + showViaProxy = false ) } @@ -172,7 +191,8 @@ fun PreviewCIMetaViewSendNoAuth() { chatItem = ChatItem.getSampleData( 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndErrorAuth() ), - null + null, + showViaProxy = false ) } @@ -183,7 +203,8 @@ fun PreviewCIMetaViewSendSent() { chatItem = ChatItem.getSampleData( 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", status = CIStatus.SndSent(SndCIStatusProgress.Complete) ), - null + null, + showViaProxy = false ) } @@ -195,7 +216,8 @@ fun PreviewCIMetaViewEdited() { 1, CIDirection.DirectSnd(), Clock.System.now(), "hello", itemEdited = true ), - null + null, + showViaProxy = false ) } @@ -208,7 +230,8 @@ fun PreviewCIMetaViewEditedUnread() { itemEdited = true, status= CIStatus.RcvNew() ), - null + null, + showViaProxy = false ) } @@ -221,7 +244,8 @@ fun PreviewCIMetaViewEditedSent() { itemEdited = true, status= CIStatus.SndSent(SndCIStatusProgress.Complete) ), - null + null, + showViaProxy = false ) } @@ -230,6 +254,7 @@ fun PreviewCIMetaViewEditedSent() { fun PreviewCIMetaViewDeletedContent() { CIMetaView( chatItem = ChatItem.getDeletedContentSampleData(), - null + null, + showViaProxy = false ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt index 318a8a6a05..eb5d1e731a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIRcvDecryptionError.kt @@ -174,7 +174,7 @@ fun DecryptionErrorItemFixButton( ) } } - CIMetaView(ci, timedMessagesTTL = null) + CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false) } } } @@ -202,7 +202,7 @@ fun DecryptionErrorItem( }, style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp) ) - CIMetaView(ci, timedMessagesTTL = null) + CIMetaView(ci, timedMessagesTTL = null, showViaProxy = false) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt index e59c5f1370..cac89b2587 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/CIVoiceView.kt @@ -35,6 +35,7 @@ fun CIVoiceView( hasText: Boolean, ci: ChatItem, timedMessagesTTL: Int?, + showViaProxy: Boolean, longClick: () -> Unit, receiveFile: (Long) -> Unit, ) { @@ -76,7 +77,7 @@ fun CIVoiceView( durationText(time / 1000) } } - VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, play, pause, longClick, receiveFile) { + VoiceLayout(file, ci, text, audioPlaying, progress, duration, brokenAudio, sent, hasText, timedMessagesTTL, showViaProxy, play, pause, longClick, receiveFile) { AudioPlayer.seekTo(it, progress, fileSource.value?.filePath) } } else { @@ -102,6 +103,7 @@ private fun VoiceLayout( sent: Boolean, hasText: Boolean, timedMessagesTTL: Int?, + showViaProxy: Boolean, play: () -> Unit, pause: () -> Unit, longClick: () -> Unit, @@ -171,7 +173,7 @@ private fun VoiceLayout( VoiceMsgIndicator(file, audioPlaying.value, sent, hasText, progress, duration, brokenAudio, play, pause, longClick, receiveFile) } Box(Modifier.padding(top = 6.dp, end = 6.dp)) { - CIMetaView(ci, timedMessagesTTL) + CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy) } } } @@ -186,7 +188,7 @@ private fun VoiceLayout( } } Box(Modifier.padding(top = 6.dp)) { - CIMetaView(ci, timedMessagesTTL) + CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt index 8dc7f8bcea..0bc0415590 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/ChatItemView.kt @@ -68,6 +68,7 @@ fun ChatItemView( setReaction: (ChatInfo, ChatItem, Boolean, MsgReaction) -> Unit, showItemDetails: (ChatInfo, ChatItem) -> Unit, developerTools: Boolean, + showViaProxy: Boolean ) { val uriHandler = LocalUriHandler.current val sent = cItem.chatDir.sent @@ -83,17 +84,15 @@ fun ChatItemView( .fillMaxWidth(), contentAlignment = alignment, ) { - val onClick = { - when (cItem.meta.itemStatus) { - is CIStatus.SndErrorAuth -> { - showMsgDeliveryErrorAlert(generalGetString(MR.strings.message_delivery_error_desc)) - } - is CIStatus.SndError -> { - showMsgDeliveryErrorAlert(generalGetString(MR.strings.unknown_error) + ": ${cItem.meta.itemStatus.agentError}") - } - else -> {} + val info = cItem.meta.itemStatus.statusInto + val onClick = if (info != null) { + { + AlertManager.shared.showAlertMsg( + title = info.first, + text = info.second, + ) } - } + } else { {} } @Composable fun ChatItemReactions() { @@ -130,7 +129,7 @@ fun ChatItemView( ) { @Composable fun framedItemView() { - FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showMenu, receiveFile, onLinkLongClick, scrollToItem) + FramedItemView(cInfo, cItem, uriHandler, imageProvider, linkMode = linkMode, showViaProxy = showViaProxy, showMenu, receiveFile, onLinkLongClick, scrollToItem) } fun deleteMessageQuestionText(): String { @@ -334,14 +333,14 @@ fun ChatItemView( fun ContentItem() { val mc = cItem.content.msgContent if (cItem.meta.itemDeleted != null && (!revealed.value || cItem.isDeletedContent)) { - MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed) + MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy) MarkedDeletedItemDropdownMenu() } else { if (cItem.quotedItem == null && cItem.meta.itemForwarded == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) { if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) { - EmojiItemView(cItem, cInfo.timedMessagesTTL) + EmojiItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy) } else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) { - CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, longClick = { onLinkLongClick("") }, receiveFile) + CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, longClick = { onLinkLongClick("") }, receiveFile) } else { framedItemView() } @@ -353,7 +352,7 @@ fun ChatItemView( } @Composable fun LegacyDeletedItem() { - DeletedItemView(cItem, cInfo.timedMessagesTTL) + DeletedItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy) DefaultDropdownMenu(showMenu) { ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) DeleteItemAction(cItem, revealed, showMenu, questionText = deleteMessageQuestionText(), deleteMessage, deleteMessages) @@ -406,7 +405,7 @@ fun ChatItemView( @Composable fun DeletedItem() { - MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed) + MarkedDeletedItemView(cItem, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy) DefaultDropdownMenu(showMenu) { ItemInfoAction(cInfo, cItem, showItemDetails, showMenu) DeleteItemAction(cItem, revealed, showMenu, questionText = generalGetString(MR.strings.delete_message_cannot_be_undone_warning), deleteMessage, deleteMessages) @@ -816,13 +815,6 @@ fun moderateMessageAlertDialog(chatItem: ChatItem, questionText: String, deleteM ) } -private fun showMsgDeliveryErrorAlert(description: String) { - AlertManager.shared.showAlertMsg( - title = generalGetString(MR.strings.message_delivery_error_title), - text = description, - ) -} - expect fun copyItemToClipboard(cItem: ChatItem, clipboard: ClipboardManager) @Preview @@ -858,6 +850,7 @@ fun PreviewChatItemView() { setReaction = { _, _, _, _ -> }, showItemDetails = { _, _ -> }, developerTools = false, + showViaProxy = false ) } } @@ -893,6 +886,7 @@ fun PreviewChatItemViewDeletedContent() { setReaction = { _, _, _, _ -> }, showItemDetails = { _, _ -> }, developerTools = false, + showViaProxy = false ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt index 7514b6e280..644c1997c4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/DeletedItemView.kt @@ -16,7 +16,7 @@ import chat.simplex.common.model.ChatItem import chat.simplex.common.ui.theme.* @Composable -fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) { +fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean) { val sent = ci.chatDir.sent val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage @@ -36,7 +36,7 @@ fun DeletedItemView(ci: ChatItem, timedMessagesTTL: Int?) { style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), modifier = Modifier.padding(end = 8.dp) ) - CIMetaView(ci, timedMessagesTTL) + CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy) } } } @@ -50,7 +50,8 @@ fun PreviewDeletedItemView() { SimpleXTheme { DeletedItemView( ChatItem.getDeletedContentSampleData(), - null + null, + showViaProxy = false ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.kt index 3ede737ffa..4969eccbb6 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/EmojiItemView.kt @@ -17,13 +17,13 @@ val largeEmojiFont: TextStyle = TextStyle(fontSize = 48.sp, fontFamily = EmojiFo val mediumEmojiFont: TextStyle = TextStyle(fontSize = 36.sp, fontFamily = EmojiFont) @Composable -fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?) { +fun EmojiItemView(chatItem: ChatItem, timedMessagesTTL: Int?, showViaProxy: Boolean) { Column( Modifier.padding(vertical = 8.dp, horizontal = 12.dp), horizontalAlignment = Alignment.CenterHorizontally ) { EmojiText(chatItem.content.text) - CIMetaView(chatItem, timedMessagesTTL) + CIMetaView(chatItem, timedMessagesTTL, showViaProxy = showViaProxy) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt index 09cb1cfd23..aec314d2c1 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/FramedItemView.kt @@ -33,6 +33,7 @@ fun FramedItemView( uriHandler: UriHandler? = null, imageProvider: (() -> ImageGalleryProvider)? = null, linkMode: SimplexLinkMode, + showViaProxy: Boolean, showMenu: MutableState, receiveFile: (Long) -> Unit, onLinkLongClick: (link: String) -> Unit = {}, @@ -180,7 +181,7 @@ fun FramedItemView( fun ciFileView(ci: ChatItem, text: String) { CIFileView(ci.file, ci.meta.itemEdited, showMenu, receiveFile) if (text != "" || ci.meta.isLive) { - CIMarkdownText(ci, chatTTL, linkMode = linkMode, uriHandler) + CIMarkdownText(ci, chatTTL, linkMode = linkMode, uriHandler, showViaProxy = showViaProxy) } } @@ -244,7 +245,7 @@ fun FramedItemView( if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { - CIMarkdownText(ci, chatTTL, linkMode, uriHandler) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy) } } is MsgContent.MCVideo -> { @@ -252,35 +253,35 @@ fun FramedItemView( if (mc.text == "" && !ci.meta.isLive) { metaColor = Color.White } else { - CIMarkdownText(ci, chatTTL, linkMode, uriHandler) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy) } } is MsgContent.MCVoice -> { - CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, longClick = { onLinkLongClick("") }, receiveFile) + CIVoiceView(mc.duration, ci.file, ci.meta.itemEdited, ci.chatDir.sent, hasText = true, ci, timedMessagesTTL = chatTTL, showViaProxy = showViaProxy, longClick = { onLinkLongClick("") }, receiveFile) if (mc.text != "") { - CIMarkdownText(ci, chatTTL, linkMode, uriHandler) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler, showViaProxy = showViaProxy) } } is MsgContent.MCFile -> ciFileView(ci, mc.text) is MsgContent.MCUnknown -> if (ci.file == null) { - CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy) } else { ciFileView(ci, mc.text) } is MsgContent.MCLink -> { ChatItemLinkView(mc.preview) Box(Modifier.widthIn(max = DEFAULT_MAX_IMAGE_WIDTH)) { - CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick) + CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy) } } - else -> CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick) + else -> CIMarkdownText(ci, chatTTL, linkMode, uriHandler, onLinkLongClick, showViaProxy = showViaProxy) } } } } Box(Modifier.padding(bottom = 6.dp, end = 12.dp)) { - CIMetaView(ci, chatTTL, metaColor) + CIMetaView(ci, chatTTL, metaColor, showViaProxy = showViaProxy) } } } @@ -292,14 +293,15 @@ fun CIMarkdownText( chatTTL: Int?, linkMode: SimplexLinkMode, uriHandler: UriHandler?, - onLinkLongClick: (link: String) -> Unit = {} + onLinkLongClick: (link: String) -> Unit = {}, + showViaProxy: Boolean ) { Box(Modifier.padding(vertical = 6.dp, horizontal = 12.dp)) { val text = if (ci.meta.isLive) ci.content.msgContent?.text ?: ci.text else ci.text MarkdownText( text, if (text.isEmpty()) emptyList() else ci.formattedText, toggleSecrets = true, meta = ci.meta, chatTTL = chatTTL, linkMode = linkMode, - uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick + uriHandler = uriHandler, senderBold = true, onLinkLongClick = onLinkLongClick, showViaProxy = showViaProxy ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt index 7be0cc2f6c..bd2aaf140c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/IntegrityErrorItemView.kt @@ -69,7 +69,7 @@ fun CIMsgError(ci: ChatItem, timedMessagesTTL: Int?, onClick: () -> Unit) { style = MaterialTheme.typography.body1.copy(lineHeight = 22.sp), modifier = Modifier.padding(end = 8.dp) ) - CIMetaView(ci, timedMessagesTTL) + CIMetaView(ci, timedMessagesTTL, showViaProxy = false) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt index 0e2e8867cb..6ecec47b6f 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/MarkedDeletedItemView.kt @@ -20,7 +20,7 @@ import dev.icerock.moko.resources.compose.stringResource import kotlinx.datetime.Clock @Composable -fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: MutableState) { +fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: MutableState, showViaProxy: Boolean) { val sentColor = CurrentColors.collectAsState().value.appColors.sentMessage val receivedColor = CurrentColors.collectAsState().value.appColors.receivedMessage Surface( @@ -35,7 +35,7 @@ fun MarkedDeletedItemView(ci: ChatItem, timedMessagesTTL: Int?, revealed: Mutabl Box(Modifier.weight(1f, false)) { MergedMarkedDeletedText(ci, revealed) } - CIMetaView(ci, timedMessagesTTL) + CIMetaView(ci, timedMessagesTTL, showViaProxy = showViaProxy) } } } @@ -112,7 +112,8 @@ fun PreviewMarkedDeletedItemView() { SimpleXTheme { DeletedItemView( ChatItem.getSampleData(itemDeleted = CIDeleted.Deleted(Clock.System.now())), - null + null, + showViaProxy = false ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt index 66061767e5..343fc47f76 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/item/TextItemView.kt @@ -69,7 +69,8 @@ fun MarkdownText ( modifier: Modifier = Modifier, linkMode: SimplexLinkMode, inlineContent: Pair Unit, Map>? = null, - onLinkLongClick: (link: String) -> Unit = {} + onLinkLongClick: (link: String) -> Unit = {}, + showViaProxy: Boolean = false ) { val textLayoutDirection = remember (text) { if (isRtl(text.subSequence(0, kotlin.math.min(50, text.length)))) LayoutDirection.Rtl else LayoutDirection.Ltr @@ -77,7 +78,7 @@ fun MarkdownText ( val reserve = if (textLayoutDirection != LocalLayoutDirection.current && meta != null) { "\n" } else if (meta != null) { - reserveSpaceForMeta(meta, chatTTL, null) // LALAL + reserveSpaceForMeta(meta, chatTTL, null, showViaProxy = showViaProxy) } else { " " } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt index 3ef219fc9f..cd3e5902d0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/AdvancedNetworkSettings.kt @@ -66,6 +66,8 @@ fun AdvancedNetworkSettingsView(chatModel: ChatModel) { hostMode = currentCfg.value.hostMode, requiredHostMode = currentCfg.value.requiredHostMode, sessionMode = currentCfg.value.sessionMode, + smpProxyMode = currentCfg.value.smpProxyMode, + smpProxyFallback = currentCfg.value.smpProxyFallback, tcpConnectTimeout = networkTCPConnectTimeout.value, tcpTimeout = networkTCPTimeout.value, tcpTimeoutPerKb = networkTCPTimeoutPerKb.value, diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index 6ca14aace2..83ca79bd3a 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -255,8 +255,20 @@ Message delivery error + Message delivery warning Most likely this contact has deleted the connection with you. + + Error: %1$s + Wrong key or unknown connection - most likely this connection is deleted. + Capacity exceeded - recipient did not receive previously sent messages. + Network issues - message expired after many attempts to send it. + Destination server error: %1$s + Forwarding server: %1$s\nError: %2$s + Forwarding server: %1$s\nDestination server error: %2$s + Server address is incompatible with network settings. + Server version is incompatible with network settings. + Reply Share diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_forward.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_forward.svg new file mode 100644 index 0000000000..d393921c05 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_arrow_forward.svg @@ -0,0 +1 @@ + \ No newline at end of file