core, ui: prohibit changing profile for prepared entity when first attempt to connect failed (#6037)

* core: prohibit changing profile for prepared entity when first attempt to connect failed

* reuse incognito

* schema

* ios

* postgres schema

* ios

* reenable tests

* kotlin

* update alert

* rename predicate, combine queries

* send the correct incognito mode, fail on attempt to change mode for prepared connection

* query plans

* ui: show group connecting status

---------

Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
spaced4ndy
2025-07-05 10:09:10 +00:00
committed by GitHub
parent e5d3029edf
commit 2dd54c6697
48 changed files with 425 additions and 133 deletions
@@ -1288,6 +1288,8 @@ interface SomeChat {
val ready: Boolean
val chatDeleted: Boolean
val nextConnect: Boolean
val nextConnectPrepared: Boolean
val profileChangeProhibited: Boolean
val incognito: Boolean
fun featureEnabled(feature: ChatFeature): Boolean
val timedMessagesTTL: Int?
@@ -1368,6 +1370,8 @@ sealed class ChatInfo: SomeChat, NamedChat {
override val ready get() = contact.ready
override val chatDeleted get() = contact.chatDeleted
override val nextConnect get() = contact.nextConnect
override val nextConnectPrepared get() = contact.nextConnectPrepared
override val profileChangeProhibited get() = contact.profileChangeProhibited
override val incognito get() = contact.incognito
override fun featureEnabled(feature: ChatFeature) = contact.featureEnabled(feature)
override val timedMessagesTTL: Int? get() = contact.timedMessagesTTL
@@ -1393,6 +1397,8 @@ sealed class ChatInfo: SomeChat, NamedChat {
override val ready get() = groupInfo.ready
override val chatDeleted get() = groupInfo.chatDeleted
override val nextConnect get() = groupInfo.nextConnect
override val nextConnectPrepared get() = groupInfo.nextConnectPrepared
override val profileChangeProhibited get() = groupInfo.profileChangeProhibited
override val incognito get() = groupInfo.incognito
override fun featureEnabled(feature: ChatFeature) = groupInfo.featureEnabled(feature)
override val timedMessagesTTL: Int? get() = groupInfo.timedMessagesTTL
@@ -1417,6 +1423,8 @@ sealed class ChatInfo: SomeChat, NamedChat {
override val ready get() = noteFolder.ready
override val chatDeleted get() = noteFolder.chatDeleted
override val nextConnect get() = noteFolder.nextConnect
override val nextConnectPrepared get() = noteFolder.nextConnectPrepared
override val profileChangeProhibited get() = noteFolder.profileChangeProhibited
override val incognito get() = noteFolder.incognito
override fun featureEnabled(feature: ChatFeature) = noteFolder.featureEnabled(feature)
override val timedMessagesTTL: Int? get() = noteFolder.timedMessagesTTL
@@ -1441,6 +1449,8 @@ sealed class ChatInfo: SomeChat, NamedChat {
override val ready get() = contactRequest.ready
override val chatDeleted get() = contactRequest.chatDeleted
override val nextConnect get() = contactRequest.nextConnect
override val nextConnectPrepared get() = contactRequest.nextConnectPrepared
override val profileChangeProhibited get() = contactRequest.profileChangeProhibited
override val incognito get() = contactRequest.incognito
override fun featureEnabled(feature: ChatFeature) = contactRequest.featureEnabled(feature)
override val timedMessagesTTL: Int? get() = contactRequest.timedMessagesTTL
@@ -1465,6 +1475,8 @@ sealed class ChatInfo: SomeChat, NamedChat {
override val ready get() = contactConnection.ready
override val chatDeleted get() = contactConnection.chatDeleted
override val nextConnect get() = contactConnection.nextConnect
override val nextConnectPrepared get() = contactConnection.nextConnectPrepared
override val profileChangeProhibited get() = contactConnection.profileChangeProhibited
override val incognito get() = contactConnection.incognito
override fun featureEnabled(feature: ChatFeature) = contactConnection.featureEnabled(feature)
override val timedMessagesTTL: Int? get() = contactConnection.timedMessagesTTL
@@ -1494,6 +1506,8 @@ sealed class ChatInfo: SomeChat, NamedChat {
override val ready get() = false
override val chatDeleted get() = false
override val nextConnect get() = false
override val nextConnectPrepared get() = false
override val profileChangeProhibited get() = false
override val incognito get() = false
override fun featureEnabled(feature: ChatFeature) = false
override val timedMessagesTTL: Int? get() = null
@@ -1688,7 +1702,8 @@ data class Contact(
val active get() = contactStatus == ContactStatus.Active
override val nextConnect get() = sendMsgToConnect
val nextSendGrpInv get() = contactGroupMemberId != null && !contactGrpInvSent
val nextConnectPrepared get() = preparedContact != null && (activeConn == null || activeConn.connStatus == ConnStatus.Prepared)
override val nextConnectPrepared get() = preparedContact != null && (activeConn == null || activeConn.connStatus == ConnStatus.Prepared)
override val profileChangeProhibited get() = activeConn != null
val nextAcceptContactRequest get() = contactRequestId != null && (activeConn == null || activeConn.connStatus == ConnStatus.New)
val sendMsgToConnect get() = nextSendGrpInv || nextConnectPrepared
override val incognito get() = contactConnIncognito
@@ -1942,7 +1957,8 @@ data class GroupInfo (
override val apiId get() = groupId
override val ready get() = membership.memberActive
override val nextConnect get() = nextConnectPrepared
val nextConnectPrepared = if (preparedGroup != null) !preparedGroup.connLinkStartedConnection else false
override val nextConnectPrepared = if (preparedGroup != null) !preparedGroup.connLinkStartedConnection else false
override val profileChangeProhibited get() = preparedGroup?.connLinkPreparedConnection ?: false
override val chatDeleted get() = false
override val incognito get() = membership.memberIncognito
override fun featureEnabled(feature: ChatFeature) = when (feature) {
@@ -2015,6 +2031,7 @@ data class GroupInfo (
@Serializable
data class PreparedGroup (
val connLinkToConnect: CreatedConnLink,
val connLinkPreparedConnection: Boolean,
val connLinkStartedConnection: Boolean
)
@@ -2396,6 +2413,8 @@ class NoteFolder(
override val chatDeleted get() = false
override val ready get() = true
override val nextConnect get() = false
override val nextConnectPrepared get() = false
override val profileChangeProhibited get() = false
override val incognito get() = false
override fun featureEnabled(feature: ChatFeature) = feature == ChatFeature.Voice
override val timedMessagesTTL: Int? get() = null
@@ -2432,6 +2451,8 @@ class UserContactRequest (
override val chatDeleted get() = false
override val ready get() = true
override val nextConnect get() = false
override val nextConnectPrepared get() = false
override val profileChangeProhibited get() = false
override val incognito get() = false
override fun featureEnabled(feature: ChatFeature) = false
override val timedMessagesTTL: Int? get() = null
@@ -2473,6 +2494,8 @@ class PendingContactConnection(
override val chatDeleted get() = false
override val ready get() = false
override val nextConnect get() = false
override val nextConnectPrepared get() = false
override val profileChangeProhibited get() = false
override val incognito get() = customUserProfileId != null
override fun featureEnabled(feature: ChatFeature) = false
override val timedMessagesTTL: Int? get() = null
@@ -2510,6 +2510,12 @@ object ChatController {
chatModel.networkStatuses[s.agentConnId] = s.networkStatus
}
}
is CR.ChatInfoUpdated ->
if (active(r.user)) {
withContext(Dispatchers.Main) {
chatModel.chatsContext.updateChatInfo(rhId, r.chatInfo)
}
}
is CR.NewChatItems -> withBGApi {
r.chatItems.forEach { chatItem ->
val cInfo = chatItem.chatInfo
@@ -5959,6 +5965,7 @@ sealed class CR {
// TODO remove above
@Serializable @SerialName("networkStatus") class NetworkStatusResp(val networkStatus: NetworkStatus, val connections: List<String>): CR()
@Serializable @SerialName("networkStatuses") class NetworkStatuses(val user_: UserRef?, val networkStatuses: List<ConnNetworkStatus>): CR()
@Serializable @SerialName("chatInfoUpdated") class ChatInfoUpdated(val user: UserRef, val chatInfo: ChatInfo): CR()
@Serializable @SerialName("newChatItems") class NewChatItems(val user: UserRef, val chatItems: List<AChatItem>): CR()
@Serializable @SerialName("chatItemsStatusesUpdated") class ChatItemsStatusesUpdated(val user: UserRef, val chatItems: List<AChatItem>): CR()
@Serializable @SerialName("chatItemUpdated") class ChatItemUpdated(val user: UserRef, val chatItem: AChatItem): CR()
@@ -6144,6 +6151,7 @@ sealed class CR {
is ContactSubSummary -> "contactSubSummary"
is NetworkStatusResp -> "networkStatus"
is NetworkStatuses -> "networkStatuses"
is ChatInfoUpdated -> "chatInfoUpdated"
is NewChatItems -> "newChatItems"
is ChatItemsStatusesUpdated -> "chatItemsStatusesUpdated"
is ChatItemUpdated -> "chatItemUpdated"
@@ -6321,6 +6329,7 @@ sealed class CR {
is ContactSubSummary -> withUser(user, json.encodeToString(contactSubscriptions))
is NetworkStatusResp -> "networkStatus $networkStatus\nconnections: $connections"
is NetworkStatuses -> withUser(user_, json.encodeToString(networkStatuses))
is ChatInfoUpdated -> withUser(user, json.encodeToString(chatInfo))
is NewChatItems -> withUser(user, chatItems.joinToString("\n") { json.encodeToString(it) })
is ChatItemsStatusesUpdated -> withUser(user, chatItems.joinToString("\n") { json.encodeToString(it) })
is ChatItemUpdated -> withUser(user, json.encodeToString(chatItem))
@@ -729,7 +729,8 @@ private fun connectingText(chatInfo: ChatInfo): String? {
is ChatInfo.Group ->
when (chatInfo.groupInfo.membership.memberStatus) {
GroupMemberStatus.MemAccepted -> generalGetString(MR.strings.group_connection_pending) // TODO [short links] add member status to show transition from prepared group to started connection earlier?
GroupMemberStatus.MemUnknown -> if (chatInfo.groupInfo.preparedGroup?.connLinkStartedConnection == true) generalGetString(MR.strings.group_connection_pending) else null
GroupMemberStatus.MemAccepted -> generalGetString(MR.strings.group_connection_pending)
else -> null
}
@@ -65,7 +65,7 @@ fun ComposeContextProfilePickerView(
Modifier.size(20.dp),
tint = MaterialTheme.colors.secondary,
)
} else {
} else if (!chat.chatInfo.profileChangeProhibited) {
Icon(
painterResource(
MR.images.ic_chevron_up
@@ -103,14 +103,21 @@ fun ComposeContextProfilePickerView(
keepingChatId = chat.id
)
if (chatModel.currentUser.value?.userId != newUser.userId) {
AlertManager.shared.showAlertMsg(generalGetString(
MR.strings.switching_profile_error_title),
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.switching_profile_error_title),
String.format(generalGetString(MR.strings.switching_profile_error_message), newUser.chatViewName)
)
}
}
}
fun showCantChangeProfileAlert() {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.context_user_picker_cant_change_profile_alert_title),
generalGetString(MR.strings.context_user_picker_cant_change_profile_alert_message)
)
}
@Composable
fun ProfilePickerUserOption(user: User) {
Row(
@@ -118,15 +125,19 @@ fun ComposeContextProfilePickerView(
.fillMaxWidth()
.sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT + 8.dp)
.clickable(onClick = {
if (selectedUser.value.userId == user.userId) {
if (!incognitoDefault) {
listExpanded.value = !listExpanded.value
if (!chat.chatInfo.profileChangeProhibited) {
if (selectedUser.value.userId == user.userId) {
if (!incognitoDefault) {
listExpanded.value = !listExpanded.value
} else {
chatModel.controller.appPrefs.incognito.set(false)
listExpanded.value = false
}
} else {
chatModel.controller.appPrefs.incognito.set(false)
listExpanded.value = false
changeProfile(user)
}
} else {
changeProfile(user)
showCantChangeProfileAlert()
}
})
.padding(horizontal = DEFAULT_PADDING_HALF, vertical = 4.dp),
@@ -156,11 +167,15 @@ fun ComposeContextProfilePickerView(
.fillMaxWidth()
.sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT + 8.dp)
.clickable(onClick = {
if (incognitoDefault) {
listExpanded.value = !listExpanded.value
if (!chat.chatInfo.profileChangeProhibited) {
if (incognitoDefault) {
listExpanded.value = !listExpanded.value
} else {
chatModel.controller.appPrefs.incognito.set(true)
listExpanded.value = false
}
} else {
chatModel.controller.appPrefs.incognito.set(true)
listExpanded.value = false
showCantChangeProfileAlert()
}
})
.padding(horizontal = DEFAULT_PADDING_HALF, vertical = 4.dp),
@@ -265,7 +280,13 @@ fun ComposeContextProfilePickerView(
color = MaterialTheme.colors.secondary
)
if (incognitoDefault) {
if (chat.chatInfo.profileChangeProhibited) {
if (chat.chatInfo.incognito) {
IncognitoOption()
} else {
ProfilePickerUserOption(selectedUser.value)
}
} else if (incognitoDefault) {
IncognitoOption()
} else {
ProfilePickerUserOption(selectedUser.value)
@@ -273,9 +294,9 @@ fun ComposeContextProfilePickerView(
}
}
if (listExpanded.value) {
ProfilePicker()
} else {
if (!listExpanded.value || chat.chatInfo.profileChangeProhibited) {
CurrentSelection()
} else {
ProfilePicker()
}
}
@@ -532,10 +532,11 @@ fun ComposeView(
suspend fun sendConnectPreparedContact() {
val mc = checkLinkPreview()
sending()
val incognito = if (chat.chatInfo.profileChangeProhibited) chat.chatInfo.incognito else chatModel.controller.appPrefs.incognito.get()
val contact = chatModel.controller.apiConnectPreparedContact(
rh = chat.remoteHostId,
contactId = chat.chatInfo.apiId,
incognito = chatModel.controller.appPrefs.incognito.get(),
incognito = incognito,
msg = mc
)
if (contact != null) {
@@ -573,10 +574,11 @@ fun ComposeView(
suspend fun connectPreparedGroup() {
val mc = checkLinkPreview()
sending()
val incognito = if (chat.chatInfo.profileChangeProhibited) chat.chatInfo.incognito else chatModel.controller.appPrefs.incognito.get()
val groupInfo = chatModel.controller.apiConnectPreparedGroup(
rh = chat.remoteHostId,
groupId = chat.chatInfo.apiId,
incognito = chatModel.controller.appPrefs.incognito.get(),
incognito = incognito,
msg = mc
)
if (groupInfo != null) {
@@ -1328,12 +1330,7 @@ fun ComposeView(
Column {
val currentUser = chatModel.currentUser.value
if ((
(chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.nextConnectPrepared)
|| (chat.chatInfo is ChatInfo.Group && chat.chatInfo.groupInfo.nextConnectPrepared)
)
&& currentUser != null
) {
if (chat.chatInfo.nextConnectPrepared && currentUser != null) {
ComposeContextProfilePickerView(
rhId = rhId,
chat = chat,
@@ -870,6 +870,8 @@
<!-- ComposeContextProfilePickerView.kt -->
<string name="context_user_picker_your_profile">Your profile</string>
<string name="context_user_picker_cant_change_profile_alert_title">Can\'t change profile</string>
<string name="context_user_picker_cant_change_profile_alert_message">To use another profile after connection attempt, delete the chat and use the link again.</string>
<!-- ScanCodeView.kt -->
<string name="scan_code">Scan code</string>