android, desktop: short links UX (main functionality) (#5999)

This commit is contained in:
spaced4ndy
2025-06-20 16:18:24 +00:00
committed by GitHub
parent fc04872c91
commit 419e71a01f
25 changed files with 922 additions and 407 deletions
@@ -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) {
@@ -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<Long>,
@@ -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<Long>,
@@ -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<MsgContent> {
override val descriptor: SerialDescriptor = buildSerialDescriptor("MsgContent", PolymorphicKind.SEALED) {
@@ -3992,6 +4045,10 @@ object MsgContentSerializer : KSerializer<MsgContent> {
element<String>("text")
element<ReportReason>("reason")
})
element("MCChat", buildClassSerialDescriptor("MCChat") {
element<String>("text")
element<MsgChatLink>("chatLink")
})
element("MCUnknown", buildClassSerialDescriptor("MCUnknown"))
}
@@ -4026,6 +4083,10 @@ object MsgContentSerializer : KSerializer<MsgContent> {
val reason = Json.decodeFromString<ReportReason>(json["reason"].toString())
MsgContent.MCReport(text, reason)
}
"chat" -> {
val chatLink = Json.decodeFromString<MsgChatLink>(json["chatLink"].toString())
MsgContent.MCChat(text, chatLink)
}
else -> MsgContent.MCUnknown(t, text, json)
}
} else {
@@ -4080,6 +4141,12 @@ object MsgContentSerializer : KSerializer<MsgContent> {
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
@@ -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<RcvCallInvitation> {
@@ -1988,18 +2045,18 @@ object ChatController {
}
}
suspend fun apiCreateGroupLink(rh: Long?, groupId: Long, memberRole: GroupMemberRole = GroupMemberRole.Member): Pair<CreatedConnLink, GroupMemberRole>? {
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<CreatedConnLink, GroupMemberRole>? {
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<CreatedConnLink, GroupMemberRole>? {
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<CreatedConnLink, GroupMemberRole>? {
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()
@@ -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)
}
@@ -934,7 +934,7 @@ fun CallButton(
}
}
} }
contact.nextSendGrpInv -> { { showCantCallContactSendMessageAlert() } }
contact.sendMsgToConnect -> { { showCantCallContactSendMessageAlert() } }
!contact.active -> { { showCantCallContactDeletedAlert() } }
!contact.ready -> { { showCantCallContactConnectingAlert() } }
needToAllowCallsToContact -> { { showNeedToAllowCallsAlert(onConfirm = { allowCallsToContact(chat) }) } }
@@ -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<ConnectionStats?, Profile?>? = null
var preloadedCode: String? = null
var preloadedLink: Pair<CreatedConnLink, GroupMemberRole>? = 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<CreatedConnLink, GroupMemberRole>? 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)
}
}
}
@@ -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),
)
}
}
@@ -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<ChatItem>? {
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)
@@ -105,7 +105,7 @@ fun getContactsToAdd(chatModel: ChatModel, search: String): List<Contact> {
.filterIsInstance<ChatInfo.Direct>()
.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()
@@ -54,12 +54,11 @@ fun ModalData.GroupChatInfoView(
chatsCtx: ChatModel.ChatsContext,
rhId: Long?,
chatId: String,
groupLink: CreatedConnLink?,
groupLinkMemberRole: GroupMemberRole?,
groupLink: GroupLink?,
selectedItems: MutableState<Set<Long>?>,
appBar: MutableState<@Composable (BoxScope.() -> Unit)?>,
scrollToItemId: MutableState<Long?>,
onGroupLinkUpdated: (Pair<CreatedConnLink, GroupMemberRole>?) -> 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<GroupMember>,
developerTools: Boolean,
onLocalAliasChanged: (String) -> Unit,
groupLink: CreatedConnLink?,
groupLink: GroupLink?,
selectedItems: MutableState<Set<Long>?>,
appBar: MutableState<@Composable (BoxScope.() -> Unit)?>,
scrollToItemId: MutableState<Long?>,
@@ -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)
}
}
}
@@ -28,22 +28,21 @@ fun GroupLinkView(
chatModel: ChatModel,
rhId: Long?,
groupInfo: GroupInfo,
connLinkContact: CreatedConnLink?,
memberRole: GroupMemberRole?,
onGroupLinkUpdated: ((Pair<CreatedConnLink, GroupMemberRole>?) -> 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<GroupMemberRole?>,
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,
@@ -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(
@@ -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)
}
}
@@ -132,7 +132,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>) {
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<Boolean>, 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<Boolean>, onSuccess: ((chat: Chat) -> Unit)? = null) {
fun ContactRequestMenuItems(rhId: Long?, contactRequestId: Long, chatModel: ChatModel, showMenu: MutableState<Boolean>, 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
}
}
}
}
@@ -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,
@@ -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 -> {}
}
@@ -53,13 +53,7 @@ fun ContactListNavLinkView(chat: Chat, nextChatSelected: State<Boolean>, 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<Boolean>, 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<Boolean>, showDel
tryOrShowError("${chat.id}ContactListNavLinkDropdown", error = {}) {
ContactRequestMenuItems(
rhId = chat.remoteHostId,
chatInfo = chat.chatInfo,
contactRequestId = chat.chatInfo.apiId,
chatModel = chatModel,
showMenu = showMenu,
onSuccess = { onRequestAccepted(it) }
@@ -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,
@@ -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<Long?, String>? = 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()
@@ -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)
}
}
}
@@ -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()
}
)
}
@@ -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<String?>(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,
@@ -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(
@@ -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<Boolean>,
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<AutoAcceptState>) -> 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 = { _, _ -> },
@@ -13,6 +13,7 @@
<string name="you_will_join_group">You will connect to all group members.</string>
<string name="connect_via_link_verb">Connect</string>
<string name="connect_via_link_incognito">Connect incognito</string>
<string name="connect_plan_open_chat">Open</string>
<string name="error_parsing_uri_title">Invalid link</string>
<string name="error_parsing_uri_desc">Please check that SimpleX link is correct.</string>
@@ -63,6 +64,7 @@
<string name="decryption_error">Decryption error</string>
<string name="encryption_renegotiation_error">Encryption re-negotiation error</string>
<string name="e2ee_info_e2ee"><![CDATA[Messages are protected by <b>end-to-end encryption</b>.]]></string>
<string name="e2ee_info_no_pq"><![CDATA[Messages, files and calls are protected by <b>end-to-end encryption</b> with perfect forward secrecy, repudiation and break-in recovery.]]></string>
<string name="e2ee_info_pq"><![CDATA[Messages, files and calls are protected by <b>quantum resistant e2e encryption</b> with perfect forward secrecy, repudiation and break-in recovery.]]></string>
<string name="e2ee_info_no_pq_short">This chat is protected by end-to-end encryption.</string>
@@ -180,6 +182,7 @@
<string name="connection_error_quota">Undelivered messages</string>
<string name="connection_error_quota_desc">The connection reached the limit of undelivered messages, your contact may be offline.</string>
<string name="error_accepting_contact_request">Error accepting contact request</string>
<string name="error_rejecting_contact_request">Error rejecting contact request</string>
<string name="sender_may_have_deleted_the_connection_request">Sender may have deleted the connection request.</string>
<string name="error_deleting_contact">Error deleting contact</string>
<string name="error_deleting_group">Error deleting group</string>
@@ -212,6 +215,10 @@
<string name="error_updating_chat_tags">Error updating chat list</string>
<string name="error_creating_chat_tags">Error creating chat list</string>
<string name="error_loading_chat_tags">Error loading chat lists</string>
<string name="error_preparing_contact">Error preparing contact</string>
<string name="error_preparing_group">Error preparing group</string>
<string name="error_changing_contact_user">Error changing contact user</string>
<string name="error_changing_group_user">Error changing group user</string>
<!-- background service notice - SimpleXAPI.kt -->
<string name="icon_descr_instant_notifications">Instant notifications</string>
@@ -421,6 +428,7 @@
<string name="toolbar_settings">Settings</string>
<string name="contact_connection_pending">connecting…</string>
<string name="member_contact_send_direct_message">send to connect</string>
<string name="group_preview_open_to_join">open to join</string>
<string name="group_preview_you_are_invited">you are invited to group</string>
<string name="group_preview_join_as">join as %s</string>
<string name="group_preview_rejected">rejected</string>
@@ -435,6 +443,7 @@
<string name="no_chats">No chats</string>
<string name="no_chats_found">No chats found</string>
<string name="contact_tap_to_connect">Tap to Connect</string>
<string name="hold_or_open_to_connect">hold or open to connect</string>
<string name="connect_with_contact_name_question">Connect with %1$s?</string>
<string name="search_or_paste_simplex_link">Search or paste SimpleX link</string>
<string name="address_creation_instruction">Tap Create SimpleX address in the menu to create it later.</string>
@@ -693,6 +702,9 @@
<string name="accept_contact_button">Accept</string>
<string name="accept_contact_incognito_button">Accept incognito</string>
<string name="reject_contact_button">Reject</string>
<string name="accept_contact_request">Accept contact request</string>
<string name="reject_contact_request">Reject contact request</string>
<string name="the_sender_will_not_be_notified">The sender will NOT be notified.</string>
<!-- Clear Chat - ChatListNavLinkView.kt -->
<string name="clear_chat_question">Clear chat?</string>
@@ -1058,6 +1070,10 @@
<string name="business_address">Business address</string>
<string name="add_your_team_members_to_conversations">Add your team members to the conversations.</string>
<string name="add_short_link">Add short link</string>
<string name="share_profile_via_link">Share profile via link</string>
<string name="share_profile_via_link_alert_text">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?</string>
<string name="share_profile_via_link_alert_confirm">Update (and share profile)</string>
<string name="share_group_profile_via_link">Share group profile via link</string>
<!-- CreateSimpleXAddress.kt -->
<string name="continue_to_next_step">Continue</string>