diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt index 795baece0c..87e317200d 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/ChatModel.kt @@ -780,6 +780,7 @@ interface SomeChat { val id: ChatId val apiId: Long val ready: Boolean + val chatDeleted: Boolean val sendMsgEnabled: Boolean val ntfsEnabled: Boolean val incognito: Boolean @@ -860,6 +861,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = contact.id override val apiId get() = contact.apiId override val ready get() = contact.ready + override val chatDeleted get() = contact.chatDeleted override val sendMsgEnabled get() = contact.sendMsgEnabled override val ntfsEnabled get() = contact.ntfsEnabled override val incognito get() = contact.incognito @@ -884,6 +886,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = groupInfo.id override val apiId get() = groupInfo.apiId override val ready get() = groupInfo.ready + override val chatDeleted get() = groupInfo.chatDeleted override val sendMsgEnabled get() = groupInfo.sendMsgEnabled override val ntfsEnabled get() = groupInfo.ntfsEnabled override val incognito get() = groupInfo.incognito @@ -908,6 +911,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = noteFolder.id override val apiId get() = noteFolder.apiId override val ready get() = noteFolder.ready + override val chatDeleted get() = noteFolder.chatDeleted override val sendMsgEnabled get() = noteFolder.sendMsgEnabled override val ntfsEnabled get() = noteFolder.ntfsEnabled override val incognito get() = noteFolder.incognito @@ -932,6 +936,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = contactRequest.id override val apiId get() = contactRequest.apiId override val ready get() = contactRequest.ready + override val chatDeleted get() = contactRequest.chatDeleted override val sendMsgEnabled get() = contactRequest.sendMsgEnabled override val ntfsEnabled get() = contactRequest.ntfsEnabled override val incognito get() = contactRequest.incognito @@ -956,6 +961,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = contactConnection.id override val apiId get() = contactConnection.apiId override val ready get() = contactConnection.ready + override val chatDeleted get() = contactConnection.chatDeleted override val sendMsgEnabled get() = contactConnection.sendMsgEnabled override val ntfsEnabled get() = false override val incognito get() = contactConnection.incognito @@ -981,6 +987,7 @@ sealed class ChatInfo: SomeChat, NamedChat { override val id get() = "" override val apiId get() = 0L override val ready get() = false + override val chatDeleted get() = false override val sendMsgEnabled get() = false override val ntfsEnabled get() = false override val incognito get() = false @@ -1057,6 +1064,7 @@ data class Contact( val chatTs: Instant?, val contactGroupMemberId: Long? = null, val contactGrpInvSent: Boolean, + override val chatDeleted: Boolean, val uiThemes: ThemeModeOverrides? = null, ): SomeChat, NamedChat { override val chatType get() = ChatType.Direct @@ -1130,6 +1138,7 @@ data class Contact( updatedAt = Clock.System.now(), chatTs = Clock.System.now(), contactGrpInvSent = false, + chatDeleted = false, uiThemes = null, ) } @@ -1138,7 +1147,8 @@ data class Contact( @Serializable enum class ContactStatus { @SerialName("active") Active, - @SerialName("deleted") Deleted; + @SerialName("deleted") Deleted, + @SerialName("deletedByUser") DeletedByUser; } @Serializable @@ -1281,6 +1291,7 @@ data class GroupInfo ( override val id get() = "#$groupId" override val apiId get() = groupId override val ready get() = membership.memberActive + override val chatDeleted get() = false override val sendMsgEnabled get() = membership.memberActive override val ntfsEnabled get() = chatSettings.enableNtfs == MsgFilter.All override val incognito get() = membership.memberIncognito @@ -1586,6 +1597,7 @@ class NoteFolder( override val chatType get() = ChatType.Local override val id get() = "*$noteFolderId" override val apiId get() = noteFolderId + override val chatDeleted get() = false override val ready get() = true override val sendMsgEnabled get() = true override val ntfsEnabled get() = false @@ -1622,6 +1634,7 @@ class UserContactRequest ( override val chatType get() = ChatType.ContactRequest override val id get() = "<@$contactRequestId" override val apiId get() = contactRequestId + override val chatDeleted get() = false override val ready get() = true override val sendMsgEnabled get() = false override val ntfsEnabled get() = false @@ -1661,6 +1674,7 @@ class PendingContactConnection( override val chatType get() = ChatType.ContactConnection override val id get () = ":$pccConnId" override val apiId get() = pccConnId + override val chatDeleted get() = false override val ready get() = false override val sendMsgEnabled get() = false override val ntfsEnabled get() = false diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt index 7633b0c808..6a28548fd0 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt @@ -213,6 +213,8 @@ class AppPreferences { val desktopWindowState = mkStrPreference(SHARED_PREFS_DESKTOP_WINDOW_STATE, null) + val showDeleteConversationNotice = mkBoolPreference(SHARED_PREFS_SHOW_DELETE_CONVERSATION_NOTICE, true) + val showDeleteContactNotice = mkBoolPreference(SHARED_PREFS_SHOW_DELETE_CONTACT_NOTICE, true) val showSentViaProxy = mkBoolPreference(SHARED_PREFS_SHOW_SENT_VIA_RPOXY, false) @@ -393,6 +395,8 @@ class AppPreferences { private const val SHARED_PREFS_CONNECT_REMOTE_VIA_MULTICAST_AUTO = "ConnectRemoteViaMulticastAuto" private const val SHARED_PREFS_OFFER_REMOTE_MULTICAST = "OfferRemoteMulticast" private const val SHARED_PREFS_DESKTOP_WINDOW_STATE = "DesktopWindowState" + private const val SHARED_PREFS_SHOW_DELETE_CONVERSATION_NOTICE = "showDeleteConversationNotice" + private const val SHARED_PREFS_SHOW_DELETE_CONTACT_NOTICE = "showDeleteContactNotice" private const val SHARED_PREFS_SHOW_SENT_VIA_RPOXY = "showSentViaProxy" private const val SHARED_PREFS_IOS_CALL_KIT_ENABLED = "iOSCallKitEnabled" @@ -1149,16 +1153,16 @@ object ChatController { } } - suspend fun deleteChat(chat: Chat, notify: Boolean? = null) { + suspend fun deleteChat(chat: Chat, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)) { val cInfo = chat.chatInfo - if (apiDeleteChat(rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, notify = notify)) { + if (apiDeleteChat(rh = chat.remoteHostId, type = cInfo.chatType, id = cInfo.apiId, chatDeleteMode = chatDeleteMode)) { chatModel.removeChat(chat.remoteHostId, cInfo.id) } } - suspend fun apiDeleteChat(rh: Long?, type: ChatType, id: Long, notify: Boolean? = null): Boolean { + suspend fun apiDeleteChat(rh: Long?, type: ChatType, id: Long, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)): Boolean { chatModel.deletedChats.value += rh to type.type + id - val r = sendCmd(rh, CC.ApiDeleteChat(type, id, notify)) + val r = sendCmd(rh, CC.ApiDeleteChat(type, id, chatDeleteMode)) val success = when { r is CR.ContactDeleted && type == ChatType.Direct -> true r is CR.ContactConnectionDeleted && type == ChatType.ContactConnection -> true @@ -1179,6 +1183,22 @@ object ChatController { return success } + suspend fun apiDeleteContact(rh: Long?, id: Long, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)): Contact? { + val type = ChatType.Direct + chatModel.deletedChats.value += rh to type.type + id + val r = sendCmd(rh, CC.ApiDeleteChat(type, id, chatDeleteMode)) + val contact = when { + r is CR.ContactDeleted -> r.contact + else -> { + val titleId = MR.strings.error_deleting_contact + apiErrorAlert("apiDeleteChat", generalGetString(titleId), r) + null + } + } + chatModel.deletedChats.value -= rh to type.type + id + return contact + } + fun clearChat(chat: Chat, close: (() -> Unit)? = null) { withBGApi { val updatedChatInfo = apiClearChat(chat.remoteHostId, chat.chatInfo.chatType, chat.chatInfo.apiId) @@ -1917,6 +1937,10 @@ object ChatController { val cInfo = r.chatItem.chatInfo val cItem = r.chatItem.chatItem if (active(r.user)) { + if (cInfo is ChatInfo.Direct && cInfo.chatDeleted) { + val updatedContact = cInfo.contact.copy(chatDeleted = false) + chatModel.updateContact(rhId, updatedContact) + } chatModel.addChatItem(rhId, cInfo, cItem) } else if (cItem.isRcvNew && cInfo.ntfsEnabled) { chatModel.increaseUnreadCounter(rhId, r.user) @@ -2630,7 +2654,7 @@ sealed class 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 ApiDeleteChat(val type: ChatType, val id: Long, val chatDeleteMode: ChatDeleteMode): CC() class ApiClearChat(val type: ChatType, val id: Long): CC() class ApiListContacts(val userId: Long): CC() class ApiUpdateProfile(val userId: Long, val profile: Profile): CC() @@ -2781,11 +2805,7 @@ sealed class CC { 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 { - "/_delete ${chatRef(type, id)}" - } + is ApiDeleteChat -> "/_delete ${chatRef(type, id)} ${chatDeleteMode.cmdString}" is ApiClearChat -> "/_clear chat ${chatRef(type, id)}" is ApiListContacts -> "/_contacts $userId" is ApiUpdateProfile -> "/_profile $userId ${json.encodeToString(profile)}" @@ -3004,8 +3024,6 @@ sealed class CC { null } - private fun onOff(b: Boolean): String = if (b) "on" else "off" - private fun maybePwd(pwd: String?): String = if (pwd == "" || pwd == null) "" else " " + json.encodeToString(pwd) companion object { @@ -3015,6 +3033,8 @@ sealed class CC { } } +fun onOff(b: Boolean): String = if (b) "on" else "off" + @Serializable data class NewUser( val profile: Profile?, @@ -4994,6 +5014,19 @@ fun chatError(r: CR): ChatErrorType? { ) } +@Serializable +sealed class ChatDeleteMode { + @Serializable @SerialName("full") class Full(val notify: Boolean): ChatDeleteMode() + @Serializable @SerialName("entity") class Entity(val notify: Boolean): ChatDeleteMode() + @Serializable @SerialName("messages") class Messages: ChatDeleteMode() + + val cmdString: String get() = when (this) { + is ChatDeleteMode.Full -> "full notify=${onOff(notify)}" + is ChatDeleteMode.Entity -> "entity notify=${onOff(notify)}" + is ChatDeleteMode.Messages -> "messages" + } +} + @Serializable sealed class ConnectionPlan { @Serializable @SerialName("invitationLink") class InvitationLink(val invitationLinkPlan: InvitationLinkPlan): ConnectionPlan() diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt index 48ed0570a7..812add8159 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatInfoView.kt @@ -12,13 +12,17 @@ import SectionView import androidx.compose.desktop.ui.tooling.preview.Preview import androidx.compose.foundation.* import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.* import androidx.compose.material.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.platform.* +import chat.simplex.common.views.call.CallMediaType +import chat.simplex.common.views.chatlist.* import androidx.compose.ui.text.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource @@ -45,9 +49,21 @@ import kotlinx.datetime.Clock import kotlinx.serialization.encodeToString import java.io.File +sealed class ContactDeleteMode { + class Full: ContactDeleteMode() + class Entity: ContactDeleteMode() + + fun toChatDeleteMode(notify: Boolean): ChatDeleteMode = + when (this) { + is Full -> ChatDeleteMode.Full(notify) + is Entity -> ChatDeleteMode.Entity(notify) + } +} + @Composable fun ChatInfoView( chatModel: ChatModel, + openedFromChatView: Boolean, contact: Contact, connectionStats: ConnectionStats?, customUserProfile: Profile?, @@ -68,6 +84,7 @@ fun ChatInfoView( val chatRh = chat.remoteHostId val sendReceipts = remember(contact.id) { mutableStateOf(SendReceipts.fromBool(contact.chatSettings.sendRcpts, currentUser.sendRcptsContacts)) } ChatInfoLayout( + openedFromChatView = openedFromChatView, chat, contact, currentUser, @@ -166,7 +183,8 @@ fun ChatInfoView( ) } } - } + }, + close = close ) } } @@ -201,53 +219,127 @@ sealed class SendReceipts { fun deleteContactDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = null) { val chatInfo = chat.chatInfo - AlertManager.shared.showAlertDialogButtonsColumn( - title = generalGetString(MR.strings.delete_contact_question), - text = AnnotatedString(generalGetString(MR.strings.delete_contact_all_messages_deleted_cannot_undo_warning)), - buttons = { - Column { - if (chatInfo is ChatInfo.Direct && chatInfo.contact.ready && chatInfo.contact.active) { - // Delete and notify contact + if (chatInfo is ChatInfo.Direct) { + AlertManager.shared.showAlertDialogButtonsColumn( + title = generalGetString(MR.strings.delete_contact_question), + buttons = { + Column { + // Delete contact SectionItemView({ AlertManager.shared.hideAlert() - deleteContact(chat, chatModel, close, notify = true) + notifyDeleteContactDialog(chat, chatModel, close, contactDeleteMode = ContactDeleteMode.Full()) }) { - Text(generalGetString(MR.strings.delete_and_notify_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + Text(generalGetString(MR.strings.button_delete_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) } - // Delete + if (!chatInfo.contact.chatDeleted) { + // Delete contact, keep conversation + SectionItemView({ + AlertManager.shared.hideAlert() + notifyDeleteContactDialog(chat, chatModel, close, contactDeleteMode = ContactDeleteMode.Entity()) + }) { + Text(generalGetString(MR.strings.delete_contact_keep_conversation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + } + // Cancel SectionItemView({ AlertManager.shared.hideAlert() - deleteContact(chat, chatModel, close, notify = false) }) { - Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } - } else { - // Delete - SectionItemView({ - AlertManager.shared.hideAlert() - deleteContact(chat, chatModel, close) - }) { - Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) - } - } - // Cancel - SectionItemView({ - AlertManager.shared.hideAlert() - }) { - Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) } } - } + ) + } +} + +fun notifyDeleteContactDialog( + chat: Chat, + chatModel: ChatModel, + close: (() -> Unit)? = null, + contactDeleteMode: ContactDeleteMode = ContactDeleteMode.Full() +) { + val chatInfo = chat.chatInfo + if (chatInfo is ChatInfo.Direct) { + val contactActive = chatInfo.contact.ready && chatInfo.contact.active + AlertManager.shared.showAlertDialogButtonsColumn( + title = if (contactActive) generalGetString(MR.strings.notify_delete_contact_question) else generalGetString(MR.strings.confirm_delete_contact_question), + text = when (contactDeleteMode) { + is ContactDeleteMode.Full -> generalGetString(MR.strings.delete_contact_all_messages_deleted_cannot_undo_warning) + is ContactDeleteMode.Entity -> generalGetString(MR.strings.delete_contact_cannot_undo_warning) + }, + buttons = { + Column { + if (contactActive) { + // Delete and notify contact + SectionItemView({ + AlertManager.shared.hideAlert() + deleteContact(chat, chatModel, close, chatDeleteMode = contactDeleteMode.toChatDeleteMode(notify = true)) + if (contactDeleteMode is ContactDeleteMode.Entity && chatModel.controller.appPrefs.showDeleteContactNotice.get()) { + showDeleteContactNotice(chatInfo.contact) + } + }) { + Text(generalGetString(MR.strings.delete_and_notify_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + // Delete without notification + SectionItemView({ + AlertManager.shared.hideAlert() + deleteContact(chat, chatModel, close, chatDeleteMode = contactDeleteMode.toChatDeleteMode(notify = false)) + if (contactDeleteMode is ContactDeleteMode.Entity && chatModel.controller.appPrefs.showDeleteContactNotice.get()) { + showDeleteContactNotice(chatInfo.contact) + } + }) { + Text(generalGetString(MR.strings.delete_without_notification), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + } else { + // Delete + SectionItemView({ + AlertManager.shared.hideAlert() + deleteContact(chat, chatModel, close, chatDeleteMode = contactDeleteMode.toChatDeleteMode(notify = false)) + if (contactDeleteMode is ContactDeleteMode.Entity && chatModel.controller.appPrefs.showDeleteContactNotice.get()) { + showDeleteContactNotice(chatInfo.contact) + } + }) { + Text(generalGetString(MR.strings.delete_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + } + // Cancel + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + ) + } +} + +private fun showDeleteContactNotice(contact: Contact) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.contact_deleted), + text = String.format(generalGetString(MR.strings.you_can_still_view_conversation_with_contact), contact.displayName), + confirmText = generalGetString(MR.strings.ok), + dismissText = generalGetString(MR.strings.dont_show_again), + onDismiss = { + chatModel.controller.appPrefs.showDeleteContactNotice.set(false) + }, ) } -fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, notify: Boolean? = null) { +fun deleteContact(chat: Chat, chatModel: ChatModel, close: (() -> Unit)?, chatDeleteMode: ChatDeleteMode = ChatDeleteMode.Full(notify = true)) { val chatInfo = chat.chatInfo withBGApi { val chatRh = chat.remoteHostId - val r = chatModel.controller.apiDeleteChat(chatRh, chatInfo.chatType, chatInfo.apiId, notify) - if (r) { - chatModel.removeChat(chatRh, chatInfo.id) + val ct = chatModel.controller.apiDeleteContact(chatRh, chatInfo.apiId, chatDeleteMode) + if (ct != null) { + when (chatDeleteMode) { + is ChatDeleteMode.Full -> + chatModel.removeChat(chatRh, chatInfo.id) + is ChatDeleteMode.Entity -> + chatModel.updateContact(chatRh, ct) + is ChatDeleteMode.Messages -> + chatModel.clearChat(chatRh, ChatInfo.Direct(ct)) + } if (chatModel.chatId.value == chatInfo.id) { chatModel.chatId.value = null ModalManager.end.closeModals() @@ -280,6 +372,7 @@ fun clearNoteFolderDialog(chat: Chat, close: (() -> Unit)? = null) { @Composable fun ChatInfoLayout( + openedFromChatView: Boolean, chat: Chat, contact: Contact, currentUser: User, @@ -300,6 +393,7 @@ fun ChatInfoLayout( syncContactConnection: () -> Unit, syncContactConnectionForce: () -> Unit, verifyClicked: () -> Unit, + close: () -> Unit, ) { val cStats = connStats.value val scrollState = rememberScrollState() @@ -319,7 +413,31 @@ fun ChatInfoLayout( } LocalAliasEditor(chat.id, localAlias, updateValue = onLocalAliasChanged) + SectionSpacer() + + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = 10.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + if (contact.activeConn == null && contact.profile.contactLink != null && contact.active) { + ConnectButton(openedFromChatView, chat, contact, close) + } else if (!contact.active && !contact.chatDeleted) { + OpenButton(openedFromChatView, chat, contact, close) + } else { + MessageButton(openedFromChatView, chat, contact, close) + } + Spacer(Modifier.width(10.dp)) + CallButton(chat, contact) + Spacer(Modifier.width(10.dp)) + VideoButton(chat, contact) + } + + SectionSpacer() + if (customUserProfile != null) { SectionView(generalGetString(MR.strings.incognito).uppercase()) { SectionItemViewSpaceBetween { @@ -535,6 +653,155 @@ fun LocalAliasEditor( } } +// when contact is a "contact card" +@Composable +private fun ConnectButton(openedFromChatView: Boolean, chat: Chat, contact: Contact, close: () -> Unit) { + InfoViewActionButton( + icon = painterResource(MR.images.ic_chat_bubble_filled), + title = generalGetString(MR.strings.info_view_connect_button), + disabled = false, + onClick = { + AlertManager.privacySensitive.showAlertDialogButtonsColumn( + title = String.format(generalGetString(MR.strings.connect_with_contact_name_question), contact.chatViewName), + buttons = { + Column { + SectionItemView({ + AlertManager.privacySensitive.hideAlert() + infoConnectContactViaAddress(openedFromChatView, chat, contact, incognito = false, close) + }) { + Text(generalGetString(MR.strings.connect_use_current_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + SectionItemView({ + AlertManager.privacySensitive.hideAlert() + infoConnectContactViaAddress(openedFromChatView, chat, contact, incognito = true, close) + }) { + Text(generalGetString(MR.strings.connect_use_new_incognito_profile), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + SectionItemView({ + AlertManager.privacySensitive.hideAlert() + }) { + Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + }, + hostDevice = hostDevice(chat.remoteHostId), + ) + } + ) +} + +private fun infoConnectContactViaAddress(openedFromChatView: Boolean, chat: Chat, contact: Contact, incognito: Boolean, close: () -> Unit) { + withBGApi { + val ok = connectContactViaAddress(chatModel, chat.remoteHostId, contact.contactId, incognito = incognito) + if (ok) { + if (openedFromChatView) { + close.invoke() + } else { + if (contact.chatDeleted) { + chatModel.updateContact(chat.remoteHostId, contact.copy(chatDeleted = false)) + } + close.invoke() + openDirectChat(chat.remoteHostId, contact.contactId, chatModel) + } + } + } +} + +@Composable +private fun OpenButton(openedFromChatView: Boolean, chat: Chat, contact: Contact, close: () -> Unit) { + InfoViewActionButton( + icon = painterResource(MR.images.ic_chat_bubble_filled), + title = generalGetString(MR.strings.info_view_open_button), + disabled = false, + onClick = { + if (openedFromChatView) { + close.invoke() + } else { + close.invoke() + withBGApi { + openDirectChat(chat.remoteHostId, contact.contactId, chatModel) + } + } + } + ) +} + +@Composable +private fun MessageButton(openedFromChatView: Boolean, chat: Chat, contact: Contact, close: () -> Unit) { + InfoViewActionButton( + icon = painterResource(MR.images.ic_chat_bubble_filled), + title = generalGetString(MR.strings.info_view_message_button), + disabled = !contact.sendMsgEnabled, + onClick = { + if (openedFromChatView) { + close.invoke() + } else { + if (contact.chatDeleted) { + chatModel.updateContact(chat.remoteHostId, contact.copy(chatDeleted = false)) + } + close.invoke() + withBGApi { + openDirectChat(chat.remoteHostId, contact.contactId, chatModel) + } + } + } + ) +} + +@Composable +fun CallButton(chat: Chat, contact: Contact) { + InfoViewActionButton( + icon = painterResource(MR.images.ic_call_filled), + title = generalGetString(MR.strings.info_view_call_button), + disabled = !contact.ready || !contact.active || !contact.mergedPreferences.calls.enabled.forUser || chatModel.activeCall.value != null, + onClick = { + startChatCall(chat, CallMediaType.Audio) + } + ) +} + +@Composable +fun VideoButton(chat: Chat, contact: Contact) { + InfoViewActionButton( + icon = painterResource(MR.images.ic_videocam_filled), + title = generalGetString(MR.strings.info_view_video_button), + disabled = !contact.ready || !contact.active || !contact.mergedPreferences.calls.enabled.forUser || chatModel.activeCall.value != null, + onClick = { + startChatCall(chat, CallMediaType.Video) + } + ) +} + +@Composable +fun InfoViewActionButton(icon: Painter, title: String, disabled: Boolean, onClick: () -> Unit) { + Surface( + Modifier + .width(96.dp) + .height(66.dp), + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colors.secondaryVariant, + ) { + val modifier = if (disabled) Modifier else Modifier.clickable { onClick () } + Column( + modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon( + icon, + contentDescription = null, + Modifier.size(26.dp), + tint = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + ) + Text( + title, + style = MaterialTheme.typography.subtitle2.copy(fontWeight = FontWeight.Normal), + color = if (disabled) MaterialTheme.colors.secondary else MaterialTheme.colors.primary + ) + } + } +} + @Composable private fun NetworkStatusRow(networkStatus: NetworkStatus) { Row( @@ -823,6 +1090,7 @@ fun queueInfoText(info: Pair): String { fun PreviewChatInfoLayout() { SimpleXTheme { ChatInfoLayout( + openedFromChatView = false, chat = Chat( remoteHostId = null, chatInfo = ChatInfo.Direct.sampleData, @@ -847,6 +1115,7 @@ fun PreviewChatInfoLayout() { syncContactConnection = {}, syncContactConnectionForce = {}, verifyClicked = {}, + close = {}, ) } } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt index ec34a73d31..b1107d058b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/ChatView.kt @@ -189,7 +189,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: code = chatModel.controller.apiGetContactCode(chatRh, chat.chatInfo.apiId)?.second preloadedCode = code } - ChatInfoView(chatModel, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close) + ChatInfoView(chatModel, openedFromChatView = true, (chat.chatInfo as ChatInfo.Direct).contact, contactInfo?.first, contactInfo?.second, chat.chatInfo.localAlias, code, close) } else if (chat?.chatInfo is ChatInfo.Group) { var link: Pair? by remember(chat.id) { mutableStateOf(preloadedLink) } KeyChangeEffect(chat.id) { @@ -306,18 +306,7 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: onComplete.invoke() } }, - startCall = out@{ media -> - withBGApi { - val cInfo = chat.chatInfo - if (cInfo is ChatInfo.Direct) { - val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId) - val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi - chatModel.activeCall.value = Call(remoteHostId = chatRh, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile) - chatModel.showCallView.value = true - chatModel.callCommand.add(WCallCommand.Capabilities(media)) - } - } - }, + startCall = out@{ media -> startChatCall(chat, media) }, endCall = { val call = chatModel.activeCall.value if (call != null) withBGApi { chatModel.callManager.endCall(call) } @@ -521,6 +510,19 @@ fun ChatView(chatId: String, chatModel: ChatModel, onComposed: suspend (chatId: } } +fun startChatCall(chat: Chat, media: CallMediaType) { + withBGApi { + val cInfo = chat.chatInfo + if (cInfo is ChatInfo.Direct) { + val contactInfo = chatModel.controller.apiContactInfo(chat.remoteHostId, cInfo.contact.contactId) + val profile = contactInfo?.second ?: chatModel.currentUser.value?.profile?.toProfile() ?: return@withBGApi + chatModel.activeCall.value = Call(remoteHostId = chat.remoteHostId, contact = cInfo.contact, callState = CallState.WaitCapabilities, localMedia = media, userProfile = profile) + chatModel.showCallView.value = true + chatModel.callCommand.add(WCallCommand.Capabilities(media)) + } + } +} + @Composable fun ChatLayout( chat: Chat, diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt index 98b9ca729f..85edf7f802 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chat/group/GroupMemberInfoView.kt @@ -246,10 +246,10 @@ fun GroupMemberInfoLayout( verifyClicked: () -> Unit, ) { val cStats = connStats.value - fun knownDirectChat(contactId: Long): Chat? { + fun knownDirectChat(contactId: Long): Pair? { val chat = getContactChat(contactId) return if (chat != null && chat.chatInfo is ChatInfo.Direct && chat.chatInfo.contact.directOrUsed) { - chat + chat to chat.chatInfo.contact } else { null } @@ -309,17 +309,37 @@ fun GroupMemberInfoLayout( val contactId = member.memberContactId + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = 10.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + val knownChat = if (contactId != null) knownDirectChat(contactId) else null + if (knownChat != null) { + val (chat, contact) = knownChat + OpenChatButton(onClick = { openDirectChat(contact.contactId) }) + Spacer(Modifier.width(10.dp)) + CallButton(chat, contact) + Spacer(Modifier.width(10.dp)) + VideoButton(chat, contact) + } else if (groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) { + if (contactId != null) { + OpenChatButton(onClick = { openDirectChat(contactId) }) + } else { + OpenChatButton(onClick = { createMemberContact() }) + } + Spacer(Modifier.width(10.dp)) + InfoViewActionButton(painterResource(MR.images.ic_call_filled), generalGetString(MR.strings.info_view_call_button), disabled = true, onClick = {}) + Spacer(Modifier.width(10.dp)) + InfoViewActionButton(painterResource(MR.images.ic_videocam_filled), generalGetString(MR.strings.info_view_video_button), disabled = true, onClick = {}) + } + } + SectionSpacer() + if (member.memberActive) { SectionView { - if (contactId != null && knownDirectChat(contactId) != null) { - OpenChatButton(onClick = { openDirectChat(contactId) }) - } else if (groupInfo.fullGroupPreferences.directMessages.on(groupInfo.membership)) { - if (contactId != null) { - OpenChatButton(onClick = { openDirectChat(contactId) }) - } else if (member.activeConn?.peerChatVRange?.isCompatibleRange(CREATE_MEMBER_CONTACT_VRANGE) == true) { - OpenChatButton(onClick = { createMemberContact() }) - } - } if (connectionCode != null) { VerifyCodeButton(member.verified, verifyClicked) } @@ -513,12 +533,11 @@ fun RemoveMemberButton(onClick: () -> Unit) { @Composable fun OpenChatButton(onClick: () -> Unit) { - SettingsActionItem( - painterResource(MR.images.ic_chat), - stringResource(MR.strings.button_send_direct_message), - click = onClick, - textColor = MaterialTheme.colors.primary, - iconColor = MaterialTheme.colors.primary, + InfoViewActionButton( + icon = painterResource(MR.images.ic_chat_bubble_filled), + title = generalGetString(MR.strings.info_view_message_button), + disabled = false, + onClick = onClick ) } diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt index 2324d62ea3..f3d25c896b 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListNavLinkView.kt @@ -172,7 +172,7 @@ fun ChatListNavLinkView(chat: Chat, nextChatSelected: State) { } @Composable -private fun ErrorChatListItem() { +fun ErrorChatListItem() { Box(Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 6.dp)) { Text(stringResource(MR.strings.error_showing_content), color = MaterialTheme.colors.error, fontStyle = FontStyle.Italic) } @@ -407,13 +407,73 @@ fun DeleteContactAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState stringResource(MR.strings.delete_contact_menu_action), painterResource(MR.images.ic_delete), onClick = { - deleteContactDialog(chat, chatModel) + deleteContactConversationDialog(chat, chatModel) showMenu.value = false }, color = Color.Red ) } +fun deleteContactConversationDialog(chat: Chat, chatModel: ChatModel, close: (() -> Unit)? = null) { + val chatInfo = chat.chatInfo + if (chatInfo is ChatInfo.Direct) { + val contactDeletedByUser = chatInfo.contact.contactStatus == ContactStatus.DeletedByUser + AlertManager.shared.showAlertDialogButtonsColumn( + title = if (contactDeletedByUser) generalGetString(MR.strings.delete_conversation_question) else generalGetString(MR.strings.delete_contact_question), + text = if (contactDeletedByUser) generalGetString(MR.strings.delete_conversation_all_messages_deleted_cannot_undo_warning) else null, + buttons = { + Column { + if (contactDeletedByUser) { + // Delete conversation + SectionItemView({ + AlertManager.shared.hideAlert() + deleteContact(chat, chatModel, close, chatDeleteMode = ChatDeleteMode.Full(notify = false)) + }) { + Text(generalGetString(MR.strings.delete_conversation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + } else { + // Delete contact + SectionItemView({ + AlertManager.shared.hideAlert() + notifyDeleteContactDialog(chat, chatModel, close) + }) { + Text(generalGetString(MR.strings.button_delete_contact), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + // Only delete conversation + SectionItemView({ + AlertManager.shared.hideAlert() + deleteContact(chat, chatModel, close, chatDeleteMode = ChatDeleteMode.Messages()) + if (chatModel.controller.appPrefs.showDeleteConversationNotice.get()) { + showDeleteConversationNotice(chatInfo.contact) + } + }) { + Text(generalGetString(MR.strings.only_delete_conversation), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.error) + } + } + // Cancel + SectionItemView({ + AlertManager.shared.hideAlert() + }) { + Text(stringResource(MR.strings.cancel_verb), Modifier.fillMaxWidth(), textAlign = TextAlign.Center, color = MaterialTheme.colors.primary) + } + } + } + ) + } +} + +private fun showDeleteConversationNotice(contact: Contact) { + AlertManager.shared.showAlertDialog( + title = generalGetString(MR.strings.conversation_deleted), + text = String.format(generalGetString(MR.strings.you_can_still_send_messages_to_contact), contact.displayName), + confirmText = generalGetString(MR.strings.ok), + dismissText = generalGetString(MR.strings.dont_show_again), + onDismiss = { + chatModel.controller.appPrefs.showDeleteConversationNotice.set(false) + }, + ) +} + @Composable fun DeleteGroupAction(chat: Chat, groupInfo: GroupInfo, chatModel: ChatModel, showMenu: MutableState) { ItemAction( diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt index 13e88c3b2f..eb76b08cac 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatListView.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.* import androidx.compose.ui.graphics.* +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.platform.* import androidx.compose.ui.text.TextRange @@ -72,7 +73,8 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) } val scope = rememberCoroutineScope() val (userPickerState, scaffoldState ) = settingsState - Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListToolbar(scaffoldState.drawerState, userPickerState, stopped)} }, + Scaffold(topBar = { Box(Modifier.padding(end = endPadding)) { ChatListTopBar(stopped) } }, + bottomBar = { Box(Modifier.padding(end = endPadding)) { ChatListBottomToolbar(scaffoldState.drawerState, userPickerState) } }, scaffoldState = scaffoldState, drawerContent = { tryOrShowError("Settings", error = { ErrorSettingsView() }) { @@ -184,10 +186,10 @@ private fun ConnectButton(text: String, onClick: () -> Unit) { } @Composable -private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableStateFlow, stopped: Boolean) { +private fun ChatListTopBar(stopped: Boolean) { val serversSummary: MutableState = remember { mutableStateOf(null) } - val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() + val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() if (stopped) { barButtons.add { IconButton(onClick = { @@ -204,26 +206,10 @@ private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableSt } } } - val scope = rememberCoroutineScope() + val clipboard = LocalClipboardManager.current + DefaultTopAppBar( - navigationButton = { - if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) { - NavigationButtonMenu { scope.launch { if (drawerState.isOpen) drawerState.close() else drawerState.open() } } - } else { - val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } } - val allRead = users - .filter { u -> !u.user.activeUser && !u.user.hidden } - .all { u -> u.unreadCount == 0 } - UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) { - if (users.size == 1 && chatModel.remoteHosts.isEmpty()) { - scope.launch { drawerState.open() } - } else { - userPickerState.value = AnimatedViewState.VISIBLE - } - } - } - }, title = { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON)) { Text( @@ -259,7 +245,89 @@ private fun ChatListToolbar(drawerState: DrawerState, userPickerState: MutableSt onSearchValueChanged = {}, buttons = barButtons ) - Divider(Modifier.padding(top = AppBarHeight * fontSizeSqrtMultiplier)) + Divider(Modifier.padding(top = AppBarHeight)) +} + +@Composable +fun SettingsButton(drawerState: DrawerState, userPickerState: MutableStateFlow) { + val scope = rememberCoroutineScope() + if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) { + NavigationButtonMenu { scope.launch { if (drawerState.isOpen) drawerState.close() else drawerState.open() } } + } else { + val users by remember { derivedStateOf { chatModel.users.filter { u -> u.user.activeUser || !u.user.hidden } } } + val allRead = users + .filter { u -> !u.user.activeUser && !u.user.hidden } + .all { u -> u.unreadCount == 0 } + + UserProfileButton(chatModel.currentUser.value?.profile?.image, allRead) { + if (users.size == 1 && chatModel.remoteHosts.isEmpty()) { + scope.launch { drawerState.open() } + } else { + userPickerState.value = AnimatedViewState.VISIBLE + } + } + } +} + +@Composable +fun toolbarIcon(icon: Painter) { + Icon( + icon, + contentDescription = null, + Modifier.size(24.dp * fontSizeSqrtMultiplier), + tint = MaterialTheme.colors.secondary + ) +} + +@Composable +fun ChatListToolbarButton(icon: @Composable () -> Unit, title: String, onClick: () -> Unit) { + Surface( + Modifier + .size(56.dp * fontSizeSqrtMultiplier), + shape = RoundedCornerShape(10.dp), + color = Color.Transparent, + ) { + Column( + Modifier + .clickable { onClick () }, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + icon() + Text( + title, + style = MaterialTheme.typography.subtitle2.copy(fontWeight = FontWeight.Normal, fontSize = 12.sp * fontSizeSqrtMultiplier), + color = MaterialTheme.colors.secondary + ) + } + } +} + +@Composable +private fun ChatListBottomToolbar(drawerState: DrawerState, userPickerState: MutableStateFlow) { + Box( + Modifier + .fillMaxWidth() + .height(BottomAppBarHeight) + .background(MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f)) + ) { + Divider() + Row( + Modifier + .fillMaxHeight() + .fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + ) { + SettingsButton(drawerState, userPickerState) + + ChatListToolbarButton( + icon = { toolbarIcon(painterResource(MR.images.ic_chat_bubble_filled)) }, + title = generalGetString(MR.strings.your_chats), + onClick = { } + ) + } + } } @Composable @@ -311,26 +379,33 @@ fun SubscriptionStatusIndicator(serversSummary: MutableState Unit) { - Row(verticalAlignment = Alignment.CenterVertically) { - IconButton(onClick = onButtonClicked) { + ChatListToolbarButton( + icon = { Box { ProfileImage( image = image, - size = 37.dp * fontSizeSqrtMultiplier, - color = MaterialTheme.colors.secondaryVariant.mixWith(MaterialTheme.colors.onBackground, 0.97f) + size = 24.dp * fontSizeSqrtMultiplier, + color = MaterialTheme.colors.secondaryVariant.mixWith( + MaterialTheme.colors.onBackground, + 0.97f + ) ) if (!allRead) { unreadBadge() } } - } - if (appPlatform.isDesktop) { - val h by remember { chatModel.currentRemoteHost } - if (h != null) { - Spacer(Modifier.width(12.dp)) - HostDisconnectButton { - stopRemoteHostAndReloadHosts(h!!, true) - } + }, + onClick = onButtonClicked, + title = generalGetString(MR.strings.toolbar_settings), + + ) + + if (appPlatform.isDesktop) { + val h by remember { chatModel.currentRemoteHost } + if (h != null) { + Spacer(Modifier.width(12.dp)) + HostDisconnectButton { + stopRemoteHostAndReloadHosts(h!!, true) } } } @@ -537,17 +612,18 @@ private fun filteredChats( } else { val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase() if (s.isEmpty() && !showUnreadAndFavorites) - chats + chats.filter { chat -> !chat.chatInfo.chatDeleted } else { chats.filter { chat -> when (val cInfo = chat.chatInfo) { - is ChatInfo.Direct -> if (s.isEmpty()) { - chat.id == chatModel.chatId.value || filtered(chat) - } else { - (viewNameContains(cInfo, s) || - cInfo.contact.profile.displayName.lowercase().contains(s) || - cInfo.contact.fullName.lowercase().contains(s)) - } + is ChatInfo.Direct -> !chat.chatInfo.chatDeleted && ( + if (s.isEmpty()) { + chat.id == chatModel.chatId.value || filtered(chat) + } else { + (viewNameContains(cInfo, s) || + cInfo.contact.profile.displayName.lowercase().contains(s) || + cInfo.contact.fullName.lowercase().contains(s)) + }) is ChatInfo.Group -> if (s.isEmpty()) { chat.id == chatModel.chatId.value || filtered(chat) || cInfo.groupInfo.membership.memberStatus == GroupMemberStatus.MemInvited } else { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt index 0e215724f7..71c83e2b98 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/ChatPreviewView.kt @@ -201,7 +201,7 @@ fun ChatPreviewView( } else { when (cInfo) { is ChatInfo.Direct -> - if (cInfo.contact.activeConn == null && cInfo.contact.profile.contactLink != null) { + 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.ready && cInfo.contact.activeConn != null) { if (cInfo.contact.nextSendGrpInv) { diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt index 8199a3f2d6..66405afb27 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/chatlist/UserPicker.kt @@ -153,6 +153,8 @@ fun UserPicker( ) { Column( Modifier + .align(Alignment.BottomStart) + .padding(bottom = BottomAppBarHeight) .widthIn(min = 260.dp) .width(IntrinsicSize.Min) .height(IntrinsicSize.Min) diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt index 3d6d242832..c7ae522684 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DefaultTopAppBar.kt @@ -16,7 +16,7 @@ import chat.simplex.res.MR @Composable fun DefaultTopAppBar( - navigationButton: @Composable RowScope.() -> Unit, + navigationButton: (@Composable RowScope.() -> Unit)? = null, title: (@Composable () -> Unit)?, onTitleClick: (() -> Unit)? = null, showSearch: Boolean, @@ -126,5 +126,6 @@ private fun TopAppBar( val AppBarHeight = 56.dp val AppBarHorizontalPadding = 4.dp +val BottomAppBarHeight = 60.dp private val TitleInsetWithoutIcon = DEFAULT_PADDING - AppBarHorizontalPadding val TitleInsetWithIcon = 72.dp diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt index 9faee4532a..58255f21b5 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/newchat/NewChatSheet.kt @@ -152,7 +152,7 @@ private fun NewChatSheetLayout( } FloatingActionButton( onClick = { if (!stopped) closeNewChatSheet(true) }, - Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING).size(AppBarHeight * fontSizeSqrtMultiplier), + Modifier.padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING + BottomAppBarHeight).size(AppBarHeight * fontSizeSqrtMultiplier), elevation = FloatingActionButtonDefaults.elevation( defaultElevation = 0.dp, pressedElevation = 0.dp, diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml index e2d10ae5ae..46f5bc022c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -341,6 +341,7 @@ Welcome! This text is available in settings Chats + Settings connecting… send direct message you are invited to group @@ -432,10 +433,28 @@ Notifications + connect + open + message + call + video Delete contact? Contact and all messages will be deleted - this cannot be undone! + Contact will be deleted - this cannot be undone! + Delete conversation? + Conversation and all messages will be deleted - this cannot be undone! + Delete conversation + Only delete conversation + Delete contact, keep conversation + Notify contact? + Confirm contact deletion? Delete and notify contact + Delete without notification Delete contact + Conversation deleted! + You can still send messages to %1$s from the Contacts tab. + Contact deleted! + You can still view conversation with %1$s in the Chats tab. Set contact name… Connected Disconnected diff --git a/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat_bubble_filled.svg b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat_bubble_filled.svg new file mode 100644 index 0000000000..15d4ef80a9 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/resources/MR/images/ic_chat_bubble_filled.svg @@ -0,0 +1 @@ + \ No newline at end of file