android: connect with contact via address (for preset simplex contact) (#3330)

This commit is contained in:
spaced4ndy 2023-11-10 10:16:28 +04:00 committed by GitHub
parent f49ded5ae5
commit c0be36737d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 165 additions and 48 deletions

View file

@ -147,12 +147,13 @@ object ChatModel {
val currentCInfo = chats[i].chatInfo
var newCInfo = cInfo
if (currentCInfo is ChatInfo.Direct && newCInfo is ChatInfo.Direct) {
val currentStats = currentCInfo.contact.activeConn.connectionStats
val newStats = newCInfo.contact.activeConn.connectionStats
if (currentStats != null && newStats == null) {
val currentStats = currentCInfo.contact.activeConn?.connectionStats
val newConn = newCInfo.contact.activeConn
val newStats = newConn?.connectionStats
if (currentStats != null && newConn != null && newStats == null) {
newCInfo = newCInfo.copy(
contact = newCInfo.contact.copy(
activeConn = newCInfo.contact.activeConn.copy(
activeConn = newConn.copy(
connectionStats = currentStats
)
)
@ -168,7 +169,7 @@ object ChatModel {
fun updateContact(contact: Contact) = updateChat(ChatInfo.Direct(contact), addMissing = contact.directOrUsed)
fun updateContactConnectionStats(contact: Contact, connectionStats: ConnectionStats) {
val updatedConn = contact.activeConn.copy(connectionStats = connectionStats)
val updatedConn = contact.activeConn?.copy(connectionStats = connectionStats)
val updatedContact = contact.copy(activeConn = updatedConn)
updateContact(updatedContact)
}
@ -570,11 +571,19 @@ object ChatModel {
}
fun setContactNetworkStatus(contact: Contact, status: NetworkStatus) {
networkStatuses[contact.activeConn.agentConnId] = status
val conn = contact.activeConn
if (conn != null) {
networkStatuses[conn.agentConnId] = status
}
}
fun contactNetworkStatus(contact: Contact): NetworkStatus =
networkStatuses[contact.activeConn.agentConnId] ?: NetworkStatus.Unknown()
fun contactNetworkStatus(contact: Contact): NetworkStatus {
val conn = contact.activeConn
return if (conn != null)
networkStatuses[conn.agentConnId] ?: NetworkStatus.Unknown()
else
NetworkStatus.Unknown()
}
fun addTerminalItem(item: TerminalItem) {
if (terminalItems.size >= 500) {
@ -891,7 +900,7 @@ data class Contact(
val contactId: Long,
override val localDisplayName: String,
val profile: LocalProfile,
val activeConn: Connection,
val activeConn: Connection? = null,
val viaGroup: Long? = null,
val contactUsed: Boolean,
val contactStatus: ContactStatus,
@ -906,10 +915,10 @@ data class Contact(
override val chatType get() = ChatType.Direct
override val id get() = "@$contactId"
override val apiId get() = contactId
override val ready get() = activeConn.connStatus == ConnStatus.Ready
override val ready get() = activeConn?.connStatus == ConnStatus.Ready
val active get() = contactStatus == ContactStatus.Active
override val sendMsgEnabled get() =
(ready && active && !(activeConn.connectionStats?.ratchetSyncSendProhibited ?: false))
(ready && active && !(activeConn?.connectionStats?.ratchetSyncSendProhibited ?: false))
|| nextSendGrpInv
val nextSendGrpInv get() = contactGroupMemberId != null && !contactGrpInvSent
override val ntfsEnabled get() = chatSettings.enableNtfs == MsgFilter.All
@ -927,13 +936,17 @@ data class Contact(
override val image get() = profile.image
val contactLink: String? = profile.contactLink
override val localAlias get() = profile.localAlias
val verified get() = activeConn.connectionCode != null
val verified get() = activeConn?.connectionCode != null
val directOrUsed: Boolean get() =
(activeConn.connLevel == 0 && !activeConn.viaGroupLink) || contactUsed
if (activeConn != null) {
(activeConn.connLevel == 0 && !activeConn.viaGroupLink) || contactUsed
} else {
true
}
val contactConnIncognito =
activeConn.customUserProfileId != null
activeConn?.customUserProfileId != null
fun allowsFeature(feature: ChatFeature): Boolean = when (feature) {
ChatFeature.TimedMessages -> mergedPreferences.timedMessages.contactPreference.allow != FeatureAllowed.NO

View file

@ -907,6 +907,23 @@ object ChatController {
}
}
suspend fun apiConnectContactViaAddress(incognito: Boolean, contactId: Long): Contact? {
val userId = chatModel.currentUser.value?.userId ?: run {
Log.e(TAG, "apiConnectContactViaAddress: no current user")
return null
}
val r = sendCmd(CC.ApiConnectContactViaAddress(userId, incognito, contactId))
when {
r is CR.SentInvitationToContact -> return r.contact
else -> {
if (!(networkErrorAlert(r))) {
apiErrorAlert("apiConnectContactViaAddress", generalGetString(MR.strings.connection_error), r)
}
return null
}
}
}
suspend fun apiDeleteChat(type: ChatType, id: Long, notify: Boolean? = null): Boolean {
val r = sendCmd(CC.ApiDeleteChat(type, id, notify))
when {
@ -1413,8 +1430,11 @@ object ChatController {
is CR.ContactConnected -> {
if (active(r.user) && r.contact.directOrUsed) {
chatModel.updateContact(r.contact)
chatModel.dismissConnReqView(r.contact.activeConn.id)
chatModel.removeChat(r.contact.activeConn.id)
val conn = r.contact.activeConn
if (conn != null) {
chatModel.dismissConnReqView(conn.id)
chatModel.removeChat(conn.id)
}
}
if (r.contact.directOrUsed) {
ntfManager.notifyContactConnected(r.user, r.contact)
@ -1424,8 +1444,11 @@ object ChatController {
is CR.ContactConnecting -> {
if (active(r.user) && r.contact.directOrUsed) {
chatModel.updateContact(r.contact)
chatModel.dismissConnReqView(r.contact.activeConn.id)
chatModel.removeChat(r.contact.activeConn.id)
val conn = r.contact.activeConn
if (conn != null) {
chatModel.dismissConnReqView(conn.id)
chatModel.removeChat(conn.id)
}
}
}
is CR.ReceivedContactRequest -> {
@ -1556,9 +1579,10 @@ object ChatController {
if (!active(r.user)) return
chatModel.updateGroup(r.groupInfo)
if (r.hostContact != null) {
chatModel.dismissConnReqView(r.hostContact.activeConn.id)
chatModel.removeChat(r.hostContact.activeConn.id)
val conn = r.hostContact?.activeConn
if (conn != null) {
chatModel.dismissConnReqView(conn.id)
chatModel.removeChat(conn.id)
}
}
is CR.GroupLinkConnecting -> {
@ -1946,6 +1970,7 @@ sealed class CC {
class ApiSetConnectionIncognito(val connId: Long, val incognito: Boolean): CC()
class APIConnectPlan(val userId: Long, val connReq: String): CC()
class APIConnect(val userId: Long, val incognito: Boolean, val connReq: String): CC()
class ApiConnectContactViaAddress(val userId: Long, val incognito: Boolean, val contactId: Long): CC()
class ApiDeleteChat(val type: ChatType, val id: Long, val notify: Boolean?): CC()
class ApiClearChat(val type: ChatType, val id: Long): CC()
class ApiListContacts(val userId: Long): CC()
@ -2057,6 +2082,7 @@ sealed class CC {
is ApiSetConnectionIncognito -> "/_set incognito :$connId ${onOff(incognito)}"
is APIConnectPlan -> "/_connect plan $userId $connReq"
is APIConnect -> "/_connect $userId incognito=${onOff(incognito)} $connReq"
is ApiConnectContactViaAddress -> "/_connect contact $userId incognito=${onOff(incognito)} $contactId"
is ApiDeleteChat -> if (notify != null) {
"/_delete ${chatRef(type, id)} notify=${onOff(notify)}"
} else {
@ -2164,6 +2190,7 @@ sealed class CC {
is ApiSetConnectionIncognito -> "apiSetConnectionIncognito"
is APIConnectPlan -> "apiConnectPlan"
is APIConnect -> "apiConnect"
is ApiConnectContactViaAddress -> "apiConnectContactViaAddress"
is ApiDeleteChat -> "apiDeleteChat"
is ApiClearChat -> "apiClearChat"
is ApiListContacts -> "apiListContacts"
@ -3379,6 +3406,7 @@ sealed class CR {
@Serializable @SerialName("connectionPlan") class CRConnectionPlan(val user: UserRef, val connectionPlan: ConnectionPlan): CR()
@Serializable @SerialName("sentConfirmation") class SentConfirmation(val user: UserRef): CR()
@Serializable @SerialName("sentInvitation") class SentInvitation(val user: UserRef): 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("contactRequestAlreadyAccepted") class ContactRequestAlreadyAccepted(val user: UserRef, val contact: Contact): CR()
@Serializable @SerialName("contactDeleted") class ContactDeleted(val user: UserRef, val contact: Contact): CR()
@ -3517,6 +3545,7 @@ sealed class CR {
is CRConnectionPlan -> "connectionPlan"
is SentConfirmation -> "sentConfirmation"
is SentInvitation -> "sentInvitation"
is SentInvitationToContact -> "sentInvitationToContact"
is ContactAlreadyExists -> "contactAlreadyExists"
is ContactRequestAlreadyAccepted -> "contactRequestAlreadyAccepted"
is ContactDeleted -> "contactDeleted"
@ -3650,6 +3679,7 @@ sealed class CR {
is CRConnectionPlan -> withUser(user, json.encodeToString(connectionPlan))
is SentConfirmation -> withUser(user, noDetails())
is SentInvitation -> withUser(user, noDetails())
is SentInvitationToContact -> withUser(user, json.encodeToString(contact))
is ContactAlreadyExists -> withUser(user, json.encodeToString(contact))
is ContactRequestAlreadyAccepted -> withUser(user, json.encodeToString(contact))
is ContactDeleted -> withUser(user, json.encodeToString(contact))
@ -3785,6 +3815,7 @@ sealed class ContactAddressPlan {
@Serializable @SerialName("connectingConfirmReconnect") object ConnectingConfirmReconnect: ContactAddressPlan()
@Serializable @SerialName("connectingProhibit") class ConnectingProhibit(val contact: Contact): ContactAddressPlan()
@Serializable @SerialName("known") class Known(val contact: Contact): ContactAddressPlan()
@Serializable @SerialName("contactViaAddress") class ContactViaAddress(val contact: Contact): ContactAddressPlan()
}
@Serializable

View file

@ -150,7 +150,7 @@ fun ChatInfoView(
val (verified, existingCode) = r
chatModel.updateContact(
ct.copy(
activeConn = ct.activeConn.copy(
activeConn = ct.activeConn?.copy(
connectionCode = if (verified) SecurityCode(existingCode, Clock.System.now()) else null
)
)

View file

@ -57,7 +57,7 @@ fun CIRcvDecryptionError(
if (cInfo is ChatInfo.Direct) {
val modelCInfo = findModelChat(cInfo.id)?.chatInfo
if (modelCInfo is ChatInfo.Direct) {
val modelContactStats = modelCInfo.contact.activeConn.connectionStats
val modelContactStats = modelCInfo.contact.activeConn?.connectionStats
if (modelContactStats != null) {
if (modelContactStats.ratchetSyncAllowed) {
DecryptionErrorItemFixButton(

View file

@ -30,6 +30,7 @@ import chat.simplex.common.views.newchat.*
import chat.simplex.res.MR
import kotlinx.coroutines.delay
import kotlinx.datetime.Clock
import java.net.URI
@Composable
fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
@ -61,8 +62,8 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
val contactNetworkStatus = chatModel.contactNetworkStatus(chat.chatInfo.contact)
ChatListNavLinkLayout(
chatLinkPreview = { ChatPreviewView(chat, showChatPreviews, chatModel.draft.value, chatModel.draftChatId.value, chatModel.currentUser.value?.profile?.displayName, contactNetworkStatus, stopped, linkMode, inProgress = false, progressByTimeout = false) },
click = { directChatAction(chat.chatInfo, chatModel) },
dropdownMenuItems = { ContactMenuItems(chat, chatModel, showMenu, showMarkRead) },
click = { directChatAction(chat.chatInfo.contact, chatModel) },
dropdownMenuItems = { ContactMenuItems(chat, chat.chatInfo.contact, chatModel, showMenu, showMarkRead) },
showMenu,
stopped,
selectedChat
@ -118,8 +119,11 @@ fun ChatListNavLinkView(chat: Chat, chatModel: ChatModel) {
}
}
fun directChatAction(chatInfo: ChatInfo, chatModel: ChatModel) {
withBGApi { openChat(chatInfo, chatModel) }
fun directChatAction(contact: Contact, chatModel: ChatModel) {
when {
contact.activeConn == null && contact.profile.contactLink != null -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, contact, close = null, openChat = true)
else -> withBGApi { openChat(ChatInfo.Direct(contact), chatModel) }
}
}
fun groupChatAction(groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState<Boolean>? = null) {
@ -192,15 +196,17 @@ suspend fun setGroupMembers(groupInfo: GroupInfo, chatModel: ChatModel) {
}
@Composable
fun ContactMenuItems(chat: Chat, chatModel: ChatModel, showMenu: MutableState<Boolean>, showMarkRead: Boolean) {
if (showMarkRead) {
MarkReadChatAction(chat, chatModel, showMenu)
} else {
MarkUnreadChatAction(chat, chatModel, showMenu)
fun ContactMenuItems(chat: Chat, contact: Contact, chatModel: ChatModel, showMenu: MutableState<Boolean>, showMarkRead: Boolean) {
if (contact.activeConn != null) {
if (showMarkRead) {
MarkReadChatAction(chat, chatModel, showMenu)
} else {
MarkUnreadChatAction(chat, chatModel, showMenu)
}
ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu)
ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu)
ClearChatAction(chat, chatModel, showMenu)
}
ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu)
ToggleNotificationsChatAction(chat, chatModel, chat.chatInfo.ntfsEnabled, showMenu)
ClearChatAction(chat, chatModel, showMenu)
DeleteContactAction(chat, chatModel, showMenu)
}
@ -591,6 +597,63 @@ fun pendingContactAlertDialog(chatInfo: ChatInfo, chatModel: ChatModel) {
)
}
fun askCurrentOrIncognitoProfileConnectContactViaAddress(
chatModel: ChatModel,
contact: Contact,
close: (() -> Unit)?,
openChat: Boolean
) {
AlertManager.shared.showAlertDialogButtonsColumn(
title = String.format(generalGetString(MR.strings.connect_with_contact_name_question), contact.chatViewName),
buttons = {
Column {
SectionItemView({
AlertManager.shared.hideAlert()
withApi {
close?.invoke()
val ok = connectContactViaAddress(chatModel, contact.contactId, incognito = false)
if (ok && openChat) {
openDirectChat(contact.contactId, chatModel)
}
}
}) {
Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
SectionItemView({
AlertManager.shared.hideAlert()
withApi {
close?.invoke()
val ok = connectContactViaAddress(chatModel, contact.contactId, incognito = true)
if (ok && openChat) {
openDirectChat(contact.contactId, chatModel)
}
}
}) {
Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
SectionItemView({
AlertManager.shared.hideAlert()
}) {
Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
}
}
)
}
suspend fun connectContactViaAddress(chatModel: ChatModel, contactId: Long, incognito: Boolean): Boolean {
val contact = chatModel.controller.apiConnectContactViaAddress(incognito, contactId)
if (contact != null) {
chatModel.updateContact(contact)
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.connection_request_sent),
text = generalGetString(MR.strings.you_will_be_connected_when_your_connection_request_is_accepted)
)
return true
}
return false
}
fun acceptGroupInvitationAlertDialog(groupInfo: GroupInfo, chatModel: ChatModel, inProgress: MutableState<Boolean>? = null) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.join_group_question),

View file

@ -146,11 +146,6 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf
@Composable
private fun OnboardingButtons(openNewChatSheet: () -> Unit) {
Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING), horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.Bottom) {
val uriHandler = LocalUriHandler.current
ConnectButton(generalGetString(MR.strings.chat_with_developers)) {
uriHandler.openVerifiedSimplexUri(simplexTeamUri)
}
Spacer(Modifier.height(DEFAULT_PADDING))
ConnectButton(generalGetString(MR.strings.tap_to_start_new_chat), openNewChatSheet)
val color = MaterialTheme.colors.primaryVariant
Canvas(modifier = Modifier.width(40.dp).height(10.dp), onDraw = {

View file

@ -185,10 +185,14 @@ fun ChatPreviewView(
} else {
when (cInfo) {
is ChatInfo.Direct ->
if (cInfo.contact.nextSendGrpInv) {
Text(stringResource(MR.strings.member_contact_send_direct_message), color = MaterialTheme.colors.secondary)
} else if (!cInfo.ready && cInfo.contact.active) {
Text(stringResource(MR.strings.contact_connection_pending), color = MaterialTheme.colors.secondary)
if (cInfo.contact.activeConn == null && cInfo.contact.profile.contactLink != null) {
Text(stringResource(MR.strings.contact_tap_to_connect), color = MaterialTheme.colors.primary)
} else if (!cInfo.ready && 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)
}
}
is ChatInfo.Group ->
when (cInfo.groupInfo.membership.memberStatus) {
@ -215,7 +219,7 @@ fun ChatPreviewView(
@Composable
fun chatStatusImage() {
if (cInfo is ChatInfo.Direct) {
if (cInfo.contact.active) {
if (cInfo.contact.active && cInfo.contact.activeConn != null) {
val descr = contactNetworkStatus?.statusString
when (contactNetworkStatus) {
is NetworkStatus.Connected ->

View file

@ -20,7 +20,7 @@ fun ShareListNavLinkView(chat: Chat, chatModel: ChatModel) {
is ChatInfo.Direct ->
ShareListNavLinkLayout(
chatLinkPreview = { SharePreviewView(chat) },
click = { directChatAction(chat.chatInfo, chatModel) },
click = { directChatAction(chat.chatInfo.contact, chatModel) },
stopped
)
is ChatInfo.Group ->

View file

@ -19,8 +19,7 @@ import androidx.compose.ui.unit.dp
import chat.simplex.common.model.*
import chat.simplex.common.platform.TAG
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.chatlist.openDirectChat
import chat.simplex.common.views.chatlist.openGroupChat
import chat.simplex.common.views.chatlist.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.usersettings.*
import chat.simplex.res.MR
@ -171,6 +170,16 @@ suspend fun planAndConnect(
String.format(generalGetString(MR.strings.you_are_already_connected_to_vName_via_this_link), contact.displayName)
)
}
is ContactAddressPlan.ContactViaAddress -> {
Log.d(TAG, "planAndConnect, .ContactAddress, .ContactViaAddress, incognito=$incognito")
val contact = connectionPlan.contactAddressPlan.contact
if (incognito != null) {
close?.invoke()
connectContactViaAddress(chatModel, contact.contactId, incognito)
} else {
askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, contact, close, openChat = false)
}
}
}
is ConnectionPlan.GroupLink -> when (connectionPlan.groupLinkPlan) {
GroupLinkPlan.Ok -> {

View file

@ -289,6 +289,8 @@
<string name="chat_with_developers">Chat with the developers</string>
<string name="you_have_no_chats">You have no chats</string>
<string name="no_filtered_chats">No filtered chats</string>
<string name="contact_tap_to_connect">Tap to Connect</string>
<string name="connect_with_contact_name_question">Connect with %1$s?</string>
<!-- ChatView.kt -->
<string name="no_selected_chat">No selected chat</string>