From 73f18e0f703965185111e50b93752619f7d76f5d Mon Sep 17 00:00:00 2001 From: AbronStudio Date: Tue, 29 Jul 2025 15:08:12 +0330 Subject: [PATCH] refactor: Update navigation and channel handling This commit refines the navigation and channel management within the chat interface. Key changes include: - Introduced a new `backToChannelsScreen` callback in `AppEntrypoint` and `ChannelScreen` to facilitate navigation back to the channels. - Updated `ChatRouterDestination` to include `ServersChannels` and modified the logic to handle channel navigation correctly. - Adjusted the `ChannelSideDrawer` to reflect the new routing logic, ensuring the correct channel is displayed based on the current server. - Removed the back navigation button from the `SettingsScreen` for a cleaner UI experience. - Updated the `ChannelNavigator` to handle the new `Settings` destination and ensure proper rendering of the `SettingsScreen`. - Enhanced the `Sidebar` component to support navigation to the `Settings` screen and manage server-specific channels effectively. --- .../chat/revolt/activities/MainActivity.kt | 1 + .../screens/chat/drawer/ChannelSideDrawer.kt | 8 +- .../revolt/screens/chat/ChatRouterScreen.kt | 86 +- .../chat/views/channel/ChannelScreen.kt | 1129 +++++++++-------- .../revolt/screens/settings/SettingsScreen.kt | 10 - 5 files changed, 627 insertions(+), 607 deletions(-) diff --git a/app/src/main/java/chat/revolt/activities/MainActivity.kt b/app/src/main/java/chat/revolt/activities/MainActivity.kt index c8a9ce47..31e24331 100644 --- a/app/src/main/java/chat/revolt/activities/MainActivity.kt +++ b/app/src/main/java/chat/revolt/activities/MainActivity.kt @@ -697,6 +697,7 @@ fun AppEntrypoint( onToggleDrawer = {}, useDrawer = false, useBackButton = true, + backToChannelsScreen = {}, backButtonAction = { navController.popBackStack() }, diff --git a/app/src/main/java/chat/revolt/composables/screens/chat/drawer/ChannelSideDrawer.kt b/app/src/main/java/chat/revolt/composables/screens/chat/drawer/ChannelSideDrawer.kt index 8b49162c..11d91893 100644 --- a/app/src/main/java/chat/revolt/composables/screens/chat/drawer/ChannelSideDrawer.kt +++ b/app/src/main/java/chat/revolt/composables/screens/chat/drawer/ChannelSideDrawer.kt @@ -121,11 +121,11 @@ fun ChannelSideDrawer( val channelListState = rememberLazyListState() LaunchedEffect(currentDestination) { - if (currentDestination is ChatRouterDestination.Channel && currentServer != null) { + if ((currentDestination is ChatRouterDestination.ServersChannels) && currentServer != null) { val channelIndex = categorisedChannels?.indexOfFirst { when (it) { - is CategorisedChannelList.Channel -> it.channel.id == currentDestination.channelId + is CategorisedChannelList.Channel -> it.channel.id == currentDestination.serverID else -> false } } ?: 0 @@ -614,9 +614,9 @@ fun ColumnScope.DirectMessagesChannelListRenderer( channelType = ChannelType.TextChannel ), iconType = ChannelItemIconType.Painter(painterResource(R.drawable.icn_star_shine_24dp)), - isCurrent = currentDestination is ChatRouterDestination.Overview, + isCurrent = currentDestination is ChatRouterDestination.Settings, onDestinationChanged = { - onDestinationChanged(ChatRouterDestination.Overview) + onDestinationChanged(ChatRouterDestination.Settings) }, hasUnread = false, onOpenChannelContextSheet = {} diff --git a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt index 8ba4d391..2fab97f9 100644 --- a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt @@ -11,7 +11,6 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize @@ -45,7 +44,6 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope @@ -84,8 +82,8 @@ import chat.revolt.screens.chat.dialogs.safety.ReportServerDialog import chat.revolt.screens.chat.dialogs.safety.ReportUserDialog import chat.revolt.screens.chat.views.FriendsScreen import chat.revolt.screens.chat.views.NoCurrentChannelScreen -import chat.revolt.screens.chat.views.OverviewScreen import chat.revolt.screens.chat.views.channel.ChannelScreen +import chat.revolt.screens.settings.SettingsScreen import chat.revolt.sheets.AddServerSheet import chat.revolt.sheets.ChangelogSheet import chat.revolt.sheets.EarlyAccessSheet @@ -107,30 +105,32 @@ import kotlinx.coroutines.launch import javax.inject.Inject sealed class ChatRouterDestination { - data object Overview : ChatRouterDestination() + data object Settings : ChatRouterDestination() data object Friends : ChatRouterDestination() data object Home : ChatRouterDestination() data class Channel(val channelId: String) : ChatRouterDestination() + data class ServersChannels(val serverID: String) : ChatRouterDestination() data class NoCurrentChannel(val serverId: String?) : ChatRouterDestination() fun asSerialisedString(): String { return when (this) { - is Overview -> "overview" + is Settings -> "overview" is Friends -> "friends" is Channel -> "channel/$channelId" + is ServersChannels -> "channel/$serverID/servers" is NoCurrentChannel -> "no_current_channel/$serverId" ChatRouterDestination.Home -> "home" } } companion object { - val default = Overview - val defaultForDMList = Overview + val default = Settings + val defaultForDMList = Settings fun fromString(destination: String): ChatRouterDestination { return when { - destination == "home" -> Overview // previous name for overview - destination == "overview" -> Overview + destination == "home" -> Settings // previous name for overview + destination == "overview" -> Settings destination == "friends" -> Friends destination.startsWith("no_current_channel/") -> NoCurrentChannel( destination.removePrefix( @@ -258,11 +258,8 @@ class ChatRouterViewModel @Inject constructor( savedLastChannel ?: RevoltAPI.serverCache[serverId]?.channels?.firstOrNull() val channelExists = RevoltAPI.channelCache.containsKey(channelId) - if (channelId != null && channelExists) { - setSaveDestination(ChatRouterDestination.Channel(channelId)) - } else { - setSaveDestination(ChatRouterDestination.NoCurrentChannel(serverId)) - } + setSaveDestination(ChatRouterDestination.ServersChannels(serverId)) +// } } } @@ -844,6 +841,7 @@ fun Sidebar( currentServer: String?, topNav: NavController, onShowStatusSheet: () -> Unit, + navigateToServer: (String) -> Unit, onShowServerContextSheet: (String) -> Unit, onShowAddServerSheet: () -> Unit, showSettingsButton: Boolean, @@ -853,7 +851,7 @@ fun Sidebar( onDestinationChanged = viewModel::setSaveDestination, currentDestination = viewModel.currentDestination, currentServer = currentServer, - navigateToServer = viewModel::navigateToServer, + navigateToServer = navigateToServer, onLongPressAvatar = onShowStatusSheet, onShowServerContextSheet = onShowServerContextSheet, showSettingsIcon = showSettingsButton, @@ -925,10 +923,10 @@ fun ChannelNavigator( label = { Text(text = "you") }, - selected = dest is ChatRouterDestination.Overview, + selected = dest is ChatRouterDestination.Settings, enabled = true, onClick = { - viewModel.setSaveDestination(ChatRouterDestination.Overview) + viewModel.setSaveDestination(ChatRouterDestination.Settings) } ) } @@ -936,10 +934,9 @@ fun ChannelNavigator( ) { innerPadding -> Column { when (dest) { - is ChatRouterDestination.Overview -> { - OverviewScreen( + is ChatRouterDestination.Settings -> { + SettingsScreen( navController = topNav, - onDrawerClicked = toggleDrawer, ) } @@ -951,25 +948,46 @@ fun ChannelNavigator( } is ChatRouterDestination.Home -> { - Row { - Sidebar( - viewModel = viewModel, - topNav = topNav, - currentServer = currentServer, - onShowStatusSheet = onShowStatusSheet, - onShowServerContextSheet = onShowServerContextSheet, - onShowAddServerSheet = onShowAddServerSheet, - showSettingsButton = isTouchExplorationEnabled, - onOpenSettings = { - topNav.navigate("settings") - }, - ) - } + Sidebar( + viewModel = viewModel, + topNav = topNav, + currentServer = currentServer, + onShowStatusSheet = onShowStatusSheet, + navigateToServer = viewModel::navigateToServer, + onShowServerContextSheet = onShowServerContextSheet, + onShowAddServerSheet = onShowAddServerSheet, + showSettingsButton = isTouchExplorationEnabled, + onOpenSettings = { + topNav.navigate("settings") + }, + ) + } + is ChatRouterDestination.ServersChannels-> { + Sidebar( + viewModel = viewModel, + topNav = topNav, + currentServer = dest.serverID, + onShowStatusSheet = onShowStatusSheet, + navigateToServer = viewModel::navigateToServer, + onShowServerContextSheet = onShowServerContextSheet, + onShowAddServerSheet = onShowAddServerSheet, + showSettingsButton = isTouchExplorationEnabled, + onOpenSettings = { + topNav.navigate("settings") + }, + ) } is ChatRouterDestination.Channel -> { ChannelScreen( channelId = dest.channelId, + backToChannelsScreen = { + currentServer?.let { + viewModel.setSaveDestination(ChatRouterDestination.ServersChannels( + serverID = currentServer + )) + } + }, onToggleDrawer = { scope.launch { if (drawerState?.isOpen == true) { diff --git a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt index 6e73fe4e..728d4d58 100644 --- a/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/views/channel/ChannelScreen.kt @@ -191,6 +191,7 @@ fun ChannelScreen( channelId: String, onToggleDrawer: () -> Unit, useDrawer: Boolean = false, + backToChannelsScreen: (() -> Unit)?, useBackButton: Boolean = false, setDrawerGestureEnabled: (Boolean) -> Unit = {}, drawerIsOpen: Boolean = false, @@ -203,6 +204,12 @@ fun ChannelScreen( val context = LocalContext.current val config = LocalConfiguration.current + backToChannelsScreen?.let { + BackHandler { + backToChannelsScreen() + } + } + LaunchedEffect(Unit) { viewModel.listenToWsEvents() } @@ -649,627 +656,631 @@ fun ChannelScreen( targetState = viewModel.ageGateUnlocked, label = "ageGateUnlocked" ) { ageGateUnlocked -> - if (ageGateUnlocked == false) { - ChannelScreenAgeGate( - onAccept = { - scope.launch { - viewModel.unlockAgeGate() - } - }, - onDeny = { - onToggleDrawer() - } - ) - } else if (ageGateUnlocked == null) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxSize(), - ) { - CircularProgressIndicator(modifier = Modifier.size(48.dp)) - } - } else if (ageGateUnlocked == true) { - Column( - modifier = Modifier - .padding(pv) - ) { - Box( - modifier = Modifier.weight(1f), - contentAlignment = Alignment.BottomCenter - ) { - LazyColumn( - state = lazyListState, - userScrollEnabled = !disableScroll, - reverseLayout = true, - contentPadding = PaddingValues(top = 16.dp, bottom = 32.dp) - ) { - - // If we don't have a guaranteed first item, the message list will not scroll - // to the bottom when new messages are added. Evil hack to make our other evil - // hack (clear/addAll) work. Too bad! - item(key = "guaranteed_first") { - Box {} + when (ageGateUnlocked) { + false -> { + ChannelScreenAgeGate( + onAccept = { + scope.launch { + viewModel.unlockAgeGate() } + }, + onDeny = { + onToggleDrawer() + } + ) + } + null -> { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize(), + ) { + CircularProgressIndicator(modifier = Modifier.size(48.dp)) + } + } + true -> { + Column( + modifier = Modifier + .padding(pv) + ) { + Box( + modifier = Modifier.weight(1f), + contentAlignment = Alignment.BottomCenter + ) { + LazyColumn( + state = lazyListState, + userScrollEnabled = !disableScroll, + reverseLayout = true, + contentPadding = PaddingValues(top = 16.dp, bottom = 32.dp) + ) { - items( - viewModel.items.size, - key = { index -> - when (val item = viewModel.items[index]) { - is ChannelScreenItem.RegularMessage -> item.message.id!! - is ChannelScreenItem.ProspectiveMessage -> item.message.id!! - is ChannelScreenItem.FailedMessage -> item.message.id!! - is ChannelScreenItem.SystemMessage -> item.message.id!! - is ChannelScreenItem.DateDivider -> item.instant.toEpochMilliseconds() - is ChannelScreenItem.LoadTrigger -> index - is ChannelScreenItem.Loading -> index - } - }, - contentType = { index -> - when (viewModel.items.getOrNull(index)) { - null -> null - is ChannelScreenItem.RegularMessage -> "RegularMessage" - is ChannelScreenItem.ProspectiveMessage -> "ProspectiveMessage" - is ChannelScreenItem.FailedMessage -> "FailedMessage" - is ChannelScreenItem.SystemMessage -> "SystemMessage" - is ChannelScreenItem.DateDivider -> "DateDivider" - is ChannelScreenItem.LoadTrigger -> "LoadTrigger" - is ChannelScreenItem.Loading -> "Loading" - } + // If we don't have a guaranteed first item, the message list will not scroll + // to the bottom when new messages are added. Evil hack to make our other evil + // hack (clear/addAll) work. Too bad! + item(key = "guaranteed_first") { + Box {} } - ) { index -> - when (val item = viewModel.items[index]) { - is ChannelScreenItem.RegularMessage -> { - RegularMessage( - item.message, - viewModel.channel, - drawerIsOpen = drawerIsOpen, - setDrawerGestureEnabled = { - setDrawerGestureEnabled(it) - }, - setDisableScroll = { - disableScroll = it - }, - showMessageBottomSheet = { - messageContextSheetTarget = it - messageContextSheetShown = true - }, - showReactBottomSheet = { - item.message.id?.let { - reactSheetTarget = it - reactSheetShown = true - } - }, - putTextAtCursorPosition = viewModel::putAtCursorPosition, - replyToMessage = viewModel::addReplyTo, - scope = scope - ) - } - is ChannelScreenItem.ProspectiveMessage -> { - Box(Modifier.alpha(0.5f)) { - Message( - message = item.message, - onMessageContextMenu = { - // TODO Context menu that allows you to cancel send - }, - onAvatarClick = {}, - onNameClick = {}, - canReply = false, - onReply = {}, - onAddReaction = {} - ) + items( + viewModel.items.size, + key = { index -> + when (val item = viewModel.items[index]) { + is ChannelScreenItem.RegularMessage -> item.message.id!! + is ChannelScreenItem.ProspectiveMessage -> item.message.id!! + is ChannelScreenItem.FailedMessage -> item.message.id!! + is ChannelScreenItem.SystemMessage -> item.message.id!! + is ChannelScreenItem.DateDivider -> item.instant.toEpochMilliseconds() + is ChannelScreenItem.LoadTrigger -> index + is ChannelScreenItem.Loading -> index + } + }, + contentType = { index -> + when (viewModel.items.getOrNull(index)) { + null -> null + is ChannelScreenItem.RegularMessage -> "RegularMessage" + is ChannelScreenItem.ProspectiveMessage -> "ProspectiveMessage" + is ChannelScreenItem.FailedMessage -> "FailedMessage" + is ChannelScreenItem.SystemMessage -> "SystemMessage" + is ChannelScreenItem.DateDivider -> "DateDivider" + is ChannelScreenItem.LoadTrigger -> "LoadTrigger" + is ChannelScreenItem.Loading -> "Loading" } } + ) { index -> + when (val item = viewModel.items[index]) { + is ChannelScreenItem.RegularMessage -> { + RegularMessage( + item.message, + viewModel.channel, + drawerIsOpen = drawerIsOpen, + setDrawerGestureEnabled = { + setDrawerGestureEnabled(it) + }, + setDisableScroll = { + disableScroll = it + }, + showMessageBottomSheet = { + messageContextSheetTarget = it + messageContextSheetShown = true + }, + showReactBottomSheet = { + item.message.id?.let { + reactSheetTarget = it + reactSheetShown = true + } + }, + putTextAtCursorPosition = viewModel::putAtCursorPosition, + replyToMessage = viewModel::addReplyTo, + scope = scope + ) + } - is ChannelScreenItem.FailedMessage -> { - CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) { - Column { + is ChannelScreenItem.ProspectiveMessage -> { + Box(Modifier.alpha(0.5f)) { Message( message = item.message, - onMessageContextMenu = {}, + onMessageContextMenu = { + // TODO Context menu that allows you to cancel send + }, onAvatarClick = {}, onNameClick = {}, canReply = false, onReply = {}, onAddReaction = {} ) - Row { - UserAvatarWidthPlaceholder() - Text( - stringResource(R.string.message_failed_to_send), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.error.copy( - alpha = 0.8f - ), - modifier = Modifier.padding( - top = 4.dp, - bottom = 4.dp, - start = 20.dp - ) - ) - } } } - } - is ChannelScreenItem.SystemMessage -> { - SystemMessage(message = item.message) - } - - is ChannelScreenItem.DateDivider -> { - DateDivider(instant = item.instant) - } - - is ChannelScreenItem.LoadTrigger -> { - LaunchedEffect(Unit) { - Log.d( - "ChannelScreen", - "LoadTrigger: After ${item.after} Before ${item.before}" - ) - } - } - - is ChannelScreenItem.Loading -> { - Column( - modifier = Modifier - .fillMaxWidth() - .shimmer(rememberShimmer(ShimmerBounds.Window)), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - MessageSkeleton(MessageSkeletonVariant.One) - MessageSkeleton(MessageSkeletonVariant.Two) - MessageSkeleton(MessageSkeletonVariant.Three) - } - } - } - } - } - - TypingIndicator( - users = viewModel.typingUsers, - serverId = viewModel.channel?.server - ) - - androidx.compose.animation.AnimatedVisibility( - !isScrolledToBottom.value, - enter = slideInVertically( - animationSpec = RevoltTweenInt, - initialOffsetY = { it } - ) + fadeIn(animationSpec = RevoltTweenFloat), - exit = slideOutVertically( - animationSpec = RevoltTweenInt, - targetOffsetY = { it } - ) + fadeOut(animationSpec = RevoltTweenFloat) - ) { - SmallFloatingActionButton( - modifier = Modifier - .padding(bottom = scrollDownFABPadding) - .align(Alignment.BottomCenter) - .padding(16.dp), - onClick = { - scope.launch { - lazyListState.animateScrollToItem(0) - } - }, - contentColor = MaterialTheme.colorScheme.onSurfaceVariant, - containerColor = MaterialTheme.colorScheme.surfaceVariant - ) { - Icon( - painter = painterResource(R.drawable.icn_south_24dp), - contentDescription = stringResource(R.string.scroll_to_bottom) - ) - } - } - - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier - .align(Alignment.TopCenter) - .padding(8.dp) - ) { - if (viewModel.showPhysicalKeyboardSpark) { - Card { - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.padding(16.dp) - ) { - Text( - stringResource(R.string.spark_keyboard_shortcuts), - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.SemiBold - ) - Text( - buildAnnotatedString { - val raw = - stringResource(R.string.spark_keyboard_shortcuts_description) - val before = raw.substringBefore("%1\$s") - val after = raw.substringAfter("%1\$s") - - append(before) - appendInlineContent("metaKey", "Meta") - append(" + /") - append(after) - }, - inlineContent = mapOf( - "metaKey" to InlineTextContent( - placeholder = Placeholder( - width = 1.em, - height = 1.em, - placeholderVerticalAlign = PlaceholderVerticalAlign.Center + is ChannelScreenItem.FailedMessage -> { + CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) { + Column { + Message( + message = item.message, + onMessageContextMenu = {}, + onAvatarClick = {}, + onNameClick = {}, + canReply = false, + onReply = {}, + onAddReaction = {} ) - ) { - with(LocalDensity.current) { - Image( - painterResource(R.drawable.ic_meta_key_24dp), - contentDescription = null, - colorFilter = ColorFilter.tint( - LocalContentColor.current + Row { + UserAvatarWidthPlaceholder() + Text( + stringResource(R.string.message_failed_to_send), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error.copy( + alpha = 0.8f + ), + modifier = Modifier.padding( + top = 4.dp, + bottom = 4.dp, + start = 20.dp ) ) } } - ), - style = MaterialTheme.typography.bodyLarge - ) - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Button( - onClick = { - viewModel.dismissPhysicalKeyboardSpark() - }, - modifier = Modifier.weight(1f) - ) { - Text(stringResource(R.string.spark_keyboard_shortcuts_dismiss)) } - TextButton( - onClick = { - (context as Activity).requestShowKeyboardShortcuts() - }, - modifier = Modifier.weight(1f) + } + + is ChannelScreenItem.SystemMessage -> { + SystemMessage(message = item.message) + } + + is ChannelScreenItem.DateDivider -> { + DateDivider(instant = item.instant) + } + + is ChannelScreenItem.LoadTrigger -> { + LaunchedEffect(Unit) { + Log.d( + "ChannelScreen", + "LoadTrigger: After ${item.after} Before ${item.before}" + ) + } + } + + is ChannelScreenItem.Loading -> { + Column( + modifier = Modifier + .fillMaxWidth() + .shimmer(rememberShimmer(ShimmerBounds.Window)), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) ) { - Text(stringResource(R.string.spark_keyboard_shortcuts_cta)) + MessageSkeleton(MessageSkeletonVariant.One) + MessageSkeleton(MessageSkeletonVariant.Two) + MessageSkeleton(MessageSkeletonVariant.Three) } } } } } - if (viewModel.channel?.channelType == ChannelType.VoiceChannel - && FeatureFlags.voiceChannels2_0Granted + TypingIndicator( + users = viewModel.typingUsers, + serverId = viewModel.channel?.server + ) + + androidx.compose.animation.AnimatedVisibility( + !isScrolledToBottom.value, + enter = slideInVertically( + animationSpec = RevoltTweenInt, + initialOffsetY = { it } + ) + fadeIn(animationSpec = RevoltTweenFloat), + exit = slideOutVertically( + animationSpec = RevoltTweenInt, + targetOffsetY = { it } + ) + fadeOut(animationSpec = RevoltTweenFloat) ) { - JoinVoiceChannelButton(channelId) + SmallFloatingActionButton( + modifier = Modifier + .padding(bottom = scrollDownFABPadding) + .align(Alignment.BottomCenter) + .padding(16.dp), + onClick = { + scope.launch { + lazyListState.animateScrollToItem(0) + } + }, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant, + containerColor = MaterialTheme.colorScheme.surfaceVariant + ) { + Icon( + painter = painterResource(R.drawable.icn_south_24dp), + contentDescription = stringResource(R.string.scroll_to_bottom) + ) + } } - } - } - Column( - modifier = Modifier - .background(MaterialTheme.colorScheme.surfaceContainer) - .fillMaxWidth(), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - AnimatedContent( - targetState = viewModel.denyMessageField, - label = "denyMessageField" - ) { deny -> - if (!deny) { - Column { - AnimatedVisibility( - visible = viewModel.draftReplyTo.isNotEmpty() && !viewModel.denyMessageField - ) { - ReplyManager( - replies = viewModel.draftReplyTo, - onToggleMention = { - scope.launch { viewModel.toggleMentionOnReply(it.id) } - }, - onRemove = { - viewModel.draftReplyTo.remove(it) - } - ) - } + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .align(Alignment.TopCenter) + .padding(8.dp) + ) { + if (viewModel.showPhysicalKeyboardSpark) { + Card { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(16.dp) + ) { + Text( + stringResource(R.string.spark_keyboard_shortcuts), + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.SemiBold + ) + Text( + buildAnnotatedString { + val raw = + stringResource(R.string.spark_keyboard_shortcuts_description) + val before = raw.substringBefore("%1\$s") + val after = raw.substringAfter("%1\$s") - AnimatedVisibility( - visible = viewModel.draftAttachments.isNotEmpty() && !viewModel.denyMessageField - ) { - AttachmentManager( - attachments = viewModel.draftAttachments, - uploading = viewModel.attachmentUploadProgress > 0, - uploadProgress = viewModel.attachmentUploadProgress, - canRemove = true, - canPreview = true, - onRemove = { - viewModel.draftAttachments.remove(it) - }, - onToggleSpoiler = { - val index = viewModel.draftAttachments - .indexOfFirst { a -> a.pickerIdentifier == it.pickerIdentifier } - - if (index != -1) { - val attachment = - viewModel.draftAttachments[index] - viewModel.draftAttachments[index] = - attachment.copy( - spoiler = !attachment.spoiler + append(before) + appendInlineContent("metaKey", "Meta") + append(" + /") + append(after) + }, + inlineContent = mapOf( + "metaKey" to InlineTextContent( + placeholder = Placeholder( + width = 1.em, + height = 1.em, + placeholderVerticalAlign = PlaceholderVerticalAlign.Center ) + ) { + with(LocalDensity.current) { + Image( + painterResource(R.drawable.ic_meta_key_24dp), + contentDescription = null, + colorFilter = ColorFilter.tint( + LocalContentColor.current + ) + ) + } + } + ), + style = MaterialTheme.typography.bodyLarge + ) + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + Button( + onClick = { + viewModel.dismissPhysicalKeyboardSpark() + }, + modifier = Modifier.weight(1f) + ) { + Text(stringResource(R.string.spark_keyboard_shortcuts_dismiss)) + } + TextButton( + onClick = { + (context as Activity).requestShowKeyboardShortcuts() + }, + modifier = Modifier.weight(1f) + ) { + Text(stringResource(R.string.spark_keyboard_shortcuts_cta)) } } - ) + } } + } - AnimatedVisibility(visible = viewModel.editingMessage != null) { - Row(Modifier.padding(start = 24.dp, top = 8.dp)) { - AssistChip( - onClick = { - viewModel.editingMessage = null - viewModel.putDraftContent("", true) + if (viewModel.channel?.channelType == ChannelType.VoiceChannel + && FeatureFlags.voiceChannels2_0Granted + ) { + JoinVoiceChannelButton(channelId) + } + } + } + + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.surfaceContainer) + .fillMaxWidth(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + AnimatedContent( + targetState = viewModel.denyMessageField, + label = "denyMessageField" + ) { deny -> + if (!deny) { + Column { + AnimatedVisibility( + visible = viewModel.draftReplyTo.isNotEmpty() && !viewModel.denyMessageField + ) { + ReplyManager( + replies = viewModel.draftReplyTo, + onToggleMention = { + scope.launch { viewModel.toggleMentionOnReply(it.id) } }, - label = { - Text(stringResource(R.string.message_field_editing_message)) + onRemove = { + viewModel.draftReplyTo.remove(it) + } + ) + } + + AnimatedVisibility( + visible = viewModel.draftAttachments.isNotEmpty() && !viewModel.denyMessageField + ) { + AttachmentManager( + attachments = viewModel.draftAttachments, + uploading = viewModel.attachmentUploadProgress > 0, + uploadProgress = viewModel.attachmentUploadProgress, + canRemove = true, + canPreview = true, + onRemove = { + viewModel.draftAttachments.remove(it) }, + onToggleSpoiler = { + val index = viewModel.draftAttachments + .indexOfFirst { a -> a.pickerIdentifier == it.pickerIdentifier } + + if (index != -1) { + val attachment = + viewModel.draftAttachments[index] + viewModel.draftAttachments[index] = + attachment.copy( + spoiler = !attachment.spoiler + ) + } + } + ) + } + + AnimatedVisibility(visible = viewModel.editingMessage != null) { + Row(Modifier.padding(start = 24.dp, top = 8.dp)) { + AssistChip( + onClick = { + viewModel.editingMessage = null + viewModel.putDraftContent("", true) + }, + label = { + Text(stringResource(R.string.message_field_editing_message)) + }, + leadingIcon = { + Icon( + painter = painterResource(R.drawable.icn_edit_24dp), + contentDescription = null + ) + }, + trailingIcon = { + Icon( + painter = painterResource(R.drawable.icn_close_24dp), + contentDescription = stringResource(R.string.message_field_editing_message_cancel_alt), + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.alpha(0.8f) + ) + } + ) + } + } + + MessageField( + initialValue = viewModel.initialTextFieldValue, + initialValueDirtyMarker = viewModel.initialTextFieldValueDirtyMarker, + onValueChange = viewModel::putDraftContent, + onAddAttachment = { + if (viewModel.activePane == ChannelScreenActivePane.AttachmentPicker) { + viewModel.activePane = + ChannelScreenActivePane.None + } else { + viewModel.activePane = + ChannelScreenActivePane.AttachmentPicker + } + }, + onCommitAttachment = { + processFileUri(it, null) + }, + onPickEmoji = { + if (viewModel.activePane == ChannelScreenActivePane.EmojiPicker) { + viewModel.activePane = + ChannelScreenActivePane.None + } else { + viewModel.activePane = + ChannelScreenActivePane.EmojiPicker + } + }, + onSendMessage = viewModel::sendPendingMessage, + channelType = viewModel.channel?.channelType + ?: ChannelType.TextChannel, + channelName = viewModel.channel?.let { channel -> + ChannelUtils.resolveName(channel) + } + ?: stringResource(R.string.unknown), + onFocusChange = { isFocused -> + if (isFocused && viewModel.activePane != ChannelScreenActivePane.None) { + viewModel.activePane = + ChannelScreenActivePane.None + imeInTransition = true + } + }, + forceSendButton = viewModel.draftAttachments.isNotEmpty(), + canAttach = (channelPermissions has PermissionBit.UploadFiles) && viewModel.editingMessage == null, + serverId = viewModel.channel?.server, + channelId = channelId, + failedValidation = viewModel.draftContent.length > 2000, + valueIsBlank = viewModel.draftContent.isBlank(), + cancelEdit = { + viewModel.editingMessage = null + viewModel.putDraftContent("", true) + } + ) + + DropdownMenu( + expanded = viewModel.activePane == ChannelScreenActivePane.AttachmentPicker && notEnoughSpaceForPanes, + onDismissRequest = { + viewModel.activePane = ChannelScreenActivePane.None + } + ) { + DropdownMenuItem( leadingIcon = { Icon( - painter = painterResource(R.drawable.icn_edit_24dp), - contentDescription = null + painter = painterResource(R.drawable.icn_attach_file_24dp), + contentDescription = null // Provided by text below ) }, - trailingIcon = { + text = { Text(stringResource(R.string.file_picker_chip_documents)) }, + onClick = { + openDocumentPickerCallback() + viewModel.activePane = + ChannelScreenActivePane.None + } + ) + DropdownMenuItem( + leadingIcon = { Icon( - painter = painterResource(R.drawable.icn_close_24dp), - contentDescription = stringResource(R.string.message_field_editing_message_cancel_alt), - tint = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.alpha(0.8f) + painter = painterResource(R.drawable.icn_camera_24dp), + contentDescription = null // Provided by text below ) + }, + text = { Text(stringResource(R.string.file_picker_chip_camera)) }, + onClick = { + openCameraCallback() + viewModel.activePane = + ChannelScreenActivePane.None + } + ) + DropdownMenuItem( + leadingIcon = { + Icon( + painter = painterResource(R.drawable.icn_photo_library_24dp), + contentDescription = null // Provided by text below + ) + }, + text = { Text(stringResource(R.string.file_picker_chip_photo_picker)) }, + onClick = { + openPhotoPickerCallback() + viewModel.activePane = + ChannelScreenActivePane.None } ) } } - - MessageField( - initialValue = viewModel.initialTextFieldValue, - initialValueDirtyMarker = viewModel.initialTextFieldValueDirtyMarker, - onValueChange = viewModel::putDraftContent, - onAddAttachment = { - if (viewModel.activePane == ChannelScreenActivePane.AttachmentPicker) { - viewModel.activePane = - ChannelScreenActivePane.None - } else { - viewModel.activePane = - ChannelScreenActivePane.AttachmentPicker - } - }, - onCommitAttachment = { - processFileUri(it, null) - }, - onPickEmoji = { - if (viewModel.activePane == ChannelScreenActivePane.EmojiPicker) { - viewModel.activePane = - ChannelScreenActivePane.None - } else { - viewModel.activePane = - ChannelScreenActivePane.EmojiPicker - } - }, - onSendMessage = viewModel::sendPendingMessage, - channelType = viewModel.channel?.channelType - ?: ChannelType.TextChannel, - channelName = viewModel.channel?.let { channel -> - ChannelUtils.resolveName(channel) - } - ?: stringResource(R.string.unknown), - onFocusChange = { isFocused -> - if (isFocused && viewModel.activePane != ChannelScreenActivePane.None) { - viewModel.activePane = - ChannelScreenActivePane.None - imeInTransition = true - } - }, - forceSendButton = viewModel.draftAttachments.isNotEmpty(), - canAttach = (channelPermissions has PermissionBit.UploadFiles) && viewModel.editingMessage == null, - serverId = viewModel.channel?.server, - channelId = channelId, - failedValidation = viewModel.draftContent.length > 2000, - valueIsBlank = viewModel.draftContent.isBlank(), - cancelEdit = { - viewModel.editingMessage = null - viewModel.putDraftContent("", true) - } - ) - - DropdownMenu( - expanded = viewModel.activePane == ChannelScreenActivePane.AttachmentPicker && notEnoughSpaceForPanes, - onDismissRequest = { - viewModel.activePane = ChannelScreenActivePane.None - } + } else { + Box( + modifier = Modifier + .padding(horizontal = 32.dp, vertical = 16.dp) ) { - DropdownMenuItem( - leadingIcon = { - Icon( - painter = painterResource(R.drawable.icn_attach_file_24dp), - contentDescription = null // Provided by text below - ) - }, - text = { Text(stringResource(R.string.file_picker_chip_documents)) }, - onClick = { - openDocumentPickerCallback() - viewModel.activePane = - ChannelScreenActivePane.None - } - ) - DropdownMenuItem( - leadingIcon = { - Icon( - painter = painterResource(R.drawable.icn_camera_24dp), - contentDescription = null // Provided by text below - ) - }, - text = { Text(stringResource(R.string.file_picker_chip_camera)) }, - onClick = { - openCameraCallback() - viewModel.activePane = - ChannelScreenActivePane.None - } - ) - DropdownMenuItem( - leadingIcon = { - Icon( - painter = painterResource(R.drawable.icn_photo_library_24dp), - contentDescription = null // Provided by text below - ) - }, - text = { Text(stringResource(R.string.file_picker_chip_photo_picker)) }, - onClick = { - openPhotoPickerCallback() - viewModel.activePane = - ChannelScreenActivePane.None - } + Text( + stringResource(viewModel.denyMessageFieldReasonResource), + color = MaterialTheme.colorScheme.error, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center ) } } - } else { - Box( - modifier = Modifier - .padding(horizontal = 32.dp, vertical = 16.dp) - ) { - Text( - stringResource(viewModel.denyMessageFieldReasonResource), - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.bodyMedium, - textAlign = TextAlign.Center - ) - } } - } - if (viewModel.activePane == ChannelScreenActivePane.None && !imeInTransition) { - Spacer( - Modifier - .imePadding() - .navigationBarsPadding() - .background(MaterialTheme.colorScheme.surfaceContainer) - ) - } else { - if (!notEnoughSpaceForPanes) { - Box( - Modifier - .heightIn(min = pxAsDp(fallbackKeyboardHeight)) - ) { - Box( - Modifier.then( - if (emojiSearchFocused) { - Modifier.requiredHeight( - pxAsDp( - max( - imeCurrentInset * 2, - fallbackKeyboardHeight - ) - ) - ) - } else { - Modifier.requiredHeight( - pxAsDp( - fallbackKeyboardHeight - ) - ) - } - ) - ) { - when (viewModel.activePane) { - ChannelScreenActivePane.EmojiPicker -> { - BackHandler(enabled = viewModel.activePane == ChannelScreenActivePane.EmojiPicker) { - viewModel.activePane = - ChannelScreenActivePane.None - } - - Column( - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.surfaceContainer) - .padding(4.dp) - .navigationBarsPadding() - ) { - EmojiPicker( - onEmojiSelected = viewModel::putAtCursorPosition, - bottomInset = pxAsDp( - max( - imeCurrentInset - navigationBarsInset, - 0 - ) - ), - onSearchFocus = { - emojiSearchFocused = it - } - ) - } - } - - ChannelScreenActivePane.AttachmentPicker -> { - BackHandler(enabled = viewModel.activePane == ChannelScreenActivePane.AttachmentPicker) { - viewModel.activePane = - ChannelScreenActivePane.None - } - - MediaPickerGateway( - onOpenPhotoPicker = { - openPhotoPickerCallback() - viewModel.activePane = - ChannelScreenActivePane.None - }, - onOpenDocumentPicker = { - openDocumentPickerCallback() - viewModel.activePane = - ChannelScreenActivePane.None - }, - onOpenCamera = { - openCameraCallback() - viewModel.activePane = - ChannelScreenActivePane.None - }, - ) - } - - else -> { - // Do nothing - } - } - } - Box(Modifier.imePadding()) - } - } else { - if (viewModel.activePane == ChannelScreenActivePane.EmojiPicker) { - BackHandler(enabled = viewModel.activePane == ChannelScreenActivePane.EmojiPicker) { - viewModel.activePane = - ChannelScreenActivePane.None - } - - Column( - modifier = Modifier - .fillMaxWidth() - .height(600.dp) - .background(MaterialTheme.colorScheme.surfaceContainer) - .padding(4.dp) - .navigationBarsPadding() - ) { - EmojiPicker( - onEmojiSelected = viewModel::putAtCursorPosition, - bottomInset = pxAsDp( - max( - imeCurrentInset - navigationBarsInset, - 0 - ) - ), - onSearchFocus = { - emojiSearchFocused = it - } - ) - } - } - Box( + if (viewModel.activePane == ChannelScreenActivePane.None && !imeInTransition) { + Spacer( Modifier .imePadding() .navigationBarsPadding() + .background(MaterialTheme.colorScheme.surfaceContainer) ) + } else { + if (!notEnoughSpaceForPanes) { + Box( + Modifier + .heightIn(min = pxAsDp(fallbackKeyboardHeight)) + ) { + Box( + Modifier.then( + if (emojiSearchFocused) { + Modifier.requiredHeight( + pxAsDp( + max( + imeCurrentInset * 2, + fallbackKeyboardHeight + ) + ) + ) + } else { + Modifier.requiredHeight( + pxAsDp( + fallbackKeyboardHeight + ) + ) + } + ) + ) { + when (viewModel.activePane) { + ChannelScreenActivePane.EmojiPicker -> { + BackHandler(enabled = viewModel.activePane == ChannelScreenActivePane.EmojiPicker) { + viewModel.activePane = + ChannelScreenActivePane.None + } + + Column( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceContainer) + .padding(4.dp) + .navigationBarsPadding() + ) { + EmojiPicker( + onEmojiSelected = viewModel::putAtCursorPosition, + bottomInset = pxAsDp( + max( + imeCurrentInset - navigationBarsInset, + 0 + ) + ), + onSearchFocus = { + emojiSearchFocused = it + } + ) + } + } + + ChannelScreenActivePane.AttachmentPicker -> { + BackHandler(enabled = viewModel.activePane == ChannelScreenActivePane.AttachmentPicker) { + viewModel.activePane = + ChannelScreenActivePane.None + } + + MediaPickerGateway( + onOpenPhotoPicker = { + openPhotoPickerCallback() + viewModel.activePane = + ChannelScreenActivePane.None + }, + onOpenDocumentPicker = { + openDocumentPickerCallback() + viewModel.activePane = + ChannelScreenActivePane.None + }, + onOpenCamera = { + openCameraCallback() + viewModel.activePane = + ChannelScreenActivePane.None + }, + ) + } + + else -> { + // Do nothing + } + } + } + Box(Modifier.imePadding()) + } + } else { + if (viewModel.activePane == ChannelScreenActivePane.EmojiPicker) { + BackHandler(enabled = viewModel.activePane == ChannelScreenActivePane.EmojiPicker) { + viewModel.activePane = + ChannelScreenActivePane.None + } + + Column( + modifier = Modifier + .fillMaxWidth() + .height(600.dp) + .background(MaterialTheme.colorScheme.surfaceContainer) + .padding(4.dp) + .navigationBarsPadding() + ) { + EmojiPicker( + onEmojiSelected = viewModel::putAtCursorPosition, + bottomInset = pxAsDp( + max( + imeCurrentInset - navigationBarsInset, + 0 + ) + ), + onSearchFocus = { + emojiSearchFocused = it + } + ) + } + } + Box( + Modifier + .imePadding() + .navigationBarsPadding() + ) + } } } } diff --git a/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt b/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt index 01af3010..877eefa5 100644 --- a/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt @@ -80,16 +80,6 @@ fun SettingsScreen( overflow = TextOverflow.Ellipsis, ) }, - navigationIcon = { - IconButton(onClick = { - navController.popBackStack() - }) { - Icon( - painter = painterResource(R.drawable.icn_arrow_back_24dp), - contentDescription = stringResource(id = R.string.back) - ) - } - }, ) }, ) { pv ->