diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 53b94434e..19fa1d910 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -357,6 +357,14 @@ func apiGroupMemberInfo(_ groupId: Int64, _ groupMemberId: Int64) async throws - throw r } +func apiSwitchContact(contactId: Int64) async throws { + try await sendCommandOkResp(.apiSwitchContact(contactId: contactId)) +} + +func apiSwitchGroupMember(_ groupId: Int64, _ groupMemberId: Int64) async throws { + try await sendCommandOkResp(.apiSwitchGroupMember(groupId: groupId, groupMemberId: groupMemberId)) +} + func apiAddContact() async -> String? { let r = await chatSendCmd(.addContact, bgTask: false) if case let .invitation(connReqInvitation) = r { return connReqInvitation } diff --git a/apps/ios/Shared/Views/Chat/ChatInfoView.swift b/apps/ios/Shared/Views/Chat/ChatInfoView.swift index 97e17c7a0..0e1060fc8 100644 --- a/apps/ios/Shared/Views/Chat/ChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/ChatInfoView.swift @@ -30,7 +30,14 @@ func localizedInfoRow(_ title: LocalizedStringKey, _ value: LocalizedStringKey) @ViewBuilder func smpServers(_ title: LocalizedStringKey, _ servers: [String]?) -> some View { if let servers = servers, servers.count > 0 { - infoRow(title, serverHost(servers[0])) + HStack { + Text(title).frame(width: 120, alignment: .leading) + Button(serverHost(servers[0])) { + UIPasteboard.general.string = servers.joined(separator: ";") + } + .foregroundColor(.secondary) + .lineLimit(1) + } } } @@ -47,7 +54,7 @@ struct ChatInfoView: View { @Environment(\.dismiss) var dismiss: DismissAction @ObservedObject var chat: Chat var contact: Contact - var connectionStats: ConnectionStats? + @Binding var connectionStats: ConnectionStats? var customUserProfile: Profile? @State var localAlias: String @FocusState private var aliasTextFieldFocused: Bool @@ -88,12 +95,15 @@ struct ChatInfoView: View { } } - if let connStats = connectionStats { - Section("Servers") { - networkStatusRow() - .onTapGesture { - alert = .networkStatusAlert - } + Section("Servers") { + networkStatusRow() + .onTapGesture { + alert = .networkStatusAlert + } + Button("Switch receiving address") { + + } + if let connStats = connectionStats { smpServers("Receiving via", connStats.rcvServers) smpServers("Sending via", connStats.sndServers) } @@ -258,6 +268,11 @@ struct ChatInfoView: View { struct ChatInfoView_Previews: PreviewProvider { static var previews: some View { - ChatInfoView(chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), contact: Contact.sampleData, localAlias: "") + ChatInfoView( + chat: Chat(chatInfo: ChatInfo.sampleData.direct, chatItems: []), + contact: Contact.sampleData, + connectionStats: Binding.constant(nil), + localAlias: "" + ) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupEventView.swift b/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift similarity index 85% rename from apps/ios/Shared/Views/Chat/ChatItem/CIGroupEventView.swift rename to apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift index 4c5579653..167186fde 100644 --- a/apps/ios/Shared/Views/Chat/ChatItem/CIGroupEventView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItem/CIEventView.swift @@ -1,5 +1,5 @@ // -// CIGroupEventView.swift +// CIEventView.swift // SimpleX (iOS) // // Created by JRoberts on 20.07.2022. @@ -9,7 +9,7 @@ import SwiftUI import SimpleXChat -struct CIGroupEventView: View { +struct CIEventView: View { var chatItem: ChatItem var body: some View { @@ -43,8 +43,8 @@ struct CIGroupEventView: View { } } -struct CIGroupEventView_Previews: PreviewProvider { +struct CIEventView_Previews: PreviewProvider { static var previews: some View { - CIGroupEventView(chatItem: ChatItem.getGroupEventSample()) + CIEventView(chatItem: ChatItem.getGroupEventSample()) } } diff --git a/apps/ios/Shared/Views/Chat/ChatItemView.swift b/apps/ios/Shared/Views/Chat/ChatItemView.swift index 983537a33..df91529a6 100644 --- a/apps/ios/Shared/Views/Chat/ChatItemView.swift +++ b/apps/ios/Shared/Views/Chat/ChatItemView.swift @@ -27,8 +27,10 @@ struct ChatItemView: View { case .rcvIntegrityError: IntegrityErrorItemView(chatItem: chatItem, showMember: showMember) case let .rcvGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole) case let .sndGroupInvitation(groupInvitation, memberRole): groupInvitationItemView(groupInvitation, memberRole) - case .rcvGroupEvent: groupEventItemView() - case .sndGroupEvent: groupEventItemView() + case .rcvGroupEvent: eventItemView() + case .sndGroupEvent: eventItemView() + case .rcvConnEvent: eventItemView() + case .sndConnEvent: eventItemView() } } @@ -52,8 +54,8 @@ struct ChatItemView: View { CIGroupInvitationView(chatItem: chatItem, groupInvitation: groupInvitation, memberRole: memberRole, chatIncognito: chatInfo.incognito) } - private func groupEventItemView() -> some View { - CIGroupEventView(chatItem: chatItem) + private func eventItemView() -> some View { + CIEventView(chatItem: chatItem) } } diff --git a/apps/ios/Shared/Views/Chat/ChatView.swift b/apps/ios/Shared/Views/Chat/ChatView.swift index 443d87fcc..d6282dd6f 100644 --- a/apps/ios/Shared/Views/Chat/ChatView.swift +++ b/apps/ios/Shared/Views/Chat/ChatView.swift @@ -107,7 +107,7 @@ struct ChatView: View { connectionStats = nil customUserProfile = nil }) { - ChatInfoView(chat: chat, contact: contact, connectionStats: connectionStats, customUserProfile: customUserProfile, localAlias: chat.chatInfo.localAlias) + ChatInfoView(chat: chat, contact: contact, connectionStats: $connectionStats, customUserProfile: customUserProfile, localAlias: chat.chatInfo.localAlias) } } else if case let .group(groupInfo) = cInfo { Button { @@ -393,7 +393,7 @@ struct ChatView: View { } } .sheet(item: $selectedMember, onDismiss: { memberConnectionStats = nil }) { member in - GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: memberConnectionStats) + GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: $memberConnectionStats) } } else { Rectangle().fill(.clear) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift index 687357f03..41db56c39 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupChatInfoView.swift @@ -72,7 +72,7 @@ struct GroupChatInfoView: View { AddGroupMembersView(chat: chat, groupInfo: groupInfo) } .sheet(item: $selectedMember, onDismiss: { connectionStats = nil }) { member in - GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: connectionStats) + GroupMemberInfoView(groupInfo: groupInfo, member: member, connectionStats: $connectionStats) } .sheet(isPresented: $showGroupProfile) { GroupProfileView(groupId: groupInfo.apiId, groupProfile: groupInfo.groupProfile) diff --git a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift index 2dc7554fb..24ca39f7d 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupMemberInfoView.swift @@ -14,7 +14,7 @@ struct GroupMemberInfoView: View { @Environment(\.dismiss) var dismiss: DismissAction var groupInfo: GroupInfo @State var member: GroupMember - var connectionStats: ConnectionStats? + @Binding var connectionStats: ConnectionStats? @State private var newRole: GroupMemberRole = .member @State private var alert: GroupMemberInfoViewAlert? @AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false @@ -77,9 +77,12 @@ struct GroupMemberInfoView: View { } } - if let connStats = connectionStats { - Section("Servers") { - // TODO network connection status + Section("Servers") { + // TODO network connection status + Button("Switch receiving address") { + + } + if let connStats = connectionStats { smpServers("Receiving via", connStats.rcvServers) smpServers("Sending via", connStats.sndServers) } @@ -214,6 +217,10 @@ struct GroupMemberInfoView: View { struct GroupMemberInfoView_Previews: PreviewProvider { static var previews: some View { - GroupMemberInfoView(groupInfo: GroupInfo.sampleData, member: GroupMember.sampleData) + GroupMemberInfoView( + groupInfo: GroupInfo.sampleData, + member: GroupMember.sampleData, + connectionStats: Binding.constant(nil) + ) } } diff --git a/apps/ios/SimpleX.xcodeproj/project.pbxproj b/apps/ios/SimpleX.xcodeproj/project.pbxproj index b1923326a..3a17317a2 100644 --- a/apps/ios/SimpleX.xcodeproj/project.pbxproj +++ b/apps/ios/SimpleX.xcodeproj/project.pbxproj @@ -125,7 +125,7 @@ 64328561290BEE2B00FBE5C8 /* libgmpxx.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6432855C290BEE2B00FBE5C8 /* libgmpxx.a */; }; 64328562290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu-ghc8.10.7.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6432855D290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu-ghc8.10.7.a */; }; 64328563290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 6432855E290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu.a */; }; - 6440CA00288857A10062C672 /* CIGroupEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIGroupEventView.swift */; }; + 6440CA00288857A10062C672 /* CIEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440C9FF288857A10062C672 /* CIEventView.swift */; }; 6440CA03288AECA70062C672 /* AddGroupMembersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */; }; 6442E0BA287F169300CEC0F9 /* AddGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */; }; 6442E0BE2880182D00CEC0F9 /* GroupChatInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */; }; @@ -324,7 +324,7 @@ 6432855C290BEE2B00FBE5C8 /* libgmpxx.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libgmpxx.a; sourceTree = ""; }; 6432855D290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu-ghc8.10.7.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu-ghc8.10.7.a"; sourceTree = ""; }; 6432855E290BEE2B00FBE5C8 /* libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libHSsimplex-chat-4.2.0-LeYwSLlznBGLTzzJTly7Iu.a"; sourceTree = ""; }; - 6440C9FF288857A10062C672 /* CIGroupEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupEventView.swift; sourceTree = ""; }; + 6440C9FF288857A10062C672 /* CIEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIEventView.swift; sourceTree = ""; }; 6440CA02288AECA70062C672 /* AddGroupMembersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupMembersView.swift; sourceTree = ""; }; 6442E0B9287F169300CEC0F9 /* AddGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddGroupView.swift; sourceTree = ""; }; 6442E0BD2880182D00CEC0F9 /* GroupChatInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupChatInfoView.swift; sourceTree = ""; }; @@ -647,7 +647,7 @@ 5C029EA72837DBB3004A9677 /* CICallItemView.swift */, 5C3F1D552842B68D00EC8A82 /* IntegrityErrorItemView.swift */, 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */, - 6440C9FF288857A10062C672 /* CIGroupEventView.swift */, + 6440C9FF288857A10062C672 /* CIEventView.swift */, ); path = ChatItem; sourceTree = ""; @@ -966,7 +966,7 @@ 5C2E260727A2941F00F70299 /* SimpleXAPI.swift in Sources */, 5CB924D427A853F100ACCCDD /* SettingsButton.swift in Sources */, 3C714777281C081000CB4D4B /* WebRTCView.swift in Sources */, - 6440CA00288857A10062C672 /* CIGroupEventView.swift in Sources */, + 6440CA00288857A10062C672 /* CIEventView.swift in Sources */, 5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */, 5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */, 64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */, diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index c9a3f4e97..8375890cb 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -55,6 +55,8 @@ public enum ChatCommand { case apiSetChatSettings(type: ChatType, id: Int64, chatSettings: ChatSettings) case apiContactInfo(contactId: Int64) case apiGroupMemberInfo(groupId: Int64, groupMemberId: Int64) + case apiSwitchContact(contactId: Int64) + case apiSwitchGroupMember(groupId: Int64, groupMemberId: Int64) case addContact case connect(connReq: String) case apiDeleteChat(type: ChatType, id: Int64) @@ -131,6 +133,8 @@ public enum ChatCommand { case let .apiSetChatSettings(type, id, chatSettings): return "/_settings \(ref(type, id)) \(encodeJSON(chatSettings))" case let .apiContactInfo(contactId): return "/_info @\(contactId)" case let .apiGroupMemberInfo(groupId, groupMemberId): return "/_info #\(groupId) \(groupMemberId)" + case let .apiSwitchContact(contactId): return "/_switch @\(contactId)" + case let .apiSwitchGroupMember(groupId, groupMemberId): return "/_switch #\(groupId) \(groupMemberId)" case .addContact: return "/connect" case let .connect(connReq): return "/connect \(connReq)" case let .apiDeleteChat(type, id): return "/_delete \(ref(type, id))" @@ -206,6 +210,8 @@ public enum ChatCommand { case .apiSetChatSettings: return "apiSetChatSettings" case .apiContactInfo: return "apiContactInfo" case .apiGroupMemberInfo: return "apiGroupMemberInfo" + case .apiSwitchContact: return "apiSwitchContact" + case .apiSwitchGroupMember: return "apiSwitchGroupMember" case .addContact: return "addContact" case .connect: return "connect" case .apiDeleteChat: return "apiDeleteChat" @@ -241,7 +247,7 @@ public enum ChatCommand { } func smpServersStr(smpServers: [String]) -> String { - smpServers.isEmpty ? "default" : smpServers.joined(separator: ",") + smpServers.isEmpty ? "default" : smpServers.joined(separator: ";") } func chatItemTTLStr(seconds: Int64?) -> String { diff --git a/apps/ios/SimpleXChat/ChatTypes.swift b/apps/ios/SimpleXChat/ChatTypes.swift index 15fc3a9f2..9db33c4b4 100644 --- a/apps/ios/SimpleXChat/ChatTypes.swift +++ b/apps/ios/SimpleXChat/ChatTypes.swift @@ -711,6 +711,11 @@ public struct GroupMember: Identifiable, Decodable { ) } +public struct GroupMemberRef: Decodable { + var groupMemberId: Int64 + var profile: Profile +} + public enum GroupMemberRole: String, Identifiable, CaseIterable, Comparable, Decodable { case member = "member" case admin = "admin" @@ -1096,6 +1101,8 @@ public enum CIContent: Decodable, ItemContent { case sndGroupInvitation(groupInvitation: CIGroupInvitation, memberRole: GroupMemberRole) case rcvGroupEvent(rcvGroupEvent: RcvGroupEvent) case sndGroupEvent(sndGroupEvent: SndGroupEvent) + case rcvConnEvent(rcvConnEvent: RcvConnEvent) + case sndConnEvent(sndConnEvent: SndConnEvent) public var text: String { get { @@ -1111,6 +1118,8 @@ public enum CIContent: Decodable, ItemContent { case let .sndGroupInvitation(groupInvitation, _): return groupInvitation.text case let .rcvGroupEvent(rcvGroupEvent): return rcvGroupEvent.text case let .sndGroupEvent(sndGroupEvent): return sndGroupEvent.text + case let .rcvConnEvent(rcvConnEvent): return rcvConnEvent.text + case let .sndConnEvent(sndConnEvent): return sndConnEvent.text } } } @@ -1498,6 +1507,44 @@ public enum SndGroupEvent: Decodable { } } +public enum RcvConnEvent: Decodable { + case switchQueue(phase: SwitchPhase) + + var text: String { + switch self { + case let .switchQueue(phase): + if case .completed = phase { + return NSLocalizedString("changed address for you", comment: "chat item text") + } + return NSLocalizedString("changing address...", comment: "chat item text") + } + } +} + +public enum SndConnEvent: Decodable { + case switchQueue(phase: SwitchPhase, member: GroupMemberRef?) + + var text: String { + switch self { + case let .switchQueue(phase, member): + if let name = member?.profile.profileViewName { + return phase == .completed + ? String.localizedStringWithFormat(NSLocalizedString("you changed address for %@", comment: "chat item text"), name) + : String.localizedStringWithFormat(NSLocalizedString("changing address for %@...", comment: "chat item text"), name) + } + return phase == .completed + ? NSLocalizedString("you changed address", comment: "chat item text") + : NSLocalizedString("changing address...", comment: "chat item text") + } + } +} + +public enum SwitchPhase: String, Decodable { + case started + case confirmed + case completed +} + public enum ChatItemTTL: Hashable, Identifiable, Comparable { case day case week diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index a04aefcec..1efedbe4c 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -1760,8 +1760,8 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = SWITCH qd phase cStats -> do toView . CRContactSwitch ct $ SwitchProgress qd phase cStats when (phase /= SPConfirmed) $ case qd of - QDRcv -> createInternalChatItem (CDDirectSnd ct) (CISndConnEvent $ SCESwitch phase Nothing) Nothing - QDSnd -> createInternalChatItem (CDDirectRcv ct) (CIRcvConnEvent $ RCESwitch phase) Nothing + QDRcv -> createInternalChatItem (CDDirectSnd ct) (CISndConnEvent $ SCESwitchQueue phase Nothing) Nothing + QDSnd -> createInternalChatItem (CDDirectRcv ct) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing OK -> -- [async agent commands] continuation on receiving OK withCompletedCommand conn agentMsg $ \CommandData {cmdFunction, cmdId} -> @@ -1907,8 +1907,8 @@ processAgentMessage (Just user@User {userId}) corrId agentConnId agentMessage = SWITCH qd phase cStats -> do toView . CRGroupMemberSwitch gInfo m $ SwitchProgress qd phase cStats when (phase /= SPConfirmed) $ case qd of - QDRcv -> createInternalChatItem (CDGroupSnd gInfo) (CISndConnEvent . SCESwitch phase . Just $ groupMemberRef m) Nothing - QDSnd -> createInternalChatItem (CDGroupRcv gInfo m) (CIRcvConnEvent $ RCESwitch phase) Nothing + QDRcv -> createInternalChatItem (CDGroupSnd gInfo) (CISndConnEvent . SCESwitchQueue phase . Just $ groupMemberRef m) Nothing + QDSnd -> createInternalChatItem (CDGroupRcv gInfo m) (CIRcvConnEvent $ RCESwitchQueue phase) Nothing OK -> -- [async agent commands] continuation on receiving OK withCompletedCommand conn agentMsg $ \CommandData {cmdFunction, cmdId} -> diff --git a/src/Simplex/Chat/Messages.hs b/src/Simplex/Chat/Messages.hs index f63e7765c..bcde4cbd9 100644 --- a/src/Simplex/Chat/Messages.hs +++ b/src/Simplex/Chat/Messages.hs @@ -525,13 +525,13 @@ sndGroupEventToText = \case rcvConnEventToText :: RcvConnEvent -> Text rcvConnEventToText = \case - RCESwitch phase -> case phase of + RCESwitchQueue phase -> case phase of SPCompleted -> "changed address for you" _ -> decodeLatin1 (strEncode phase) <> " changing address for you..." sndConnEventToText :: SndConnEvent -> Text sndConnEventToText = \case - SCESwitch phase m -> case phase of + SCESwitchQueue phase m -> case phase of SPCompleted -> "you changed address" <> forMember m _ -> decodeLatin1 (strEncode phase) <> " changing address" <> forMember m <> "..." where @@ -620,10 +620,10 @@ instance ToJSON DBSndGroupEvent where toJSON (SGE v) = J.genericToJSON (singleFieldJSON $ dropPrefix "SGE") v toEncoding (SGE v) = J.genericToEncoding (singleFieldJSON $ dropPrefix "SGE") v -data RcvConnEvent = RCESwitch {phase :: SwitchPhase} +data RcvConnEvent = RCESwitchQueue {phase :: SwitchPhase} deriving (Show, Generic) -data SndConnEvent = SCESwitch {phase :: SwitchPhase, member :: Maybe GroupMemberRef} +data SndConnEvent = SCESwitchQueue {phase :: SwitchPhase, member :: Maybe GroupMemberRef} deriving (Show, Generic) instance FromJSON RcvConnEvent where