diff --git a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt b/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt index cf19589d4a..5d8371708c 100644 --- a/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt +++ b/apps/multiplatform/android/src/main/java/chat/simplex/app/model/NtfManager.android.kt @@ -136,7 +136,6 @@ object NtfManager { val actionPendingIntent: PendingIntent = PendingIntent.getBroadcast(SimplexApp.context, 0, actionIntent, flags) val actionButton = when (action) { NotificationAction.ACCEPT_CONTACT_REQUEST -> generalGetString(MR.strings.accept) - NotificationAction.ACCEPT_CONTACT_REQUEST_INCOGNITO -> generalGetString(MR.strings.accept_contact_incognito_button) } builder.addAction(0, actionButton, actionPendingIntent) } @@ -316,7 +315,6 @@ object NtfManager { val m = SimplexApp.context.chatModel when (intent.action) { NotificationAction.ACCEPT_CONTACT_REQUEST.name -> ntfManager.acceptContactRequestAction(userId, incognito = false, chatId) - NotificationAction.ACCEPT_CONTACT_REQUEST_INCOGNITO.name -> ntfManager.acceptContactRequestAction(userId, incognito = true, chatId) RejectCallAction -> { val invitation = m.callInvitations[chatId] if (invitation != null) { 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 270b3a73b2..b1ffa20b2e 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 @@ -171,6 +171,8 @@ object ChatModel { // return true if you handled the click var centerPanelBackgroundClickHandler: (() -> Boolean)? = null + var addressShortLinkDataSet: Boolean = userAddress.value?.shortLinkDataSet ?: true + fun getUser(userId: Long): User? = if (currentUser.value?.userId == userId) { currentUser.value } else { @@ -1272,6 +1274,7 @@ interface SomeChat { val apiId: Long val ready: Boolean val chatDeleted: Boolean + val nextConnect: Boolean val incognito: Boolean fun featureEnabled(feature: ChatFeature): Boolean val timedMessagesTTL: Int? @@ -1351,6 +1354,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = contact.apiId override val ready get() = contact.ready override val chatDeleted get() = contact.chatDeleted + override val nextConnect get() = contact.nextConnect override val incognito get() = contact.incognito override fun featureEnabled(feature: ChatFeature) = contact.featureEnabled(feature) override val timedMessagesTTL: Int? get() = contact.timedMessagesTTL @@ -1375,6 +1379,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = groupInfo.apiId override val ready get() = groupInfo.ready override val chatDeleted get() = groupInfo.chatDeleted + override val nextConnect get() = groupInfo.nextConnect override val incognito get() = groupInfo.incognito override fun featureEnabled(feature: ChatFeature) = groupInfo.featureEnabled(feature) override val timedMessagesTTL: Int? get() = groupInfo.timedMessagesTTL @@ -1398,6 +1403,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = noteFolder.apiId override val ready get() = noteFolder.ready override val chatDeleted get() = noteFolder.chatDeleted + override val nextConnect get() = noteFolder.nextConnect override val incognito get() = noteFolder.incognito override fun featureEnabled(feature: ChatFeature) = noteFolder.featureEnabled(feature) override val timedMessagesTTL: Int? get() = noteFolder.timedMessagesTTL @@ -1421,6 +1427,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = contactRequest.apiId override val ready get() = contactRequest.ready override val chatDeleted get() = contactRequest.chatDeleted + override val nextConnect get() = contactRequest.nextConnect override val incognito get() = contactRequest.incognito override fun featureEnabled(feature: ChatFeature) = contactRequest.featureEnabled(feature) override val timedMessagesTTL: Int? get() = contactRequest.timedMessagesTTL @@ -1444,6 +1451,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val apiId get() = contactConnection.apiId override val ready get() = contactConnection.ready override val chatDeleted get() = contactConnection.chatDeleted + override val nextConnect get() = contactConnection.nextConnect override val incognito get() = contactConnection.incognito override fun featureEnabled(feature: ChatFeature) = contactConnection.featureEnabled(feature) override val timedMessagesTTL: Int? get() = contactConnection.timedMessagesTTL @@ -1472,6 +1480,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = "?$apiId" override val ready get() = false override val chatDeleted get() = false + override val nextConnect get() = false override val incognito get() = false override fun featureEnabled(feature: ChatFeature) = false override val timedMessagesTTL: Int? get() = null @@ -1490,8 +1499,8 @@ sealed class ChatInfo: SomeChat, NamedChat { get() { when (this) { is Direct -> { - // TODO [short links] this will have additional statuses for pending contact requests before they are accepted - if (contact.nextSendGrpInv) return null + if (contact.sendMsgToConnect) return null + if (contact.nextAcceptContactRequest) { return generalGetString(MR.strings.cant_send_message_generic) to null } if (!contact.active) return generalGetString(MR.strings.cant_send_message_contact_deleted) to null if (!contact.sndReady) return generalGetString(MR.strings.cant_send_message_contact_not_ready) to null if (contact.activeConn?.connectionStats?.ratchetSyncSendProhibited == true) return generalGetString(MR.strings.cant_send_message_contact_not_synchronized) to null @@ -1641,6 +1650,8 @@ data class Contact( override val createdAt: Instant, override val updatedAt: Instant, val chatTs: Instant?, + val preparedContact: PreparedContact?, + val contactRequestId: Long?, val contactGroupMemberId: Long? = null, val contactGrpInvSent: Boolean, val chatTags: List, @@ -1654,7 +1665,11 @@ data class Contact( override val ready get() = activeConn?.connStatus == ConnStatus.Ready val sndReady get() = ready || activeConn?.connStatus == ConnStatus.SndReady val active get() = contactStatus == ContactStatus.Active + override val nextConnect get() = sendMsgToConnect val nextSendGrpInv get() = contactGroupMemberId != null && !contactGrpInvSent + val nextConnectPrepared get() = preparedContact != null && activeConn == null + val nextAcceptContactRequest get() = contactRequestId != null && activeConn == null + val sendMsgToConnect get() = nextSendGrpInv || nextConnectPrepared override val incognito get() = contactConnIncognito override fun featureEnabled(feature: ChatFeature) = when (feature) { ChatFeature.TimedMessages -> mergedPreferences.timedMessages.enabled.forUser @@ -1717,6 +1732,8 @@ data class Contact( createdAt = Clock.System.now(), updatedAt = Clock.System.now(), chatTs = Clock.System.now(), + preparedContact = null, + contactRequestId = null, contactGrpInvSent = false, chatDeleted = false, uiThemes = null, @@ -1732,6 +1749,18 @@ data class NavigationInfo( val afterTotal: Int = 0 ) +@Serializable +data class PreparedContact ( + val connLinkToConnect: CreatedConnLink, + val uiConnLinkType: ConnectionMode +) + +@Serializable +enum class ConnectionMode { + @SerialName("inv") Inv, + @SerialName("con") Con +} + @Serializable enum class ContactStatus { @SerialName("active") Active, @@ -1877,6 +1906,8 @@ data class GroupInfo ( override val createdAt: Instant, override val updatedAt: Instant, val chatTs: Instant?, + val connLinkToConnect: CreatedConnLink?, + val connLinkStartedConnection: Boolean, val uiThemes: ThemeModeOverrides? = null, val membersRequireAttention: Int, val chatTags: List, @@ -1887,6 +1918,8 @@ data class GroupInfo ( override val id get() = "#$groupId" override val apiId get() = groupId override val ready get() = membership.memberActive + override val nextConnect get() = nextConnectPrepared + val nextConnectPrepared = connLinkToConnect != null && !connLinkStartedConnection override val chatDeleted get() = false override val incognito get() = membership.memberIncognito override fun featureEnabled(feature: ChatFeature) = when (feature) { @@ -1939,6 +1972,8 @@ data class GroupInfo ( createdAt = Clock.System.now(), updatedAt = Clock.System.now(), chatTs = Clock.System.now(), + connLinkToConnect = null, + connLinkStartedConnection = false, uiThemes = null, membersRequireAttention = 0, chatTags = emptyList(), @@ -1990,6 +2025,18 @@ enum class MemberCriteria { } } +@Serializable +data class ContactShortLinkData ( + val profile: Profile, + val message: String?, + val business: Boolean +) + +@Serializable +data class GroupShortLinkData ( + val groupProfile: GroupProfile +) + @Serializable data class BusinessChatInfo ( val chatType: BusinessChatType, @@ -2313,6 +2360,7 @@ class NoteFolder( override val apiId get() = noteFolderId override val chatDeleted get() = false override val ready get() = true + override val nextConnect get() = false override val incognito get() = false override fun featureEnabled(feature: ChatFeature) = feature == ChatFeature.Voice override val timedMessagesTTL: Int? get() = null @@ -2344,10 +2392,11 @@ class UserContactRequest ( override val updatedAt: Instant ): SomeChat, NamedChat { override val chatType get() = ChatType.ContactRequest - override val id get() = "<@$contactRequestId" + override val id get() = contactRequestChatId(contactRequestId) override val apiId get() = contactRequestId override val chatDeleted get() = false override val ready get() = true + override val nextConnect get() = false override val incognito get() = false override fun featureEnabled(feature: ChatFeature) = false override val timedMessagesTTL: Int? get() = null @@ -2368,6 +2417,8 @@ class UserContactRequest ( } } +fun contactRequestChatId(contactRequestId: Long): String = "<@$contactRequestId" + @Serializable class PendingContactConnection( val pccConnId: Long, @@ -2386,6 +2437,7 @@ class PendingContactConnection( override val apiId get() = pccConnId override val chatDeleted get() = false override val ready get() = false + override val nextConnect get() = false override val incognito get() = customUserProfileId != null override fun featureEnabled(feature: ChatFeature) = false override val timedMessagesTTL: Int? get() = null @@ -3433,7 +3485,7 @@ sealed class CIContent: ItemContent { companion object { fun directE2EEInfoStr(e2EEInfo: E2EEInfo): String = - if (e2EEInfo.pqEnabled) { + if (e2EEInfo.pqEnabled == true) { generalGetString(MR.strings.e2ee_info_pq_short) } else { e2eeInfoNoPQStr @@ -3912,6 +3964,7 @@ sealed class MsgContent { @Serializable(with = MsgContentSerializer::class) class MCVoice(override val text: String, val duration: Int): MsgContent() @Serializable(with = MsgContentSerializer::class) class MCFile(override val text: String): MsgContent() @Serializable(with = MsgContentSerializer::class) class MCReport(override val text: String, val reason: ReportReason): MsgContent() + @Serializable(with = MsgContentSerializer::class) class MCChat(override val text: String, val chatLink: MsgChatLink): MsgContent() @Serializable(with = MsgContentSerializer::class) class MCUnknown(val type: String? = null, override val text: String, val json: JsonElement): MsgContent() val isVoice: Boolean get() = @@ -3965,7 +4018,7 @@ enum class CIGroupInvitationStatus { } @Serializable -class E2EEInfo (val pqEnabled: Boolean) {} +class E2EEInfo (val pqEnabled: Boolean?) {} object MsgContentSerializer : KSerializer { override val descriptor: SerialDescriptor = buildSerialDescriptor("MsgContent", PolymorphicKind.SEALED) { @@ -3992,6 +4045,10 @@ object MsgContentSerializer : KSerializer { element("text") element("reason") }) + element("MCChat", buildClassSerialDescriptor("MCChat") { + element("text") + element("chatLink") + }) element("MCUnknown", buildClassSerialDescriptor("MCUnknown")) } @@ -4026,6 +4083,10 @@ object MsgContentSerializer : KSerializer { val reason = Json.decodeFromString(json["reason"].toString()) MsgContent.MCReport(text, reason) } + "chat" -> { + val chatLink = Json.decodeFromString(json["chatLink"].toString()) + MsgContent.MCChat(text, chatLink) + } else -> MsgContent.MCUnknown(t, text, json) } } else { @@ -4080,6 +4141,12 @@ object MsgContentSerializer : KSerializer { put("text", value.text) put("reason", json.encodeToJsonElement(value.reason)) } + is MsgContent.MCChat -> + buildJsonObject { + put("type", "chat") + put("text", value.text) + put("chatLink", json.encodeToJsonElement(value.chatLink)) + } is MsgContent.MCUnknown -> value.json } encoder.encodeJsonElement(json) @@ -4095,6 +4162,14 @@ enum class MsgContentTag { @SerialName("voice") Voice, @SerialName("file") File, @SerialName("report") Report, + @SerialName("chat") Chat, +} + +@Serializable +sealed class MsgChatLink { + @Serializable @SerialName("contact") data class Contact(val connLink: String, val profile: Profile, val business: Boolean) : MsgChatLink() + @Serializable @SerialName("invitation") data class Invitation(val invLink: String, val profile: Profile) : MsgChatLink() + @Serializable @SerialName("group") data class Group(val connLink: String, val groupProfile: GroupProfile) : MsgChatLink() } @Serializable 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 b0e5e9896a..538393bb62 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 @@ -1391,6 +1391,60 @@ object ChatController { } } + suspend fun apiPrepareContact(rh: Long?, connLink: CreatedConnLink, contactShortLinkData: ContactShortLinkData): Contact? { + val userId = try { currentUserId("apiPrepareContact") } catch (e: Exception) { return null } + val r = sendCmd(rh, CC.APIPrepareContact(userId, connLink, contactShortLinkData)) + if (r is API.Result && r.res is CR.NewPreparedContact) return r.res.contact + Log.e(TAG, "apiPrepareContact bad response: ${r.responseType} ${r.details}") + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_preparing_contact), "${r.responseType}: ${r.details}") + return null + } + + suspend fun apiPrepareGroup(rh: Long?, connLink: CreatedConnLink, groupShortLinkData: GroupShortLinkData): GroupInfo? { + val userId = try { currentUserId("apiPrepareGroup") } catch (e: Exception) { return null } + val r = sendCmd(rh, CC.APIPrepareGroup(userId, connLink, groupShortLinkData)) + if (r is API.Result && r.res is CR.NewPreparedGroup) return r.res.groupInfo + Log.e(TAG, "apiPrepareGroup bad response: ${r.responseType} ${r.details}") + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_preparing_group), "${r.responseType}: ${r.details}") + return null + } + + suspend fun apiChangePreparedContactUser(rh: Long?, contactId: Long, newUserId: Long): Contact? { + val r = sendCmd(rh, CC.APIChangePreparedContactUser(contactId, newUserId)) + if (r is API.Result && r.res is CR.ContactUserChanged) return r.res.toContact + Log.e(TAG, "apiChangePreparedContactUser bad response: ${r.responseType} ${r.details}") + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_changing_contact_user), "${r.responseType}: ${r.details}") + return null + } + + suspend fun apiChangePreparedGroupUser(rh: Long?, groupId: Long, newUserId: Long): GroupInfo? { + val r = sendCmd(rh, CC.APIChangePreparedGroupUser(groupId, newUserId)) + if (r is API.Result && r.res is CR.GroupUserChanged) return r.res.toGroup + Log.e(TAG, "apiChangePreparedGroupUser bad response: ${r.responseType} ${r.details}") + AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_changing_group_user), "${r.responseType}: ${r.details}") + return null + } + + suspend fun apiConnectPreparedContact(rh: Long?, contactId: Long, incognito: Boolean, msg: MsgContent): Contact? { + val r = sendCmd(rh, CC.APIConnectPreparedContact(contactId, incognito, msg)) + if (r is API.Result && r.res is CR.StartedConnectionToContact) return r.res.contact + Log.e(TAG, "apiConnectPreparedContact bad response: ${r.responseType} ${r.details}") + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiConnectPreparedContact", generalGetString(MR.strings.connection_error), r) + } + return null + } + + suspend fun apiConnectPreparedGroup(rh: Long?, groupId: Long, incognito: Boolean): GroupInfo? { + val r = sendCmd(rh, CC.APIConnectPreparedGroup(groupId, incognito)) + if (r is API.Result && r.res is CR.StartedConnectionToGroup) return r.res.groupInfo + Log.e(TAG, "apiConnectPreparedGroup bad response: ${r.responseType} ${r.details}") + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiConnectPreparedGroup", generalGetString(MR.strings.connection_error), r) + } + return null + } + suspend fun apiConnectContactViaAddress(rh: Long?, incognito: Boolean, contactId: Long): Contact? { val userId = try { currentUserId("apiConnectContactViaAddress") } catch (e: Exception) { return null } val r = sendCmd(rh, CC.ApiConnectContactViaAddress(userId, incognito, contactId)) @@ -1612,11 +1666,14 @@ object ChatController { } } - suspend fun apiRejectContactRequest(rh: Long?, contactReqId: Long): Boolean { + suspend fun apiRejectContactRequest(rh: Long?, contactReqId: Long): Contact? { val r = sendCmd(rh, CC.ApiRejectContact(contactReqId)) - if (r is API.Result && r.res is CR.ContactRequestRejected) return true + if (r is API.Result && r.res is CR.ContactRequestRejected) return r.res.contact_ Log.e(TAG, "apiRejectContactRequest bad response: ${r.responseType} ${r.details}") - return false + if (!(networkErrorAlert(r))) { + apiErrorAlert("apiRejectContactRequest", generalGetString(MR.strings.error_rejecting_contact_request), r) + } + return null } suspend fun apiGetCallInvitations(rh: Long?): List { @@ -1988,18 +2045,18 @@ object ChatController { } } - suspend fun apiCreateGroupLink(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair? { + suspend fun apiCreateGroupLink(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): GroupLink? { val r = sendCmd(rh, CC.APICreateGroupLink(groupId, memberRole)) - if (r is API.Result && r.res is CR.GroupLinkCreated) return r.res.connLinkContact to r.res.memberRole + if (r is API.Result && r.res is CR.GroupLinkCreated) return r.res.groupLink if (!(networkErrorAlert(r))) { apiErrorAlert("apiCreateGroupLink", generalGetString(MR.strings.error_creating_link_for_group), r) } return null } - suspend fun apiGroupLinkMemberRole(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair? { + suspend fun apiGroupLinkMemberRole(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): GroupLink? { val r = sendCmd(rh, CC.APIGroupLinkMemberRole(groupId, memberRole)) - if (r is API.Result && r.res is CR.GroupLink) return r.res.connLinkContact to r.res.memberRole + if (r is API.Result && r.res is CR.CRGroupLink) return r.res.groupLink if (!(networkErrorAlert(r))) { apiErrorAlert("apiGroupLinkMemberRole", generalGetString(MR.strings.error_updating_link_for_group), r) } @@ -2015,16 +2072,16 @@ object ChatController { return false } - suspend fun apiGetGroupLink(rh: Long?, groupId: Long): Pair? { + suspend fun apiGetGroupLink(rh: Long?, groupId: Long): GroupLink? { val r = sendCmd(rh, CC.APIGetGroupLink(groupId)) - if (r is API.Result && r.res is CR.GroupLink) return r.res.connLinkContact to r.res.memberRole + if (r is API.Result && r.res is CR.CRGroupLink) return r.res.groupLink Log.e(TAG, "apiGetGroupLink bad response: ${r.responseType} ${r.details}") return null } - suspend fun apiAddGroupShortLink(rh: Long?, groupId: Long): Pair? { + suspend fun apiAddGroupShortLink(rh: Long?, groupId: Long): GroupLink? { val r = sendCmd(rh, CC.ApiAddGroupShortLink(groupId)) - if (r is API.Result && r.res is CR.GroupLink) return r.res.connLinkContact to r.res.memberRole + if (r is API.Result && r.res is CR.CRGroupLink) return r.res.groupLink if (!(networkErrorAlert(r))) { apiErrorAlert("apiAddGroupShortLink", generalGetString(MR.strings.error_creating_link_for_group), r) } @@ -2376,17 +2433,26 @@ object ChatController { } is CR.ReceivedContactRequest -> { val contactRequest = r.contactRequest - val cInfo = ChatInfo.ContactRequest(contactRequest) if (active(r.user)) { withContext(Dispatchers.Main) { - if (chatModel.chatsContext.hasChat(rhId, contactRequest.id)) { - chatModel.chatsContext.updateChatInfo(rhId, cInfo) + if (r.contact_ != null) { // means contact request was created with contact, so we need to add/update contact chat + val contact = r.contact_ + if (chatModel.chatsContext.hasChat(rhId, contact.id)) { + chatModel.chatsContext.updateContact(rhId, contact) + } else { + chatModel.chatsContext.addChat(Chat(remoteHostId = rhId, chatInfo = ChatInfo.Direct(contact), chatItems = listOf())) + } } else { - chatModel.chatsContext.addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = listOf())) + val cInfo = ChatInfo.ContactRequest(contactRequest) + if (chatModel.chatsContext.hasChat(rhId, contactRequest.id)) { + chatModel.chatsContext.updateChatInfo(rhId, cInfo) + } else { + chatModel.chatsContext.addChat(Chat(remoteHostId = rhId, chatInfo = cInfo, chatItems = listOf())) + } } } } - ntfManager.notifyContactRequestReceived(r.user, cInfo) + ntfManager.notifyContactRequestReceived(r.user, ChatInfo.ContactRequest(contactRequest)) } is CR.ContactUpdated -> { if (active(r.user) && chatModel.chatsContext.hasChat(rhId, r.toContact.id)) { @@ -3423,6 +3489,12 @@ sealed class CC { class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC() class ApiChangeConnectionUser(val connId: Long, val userId: Long): CC() class APIConnectPlan(val userId: Long, val connLink: String): CC() + class APIPrepareContact(val userId: Long, val connLink: CreatedConnLink, val contactShortLinkData: ContactShortLinkData): CC() + class APIPrepareGroup(val userId: Long, val connLink: CreatedConnLink, val groupShortLinkData: GroupShortLinkData): CC() + class APIChangePreparedContactUser(val contactId: Long, val newUserId: Long): CC() + class APIChangePreparedGroupUser(val groupId: Long, val newUserId: Long): CC() + class APIConnectPreparedContact(val contactId: Long, val incognito: Boolean, val msg: MsgContent): CC() + class APIConnectPreparedGroup(val groupId: Long, val incognito: Boolean): CC() class APIConnect(val userId: Long, val incognito: Boolean, val connLink: CreatedConnLink): CC() class ApiConnectContactViaAddress(val userId: Long, val incognito: Boolean, val contactId: Long): CC() class ApiDeleteChat(val type: ChatType, val id: Long, val chatDeleteMode: ChatDeleteMode): CC() @@ -3612,6 +3684,12 @@ sealed class CC { is ApiSetConnectionIncognito -> "/_set incognito :$connId ${onOff(incognito)}" is ApiChangeConnectionUser -> "/_set conn user :$connId $userId" is APIConnectPlan -> "/_connect plan $userId $connLink" + is APIPrepareContact -> "/_prepare contact $userId ${connLink.connFullLink} ${connLink.connShortLink ?: ""} ${json.encodeToString(contactShortLinkData)}" + is APIPrepareGroup -> "/_prepare group $userId ${connLink.connFullLink} ${connLink.connShortLink ?: ""} ${json.encodeToString(groupShortLinkData)}" + is APIChangePreparedContactUser -> "/_set contact user @$contactId $newUserId" + is APIChangePreparedGroupUser -> "/_set group user #$groupId $newUserId" + is APIConnectPreparedContact -> "/_connect contact @$contactId incognito=${onOff(incognito)} ${msg.cmdString}" + is APIConnectPreparedGroup -> "/_connect group #$groupId incognito=${onOff(incognito)}" is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} ${connLink.connFullLink} ${connLink.connShortLink ?: ""}" is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId" is ApiDeleteChat -> "/_delete ${chatRef(type, id, scope = null)} ${chatDeleteMode.cmdString}" @@ -3779,6 +3857,12 @@ sealed class CC { is ApiSetConnectionIncognito -> "apiSetConnectionIncognito" is ApiChangeConnectionUser -> "apiChangeConnectionUser" is APIConnectPlan -> "apiConnectPlan" + is APIPrepareContact -> "apiPrepareContact" + is APIPrepareGroup -> "apiPrepareGroup" + is APIChangePreparedContactUser -> "apiChangePreparedContactUser" + is APIChangePreparedGroupUser -> "apiChangePreparedGroupUser" + is APIConnectPreparedContact -> "apiConnectPreparedContact" + is APIConnectPreparedGroup -> "apiConnectPreparedGroup" is APIConnect -> "apiConnect" is ApiConnectContactViaAddress -> "apiConnectContactViaAddress" is ApiDeleteChat -> "apiDeleteChat" @@ -5829,8 +5913,14 @@ sealed class CR { @Serializable @SerialName("connectionIncognitoUpdated") class ConnectionIncognitoUpdated(val user: UserRef, val toConnection: PendingContactConnection): CR() @Serializable @SerialName("connectionUserChanged") class ConnectionUserChanged(val user: UserRef, val fromConnection: PendingContactConnection, val toConnection: PendingContactConnection, val newUser: UserRef): CR() @Serializable @SerialName("connectionPlan") class CRConnectionPlan(val user: UserRef, val connLink: CreatedConnLink, val connectionPlan: ConnectionPlan): CR() + @Serializable @SerialName("newPreparedContact") class NewPreparedContact(val user: UserRef, val contact: Contact): CR() + @Serializable @SerialName("newPreparedGroup") class NewPreparedGroup(val user: UserRef, val groupInfo: GroupInfo): CR() + @Serializable @SerialName("contactUserChanged") class ContactUserChanged(val user: UserRef, val fromContact: Contact, val newUser: UserRef, val toContact: Contact): CR() + @Serializable @SerialName("groupUserChanged") class GroupUserChanged(val user: UserRef, val fromGroup: GroupInfo, val newUser: UserRef, val toGroup: GroupInfo): CR() @Serializable @SerialName("sentConfirmation") class SentConfirmation(val user: UserRef, val connection: PendingContactConnection): CR() @Serializable @SerialName("sentInvitation") class SentInvitation(val user: UserRef, val connection: PendingContactConnection): CR() + @Serializable @SerialName("startedConnectionToContact") class StartedConnectionToContact(val user: UserRef, val contact: Contact): CR() + @Serializable @SerialName("startedConnectionToGroup") class StartedConnectionToGroup(val user: UserRef, val groupInfo: GroupInfo): CR() @Serializable @SerialName("sentInvitationToContact") class SentInvitationToContact(val user: UserRef, val contact: Contact, val customUserProfile: Profile?): CR() @Serializable @SerialName("contactAlreadyExists") class ContactAlreadyExists(val user: UserRef, val contact: Contact): CR() @Serializable @SerialName("contactDeleted") class ContactDeleted(val user: UserRef, val contact: Contact): CR() @@ -5851,9 +5941,9 @@ sealed class CR { @Serializable @SerialName("contactConnected") class ContactConnected(val user: UserRef, val contact: Contact, val userCustomProfile: Profile? = null): CR() @Serializable @SerialName("contactConnecting") class ContactConnecting(val user: UserRef, val contact: Contact): CR() @Serializable @SerialName("contactSndReady") class ContactSndReady(val user: UserRef, val contact: Contact): CR() - @Serializable @SerialName("receivedContactRequest") class ReceivedContactRequest(val user: UserRef, val contactRequest: UserContactRequest): CR() + @Serializable @SerialName("receivedContactRequest") class ReceivedContactRequest(val user: UserRef, val contactRequest: UserContactRequest, val contact_: Contact?): CR() @Serializable @SerialName("acceptingContactRequest") class AcceptingContactRequest(val user: UserRef, val contact: Contact): CR() - @Serializable @SerialName("contactRequestRejected") class ContactRequestRejected(val user: UserRef): CR() + @Serializable @SerialName("contactRequestRejected") class ContactRequestRejected(val user: UserRef, val contactRequest: UserContactRequest, val contact_: Contact?): CR() @Serializable @SerialName("contactUpdated") class ContactUpdated(val user: UserRef, val toContact: Contact): CR() @Serializable @SerialName("groupMemberUpdated") class GroupMemberUpdated(val user: UserRef, val groupInfo: GroupInfo, val fromMember: GroupMember, val toMember: GroupMember): CR() // TODO remove below @@ -5900,8 +5990,8 @@ sealed class CR { @Serializable @SerialName("joinedGroupMember") class JoinedGroupMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("connectedToGroupMember") class ConnectedToGroupMember(val user: UserRef, val groupInfo: GroupInfo, val member: GroupMember, val memberContact: Contact? = null): CR() @Serializable @SerialName("groupUpdated") class GroupUpdated(val user: UserRef, val toGroup: GroupInfo): CR() - @Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: UserRef, val groupInfo: GroupInfo, val connLinkContact: CreatedConnLink, val memberRole: GroupMemberRole): CR() - @Serializable @SerialName("groupLink") class GroupLink(val user: UserRef, val groupInfo: GroupInfo, val connLinkContact: CreatedConnLink, val memberRole: GroupMemberRole): CR() + @Serializable @SerialName("groupLinkCreated") class GroupLinkCreated(val user: UserRef, val groupInfo: GroupInfo, val groupLink: GroupLink): CR() + @Serializable @SerialName("groupLink") class CRGroupLink(val user: UserRef, val groupInfo: GroupInfo, val groupLink: GroupLink): CR() @Serializable @SerialName("groupLinkDeleted") class GroupLinkDeleted(val user: UserRef, val groupInfo: GroupInfo): CR() @Serializable @SerialName("newMemberContact") class NewMemberContact(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR() @Serializable @SerialName("newMemberContactSentInv") class NewMemberContactSentInv(val user: UserRef, val contact: Contact, val groupInfo: GroupInfo, val member: GroupMember): CR() @@ -6011,8 +6101,14 @@ sealed class CR { is ConnectionIncognitoUpdated -> "connectionIncognitoUpdated" is ConnectionUserChanged -> "ConnectionUserChanged" is CRConnectionPlan -> "connectionPlan" + is NewPreparedContact -> "newPreparedContact" + is NewPreparedGroup -> "newPreparedGroup" + is ContactUserChanged -> "contactUserChanged" + is GroupUserChanged -> "groupUserChanged" is SentConfirmation -> "sentConfirmation" is SentInvitation -> "sentInvitation" + is StartedConnectionToContact -> "startedConnectionToContact" + is StartedConnectionToGroup -> "startedConnectionToGroup" is SentInvitationToContact -> "sentInvitationToContact" is ContactAlreadyExists -> "contactAlreadyExists" is ContactDeleted -> "contactDeleted" @@ -6080,7 +6176,7 @@ sealed class CR { is ConnectedToGroupMember -> "connectedToGroupMember" is GroupUpdated -> "groupUpdated" is GroupLinkCreated -> "groupLinkCreated" - is GroupLink -> "groupLink" + is CRGroupLink -> "groupLink" is GroupLinkDeleted -> "groupLinkDeleted" is NewMemberContact -> "newMemberContact" is NewMemberContactSentInv -> "newMemberContactSentInv" @@ -6183,8 +6279,14 @@ sealed class CR { is ConnectionIncognitoUpdated -> withUser(user, json.encodeToString(toConnection)) is ConnectionUserChanged -> withUser(user, "fromConnection: ${json.encodeToString(fromConnection)}\ntoConnection: ${json.encodeToString(toConnection)}\nnewUser: ${json.encodeToString(newUser)}" ) is CRConnectionPlan -> withUser(user, "connLink: ${json.encodeToString(connLink)}\nconnectionPlan: ${json.encodeToString(connectionPlan)}") + is NewPreparedContact -> withUser(user, json.encodeToString(contact)) + is NewPreparedGroup -> withUser(user, json.encodeToString(groupInfo)) + is ContactUserChanged -> withUser(user, "fromContact: ${json.encodeToString(fromContact)}\nnewUserId: ${json.encodeToString(newUser.userId)}\ntoContact: ${json.encodeToString(toContact)}") + is GroupUserChanged -> withUser(user, "fromGroup: ${json.encodeToString(fromGroup)}\nnewUserId: ${json.encodeToString(newUser.userId)}\ntoGroup: ${json.encodeToString(toGroup)}") is SentConfirmation -> withUser(user, json.encodeToString(connection)) is SentInvitation -> withUser(user, json.encodeToString(connection)) + is StartedConnectionToContact -> withUser(user, json.encodeToString(contact)) + is StartedConnectionToGroup -> withUser(user, json.encodeToString(groupInfo)) is SentInvitationToContact -> withUser(user, json.encodeToString(contact)) is ContactAlreadyExists -> withUser(user, json.encodeToString(contact)) is ContactDeleted -> withUser(user, json.encodeToString(contact)) @@ -6198,16 +6300,16 @@ sealed class CR { is GroupAliasUpdated -> withUser(user, json.encodeToString(toGroup)) is ConnectionAliasUpdated -> withUser(user, json.encodeToString(toConnection)) is ContactPrefsUpdated -> withUser(user, "fromContact: $fromContact\ntoContact: \n${json.encodeToString(toContact)}") - is UserContactLink -> withUser(user, contactLink.responseDetails) - is UserContactLinkUpdated -> withUser(user, contactLink.responseDetails) + is UserContactLink -> withUser(user, json.encodeToString(contactLink)) + is UserContactLinkUpdated -> withUser(user, json.encodeToString(contactLink)) is UserContactLinkCreated -> withUser(user, json.encodeToString(connLinkContact)) is UserContactLinkDeleted -> withUser(user, noDetails()) is ContactConnected -> withUser(user, json.encodeToString(contact)) is ContactConnecting -> withUser(user, json.encodeToString(contact)) is ContactSndReady -> withUser(user, json.encodeToString(contact)) - is ReceivedContactRequest -> withUser(user, json.encodeToString(contactRequest)) + is ReceivedContactRequest -> withUser(user, "contactRequest: ${json.encodeToString(contactRequest)}\ncontact_: ${json.encodeToString(contact_)}") is AcceptingContactRequest -> withUser(user, json.encodeToString(contact)) - is ContactRequestRejected -> withUser(user, noDetails()) + is ContactRequestRejected -> withUser(user, "contactRequest: ${json.encodeToString(contactRequest)}\ncontact_: ${json.encodeToString(contact_)}") is ContactUpdated -> withUser(user, json.encodeToString(toContact)) is GroupMemberUpdated -> withUser(user, "groupInfo: $groupInfo\nfromMember: $fromMember\ntoMember: $toMember") is ContactsSubscribed -> "server: $server\ncontacts:\n${json.encodeToString(contactRefs)}" @@ -6251,8 +6353,8 @@ sealed class CR { is JoinedGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member") is ConnectedToGroupMember -> withUser(user, "groupInfo: $groupInfo\nmember: $member\nmemberContact: $memberContact") is GroupUpdated -> withUser(user, json.encodeToString(toGroup)) - is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\nconnLinkContact: $connLinkContact\nmemberRole: $memberRole") - is GroupLink -> withUser(user, "groupInfo: $groupInfo\nconnLinkContact: $connLinkContact\nmemberRole: $memberRole") + is GroupLinkCreated -> withUser(user, "groupInfo: $groupInfo\ngroupLink: $groupLink") + is CRGroupLink -> withUser(user, "groupInfo: $groupInfo\ngroupLink: $groupLink") is GroupLinkDeleted -> withUser(user, json.encodeToString(groupInfo)) is NewMemberContact -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member") is NewMemberContactSentInv -> withUser(user, "contact: $contact\ngroupInfo: $groupInfo\nmember: $member") @@ -6388,7 +6490,7 @@ sealed class ConnectionPlan { @Serializable sealed class InvitationLinkPlan { - @Serializable @SerialName("ok") object Ok: InvitationLinkPlan() + @Serializable @SerialName("ok") class Ok(val contactSLinkData_: ContactShortLinkData? = null): InvitationLinkPlan() @Serializable @SerialName("ownLink") object OwnLink: InvitationLinkPlan() @Serializable @SerialName("connecting") class Connecting(val contact_: Contact? = null): InvitationLinkPlan() @Serializable @SerialName("known") class Known(val contact: Contact): InvitationLinkPlan() @@ -6396,7 +6498,7 @@ sealed class InvitationLinkPlan { @Serializable sealed class ContactAddressPlan { - @Serializable @SerialName("ok") object Ok: ContactAddressPlan() + @Serializable @SerialName("ok") class Ok(val contactSLinkData_: ContactShortLinkData? = null): ContactAddressPlan() @Serializable @SerialName("ownLink") object OwnLink: ContactAddressPlan() @Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: ContactAddressPlan() @Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val contact: Contact): ContactAddressPlan() @@ -6406,7 +6508,7 @@ sealed class ContactAddressPlan { @Serializable sealed class GroupLinkPlan { - @Serializable @SerialName("ok") object Ok: GroupLinkPlan() + @Serializable @SerialName("ok") class Ok(val groupSLinkData_: GroupShortLinkData? = null): GroupLinkPlan() @Serializable @SerialName("ownLink") class OwnLink(val groupInfo: GroupInfo): GroupLinkPlan() @Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: GroupLinkPlan() @Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val groupInfo_: GroupInfo? = null): GroupLinkPlan() @@ -6516,9 +6618,11 @@ enum class RatchetSyncState { } @Serializable -class UserContactLinkRec(val connLinkContact: CreatedConnLink, val autoAccept: AutoAccept? = null) { - val responseDetails: String get() = "connLinkContact: ${connLinkContact}\nautoAccept: ${AutoAccept.cmdString(autoAccept)}" -} +data class UserContactLinkRec( + val connLinkContact: CreatedConnLink, + val shortLinkDataSet: Boolean, + val autoAccept: AutoAccept? = null +) @Serializable class AutoAccept(val businessAddress: Boolean, val acceptIncognito: Boolean, val autoReply: MsgContent?) { @@ -6537,6 +6641,15 @@ class AutoAccept(val businessAddress: Boolean, val acceptIncognito: Boolean, val } } +@Serializable +data class GroupLink( + val userContactLinkId: Long, + val connLinkContact: CreatedConnLink, + val shortLinkDataSet: Boolean, + val groupLinkId: String, + val acceptMemberRole: GroupMemberRole +) + @Serializable data class CoreVersionInfo( val version: String, @@ -6621,6 +6734,7 @@ sealed class ChatErrorType { is InvalidConnReq -> "invalidConnReq" is UnsupportedConnReq -> "unsupportedConnReq" is InvalidChatMessage -> "invalidChatMessage" + is ConnReqMessageProhibited -> "connReqMessageProhibited" is ContactNotReady -> "contactNotReady" is ContactNotActive -> "contactNotActive" is ContactDisabled -> "contactDisabled" @@ -6636,6 +6750,7 @@ sealed class ChatErrorType { is GroupMemberNotActive -> "groupMemberNotActive" is GroupMemberUserRemoved -> "groupMemberUserRemoved" is GroupMemberNotFound -> "groupMemberNotFound" + is GroupHostMemberNotFound -> "groupHostMemberNotFound" is GroupCantResendInvitation -> "groupCantResendInvitation" is GroupInternal -> "groupInternal" is FileNotFound -> "fileNotFound" @@ -6700,6 +6815,7 @@ sealed class ChatErrorType { @Serializable @SerialName("invalidConnReq") object InvalidConnReq: ChatErrorType() @Serializable @SerialName("unsupportedConnReq") object UnsupportedConnReq: ChatErrorType() @Serializable @SerialName("invalidChatMessage") class InvalidChatMessage(val connection: Connection, val message: String): ChatErrorType() + @Serializable @SerialName("connReqMessageProhibited") object ConnReqMessageProhibited: ChatErrorType() @Serializable @SerialName("contactNotReady") class ContactNotReady(val contact: Contact): ChatErrorType() @Serializable @SerialName("contactNotActive") class ContactNotActive(val contact: Contact): ChatErrorType() @Serializable @SerialName("contactDisabled") class ContactDisabled(val contact: Contact): ChatErrorType() @@ -6715,6 +6831,7 @@ sealed class ChatErrorType { @Serializable @SerialName("groupMemberNotActive") object GroupMemberNotActive: ChatErrorType() @Serializable @SerialName("groupMemberUserRemoved") object GroupMemberUserRemoved: ChatErrorType() @Serializable @SerialName("groupMemberNotFound") object GroupMemberNotFound: ChatErrorType() + @Serializable @SerialName("groupHostMemberNotFound") class GroupHostMemberNotFound(val groupId: Long): ChatErrorType() @Serializable @SerialName("groupCantResendInvitation") class GroupCantResendInvitation(val groupInfo: GroupInfo, val contactName: String): ChatErrorType() @Serializable @SerialName("groupInternal") class GroupInternal(val message: String): ChatErrorType() @Serializable @SerialName("fileNotFound") class FileNotFound(val message: String): ChatErrorType() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt index 5b9e63963c..d906ef7baf 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/platform/NtfManager.kt @@ -10,8 +10,7 @@ import chat.simplex.res.MR import kotlinx.coroutines.delay enum class NotificationAction { - ACCEPT_CONTACT_REQUEST, - ACCEPT_CONTACT_REQUEST_INCOGNITO + ACCEPT_CONTACT_REQUEST } lateinit var ntfManager: NtfManager @@ -31,8 +30,7 @@ abstract class NtfManager { msgText = generalGetString(MR.strings.notification_new_contact_request), image = cInfo.image, listOf( - NotificationAction.ACCEPT_CONTACT_REQUEST to { acceptContactRequestAction(user.userId, incognito = false, cInfo.id) }, - NotificationAction.ACCEPT_CONTACT_REQUEST_INCOGNITO to { acceptContactRequestAction(user.userId, incognito = true, cInfo.id) } + NotificationAction.ACCEPT_CONTACT_REQUEST to { acceptContactRequestAction(user.userId, incognito = false, cInfo.id) } ) ) @@ -51,14 +49,9 @@ abstract class NtfManager { fun acceptContactRequestAction(userId: Long?, incognito: Boolean, chatId: ChatId) { val isCurrentUser = ChatModel.currentUser.value?.userId == userId - val cInfo: ChatInfo.ContactRequest? = if (isCurrentUser) { - (ChatModel.getChat(chatId)?.chatInfo as? ChatInfo.ContactRequest) ?: return - } else { - null - } val apiId = chatId.replace("<@", "").toLongOrNull() ?: return // TODO include remote host in notification - acceptContactRequest(null, incognito, apiId, cInfo, isCurrentUser, ChatModel) + acceptContactRequest(null, incognito, apiId, isCurrentUser, ChatModel) cancelNotificationsForChat(chatId) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index a8b77e8fdd..115d5b3c78 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -934,7 +934,7 @@ fun CallButton( } } } } - contact.nextSendGrpInv -> { { showCantCallContactSendMessageAlert() } } + contact.sendMsgToConnect -> { { showCantCallContactSendMessageAlert() } } !contact.active -> { { showCantCallContactDeletedAlert() } } !contact.ready -> { { showCantCallContactConnectingAlert() } } needToAllowCallsToContact -> { { showNeedToAllowCallsAlert(onConfirm = { allowCallsToContact(chat) }) } } 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 2ca0dcc35d..522ba9ac40 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 @@ -181,7 +181,8 @@ fun ChatView( chatInfo is ChatInfo.Direct && !chatInfo.contact.sndReady && chatInfo.contact.active - && !chatInfo.contact.nextSendGrpInv + && !chatInfo.contact.sendMsgToConnect + && !chatInfo.contact.nextAcceptContactRequest ) { Text( generalGetString(MR.strings.contact_connection_pending), @@ -287,7 +288,7 @@ fun ChatView( // The idea is to preload information before showing a modal because large groups can take time to load all members var preloadedContactInfo: Pair? = null var preloadedCode: String? = null - var preloadedLink: Pair? = null + var preloadedLink: GroupLink? = null if (chatInfo is ChatInfo.Direct) { preloadedContactInfo = chatModel.controller.apiContactInfo(chatRh, chatInfo.apiId) preloadedCode = chatModel.controller.apiGetContactCode(chatRh, chatInfo.apiId)?.second @@ -315,13 +316,13 @@ fun ChatView( showSearch.value = true } } else if (chatInfo is ChatInfo.Group) { - var link: Pair? by remember(chatInfo.id) { mutableStateOf(preloadedLink) } + var link: GroupLink? by remember(chatInfo.id) { mutableStateOf(preloadedLink) } KeyChangeEffect(chatInfo.id) { setGroupMembers(chatRh, chatInfo.groupInfo, chatModel) link = chatModel.controller.apiGetGroupLink(chatRh, chatInfo.groupInfo.groupId) preloadedLink = link } - GroupChatInfoView(chatsCtx, chatRh, chatInfo.id, link?.first, link?.second, selectedItems, appBar, scrollToItemId, { + GroupChatInfoView(chatsCtx, chatRh, chatInfo.id, link, selectedItems, appBar, scrollToItemId, { link = it preloadedLink = it }, close, { showSearch.value = true }) @@ -2509,7 +2510,7 @@ fun openGroupLink(groupInfo: GroupInfo, rhId: Long?, view: Any? = null, close: ( val link = chatModel.controller.apiGetGroupLink(rhId, groupInfo.groupId) close?.invoke() ModalManager.end.showModalCloseable(true) { - GroupLinkView(chatModel, rhId, groupInfo, link?.first, link?.second, onGroupLinkUpdated = null) + GroupLinkView(chatModel, rhId, groupInfo, link, onGroupLinkUpdated = null) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextContactRequestActionsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextContactRequestActionsView.kt new file mode 100644 index 0000000000..c35261138a --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeContextContactRequestActionsView.kt @@ -0,0 +1,126 @@ +package chat.simplex.common.views.chat + +import SectionItemView +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import chat.simplex.common.platform.chatModel +import chat.simplex.common.views.chatlist.acceptContactRequest +import chat.simplex.common.views.chatlist.rejectContactRequest +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.stringResource + +@Composable +fun ComposeContextContactRequestActionsView( + rhId: Long?, + contactRequestId: Long +) { + Column( + Modifier + .height(60.dp) + .background(MaterialTheme.colors.surface) + ) { + Divider() + + Row( + Modifier + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + Column( + Modifier + .fillMaxWidth() + .fillMaxHeight() + .weight(1F) + .clickable { + showRejectRequestAlert(rhId, contactRequestId) + }, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(stringResource(MR.strings.reject_contact_button), color = Color.Red) + } + + Column( + Modifier + .fillMaxWidth() + .fillMaxHeight() + .weight(1F) + .clickable { + showAcceptRequestAlert(rhId, contactRequestId) + }, + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(stringResource(MR.strings.accept_contact_button), color = MaterialTheme.colors.primary) + } + } + } +} + +fun showRejectRequestAlert(rhId: Long?, contactRequestId: Long) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.reject_contact_request), + text = generalGetString(MR.strings.the_sender_will_not_be_notified), + confirmText = generalGetString(MR.strings.reject_contact_button), + onConfirm = { + AlertManager.shared.hideAlert() + rejectContactRequest(rhId, contactRequestId, chatModel, dismissToChatList = true) + }, + destructive = true, + hostDevice = hostDevice(rhId), + ) +} + +fun showAcceptRequestAlert(rhId: Long?, contactRequestId: Long) { + // Show 2 buttons in a row + if (chatModel.addressShortLinkDataSet) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.accept_contact_request), + confirmText = generalGetString(MR.strings.accept_contact_button), + onConfirm = { + AlertManager.shared.hideAlert() + acceptContactRequest(rhId, incognito = false, contactRequestId, isCurrentUser = true, chatModel) + }, + hostDevice = hostDevice(rhId), + ) + // Show 3 buttons in a column + } else { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.accept_contact_request), + buttons = { + Column { + // Accept + SectionItemView({ + AlertManager.shared.hideAlert() + acceptContactRequest(rhId, incognito = false, contactRequestId, isCurrentUser = true, chatModel) + }) { + Text(generalGetString(MR.strings.accept_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + // Accept incognito + SectionItemView({ + AlertManager.shared.hideAlert() + acceptContactRequest(rhId, incognito = true, contactRequestId, isCurrentUser = true, chatModel) + }) { + Text(generalGetString(MR.strings.accept_contact_incognito_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + // Cancel + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + }, + hostDevice = hostDevice(rhId), + ) + } +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt index ca6279fd88..57373c95a3 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ComposeView.kt @@ -245,6 +245,7 @@ fun chatItemPreview(chatItem: ChatItem): ComposePreview { is MsgContent.MCVoice -> ComposePreview.VoicePreview(voice = fileName, mc.duration / 1000, true) is MsgContent.MCFile -> ComposePreview.FilePreview(fileName, getAppFileUri(fileName)) is MsgContent.MCReport -> ComposePreview.NoPreview + is MsgContent.MCChat -> ComposePreview.NoPreview is MsgContent.MCUnknown, null -> ComposePreview.NoPreview } } @@ -485,6 +486,80 @@ fun ComposeView( return null } + fun checkLinkPreview(): MsgContent { + val msgText = composeState.value.message.text + return when (val composePreview = composeState.value.preview) { + is ComposePreview.CLinkPreview -> { + val parsedMsg = parseToMarkdown(msgText) + val url = getSimplexLink(parsedMsg).first + val lp = composePreview.linkPreview + if (lp != null && url == lp.uri) { + MsgContent.MCLink(msgText, preview = lp) + } else { + MsgContent.MCText(msgText) + } + } + + else -> MsgContent.MCText(msgText) + } + } + + suspend fun sendMemberContactInvitation() { + val mc = checkLinkPreview() + val contact = chatModel.controller.apiSendMemberContactInvitation(chat.remoteHostId, chat.chatInfo.apiId, mc) + if (contact != null) { + withContext(Dispatchers.Main) { + chatsCtx.updateContact(chat.remoteHostId, contact) + clearState() + } + } + } + + suspend fun sendConnectPreparedContact() { + val mc = checkLinkPreview() + // TODO [short links] use incognito default (incognito choice will be available via context profile picker) + val contact = chatModel.controller.apiConnectPreparedContact(chat.remoteHostId, chat.chatInfo.apiId, incognito = false, msg = mc) + if (contact != null) { + withContext(Dispatchers.Main) { + chatsCtx.updateContact(chat.remoteHostId, contact) + clearState() + } + } + } + + suspend fun connectPreparedGroup() { + // TODO [short links] use incognito default (incognito choice will be available via context profile picker) + val groupInfo = chatModel.controller.apiConnectPreparedGroup(chat.remoteHostId, chat.chatInfo.apiId, incognito = false) + if (groupInfo != null) { + withContext(Dispatchers.Main) { + chatsCtx.updateGroup(chat.remoteHostId, groupInfo) + clearState() + } + } + } + + // TODO [short links] next connect button design, rework compose to not show send button, align with Swift + @Composable + fun NextConnectPreparedButton() { + TextButton(onClick = { + withBGApi { + if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.nextSendGrpInv) { + sendMemberContactInvitation() + } else if (chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.nextConnectPrepared) { + sendConnectPreparedContact() + } else if (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.nextConnectPrepared) { + connectPreparedGroup() + } + } + }) { + if (chat.chatInfo is ChatInfo.Group) { + Text("Join") + } else { + Text("Connect") + } + } + } + suspend fun sendMessageAsync(text: String?, live: Boolean, ttl: Int?): List? { val cInfo = chat.chatInfo val cs = composeState.value @@ -525,23 +600,6 @@ fun ComposeView( return chatItems } - fun checkLinkPreview(): MsgContent { - return when (val composePreview = cs.preview) { - is ComposePreview.CLinkPreview -> { - val parsedMsg = parseToMarkdown(msgText) - val url = getSimplexLink(parsedMsg).first - val lp = composePreview.linkPreview - if (lp != null && url == lp.uri) { - MsgContent.MCLink(msgText, preview = lp) - } else { - MsgContent.MCText(msgText) - } - } - - else -> MsgContent.MCText(msgText) - } - } - fun constructFailedMessage(cs: ComposeState): ComposeState { val preview = when (cs.preview) { is ComposePreview.MediaPreview -> { @@ -564,6 +622,8 @@ fun ComposeView( is MsgContent.MCVoice -> MsgContent.MCVoice(msgText, duration = msgContent.duration) is MsgContent.MCFile -> MsgContent.MCFile(msgText) is MsgContent.MCReport -> MsgContent.MCReport(msgText, reason = msgContent.reason) + // TODO [short links] update chat link + is MsgContent.MCChat -> MsgContent.MCChat(msgText, chatLink = msgContent.chatLink) is MsgContent.MCUnknown -> MsgContent.MCUnknown(type = msgContent.type, text = msgText, json = msgContent.json) } } @@ -586,16 +646,6 @@ fun ComposeView( return cItems?.map { it.chatItem } } - suspend fun sendMemberContactInvitation() { - val mc = checkLinkPreview() - val contact = chatModel.controller.apiSendMemberContactInvitation(chat.remoteHostId, chat.chatInfo.apiId, mc) - if (contact != null) { - withContext(Dispatchers.Main) { - chatsCtx.updateContact(chat.remoteHostId, contact) - } - } - } - suspend fun updateMessage(ei: ChatItem, chat: Chat, live: Boolean): ChatItem? { val cInfo = chat.chatInfo val oldMsgContent = ei.content.msgContent @@ -1015,6 +1065,23 @@ fun ComposeView( val nextSendGrpInv = rememberUpdatedState(chat.nextSendGrpInv) Column { + // TODO [short links] move button to the right of send field, rework SendMsgView to not show send button, align with Swift + if (chat.chatInfo.nextConnect) { + NextConnectPreparedButton() + } + // TODO ^^^ (this shouldn't be here) + + if ( + chat.chatInfo is ChatInfo.Direct + && chat.chatInfo.contact.nextAcceptContactRequest + && chat.chatInfo.contact.contactRequestId != null + ) { + ComposeContextContactRequestActionsView( + rhId = rhId, + contactRequestId = chat.chatInfo.contact.contactRequestId + ) + } + if ( chat.chatInfo is ChatInfo.Group && chatsCtx.secondaryContextFilter is SecondaryContextFilter.GroupChatScopeContext @@ -1030,13 +1097,16 @@ fun ComposeView( member = chatsCtx.secondaryContextFilter.groupScopeInfo.groupMember_ ) } + if (nextSendGrpInv.value) { ComposeContextInvitingContactMemberView() } + val ctx = composeState.value.contextItem if (ctx is ComposeContextItem.ReportedItem) { ReportReasonView(ctx.reason) } + val simplexLinkProhibited = chatsCtx.secondaryContextFilter == null && hasSimplexLink.value && !chat.groupFeatureEnabled(GroupFeature.SimplexLinks) val fileProhibited = chatsCtx.secondaryContextFilter == null && composeState.value.attachmentPreview && !chat.groupFeatureEnabled(GroupFeature.Files) val voiceProhibited = composeState.value.preview is ComposePreview.VoicePreview && !chat.chatInfo.featureEnabled(ChatFeature.Voice) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt index 827af085ea..9298b600e9 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/AddGroupMembersView.kt @@ -105,7 +105,7 @@ fun getContactsToAdd(chatModel: ChatModel, search: String): List { .filterIsInstance() .filter { it.sendMsgEnabled } .map { it.contact } - .filter { c -> !c.nextSendGrpInv && c.contactId !in memberContactIds && c.anyNameContains(s) + .filter { c -> !c.sendMsgToConnect && c.contactId !in memberContactIds && c.anyNameContains(s) } .sortedBy { it.displayName.lowercase() } .toList() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt index db6eff562e..1a7192b22d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupChatInfoView.kt @@ -54,12 +54,11 @@ fun ModalData.GroupChatInfoView( chatsCtx: ChatModel.ChatsContext, rhId: Long?, chatId: String, - groupLink: CreatedConnLink?, - groupLinkMemberRole: GroupMemberRole?, + groupLink: GroupLink?, selectedItems: MutableState?>, appBar: MutableState<@Composable (BoxScope.() -> Unit)?>, scrollToItemId: MutableState, - onGroupLinkUpdated: (Pair?) -> Unit, + onGroupLinkUpdated: (GroupLink?) -> Unit, close: () -> Unit, onSearchClicked: () -> Unit ) { @@ -166,7 +165,7 @@ fun ModalData.GroupChatInfoView( clearChat = { clearChatDialog(chat, close) }, leaveGroup = { leaveGroupDialog(rhId, groupInfo, chatModel, close) }, manageGroupLink = { - ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, groupLinkMemberRole, onGroupLinkUpdated) } + ModalManager.end.showModal { GroupLinkView(chatModel, rhId, groupInfo, groupLink, onGroupLinkUpdated) } }, onSearchClicked = onSearchClicked, deletingItems = deletingItems @@ -375,7 +374,7 @@ fun ModalData.GroupChatInfoLayout( activeSortedMembers: List, developerTools: Boolean, onLocalAliasChanged: (String) -> Unit, - groupLink: CreatedConnLink?, + groupLink: GroupLink?, selectedItems: MutableState?>, appBar: MutableState<@Composable (BoxScope.() -> Unit)?>, scrollToItemId: MutableState, @@ -537,53 +536,57 @@ fun ModalData.GroupChatInfoLayout( } SectionDividerSpaced(maxTopPadding = true, maxBottomPadding = true) - SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), activeSortedMembers.count() + 1)) { - if (groupInfo.canAddMembers) { - val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers - val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary - val addMembersTitleId = when (groupInfo.businessChat?.chatType) { - BusinessChatType.Customer -> MR.strings.button_add_team_members - BusinessChatType.Business -> MR.strings.button_add_friends - null -> MR.strings.button_add_members + if (!groupInfo.nextConnectPrepared) { + SectionView(title = String.format(generalGetString(MR.strings.group_info_section_title_num_members), activeSortedMembers.count() + 1)) { + if (groupInfo.canAddMembers) { + val onAddMembersClick = if (chat.chatInfo.incognito) ::cantInviteIncognitoAlert else addMembers + val tint = if (chat.chatInfo.incognito) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + val addMembersTitleId = when (groupInfo.businessChat?.chatType) { + BusinessChatType.Customer -> MR.strings.button_add_team_members + BusinessChatType.Business -> MR.strings.button_add_friends + null -> MR.strings.button_add_members + } + AddMembersButton(addMembersTitleId, tint, onAddMembersClick) } - AddMembersButton(addMembersTitleId, tint, onAddMembersClick) - } - if (activeSortedMembers.size > 8) { - SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) { - MemberListSearchRowView(searchText) + if (activeSortedMembers.size > 8) { + SectionItemView(padding = PaddingValues(start = 14.dp, end = DEFAULT_PADDING_HALF)) { + MemberListSearchRowView(searchText) + } + } + SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) { + MemberRow(groupInfo.membership, user = true) } - } - SectionItemView(minHeight = 54.dp, padding = PaddingValues(horizontal = DEFAULT_PADDING)) { - MemberRow(groupInfo.membership, user = true) } } } - items(filteredMembers.value, key = { it.groupMemberId }) { member -> - Divider() - val showMenu = remember { mutableStateOf(false) } - val canBeSelected = groupInfo.membership.memberRole >= member.memberRole && member.memberRole < GroupMemberRole.Moderator - SectionItemViewLongClickable( - click = { - if (selectedItems.value != null) { - if (canBeSelected) { - toggleItemSelection(member.groupMemberId, selectedItems) + if (!groupInfo.nextConnectPrepared) { + items(filteredMembers.value, key = { it.groupMemberId }) { member -> + Divider() + val showMenu = remember { mutableStateOf(false) } + val canBeSelected = groupInfo.membership.memberRole >= member.memberRole && member.memberRole < GroupMemberRole.Moderator + SectionItemViewLongClickable( + click = { + if (selectedItems.value != null) { + if (canBeSelected) { + toggleItemSelection(member.groupMemberId, selectedItems) + } + } else { + showMemberInfo(member) + } + }, + longClick = { showMenu.value = true }, + minHeight = 54.dp, + padding = PaddingValues(horizontal = DEFAULT_PADDING) + ) { + Box(contentAlignment = Alignment.CenterStart) { + androidx.compose.animation.AnimatedVisibility(selectedItems.value != null, enter = fadeIn(), exit = fadeOut()) { + SelectedListItem(Modifier.alpha(if (canBeSelected) 1f else 0f).padding(start = 2.dp), member.groupMemberId, selectedItems) + } + val selectionOffset by animateDpAsState(if (selectedItems.value != null) 20.dp + 22.dp * fontSizeMultiplier else 0.dp) + DropDownMenuForMember(chat.remoteHostId, member, groupInfo, selectedItems, showMenu) + Box(Modifier.padding(start = selectionOffset)) { + MemberRow(member) } - } else { - showMemberInfo(member) - } - }, - longClick = { showMenu.value = true }, - minHeight = 54.dp, - padding = PaddingValues(horizontal = DEFAULT_PADDING) - ) { - Box(contentAlignment = Alignment.CenterStart) { - androidx.compose.animation.AnimatedVisibility(selectedItems.value != null, enter = fadeIn(), exit = fadeOut()) { - SelectedListItem(Modifier.alpha(if (canBeSelected) 1f else 0f).padding(start = 2.dp), member.groupMemberId, selectedItems) - } - val selectionOffset by animateDpAsState(if (selectedItems.value != null) 20.dp + 22.dp * fontSizeMultiplier else 0.dp) - DropDownMenuForMember(chat.remoteHostId, member, groupInfo, selectedItems, showMenu) - Box(Modifier.padding(start = selectionOffset)) { - MemberRow(member) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt index 25f8a861bb..fc44fdb7a8 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupLinkView.kt @@ -28,22 +28,21 @@ fun GroupLinkView( chatModel: ChatModel, rhId: Long?, groupInfo: GroupInfo, - connLinkContact: CreatedConnLink?, - memberRole: GroupMemberRole?, - onGroupLinkUpdated: ((Pair?) -> Unit)?, + groupLink: GroupLink?, + onGroupLinkUpdated: ((GroupLink?) -> Unit)?, creatingGroup: Boolean = false, close: (() -> Unit)? = null ) { - var groupLink by rememberSaveable(stateSaver = CreatedConnLink.nullableStateSaver) { mutableStateOf(connLinkContact) } - val groupLinkMemberRole = rememberSaveable { mutableStateOf(memberRole) } + var groupLinkVar by rememberSaveable { mutableStateOf(groupLink) } + val groupLinkMemberRole = rememberSaveable { mutableStateOf(groupLink?.acceptMemberRole) } var creatingLink by rememberSaveable { mutableStateOf(false) } fun createLink() { creatingLink = true withBGApi { val link = chatModel.controller.apiCreateGroupLink(rhId, groupInfo.groupId) if (link != null) { - groupLink = link.first - groupLinkMemberRole.value = link.second + groupLinkVar = link + groupLinkMemberRole.value = link.acceptMemberRole onGroupLinkUpdated?.invoke(link) } creatingLink = false @@ -54,8 +53,8 @@ fun GroupLinkView( withBGApi { val link = chatModel.controller.apiAddGroupShortLink(rhId, groupInfo.groupId) if (link != null) { - groupLink = link.first - groupLinkMemberRole.value = link.second + groupLinkVar = link + groupLinkMemberRole.value = link.acceptMemberRole onGroupLinkUpdated?.invoke(link) } creatingLink = false @@ -67,7 +66,7 @@ fun GroupLinkView( } } GroupLinkLayout( - groupLink = groupLink, + groupLink = groupLinkVar, groupInfo, groupLinkMemberRole, creatingLink, @@ -79,8 +78,8 @@ fun GroupLinkView( withBGApi { val link = chatModel.controller.apiGroupLinkMemberRole(rhId, groupInfo.groupId, role) if (link != null) { - groupLink = link.first - groupLinkMemberRole.value = link.second + groupLinkVar = link + groupLinkMemberRole.value = link.acceptMemberRole onGroupLinkUpdated?.invoke(link) } } @@ -95,7 +94,7 @@ fun GroupLinkView( withBGApi { val r = chatModel.controller.apiDeleteGroupLink(rhId, groupInfo.groupId) if (r) { - groupLink = null + groupLinkVar = null onGroupLinkUpdated?.invoke(null) } } @@ -113,7 +112,7 @@ fun GroupLinkView( @Composable fun GroupLinkLayout( - groupLink: CreatedConnLink?, + groupLink: GroupLink?, groupInfo: GroupInfo, groupLinkMemberRole: MutableState, creatingLink: Boolean, @@ -167,11 +166,11 @@ fun GroupLinkLayout( } val showShortLink = remember { mutableStateOf(true) } Spacer(Modifier.height(DEFAULT_PADDING_HALF)) - if (groupLink.connShortLink == null) { - SimpleXCreatedLinkQRCode(groupLink, short = false) + if (groupLink.connLinkContact.connShortLink == null) { + SimpleXCreatedLinkQRCode(groupLink.connLinkContact, short = false) } else { SectionViewWithButton(titleButton = { ToggleShortLinkButton(showShortLink) }) { - SimpleXCreatedLinkQRCode(groupLink, short = showShortLink.value) + SimpleXCreatedLinkQRCode(groupLink.connLinkContact, short = showShortLink.value) } } Row( @@ -183,7 +182,7 @@ fun GroupLinkLayout( SimpleButton( stringResource(MR.strings.share_link), icon = painterResource(MR.images.ic_share), - click = { clipboard.shareText(groupLink.simplexChatUri(short = showShortLink.value)) } + click = { clipboard.shareText(groupLink.connLinkContact.simplexChatUri(short = showShortLink.value)) } ) if (creatingGroup && close != null) { ContinueButton(close) @@ -196,8 +195,10 @@ fun GroupLinkLayout( ) } } - if (groupLink.connShortLink == null) { - AddShortLinkButton(addShortLink) + if (groupLink.connLinkContact.connShortLink == null) { + AddShortLinkButton(text = stringResource(MR.strings.add_short_link), addShortLink) + } else if (!groupLink.shortLinkDataSet) { + AddShortLinkButton(text = stringResource(MR.strings.share_group_profile_via_link), addShortLink) } } } @@ -206,10 +207,10 @@ fun GroupLinkLayout( } @Composable -private fun AddShortLinkButton(onClick: () -> Unit) { +private fun AddShortLinkButton(text: String, onClick: () -> Unit) { SettingsActionItem( painterResource(MR.images.ic_add), - stringResource(MR.strings.add_short_link), + text, onClick, iconColor = MaterialTheme.colors.primary, textColor = MaterialTheme.colors.primary, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index e56bc36562..ac06b88222 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt @@ -788,7 +788,7 @@ fun updateMembersRoleDialog( fun connectViaMemberAddressAlert(rhId: Long?, connReqUri: String) { try { withBGApi { - planAndConnect(rhId, connReqUri, incognito = null, close = { ModalManager.closeAllModalsEverywhere() }) + planAndConnect(rhId, connReqUri, close = { ModalManager.closeAllModalsEverywhere() }) } } catch (e: RuntimeException) { AlertManager.shared.showAlertMsg( 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 6e938aa5c4..5cf30f4d28 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 @@ -33,6 +33,7 @@ import chat.simplex.common.views.chatlist.openChat import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import dev.icerock.moko.resources.ImageResource +import dev.icerock.moko.resources.StringResource import kotlinx.datetime.Clock import kotlin.math.* @@ -667,26 +668,30 @@ fun ChatItemView( } @Composable - fun E2EEInfoNoPQText() { + fun e2eeInfoText(sId: StringResource) { Text( buildAnnotatedString { - withStyle(chatEventStyle) { append(annotatedStringResource(MR.strings.e2ee_info_no_pq)) } + withStyle(chatEventStyle) { append(annotatedStringResource(sId)) } }, Modifier.padding(horizontal = 6.dp, vertical = 6.dp) ) } + @Composable + fun E2EEInfoNoPQText() { + e2eeInfoText(MR.strings.e2ee_info_no_pq) + } + @Composable fun DirectE2EEInfoText(e2EEInfo: E2EEInfo) { - if (e2EEInfo.pqEnabled) { - Text( - buildAnnotatedString { - withStyle(chatEventStyle) { append(annotatedStringResource(MR.strings.e2ee_info_pq)) } - }, - Modifier.padding(horizontal = 6.dp, vertical = 6.dp) - ) + if (e2EEInfo.pqEnabled != null) { + if (e2EEInfo.pqEnabled) { + e2eeInfoText(MR.strings.e2ee_info_pq) + } else { + E2EEInfoNoPQText() + } } else { - E2EEInfoNoPQText() + e2eeInfoText(MR.strings.e2ee_info_e2ee) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index 52b4059eef..d602c95c03 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -132,7 +132,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { click = { contactRequestAlertDialog(chat.remoteHostId, chat.chatInfo, chatModel) { onRequestAccepted(it) } }, dropdownMenuItems = { tryOrShowError("${chat.id}ChatListNavLinkDropdown", error = {}) { - ContactRequestMenuItems(chat.remoteHostId, chat.chatInfo, chatModel, showMenu) + ContactRequestMenuItems(chat.remoteHostId, contactRequestId = chat.chatInfo.apiId, chatModel, showMenu) } }, showMenu, @@ -271,18 +271,22 @@ suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatMo @Composable fun ContactMenuItems(chat: Chat, contact: Contact, chatModel: ChatModel, showMenu: MutableState, showMarkRead: Boolean) { - if (contact.activeConn != null) { - if (showMarkRead) { - MarkReadChatAction(chat, showMenu) - } else { - MarkUnreadChatAction(chat, chatModel, showMenu) + if (contact.nextAcceptContactRequest && contact.contactRequestId != null) { + ContactRequestMenuItems(chat.remoteHostId, contactRequestId = contact.contactRequestId, chatModel, showMenu) + } else { + if (contact.activeConn != null) { + if (showMarkRead) { + MarkReadChatAction(chat, showMenu) + } else { + MarkUnreadChatAction(chat, chatModel, showMenu) + } + ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu) + ToggleNotificationsChatAction(chat, chatModel, contact.chatSettings.enableNtfs.nextMode(false), showMenu) + TagListAction(chat, showMenu) + ClearChatAction(chat, showMenu) } - ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu) - ToggleNotificationsChatAction(chat, chatModel, contact.chatSettings.enableNtfs.nextMode(false), showMenu) - TagListAction(chat, showMenu) - ClearChatAction(chat, showMenu) + DeleteContactAction(chat, chatModel, showMenu) } - DeleteContactAction(chat, chatModel, showMenu) } @Composable @@ -509,30 +513,32 @@ fun LeaveGroupAction(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel, sh } @Composable -fun ContactRequestMenuItems(rhId: Long?, chatInfo: ChatInfo.ContactRequest, chatModel: ChatModel, showMenu: MutableState, onSuccess: ((chat: Chat) -> Unit)? = null) { +fun ContactRequestMenuItems(rhId: Long?, contactRequestId: Long, chatModel: ChatModel, showMenu: MutableState, onSuccess: ((chat: Chat) -> Unit)? = null) { ItemAction( stringResource(MR.strings.accept_contact_button), painterResource(MR.images.ic_check), color = MaterialTheme.colors.onBackground, onClick = { - acceptContactRequest(rhId, incognito = false, chatInfo.apiId, chatInfo, true, chatModel, onSuccess) - showMenu.value = false - } - ) - ItemAction( - stringResource(MR.strings.accept_contact_incognito_button), - painterResource(MR.images.ic_theater_comedy), - color = MaterialTheme.colors.onBackground, - onClick = { - acceptContactRequest(rhId, incognito = true, chatInfo.apiId, chatInfo, true, chatModel, onSuccess) + acceptContactRequest(rhId, incognito = false, contactRequestId, true, chatModel, onSuccess) showMenu.value = false } ) + if (!chatModel.addressShortLinkDataSet) { + ItemAction( + stringResource(MR.strings.accept_contact_incognito_button), + painterResource(MR.images.ic_theater_comedy), + color = MaterialTheme.colors.onBackground, + onClick = { + acceptContactRequest(rhId, incognito = true, contactRequestId, true, chatModel, onSuccess) + showMenu.value = false + } + ) + } ItemAction( stringResource(MR.strings.reject_contact_button), painterResource(MR.images.ic_close), onClick = { - rejectContactRequest(rhId, chatInfo, chatModel) + rejectContactRequest(rhId, contactRequestId, chatModel) showMenu.value = false }, color = Color.Red @@ -665,19 +671,21 @@ fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactReque Column { SectionItemView({ AlertManager.shared.hideAlert() - acceptContactRequest(rhId, incognito = false, contactRequest.apiId, contactRequest, true, chatModel, onSucess) + acceptContactRequest(rhId, incognito = false, contactRequest.apiId, true, chatModel, onSucess) }) { Text(generalGetString(MR.strings.accept_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } - SectionItemView({ - AlertManager.shared.hideAlert() - acceptContactRequest(rhId, incognito = true, contactRequest.apiId, contactRequest, true, chatModel, onSucess) - }) { - Text(generalGetString(MR.strings.accept_contact_incognito_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + if (!chatModel.addressShortLinkDataSet) { + SectionItemView({ + AlertManager.shared.hideAlert() + acceptContactRequest(rhId, incognito = true, contactRequest.apiId, true, chatModel, onSucess) + }) { + Text(generalGetString(MR.strings.accept_contact_incognito_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } } SectionItemView({ AlertManager.shared.hideAlert() - rejectContactRequest(rhId, contactRequest, chatModel) + rejectContactRequest(rhId, contactRequest.apiId, chatModel) }) { Text(generalGetString(MR.strings.reject_contact_button), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red) } @@ -687,13 +695,17 @@ fun contactRequestAlertDialog(rhId: Long?, contactRequest: ChatInfo.ContactReque ) } -fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRequest: ChatInfo.ContactRequest?, isCurrentUser: Boolean, chatModel: ChatModel, close: ((chat: Chat) -> Unit)? = null ) { +fun acceptContactRequest(rhId: Long?, incognito: Boolean, contactRequestId: Long, isCurrentUser: Boolean, chatModel: ChatModel, close: ((chat: Chat) -> Unit)? = null ) { withBGApi { - val contact = chatModel.controller.apiAcceptContactRequest(rhId, incognito, apiId) - if (contact != null && isCurrentUser && contactRequest != null) { + val contact = chatModel.controller.apiAcceptContactRequest(rhId, incognito, contactRequestId) + if (contact != null && isCurrentUser) { val chat = Chat(remoteHostId = rhId, ChatInfo.Direct(contact), listOf()) withContext(Dispatchers.Main) { - chatModel.chatsContext.replaceChat(rhId, contactRequest.id, chat) + if (contact.contactRequestId != null) { // means contact request was initially created with contact, so we don't need to replace it + chatModel.chatsContext.updateContact(rhId, contact) + } else { + chatModel.chatsContext.replaceChat(rhId, contactRequestChatId(contactRequestId), chat) + } } chatModel.setContactNetworkStatus(contact, NetworkStatus.Connected()) close?.invoke(chat) @@ -701,11 +713,18 @@ fun acceptContactRequest(rhId: Long?, incognito: Boolean, apiId: Long, contactRe } } -fun rejectContactRequest(rhId: Long?, contactRequest: ChatInfo.ContactRequest, chatModel: ChatModel) { +fun rejectContactRequest(rhId: Long?, contactRequestId: Long, chatModel: ChatModel, dismissToChatList: Boolean = false) { withBGApi { - chatModel.controller.apiRejectContactRequest(rhId, contactRequest.apiId) + val contact_ = chatModel.controller.apiRejectContactRequest(rhId, contactRequestId) withContext(Dispatchers.Main) { - chatModel.chatsContext.removeChat(rhId, contactRequest.id) + if (contact_ != null) { // means contact request was initially created with contact, so we need to remove contact chat + chatModel.chatsContext.removeChat(rhId, contact_.id) + } else { + chatModel.chatsContext.removeChat(rhId, contactRequestChatId(contactRequestId)) + } + if (dismissToChatList) { + chatModel.chatId.value = null + } } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 87c02f038c..334b220ee7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -590,7 +590,7 @@ fun connectIfOpenedViaUri(rhId: Long?, uri: String, chatModel: ChatModel) { chatModel.appOpenUrl.value = rhId to uri } else { withBGApi { - planAndConnect(rhId, uri, incognito = null, close = null) + planAndConnect(rhId, uri, close = null) } } } @@ -677,7 +677,6 @@ private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState< planAndConnect( chatModel.remoteHostId(), link, - incognito = null, filterKnownContact = { searchChatFilteredBySimplexLink.value = it.id }, filterKnownGroup = { searchChatFilteredBySimplexLink.value = it.id }, close = null, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index 65a9d175dd..63ab0440f2 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -145,8 +145,11 @@ fun ChatPreviewView( chatPreviewTitleText( if (deleting) MaterialTheme.colors.secondary - else + else if (cInfo.contact.nextAcceptContactRequest) { + MaterialTheme.colors.primary + } else { Color.Unspecified + } ) } is ChatInfo.Group -> @@ -229,21 +232,25 @@ fun ChatPreviewView( is ChatInfo.Direct -> if (cInfo.contact.activeConn == null && cInfo.contact.profile.contactLink != null && cInfo.contact.active) { Text(stringResource(MR.strings.contact_tap_to_connect), color = MaterialTheme.colors.primary) - } else if (!cInfo.contact.sndReady && cInfo.contact.activeConn != null) { - if (cInfo.contact.nextSendGrpInv) { - Text(stringResource(MR.strings.member_contact_send_direct_message), color = MaterialTheme.colors.secondary) - } else if (cInfo.contact.active) { - Text(stringResource(MR.strings.contact_connection_pending), color = MaterialTheme.colors.secondary) - } + } else if (cInfo.contact.nextAcceptContactRequest) { + Text(stringResource(MR.strings.hold_or_open_to_connect)) + } else if (cInfo.contact.sendMsgToConnect) { + Text(stringResource(MR.strings.member_contact_send_direct_message)) + } else if (!cInfo.contact.sndReady && cInfo.contact.activeConn != null && cInfo.contact.active) { + Text(stringResource(MR.strings.contact_connection_pending), color = MaterialTheme.colors.secondary) } is ChatInfo.Group -> - when (cInfo.groupInfo.membership.memberStatus) { - GroupMemberStatus.MemRejected -> Text(stringResource(MR.strings.group_preview_rejected)) - GroupMemberStatus.MemInvited -> Text(groupInvitationPreviewText(currentUserProfileDisplayName, cInfo.groupInfo)) - GroupMemberStatus.MemAccepted -> Text(stringResource(MR.strings.group_connection_pending), color = MaterialTheme.colors.secondary) - GroupMemberStatus.MemPendingReview, GroupMemberStatus.MemPendingApproval -> - Text(stringResource(MR.strings.reviewed_by_admins), color = MaterialTheme.colors.secondary) - else -> {} + if (cInfo.groupInfo.nextConnectPrepared) { + Text(stringResource(MR.strings.group_preview_open_to_join)) + } else { + when (cInfo.groupInfo.membership.memberStatus) { + GroupMemberStatus.MemRejected -> Text(stringResource(MR.strings.group_preview_rejected)) + GroupMemberStatus.MemInvited -> Text(groupInvitationPreviewText(currentUserProfileDisplayName, cInfo.groupInfo)) + GroupMemberStatus.MemAccepted -> Text(stringResource(MR.strings.group_connection_pending), color = MaterialTheme.colors.secondary) + GroupMemberStatus.MemPendingReview, GroupMemberStatus.MemPendingApproval -> + Text(stringResource(MR.strings.reviewed_by_admins), color = MaterialTheme.colors.secondary) + else -> {} + } } else -> {} } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt index 4e65a3649e..83df302064 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt @@ -53,13 +53,7 @@ fun ContactListNavLinkView(chat: Chat, nextChatSelected: State, showDel click = { hideKeyboard(view) when (contactType) { - ContactType.RECENT -> { - withApi { - openChat(secondaryChatsCtx = null, rhId, chat.chatInfo) - ModalManager.start.closeModals() - } - } - ContactType.CHAT_DELETED -> { + ContactType.RECENT, ContactType.CONTACT_WITH_REQUEST, ContactType.CHAT_DELETED -> { withApi { openChat(secondaryChatsCtx = null, rhId, chat.chatInfo) ModalManager.start.closeModals() @@ -79,7 +73,17 @@ fun ContactListNavLinkView(chat: Chat, nextChatSelected: State, showDel }, dropdownMenuItems = { tryOrShowError("${chat.id}ContactListNavLinkDropdown", error = {}) { - DeleteContactAction(chat, chatModel, showMenu) + if (contactType == ContactType.CONTACT_WITH_REQUEST && chat.chatInfo.contact.contactRequestId != null) { + ContactRequestMenuItems( + rhId = chat.remoteHostId, + contactRequestId = chat.chatInfo.contact.contactRequestId, + chatModel = chatModel, + showMenu = showMenu, + onSuccess = { onRequestAccepted(it) } + ) + } else { + DeleteContactAction(chat, chatModel, showMenu) + } } }, showMenu, @@ -108,7 +112,7 @@ fun ContactListNavLinkView(chat: Chat, nextChatSelected: State, showDel tryOrShowError("${chat.id}ContactListNavLinkDropdown", error = {}) { ContactRequestMenuItems( rhId = chat.remoteHostId, - chatInfo = chat.chatInfo, + contactRequestId = chat.chatInfo.apiId, chatModel = chatModel, showMenu = showMenu, onSuccess = { onRequestAccepted(it) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactPreviewView.kt index dd03bca921..c02a35891b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactPreviewView.kt @@ -38,6 +38,7 @@ fun ContactPreviewView( val textColor = when { deleting -> MaterialTheme.colors.secondary contactType == ContactType.CARD -> MaterialTheme.colors.primary + contactType == ContactType.CONTACT_WITH_REQUEST -> MaterialTheme.colors.primary contactType == ContactType.REQUEST -> MaterialTheme.colors.primary contactType == ContactType.RECENT && chat.chatInfo.incognito -> Indigo else -> Color.Unspecified @@ -85,7 +86,7 @@ fun ContactPreviewView( Spacer(Modifier.fillMaxWidth().weight(1f)) - if (chat.chatInfo is ChatInfo.ContactRequest) { + if (chat.chatInfo is ChatInfo.ContactRequest || contactType == ContactType.CONTACT_WITH_REQUEST) { Icon( painterResource(MR.images.ic_check), contentDescription = null, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt index 6bfcf2809f..399034f66b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/AlertManager.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.* import chat.simplex.common.model.ChatModel @@ -266,6 +267,78 @@ class AlertManager { hostDevice: Pair? = null, ) = showAlertMsg(generalGetString(title), if (text != null) generalGetString(text) else null, generalGetString(confirmText), onConfirm, hostDevice) + fun showOpenChatAlert( + profileName: String, + profileImage: @Composable () -> Unit, + confirmText: String = generalGetString(MR.strings.connect_plan_open_chat), + onConfirm: () -> Unit, + dismissText: String = generalGetString(MR.strings.cancel_verb), + onDismiss: (() -> Unit)?, + ) { + showAlert { + AlertDialog( + onDismissRequest = { + onDismiss?.invoke() + hideAlert() + }, + buttons = { + AlertContent(text = null as String?, null) { + Column( + Modifier + .width(360.dp) + .padding(top = DEFAULT_PADDING), + verticalArrangement = Arrangement.SpaceEvenly + ) { + Row( + Modifier.fillMaxWidth().padding(horizontal = DEFAULT_PADDING), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start + ) { + profileImage() + + Spacer(Modifier.width(DEFAULT_PADDING_HALF)) + + Text( + profileName, + fontWeight = FontWeight.SemiBold, + maxLines = 2 + ) + } + + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceEvenly + ) { + val focusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { + // Wait before focusing to prevent auto-confirming if a user used Enter key on hardware keyboard + delay(200) + focusRequester.requestFocus() + } + TextButton(onClick = { + onDismiss?.invoke() + hideAlert() + }) { + Text(dismissText) + } + + Spacer(Modifier.width(0.dp)) + + TextButton(onClick = { + onConfirm.invoke() + hideAlert() + }, Modifier.focusRequester(focusRequester)) { + Text(confirmText) + } + } + } + } + } + ) + } + } + @Composable fun showInView() { alertViews.collectAsState().value.lastOrNull()?.invoke() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt index 2db40f770d..fd0d41478a 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/AddGroupView.kt @@ -56,7 +56,7 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit, c } } else { ModalManager.end.showModalCloseable(true) { close -> - GroupLinkView(chatModel, rhId, groupInfo, connLinkContact = null, memberRole = null, onGroupLinkUpdated = null, creatingGroup = true, close) + GroupLinkView(chatModel, rhId, groupInfo, groupLink = null, onGroupLinkUpdated = null, creatingGroup = true, close) } } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt index 330c80b7a2..e722d0223c 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/ConnectPlan.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.material.* import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp import dev.icerock.moko.resources.compose.stringResource import chat.simplex.common.model.* import chat.simplex.common.platform.* @@ -20,7 +21,6 @@ enum class ConnectionLinkType { suspend fun planAndConnect( rhId: Long?, shortOrFullLink: String, - incognito: Boolean?, close: (() -> Unit)?, cleanup: (() -> Unit)? = null, filterKnownContact: ((Contact) -> Unit)? = null, @@ -46,11 +46,18 @@ suspend fun planAndConnect( "" when (connectionPlan) { is ConnectionPlan.InvitationLink -> when (connectionPlan.invitationLinkPlan) { - InvitationLinkPlan.Ok -> { - Log.d(TAG, "planAndConnect, .InvitationLink, .Ok, incognito=$incognito") - if (incognito != null) { - connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) + is InvitationLinkPlan.Ok -> + if (connectionPlan.invitationLinkPlan.contactSLinkData_ != null) { + Log.d(TAG, "planAndConnect, .InvitationLink, .Ok, short link data present") + showPrepareContactAlert( + rhId, + connectionLink, + connectionPlan.invitationLinkPlan.contactSLinkData_, + close, + cleanup + ) } else { + Log.d(TAG, "planAndConnect, .InvitationLink, .Ok, no short link data") askCurrentOrIncognitoProfileAlert( chatModel, rhId, connectionLink, connectionPlan, close, title = generalGetString(MR.strings.connect_via_invitation_link), @@ -59,32 +66,18 @@ suspend fun planAndConnect( cleanup = cleanup, ) } - } InvitationLinkPlan.OwnLink -> { - Log.d(TAG, "planAndConnect, .InvitationLink, .OwnLink, incognito=$incognito") - if (incognito != null) { - AlertManager.privacySensitive.showAlertDialog( - title = generalGetString(MR.strings.connect_plan_connect_to_yourself), - text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link) + linkText, - confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), - onConfirm = { withBGApi { connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } }, - onDismiss = cleanup, - onDismissRequest = cleanup, - destructive = true, - hostDevice = hostDevice(rhId), - ) - } else { - askCurrentOrIncognitoProfileAlert( - chatModel, rhId, connectionLink, connectionPlan, close, - title = generalGetString(MR.strings.connect_plan_connect_to_yourself), - text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link) + linkText, - connectDestructive = true, - cleanup = cleanup, - ) - } + Log.d(TAG, "planAndConnect, .InvitationLink, .OwnLink") + askCurrentOrIncognitoProfileAlert( + chatModel, rhId, connectionLink, connectionPlan, close, + title = generalGetString(MR.strings.connect_plan_connect_to_yourself), + text = generalGetString(MR.strings.connect_plan_this_is_your_own_one_time_link) + linkText, + connectDestructive = true, + cleanup = cleanup, + ) } is InvitationLinkPlan.Connecting -> { - Log.d(TAG, "planAndConnect, .InvitationLink, .Connecting, incognito=$incognito") + Log.d(TAG, "planAndConnect, .InvitationLink, .Connecting") val contact = connectionPlan.invitationLinkPlan.contact_ if (contact != null) { if (filterKnownContact != null) { @@ -108,7 +101,7 @@ suspend fun planAndConnect( } } is InvitationLinkPlan.Known -> { - Log.d(TAG, "planAndConnect, .InvitationLink, .Known, incognito=$incognito") + Log.d(TAG, "planAndConnect, .InvitationLink, .Known") val contact = connectionPlan.invitationLinkPlan.contact if (filterKnownContact != null) { filterKnownContact(contact) @@ -124,11 +117,18 @@ suspend fun planAndConnect( } } is ConnectionPlan.ContactAddress -> when (connectionPlan.contactAddressPlan) { - ContactAddressPlan.Ok -> { - Log.d(TAG, "planAndConnect, .ContactAddress, .Ok, incognito=$incognito") - if (incognito != null) { - connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) + is ContactAddressPlan.Ok -> + if (connectionPlan.contactAddressPlan.contactSLinkData_ != null) { + Log.d(TAG, "planAndConnect, .ContactAddress, .Ok, short link data present") + showPrepareContactAlert( + rhId, + connectionLink, + connectionPlan.contactAddressPlan.contactSLinkData_, + close, + cleanup + ) } else { + Log.d(TAG, "planAndConnect, .ContactAddress, .Ok, no short link data") askCurrentOrIncognitoProfileAlert( chatModel, rhId, connectionLink, connectionPlan, close, title = generalGetString(MR.strings.connect_via_contact_link), @@ -137,55 +137,28 @@ suspend fun planAndConnect( cleanup, ) } - } ContactAddressPlan.OwnLink -> { - Log.d(TAG, "planAndConnect, .ContactAddress, .OwnLink, incognito=$incognito") - if (incognito != null) { - AlertManager.privacySensitive.showAlertDialog( - title = generalGetString(MR.strings.connect_plan_connect_to_yourself), - text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address) + linkText, - confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), - onConfirm = { withBGApi { connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } }, - destructive = true, - onDismiss = cleanup, - onDismissRequest = cleanup, - hostDevice = hostDevice(rhId), - ) - } else { - askCurrentOrIncognitoProfileAlert( - chatModel, rhId, connectionLink, connectionPlan, close, - title = generalGetString(MR.strings.connect_plan_connect_to_yourself), - text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address) + linkText, - connectDestructive = true, - cleanup = cleanup, - ) - } + Log.d(TAG, "planAndConnect, .ContactAddress, .OwnLink") + askCurrentOrIncognitoProfileAlert( + chatModel, rhId, connectionLink, connectionPlan, close, + title = generalGetString(MR.strings.connect_plan_connect_to_yourself), + text = generalGetString(MR.strings.connect_plan_this_is_your_own_simplex_address) + linkText, + connectDestructive = true, + cleanup = cleanup, + ) } ContactAddressPlan.ConnectingConfirmReconnect -> { - Log.d(TAG, "planAndConnect, .ContactAddress, .ConnectingConfirmReconnect, incognito=$incognito") - if (incognito != null) { - AlertManager.privacySensitive.showAlertDialog( - title = generalGetString(MR.strings.connect_plan_repeat_connection_request), - text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address) + linkText, - confirmText = if (incognito) generalGetString(MR.strings.connect_via_link_incognito) else generalGetString(MR.strings.connect_via_link_verb), - onConfirm = { withBGApi { connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } }, - onDismiss = cleanup, - onDismissRequest = cleanup, - destructive = true, - hostDevice = hostDevice(rhId), - ) - } else { - askCurrentOrIncognitoProfileAlert( - chatModel, rhId, connectionLink, connectionPlan, close, - title = generalGetString(MR.strings.connect_plan_repeat_connection_request), - text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address) + linkText, - connectDestructive = true, - cleanup = cleanup, - ) - } + Log.d(TAG, "planAndConnect, .ContactAddress, .ConnectingConfirmReconnect") + askCurrentOrIncognitoProfileAlert( + chatModel, rhId, connectionLink, connectionPlan, close, + title = generalGetString(MR.strings.connect_plan_repeat_connection_request), + text = generalGetString(MR.strings.connect_plan_you_have_already_requested_connection_via_this_address) + linkText, + connectDestructive = true, + cleanup = cleanup, + ) } is ContactAddressPlan.ConnectingProhibit -> { - Log.d(TAG, "planAndConnect, .ContactAddress, .ConnectingProhibit, incognito=$incognito") + Log.d(TAG, "planAndConnect, .ContactAddress, .ConnectingProhibit") val contact = connectionPlan.contactAddressPlan.contact if (filterKnownContact != null) { filterKnownContact(contact) @@ -200,7 +173,7 @@ suspend fun planAndConnect( } } is ContactAddressPlan.Known -> { - Log.d(TAG, "planAndConnect, .ContactAddress, .Known, incognito=$incognito") + Log.d(TAG, "planAndConnect, .ContactAddress, .Known") val contact = connectionPlan.contactAddressPlan.contact if (filterKnownContact != null) { filterKnownContact(contact) @@ -215,31 +188,25 @@ suspend fun planAndConnect( } } is ContactAddressPlan.ContactViaAddress -> { - Log.d(TAG, "planAndConnect, .ContactAddress, .ContactViaAddress, incognito=$incognito") + Log.d(TAG, "planAndConnect, .ContactAddress, .ContactViaAddress") val contact = connectionPlan.contactAddressPlan.contact - if (incognito != null) { - close() - connectContactViaAddress(chatModel, rhId, contact.contactId, incognito) - } else { - askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close, openChat = false) - } + askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close, openChat = false) cleanup() } } is ConnectionPlan.GroupLink -> when (connectionPlan.groupLinkPlan) { - GroupLinkPlan.Ok -> { - Log.d(TAG, "planAndConnect, .GroupLink, .Ok, incognito=$incognito") - if (incognito != null) { - AlertManager.privacySensitive.showAlertDialog( - title = generalGetString(MR.strings.connect_via_group_link), - text = generalGetString(MR.strings.you_will_join_group) + linkText, - confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), - onConfirm = { withBGApi { connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } }, - onDismiss = cleanup, - onDismissRequest = cleanup, - hostDevice = hostDevice(rhId), + is GroupLinkPlan.Ok -> + if (connectionPlan.groupLinkPlan.groupSLinkData_ != null) { + Log.d(TAG, "planAndConnect, .GroupLink, .Ok, short link data present") + showPrepareGroupAlert( + rhId, + connectionLink, + connectionPlan.groupLinkPlan.groupSLinkData_, + close, + cleanup ) } else { + Log.d(TAG, "planAndConnect, .GroupLink, .Ok, no short link data") askCurrentOrIncognitoProfileAlert( chatModel, rhId, connectionLink, connectionPlan, close, title = generalGetString(MR.strings.connect_via_group_link), @@ -248,41 +215,27 @@ suspend fun planAndConnect( cleanup = cleanup, ) } - } is GroupLinkPlan.OwnLink -> { - Log.d(TAG, "planAndConnect, .GroupLink, .OwnLink, incognito=$incognito") + Log.d(TAG, "planAndConnect, .GroupLink, .OwnLink") val groupInfo = connectionPlan.groupLinkPlan.groupInfo if (filterKnownGroup != null) { filterKnownGroup(groupInfo) } else { - ownGroupLinkConfirmConnect(chatModel, rhId, connectionLink, linkText, incognito, connectionPlan, groupInfo, close, cleanup) + ownGroupLinkConfirmConnect(chatModel, rhId, connectionLink, linkText, connectionPlan, groupInfo, close, cleanup) } } GroupLinkPlan.ConnectingConfirmReconnect -> { - Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingConfirmReconnect, incognito=$incognito") - if (incognito != null) { - AlertManager.privacySensitive.showAlertDialog( - title = generalGetString(MR.strings.connect_plan_repeat_join_request), - text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link) + linkText, - confirmText = if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), - onConfirm = { withBGApi { connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) } }, - onDismiss = cleanup, - onDismissRequest = cleanup, - destructive = true, - hostDevice = hostDevice(rhId), - ) - } else { - askCurrentOrIncognitoProfileAlert( - chatModel, rhId, connectionLink, connectionPlan, close, - title = generalGetString(MR.strings.connect_plan_repeat_join_request), - text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link) + linkText, - connectDestructive = true, - cleanup = cleanup, - ) - } + Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingConfirmReconnect") + askCurrentOrIncognitoProfileAlert( + chatModel, rhId, connectionLink, connectionPlan, close, + title = generalGetString(MR.strings.connect_plan_repeat_join_request), + text = generalGetString(MR.strings.connect_plan_you_are_already_joining_the_group_via_this_link) + linkText, + connectDestructive = true, + cleanup = cleanup, + ) } is GroupLinkPlan.ConnectingProhibit -> { - Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingProhibit, incognito=$incognito") + Log.d(TAG, "planAndConnect, .GroupLink, .ConnectingProhibit") val groupInfo = connectionPlan.groupLinkPlan.groupInfo_ if (groupInfo != null) { if (groupInfo.businessChat == null) { @@ -306,7 +259,7 @@ suspend fun planAndConnect( cleanup() } is GroupLinkPlan.Known -> { - Log.d(TAG, "planAndConnect, .GroupLink, .Known, incognito=$incognito") + Log.d(TAG, "planAndConnect, .GroupLink, .Known") val groupInfo = connectionPlan.groupLinkPlan.groupInfo if (filterKnownGroup != null) { filterKnownGroup(groupInfo) @@ -331,16 +284,12 @@ suspend fun planAndConnect( } is ConnectionPlan.Error -> { Log.d(TAG, "planAndConnect, error ${connectionPlan.chatError}") - if (incognito != null) { - connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan = null, close, cleanup) - } else { - askCurrentOrIncognitoProfileAlert( - chatModel, rhId, connectionLink, connectionPlan = null, close, - title = generalGetString(MR.strings.connect_plan_connect_via_link), - connectDestructive = false, - cleanup = cleanup, - ) - } + askCurrentOrIncognitoProfileAlert( + chatModel, rhId, connectionLink, connectionPlan = null, close, + title = generalGetString(MR.strings.connect_plan_connect_via_link), + connectDestructive = false, + cleanup = cleanup, + ) } } } @@ -448,7 +397,6 @@ fun ownGroupLinkConfirmConnect( rhId: Long?, connectionLink: CreatedConnLink, linkText: String, - incognito: Boolean?, connectionPlan: ConnectionPlan?, groupInfo: GroupInfo, close: (() -> Unit)?, @@ -467,38 +415,23 @@ fun ownGroupLinkConfirmConnect( }) { Text(generalGetString(MR.strings.connect_plan_open_group), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } - if (incognito != null) { - // Join incognito / Join with current profile - SectionItemView({ - AlertManager.privacySensitive.hideAlert() - withBGApi { - connectViaUri(chatModel, rhId, connectionLink, incognito, connectionPlan, close, cleanup) - } - }) { - Text( - if (incognito) generalGetString(MR.strings.join_group_incognito_button) else generalGetString(MR.strings.join_group_button), - Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error - ) + // Use current profile + SectionItemView({ + AlertManager.privacySensitive.hideAlert() + withBGApi { + connectViaUri(chatModel, rhId, connectionLink, incognito = false, connectionPlan, close, cleanup) } - } else { - // Use current profile - SectionItemView({ - AlertManager.privacySensitive.hideAlert() - withBGApi { - connectViaUri(chatModel, rhId, connectionLink, incognito = false, connectionPlan, close, cleanup) - } - }) { - Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) - } - // Use new incognito profile - SectionItemView({ - AlertManager.privacySensitive.hideAlert() - withBGApi { - connectViaUri(chatModel, rhId, connectionLink, incognito = true, connectionPlan, close, cleanup) - } - }) { - Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + }) { + Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + // Use new incognito profile + SectionItemView({ + AlertManager.privacySensitive.hideAlert() + withBGApi { + connectViaUri(chatModel, rhId, connectionLink, incognito = true, connectionPlan, close, cleanup) } + }) { + Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) } // Cancel SectionItemView({ @@ -523,3 +456,63 @@ fun openKnownGroup(chatModel: ChatModel, rhId: Long?, close: (() -> Unit)?, grou } } } + +fun showPrepareContactAlert( + rhId: Long?, + connectionLink: CreatedConnLink, + contactShortLinkData: ContactShortLinkData, + close: (() -> Unit)?, + cleanup: (() -> Unit)? +) { + AlertManager.privacySensitive.showOpenChatAlert( + profileName = contactShortLinkData.profile.displayName, + profileImage = { ProfileImage(size = 72.dp, image = contactShortLinkData.profile.image) }, + onConfirm = { + AlertManager.privacySensitive.hideAlert() + withBGApi { + val contact = chatModel.controller.apiPrepareContact(rhId, connectionLink, contactShortLinkData) + if (contact != null) { + withContext(Dispatchers.Main) { + val chatInfo = ChatInfo.Direct(contact) + ChatController.chatModel.chatsContext.addChat(Chat(rhId, chatInfo, chatItems = listOf())) + openKnownContact(chatModel, rhId, close, contact) + } + } + cleanup?.invoke() + } + }, + onDismiss = { + cleanup?.invoke() + } + ) +} + +fun showPrepareGroupAlert( + rhId: Long?, + connectionLink: CreatedConnLink, + groupShortLinkData: GroupShortLinkData, + close: (() -> Unit)?, + cleanup: (() -> Unit)? +) { + AlertManager.privacySensitive.showOpenChatAlert( + profileName = groupShortLinkData.groupProfile.displayName, + profileImage = { ProfileImage(size = 72.dp, image = groupShortLinkData.groupProfile.image, icon = MR.images.ic_supervised_user_circle_filled) }, + onConfirm = { + AlertManager.privacySensitive.hideAlert() + withBGApi { + val groupInfo = chatModel.controller.apiPrepareGroup(rhId, connectionLink, groupShortLinkData) + if (groupInfo != null) { + withContext(Dispatchers.Main) { + val chatInfo = ChatInfo.Group(groupInfo, groupChatScope = null) + ChatController.chatModel.chatsContext.addChat(Chat(rhId, chatInfo, chatItems = listOf())) + openKnownGroup(chatModel, rhId, close, groupInfo) + } + } + cleanup?.invoke() + } + }, + onDismiss = { + cleanup?.invoke() + } + ) +} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt index cb4991c99f..595e18d404 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt @@ -73,7 +73,7 @@ fun ModalData.NewChatSheet(rh: RemoteHostInfo?, close: () -> Unit) { } enum class ContactType { - CARD, REQUEST, RECENT, CHAT_DELETED, UNLISTED + CARD, CONTACT_WITH_REQUEST, REQUEST, RECENT, CHAT_DELETED, UNLISTED } fun chatContactType(chat: Chat): ContactType { @@ -81,8 +81,8 @@ fun chatContactType(chat: Chat): ContactType { is ChatInfo.ContactRequest -> ContactType.REQUEST is ChatInfo.Direct -> { val contact = cInfo.contact - when { + contact.nextAcceptContactRequest -> ContactType.CONTACT_WITH_REQUEST contact.activeConn == null && contact.profile.contactLink != null && contact.active -> ContactType.CARD contact.chatDeleted -> ContactType.CHAT_DELETED contact.contactStatus == ContactStatus.Active -> ContactType.RECENT @@ -127,7 +127,7 @@ private fun ModalData.NewChatSheetLayout( val searchShowingSimplexLink = remember { mutableStateOf(false) } val searchChatFilteredBySimplexLink = remember { mutableStateOf(null) } val showUnreadAndFavorites = remember { ChatController.appPrefs.showUnreadAndFavorites.state }.value - val baseContactTypes = remember { listOf(ContactType.CARD, ContactType.RECENT, ContactType.REQUEST) } + val baseContactTypes = remember { listOf(ContactType.CARD, ContactType.CONTACT_WITH_REQUEST, ContactType.REQUEST, ContactType.RECENT) } val contactTypes by remember(searchText.value.text.isEmpty()) { derivedStateOf { contactTypesSearchTargets(baseContactTypes, searchText.value.text.isEmpty()) } } @@ -557,7 +557,6 @@ private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState< planAndConnect( chatModel.remoteHostId(), link, - incognito = null, filterKnownContact = { searchChatFilteredBySimplexLink.value = it.id }, close = close, cleanup = cleanup, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt index 1b3138d21c..b0a2ce52d4 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatView.kt @@ -409,7 +409,10 @@ fun ActiveProfilePicker( val activeProfile = filteredProfiles.firstOrNull { it.activeUser } if (activeProfile != null) { - val otherProfiles = filteredProfiles.filter { it.userId != activeProfile.userId } + val otherProfiles = + filteredProfiles + .filter { it.userId != activeProfile.userId } + .sortedByDescending { it.activeOrder } item { when { !showIncognito -> @@ -687,8 +690,7 @@ private suspend fun connect(rhId: Long?, link: String, close: () -> Unit, cleanu rhId, link, close = close, - cleanup = cleanup, - incognito = null + cleanup = cleanup ).await() private fun createInvitation( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt index bdb8e385e1..2ec9ebabb7 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/usersettings/UserAddressView.kt @@ -65,7 +65,7 @@ fun UserAddressView( progressIndicator = true val connReqContact = chatModel.controller.apiCreateUserAddress(user.value?.remoteHostId) if (connReqContact != null) { - chatModel.userAddress.value = UserContactLinkRec(connReqContact) + chatModel.userAddress.value = UserContactLinkRec(connReqContact, shortLinkDataSet = connReqContact.connShortLink != null) AlertManager.shared.showAlertDialog( title = generalGetString(MR.strings.share_address_with_contacts_question), @@ -92,6 +92,17 @@ fun UserAddressView( } } + fun showAddShortLinkAlert() { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.share_profile_via_link), + text = generalGetString(MR.strings.share_profile_via_link_alert_text), + confirmText = generalGetString(MR.strings.share_profile_via_link_alert_confirm), + onConfirm = { + addShortLink() + } + ) + } + LaunchedEffect(autoCreateAddress) { if (chatModel.userAddress.value == null && autoCreateAddress) { createAddress() @@ -106,7 +117,7 @@ fun UserAddressView( userAddress = userAddress.value, shareViaProfile, createAddress = { createAddress() }, - addShortLink = { addShortLink() }, + showAddShortLinkAlert = { showAddShortLinkAlert() }, learnMore = { ModalManager.start.showModal { UserAddressLearnMore() @@ -181,7 +192,7 @@ private fun UserAddressLayout( userAddress: UserContactLinkRec?, shareViaProfile: MutableState, createAddress: () -> Unit, - addShortLink: () -> Unit, + showAddShortLinkAlert: () -> Unit, learnMore: () -> Unit, share: (String) -> Unit, sendEmail: (UserContactLinkRec) -> Unit, @@ -225,7 +236,9 @@ private fun UserAddressLayout( BusinessAddressToggle(autoAcceptState) { saveAas(autoAcceptState.value, autoAcceptStateSaved) } AddressSettingsButton(user, userAddress, shareViaProfile, setProfileAddress, saveAas) if (userAddress.connLinkContact.connShortLink == null) { - AddShortLinkButton(addShortLink) + AddShortLinkButton(text = stringResource(MR.strings.add_short_link), showAddShortLinkAlert) + } else if (!userAddress.shortLinkDataSet) { + AddShortLinkButton(text = stringResource(MR.strings.share_profile_via_link), showAddShortLinkAlert) } if (autoAcceptState.value.business) { @@ -265,10 +278,10 @@ private fun CreateAddressButton(onClick: () -> Unit) { } @Composable -private fun AddShortLinkButton(onClick: () -> Unit) { +private fun AddShortLinkButton(text: String, onClick: () -> Unit) { SettingsActionItem( painterResource(MR.images.ic_add), - stringResource(MR.strings.add_short_link), + text, onClick, iconColor = MaterialTheme.colors.primary, textColor = MaterialTheme.colors.primary, @@ -536,7 +549,7 @@ private fun AutoAcceptSection( saveAas: (AutoAcceptState, MutableState) -> Unit ) { SectionView(stringResource(MR.strings.auto_accept_contact).uppercase()) { - if (!autoAcceptState.value.business) { + if (!chatModel.addressShortLinkDataSet && !autoAcceptState.value.business) { AcceptIncognitoToggle(autoAcceptState) } WelcomeMessageEditor(autoAcceptState) @@ -586,7 +599,7 @@ fun PreviewUserAddressLayoutNoAddress() { user = User.sampleData, userAddress = null, createAddress = {}, - addShortLink = {}, + showAddShortLinkAlert = {}, share = { _ -> }, deleteAddress = {}, saveAas = { _, _ -> }, @@ -618,9 +631,9 @@ fun PreviewUserAddressLayoutAddressCreated() { SimpleXTheme { UserAddressLayout( user = User.sampleData, - userAddress = UserContactLinkRec(CreatedConnLink("https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", null)), + userAddress = UserContactLinkRec(CreatedConnLink("https://simplex.chat/contact#/?v=1&smp=smp%3A%2F%2FPQUV2eL0t7OStZOoAsPEV2QYWt4-xilbakvGUGOItUo%3D%40smp6.simplex.im%2FK1rslx-m5bpXVIdMZg9NLUZ_8JBm8xTt%23MCowBQYDK2VuAyEALDeVe-sG8mRY22LsXlPgiwTNs9dbiLrNuA7f3ZMAJ2w%3D", null), shortLinkDataSet = false), createAddress = {}, - addShortLink = {}, + showAddShortLinkAlert = {}, share = { _ -> }, deleteAddress = {}, saveAas = { _, _ -> }, 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 501a4050d5..05bae2f200 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -13,6 +13,7 @@ You will connect to all group members. Connect Connect incognito + Open Invalid link Please check that SimpleX link is correct. @@ -63,6 +64,7 @@ Decryption error Encryption re-negotiation error + end-to-end encryption.]]> end-to-end encryption with perfect forward secrecy, repudiation and break-in recovery.]]> quantum resistant e2e encryption with perfect forward secrecy, repudiation and break-in recovery.]]> This chat is protected by end-to-end encryption. @@ -180,6 +182,7 @@ Undelivered messages The connection reached the limit of undelivered messages, your contact may be offline. Error accepting contact request + Error rejecting contact request Sender may have deleted the connection request. Error deleting contact Error deleting group @@ -212,6 +215,10 @@ Error updating chat list Error creating chat list Error loading chat lists + Error preparing contact + Error preparing group + Error changing contact user + Error changing group user Instant notifications @@ -421,6 +428,7 @@ Settings connecting… send to connect + open to join you are invited to group join as %s rejected @@ -435,6 +443,7 @@ No chats No chats found Tap to Connect + hold or open to connect Connect with %1$s? Search or paste SimpleX link Tap Create SimpleX address in the menu to create it later. @@ -693,6 +702,9 @@ Accept Accept incognito Reject + Accept contact request + Reject contact request + The sender will NOT be notified. Clear chat? @@ -1058,6 +1070,10 @@ Business address Add your team members to the conversations. Add short link + Share profile via link + Profile will be shared via the address short link. This change to the address cannot be reversed, other than fully deleting it. Do you wish to update the address? + Update (and share profile) + Share group profile via link Continue