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 667f0c110f..824a7e48a7 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 @@ -179,6 +179,8 @@ class AppPreferences { val oneHandUICardShown = mkBoolPreference(SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN, false) val addressCreationCardShown = mkBoolPreference(SHARED_PREFS_ADDRESS_CREATION_CARD_SHOWN, false) val connectBannerCardShown = mkBoolPreference(SHARED_PREFS_CONNECT_BANNER_CARD_SHOWN, false) + /** After the chat list had at least one real conversation once, the full-screen "Now you can" invite UI stays off. */ + val chatListInviteEmptyDismissed = mkBoolPreference(SHARED_PREFS_CHAT_LIST_INVITE_EMPTY_DISMISSED, false) val showMuteProfileAlert = mkBoolPreference(SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT, true) val showReportsInSupportChatAlert = mkBoolPreference(SHARED_PREFS_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT, true) val appLanguage = mkStrPreference(SHARED_PREFS_APP_LANGUAGE, null) @@ -441,6 +443,7 @@ class AppPreferences { private const val SHARED_PREFS_ONE_HAND_UI_CARD_SHOWN = "OneHandUICardShown" private const val SHARED_PREFS_ADDRESS_CREATION_CARD_SHOWN = "AddressCreationCardShown" private const val SHARED_PREFS_CONNECT_BANNER_CARD_SHOWN = "ConnectBannerCardShown" + private const val SHARED_PREFS_CHAT_LIST_INVITE_EMPTY_DISMISSED = "ChatListInviteEmptyDismissed" private const val SHARED_PREFS_SHOW_MUTE_PROFILE_ALERT = "ShowMuteProfileAlert" private const val SHARED_PREFS_SHOW_REPORTS_IN_SUPPORT_CHAT_ALERT = "ShowReportsInSupportChatAlert" private const val SHARED_PREFS_STORE_DB_PASSPHRASE = "StoreDBPassphrase" 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 3a10ab4d09..8a93ca070e 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 @@ -50,8 +50,8 @@ import kotlin.time.Duration.Companion.seconds enum class PresetTagKind { GROUP_REPORTS, FAVORITES, CONTACTS, GROUPS, BUSINESS, NOTES } sealed class ActiveFilter { - data class PresetTag(val tag: PresetTagKind) : ActiveFilter() - data class UserTag(val tag: ChatTag) : ActiveFilter() + data class PresetTag(val tag: PresetTagKind): ActiveFilter() + data class UserTag(val tag: ChatTag): ActiveFilter() data object Unread: ActiveFilter() } @@ -150,7 +150,6 @@ fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow Unit = { connectSheetScope.launch { connectSheetState.show() } } - var showOneTimeLinkSheet by remember { mutableStateOf(false) } val oneTimeLinkSheetState = rememberModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden, skipHalfExpanded = true) val oneTimeLinkSheetScope = rememberCoroutineScope() @@ -164,7 +163,6 @@ fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow Unit = { connectBannerSheetSelection = NewChatOption.INVITE connectBannerSheetShowQR = false @@ -207,65 +204,64 @@ fun ChatListView(chatModel: ChatModel, userPickerState: MutableStateFlow @@ -379,612 +375,638 @@ private fun AddressCreationCard() { } } -@Composable -private fun BoxScope.ChatListWithLoadingScreen(searchText: MutableState, listState: LazyListState, onConnectClick: () -> Unit, onOneTimeLinkClick: () -> Unit, onBannerInviteClick: () -> Unit, onBannerScanPasteClick: () -> Unit) { - if (!chatModel.desktopNoUserNoRemote) { - if (chatModel.chats.value.isEmpty()) { - EmptyChatListView(onConnectClick = onConnectClick, onOneTimeLinkClick = onOneTimeLinkClick) - } else { - ChatList(searchText = searchText, listState, onBannerInviteClick, onBannerScanPasteClick) - } - return +/** + * Chats that do not count as “you already have someone to talk to” for the invite empty screen. + * Matches rows excluded by [filteredChats] with empty search: note folders, hidden contact cards, + * deleted chats, and invalid placeholders. + */ +private fun Chat.ignorableForInviteEmptyState(): Boolean = + when (val c = chatInfo) { + is ChatInfo.Local -> true + is ChatInfo.Direct -> c.contact.chatDeleted || c.contact.isContactCard + is ChatInfo.Group -> c.groupInfo.chatDeleted + is ChatInfo.ContactRequest -> c.contactRequest.chatDeleted + is ChatInfo.ContactConnection -> c.contactConnection.chatDeleted + is ChatInfo.InvalidJSON -> true } - if (chatModel.chats.value.isEmpty() && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote) { - if (chatModel.chatRunning.value == null) { +/** True when every chat is ignorable—same as “nothing would show” in the main list aside from open-chat pinning. */ +private fun noConversationChatsYet(chats: List): Boolean = + chats.all { it.ignorableForInviteEmptyState() } + +@Composable +private fun BoxScope.ChatListWithLoadingScreen(searchText: MutableState, listState: LazyListState, onConnectClick: () -> Unit, onOneTimeLinkClick: () -> Unit, onBannerInviteClick: () -> Unit, onBannerScanPasteClick: () -> Unit) { + val chats = chatModel.chats.value + val emptyForInvite = noConversationChatsYet(chats) + val inviteEmptyDismissed = remember { appPrefs.chatListInviteEmptyDismissed.state } + + LaunchedEffect(emptyForInvite) { + if (!emptyForInvite) { + appPrefs.chatListInviteEmptyDismissed.set(true) + } + } + + when { + emptyForInvite && !chatModel.switchingUsersAndHosts.value && !chatModel.desktopNoUserNoRemote && chatModel.chatRunning.value == null -> { Text( stringResource(MR.strings.loading_chats), Modifier.align(Alignment.Center), color = MaterialTheme.colors.secondary ) - } else { + } + emptyForInvite && !inviteEmptyDismissed.value -> { EmptyChatListView(onConnectClick = onConnectClick, onOneTimeLinkClick = onOneTimeLinkClick) } + !chatModel.desktopNoUserNoRemote -> { + ChatList(searchText = searchText, listState, onBannerInviteClick, onBannerScanPasteClick) + } } } -@Composable -private fun BoxScope.NewChatSheetFloatingButton(oneHandUI: State, stopped: Boolean) { - FloatingActionButton( - onClick = { - if (!stopped) { - showNewChatSheet(oneHandUI) - } - }, - Modifier - .navigationBarsPadding() - .padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING) - .align(Alignment.BottomEnd) - .size(AppBarHeight * fontSizeSqrtMultiplier), - elevation = FloatingActionButtonDefaults.elevation( - defaultElevation = 0.dp, - pressedElevation = 0.dp, - hoveredElevation = 0.dp, - focusedElevation = 0.dp, - ), - backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, - contentColor = Color.White - ) { - Icon(painterResource(MR.images.ic_edit_filled), stringResource(MR.strings.add_contact_or_create_group), Modifier.size(22.dp * fontSizeSqrtMultiplier)) - } -} - -@Composable -private fun ConnectButton(text: String, onClick: () -> Unit) { - Button( - onClick, - shape = RoundedCornerShape(21.dp), - colors = ButtonDefaults.textButtonColors( - backgroundColor = MaterialTheme.colors.primaryVariant - ), - elevation = null, - contentPadding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), - modifier = Modifier.height(42.dp) - ) { - Text(text, color = Color.White) - } -} - -@Composable -private fun ChatListToolbar(userPickerState: MutableStateFlow, listState: LazyListState, stopped: Boolean, setPerformLA: (Boolean) -> Unit) { - val serversSummary: MutableState = remember { mutableStateOf(null) } - val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() - val updatingProgress = remember { chatModel.updatingProgress }.value - val oneHandUI = remember { appPrefs.oneHandUI.state } - - if (updatingProgress != null) { - barButtons.add { - val interactionSource = remember { MutableInteractionSource() } - val hovered = interactionSource.collectIsHoveredAsState().value - IconButton(onClick = { - chatModel.updatingRequest?.close() - }, Modifier.hoverable(interactionSource)) { - if (hovered) { - Icon(painterResource(MR.images.ic_close), null, tint = WarningOrange) - } else if (updatingProgress == -1f) { - CIFileViewScope.progressIndicator() - } else { - CIFileViewScope.progressCircle((updatingProgress * 100).toLong(), 100) + @Composable + private fun BoxScope.NewChatSheetFloatingButton(oneHandUI: State, stopped: Boolean) { + FloatingActionButton( + onClick = { + if (!stopped) { + showNewChatSheet(oneHandUI) } - } + }, + Modifier + .navigationBarsPadding() + .padding(end = DEFAULT_PADDING, bottom = DEFAULT_PADDING) + .align(Alignment.BottomEnd) + .size(AppBarHeight * fontSizeSqrtMultiplier), + elevation = FloatingActionButtonDefaults.elevation( + defaultElevation = 0.dp, + pressedElevation = 0.dp, + hoveredElevation = 0.dp, + focusedElevation = 0.dp, + ), + backgroundColor = if (!stopped) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, + contentColor = Color.White + ) { + Icon(painterResource(MR.images.ic_edit_filled), stringResource(MR.strings.add_contact_or_create_group), Modifier.size(22.dp * fontSizeSqrtMultiplier)) } } - if (stopped) { - barButtons.add { - IconButton(onClick = { - AlertManager.shared.showAlertMsg( - generalGetString(MR.strings.chat_is_stopped_indication), - generalGetString(MR.strings.you_can_start_chat_via_setting_or_by_restarting_the_app) - ) - }) { - Icon( - painterResource(MR.images.ic_report_filled), - generalGetString(MR.strings.chat_is_stopped_indication), - tint = Color.Red, - ) - } + @Composable + private fun ConnectButton(text: String, onClick: () -> Unit) { + Button( + onClick, + shape = RoundedCornerShape(21.dp), + colors = ButtonDefaults.textButtonColors( + backgroundColor = MaterialTheme.colors.primaryVariant + ), + elevation = null, + contentPadding = PaddingValues(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF), + modifier = Modifier.height(42.dp) + ) { + Text(text, color = Color.White) } - } else { - if (connectProgressManager.showConnectProgress != null) { + } + + @Composable + private fun ChatListToolbar(userPickerState: MutableStateFlow, listState: LazyListState, stopped: Boolean, setPerformLA: (Boolean) -> Unit) { + val serversSummary: MutableState = remember { mutableStateOf(null) } + val barButtons = arrayListOf<@Composable RowScope.() -> Unit>() + val updatingProgress = remember { chatModel.updatingProgress }.value + val oneHandUI = remember { appPrefs.oneHandUI.state } + + if (updatingProgress != null) { barButtons.add { - Box(Modifier.padding(horizontal = DEFAULT_PADDING_HALF)) { - CIFileViewScope.progressIndicator() + val interactionSource = remember { MutableInteractionSource() } + val hovered = interactionSource.collectIsHoveredAsState().value + IconButton(onClick = { + chatModel.updatingRequest?.close() + }, Modifier.hoverable(interactionSource)) { + if (hovered) { + Icon(painterResource(MR.images.ic_close), null, tint = WarningOrange) + } else if (updatingProgress == -1f) { + CIFileViewScope.progressIndicator() + } else { + CIFileViewScope.progressCircle((updatingProgress * 100).toLong(), 100) + } } } } - if (oneHandUI.value) { - val sp16 = with(LocalDensity.current) { 16.sp.toDp() } + if (stopped) { + barButtons.add { + IconButton(onClick = { + AlertManager.shared.showAlertMsg( + generalGetString(MR.strings.chat_is_stopped_indication), + generalGetString(MR.strings.you_can_start_chat_via_setting_or_by_restarting_the_app) + ) + }) { + Icon( + painterResource(MR.images.ic_report_filled), + generalGetString(MR.strings.chat_is_stopped_indication), + tint = Color.Red, + ) + } + } + } else { + if (connectProgressManager.showConnectProgress != null) { + barButtons.add { + Box(Modifier.padding(horizontal = DEFAULT_PADDING_HALF)) { + CIFileViewScope.progressIndicator() + } + } + } - if (appPlatform.isDesktop && oneHandUI.value) { - val call = remember { chatModel.activeCall } - if (call.value != null) { - barButtons.add { - val c = call.value - if (c != null) { - ActiveCallInteractiveArea(c) - Spacer(Modifier.width(5.dp)) + if (oneHandUI.value) { + val sp16 = with(LocalDensity.current) { 16.sp.toDp() } + + if (appPlatform.isDesktop && oneHandUI.value) { + val call = remember { chatModel.activeCall } + if (call.value != null) { + barButtons.add { + val c = call.value + if (c != null) { + ActiveCallInteractiveArea(c) + Spacer(Modifier.width(5.dp)) + } + } + } + } + + barButtons.add { + IconButton( + onClick = { + showNewChatSheet(oneHandUI) + }, + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .background(MaterialTheme.colors.primary, shape = CircleShape) + .size(33.dp * fontSizeSqrtMultiplier) + ) { + Icon( + painterResource(MR.images.ic_add), + stringResource(MR.strings.add_contact_or_create_group), + Modifier.size(sp16), + tint = Color.White + ) } } } } - - barButtons.add { - IconButton( - onClick = { - showNewChatSheet(oneHandUI) - }, - ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .background(MaterialTheme.colors.primary, shape = CircleShape) - .size(33.dp * fontSizeSqrtMultiplier) - ) { - Icon( - painterResource(MR.images.ic_add), - stringResource(MR.strings.add_contact_or_create_group), - Modifier.size(sp16), - tint = Color.White - ) - } - } - } } - } - - val clipboard = LocalClipboardManager.current - val scope = rememberCoroutineScope() - val canScrollToZero = remember { derivedStateOf { listState.firstVisibleItemIndex != 0 || listState.firstVisibleItemScrollOffset != 0 } } - DefaultAppBar( - navigationButton = { - if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) { - NavigationButtonMenu { - ModalManager.start.showModalCloseable { close -> - SettingsView(chatModel, setPerformLA, close) + val clipboard = LocalClipboardManager.current + val scope = rememberCoroutineScope() + val canScrollToZero = remember { derivedStateOf { listState.firstVisibleItemIndex != 0 || listState.firstVisibleItemScrollOffset != 0 } } + DefaultAppBar( + navigationButton = { + if (chatModel.users.isEmpty() && !chatModel.desktopNoUserNoRemote) { + NavigationButtonMenu { + ModalManager.start.showModalCloseable { close -> + SettingsView(chatModel, setPerformLA, close) + } + } + } 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) { + userPickerState.value = AnimatedViewState.VISIBLE } } - } 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) { - userPickerState.value = AnimatedViewState.VISIBLE - } - } - }, - title = { - Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON)) { - Text( - stringResource(MR.strings.your_chats), - color = MaterialTheme.colors.onBackground, - fontWeight = FontWeight.SemiBold, - ) - SubscriptionStatusIndicator( - click = { - ModalManager.start.closeModals() - val summary = serversSummary.value - ModalManager.start.showModalCloseable( - endButtons = { - if (summary != null) { - ShareButton { - val json = Json { - prettyPrint = true + }, + title = { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(DEFAULT_SPACE_AFTER_ICON)) { + Text( + stringResource(MR.strings.your_chats), + color = MaterialTheme.colors.onBackground, + fontWeight = FontWeight.SemiBold, + ) + SubscriptionStatusIndicator( + click = { + ModalManager.start.closeModals() + val summary = serversSummary.value + ModalManager.start.showModalCloseable( + endButtons = { + if (summary != null) { + ShareButton { + val json = Json { + prettyPrint = true + } + val text = json.encodeToString(PresentedServersSummary.serializer(), summary) + clipboard.shareText(text) } - val text = json.encodeToString(PresentedServersSummary.serializer(), summary) - clipboard.shareText(text) } } - } - ) { ServersSummaryView(chatModel.currentRemoteHost.value, serversSummary) } + ) { ServersSummaryView(chatModel.currentRemoteHost.value, serversSummary) } + } + ) + } + }, + onTitleClick = if (canScrollToZero.value) { + { scrollToBottom(scope, listState) } + } else null, + onTop = !oneHandUI.value, + onSearchValueChanged = {}, + buttons = { barButtons.forEach { it() } } + ) + } + + @Composable + fun SubscriptionStatusIndicator(click: (() -> Unit)) { + var subs by remember { mutableStateOf(SMPServerSubs.newSMPServerSubs) } + var hasSess by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + + suspend fun setSubsTotal() { + if (chatModel.currentUser.value != null && chatModel.controller.hasChatCtrl() && chatModel.chatRunning.value == true) { + val r = chatModel.controller.getAgentSubsTotal(chatModel.remoteHostId()) + if (r != null) { + subs = r.first + hasSess = r.second + } + } + } + + LaunchedEffect(Unit) { + setSubsTotal() + scope.launch { + while (isActive) { + delay(1.seconds) + if ((appPlatform.isDesktop || chatModel.chatId.value == null) && !ModalManager.start.hasModalsOpen() && !ModalManager.fullscreen.hasModalsOpen() && isAppVisibleAndFocused()) { + setSubsTotal() } - ) + } } - }, - onTitleClick = if (canScrollToZero.value) { { scrollToBottom(scope, listState) } } else null, - onTop = !oneHandUI.value, - onSearchValueChanged = {}, - buttons = { barButtons.forEach { it() } } - ) -} + } -@Composable -fun SubscriptionStatusIndicator(click: (() -> Unit)) { - var subs by remember { mutableStateOf(SMPServerSubs.newSMPServerSubs) } - var hasSess by remember { mutableStateOf(false) } - val scope = rememberCoroutineScope() - - suspend fun setSubsTotal() { - if (chatModel.currentUser.value != null && chatModel.controller.hasChatCtrl() && chatModel.chatRunning.value == true) { - val r = chatModel.controller.getAgentSubsTotal(chatModel.remoteHostId()) - if (r != null) { - subs = r.first - hasSess = r.second - } + SimpleButtonFrame( + click = click, + disabled = chatModel.chatRunning.value != true + ) { + SubscriptionStatusIndicatorView(subs = subs, hasSess = hasSess) } } - LaunchedEffect(Unit) { - setSubsTotal() - scope.launch { - while (isActive) { - delay(1.seconds) - if ((appPlatform.isDesktop || chatModel.chatId.value == null) && !ModalManager.start.hasModalsOpen() && !ModalManager.fullscreen.hasModalsOpen() && isAppVisibleAndFocused()) { - setSubsTotal() + @Composable + fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> Unit) { + Row(verticalAlignment = Alignment.CenterVertically) { + IconButton(onClick = onButtonClicked) { + Box { + ProfileImage( + image = image, + size = 37.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) + } } } } } - SimpleButtonFrame( - click = click, - disabled = chatModel.chatRunning.value != true - ) { - SubscriptionStatusIndicatorView(subs = subs, hasSess = hasSess) - } -} - -@Composable -fun UserProfileButton(image: String?, allRead: Boolean, onButtonClicked: () -> Unit) { - Row(verticalAlignment = Alignment.CenterVertically) { - IconButton(onClick = onButtonClicked) { - Box { - ProfileImage( - image = image, - size = 37.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) - } - } - } - } -} - - -@Composable -private fun BoxScope.unreadBadge(text: String? = "") { - Text( - text ?: "", - color = MaterialTheme.colors.onPrimary, - fontSize = 6.sp, - modifier = Modifier - .background(MaterialTheme.colors.primary, shape = CircleShape) - .badgeLayout() - .padding(horizontal = 3.dp) - .padding(vertical = 1.dp) - .align(Alignment.TopEnd) - ) -} - -@Composable -private fun ToggleFilterEnabledButton() { - val showUnread = remember { chatModel.activeChatTagFilter }.value == ActiveFilter.Unread - - IconButton(onClick = { - if (showUnread) { - chatModel.activeChatTagFilter.value = null - } else { - chatModel.activeChatTagFilter.value = ActiveFilter.Unread - } - }) { - val sp16 = with(LocalDensity.current) { 16.sp.toDp() } - Icon( - painterResource(MR.images.ic_filter_list), - null, - tint = if (showUnread) MaterialTheme.colors.background else MaterialTheme.colors.secondary, + @Composable + private fun BoxScope.unreadBadge(text: String? = "") { + Text( + text ?: "", + color = MaterialTheme.colors.onPrimary, + fontSize = 6.sp, modifier = Modifier - .padding(3.dp) - .background(color = if (showUnread) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50)) - .border(width = 1.dp, color = if (showUnread) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50)) - .padding(3.dp) - .size(sp16) + .background(MaterialTheme.colors.primary, shape = CircleShape) + .badgeLayout() + .padding(horizontal = 3.dp) + .padding(vertical = 1.dp) + .align(Alignment.TopEnd) ) } -} -@Composable -expect fun ActiveCallInteractiveArea(call: Call) + @Composable + private fun ToggleFilterEnabledButton() { + val showUnread = remember { chatModel.activeChatTagFilter }.value == ActiveFilter.Unread -fun connectIfOpenedViaUri(rhId: Long?, uri: String, chatModel: ChatModel) { - Log.d(TAG, "connectIfOpenedViaUri: opened via link") - if (chatModel.currentUser.value == null) { - chatModel.appOpenUrl.value = rhId to uri - } else { - withBGApi { - chatModel.appOpenUrlConnecting.value = true - planAndConnect(rhId, uri, close = null, cleanup = { chatModel.appOpenUrlConnecting.value = false }) - } - } -} - -@Composable -private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState, searchShowingSimplexLink: MutableState, searchChatFilteredBySimplexLink: MutableState) { - Box { - Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { - val focusRequester = remember { FocusRequester() } - var focused by remember { mutableStateOf(false) } - Icon( - painterResource(MR.images.ic_search), - contentDescription = null, - Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF).size(22.dp * fontSizeSqrtMultiplier), - tint = MaterialTheme.colors.secondary - ) - SearchTextField( - Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester), - placeholder = stringResource(MR.strings.search_or_paste_simplex_link), - alwaysVisible = true, - searchText = searchText, - enabled = !remember { searchShowingSimplexLink }.value, - 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() - } + IconButton(onClick = { + if (showUnread) { + chatModel.activeChatTagFilter.value = null } else { - val padding = if (appPlatform.isDesktop) 0.dp else 7.dp - if (chatModel.chats.value.isNotEmpty()) { - ToggleFilterEnabledButton() - } - Spacer(Modifier.width(padding)) + chatModel.activeChatTagFilter.value = ActiveFilter.Unread } - val focusManager = LocalFocusManager.current - val keyboardState = getKeyboardState() - LaunchedEffect(keyboardState.value) { - if (keyboardState.value == KeyboardState.Closed && focused) { - focusManager.clearFocus() - } - } - val view = LocalMultiplatformView() - LaunchedEffect(Unit) { - snapshotFlow { searchText.value.text } - .distinctUntilChanged() - .collect { - val link = strHasSingleSimplexLink(it.trim()) - if (link != null) { - // if SimpleX link is pasted, show connection dialogue - hideKeyboard(view) - if (link.format is Format.SimplexLink) { - val linkText = link.format.simplexLinkText - searchText.value = searchText.value.copy(linkText, selection = TextRange.Zero) - } - searchShowingSimplexLink.value = true - searchChatFilteredBySimplexLink.value = null - connect(link.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() } - } else if (!searchShowingSimplexLink.value || it.isEmpty()) { - if (it.isNotEmpty()) { - // if some other text is pasted, enter search mode - focusRequester.requestFocus() - } else { - if (!chatModel.appOpenUrlConnecting.value) { - connectProgressManager.cancelConnectProgress() - } - if (listState.layoutInfo.totalItemsCount > 0) { - listState.scrollToItem(0) - } - } - searchShowingSimplexLink.value = false - searchChatFilteredBySimplexLink.value = null - } - } + }) { + val sp16 = with(LocalDensity.current) { 16.sp.toDp() } + Icon( + painterResource(MR.images.ic_filter_list), + null, + tint = if (showUnread) MaterialTheme.colors.background else MaterialTheme.colors.secondary, + modifier = Modifier + .padding(3.dp) + .background(color = if (showUnread) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50)) + .border(width = 1.dp, color = if (showUnread) MaterialTheme.colors.primary else Color.Unspecified, shape = RoundedCornerShape(50)) + .padding(3.dp) + .size(sp16) + ) + } + } + + @Composable + expect fun ActiveCallInteractiveArea(call: Call) + + fun connectIfOpenedViaUri(rhId: Long?, uri: String, chatModel: ChatModel) { + Log.d(TAG, "connectIfOpenedViaUri: opened via link") + if (chatModel.currentUser.value == null) { + chatModel.appOpenUrl.value = rhId to uri + } else { + withBGApi { + chatModel.appOpenUrlConnecting.value = true + planAndConnect(rhId, uri, close = null, cleanup = { chatModel.appOpenUrlConnecting.value = false }) } } - val oneHandUI = remember { appPrefs.oneHandUI.state } - Divider(Modifier.align(if (oneHandUI.value) Alignment.TopStart else Alignment.BottomStart)) - } -} - -private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState, cleanup: (() -> Unit)?) { - withBGApi { - planAndConnect( - chatModel.remoteHostId(), - link, - filterKnownContact = { searchChatFilteredBySimplexLink.value = it.id }, - filterKnownGroup = { searchChatFilteredBySimplexLink.value = it.id }, - close = null, - cleanup = cleanup, - ) - } -} - -@Composable -private fun ErrorSettingsView() { - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - Text(generalGetString(MR.strings.error_showing_content), color = MaterialTheme.colors.error, fontStyle = FontStyle.Italic) - } -} - -private var lazyListState = 0 to 0 - -enum class ScrollDirection { - Up, Down, Idle -} - -@Composable -fun BoxScope.StatusBarBackground() { - if (appPlatform.isAndroid) { - val finalColor = MaterialTheme.colors.background.copy(0.88f) - Box(Modifier.fillMaxWidth().windowInsetsTopHeight(WindowInsets.statusBars).background(finalColor)) - } -} - -@Composable -fun BoxScope.NavigationBarBackground(appBarOnBottom: Boolean = false, mixedColor: Boolean, noAlpha: Boolean = false) { - if (appPlatform.isAndroid) { - val barPadding = WindowInsets.navigationBars.asPaddingValues() - val paddingBottom = barPadding.calculateBottomPadding() - val color = if (mixedColor) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f) else MaterialTheme.colors.background - val finalColor = color.copy(if (noAlpha) 1f else if (appBarOnBottom) remember { appPrefs.inAppBarsAlpha.state }.value else 0.6f) - Box(Modifier.align(Alignment.BottomStart).height(paddingBottom).fillMaxWidth().background(finalColor)) - } -} - -@Composable -fun BoxScope.NavigationBarBackground(modifier: Modifier, color: Color = MaterialTheme.colors.background) { - val keyboardState = getKeyboardState() - if (appPlatform.isAndroid && keyboardState.value == KeyboardState.Closed) { - val barPadding = WindowInsets.navigationBars.asPaddingValues() - val paddingBottom = barPadding.calculateBottomPadding() - val finalColor = color.copy(0.6f) - Box(modifier.align(Alignment.BottomStart).height(paddingBottom).fillMaxWidth().background(finalColor)) - } -} - -@Composable -private fun BoxScope.ChatList(searchText: MutableState, listState: LazyListState, onBannerInviteClick: () -> Unit, onBannerScanPasteClick: () -> Unit) { - var scrollDirection by remember { mutableStateOf(ScrollDirection.Idle) } - var previousIndex by remember { mutableStateOf(0) } - var previousScrollOffset by remember { mutableStateOf(0) } - val keyboardState by getKeyboardState() - val oneHandUI = remember { appPrefs.oneHandUI.state } - val oneHandUICardShown = remember { appPrefs.oneHandUICardShown.state } - val addressCreationCardShown = remember { appPrefs.addressCreationCardShown.state } - val connectBannerCardShown = remember { appPrefs.connectBannerCardShown.state } - val activeFilter = remember { chatModel.activeChatTagFilter } - - LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) { - val currentIndex = listState.firstVisibleItemIndex - val currentScrollOffset = listState.firstVisibleItemScrollOffset - val threshold = 25 - - scrollDirection = when { - currentIndex > previousIndex -> ScrollDirection.Down - currentIndex < previousIndex -> ScrollDirection.Up - currentScrollOffset > previousScrollOffset + threshold -> ScrollDirection.Down - currentScrollOffset < previousScrollOffset - threshold -> ScrollDirection.Up - currentScrollOffset == previousScrollOffset -> ScrollDirection.Idle - else -> scrollDirection - } - - previousIndex = currentIndex - previousScrollOffset = currentScrollOffset } - DisposableEffect(Unit) { - onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset } - } - val allChats = remember { chatModel.chats } - // In some not always reproducible situations this code produce IndexOutOfBoundsException on Compose's side - // which is related to [derivedStateOf]. Using safe alternative instead - // val chats by remember(search, showUnreadAndFavorites) { derivedStateOf { filteredChats(showUnreadAndFavorites, search, allChats.toList()) } } - val searchShowingSimplexLink = remember { mutableStateOf(false) } - val searchChatFilteredBySimplexLink = remember { mutableStateOf(null) } - val chats = filteredChats(searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.value.toList(), activeFilter.value) - val topPaddingToContent = topPaddingToContent(false) - val blankSpaceSize = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else topPaddingToContent - LazyColumnWithScrollBar( - if (!oneHandUI.value) Modifier.imePadding() else Modifier, - listState, - reverseLayout = oneHandUI.value - ) { - item { Spacer(Modifier.height(blankSpaceSize)) } - stickyHeader { - Column( - Modifier - .zIndex(1f) - .offset { - val offsetMultiplier = if (oneHandUI.value) 1 else -1 - val y = if (searchText.value.text.isNotEmpty() || (appPlatform.isAndroid && keyboardState == KeyboardState.Opened) || scrollDirection == ScrollDirection.Up) { - if (listState.firstVisibleItemIndex == 0) -offsetMultiplier * listState.firstVisibleItemScrollOffset - else -offsetMultiplier * blankSpaceSize.roundToPx() - } else { - when (listState.firstVisibleItemIndex) { - 0 -> 0 - 1 -> offsetMultiplier * listState.firstVisibleItemScrollOffset - else -> offsetMultiplier * 1000 - } - } - IntOffset(0, y) - } - .background(MaterialTheme.colors.background) + @Composable + private fun ChatListSearchBar(listState: LazyListState, searchText: MutableState, searchShowingSimplexLink: MutableState, searchChatFilteredBySimplexLink: MutableState) { + Box { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + val focusRequester = remember { FocusRequester() } + var focused by remember { mutableStateOf(false) } + Icon( + painterResource(MR.images.ic_search), + contentDescription = null, + Modifier.padding(start = DEFAULT_PADDING, end = DEFAULT_PADDING_HALF).size(22.dp * fontSizeSqrtMultiplier), + tint = MaterialTheme.colors.secondary + ) + SearchTextField( + Modifier.weight(1f).onFocusChanged { focused = it.hasFocus }.focusRequester(focusRequester), + placeholder = stringResource(MR.strings.search_or_paste_simplex_link), + alwaysVisible = true, + searchText = searchText, + enabled = !remember { searchShowingSimplexLink }.value, + trailingContent = null, ) { - if (oneHandUI.value) { - Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight))) { - Divider() - TagsView(searchText) - ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink) - Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) + 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 { - ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink) - TagsView(searchText) - Divider() + val padding = if (appPlatform.isDesktop) 0.dp else 7.dp + if (chatModel.chats.value.isNotEmpty()) { + ToggleFilterEnabledButton() + } + Spacer(Modifier.width(padding)) + } + val focusManager = LocalFocusManager.current + val keyboardState = getKeyboardState() + LaunchedEffect(keyboardState.value) { + if (keyboardState.value == KeyboardState.Closed && focused) { + focusManager.clearFocus() + } + } + val view = LocalMultiplatformView() + LaunchedEffect(Unit) { + snapshotFlow { searchText.value.text } + .distinctUntilChanged() + .collect { + val link = strHasSingleSimplexLink(it.trim()) + if (link != null) { + // if SimpleX link is pasted, show connection dialogue + hideKeyboard(view) + if (link.format is Format.SimplexLink) { + val linkText = link.format.simplexLinkText + searchText.value = searchText.value.copy(linkText, selection = TextRange.Zero) + } + searchShowingSimplexLink.value = true + searchChatFilteredBySimplexLink.value = null + connect(link.text, searchChatFilteredBySimplexLink) { searchText.value = TextFieldValue() } + } else if (!searchShowingSimplexLink.value || it.isEmpty()) { + if (it.isNotEmpty()) { + // if some other text is pasted, enter search mode + focusRequester.requestFocus() + } else { + if (!chatModel.appOpenUrlConnecting.value) { + connectProgressManager.cancelConnectProgress() + } + if (listState.layoutInfo.totalItemsCount > 0) { + listState.scrollToItem(0) + } + } + searchShowingSimplexLink.value = false + searchChatFilteredBySimplexLink.value = null + } + } + } + } + val oneHandUI = remember { appPrefs.oneHandUI.state } + Divider(Modifier.align(if (oneHandUI.value) Alignment.TopStart else Alignment.BottomStart)) + } + } + + private fun connect(link: String, searchChatFilteredBySimplexLink: MutableState, cleanup: (() -> Unit)?) { + withBGApi { + planAndConnect( + chatModel.remoteHostId(), + link, + filterKnownContact = { searchChatFilteredBySimplexLink.value = it.id }, + filterKnownGroup = { searchChatFilteredBySimplexLink.value = it.id }, + close = null, + cleanup = cleanup, + ) + } + } + + @Composable + private fun ErrorSettingsView() { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text(generalGetString(MR.strings.error_showing_content), color = MaterialTheme.colors.error, fontStyle = FontStyle.Italic) + } + } + + private var lazyListState = 0 to 0 + + enum class ScrollDirection { + Up, Down, Idle + } + + @Composable + fun BoxScope.StatusBarBackground() { + if (appPlatform.isAndroid) { + val finalColor = MaterialTheme.colors.background.copy(0.88f) + Box(Modifier.fillMaxWidth().windowInsetsTopHeight(WindowInsets.statusBars).background(finalColor)) + } + } + + @Composable + fun BoxScope.NavigationBarBackground(appBarOnBottom: Boolean = false, mixedColor: Boolean, noAlpha: Boolean = false) { + if (appPlatform.isAndroid) { + val barPadding = WindowInsets.navigationBars.asPaddingValues() + val paddingBottom = barPadding.calculateBottomPadding() + val color = if (mixedColor) MaterialTheme.colors.background.mixWith(MaterialTheme.colors.onBackground, 0.97f) else MaterialTheme.colors.background + val finalColor = color.copy(if (noAlpha) 1f else if (appBarOnBottom) remember { appPrefs.inAppBarsAlpha.state }.value else 0.6f) + Box(Modifier.align(Alignment.BottomStart).height(paddingBottom).fillMaxWidth().background(finalColor)) + } + } + + @Composable + fun BoxScope.NavigationBarBackground(modifier: Modifier, color: Color = MaterialTheme.colors.background) { + val keyboardState = getKeyboardState() + if (appPlatform.isAndroid && keyboardState.value == KeyboardState.Closed) { + val barPadding = WindowInsets.navigationBars.asPaddingValues() + val paddingBottom = barPadding.calculateBottomPadding() + val finalColor = color.copy(0.6f) + Box(modifier.align(Alignment.BottomStart).height(paddingBottom).fillMaxWidth().background(finalColor)) + } + } + + @Composable + private fun BoxScope.ChatList(searchText: MutableState, listState: LazyListState, onBannerInviteClick: () -> Unit, onBannerScanPasteClick: () -> Unit) { + var scrollDirection by remember { mutableStateOf(ScrollDirection.Idle) } + var previousIndex by remember { mutableStateOf(0) } + var previousScrollOffset by remember { mutableStateOf(0) } + val keyboardState by getKeyboardState() + val oneHandUI = remember { appPrefs.oneHandUI.state } + val oneHandUICardShown = remember { appPrefs.oneHandUICardShown.state } + val addressCreationCardShown = remember { appPrefs.addressCreationCardShown.state } + val connectBannerCardShown = remember { appPrefs.connectBannerCardShown.state } + val activeFilter = remember { chatModel.activeChatTagFilter } + + LaunchedEffect(listState.firstVisibleItemIndex, listState.firstVisibleItemScrollOffset) { + val currentIndex = listState.firstVisibleItemIndex + val currentScrollOffset = listState.firstVisibleItemScrollOffset + val threshold = 25 + + scrollDirection = when { + currentIndex > previousIndex -> ScrollDirection.Down + currentIndex < previousIndex -> ScrollDirection.Up + currentScrollOffset > previousScrollOffset + threshold -> ScrollDirection.Down + currentScrollOffset < previousScrollOffset - threshold -> ScrollDirection.Up + currentScrollOffset == previousScrollOffset -> ScrollDirection.Idle + else -> scrollDirection + } + + previousIndex = currentIndex + previousScrollOffset = currentScrollOffset + } + + DisposableEffect(Unit) { + onDispose { lazyListState = listState.firstVisibleItemIndex to listState.firstVisibleItemScrollOffset } + } + val allChats = remember { chatModel.chats } + // In some not always reproducible situations this code produce IndexOutOfBoundsException on Compose's side + // which is related to [derivedStateOf]. Using safe alternative instead + // val chats by remember(search, showUnreadAndFavorites) { derivedStateOf { filteredChats(showUnreadAndFavorites, search, allChats.toList()) } } + val searchShowingSimplexLink = remember { mutableStateOf(false) } + val searchChatFilteredBySimplexLink = remember { mutableStateOf(null) } + val chats = filteredChats(searchShowingSimplexLink, searchChatFilteredBySimplexLink, searchText.value.text, allChats.value.toList(), activeFilter.value) + val topPaddingToContent = topPaddingToContent(false) + val blankSpaceSize = if (oneHandUI.value) WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + AppBarHeight * fontSizeSqrtMultiplier else topPaddingToContent + LazyColumnWithScrollBar( + if (!oneHandUI.value) Modifier.imePadding() else Modifier, + listState, + reverseLayout = oneHandUI.value + ) { + item { Spacer(Modifier.height(blankSpaceSize)) } + stickyHeader { + Column( + Modifier + .zIndex(1f) + .offset { + val offsetMultiplier = if (oneHandUI.value) 1 else -1 + val y = if (searchText.value.text.isNotEmpty() || (appPlatform.isAndroid && keyboardState == KeyboardState.Opened) || scrollDirection == ScrollDirection.Up) { + if (listState.firstVisibleItemIndex == 0) -offsetMultiplier * listState.firstVisibleItemScrollOffset + else -offsetMultiplier * blankSpaceSize.roundToPx() + } else { + when (listState.firstVisibleItemIndex) { + 0 -> 0 + 1 -> offsetMultiplier * listState.firstVisibleItemScrollOffset + else -> offsetMultiplier * 1000 + } + } + IntOffset(0, y) + } + .background(MaterialTheme.colors.background) + ) { + if (oneHandUI.value) { + Column(Modifier.consumeWindowInsets(WindowInsets.navigationBars).consumeWindowInsets(PaddingValues(bottom = AppBarHeight))) { + Divider() + TagsView(searchText) + ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink) + Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) + } + } else { + ChatListSearchBar(listState, searchText, searchShowingSimplexLink, searchChatFilteredBySimplexLink) + TagsView(searchText) + Divider() + } + } + } + itemsIndexed(chats, key = { _, chat -> chat.remoteHostId to chat.id }) { index, chat -> + val nextChatSelected = remember(chat.id, chats) { + derivedStateOf { + chatModel.chatId.value != null && chats.getOrNull(index + 1)?.id == chatModel.chatId.value + } + } + ChatListNavLinkView(chat, nextChatSelected) + } + if (!oneHandUICardShown.value || !addressCreationCardShown.value || !connectBannerCardShown.value) { + item { + ChatListFeatureCards(onBannerInviteClick, onBannerScanPasteClick) + } + } + if (appPlatform.isAndroid) { + item { Spacer(if (oneHandUI.value) Modifier.windowInsetsTopHeight(WindowInsets.statusBars) else Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) } + } + } + if (chats.isEmpty() && chatModel.chats.value.isNotEmpty()) { + Box(Modifier.fillMaxSize().imePadding().padding(horizontal = DEFAULT_PADDING), contentAlignment = Alignment.Center) { + NoChatsView(searchText = searchText) + } + } + if (oneHandUI.value) { + StatusBarBackground() + } else { + NavigationBarBackground(oneHandUI.value, true) + } + if (!oneHandUICardShown.value) { + LaunchedEffect(chats.size) { + if (chats.size >= 3) { + appPrefs.oneHandUICardShown.set(true) } } } - itemsIndexed(chats, key = { _, chat -> chat.remoteHostId to chat.id }) { index, chat -> - val nextChatSelected = remember(chat.id, chats) { derivedStateOf { - chatModel.chatId.value != null && chats.getOrNull(index + 1)?.id == chatModel.chatId.value - } } - ChatListNavLinkView(chat, nextChatSelected) - } - if (!oneHandUICardShown.value || !addressCreationCardShown.value || !connectBannerCardShown.value) { - item { - ChatListFeatureCards(onBannerInviteClick, onBannerScanPasteClick) + + if (!addressCreationCardShown.value) { + LaunchedEffect(chatModel.userAddress.value) { + if (chatModel.userAddress.value != null) { + appPrefs.addressCreationCardShown.set(true) + } } } - if (appPlatform.isAndroid) { - item { Spacer(if (oneHandUI.value) Modifier.windowInsetsTopHeight(WindowInsets.statusBars) else Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars)) } - } - } - if (chats.isEmpty() && chatModel.chats.value.isNotEmpty()) { - Box(Modifier.fillMaxSize().imePadding().padding(horizontal = DEFAULT_PADDING), contentAlignment = Alignment.Center) { - NoChatsView(searchText = searchText) - } - } - if (oneHandUI.value) { - StatusBarBackground() - } else { - NavigationBarBackground(oneHandUI.value, true) - } - if (!oneHandUICardShown.value) { - LaunchedEffect(chats.size) { - if (chats.size >= 3) { - appPrefs.oneHandUICardShown.set(true) - } + + LaunchedEffect(activeFilter.value) { + searchText.value = TextFieldValue("") } } - if (!addressCreationCardShown.value) { - LaunchedEffect(chatModel.userAddress.value) { - if (chatModel.userAddress.value != null) { - appPrefs.addressCreationCardShown.set(true) - } - } - } + @Composable + private fun NoChatsView(searchText: MutableState) { + val activeFilter = remember { chatModel.activeChatTagFilter }.value - LaunchedEffect(activeFilter.value) { - searchText.value = TextFieldValue("") - } -} - -@Composable -private fun NoChatsView(searchText: MutableState) { - val activeFilter = remember { chatModel.activeChatTagFilter }.value - - if (searchText.value.text.isBlank()) { - when (activeFilter) { - is ActiveFilter.PresetTag -> Text(generalGetString(MR.strings.no_filtered_chats), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center) // this should not happen - is ActiveFilter.UserTag -> Text(String.format(generalGetString(MR.strings.no_chats_in_list), activeFilter.tag.chatTagText), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center) - is ActiveFilter.Unread -> { + if (searchText.value.text.isBlank()) { + when (activeFilter) { + is ActiveFilter.PresetTag -> Text(generalGetString(MR.strings.no_filtered_chats), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center) // this should not happen + is ActiveFilter.UserTag -> Text(String.format(generalGetString(MR.strings.no_chats_in_list), activeFilter.tag.chatTagText), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center) + is ActiveFilter.Unread -> { Row( Modifier.clip(shape = CircleShape).clickable { chatModel.activeChatTagFilter.value = null }.padding(DEFAULT_PADDING_HALF), horizontalArrangement = Arrangement.spacedBy(4.dp), @@ -997,380 +1019,386 @@ private fun NoChatsView(searchText: MutableState) { ) Text(generalGetString(MR.strings.no_unread_chats), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center) } - } - null -> { - Text(generalGetString(MR.strings.no_chats), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center) - } - } - } else { - Text(generalGetString(MR.strings.no_chats_found), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center) - } -} - -@Composable -private fun ChatListFeatureCards(onBannerInviteClick: () -> Unit, onBannerScanPasteClick: () -> Unit) { - val oneHandUI = remember { appPrefs.oneHandUI.state } - val oneHandUICardShown = remember { appPrefs.oneHandUICardShown.state } - val addressCreationCardShown = remember { appPrefs.addressCreationCardShown.state } - val connectBannerCardShown = remember { appPrefs.connectBannerCardShown.state } - - Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { - if (!connectBannerCardShown.value) { - ConnectBannerCard(onBannerInviteClick, onBannerScanPasteClick) - } - if (!oneHandUICardShown.value && !oneHandUI.value) { - ToggleChatListCard() - } -// if (!addressCreationCardShown.value) { -// AddressCreationCard() -// } - if (!oneHandUICardShown.value && oneHandUI.value) { - ToggleChatListCard() - } - } -} - -private val TAG_MIN_HEIGHT = 35.dp - -@Composable -private fun TagsView(searchText: MutableState) { - val userTags = remember { chatModel.userTags } - val presetTags = remember { chatModel.presetTags } - val collapsiblePresetTags = presetTags.filter { presetCanBeCollapsed(it.key) && it.value > 0 } - val alwaysShownPresetTags = presetTags.filter { !presetCanBeCollapsed(it.key) && it.value > 0 } - val activeFilter = remember { chatModel.activeChatTagFilter } - val unreadTags = remember { chatModel.unreadTags } - val rhId = chatModel.remoteHostId() - - val rowSizeModifier = Modifier.sizeIn(minHeight = TAG_MIN_HEIGHT * fontSizeSqrtMultiplier) - - TagsRow { - if (collapsiblePresetTags.size > 1) { - if (collapsiblePresetTags.size + alwaysShownPresetTags.size + userTags.value.size <= 3) { - PresetTagKind.entries.filter { t -> (presetTags[t] ?: 0) > 0 }.forEach { tag -> - ExpandedTagFilterView(tag) } - } else { - CollapsedTagsFilterView(searchText) - alwaysShownPresetTags.forEach { tag -> - ExpandedTagFilterView(tag.key) - } - } - } - userTags.value.forEach { tag -> - val current = when (val af = activeFilter.value) { - is ActiveFilter.UserTag -> af.tag == tag - else -> false - } - val interactionSource = remember { MutableInteractionSource() } - 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) - } - } - 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 - ) - } + null -> { + Text(generalGetString(MR.strings.no_chats), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center) } - TagsDropdownMenu(rhId, tag, showMenu, saving) - } - } - val plusClickModifier = Modifier - .clickable { - ModalManager.start.showModalCloseable { close -> - TagListEditor(rhId = rhId, close = close) - } - } - - if (userTags.value.isEmpty()) { - Row(rowSizeModifier.clip(shape = CircleShape).then(plusClickModifier).padding(start = 2.dp, top = 4.dp, end = 6.dp, bottom = 4.dp), verticalAlignment = Alignment.CenterVertically) { - Icon(painterResource(MR.images.ic_add), stringResource(MR.strings.chat_list_add_list), Modifier.size(18.sp.toDp()), tint = MaterialTheme.colors.secondary) - Spacer(Modifier.width(2.dp)) - Text(stringResource(MR.strings.chat_list_add_list), color = MaterialTheme.colors.secondary, fontSize = 15.sp) } } else { - Box(rowSizeModifier, contentAlignment = Alignment.Center) { - Icon( - painterResource(MR.images.ic_add), stringResource(MR.strings.chat_list_add_list), Modifier.clip(shape = CircleShape).then(plusClickModifier).padding(2.dp), tint = MaterialTheme.colors.secondary - ) + Text(generalGetString(MR.strings.no_chats_found), color = MaterialTheme.colors.secondary, textAlign = TextAlign.Center) + } + } + + @Composable + private fun ChatListFeatureCards(onBannerInviteClick: () -> Unit, onBannerScanPasteClick: () -> Unit) { + val oneHandUI = remember { appPrefs.oneHandUI.state } + val oneHandUICardShown = remember { appPrefs.oneHandUICardShown.state } + val addressCreationCardShown = remember { appPrefs.addressCreationCardShown.state } + val connectBannerCardShown = remember { appPrefs.connectBannerCardShown.state } + + Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { + if (!connectBannerCardShown.value) { + ConnectBannerCard(onBannerInviteClick, onBannerScanPasteClick) + } + if (!oneHandUICardShown.value && !oneHandUI.value) { + ToggleChatListCard() + } + // if (!addressCreationCardShown.value) { + // AddressCreationCard() + // } + if (!oneHandUICardShown.value && oneHandUI.value) { + ToggleChatListCard() } } } -} -@Composable -expect fun TagsRow(content: @Composable() (() -> Unit)) + private val TAG_MIN_HEIGHT = 35.dp -@Composable -private fun ExpandedTagFilterView(tag: PresetTagKind) { - val activeFilter = remember { chatModel.activeChatTagFilter } - val active = when (val af = activeFilter.value) { - is ActiveFilter.PresetTag -> af.tag == tag - else -> false - } - val (icon, text) = presetTagLabel(tag, active) - val color = if (active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary + @Composable + private fun TagsView(searchText: MutableState) { + val userTags = remember { chatModel.userTags } + val presetTags = remember { chatModel.presetTags } + val collapsiblePresetTags = presetTags.filter { presetCanBeCollapsed(it.key) && it.value > 0 } + val alwaysShownPresetTags = presetTags.filter { !presetCanBeCollapsed(it.key) && it.value > 0 } + val activeFilter = remember { chatModel.activeChatTagFilter } + val unreadTags = remember { chatModel.unreadTags } + val rhId = chatModel.remoteHostId() + val rowSizeModifier = Modifier.sizeIn(minHeight = TAG_MIN_HEIGHT * fontSizeSqrtMultiplier) - Row( - modifier = Modifier - .sizeIn(minHeight = TAG_MIN_HEIGHT * fontSizeSqrtMultiplier) - .clip(shape = CircleShape) - .clickable { - if (activeFilter.value == ActiveFilter.PresetTag(tag)) { - chatModel.activeChatTagFilter.value = null + TagsRow { + if (collapsiblePresetTags.size > 1) { + if (collapsiblePresetTags.size + alwaysShownPresetTags.size + userTags.value.size <= 3) { + PresetTagKind.entries.filter { t -> (presetTags[t] ?: 0) > 0 }.forEach { tag -> + ExpandedTagFilterView(tag) + } } else { - chatModel.activeChatTagFilter.value = ActiveFilter.PresetTag(tag) + CollapsedTagsFilterView(searchText) + alwaysShownPresetTags.forEach { tag -> + ExpandedTagFilterView(tag.key) + } + } + } + + userTags.value.forEach { tag -> + val current = when (val af = activeFilter.value) { + is ActiveFilter.UserTag -> af.tag == tag + else -> false + } + val interactionSource = remember { MutableInteractionSource() } + 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) + } + } + 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 + ) + } + } + TagsDropdownMenu(rhId, tag, showMenu, saving) + } + } + val plusClickModifier = Modifier + .clickable { + ModalManager.start.showModalCloseable { close -> + TagListEditor(rhId = rhId, close = close) + } + } + + if (userTags.value.isEmpty()) { + Row(rowSizeModifier.clip(shape = CircleShape).then(plusClickModifier).padding(start = 2.dp, top = 4.dp, end = 6.dp, bottom = 4.dp), verticalAlignment = Alignment.CenterVertically) { + Icon(painterResource(MR.images.ic_add), stringResource(MR.strings.chat_list_add_list), Modifier.size(18.sp.toDp()), tint = MaterialTheme.colors.secondary) + Spacer(Modifier.width(2.dp)) + Text(stringResource(MR.strings.chat_list_add_list), color = MaterialTheme.colors.secondary, fontSize = 15.sp) + } + } else { + Box(rowSizeModifier, contentAlignment = Alignment.Center) { + Icon( + painterResource(MR.images.ic_add), stringResource(MR.strings.chat_list_add_list), Modifier.clip(shape = CircleShape).then(plusClickModifier).padding(2.dp), tint = MaterialTheme.colors.secondary + ) } } - .padding(horizontal = 5.dp, vertical = 4.dp) - , - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - Icon( - painterResource(icon), - stringResource(text), - Modifier.size(18.sp.toDp()), - tint = color - ) - Spacer(Modifier.width(4.dp)) - Box { - Text( - stringResource(text), - color = if (active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, - fontWeight = if (active) FontWeight.Medium else FontWeight.Normal, - fontSize = 15.sp - ) - Text( - stringResource(text), - color = Color.Transparent, - fontWeight = FontWeight.Medium, - fontSize = 15.sp - ) } } -} + @Composable + expect fun TagsRow(content: @Composable() (() -> Unit)) -@Composable -private fun CollapsedTagsFilterView(searchText: MutableState) { - val activeFilter = remember { chatModel.activeChatTagFilter } - val presetTags = remember { chatModel.presetTags } - val showMenu = remember { mutableStateOf(false) } + @Composable + private fun ExpandedTagFilterView(tag: PresetTagKind) { + val activeFilter = remember { chatModel.activeChatTagFilter } + val active = when (val af = activeFilter.value) { + is ActiveFilter.PresetTag -> af.tag == tag + else -> false + } + val (icon, text) = presetTagLabel(tag, active) + val color = if (active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary - val selectedPresetTag = when (val af = activeFilter.value) { - is ActiveFilter.PresetTag -> if (presetCanBeCollapsed(af.tag)) af.tag else null - else -> null - } - - Box(Modifier - .clip(shape = CircleShape) - .size(TAG_MIN_HEIGHT * fontSizeSqrtMultiplier) - .clickable { showMenu.value = true }, - contentAlignment = Alignment.Center - ) { - if (selectedPresetTag != null) { - val (icon, text) = presetTagLabel(selectedPresetTag, true) + Row( + modifier = Modifier + .sizeIn(minHeight = TAG_MIN_HEIGHT * fontSizeSqrtMultiplier) + .clip(shape = CircleShape) + .clickable { + if (activeFilter.value == ActiveFilter.PresetTag(tag)) { + chatModel.activeChatTagFilter.value = null + } else { + chatModel.activeChatTagFilter.value = ActiveFilter.PresetTag(tag) + } + } + .padding(horizontal = 5.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { Icon( painterResource(icon), stringResource(text), Modifier.size(18.sp.toDp()), - tint = MaterialTheme.colors.primary + tint = color ) - } else { - Icon( - painterResource(MR.images.ic_menu), - stringResource(MR.strings.chat_list_all), - tint = MaterialTheme.colors.secondary - ) - } - - 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 = { - onCloseMenuAction.value = { - searchText.value = TextFieldValue() - chatModel.activeChatTagFilter.value = null - onCloseMenuAction.value = {} - } - showMenu.value = false - } + Spacer(Modifier.width(4.dp)) + Box { + Text( + stringResource(text), + color = if (active) MaterialTheme.colors.primary else MaterialTheme.colors.secondary, + fontWeight = if (active) FontWeight.Medium else FontWeight.Normal, + fontSize = 15.sp + ) + Text( + stringResource(text), + color = Color.Transparent, + fontWeight = FontWeight.Medium, + fontSize = 15.sp ) } - PresetTagKind.entries.forEach { tag -> - if ((presetTags[tag] ?: 0) > 0 && presetCanBeCollapsed(tag)) { - ItemPresetFilterAction(tag, tag == selectedPresetTag, showMenu, onCloseMenuAction) + } + } + + @Composable + private fun CollapsedTagsFilterView(searchText: MutableState) { + val activeFilter = remember { chatModel.activeChatTagFilter } + val presetTags = remember { chatModel.presetTags } + val showMenu = remember { mutableStateOf(false) } + val selectedPresetTag = when (val af = activeFilter.value) { + is ActiveFilter.PresetTag -> if (presetCanBeCollapsed(af.tag)) af.tag else null + else -> null + } + + Box( + Modifier + .clip(shape = CircleShape) + .size(TAG_MIN_HEIGHT * fontSizeSqrtMultiplier) + .clickable { showMenu.value = true }, + contentAlignment = Alignment.Center + ) { + if (selectedPresetTag != null) { + val (icon, text) = presetTagLabel(selectedPresetTag, true) + Icon( + painterResource(icon), + stringResource(text), + Modifier.size(18.sp.toDp()), + tint = MaterialTheme.colors.primary + ) + } else { + Icon( + painterResource(MR.images.ic_menu), + stringResource(MR.strings.chat_list_all), + tint = MaterialTheme.colors.secondary + ) + } + 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 = { + onCloseMenuAction.value = { + searchText.value = TextFieldValue() + chatModel.activeChatTagFilter.value = null + onCloseMenuAction.value = {} + } + showMenu.value = false + } + ) + } + PresetTagKind.entries.forEach { tag -> + if ((presetTags[tag] ?: 0) > 0 && presetCanBeCollapsed(tag)) { + ItemPresetFilterAction(tag, tag == selectedPresetTag, showMenu, onCloseMenuAction) + } } } } } -} -@Composable -fun ItemPresetFilterAction( - presetTag: PresetTagKind, - active: Boolean, - showMenu: MutableState, - onCloseMenuAction: MutableState<(() -> Unit)> -) { - val (icon, text) = presetTagLabel(presetTag, active) - ItemAction( - stringResource(text), - painterResource(icon), - color = if (active) MaterialTheme.colors.primary else Color.Unspecified, - onClick = { - onCloseMenuAction.value = { - chatModel.activeChatTagFilter.value = ActiveFilter.PresetTag(presetTag) - onCloseMenuAction.value = {} + @Composable + fun ItemPresetFilterAction( + presetTag: PresetTagKind, + active: Boolean, + showMenu: MutableState, + onCloseMenuAction: MutableState<(() -> Unit)> + ) { + val (icon, text) = presetTagLabel(presetTag, active) + ItemAction( + stringResource(text), + painterResource(icon), + color = if (active) MaterialTheme.colors.primary else Color.Unspecified, + onClick = { + onCloseMenuAction.value = { + chatModel.activeChatTagFilter.value = ActiveFilter.PresetTag(presetTag) + onCloseMenuAction.value = {} + } + showMenu.value = false } - showMenu.value = false - } - ) -} + ) + } -fun filteredChats( - searchShowingSimplexLink: State, - searchChatFilteredBySimplexLink: State, - searchText: String, - chats: List, - activeFilter: ActiveFilter? = null, -): List { - val linkChatId = searchChatFilteredBySimplexLink.value - return if (linkChatId != null) { - chats.filter { it.id == linkChatId } - } else { - val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase() - if (s.isEmpty()) - chats.filter { chat -> chat.id == chatModel.chatId.value || (!chat.chatInfo.chatDeleted && !chat.chatInfo.contactCard && filtered(chat, activeFilter)) } - else { - chats.filter { chat -> - 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 - } + fun filteredChats( + searchShowingSimplexLink: State, + searchChatFilteredBySimplexLink: State, + searchText: String, + chats: List, + activeFilter: ActiveFilter? = null, + ): List { + val linkChatId = searchChatFilteredBySimplexLink.value + return if (linkChatId != null) { + chats.filter { it.id == linkChatId } + } else { + val s = if (searchShowingSimplexLink.value) "" else searchText.trim().lowercase() + if (s.isEmpty()) + chats.filter { chat -> chat.id == chatModel.chatId.value || (!chat.chatInfo.chatDeleted && !chat.chatInfo.contactCard && filtered(chat, activeFilter)) } + else { + chats.filter { chat -> + 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 + } + } } } } -} -private fun filtered(chat: Chat, activeFilter: ActiveFilter?): Boolean = - when (activeFilter) { - is ActiveFilter.PresetTag -> presetTagMatchesChat(activeFilter.tag, chat.chatInfo, chat.chatStats) - is ActiveFilter.UserTag -> chat.chatInfo.chatTags?.contains(activeFilter.tag.chatTagId) ?: false - is ActiveFilter.Unread -> chat.unreadTag + private fun filtered(chat: Chat, activeFilter: ActiveFilter?): Boolean = + when (activeFilter) { + is ActiveFilter.PresetTag -> presetTagMatchesChat(activeFilter.tag, chat.chatInfo, chat.chatStats) + is ActiveFilter.UserTag -> chat.chatInfo.chatTags?.contains(activeFilter.tag.chatTagId) ?: false + is ActiveFilter.Unread -> chat.unreadTag + else -> true + } + + fun presetTagMatchesChat(tag: PresetTagKind, chatInfo: ChatInfo, chatStats: Chat.ChatStats): Boolean = + when (tag) { + PresetTagKind.GROUP_REPORTS -> chatStats.reportsCount > 0 + PresetTagKind.FAVORITES -> chatInfo.chatSettings?.favorite == true + PresetTagKind.CONTACTS -> when (chatInfo) { + is ChatInfo.Direct -> !chatInfo.contact.isContactCard && !chatInfo.contact.chatDeleted + is ChatInfo.ContactRequest -> true + is ChatInfo.ContactConnection -> true + is ChatInfo.Group -> chatInfo.groupInfo.businessChat?.chatType == BusinessChatType.Customer + else -> false + } + + PresetTagKind.GROUPS -> when (chatInfo) { + is ChatInfo.Group -> chatInfo.groupInfo.businessChat == null + else -> false + } + + PresetTagKind.BUSINESS -> when (chatInfo) { + 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 = + when (tag) { + PresetTagKind.GROUP_REPORTS -> (if (active) MR.images.ic_flag_filled else MR.images.ic_flag) to MR.strings.chat_list_group_reports + PresetTagKind.FAVORITES -> (if (active) MR.images.ic_star_filled else MR.images.ic_star) to MR.strings.chat_list_favorites + 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 + } + + private fun presetCanBeCollapsed(tag: PresetTagKind): Boolean = when (tag) { + PresetTagKind.GROUP_REPORTS -> false else -> true } -fun presetTagMatchesChat(tag: PresetTagKind, chatInfo: ChatInfo, chatStats: Chat.ChatStats): Boolean = - when (tag) { - PresetTagKind.GROUP_REPORTS -> chatStats.reportsCount > 0 - PresetTagKind.FAVORITES -> chatInfo.chatSettings?.favorite == true - PresetTagKind.CONTACTS -> when (chatInfo) { - is ChatInfo.Direct -> !chatInfo.contact.isContactCard && !chatInfo.contact.chatDeleted - is ChatInfo.ContactRequest -> true - is ChatInfo.ContactConnection -> true - is ChatInfo.Group -> chatInfo.groupInfo.businessChat?.chatType == BusinessChatType.Customer - else -> false - } - PresetTagKind.GROUPS -> when (chatInfo) { - is ChatInfo.Group -> chatInfo.groupInfo.businessChat == null - else -> false - } - PresetTagKind.BUSINESS -> when (chatInfo) { - is ChatInfo.Group -> chatInfo.groupInfo.businessChat?.chatType == BusinessChatType.Business - else -> false - } - PresetTagKind.NOTES -> when (chatInfo) { - is ChatInfo.Local -> !chatInfo.noteFolder.chatDeleted - else -> false + fun scrollToBottom(scope: CoroutineScope, listState: LazyListState) { + scope.launch { + try { + listState.animateScrollToItem(0) + } catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) + } } } - -private fun presetTagLabel(tag: PresetTagKind, active: Boolean): Pair = - when (tag) { - PresetTagKind.GROUP_REPORTS -> (if (active) MR.images.ic_flag_filled else MR.images.ic_flag) to MR.strings.chat_list_group_reports - PresetTagKind.FAVORITES -> (if (active) MR.images.ic_star_filled else MR.images.ic_star) to MR.strings.chat_list_favorites - 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 - } - -private fun presetCanBeCollapsed(tag: PresetTagKind): Boolean = when (tag) { - PresetTagKind.GROUP_REPORTS -> false - else -> true -} - -fun scrollToBottom(scope: CoroutineScope, listState: LazyListState) { - scope.launch { try { listState.animateScrollToItem(0) } catch (e: Exception) { Log.e(TAG, e.stackTraceToString()) } } -} diff --git a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/invitation_redesign/NowYouCanView.kt b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/invitation_redesign/NowYouCanView.kt index 75fbc58a95..99da8e31ff 100644 --- a/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/invitation_redesign/NowYouCanView.kt +++ b/apps/multiplatform/common/src/commonMain/kotlin/chat/simplex/common/views/invitation_redesign/NowYouCanView.kt @@ -52,7 +52,8 @@ fun EmptyChatListView(onConnectClick: () -> Unit, onOneTimeLinkClick: () -> Unit Modifier .fillMaxWidth() .padding(horizontal = DEFAULT_PADDING) - .padding(top = AppBarHeight), + .padding(top = AppBarHeight) + .verticalScroll(rememberScrollState()), horizontalAlignment = Alignment.CenterHorizontally ) { Text( @@ -74,7 +75,10 @@ fun EmptyChatListView(onConnectClick: () -> Unit, onOneTimeLinkClick: () -> Unit title = stringResource(MR.strings.connect_via_link_or_qr), modifier = Modifier.fillMaxWidth().clickable { onConnectClick() } ) + + Spacer(Modifier.height(DEFAULT_BIG_PADDING * 3)) } + } }