Merge branch 'master' into master-android

This commit is contained in:
Evgeny Poberezkin
2025-12-11 18:26:19 +00:00
102 changed files with 3539 additions and 1227 deletions
@@ -683,11 +683,6 @@ object ChatModel {
return updatedItem
}
// this should not happen, only another member can "remove" user, user can only "leave" (another event).
if (byMember.groupMemberId == groupInfo.membership.groupMemberId) {
Log.d(TAG, "exiting removeMemberItems")
return
}
val cInfo = ChatInfo.Group(groupInfo, groupChatScope = null) // TODO [knocking] review
if (chatId.value == groupInfo.id) {
for (i in 0 until chatItems.value.size) {
@@ -2295,19 +2290,18 @@ data class GroupMember (
fun canBeRemoved(groupInfo: GroupInfo): Boolean {
val userRole = groupInfo.membership.memberRole
return memberStatus != GroupMemberStatus.MemRemoved && memberStatus != GroupMemberStatus.MemLeft
&& userRole >= GroupMemberRole.Admin && userRole >= memberRole && groupInfo.membership.memberActive
return userRole >= GroupMemberRole.Admin && userRole >= memberRole && groupInfo.membership.memberActive
}
fun canChangeRoleTo(groupInfo: GroupInfo): List<GroupMemberRole>? =
if (!canBeRemoved(groupInfo) || memberPending) null
if (!canBeRemoved(groupInfo) || memberStatus == GroupMemberStatus.MemRemoved || memberStatus == GroupMemberStatus.MemLeft || memberPending) null
else groupInfo.membership.memberRole.let { userRole ->
GroupMemberRole.selectableRoles.filter { it <= userRole }
}
fun canBlockForAll(groupInfo: GroupInfo): Boolean {
val userRole = groupInfo.membership.memberRole
return memberStatus != GroupMemberStatus.MemRemoved && memberStatus != GroupMemberStatus.MemLeft && memberRole < GroupMemberRole.Moderator
return memberRole < GroupMemberRole.Moderator
&& userRole >= GroupMemberRole.Moderator && userRole >= memberRole && groupInfo.membership.memberActive
&& !memberPending
}
@@ -2796,30 +2790,29 @@ data class ChatItem (
}
val mergeCategory: CIMergeCategory?
get() = when (content) {
is CIContent.RcvChatFeature,
is CIContent.SndChatFeature,
is CIContent.RcvGroupFeature,
is CIContent.SndGroupFeature -> CIMergeCategory.ChatFeature
is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) {
is RcvGroupEvent.UserRole,
is RcvGroupEvent.UserDeleted,
is RcvGroupEvent.GroupDeleted,
is RcvGroupEvent.MemberCreatedContact,
is RcvGroupEvent.NewMemberPendingReview ->
null
else -> CIMergeCategory.RcvGroupEvent
}
is CIContent.SndGroupEventContent -> when (content.sndGroupEvent) {
is SndGroupEvent.UserRole, is SndGroupEvent.UserLeft, is SndGroupEvent.MemberAccepted, is SndGroupEvent.UserPendingReview -> null
else -> CIMergeCategory.SndGroupEvent
}
else -> {
if (meta.itemDeleted == null) {
null
} else {
if (chatDir.sent) CIMergeCategory.SndItemDeleted else CIMergeCategory.RcvItemDeleted
get() = if (meta.itemDeleted != null) {
if (chatDir.sent) CIMergeCategory.SndItemDeleted else CIMergeCategory.RcvItemDeleted
} else {
when (content) {
is CIContent.RcvChatFeature,
is CIContent.SndChatFeature,
is CIContent.RcvGroupFeature,
is CIContent.SndGroupFeature -> CIMergeCategory.ChatFeature
is CIContent.RcvGroupEventContent -> when (content.rcvGroupEvent) {
is RcvGroupEvent.UserRole,
is RcvGroupEvent.UserDeleted,
is RcvGroupEvent.GroupDeleted,
is RcvGroupEvent.MemberCreatedContact,
is RcvGroupEvent.NewMemberPendingReview ->
null
else -> CIMergeCategory.RcvGroupEvent
}
is CIContent.SndGroupEventContent -> when (content.sndGroupEvent) {
is SndGroupEvent.UserRole, is SndGroupEvent.UserLeft, is SndGroupEvent.MemberAccepted, is SndGroupEvent.UserPendingReview -> null
else -> CIMergeCategory.SndGroupEvent
}
else ->
null
}
}
@@ -21,7 +21,7 @@ sealed class WriteFileResult {
* */
fun writeCryptoFile(path: String, data: ByteArray): CryptoFileArgs {
val ctrl = ChatController.ctrl ?: throw Exception("Controller is not initialized")
val ctrl = ChatController.getChatCtrl() ?: throw Exception("Controller is not initialized")
val buffer = ByteBuffer.allocateDirect(data.size)
buffer.put(data)
buffer.rewind()
@@ -44,7 +44,7 @@ fun readCryptoFile(path: String, cryptoArgs: CryptoFileArgs): ByteArray {
}
fun encryptCryptoFile(fromPath: String, toPath: String): CryptoFileArgs {
val ctrl = ChatController.ctrl ?: throw Exception("Controller is not initialized")
val ctrl = ChatController.getChatCtrl() ?: throw Exception("Controller is not initialized")
val str = chatEncryptFile(ctrl, fromPath, toPath)
val d = json.decodeFromString(WriteFileResult.serializer(), str)
return when (d) {
@@ -490,17 +490,26 @@ class AppPreferences {
private const val MESSAGE_TIMEOUT: Int = 300_000_000
object ChatController {
var ctrl: ChatCtrl? = -1
private var chatCtrl: ChatCtrl? = -1
val appPrefs: AppPreferences by lazy { AppPreferences() }
val messagesChannel: Channel<API> = Channel()
val chatModel = ChatModel
private var receiverStarted = false
private var receiverJob: Job? = null
var lastMsgReceivedTimestamp: Long = System.currentTimeMillis()
private set
fun hasChatCtrl() = ctrl != -1L && ctrl != null
fun hasChatCtrl() = chatCtrl != -1L && chatCtrl != null
fun getChatCtrl(): ChatCtrl? = chatCtrl
fun setChatCtrl(ctrl: ChatCtrl?) {
val wasRunning = receiverJob != null
stopReceiver()
chatCtrl = ctrl
if (wasRunning && ctrl != null) startReceiver()
}
suspend fun getAgentSubsTotal(rh: Long?): Pair<SMPServerSubs, Boolean>? {
val userId = currentUserId("getAgentSubsTotal")
@@ -647,17 +656,16 @@ object ChatController {
private fun startReceiver() {
Log.d(TAG, "ChatController startReceiver")
if (receiverStarted) return
receiverStarted = true
CoroutineScope(Dispatchers.IO).launch {
if (receiverJob != null || chatCtrl == null) return
receiverJob = CoroutineScope(Dispatchers.IO).launch {
var releaseLock: (() -> Unit) = {}
while (true) {
while (isActive) {
/** Global [ctrl] can be null. It's needed for having the same [ChatModel] that already made in [ChatController] without the need
* to change it everywhere in code after changing a database.
* Since it can be changed in background thread, making this check to prevent NullPointerException */
val ctrl = ctrl
val ctrl = chatCtrl
if (ctrl == null) {
receiverStarted = false
stopReceiver()
break
}
try {
@@ -697,6 +705,15 @@ object ChatController {
}
}
private fun stopReceiver() {
Log.d(TAG, "ChatController stopReceiver")
val job = receiverJob
if (job != null) {
receiverJob = null
job.cancel()
}
}
private suspend fun sendCmdWithRetry(rhId: Long?, cmd: CC, inProgress: MutableState<Boolean>? = null, retryNum: Int = 0): API? {
val r = sendCmd(rhId, cmd, retryNum = retryNum)
val alert = if (r is API.Error) retryableNetworkErrorAlert(r.err) else null
@@ -781,7 +798,7 @@ object ChatController {
}
suspend fun sendCmd(rhId: Long?, cmd: CC, otherCtrl: ChatCtrl? = null, retryNum: Int = 0, log: Boolean = true): API {
val ctrl = otherCtrl ?: ctrl ?: throw Exception("Controller is not initialized")
val ctrl = otherCtrl ?: chatCtrl ?: throw Exception("Controller is not initialized")
return withContext(Dispatchers.IO) {
val c = cmd.cmdString
@@ -2118,7 +2135,7 @@ object ChatController {
return null
}
suspend fun apiRemoveMembers(rh: Long?, groupId: Long, memberIds: List<Long>, withMessages: Boolean = false): Pair<GroupInfo, List<GroupMember>>? {
suspend fun apiRemoveMembers(rh: Long?, groupId: Long, memberIds: List<Long>, withMessages: Boolean): Pair<GroupInfo, List<GroupMember>>? {
val r = sendCmd(rh, CC.ApiRemoveMembers(groupId, memberIds, withMessages))
if (r is API.Result && r.res is CR.UserDeletedMembers) return r.res.groupInfo to r.res.members
if (!(networkErrorAlert(r))) {
@@ -56,6 +56,7 @@ fun initChatControllerOnStart() {
}
suspend fun initChatController(useKey: String? = null, confirmMigrations: MigrationConfirmation? = null, startChat: () -> CompletableDeferred<Boolean> = { CompletableDeferred(true) }) {
Log.d(TAG, "initChatController")
try {
if (chatModel.ctrlInitInProgress.value) return
chatModel.ctrlInitInProgress.value = true
@@ -92,7 +93,7 @@ suspend fun initChatController(useKey: String? = null, confirmMigrations: Migrat
val ctrl = if (res is DBMigrationResult.OK) {
migrated[1] as Long
} else null
chatController.ctrl = ctrl
chatController.setChatCtrl(ctrl)
chatModel.chatDbEncrypted.value = dbKey != ""
chatModel.chatDbStatus.value = res
if (res != DBMigrationResult.OK) {
@@ -206,7 +207,7 @@ fun chatInitControllerRemovingDatabases() {
}.getOrElse { DBMigrationResult.Unknown(migrated[0] as String) }
val ctrl = migrated[1] as Long
chatController.ctrl = ctrl
chatController.setChatCtrl(ctrl)
// We need only controller, not databases
File(dbPath + "_chat.db").delete()
File(dbPath + "_agent.db").delete()
@@ -804,7 +804,8 @@ fun SimpleXTheme(darkTheme: Boolean? = null, content: @Composable () -> Unit) {
LocalAppColors provides rememberedAppColors,
LocalAppWallpaper provides rememberedWallpaper,
LocalDensity provides density,
content = content)
content = content
)
}
)
}
@@ -45,7 +45,7 @@ fun ComposeContextPendingMemberActionsView(
.fillMaxHeight()
.weight(1F)
.clickable {
rejectMemberDialog(rhId, member, chatModel, close = { ModalManager.end.closeModal() })
rejectMemberDialog(rhId, groupInfo, member, chatModel, close = { ModalManager.end.closeModal() })
},
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
@@ -70,12 +70,12 @@ fun ComposeContextPendingMemberActionsView(
}
}
fun rejectMemberDialog(rhId: Long?, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) {
fun rejectMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.reject_pending_member_alert_title),
confirmText = generalGetString(MR.strings.reject_pending_member_button),
onConfirm = {
removeMember(rhId, member, chatModel, close)
removeMember(rhId, groupInfo, member, withMessages = false, chatModel, close)
},
destructive = true,
)
@@ -233,15 +233,30 @@ private fun removeMemberAlert(rhId: Long?, groupInfo: GroupInfo, mem: GroupMembe
MR.strings.member_will_be_removed_from_group_cannot_be_undone
else
MR.strings.member_will_be_removed_from_chat_cannot_be_undone
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.button_remove_member_question),
text = generalGetString(messageId),
confirmText = generalGetString(MR.strings.remove_member_confirmation),
onConfirm = {
removeMembers(rhId, groupInfo, listOf(mem.groupMemberId))
},
destructive = true,
)
AlertManager.shared.showAlertDialogButtonsColumn(
generalGetString(MR.strings.button_remove_member_question),
generalGetString(messageId),
buttons = {
Column {
SectionItemView({
AlertManager.shared.hideAlert()
removeMembers(rhId, groupInfo, listOf(mem.groupMemberId), withMessages = false)
}) {
Text(generalGetString(MR.strings.remove_member_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red)
}
SectionItemView({
AlertManager.shared.hideAlert()
removeMembers(rhId, groupInfo, listOf(mem.groupMemberId), withMessages = true)
}) {
Text(generalGetString(MR.strings.remove_member_delete_messages_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red)
}
SectionItemView({
AlertManager.shared.hideAlert()
}) {
Text(generalGetString(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
}
})
}
private fun removeMembersAlert(rhId: Long?, groupInfo: GroupInfo, memberIds: List<Long>, onSuccess: () -> Unit = {}) {
@@ -249,15 +264,30 @@ private fun removeMembersAlert(rhId: Long?, groupInfo: GroupInfo, memberIds: Lis
MR.strings.members_will_be_removed_from_group_cannot_be_undone
else
MR.strings.members_will_be_removed_from_chat_cannot_be_undone
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.button_remove_members_question),
text = generalGetString(messageId),
confirmText = generalGetString(MR.strings.remove_member_confirmation),
onConfirm = {
removeMembers(rhId, groupInfo, memberIds, onSuccess)
},
destructive = true,
)
AlertManager.shared.showAlertDialogButtonsColumn(
generalGetString(MR.strings.button_remove_members_question),
generalGetString(messageId),
buttons = {
Column {
SectionItemView({
AlertManager.shared.hideAlert()
removeMembers(rhId, groupInfo, memberIds, withMessages = false, onSuccess = onSuccess)
}) {
Text(generalGetString(MR.strings.remove_member_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red)
}
SectionItemView({
AlertManager.shared.hideAlert()
removeMembers(rhId, groupInfo, memberIds, withMessages = true, onSuccess = onSuccess)
}) {
Text(generalGetString(MR.strings.remove_member_delete_messages_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red)
}
SectionItemView({
AlertManager.shared.hideAlert()
}) {
Text(generalGetString(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
}
})
}
@Composable
@@ -1052,20 +1082,26 @@ private fun setGroupAlias(chat: Chat, localAlias: String, chatModel: ChatModel)
}
}
fun removeMembers(rhId: Long?, groupInfo: GroupInfo, memberIds: List<Long>, onSuccess: () -> Unit = {}) {
fun removeMembers(rhId: Long?, groupInfo: GroupInfo, memberIds: List<Long>, withMessages: Boolean, onSuccess: () -> Unit = {}) {
withBGApi {
val r = chatModel.controller.apiRemoveMembers(rhId, groupInfo.groupId, memberIds)
val r = chatModel.controller.apiRemoveMembers(rhId, groupInfo.groupId, memberIds, withMessages = withMessages)
if (r != null) {
val (updatedGroupInfo, updatedMembers) = r
withContext(Dispatchers.Main) {
chatModel.chatsContext.updateGroup(rhId, updatedGroupInfo)
updatedMembers.forEach { updatedMember ->
chatModel.chatsContext.upsertGroupMember(rhId, updatedGroupInfo, updatedMember)
if (withMessages) {
chatModel.chatsContext.removeMemberItems(rhId, updatedMember, byMember = groupInfo.membership, groupInfo)
}
}
}
withContext(Dispatchers.Main) {
updatedMembers.forEach { updatedMember ->
chatModel.secondaryChatsContext.value?.upsertGroupMember(rhId, updatedGroupInfo, updatedMember)
if (withMessages) {
chatModel.chatsContext.removeMemberItems(rhId, updatedMember, byMember = groupInfo.membership, groupInfo)
}
}
}
onSuccess()
@@ -136,6 +136,7 @@ fun GroupMemberInfoView(
blockForAll = { blockForAllAlert(rhId, groupInfo, member) },
unblockForAll = { unblockForAllAlert(rhId, groupInfo, member) },
removeMember = { removeMemberDialog(rhId, groupInfo, member, chatModel, close) },
deleteMemberMessages = { deleteMemberMessagesDialog(rhId, groupInfo, member, chatModel, close) },
onRoleSelected = {
if (it == newRole.value) return@GroupMemberInfoLayout
val prevValue = newRole.value
@@ -243,26 +244,56 @@ fun removeMemberDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, c
MR.strings.member_will_be_removed_from_group_cannot_be_undone
else
MR.strings.member_will_be_removed_from_chat_cannot_be_undone
AlertManager.shared.showAlertDialogButtonsColumn(
generalGetString(MR.strings.button_remove_member_question),
generalGetString(messageId),
buttons = {
Column {
SectionItemView({
AlertManager.shared.hideAlert()
removeMember(rhId, groupInfo, member, withMessages = false, chatModel, close)
}) {
Text(generalGetString(MR.strings.remove_member_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red)
}
SectionItemView({
AlertManager.shared.hideAlert()
removeMember(rhId, groupInfo, member, withMessages = true, chatModel, close)
}) {
Text(generalGetString(MR.strings.remove_member_delete_messages_confirmation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = Color.Red)
}
SectionItemView({
AlertManager.shared.hideAlert()
}) {
Text(generalGetString(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary)
}
}
})
}
fun deleteMemberMessagesDialog(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.button_remove_member),
text = generalGetString(messageId),
confirmText = generalGetString(MR.strings.remove_member_confirmation),
title = generalGetString(MR.strings.button_delete_member_messages_question),
text = generalGetString(MR.strings.member_messages_will_be_deleted_cannot_be_undone),
confirmText = generalGetString(MR.strings.delete_member_messages_confirmation),
onConfirm = {
removeMember(rhId, member, chatModel, close)
removeMember(rhId, groupInfo, member, withMessages = true, chatModel, close)
},
destructive = true,
)
}
fun removeMember(rhId: Long?, member: GroupMember, chatModel: ChatModel, close: (() -> Unit)? = null) {
fun removeMember(rhId: Long?, groupInfo: GroupInfo, member: GroupMember, withMessages: Boolean, chatModel: ChatModel, close: (() -> Unit)? = null) {
withBGApi {
val r = chatModel.controller.apiRemoveMembers(rhId, member.groupId, listOf(member.groupMemberId))
val r = chatModel.controller.apiRemoveMembers(rhId, member.groupId, listOf(member.groupMemberId), withMessages = withMessages)
if (r != null) {
val (updatedGroupInfo, removedMembers) = r
withContext(Dispatchers.Main) {
chatModel.chatsContext.updateGroup(rhId, updatedGroupInfo)
removedMembers.forEach { removedMember ->
chatModel.chatsContext.upsertGroupMember(rhId, updatedGroupInfo, removedMember)
if (withMessages) {
chat.simplex.common.platform.chatModel.chatsContext.removeMemberItems(rhId, removedMember, byMember = groupInfo.membership, groupInfo)
}
}
}
}
@@ -289,6 +320,7 @@ fun GroupMemberInfoLayout(
blockForAll: () -> Unit,
unblockForAll: () -> Unit,
removeMember: () -> Unit,
deleteMemberMessages: () -> Unit,
onRoleSelected: (GroupMemberRole) -> Unit,
switchMemberAddress: () -> Unit,
abortSwitchMemberAddress: () -> Unit,
@@ -345,7 +377,11 @@ fun GroupMemberInfoLayout(
}
}
if (canRemove) {
RemoveMemberButton(removeMember)
if (member.memberStatus == GroupMemberStatus.MemRemoved || member.memberStatus == GroupMemberStatus.MemLeft) {
DeleteMemberMessagesButton(deleteMemberMessages)
} else {
RemoveMemberButton(removeMember)
}
}
}
}
@@ -669,6 +705,17 @@ fun RemoveMemberButton(onClick: () -> Unit) {
)
}
@Composable
fun DeleteMemberMessagesButton(onClick: () -> Unit) {
SettingsActionItem(
painterResource(MR.images.ic_delete),
stringResource(MR.strings.button_delete_member_messages),
click = onClick,
textColor = Color.Red,
iconColor = Color.Red,
)
}
@Composable
fun OpenChatButton(
modifier: Modifier,
@@ -937,6 +984,7 @@ fun PreviewGroupMemberInfoLayout() {
blockForAll = {},
unblockForAll = {},
removeMember = {},
deleteMemberMessages = {},
onRoleSelected = {},
switchMemberAddress = {},
abortSwitchMemberAddress = {},
@@ -563,23 +563,18 @@ fun ChatItemView(
@Composable
fun ContentItem() {
val mc = cItem.content.msgContent
if (cItem.meta.itemDeleted != null && (!revealed.value || cItem.isDeletedContent)) {
MarkedDeletedItemView(chatsCtx, cItem, cInfo, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
MarkedDeletedItemDropdownMenu()
} else {
if (cItem.quotedItem == null && cItem.meta.itemForwarded == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) {
if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) {
EmojiItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
} else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) {
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp, longClick = { onLinkLongClick("") }, receiveFile = receiveFile)
} else {
framedItemView()
}
if (cItem.quotedItem == null && cItem.meta.itemForwarded == null && cItem.meta.itemDeleted == null && !cItem.meta.isLive) {
if (mc is MsgContent.MCText && isShortEmoji(cItem.content.text)) {
EmojiItemView(cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
} else if (mc is MsgContent.MCVoice && cItem.content.text.isEmpty()) {
CIVoiceView(mc.duration, cItem.file, cItem.meta.itemEdited, cItem.chatDir.sent, hasText = false, cItem, cInfo.timedMessagesTTL, showViaProxy = showViaProxy, showTimestamp = showTimestamp, longClick = { onLinkLongClick("") }, receiveFile = receiveFile)
} else {
framedItemView()
}
MsgContentItemDropdownMenu()
} else {
framedItemView()
}
MsgContentItemDropdownMenu()
}
@Composable fun LegacyDeletedItem() {
@@ -696,102 +691,107 @@ fun ChatItemView(
}
}
when (val c = cItem.content) {
is CIContent.SndMsgContent -> ContentItem()
is CIContent.RcvMsgContent -> ContentItem()
is CIContent.SndDeleted -> LegacyDeletedItem()
is CIContent.RcvDeleted -> LegacyDeletedItem()
is CIContent.SndCall -> CallItem(c.status, c.duration)
is CIContent.RcvCall -> CallItem(c.status, c.duration)
is CIContent.RcvIntegrityError -> if (developerTools) {
IntegrityErrorItemView(c.msgError, cItem, showTimestamp, cInfo.timedMessagesTTL)
DeleteItemMenu()
} else {
Box(Modifier.size(0.dp)) {}
}
is CIContent.RcvDecryptionError -> {
CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cInfo, cItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember)
DeleteItemMenu()
}
is CIContent.RcvGroupInvitation -> {
CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, showTimestamp = showTimestamp, timedMessagesTTL = cInfo.timedMessagesTTL)
DeleteItemMenu()
}
is CIContent.SndGroupInvitation -> {
CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, showTimestamp = showTimestamp, timedMessagesTTL = cInfo.timedMessagesTTL)
DeleteItemMenu()
}
is CIContent.RcvDirectEventContent -> {
EventItemView()
MsgContentItemDropdownMenu()
}
is CIContent.RcvGroupEventContent -> {
when (c.rcvGroupEvent) {
is RcvGroupEvent.MemberCreatedContact -> CIMemberCreatedContactView(cItem, openDirectChat)
is RcvGroupEvent.NewMemberPendingReview -> PendingReviewEventItemView()
else -> EventItemView()
if (cItem.meta.itemDeleted != null && (!revealed.value || cItem.isDeletedContent)) {
MarkedDeletedItemView(chatsCtx, cItem, cInfo, cInfo.timedMessagesTTL, revealed, showViaProxy = showViaProxy, showTimestamp = showTimestamp)
MarkedDeletedItemDropdownMenu()
} else {
when (val c = cItem.content) {
is CIContent.SndMsgContent -> ContentItem()
is CIContent.RcvMsgContent -> ContentItem()
is CIContent.SndDeleted -> LegacyDeletedItem()
is CIContent.RcvDeleted -> LegacyDeletedItem()
is CIContent.SndCall -> CallItem(c.status, c.duration)
is CIContent.RcvCall -> CallItem(c.status, c.duration)
is CIContent.RcvIntegrityError -> if (developerTools) {
IntegrityErrorItemView(c.msgError, cItem, showTimestamp, cInfo.timedMessagesTTL)
DeleteItemMenu()
} else {
Box(Modifier.size(0.dp)) {}
}
MsgContentItemDropdownMenu()
}
is CIContent.SndGroupEventContent -> {
when (c.sndGroupEvent) {
is SndGroupEvent.UserPendingReview -> PendingReviewEventItemView()
else -> EventItemView()
is CIContent.RcvDecryptionError -> {
CIRcvDecryptionError(c.msgDecryptError, c.msgCount, cInfo, cItem, updateContactStats = updateContactStats, updateMemberStats = updateMemberStats, syncContactConnection = syncContactConnection, syncMemberConnection = syncMemberConnection, findModelChat = findModelChat, findModelMember = findModelMember)
DeleteItemMenu()
}
is CIContent.RcvGroupInvitation -> {
CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, showTimestamp = showTimestamp, timedMessagesTTL = cInfo.timedMessagesTTL)
DeleteItemMenu()
}
is CIContent.SndGroupInvitation -> {
CIGroupInvitationView(cItem, c.groupInvitation, c.memberRole, joinGroup = joinGroup, chatIncognito = cInfo.incognito, showTimestamp = showTimestamp, timedMessagesTTL = cInfo.timedMessagesTTL)
DeleteItemMenu()
}
is CIContent.RcvDirectEventContent -> {
EventItemView()
MsgContentItemDropdownMenu()
}
is CIContent.RcvGroupEventContent -> {
when (c.rcvGroupEvent) {
is RcvGroupEvent.MemberCreatedContact -> CIMemberCreatedContactView(cItem, openDirectChat)
is RcvGroupEvent.NewMemberPendingReview -> PendingReviewEventItemView()
else -> EventItemView()
}
MsgContentItemDropdownMenu()
}
is CIContent.SndGroupEventContent -> {
when (c.sndGroupEvent) {
is SndGroupEvent.UserPendingReview -> PendingReviewEventItemView()
else -> EventItemView()
}
MsgContentItemDropdownMenu()
}
is CIContent.RcvConnEventContent -> {
EventItemView()
MsgContentItemDropdownMenu()
}
is CIContent.SndConnEventContent -> {
EventItemView()
MsgContentItemDropdownMenu()
}
is CIContent.RcvChatFeature -> {
CIChatFeatureView(chatsCtx, cInfo, cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.SndChatFeature -> {
CIChatFeatureView(chatsCtx, cInfo, cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.RcvChatPreference -> {
val ct = if (cInfo is ChatInfo.Direct) cInfo.contact else null
CIFeaturePreferenceView(cItem, ct, c.feature, c.allowed, acceptFeature)
DeleteItemMenu()
}
is CIContent.SndChatPreference -> {
CIChatFeatureView(chatsCtx, cInfo, cItem, c.feature, MaterialTheme.colors.secondary, icon = c.feature.icon, revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.RcvGroupFeature -> {
CIChatFeatureView(chatsCtx, cInfo, cItem, c.groupFeature, c.preference.enabled(c.memberRole_, (cInfo as? ChatInfo.Group)?.groupInfo?.membership).iconColor, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.SndGroupFeature -> {
CIChatFeatureView(chatsCtx, cInfo, cItem, c.groupFeature, c.preference.enabled(c.memberRole_, (cInfo as? ChatInfo.Group)?.groupInfo?.membership).iconColor, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.RcvChatFeatureRejected -> {
CIChatFeatureView(chatsCtx, cInfo, cItem, c.feature, Color.Red, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.RcvGroupFeatureRejected -> {
CIChatFeatureView(chatsCtx, cInfo, cItem, c.groupFeature, Color.Red, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.SndModerated -> DeletedItem()
is CIContent.RcvModerated -> DeletedItem()
is CIContent.RcvBlocked -> DeletedItem()
is CIContent.SndDirectE2EEInfo -> DirectE2EEInfoText(c.e2eeInfo)
is CIContent.RcvDirectE2EEInfo -> DirectE2EEInfoText(c.e2eeInfo)
is CIContent.SndGroupE2EEInfo -> E2EEInfoNoPQText()
is CIContent.RcvGroupE2EEInfo -> E2EEInfoNoPQText()
is CIContent.ChatBanner -> Spacer(modifier = Modifier.size(0.dp))
is CIContent.InvalidJSON -> {
CIInvalidJSONView(c.json)
DeleteItemMenu()
}
MsgContentItemDropdownMenu()
}
is CIContent.RcvConnEventContent -> {
EventItemView()
MsgContentItemDropdownMenu()
}
is CIContent.SndConnEventContent -> {
EventItemView()
MsgContentItemDropdownMenu()
}
is CIContent.RcvChatFeature -> {
CIChatFeatureView(chatsCtx, cInfo, cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.SndChatFeature -> {
CIChatFeatureView(chatsCtx, cInfo, cItem, c.feature, c.enabled.iconColor, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.RcvChatPreference -> {
val ct = if (cInfo is ChatInfo.Direct) cInfo.contact else null
CIFeaturePreferenceView(cItem, ct, c.feature, c.allowed, acceptFeature)
DeleteItemMenu()
}
is CIContent.SndChatPreference -> {
CIChatFeatureView(chatsCtx, cInfo, cItem, c.feature, MaterialTheme.colors.secondary, icon = c.feature.icon, revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.RcvGroupFeature -> {
CIChatFeatureView(chatsCtx, cInfo, cItem, c.groupFeature, c.preference.enabled(c.memberRole_, (cInfo as? ChatInfo.Group)?.groupInfo?.membership).iconColor, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.SndGroupFeature -> {
CIChatFeatureView(chatsCtx, cInfo, cItem, c.groupFeature, c.preference.enabled(c.memberRole_, (cInfo as? ChatInfo.Group)?.groupInfo?.membership).iconColor, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.RcvChatFeatureRejected -> {
CIChatFeatureView(chatsCtx, cInfo, cItem, c.feature, Color.Red, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.RcvGroupFeatureRejected -> {
CIChatFeatureView(chatsCtx, cInfo, cItem, c.groupFeature, Color.Red, revealed = revealed, showMenu = showMenu)
MsgContentItemDropdownMenu()
}
is CIContent.SndModerated -> DeletedItem()
is CIContent.RcvModerated -> DeletedItem()
is CIContent.RcvBlocked -> DeletedItem()
is CIContent.SndDirectE2EEInfo -> DirectE2EEInfoText(c.e2eeInfo)
is CIContent.RcvDirectE2EEInfo -> DirectE2EEInfoText(c.e2eeInfo)
is CIContent.SndGroupE2EEInfo -> E2EEInfoNoPQText()
is CIContent.RcvGroupE2EEInfo -> E2EEInfoNoPQText()
is CIContent.ChatBanner -> Spacer(modifier = Modifier.size(0.dp))
is CIContent.InvalidJSON -> {
CIInvalidJSONView(c.json)
DeleteItemMenu()
}
}
}
@@ -131,7 +131,7 @@ fun UserPicker(
}
LaunchedEffect(Unit) {
// Controller.ctrl can be null when self-destructing activates
if (controller.ctrl != null && controller.ctrl != -1L) {
if (controller.hasChatCtrl()) {
withBGApi {
controller.reloadRemoteHosts()
}
@@ -366,6 +366,7 @@ fun startChat(
chatDbChanged: MutableState<Boolean>,
progressIndicator: MutableState<Boolean>? = null
) {
Log.d(TAG, "startChat")
withLongRunningApi {
try {
progressIndicator?.value = true
@@ -532,7 +533,7 @@ fun deleteChatDatabaseFilesAndState() {
appPrefs.newDatabaseInitialized.set(false)
chatModel.desktopOnboardingRandomPassword.value = false
controller.appPrefs.storeDBPassphrase.set(true)
controller.ctrl = null
controller.setChatCtrl(null)
// Clear sensitive data on screen just in case ModalManager will fail to prevent hiding its modals while database encrypts itself
chatModel.chatId.value = null
@@ -33,7 +33,7 @@ fun LocalAuthView(m: ChatModel, authRequest: LocalAuthRequest) {
}
} else {
val r: LAResult = if (passcode.value == authRequest.password) {
if (authRequest.selfDestruct && sdPassword != null && controller.ctrl == -1L) {
if (authRequest.selfDestruct && sdPassword != null && controller.getChatCtrl() == -1L) {
initChatControllerOnStart()
}
LAResult.Success
@@ -58,7 +58,7 @@ private fun deleteStorageAndRestart(m: ChatModel, password: String, completed: (
if (m.chatRunning.value == true) {
stopChatAsync(m)
}
val ctrl = m.controller.ctrl
val ctrl = m.controller.getChatCtrl()
if (ctrl != null && ctrl != -1L) {
/**
* The following sequence can bring a user here:
@@ -633,7 +633,7 @@ private fun MutableState<MigrationToState?>.startDownloading(
private fun MutableState<MigrationToState?>.importArchive(archivePath: String, netCfg: NetCfg, networkProxy: NetworkProxy?) {
withLongRunningApi {
try {
if (ChatController.ctrl == null || ChatController.ctrl == -1L) {
if (!ChatController.hasChatCtrl()) {
chatInitControllerRemovingDatabases()
}
controller.apiDeleteStorage()
@@ -1861,14 +1861,19 @@
<!-- GroupMemberInfoView.kt -->
<string name="button_remove_member_question">Remove member?</string>
<string name="button_remove_members_question">Remove members?</string>
<string name="button_delete_member_messages_question">Delete member messages?</string>
<string name="button_remove_member">Remove member</string>
<string name="button_delete_member_messages">Delete member messages</string>
<string name="button_support_chat_member">Chat with member</string>
<string name="button_send_direct_message">Send direct message</string>
<string name="member_will_be_removed_from_group_cannot_be_undone">Member will be removed from group - this cannot be undone!</string>
<string name="members_will_be_removed_from_group_cannot_be_undone">Members will be removed from group - this cannot be undone!</string>
<string name="member_will_be_removed_from_chat_cannot_be_undone">Member will be removed from chat - this cannot be undone!</string>
<string name="members_will_be_removed_from_chat_cannot_be_undone">Members will be removed from chat - this cannot be undone!</string>
<string name="member_messages_will_be_deleted_cannot_be_undone">Member messages will be deleted - this cannot be undone!</string>
<string name="remove_member_confirmation">Remove</string>
<string name="remove_member_delete_messages_confirmation">Remove and delete messages</string>
<string name="delete_member_messages_confirmation">Delete messages</string>
<string name="remove_member_button">Remove member</string>
<string name="block_member_question">Block member?</string>
<string name="block_member_button">Block member</string>