From 341199d5996c83ffa9430d201d0962e8405d00c3 Mon Sep 17 00:00:00 2001 From: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com> Date: Sun, 23 Oct 2022 11:16:56 +0100 Subject: [PATCH] mobile: update type for UserContactLink changes, add addressAutoAccept API (#1245) --- .../java/chat/simplex/app/model/ChatModel.kt | 2 +- .../java/chat/simplex/app/model/SimpleXAPI.kt | 43 +++++++++++++++++-- .../app/views/usersettings/UserAddressView.kt | 14 +++--- apps/ios/Shared/Model/ChatModel.swift | 2 +- apps/ios/Shared/Model/SimpleXAPI.swift | 17 +++++--- .../Views/UserSettings/UserAddress.swift | 8 ++-- apps/ios/SimpleXChat/APITypes.swift | 36 +++++++++++++++- 7 files changed, 100 insertions(+), 22 deletions(-) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt index bbd9aea77e..d80bea76da 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/ChatModel.kt @@ -38,7 +38,7 @@ class ChatModel(val controller: ChatController) { val groupMembers = mutableStateListOf() val terminalItems = mutableStateListOf() - val userAddress = mutableStateOf(null) + val userAddress = mutableStateOf(null) val userSMPServers = mutableStateOf<(List)?>(null) val chatItemTTL = mutableStateOf(ChatItemTTL.None) diff --git a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt index fe1eab7be2..4de5359e05 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/model/SimpleXAPI.kt @@ -628,9 +628,9 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a return false } - private suspend fun apiGetUserAddress(): String? { + private suspend fun apiGetUserAddress(): UserContactLinkRec? { val r = sendCmd(CC.ShowMyAddress()) - if (r is CR.UserContactLink) return r.connReqContact + if (r is CR.UserContactLink) return r.contactLink if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore && r.chatError.storeError is StoreError.UserContactLinkNotFound) { return null @@ -639,6 +639,17 @@ open class ChatController(var ctrl: ChatCtrl?, val ntfManager: NtfManager, val a return null } + suspend fun userAddressAutoAccept(autoAccept: AutoAccept?): UserContactLinkRec? { + val r = sendCmd(CC.AddressAutoAccept(autoAccept)) + if (r is CR.UserContactLinkUpdated) return r.contactLink + if (r is CR.ChatCmdError && r.chatError is ChatError.ChatErrorStore + && r.chatError.storeError is StoreError.UserContactLinkNotFound) { + return null + } + Log.e(TAG, "userAddressAutoAccept bad response: ${r.responseType} ${r.details}") + return null + } + suspend fun apiAcceptContactRequest(contactReqId: Long): Contact? { val r = sendCmd(CC.ApiAcceptContact(contactReqId)) return when { @@ -1422,6 +1433,7 @@ sealed class CC { class CreateMyAddress: CC() class DeleteMyAddress: CC() class ShowMyAddress: CC() + class AddressAutoAccept(val autoAccept: AutoAccept?): CC() class ApiSendCallInvitation(val contact: Contact, val callType: CallType): CC() class ApiRejectCall(val contact: Contact): CC() class ApiSendCallOffer(val contact: Contact, val callOffer: WebRTCCallOffer): CC() @@ -1484,6 +1496,7 @@ sealed class CC { is CreateMyAddress -> "/address" is DeleteMyAddress -> "/delete_address" is ShowMyAddress -> "/show_address" + is AddressAutoAccept -> "/auto_accept ${AutoAccept.cmdString(autoAccept)}" is ApiAcceptContact -> "/_accept $contactReqId" is ApiRejectContact -> "/_reject $contactReqId" is ApiSendCallInvitation -> "/_call invite @${contact.apiId} ${json.encodeToString(callType)}" @@ -1547,6 +1560,7 @@ sealed class CC { is CreateMyAddress -> "createMyAddress" is DeleteMyAddress -> "deleteMyAddress" is ShowMyAddress -> "showMyAddress" + is AddressAutoAccept -> "addressAutoAccept" is ApiAcceptContact -> "apiAcceptContact" is ApiRejectContact -> "apiRejectContact" is ApiSendCallInvitation -> "apiSendCallInvitation" @@ -1743,7 +1757,8 @@ sealed class CR { @Serializable @SerialName("contactAliasUpdated") class ContactAliasUpdated(val toContact: Contact): CR() @Serializable @SerialName("connectionAliasUpdated") class ConnectionAliasUpdated(val toConnection: PendingContactConnection): CR() @Serializable @SerialName("apiParsedMarkdown") class ParsedMarkdown(val formattedText: List? = null): CR() - @Serializable @SerialName("userContactLink") class UserContactLink(val connReqContact: String): CR() + @Serializable @SerialName("userContactLink") class UserContactLink(val contactLink: UserContactLinkRec): CR() + @Serializable @SerialName("userContactLinkUpdated") class UserContactLinkUpdated(val contactLink: UserContactLinkRec): CR() @Serializable @SerialName("userContactLinkCreated") class UserContactLinkCreated(val connReqContact: String): CR() @Serializable @SerialName("userContactLinkDeleted") class UserContactLinkDeleted: CR() @Serializable @SerialName("contactConnected") class ContactConnected(val contact: Contact): CR() @@ -1839,6 +1854,7 @@ sealed class CR { is ConnectionAliasUpdated -> "connectionAliasUpdated" is ParsedMarkdown -> "apiParsedMarkdown" is UserContactLink -> "userContactLink" + is UserContactLinkUpdated -> "userContactLinkUpdated" is UserContactLinkCreated -> "userContactLinkCreated" is UserContactLinkDeleted -> "userContactLinkDeleted" is ContactConnected -> "contactConnected" @@ -1931,7 +1947,8 @@ sealed class CR { is ContactAliasUpdated -> json.encodeToString(toContact) is ConnectionAliasUpdated -> json.encodeToString(toConnection) is ParsedMarkdown -> json.encodeToString(formattedText) - is UserContactLink -> connReqContact + is UserContactLink -> contactLink.responseDetails + is UserContactLinkUpdated -> contactLink.responseDetails is UserContactLinkCreated -> connReqContact is UserContactLinkDeleted -> noDetails() is ContactConnected -> json.encodeToString(contact) @@ -2034,6 +2051,24 @@ abstract class TerminalItem { @Serializable class ConnectionStats(val rcvServers: List?, val sndServers: List?) +@Serializable +class UserContactLinkRec(val connReqContact: String, val autoAccept: AutoAccept? = null) { + val responseDetails: String get() = "connReqContact: ${connReqContact}\nautoAccept: ${AutoAccept.cmdString(autoAccept)}" +} + +@Serializable +class AutoAccept(val acceptIncognito: Boolean, val autoReply: MsgContent?) { + companion object { + fun cmdString(autoAccept: AutoAccept?): String { + if (autoAccept == null) return "off" + val s = "on" + if (autoAccept.acceptIncognito) " incognito=on" else "" + val msg = autoAccept.autoReply ?: return s + return s + " " + msg.cmdString + } + } +} + + @Serializable sealed class ChatError { val string: String get() = when (this) { diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserAddressView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserAddressView.kt index 77407ba1e3..34b117148a 100644 --- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserAddressView.kt +++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/UserAddressView.kt @@ -16,6 +16,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import chat.simplex.app.R import chat.simplex.app.model.ChatModel +import chat.simplex.app.model.UserContactLinkRec import chat.simplex.app.ui.theme.SimpleButton import chat.simplex.app.ui.theme.SimpleXTheme import chat.simplex.app.views.helpers.* @@ -28,7 +29,10 @@ fun UserAddressView(chatModel: ChatModel) { userAddress = chatModel.userAddress.value, createAddress = { withApi { - chatModel.userAddress.value = chatModel.controller.apiCreateUserAddress() + val connReqContact = chatModel.controller.apiCreateUserAddress() + if (connReqContact != null) { + chatModel.userAddress.value = UserContactLinkRec(connReqContact) + } } }, share = { userAddress: String -> shareText(cxt, userAddress) }, @@ -50,7 +54,7 @@ fun UserAddressView(chatModel: ChatModel) { @Composable fun UserAddressLayout( - userAddress: String?, + userAddress: UserContactLinkRec?, createAddress: () -> Unit, share: (String) -> Unit, deleteAddress: () -> Unit @@ -83,7 +87,7 @@ fun UserAddressLayout( Modifier.padding(bottom = 24.dp), lineHeight = 22.sp ) - QRCode(userAddress, Modifier.weight(1f, fill = false).aspectRatio(1f)) + QRCode(userAddress.connReqContact, Modifier.weight(1f, fill = false).aspectRatio(1f)) Row( horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically, @@ -92,7 +96,7 @@ fun UserAddressLayout( SimpleButton( stringResource(R.string.share_link), icon = Icons.Outlined.Share, - click = { share(userAddress) }) + click = { share(userAddress.connReqContact) }) SimpleButton( stringResource(R.string.delete_address), icon = Icons.Outlined.Delete, @@ -133,7 +137,7 @@ fun PreviewUserAddressLayoutNoAddress() { fun PreviewUserAddressLayoutAddressCreated() { SimpleXTheme { UserAddressLayout( - userAddress = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", + userAddress = UserContactLinkRec("https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D"), createAddress = {}, share = { _ -> }, deleteAddress = {}, diff --git a/apps/ios/Shared/Model/ChatModel.swift b/apps/ios/Shared/Model/ChatModel.swift index 3048309075..b9277284cd 100644 --- a/apps/ios/Shared/Model/ChatModel.swift +++ b/apps/ios/Shared/Model/ChatModel.swift @@ -30,7 +30,7 @@ final class ChatModel: ObservableObject { @Published var groupMembers: [GroupMember] = [] // items in the terminal view @Published var terminalItems: [TerminalItem] = [] - @Published var userAddress: String? + @Published var userAddress: UserContactLink? @Published var userSMPServers: [String]? @Published var chatItemTTL: ChatItemTTL = .none @Published var appOpenUrl: URL? diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 39fa1763fb..604a6cc7be 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -488,13 +488,20 @@ func apiDeleteUserAddress() async throws { throw r } -func apiGetUserAddress() throws -> String? { +func apiGetUserAddress() throws -> UserContactLink? { let r = chatSendCmdSync(.showMyAddress) switch r { - case let .userContactLink(connReq): - return connReq - case .chatCmdError(chatError: .errorStore(storeError: .userContactLinkNotFound)): - return nil + case let .userContactLink(contactLink): return contactLink + case .chatCmdError(chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil + default: throw r + } +} + +func userAddressAutoAccept(_ autoAccept: AutoAccept?) async throws -> UserContactLink? { + let r = await chatSendCmd(.addressAutoAccept(autoAccept: autoAccept)) + switch r { + case let .userContactLinkUpdated(contactLink): return contactLink + case .chatCmdError(chatError: .errorStore(storeError: .userContactLinkNotFound)): return nil default: throw r } } diff --git a/apps/ios/Shared/Views/UserSettings/UserAddress.swift b/apps/ios/Shared/Views/UserSettings/UserAddress.swift index 647f421bc7..06bc2ab7b3 100644 --- a/apps/ios/Shared/Views/UserSettings/UserAddress.swift +++ b/apps/ios/Shared/Views/UserSettings/UserAddress.swift @@ -36,7 +36,7 @@ struct UserAddress: View { Text("You can share your address as a link or as a QR code - anybody will be able to connect to you. You won't lose your contacts if you later delete it.") .padding(.bottom) if let userAdress = chatModel.userAddress { - QRCode(uri: userAdress) + QRCode(uri: userAdress.connReqContact) HStack { Button { showShareSheet(items: [userAdress]) @@ -55,9 +55,9 @@ struct UserAddress: View { Button { Task { do { - let userAddress = try await apiCreateUserAddress() + let connReqContact = try await apiCreateUserAddress() DispatchQueue.main.async { - chatModel.userAddress = userAddress + chatModel.userAddress = UserContactLink(connReqContact: connReqContact) } } catch let error { logger.error("UserAddress apiCreateUserAddress: \(responseError(error))") @@ -101,7 +101,7 @@ struct UserAddress: View { struct UserAddress_Previews: PreviewProvider { static var previews: some View { let chatModel = ChatModel() - chatModel.userAddress = "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D" + chatModel.userAddress = UserContactLink(connReqContact: "https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D") return Group { UserAddress() .environmentObject(chatModel) diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 102a15088d..dfd0a47fc5 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -66,6 +66,7 @@ public enum ChatCommand { case createMyAddress case deleteMyAddress case showMyAddress + case addressAutoAccept(autoAccept: AutoAccept?) case apiAcceptContact(contactReqId: Int64) case apiRejectContact(contactReqId: Int64) // WebRTC calls @@ -141,6 +142,7 @@ public enum ChatCommand { case .createMyAddress: return "/address" case .deleteMyAddress: return "/delete_address" case .showMyAddress: return "/show_address" + case let .addressAutoAccept(autoAccept): return "/auto_accept \(AutoAccept.cmdString(autoAccept))" case let .apiAcceptContact(contactReqId): return "/_accept \(contactReqId)" case let .apiRejectContact(contactReqId): return "/_reject \(contactReqId)" case let .apiSendCallInvitation(contact, callType): return "/_call invite @\(contact.apiId) \(encodeJSON(callType))" @@ -215,6 +217,7 @@ public enum ChatCommand { case .createMyAddress: return "createMyAddress" case .deleteMyAddress: return "deleteMyAddress" case .showMyAddress: return "showMyAddress" + case .addressAutoAccept: return "addressAutoAccept" case .apiAcceptContact: return "apiAcceptContact" case .apiRejectContact: return "apiRejectContact" case .apiSendCallInvitation: return "apiSendCallInvitation" @@ -294,7 +297,8 @@ public enum ChatResponse: Decodable, Error { case userProfileUpdated(fromProfile: Profile, toProfile: Profile) case contactAliasUpdated(toContact: Contact) case connectionAliasUpdated(toConnection: PendingContactConnection) - case userContactLink(connReqContact: String) + case userContactLink(contactLink: UserContactLink) + case userContactLinkUpdated(contactLink: UserContactLink) case userContactLinkCreated(connReqContact: String) case userContactLinkDeleted case contactConnected(contact: Contact) @@ -395,6 +399,7 @@ public enum ChatResponse: Decodable, Error { case .contactAliasUpdated: return "contactAliasUpdated" case .connectionAliasUpdated: return "connectionAliasUpdated" case .userContactLink: return "userContactLink" + case .userContactLinkUpdated: return "userContactLinkUpdated" case .userContactLinkCreated: return "userContactLinkCreated" case .userContactLinkDeleted: return "userContactLinkDeleted" case .contactConnected: return "contactConnected" @@ -494,7 +499,8 @@ public enum ChatResponse: Decodable, Error { case let .userProfileUpdated(_, toProfile): return String(describing: toProfile) case let .contactAliasUpdated(toContact): return String(describing: toContact) case let .connectionAliasUpdated(toConnection): return String(describing: toConnection) - case let .userContactLink(connReq): return connReq + case let .userContactLink(contactLink): return contactLink.responseDetails + case let .userContactLinkUpdated(contactLink): return contactLink.responseDetails case let .userContactLinkCreated(connReq): return connReq case .userContactLinkDeleted: return noDetails case let .contactConnected(contact): return String(describing: contact) @@ -702,6 +708,32 @@ public struct ConnectionStats: Codable { public var sndServers: [String]? } +public struct UserContactLink: Decodable { + public var connReqContact: String + public var autoAccept: AutoAccept? + + public init(connReqContact: String, autoAccept: AutoAccept? = nil) { + self.connReqContact = connReqContact + self.autoAccept = autoAccept + } + + var responseDetails: String { + "connReqContact: \(connReqContact)\nautoAccept: \(AutoAccept.cmdString(autoAccept))" + } +} + +public struct AutoAccept: Codable { + public var acceptIncognito: Bool + public var autoReply: MsgContent? + + static func cmdString(_ autoAccept: AutoAccept?) -> String { + guard let autoAccept = autoAccept else { return "off" } + let s = "on" + (autoAccept.acceptIncognito ? " incognito=on" : "") + guard let msg = autoAccept.autoReply else { return s } + return s + " " + msg.cmdString + } +} + public protocol SelectableItem: Hashable, Identifiable { var label: LocalizedStringKey { get } static var values: [Self] { get }