Merge branch 'master' into dc/ios-chat-tags-ux-improvements

This commit is contained in:
Evgeny Poberezkin 2025-01-01 22:22:14 +00:00
commit 91b5d7faac
No known key found for this signature in database
GPG key ID: 494BDDD9A28B577D
21 changed files with 302 additions and 219 deletions

View file

@ -14,6 +14,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import chat.simplex.common.AppScreen
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.model.clear
import chat.simplex.common.model.clearAndNotify
import chat.simplex.common.views.helpers.*
@ -74,9 +75,13 @@ actual class GlobalExceptionsHandler: Thread.UncaughtExceptionHandler {
if (ModalManager.start.hasModalsOpen()) {
ModalManager.start.closeModal()
} else if (chatModel.chatId.value != null) {
// Since no modals are open, the problem is probably in ChatView
chatModel.chatId.value = null
chatModel.chatItems.clearAndNotify()
withApi {
withChats {
// Since no modals are open, the problem is probably in ChatView
chatModel.chatId.value = null
chatItems.clearAndNotify()
}
}
} else {
// ChatList, nothing to do. Maybe to show other view except ChatList
}

View file

@ -29,6 +29,19 @@ private val CALL_TOP_GREEN_LINE_HEIGHT = ANDROID_CALL_TOP_PADDING - CALL_TOP_OFF
private val CALL_BOTTOM_ICON_OFFSET = (-15).dp
private val CALL_BOTTOM_ICON_HEIGHT = CALL_INTERACTIVE_AREA_HEIGHT + CALL_BOTTOM_ICON_OFFSET
@Composable
actual fun TagsRow(content: @Composable() (() -> Unit)) {
Row(
modifier = Modifier
.padding(horizontal = 14.dp)
.horizontalScroll(rememberScrollState()),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
content()
}
}
@Composable
actual fun ActiveCallInteractiveArea(call: Call) {
val onClick = { platform.androidStartCallActivity(false) }

View file

@ -61,7 +61,6 @@ object ChatModel {
val incompleteInitializedDbRemoved = mutableStateOf(false)
private val _chats = mutableStateOf(SnapshotStateList<Chat>())
val chats: State<List<Chat>> = _chats
private val chatsContext = ChatsContext()
// map of connections network statuses, key is agent connection id
val networkStatuses = mutableStateMapOf<String, NetworkStatus>()
val switchingUsersAndHosts = mutableStateOf(false)
@ -72,15 +71,18 @@ object ChatModel {
* If some helper is missing, create it. Notify is needed to track state of items that we added manually (not via api call). See [apiLoadMessages].
* If you use api call to get the items, use just [add] instead of [addAndNotify].
* Never modify underlying list directly because it produces unexpected results in ChatView's LazyColumn (setting by index is ok) */
val chatItems = mutableStateOf(SnapshotStateList<ChatItem>())
private val _chatItems = mutableStateOf(SnapshotStateList<ChatItem>())
val chatItems: State<SnapshotStateList<ChatItem>> = _chatItems
// declaration of chatsContext should be after any other variable that is directly attached to ChatsContext class, otherwise, strange crash with NullPointerException for "this" parameter in random functions
private val chatsContext = ChatsContext()
// set listener here that will be notified on every add/delete of a chat item
var chatItemsChangesListener: ChatItemsChangesListener? = null
val chatState = ActiveChatState()
// rhId, chatId
val deletedChats = mutableStateOf<List<Pair<Long?, String>>>(emptyList())
val chatItemStatuses = mutableMapOf<Long, CIStatus>()
val groupMembers = mutableStateListOf<GroupMember>()
val groupMembersIndexes = mutableStateMapOf<Long, Int>()
val groupMembers = mutableStateOf<List<GroupMember>>(emptyList())
val groupMembersIndexes = mutableStateOf<Map<Long, Int>>(emptyMap())
// Chat Tags
val userTags = mutableStateOf(emptyList<ChatTag>())
@ -157,7 +159,6 @@ object ChatModel {
val updatingProgress = mutableStateOf(null as Float?)
var updatingRequest: Closeable? = null
private val updatingChatsMutex: Mutex = Mutex()
val changingActiveUserMutex: Mutex = Mutex()
val desktopNoUserNoRemote: Boolean @Composable get() = appPlatform.isDesktop && currentUser.value == null && currentRemoteHost.value == null
@ -321,27 +322,31 @@ object ChatModel {
fun getGroupChat(groupId: Long): Chat? = chats.value.firstOrNull { it.chatInfo is ChatInfo.Group && it.chatInfo.apiId == groupId }
fun populateGroupMembersIndexes() {
groupMembersIndexes.clear()
groupMembers.forEachIndexed { i, member ->
groupMembersIndexes[member.groupMemberId] = i
groupMembersIndexes.value = emptyMap()
val gmIndexes = groupMembersIndexes.value.toMutableMap()
groupMembers.value.forEachIndexed { i, member ->
gmIndexes[member.groupMemberId] = i
}
groupMembersIndexes.value = gmIndexes
}
fun getGroupMember(groupMemberId: Long): GroupMember? {
val memberIndex = groupMembersIndexes[groupMemberId]
val memberIndex = groupMembersIndexes.value[groupMemberId]
return if (memberIndex != null) {
groupMembers[memberIndex]
groupMembers.value[memberIndex]
} else {
null
}
}
suspend fun <T> withChats(action: suspend ChatsContext.() -> T): T = updatingChatsMutex.withLock {
// running everything inside the block on main thread. Make sure any heavy computation is moved to a background thread
suspend fun <T> withChats(action: suspend ChatsContext.() -> T): T = withContext(Dispatchers.Main) {
chatsContext.action()
}
class ChatsContext {
val chats = _chats
val chatItems = _chatItems
suspend fun addChat(chat: Chat) {
chats.add(index = 0, chat)
@ -694,7 +699,7 @@ object ChatModel {
}
// update current chat
return if (chatId.value == groupInfo.id) {
val memberIndex = groupMembersIndexes[member.groupMemberId]
val memberIndex = groupMembersIndexes.value[member.groupMemberId]
val updated = chatItems.value.map {
// Take into account only specific changes, not all. Other member updates are not important and can be skipped
if (it.chatDir is CIDirection.GroupRcv && it.chatDir.groupMember.groupMemberId == member.groupMemberId &&
@ -710,12 +715,17 @@ object ChatModel {
if (updated != chatItems.value) {
chatItems.replaceAll(updated)
}
val gMembers = groupMembers.value.toMutableList()
if (memberIndex != null) {
groupMembers[memberIndex] = member
gMembers[memberIndex] = member
groupMembers.value = gMembers
false
} else {
groupMembers.add(member)
groupMembersIndexes[member.groupMemberId] = groupMembers.size - 1
gMembers.add(member)
groupMembers.value = gMembers
val gmIndexes = groupMembersIndexes.value.toMutableMap()
gmIndexes[member.groupMemberId] = groupMembers.size - 1
groupMembersIndexes.value = gmIndexes
true
}
} else {
@ -762,7 +772,7 @@ object ChatModel {
suspend fun addLiveDummy(chatInfo: ChatInfo): ChatItem {
val cItem = ChatItem.liveDummy(chatInfo is ChatInfo.Direct)
withContext(Dispatchers.Main) {
withChats {
chatItems.addAndNotify(cItem)
}
return cItem
@ -770,7 +780,11 @@ object ChatModel {
fun removeLiveDummy() {
if (chatItems.value.lastOrNull()?.id == ChatItem.TEMP_LIVE_CHAT_ITEM_ID) {
chatItems.removeLastAndNotify()
withApi {
withChats {
chatItems.removeLastAndNotify()
}
}
}
}
@ -891,19 +905,25 @@ object ChatModel {
fun replaceConnReqView(id: String, withId: String) {
if (id == showingInvitation.value?.connId) {
showingInvitation.value = null
chatModel.chatItems.clearAndNotify()
chatModel.chatId.value = withId
withApi {
withChats {
showingInvitation.value = null
chatItems.clearAndNotify()
chatModel.chatId.value = withId
}
}
ModalManager.start.closeModals()
ModalManager.end.closeModals()
}
}
fun dismissConnReqView(id: String) {
fun dismissConnReqView(id: String) = withApi {
if (id == showingInvitation.value?.connId) {
showingInvitation.value = null
chatModel.chatItems.clearAndNotify()
chatModel.chatId.value = null
withChats {
showingInvitation.value = null
chatItems.clearAndNotify()
chatModel.chatId.value = null
}
// Close NewChatView
ModalManager.start.closeModals()
ModalManager.center.closeModals()
@ -1334,7 +1354,13 @@ sealed class ChatInfo: SomeChat, NamedChat {
is Group -> groupInfo.chatTags
else -> null
}
}
val contactCard: Boolean
get() = when (this) {
is Direct -> contact.activeConn == null && contact.profile.contactLink != null && contact.active
else -> false
}
}
@Serializable
sealed class NetworkStatus {

View file

@ -683,6 +683,8 @@ object ChatController {
Log.d(TAG, "sendCmd: ${cmd.cmdType}")
}
val json = if (rhId == null) chatSendCmd(ctrl, c) else chatSendRemoteCmd(ctrl, rhId.toInt(), c)
// coroutine was cancelled already, no need to process response (helps with apiListMembers - very heavy query in large groups)
interruptIfCancelled()
val r = APIResponse.decodeStr(json)
if (log) {
Log.d(TAG, "sendCmd response type ${r.resp.responseType}")
@ -3044,8 +3046,8 @@ object ChatController {
chatModel.users.addAll(users)
chatModel.currentUser.value = user
if (user == null) {
chatModel.chatItems.clearAndNotify()
withChats {
chatItems.clearAndNotify()
chats.clear()
popChatCollector.clear()
}

View file

@ -45,9 +45,9 @@ suspend fun apiLoadMessages(
addChat(chat)
}
}
withContext(Dispatchers.Main) {
withChats {
chatModel.chatItemStatuses.clear()
chatModel.chatItems.replaceAll(chat.chatItems)
chatItems.replaceAll(chat.chatItems)
chatModel.chatId.value = chat.chatInfo.id
splits.value = newSplits
if (chat.chatItems.isNotEmpty()) {
@ -70,8 +70,8 @@ suspend fun apiLoadMessages(
)
val insertAt = (indexInCurrentItems - (wasSize - newItems.size) + trimmedIds.size).coerceAtLeast(0)
newItems.addAll(insertAt, chat.chatItems)
withContext(Dispatchers.Main) {
chatModel.chatItems.replaceAll(newItems)
withChats {
chatItems.replaceAll(newItems)
splits.value = newSplits
chatState.moveUnreadAfterItem(oldUnreadSplitIndex, newUnreadSplitIndex, oldItems)
}
@ -89,8 +89,8 @@ suspend fun apiLoadMessages(
val indexToAdd = min(indexInCurrentItems + 1, newItems.size)
val indexToAddIsLast = indexToAdd == newItems.size
newItems.addAll(indexToAdd, chat.chatItems)
withContext(Dispatchers.Main) {
chatModel.chatItems.replaceAll(newItems)
withChats {
chatItems.replaceAll(newItems)
splits.value = newSplits
chatState.moveUnreadAfterItem(splits.value.firstOrNull() ?: newItems.last().id, newItems)
// loading clear bottom area, updating number of unread items after the newest loaded item
@ -104,8 +104,8 @@ suspend fun apiLoadMessages(
val newSplits = removeDuplicatesAndUpperSplits(newItems, chat, splits, visibleItemIndexesNonReversed)
// currently, items will always be added on top, which is index 0
newItems.addAll(0, chat.chatItems)
withContext(Dispatchers.Main) {
chatModel.chatItems.replaceAll(newItems)
withChats {
chatItems.replaceAll(newItems)
splits.value = listOf(chat.chatItems.last().id) + newSplits
unreadAfterItemId.value = chat.chatItems.last().id
totalAfter.value = navInfo.afterTotal
@ -119,8 +119,8 @@ suspend fun apiLoadMessages(
newItems.addAll(oldItems)
removeDuplicates(newItems, chat)
newItems.addAll(chat.chatItems)
withContext(Dispatchers.Main) {
chatModel.chatItems.replaceAll(newItems)
withChats {
chatItems.replaceAll(newItems)
unreadAfterNewestLoaded.value = 0
}
}

View file

@ -114,6 +114,7 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
CompositionLocalProvider(LocalAppBarHandler provides rememberAppBarHandler(chatInfo.id, keyboardCoversBar = false)) {
when (chatInfo) {
is ChatInfo.Direct, is ChatInfo.Group, is ChatInfo.Local -> {
var groupMembersJob: Job = remember { Job() }
val perChatTheme = remember(chatInfo, CurrentColors.value.base) { if (chatInfo is ChatInfo.Direct) chatInfo.contact.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else if (chatInfo is ChatInfo.Group) chatInfo.groupInfo.uiThemes?.preferredMode(!CurrentColors.value.colors.isLight) else null }
val overrides = if (perChatTheme != null) ThemeManager.currentColors(null, perChatTheme, chatModel.currentUser.value?.uiThemes, appPrefs.themeOverrides.get()) else null
val fullDeleteAllowed = remember(chatInfo) { chatInfo.featureEnabled(ChatFeature.FullDelete) }
@ -220,8 +221,8 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
hideKeyboard(view)
AudioPlayer.stop()
chatModel.chatId.value = null
chatModel.groupMembers.clear()
chatModel.groupMembersIndexes.clear()
chatModel.groupMembers.value = emptyList()
chatModel.groupMembersIndexes.value = emptyMap()
},
info = {
if (ModalManager.end.hasModalsOpen()) {
@ -229,7 +230,8 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
return@ChatLayout
}
hideKeyboard(view)
withBGApi {
groupMembersJob.cancel()
groupMembersJob = scope.launch(Dispatchers.Default) {
// The idea is to preload information before showing a modal because large groups can take time to load all members
var preloadedContactInfo: Pair<ConnectionStats?, Profile?>? = null
var preloadedCode: String? = null
@ -241,6 +243,8 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
setGroupMembers(chatRh, chatInfo.groupInfo, chatModel)
preloadedLink = chatModel.controller.apiGetGroupLink(chatRh, chatInfo.groupInfo.groupId)
}
if (!isActive) return@launch
ModalManager.end.showModalCloseable(true) { close ->
val chatInfo = remember { activeChatInfo }.value
if (chatInfo is ChatInfo.Direct) {
@ -276,7 +280,8 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
},
showMemberInfo = { groupInfo: GroupInfo, member: GroupMember ->
hideKeyboard(view)
withBGApi {
groupMembersJob.cancel()
groupMembersJob = scope.launch(Dispatchers.Default) {
val r = chatModel.controller.apiGroupMemberInfo(chatRh, groupInfo.groupId, member.groupMemberId)
val stats = r?.second
val (_, code) = if (member.memberActive) {
@ -286,6 +291,8 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
member to null
}
setGroupMembers(chatRh, groupInfo, chatModel)
if (!isActive) return@launch
ModalManager.end.closeModals()
ModalManager.end.showModalCloseable(true) { close ->
remember { derivedStateOf { chatModel.getGroupMember(member.groupMemberId) } }.value?.let { mem ->
@ -431,7 +438,7 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
chatModel.getChat(chatId)
},
findModelMember = { memberId ->
chatModel.groupMembers.find { it.id == memberId }
chatModel.groupMembers.value.find { it.id == memberId }
},
setReaction = { cInfo, cItem, add, reaction ->
withBGApi {
@ -451,17 +458,19 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
}
},
showItemDetails = { cInfo, cItem ->
suspend fun loadChatItemInfo(): ChatItemInfo? {
suspend fun loadChatItemInfo(): ChatItemInfo? = coroutineScope {
val ciInfo = chatModel.controller.apiGetChatItemInfo(chatRh, cInfo.chatType, cInfo.apiId, cItem.id)
if (ciInfo != null) {
if (chatInfo is ChatInfo.Group) {
setGroupMembers(chatRh, chatInfo.groupInfo, chatModel)
if (!isActive) return@coroutineScope null
}
}
return ciInfo
ciInfo
}
withBGApi {
var initialCiInfo = loadChatItemInfo() ?: return@withBGApi
groupMembersJob.cancel()
groupMembersJob = scope.launch(Dispatchers.Default) {
var initialCiInfo = loadChatItemInfo() ?: return@launch
ModalManager.end.closeModals()
ModalManager.end.showModalCloseable(endButtons = {
ShareButton {
@ -550,7 +559,9 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
LaunchedEffect(chatInfo.id) {
onComposed(chatInfo.id)
ModalManager.end.closeModals()
chatModel.chatItems.clearAndNotify()
withChats {
chatItems.clearAndNotify()
}
}
}
is ChatInfo.InvalidJSON -> {
@ -561,7 +572,9 @@ fun ChatView(staleChatId: State<String?>, onComposed: suspend (chatId: String) -
LaunchedEffect(chatInfo.id) {
onComposed(chatInfo.id)
ModalManager.end.closeModals()
chatModel.chatItems.clearAndNotify()
withChats {
chatItems.clearAndNotify()
}
}
}
else -> {}

View file

@ -83,7 +83,7 @@ fun AddGroupMembersView(rhId: Long?, groupInfo: GroupInfo, creatingGroup: Boolea
fun getContactsToAdd(chatModel: ChatModel, search: String): List<Contact> {
val s = search.trim().lowercase()
val memberContactIds = chatModel.groupMembers
val memberContactIds = chatModel.groupMembers.value
.filter { it.memberCurrent }
.mapNotNull { it.memberContactId }
return chatModel.chats.value

View file

@ -40,7 +40,7 @@ import chat.simplex.common.views.chat.item.ItemAction
import chat.simplex.common.views.chatlist.*
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
import kotlinx.coroutines.launch
import kotlinx.coroutines.*
const val SMALL_GROUPS_RCPS_MEM_LIMIT: Int = 20
@ -54,6 +54,7 @@ fun ModalData.GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: Strin
if (chat != null && chat.chatInfo is ChatInfo.Group && currentUser != null) {
val groupInfo = chat.chatInfo.groupInfo
val sendReceipts = remember { mutableStateOf(SendReceipts.fromBool(groupInfo.chatSettings.sendRcpts, currentUser.sendRcptsSmallGroups)) }
val scope = rememberCoroutineScope()
GroupChatInfoLayout(
chat,
groupInfo,
@ -64,14 +65,16 @@ fun ModalData.GroupChatInfoView(chatModel: ChatModel, rhId: Long?, chatId: Strin
updateChatSettings(chat.remoteHostId, chat.chatInfo, chatSettings, chatModel)
sendReceipts.value = sendRcpts
},
members = chatModel.groupMembers
members = remember { chatModel.groupMembers }.value
.filter { it.memberStatus != GroupMemberStatus.MemLeft && it.memberStatus != GroupMemberStatus.MemRemoved }
.sortedByDescending { it.memberRole },
developerTools,
groupLink,
addMembers = {
withBGApi {
scope.launch(Dispatchers.Default) {
setGroupMembers(rhId, groupInfo, chatModel)
if (!isActive) return@launch
ModalManager.end.showModalCloseable(true) { close ->
AddGroupMembersView(rhId, groupInfo, false, chatModel, close)
}

View file

@ -98,8 +98,8 @@ fun GroupMemberInfoView(
val memberChat = Chat(remoteHostId = rhId, ChatInfo.Direct(memberContact), chatItems = arrayListOf())
withChats {
addChat(memberChat)
openLoadedChat(memberChat)
}
openLoadedChat(memberChat)
closeAll()
chatModel.setContactNetworkStatus(memberContact, NetworkStatus.Connected())
}

View file

@ -216,21 +216,26 @@ suspend fun openChat(rhId: Long?, chatInfo: ChatInfo) = openChat(rhId, chatInfo.
private suspend fun openChat(rhId: Long?, chatType: ChatType, apiId: Long) =
apiLoadMessages(rhId, chatType, apiId, ChatPagination.Initial(ChatPagination.INITIAL_COUNT), chatModel.chatState)
fun openLoadedChat(chat: Chat) {
chatModel.chatItemStatuses.clear()
chatModel.chatItems.replaceAll(chat.chatItems)
chatModel.chatId.value = chat.chatInfo.id
chatModel.chatState.clear()
suspend fun openLoadedChat(chat: Chat) {
withChats {
chatModel.chatItemStatuses.clear()
chatItems.replaceAll(chat.chatItems)
chatModel.chatId.value = chat.chatInfo.id
chatModel.chatState.clear()
}
}
suspend fun apiFindMessages(ch: Chat, search: String) {
chatModel.chatItems.clearAndNotify()
withChats {
chatItems.clearAndNotify()
}
apiLoadMessages(ch.remoteHostId, ch.chatInfo.chatType, ch.chatInfo.apiId, pagination = ChatPagination.Last(ChatPagination.INITIAL_COUNT), chatModel.chatState, search = search)
}
suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) {
suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatModel) = coroutineScope {
// groupMembers loading can take a long time and if the user already closed the screen, coroutine may be canceled
val groupMembers = chatModel.controller.apiListMembers(rhId, groupInfo.groupId)
val currentMembers = chatModel.groupMembers
val currentMembers = chatModel.groupMembers.value
val newMembers = groupMembers.map { newMember ->
val currentMember = currentMembers.find { it.id == newMember.id }
val currentMemberStats = currentMember?.activeConn?.connectionStats
@ -241,9 +246,8 @@ suspend fun setGroupMembers(rhId: Long?, groupInfo: GroupInfo, chatModel: ChatMo
newMember
}
}
chatModel.groupMembers.clear()
chatModel.groupMembersIndexes.clear()
chatModel.groupMembers.addAll(newMembers)
chatModel.groupMembersIndexes.value = emptyMap()
chatModel.groupMembers.value = newMembers
chatModel.populateGroupMembersIndexes()
}
@ -351,14 +355,14 @@ fun TagListAction(
) {
val userTags = remember { chatModel.userTags }
ItemAction(
stringResource(MR.strings.list_menu),
stringResource(if (chat.chatInfo.chatTags.isNullOrEmpty()) MR.strings.add_to_list else MR.strings.change_list),
painterResource(MR.images.ic_label),
onClick = {
ModalManager.start.showModalCloseable { close ->
if (userTags.value.isEmpty()) {
TagListEditor(rhId = chat.remoteHostId, chat = chat, close = close)
} else {
TagListView(rhId = chat.remoteHostId, chat = chat, close = close)
TagListView(rhId = chat.remoteHostId, chat = chat, close = close, reorderMode = false)
}
}
showMenu.value = false

View file

@ -49,7 +49,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.serialization.json.Json
import kotlin.time.Duration.Companion.seconds
enum class PresetTagKind { FAVORITES, CONTACTS, GROUPS, BUSINESS }
enum class PresetTagKind { FAVORITES, CONTACTS, GROUPS, BUSINESS, NOTES }
sealed class ActiveFilter {
data class PresetTag(val tag: PresetTagKind) : ActiveFilter()
@ -815,13 +815,13 @@ private fun BoxScope.ChatList(searchText: MutableState<TextFieldValue>, listStat
if (oneHandUI.value) {
Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight))) {
Divider()
TagsView()
TagsView(searchText)
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime))
}
} else {
ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink)
TagsView()
TagsView(searchText)
Divider()
}
}
@ -925,25 +925,13 @@ private fun ChatListFeatureCards() {
private val TAG_MIN_HEIGHT = 35.dp
@Composable
private fun TagsView() {
private fun TagsView(searchText: MutableState<TextFieldValue>) {
val userTags = remember { chatModel.userTags }
val presetTags = remember { chatModel.presetTags }
val activeFilter = remember { chatModel.activeChatTagFilter }
val unreadTags = remember { chatModel.unreadTags }
val rhId = chatModel.remoteHostId()
fun showTagList() {
ModalManager.start.showCustomModal { close ->
val editMode = remember { stateGetOrPut("editMode") { false } }
ModalView(close, showClose = true, endButtons = {
TextButton(onClick = { editMode.value = !editMode.value }, modifier = Modifier.clip(shape = CircleShape)) {
Text(stringResource(if (editMode.value) MR.strings.cancel_verb else MR.strings.edit_verb))
}
}) {
TagListView(rhId = rhId, close = close, editMode = editMode)
}
}
}
val rowSizeModifier = Modifier.sizeIn(minHeight = TAG_MIN_HEIGHT * fontSizeSqrtMultiplier)
TagsRow {
@ -953,7 +941,7 @@ private fun TagsView() {
ExpandedTagFilterView(tag)
}
} else {
CollapsedTagsFilterView()
CollapsedTagsFilterView(searchText)
}
}
@ -963,69 +951,75 @@ private fun TagsView() {
else -> false
}
val interactionSource = remember { MutableInteractionSource() }
Row(
rowSizeModifier
.clip(shape = CircleShape)
.combinedClickable(
onClick = {
if (chatModel.activeChatTagFilter.value == ActiveFilter.UserTag(tag)) {
chatModel.activeChatTagFilter.value = null
} else {
chatModel.activeChatTagFilter.value = ActiveFilter.UserTag(tag)
val showMenu = rememberSaveable { mutableStateOf(false) }
val saving = remember { mutableStateOf(false) }
Box {
Row(
rowSizeModifier
.clip(shape = CircleShape)
.combinedClickable(
onClick = {
if (chatModel.activeChatTagFilter.value == ActiveFilter.UserTag(tag)) {
chatModel.activeChatTagFilter.value = null
} else {
chatModel.activeChatTagFilter.value = ActiveFilter.UserTag(tag)
}
},
onLongClick = { showMenu.value = true },
interactionSource = interactionSource,
indication = LocalIndication.current,
enabled = !saving.value
)
.onRightClick { showMenu.value = true }
.padding(4.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
if (tag.chatTagEmoji != null) {
ReactionIcon(tag.chatTagEmoji, fontSize = 14.sp)
} else {
Icon(
painterResource(if (current) MR.images.ic_label_filled else MR.images.ic_label),
null,
Modifier.size(18.sp.toDp()),
tint = if (current) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground
)
}
Spacer(Modifier.width(4.dp))
Box {
val badgeText = if ((unreadTags[tag.chatTagId] ?: 0) > 0) "" else ""
val invisibleText = buildAnnotatedString {
append(tag.chatTagText)
withStyle(SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.SemiBold)) {
append(badgeText)
}
},
onLongClick = { showTagList() },
interactionSource = interactionSource,
indication = LocalIndication.current
)
.onRightClick { showTagList() }
.padding(4.dp),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
if (tag.chatTagEmoji != null) {
ReactionIcon(tag.chatTagEmoji, fontSize = 14.sp)
} else {
Icon(
painterResource(if (current) MR.images.ic_label_filled else MR.images.ic_label),
null,
Modifier.size(18.sp.toDp()),
tint = if (current) MaterialTheme.colors.primary else MaterialTheme.colors.onBackground
)
}
Spacer(Modifier.width(4.dp))
Box {
val badgeText = if ((unreadTags[tag.chatTagId] ?: 0) > 0) "" else ""
val invisibleText = buildAnnotatedString {
append(tag.chatTagText)
withStyle(SpanStyle(fontSize = 12.sp, fontWeight = FontWeight.SemiBold)) {
append(badgeText)
}
}
Text(
text = invisibleText,
fontWeight = FontWeight.Medium,
fontSize = 15.sp,
color = Color.Transparent,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
// Visible text with styles
val visibleText = buildAnnotatedString {
append(tag.chatTagText)
withStyle(SpanStyle(fontSize = 12.5.sp, color = MaterialTheme.colors.primary)) {
append(badgeText)
Text(
text = invisibleText,
fontWeight = FontWeight.Medium,
fontSize = 15.sp,
color = Color.Transparent,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
// Visible text with styles
val visibleText = buildAnnotatedString {
append(tag.chatTagText)
withStyle(SpanStyle(fontSize = 12.5.sp, color = MaterialTheme.colors.primary)) {
append(badgeText)
}
}
Text(
text = visibleText,
fontWeight = if (current) FontWeight.Medium else FontWeight.Normal,
fontSize = 15.sp,
color = if (current) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
Text(
text = visibleText,
fontWeight = if (current) FontWeight.Medium else FontWeight.Normal,
fontSize = 15.sp,
color = if (current) MaterialTheme.colors.primary else MaterialTheme.colors.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
TagsDropdownMenu(rhId, tag, showMenu, saving)
}
}
val plusClickModifier = Modifier
@ -1051,23 +1045,8 @@ private fun TagsView() {
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun TagsRow(content: @Composable() (() -> Unit)) {
if (appPlatform.isAndroid) {
Row(
modifier = Modifier
.padding(horizontal = 14.dp)
.horizontalScroll(rememberScrollState()),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(2.dp)
) {
content()
}
} else {
FlowRow(modifier = Modifier.padding(horizontal = 14.dp)) { content() }
}
}
expect fun TagsRow(content: @Composable() (() -> Unit))
@Composable
private fun ExpandedTagFilterView(tag: PresetTagKind) {
@ -1076,12 +1055,12 @@ private fun ExpandedTagFilterView(tag: PresetTagKind) {
is ActiveFilter.PresetTag -> af.tag == tag
else -> false
}
val rowSizeModifier = Modifier.sizeIn(minHeight = TAG_MIN_HEIGHT * fontSizeSqrtMultiplier)
val (icon, text) = presetTagLabel(tag, active)
val color = if (active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary
Row(
modifier = rowSizeModifier
modifier = Modifier
.sizeIn(minHeight = TAG_MIN_HEIGHT * fontSizeSqrtMultiplier)
.clip(shape = CircleShape)
.clickable {
if (activeFilter.value == ActiveFilter.PresetTag(tag)) {
@ -1121,7 +1100,7 @@ private fun ExpandedTagFilterView(tag: PresetTagKind) {
@Composable
private fun CollapsedTagsFilterView() {
private fun CollapsedTagsFilterView(searchText: MutableState<TextFieldValue>) {
val activeFilter = remember { chatModel.activeChatTagFilter }
val presetTags = remember { chatModel.presetTags }
val showMenu = remember { mutableStateOf(false) }
@ -1145,7 +1124,7 @@ private fun CollapsedTagsFilterView() {
painterResource(icon),
stringResource(text),
Modifier.size(18.sp.toDp()),
tint = MaterialTheme.colors.secondary
tint = MaterialTheme.colors.primary
)
} else {
Icon(
@ -1155,20 +1134,26 @@ private fun CollapsedTagsFilterView() {
)
}
DefaultDropdownMenu(showMenu = showMenu) {
if (selectedPresetTag != null) {
val onCloseMenuAction = remember { mutableStateOf<(() -> Unit)>({}) }
DefaultDropdownMenu(showMenu = showMenu, onClosed = onCloseMenuAction) {
if (activeFilter.value != null || searchText.value.text.isNotBlank()) {
ItemAction(
stringResource(MR.strings.chat_list_all),
painterResource(MR.images.ic_menu),
onClick = {
chatModel.activeChatTagFilter.value = null
onCloseMenuAction.value = {
searchText.value = TextFieldValue()
chatModel.activeChatTagFilter.value = null
onCloseMenuAction.value = {}
}
showMenu.value = false
}
)
}
PresetTagKind.entries.forEach { tag ->
if ((presetTags[tag] ?: 0) > 0) {
ItemPresetFilterAction(tag, tag == selectedPresetTag, showMenu)
ItemPresetFilterAction(tag, tag == selectedPresetTag, showMenu, onCloseMenuAction)
}
}
}
@ -1179,14 +1164,19 @@ private fun CollapsedTagsFilterView() {
fun ItemPresetFilterAction(
presetTag: PresetTagKind,
active: Boolean,
showMenu: MutableState<Boolean>
showMenu: MutableState<Boolean>,
onCloseMenuAction: MutableState<(() -> Unit)>
) {
val (icon, text) = presetTagLabel(presetTag, active)
ItemAction(
stringResource(text),
painterResource(icon),
color = if (active) MaterialTheme.colors.primary else Color.Unspecified,
onClick = {
chatModel.activeChatTagFilter.value = ActiveFilter.PresetTag(presetTag)
onCloseMenuAction.value = {
chatModel.activeChatTagFilter.value = ActiveFilter.PresetTag(presetTag)
onCloseMenuAction.value = {}
}
showMenu.value = false
}
)
@ -1205,26 +1195,18 @@ fun filteredChats(
} else {
val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase()
if (s.isEmpty())
chats.filter { chat -> !chat.chatInfo.chatDeleted && chatContactType(chat) != ContactType.CARD && filtered(chat, activeFilter) }
chats.filter { chat -> chat.id == chatModel.chatId.value || (!chat.chatInfo.chatDeleted && !chat.chatInfo.contactCard && filtered(chat, activeFilter)) }
else {
chats.filter { chat ->
when (val cInfo = chat.chatInfo) {
is ChatInfo.Direct -> chatContactType(chat) != ContactType.CARD && !chat.chatInfo.chatDeleted && (
if (s.isEmpty()) {
chat.id == chatModel.chatId.value || filtered(chat, activeFilter)
} else {
cInfo.anyNameContains(s)
})
is ChatInfo.Group -> if (s.isEmpty()) {
chat.id == chatModel.chatId.value || filtered(chat, activeFilter) || cInfo.groupInfo.membership.memberStatus == GroupMemberStatus.MemInvited
} else {
cInfo.anyNameContains(s)
chat.id == chatModel.chatId.value ||
when (val cInfo = chat.chatInfo) {
is ChatInfo.Direct -> !cInfo.contact.chatDeleted && !chat.chatInfo.contactCard && cInfo.anyNameContains(s)
is ChatInfo.Group -> cInfo.anyNameContains(s)
is ChatInfo.Local -> cInfo.anyNameContains(s)
is ChatInfo.ContactRequest -> cInfo.anyNameContains(s)
is ChatInfo.ContactConnection -> cInfo.contactConnection.localAlias.lowercase().contains(s)
is ChatInfo.InvalidJSON -> false
}
is ChatInfo.Local -> s.isEmpty() || cInfo.anyNameContains(s)
is ChatInfo.ContactRequest -> s.isEmpty() || cInfo.anyNameContains(s)
is ChatInfo.ContactConnection -> (s.isNotEmpty() && cInfo.anyNameContains(s)) || (s.isEmpty() && chat.id == chatModel.chatId.value)
is ChatInfo.InvalidJSON -> chat.id == chatModel.chatId.value
}
}
}
}
@ -1256,6 +1238,10 @@ fun presetTagMatchesChat(tag: PresetTagKind, chatInfo: ChatInfo): Boolean =
is ChatInfo.Group -> chatInfo.groupInfo.businessChat?.chatType == BusinessChatType.Business
else -> false
}
PresetTagKind.NOTES -> when (chatInfo) {
is ChatInfo.Local -> !chatInfo.noteFolder.chatDeleted
else -> false
}
}
private fun presetTagLabel(tag: PresetTagKind, active: Boolean): Pair<ImageResource, StringResource> =
@ -1264,6 +1250,7 @@ private fun presetTagLabel(tag: PresetTagKind, active: Boolean): Pair<ImageResou
PresetTagKind.CONTACTS -> (if (active) MR.images.ic_person_filled else MR.images.ic_person) to MR.strings.chat_list_contacts
PresetTagKind.GROUPS -> (if (active) MR.images.ic_group_filled else MR.images.ic_group) to MR.strings.chat_list_groups
PresetTagKind.BUSINESS -> (if (active) MR.images.ic_work_filled else MR.images.ic_work) to MR.strings.chat_list_businesses
PresetTagKind.NOTES -> (if (active) MR.images.ic_folder_closed_filled else MR.images.ic_folder_closed) to MR.strings.chat_list_notes
}
fun scrollToBottom(scope: CoroutineScope, listState: LazyListState) {

View file

@ -5,8 +5,7 @@ import SectionDivider
import SectionItemView
import TextIconSpaced
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.itemsIndexed
@ -44,12 +43,7 @@ import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@Composable
fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, editMode: MutableState<Boolean> = remember { mutableStateOf(false) }) {
if (remember { editMode }.value) {
BackHandler {
editMode.value = false
}
}
fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, reorderMode: Boolean) {
val userTags = remember { chatModel.userTags }
val oneHandUI = remember { appPrefs.oneHandUI.state }
val listState = LocalAppBarHandler.current?.listState ?: rememberLazyListState()
@ -77,7 +71,7 @@ fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, editMode: Mu
val topPaddingToContent = topPaddingToContent(false)
LazyColumnWithScrollBar(
modifier = if (editMode.value) Modifier.dragContainer(dragDropState) else Modifier,
modifier = if (reorderMode) Modifier.dragContainer(dragDropState) else Modifier,
contentPadding = PaddingValues(
top = if (oneHandUI.value) WindowInsets.statusBars.asPaddingValues().calculateTopPadding() else topPaddingToContent,
bottom = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else 0.dp
@ -97,7 +91,7 @@ fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, editMode: Mu
}
}
if (oneHandUI.value && !editMode.value) {
if (oneHandUI.value && !reorderMode) {
item {
CreateList()
}
@ -111,15 +105,14 @@ fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, editMode: Mu
backgroundColor = if (isDragging) colors.surface else Color.Unspecified
) {
Column {
val showMenu = remember { mutableStateOf(false) }
val selected = chatTagIds.value.contains(tag.chatTagId)
Row(
Modifier
.fillMaxWidth()
.sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT)
.combinedClickable(
enabled = !saving.value,
.clickable(
enabled = !saving.value && !reorderMode,
onClick = {
if (chat == null) {
ModalManager.start.showModalCloseable { close ->
@ -139,13 +132,7 @@ fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, editMode: Mu
})
}
},
onLongClick = if (editMode.value) null else {
{ showMenu.value = true }
},
interactionSource = remember { MutableInteractionSource() },
indication = LocalIndication.current
)
.onRightClick { showMenu.value = true }
.padding(PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_MIN_SECTION_ITEM_PADDING_VERTICAL)),
verticalAlignment = Alignment.CenterVertically
) {
@ -163,21 +150,17 @@ fun TagListView(rhId: Long?, chat: Chat? = null, close: () -> Unit, editMode: Mu
if (selected) {
Spacer(Modifier.weight(1f))
Icon(painterResource(MR.images.ic_done_filled), null, Modifier.size(20.dp), tint = MaterialTheme.colors.onBackground)
} else if (editMode.value) {
} else if (reorderMode) {
Spacer(Modifier.weight(1f))
Icon(painterResource(MR.images.ic_drag_handle), null, Modifier.size(20.dp), tint = MaterialTheme.colors.secondary)
}
DefaultDropdownMenu(showMenu, dropdownMenuItems = {
EditTagAction(rhId, tag, showMenu)
DeleteTagAction(rhId, tag, showMenu, saving)
})
}
SectionDivider()
}
}
}
}
if (!oneHandUI.value && !editMode.value) {
if (!oneHandUI.value && !reorderMode) {
item {
CreateList()
}
@ -279,7 +262,7 @@ fun ModalData.TagListEditor(
SectionItemView(click = { if (tagId == null) createTag() else updateTag() }, disabled = disabled) {
Text(
generalGetString(if (chat != null) MR.strings.add_to_list else if (tagId == null) MR.strings.create_list else MR.strings.save_list),
generalGetString(if (chat != null) MR.strings.add_to_list else MR.strings.save_list),
color = if (disabled) colors.secondary else colors.primary
)
}
@ -309,6 +292,15 @@ fun ModalData.TagListEditor(
}
}
@Composable
fun TagsDropdownMenu(rhId: Long?, tag: ChatTag, showMenu: MutableState<Boolean>, saving: MutableState<Boolean>) {
DefaultDropdownMenu(showMenu, dropdownMenuItems = {
EditTagAction(rhId, tag, showMenu)
DeleteTagAction(rhId, tag, showMenu, saving)
ChangeOrderTagAction(rhId, showMenu)
})
}
@Composable
private fun DeleteTagAction(rhId: Long?, tag: ChatTag, showMenu: MutableState<Boolean>, saving: MutableState<Boolean>) {
ItemAction(
@ -343,6 +335,21 @@ private fun EditTagAction(rhId: Long?, tag: ChatTag, showMenu: MutableState<Bool
)
}
@Composable
private fun ChangeOrderTagAction(rhId: Long?, showMenu: MutableState<Boolean>) {
ItemAction(
stringResource(MR.strings.change_order_chat_list_menu_action),
painterResource(MR.images.ic_drag_handle),
onClick = {
showMenu.value = false
ModalManager.start.showModalCloseable { close ->
TagListView(rhId = rhId, close = close, reorderMode = true)
}
},
color = MenuTextColor
)
}
@Composable
expect fun ChatTagInput(name: MutableState<String>, showError: State<Boolean>, emoji: MutableState<String?>)

View file

@ -21,7 +21,9 @@ fun onRequestAccepted(chat: Chat) {
if (chatInfo is ChatInfo.Direct) {
ModalManager.start.closeModals()
if (chatInfo.contact.sndReady) {
openLoadedChat(chat)
withApi {
openLoadedChat(chat)
}
}
}
}

View file

@ -528,9 +528,9 @@ fun deleteChatDatabaseFilesAndState() {
// Clear sensitive data on screen just in case ModalManager will fail to prevent hiding its modals while database encrypts itself
chatModel.chatId.value = null
chatModel.chatItems.clearAndNotify()
withLongRunningApi {
withChats {
chatItems.clearAndNotify()
chats.clear()
popChatCollector.clear()
}

View file

@ -5,8 +5,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
@ -16,6 +15,7 @@ fun DefaultDropdownMenu(
showMenu: MutableState<Boolean>,
modifier: Modifier = Modifier,
offset: DpOffset = DpOffset(0.dp, 0.dp),
onClosed: State<() -> Unit> = remember { mutableStateOf({}) },
dropdownMenuItems: (@Composable () -> Unit)?
) {
MaterialTheme(
@ -31,6 +31,11 @@ fun DefaultDropdownMenu(
offset = offset,
) {
dropdownMenuItems?.invoke()
DisposableEffect(Unit) {
onDispose {
onClosed.value()
}
}
}
}
}

View file

@ -44,7 +44,7 @@ fun AddGroupView(chatModel: ChatModel, rh: RemoteHostInfo?, close: () -> Unit, c
if (groupInfo != null) {
withChats {
updateGroup(rhId = rhId, groupInfo)
chatModel.chatItems.clearAndNotify()
chatItems.clearAndNotify()
chatModel.chatItemStatuses.clear()
chatModel.chatId.value = groupInfo.id
}

View file

@ -421,6 +421,7 @@
<string name="chat_list_contacts">Contacts</string>
<string name="chat_list_groups">Groups</string>
<string name="chat_list_businesses">Businesses</string>
<string name="chat_list_notes">Notes</string>
<string name="chat_list_all">All</string>
<string name="chat_list_add_list">Add list</string>
@ -644,6 +645,7 @@
<!-- Tags - ChatListNavLinkView.kt -->
<string name="create_list">Create list</string>
<string name="add_to_list">Add to list</string>
<string name="change_list">Change list</string>
<string name="save_list">Save list</string>
<string name="list_name_field_placeholder">List name...</string>
<string name="duplicated_list_error">List name and emoji should be different for all lists.</string>
@ -651,6 +653,7 @@
<string name="delete_chat_list_question">Delete list?</string>
<string name="delete_chat_list_warning">All chats will be removed from the list %s, and the list deleted</string>
<string name="edit_chat_list_menu_action">Edit</string>
<string name="change_order_chat_list_menu_action">Change order</string>
<!-- Pending contact connection alert dialogues -->
<string name="you_invited_a_contact">You invited a contact</string>

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M165-170q-30.94 0-52.97-22.03Q90-214.06 90-245v-470q0-30.94 22.03-52.97Q134.06-790 165-790h209q15.14 0 28.87 5.74Q416.59-778.52 427-768l53 53h315q30.94 0 52.97 22.03Q870-670.94 870-640v395q0 30.94-22.03 52.97Q825.94-170 795-170H165Zm0-75h630v-395H449l-75-75H165v470Zm0 0v-470 470Z"/></svg>

After

Width:  |  Height:  |  Size: 386 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="22" viewBox="0 -960 960 960" width="22"><path d="M165-170q-30.94 0-52.97-22.03Q90-214.06 90-245v-470q0-30.94 22.03-52.97Q134.06-790 165-790h209q15.14 0 28.87 5.74Q416.59-778.52 427-768l53 53h315q30.94 0 52.97 22.03Q870-670.94 870-640v395q0 30.94-22.03 52.97Q825.94-170 795-170H165Z"/></svg>

After

Width:  |  Height:  |  Size: 338 B

View file

@ -14,6 +14,7 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.*
import chat.simplex.common.model.*
import chat.simplex.common.model.ChatModel.withChats
import chat.simplex.common.platform.*
import chat.simplex.common.ui.theme.DEFAULT_START_MODAL_WIDTH
import chat.simplex.common.ui.theme.SimpleXTheme
@ -55,8 +56,12 @@ fun showApp() {
// Better to not close fullscreen since it can contain passcode
} else {
// The last possible cause that can be closed
chatModel.chatId.value = null
chatModel.chatItems.clearAndNotify()
withApi {
withChats {
chatModel.chatId.value = null
chatItems.clearAndNotify()
}
}
}
chatModel.activeCall.value?.let {
withBGApi {

View file

@ -21,6 +21,12 @@ import chat.simplex.res.MR
import dev.icerock.moko.resources.compose.painterResource
import dev.icerock.moko.resources.compose.stringResource
@OptIn(ExperimentalLayoutApi::class)
@Composable
actual fun TagsRow(content: @Composable() (() -> Unit)) {
FlowRow(modifier = Modifier.padding(horizontal = 14.dp)) { content() }
}
@Composable
actual fun ActiveCallInteractiveArea(call: Call) {
val showMenu = remember { mutableStateOf(false) }