From a57a2c277df524d4210de78648f00fc137cc1736 Mon Sep 17 00:00:00 2001 From: Diogo Cunha Date: Sun, 14 Jul 2024 22:19:43 +0100 Subject: [PATCH] (early draft) android, desktop: new chat sheet --- .../common/views/chatlist/ChatListView.kt | 5 +- .../views/contacts/ContactListNavView.kt | 105 ++++++ .../views/contacts/ContactPreviewView.kt | 125 +++++++ .../common/views/contacts/ContactsView.kt | 311 ++++++++++++++++++ .../common/views/newchat/NewChatSheet.kt | 83 ++++- .../commonMain/resources/MR/base/strings.xml | 6 + 6 files changed, 632 insertions(+), 3 deletions(-) create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactPreviewView.kt create mode 100644 apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactsView.kt 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 332c1d070e..fd43b52cc8 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 @@ -101,7 +101,10 @@ fun ChatListView(chatModel: ChatModel, settingsState: SettingsViewState, setPerf FloatingActionButton( onClick = { if (!stopped) { - if (newChatSheetState.value.isVisible()) hideNewChatSheet(true) else showNewChatSheet() + ModalManager.start.closeModals() + ModalManager.start.showModalCloseable{ + NewChatView(rh = chatModel.currentRemoteHost.value) + } } }, Modifier.padding(end = DEFAULT_PADDING - 16.dp + endPadding, bottom = bottom).size(AppBarHeight * fontSizeSqrtMultiplier), diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt new file mode 100644 index 0000000000..549d4bdb47 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactListNavView.kt @@ -0,0 +1,105 @@ +package chat.simplex.common.views.contacts + +import androidx.compose.runtime.* +import androidx.compose.ui.graphics.Color +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import chat.simplex.common.model.* +import chat.simplex.common.platform.* +import chat.simplex.common.views.chat.* +import chat.simplex.common.views.chat.item.ItemAction +import chat.simplex.common.views.chatlist.* +import chat.simplex.common.views.helpers.* +import chat.simplex.res.MR +import kotlinx.coroutines.delay + +@Composable +fun ContactListNavLinkView(chat: Chat, nextChatSelected: State, oneHandUI: State) { + val showMenu = remember { mutableStateOf(false) } + val disabled = chatModel.chatRunning.value == false || chatModel.deletedChats.value.contains(chat.remoteHostId to chat.chatInfo.id) + LaunchedEffect(chat.id) { + showMenu.value = false + delay(500L) + } + val selectedChat = remember(chat.id) { derivedStateOf { chat.id == chatModel.chatId.value } } + + when (chat.chatInfo) { + is ChatInfo.Direct -> { + ChatListNavLinkLayout( + chatLinkPreview = { + tryOrShowError("${chat.id}ContactListNavLink", error = { ErrorChatListItem() }) { + ContactPreviewView(chat, disabled) + } + }, + click = { + directChatAction(chat.remoteHostId, chat.chatInfo.contact, chatModel) + ModalManager.start.closeModals() + }, + dropdownMenuItems = { + tryOrShowError("${chat.id}ContactListNavLinkDropdown", error = {}) { + ContactMenuItems(chat, chat.chatInfo.contact, chatModel, showMenu) + } + }, + showMenu, + disabled, + selectedChat, + nextChatSelected, + oneHandUI + ) + } + is ChatInfo.ContactRequest -> { + ChatListNavLinkLayout( + chatLinkPreview = { + tryOrShowError("${chat.id}ContactListNavLink", error = { ErrorChatListItem() }) { + ContactPreviewView(chat, disabled) + } + }, + click = { contactRequestAlertDialog(chat.remoteHostId, chat.chatInfo, chatModel) }, + dropdownMenuItems = { + tryOrShowError("${chat.id}ContactListNavLinkDropdown", error = {}) { + ContactRequestMenuItems(chat.remoteHostId, chat.chatInfo, chatModel, showMenu) + } + }, + showMenu, + disabled, + selectedChat, + nextChatSelected, + oneHandUI + ) + } + else -> {} + } +} + +@Composable +fun ContactMenuItems(chat: Chat, contact: Contact, chatModel: ChatModel, showMenu: MutableState) { + if (contact.activeConn != null) { + ToggleFavoritesChatAction(chat, chatModel, chat.chatInfo.chatSettings?.favorite == true, showMenu) + } + DeleteContactAction(chat, chatModel, showMenu) +} + +@Composable +fun ToggleFavoritesChatAction(chat: Chat, chatModel: ChatModel, favorite: Boolean, showMenu: MutableState) { + ItemAction( + if (favorite) stringResource(MR.strings.unfavorite_chat) else stringResource(MR.strings.favorite_chat), + if (favorite) painterResource(MR.images.ic_star_off) else painterResource(MR.images.ic_star), + onClick = { + toggleChatFavorite(chat, !favorite, chatModel) + showMenu.value = false + } + ) +} + +@Composable +fun DeleteContactAction(chat: Chat, chatModel: ChatModel, showMenu: MutableState) { + ItemAction( + stringResource(MR.strings.delete_contact_menu_action), + painterResource(MR.images.ic_delete), + onClick = { + deleteContactDialog(chat, chatModel) + showMenu.value = false + }, + color = Color.Red + ) +} \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactPreviewView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactPreviewView.kt new file mode 100644 index 0000000000..38d99e9311 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactPreviewView.kt @@ -0,0 +1,125 @@ +package chat.simplex.common.views.contacts + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +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 dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.model.* +import chat.simplex.common.platform.chatModel +import chat.simplex.common.ui.theme.DEFAULT_SPACE_AFTER_ICON +import chat.simplex.res.MR + +@Composable +fun ContactPreviewView( + chat: Chat, + disabled: Boolean, +) { + val cInfo = chat.chatInfo + + @Composable + fun inactiveIcon() { + Icon( + painterResource(MR.images.ic_cancel_filled), + stringResource(MR.strings.icon_descr_group_inactive), + Modifier.size(18.dp).background(MaterialTheme.colors.background, CircleShape), + tint = MaterialTheme.colors.secondary + ) + } + + @Composable + fun chatPreviewImageOverlayIcon() { + when (cInfo) { + is ChatInfo.Direct -> + if (!cInfo.contact.active) { + inactiveIcon() + } + + else -> {} + } + } + + @Composable + fun VerifiedIcon() { + Icon(painterResource(MR.images.ic_verified_user), null, Modifier.size(19.dp).padding(end = 3.dp, top = 1.dp), tint = MaterialTheme.colors.secondary) + } + + @Composable + fun chatPreviewTitle() { + val deleting by remember(disabled, chat.id) { mutableStateOf(chatModel.deletedChats.value.contains(chat.remoteHostId to chat.chatInfo.id)) } + when (cInfo) { + is ChatInfo.Direct -> + Row(verticalAlignment = Alignment.CenterVertically) { + if (cInfo.contact.verified) { + VerifiedIcon() + } + Text( + cInfo.chatViewName, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = if (deleting) MaterialTheme.colors.secondary else Color.Unspecified + ) + } + is ChatInfo.ContactRequest -> + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + cInfo.chatViewName, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = Color.Unspecified + ) + } + else -> {} + } + } + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + Box(contentAlignment = Alignment.BottomEnd) { + ChatInfoImage(cInfo, size = 42.dp) + Box(Modifier.padding(end = 2.dp, bottom = 2.dp)) { + chatPreviewImageOverlayIcon() + } + } + + Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON)) + + Box(modifier = Modifier.weight(10f, fill = true)) { + chatPreviewTitle() + } + + Spacer(Modifier.fillMaxWidth().weight(1f)) + + if (chat.chatInfo.chatSettings?.favorite == true) { + Icon( + painterResource(MR.images.ic_star_filled), + contentDescription = generalGetString(MR.strings.favorite_chat), + tint = MaterialTheme.colors.secondary, + modifier = Modifier + .size(17.dp) + ) + if (chat.chatInfo.incognito) { + Spacer(Modifier.width(DEFAULT_SPACE_AFTER_ICON)) + } + } + + if (chat.chatInfo.incognito) { + Icon( + painterResource(MR.images.ic_theater_comedy), + contentDescription = null, + tint = MaterialTheme.colors.secondary, + modifier = Modifier + .size(21.dp) + ) + } + } +} \ No newline at end of file diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactsView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactsView.kt new file mode 100644 index 0000000000..85c891d5c9 --- /dev/null +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/contacts/ContactsView.kt @@ -0,0 +1,311 @@ +package chat.simplex.common.views.contacts + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import chat.simplex.common.model.Chat +import chat.simplex.common.model.ChatController +import chat.simplex.common.model.ChatInfo +import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.ChatType +import chat.simplex.common.model.ContactStatus +import chat.simplex.common.model.Format +import chat.simplex.common.model.SharedPreference +import chat.simplex.common.platform.BackHandler +import chat.simplex.common.platform.ColumnWithScrollBar +import chat.simplex.common.platform.LazyColumnWithScrollBar +import chat.simplex.common.platform.LocalMultiplatformView +import chat.simplex.common.platform.Log +import chat.simplex.common.platform.TAG +import chat.simplex.common.platform.appPlatform +import chat.simplex.common.platform.chatModel +import chat.simplex.common.platform.getKeyboardState +import chat.simplex.common.platform.hideKeyboard +import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.helpers.* +import chat.simplex.common.views.newchat.strHasSingleSimplexLink +import chat.simplex.res.MR +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch + + +enum class ContactType { + RECENT, NEW, REMOVED +} + +private fun contactChats(c: List, contactType: ContactType): List { + return c.filter { chat -> + when (val cInfo = chat.chatInfo) { + is ChatInfo.Direct -> when (contactType) { + ContactType.REMOVED -> cInfo.contact.contactStatus == ContactStatus.DeletedByUser || cInfo.contact.contactStatus == ContactStatus.Deleted + ContactType.NEW -> false + ContactType.RECENT -> cInfo.contact.contactStatus != ContactStatus.DeletedByUser && cInfo.contact.contactStatus != ContactStatus.Deleted + } + is ChatInfo.ContactRequest -> when (contactType) { + ContactType.NEW -> true + else -> false + } + else -> false + } + } +} + +@Composable +fun ContactTypeTabs(contactActions: @Composable () -> Unit, searchText: MutableState) { + val scope = rememberCoroutineScope() + val selectedContactType = + remember { mutableStateOf(ContactType.RECENT) } + + val contactTypeTabTitles = ContactType.entries.map { + when (it) { + ContactType.NEW -> + stringResource(MR.strings.contact_type_new) + + ContactType.RECENT -> + stringResource(MR.strings.contact_type_recent) + + ContactType.REMOVED -> + stringResource(MR.strings.contact_type_removed) + } + } + + val contactTypePagerState = rememberPagerState( + initialPage = selectedContactType.value.ordinal, + initialPageOffsetFraction = 0f + ) { ContactType.entries.size } + + KeyChangeEffect(contactTypePagerState.currentPage) { + selectedContactType.value = ContactType.values()[contactTypePagerState.currentPage] + } + val listState = rememberLazyListState(lazyListState.first, lazyListState.second) + ContactsSearchBar(listState, searchText) + contactActions() + + TabRow( + selectedTabIndex = contactTypePagerState.currentPage, + backgroundColor = Color.Transparent, + contentColor = MaterialTheme.colors.primary, + ) { + contactTypeTabTitles.forEachIndexed { index, it -> + Tab( + selected = contactTypePagerState.currentPage == index, + onClick = { + scope.launch { + contactTypePagerState.animateScrollToPage(index) + } + }, + text = { Text(it, fontSize = 13.sp) }, + selectedContentColor = MaterialTheme.colors.primary, + unselectedContentColor = MaterialTheme.colors.secondary, + ) + } + } + + HorizontalPager( + state = contactTypePagerState, + Modifier.fillMaxSize(), + verticalAlignment = Alignment.Top, + userScrollEnabled = appPlatform.isAndroid + ) { index -> + val contactType = when (index) { + ContactType.NEW.ordinal -> ContactType.NEW + ContactType.RECENT.ordinal -> ContactType.RECENT + ContactType.REMOVED.ordinal -> ContactType.REMOVED + else -> ContactType.RECENT + } + + ContactsList(listState = listState, chatModel = chatModel, searchText = searchText, contactType = contactType) + } +} + + +@Composable +fun ContactsSearchBar(listState: LazyListState, searchText: MutableState) { + Row(verticalAlignment = Alignment.CenterVertically) { + val focusRequester = remember { FocusRequester() } + var focused by remember { mutableStateOf(false) } + Icon(painterResource(MR.images.ic_search), null, Modifier.padding(horizontal = DEFAULT_PADDING_HALF), tint = MaterialTheme.colors.secondary) + SearchTextField( + Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester), + placeholder = stringResource(MR.strings.search_verb), + alwaysVisible = true, + searchText = searchText, + trailingContent = null, + ) { + searchText.value = searchText.value.copy(it) + } + val hasText = remember { derivedStateOf { searchText.value.text.isNotEmpty() } } + if (hasText.value) { + val hideSearchOnBack: () -> Unit = { searchText.value = TextFieldValue() } + BackHandler(onBack = hideSearchOnBack) + KeyChangeEffect(chatModel.currentRemoteHost.value) { + hideSearchOnBack() + } + } else { + Row { + val padding = if (appPlatform.isDesktop) 0.dp else 7.dp + if (chatModel.chats.size > 0) { + ToggleFilterButton() + } + Spacer(Modifier.width(padding)) + } + } + val focusManager = LocalFocusManager.current + val keyboardState = getKeyboardState() + LaunchedEffect(keyboardState.value) { + if (keyboardState.value == KeyboardState.Closed && focused) { + focusManager.clearFocus() + } + } + LaunchedEffect(Unit) { + snapshotFlow { searchText.value.text } + .distinctUntilChanged() + .collect { + if (it.isNotEmpty()) { + focusRequester.requestFocus() + } else if (listState.layoutInfo.totalItemsCount > 0) { + listState.scrollToItem(0) + } + } + } + } +} + +@Composable +fun ToggleFilterButton() { + val pref = remember { ChatController.appPrefs.showUnreadAndFavorites } + IconButton(onClick = { pref.set(!pref.get()) }) { + val sp16 = with(LocalDensity.current) { 16.sp.toDp() } + Icon( + painterResource(MR.images.ic_filter_list), + null, + tint = if (pref.state.value) MaterialTheme.colors.background else MaterialTheme.colors.secondary, + modifier = Modifier + .padding(3.dp) + .background(color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50)) + .border(width = 1.dp, color = if (pref.state.value) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50)) + .padding(3.dp) + .size(sp16) + ) + } +} + +private var lazyListState = 0 to 0 + + +@Composable +fun ContactsList(listState: LazyListState, chatModel: ChatModel, searchText: MutableState, contactType: ContactType) { + val oneHandUI = remember { chatModel.controller.appPrefs.oneHandUI } + DisposableEffect(Unit) { + onDispose { + lazyListState = + listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset + } + } + val showUnreadAndFavorites = + remember { ChatController.appPrefs.showUnreadAndFavorites.state }.value + val allChats = remember { contactChats(chatModel.chats, contactType) } + + val filteredContactChats = filteredContactChats( + showUnreadAndFavorites = showUnreadAndFavorites, + searchText = searchText.value.text, + contactChats = allChats + ) + + if (filteredContactChats.isEmpty() && allChats.isNotEmpty()) { + Column(Modifier.fillMaxSize().padding(DEFAULT_PADDING)) { + Box(Modifier.fillMaxWidth() ,contentAlignment = Alignment.Center) { + Text( + generalGetString(MR.strings.no_filtered_contacts), + color = MaterialTheme.colors.secondary + ) + } + } + } else { + LazyColumnWithScrollBar( + Modifier.fillMaxWidth(), + listState + ) { + itemsIndexed(filteredContactChats) { index, chat -> + val nextChatSelected = remember(chat.id, filteredContactChats) { + derivedStateOf { + chatModel.chatId.value != null && filteredContactChats.getOrNull(index + 1)?.id == chatModel.chatId.value + } + } + ContactListNavLinkView(chat, nextChatSelected, oneHandUI.state) + } + } + } +} + +private fun filterChat(chat: Chat, searchText: String, showUnreadAndFavorites: Boolean): Boolean { + var meetsPredicate = true; + val s = searchText.trim().lowercase() + val cInfo = chat.chatInfo + + if (searchText.isNotEmpty()) { + meetsPredicate = viewNameContains(cInfo, s) || + if (cInfo is ChatInfo.Direct) (cInfo.contact.profile.displayName.lowercase().contains(s) || + cInfo.contact.fullName.lowercase().contains(s)) else false + } + + if (showUnreadAndFavorites) { + meetsPredicate = meetsPredicate && (cInfo.chatSettings?.favorite ?: false) + } + + return meetsPredicate; +} + +private fun filteredContactChats( + showUnreadAndFavorites: Boolean, + searchText: String, + contactChats: List +): List { + val s = searchText.trim().lowercase() + + return contactChats + .filter { chat -> filterChat( + chat = chat, + searchText = searchText, + showUnreadAndFavorites = showUnreadAndFavorites) } + .sortedByDescending { it.chatInfo.chatTs } +} + +private fun viewNameContains(cInfo: ChatInfo, s: String): Boolean = + cInfo.chatViewName.lowercase().contains(s.lowercase()) \ No newline at end of file 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 58255f21b5..b6667a0099 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 @@ -10,6 +10,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.* import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind @@ -19,12 +20,15 @@ import androidx.compose.ui.platform.* import dev.icerock.moko.resources.compose.painterResource import dev.icerock.moko.resources.compose.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import chat.simplex.common.model.ChatModel +import chat.simplex.common.model.RemoteHostInfo import chat.simplex.common.platform.* import chat.simplex.common.ui.theme.* +import chat.simplex.common.views.contacts.ContactTypeTabs import chat.simplex.common.views.helpers.* import chat.simplex.res.MR import kotlinx.coroutines.flow.MutableStateFlow @@ -32,10 +36,86 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import kotlin.math.roundToInt +@Composable +fun ModalData.NewChatView(rh: RemoteHostInfo?) { + Column( + Modifier.fillMaxSize(), + ) { + Box(contentAlignment = Alignment.Center) { + val bottomPadding = DEFAULT_PADDING + AppBarTitle( + stringResource(MR.strings.new_chat), + hostDevice(rh?.remoteHostId), + bottomPadding = bottomPadding + ) + } + val searchText = rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf( + TextFieldValue("") + ) } + + ContactTypeTabs( + searchText = searchText, + contactActions = { + NewChatOptions( + addContact = { + ModalManager.center.closeModals() + ModalManager.center.showModalCloseable { close -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.INVITE, close = close) } + }, + scanPaste = { + ModalManager.center.closeModals() + ModalManager.center.showModalCloseable { close -> NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = true, close = close) } + }, + createGroup = { + ModalManager.center.closeModals() + ModalManager.center.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close) } + } + ) + } + ) + } +} + +@Composable +fun NewChatOptions(addContact: () -> Unit, scanPaste: () -> Unit, createGroup: () -> Unit) { + val actions = remember { listOf(addContact, scanPaste, createGroup) } + val backgroundColor = if (isInDarkTheme()) + blendARGB(MaterialTheme.colors.primary, Color.Black, 0.7F) + else + MaterialTheme.colors.background + + LazyColumn { + items(actions.size) { index -> + Row { + Box(contentAlignment = Alignment.Center) { + Button( + actions[index], + shape = RoundedCornerShape(21.dp * fontSizeSqrtMultiplier), + colors = ButtonDefaults.textButtonColors(backgroundColor = backgroundColor), + elevation = null, + contentPadding = PaddingValues(horizontal = DEFAULT_PADDING_HALF, vertical = DEFAULT_PADDING_HALF), + modifier = Modifier.height(42.dp * fontSizeSqrtMultiplier) + ) { + Icon( + painterResource(icons[index]), + stringResource(titles[index]), + Modifier.size(42.dp * fontSizeSqrtMultiplier), + tint = if (isInDarkTheme()) MaterialTheme.colors.primary else MaterialTheme.colors.primary + ) + Text( + stringResource(titles[index]), + color = if (isInDarkTheme()) MaterialTheme.colors.primary else MaterialTheme.colors.primary, + fontWeight = FontWeight.Medium, + ) + } + } + } + } + } +} + @Composable fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow, stopped: Boolean, closeNewChatSheet: (animated: Boolean) -> Unit) { // TODO close new chat if remote host changes in model - if (newChatSheetState.collectAsState().value.isVisible()) BackHandler { closeNewChatSheet(true) } NewChatSheetLayout( newChatSheetState, stopped, @@ -50,7 +130,6 @@ fun NewChatSheet(chatModel: ChatModel, newChatSheetState: StateFlow NewChatView(chatModel.currentRemoteHost.value, NewChatOption.CONNECT, showQRCodeScanner = true, close = close) } }, createGroup = { - closeNewChatSheet(false) ModalManager.center.closeModals() ModalManager.center.showCustomModal { close -> AddGroupView(chatModel, chatModel.currentRemoteHost.value, close) } }, 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 c05cfe6780..95a8eb4e6c 100644 --- a/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml +++ b/apps/multiplatform/common/src/commonMain/resources/MR/base/strings.xml @@ -2187,4 +2187,10 @@ Download errors Server address Open server settings + + + New + Removed + Recent + No filtered contacts \ No newline at end of file