diff --git a/core/ui/src/main/res/values-ru/strings.xml b/core/ui/src/main/res/values-ru/strings.xml index 71ec819b..458b610d 100644 --- a/core/ui/src/main/res/values-ru/strings.xml +++ b/core/ui/src/main/res/values-ru/strings.xml @@ -269,6 +269,7 @@ Курсив Подчёркнутый Ссылка + Обычный Регистрация Забыли пароль? diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index 02c6ed12..6bc4b50a 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -345,6 +345,7 @@ Italic Underline Link + Regular Sign up Forgot password? diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt index 9dd3ef9f..ed63f6d3 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt @@ -106,10 +106,7 @@ fun ConversationsScreen( onErrorViewButtonClicked: () -> Unit = {} ) { val currentTheme = LocalThemeConfig.current - - val maxLines by remember(currentTheme) { - mutableIntStateOf(if (currentTheme.enableMultiline) 2 else 1) - } + val maxLines = if (currentTheme.enableMultiline) 2 else 1 val listState = rememberLazyListState( initialFirstVisibleItemIndex = screenState.scrollIndex, diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt index 0d5f50d4..a225e50d 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt @@ -111,6 +111,7 @@ interface MessagesHistoryViewModel { fun onItalicClicked() fun onUnderlineClicked() fun onLinkClicked() + fun onRegularClicked() } class MessagesHistoryViewModelImpl( @@ -571,6 +572,11 @@ class MessagesHistoryViewModelImpl( } + override fun onRegularClicked() { + formatData = formatData.copy(items = emptyList()) + updateStyles() + } + private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) { val message = event.message diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryInputBar.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryInputBar.kt new file mode 100644 index 00000000..948e1c83 --- /dev/null +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryInputBar.kt @@ -0,0 +1,340 @@ +package dev.meloda.fast.messageshistory.presentation + +import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imeNestedScroll +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.contextmenu.builder.item +import androidx.compose.foundation.text.contextmenu.modifier.addTextContextMenuComponents +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.core.view.HapticFeedbackConstantsCompat +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi +import dev.chrisbanes.haze.materials.HazeMaterials +import dev.meloda.fast.datastore.AppSettings +import dev.meloda.fast.messageshistory.model.ActionMode +import dev.meloda.fast.ui.components.IconButton +import dev.meloda.fast.ui.theme.LocalThemeConfig +import kotlinx.coroutines.launch +import dev.meloda.fast.ui.R as UiR + +@OptIn(ExperimentalLayoutApi::class, ExperimentalHazeMaterialsApi::class) +@Composable +fun MessagesHistoryInputBar( + modifier: Modifier = Modifier, + message: TextFieldValue, + hazeState: HazeState, + showEmojiButton: Boolean, + actionMode: ActionMode, + onMessageInputChanged: (TextFieldValue) -> Unit = {}, + onBoldRequested: () -> Unit = {}, + onItalicRequested: () -> Unit = {}, + onUnderlineRequested: () -> Unit = {}, + onLinkRequested: () -> Unit = {}, + onRegularRequested: () -> Unit = {}, + onSetMessageBarHeight: (Dp) -> Unit = {}, + onEmojiButtonLongClicked: () -> Unit = {}, + onAttachmentButtonClicked: () -> Unit = {}, + onActionButtonClicked: () -> Unit = {} +) { + val view = LocalView.current + val context = LocalContext.current + val density = LocalDensity.current + + val theme = LocalThemeConfig.current + + Column( + modifier = modifier + .fillMaxWidth() + .background(Color.Transparent) + .padding(bottom = 8.dp) + .navigationBarsPadding() + .imePadding() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 60.dp) + .imeNestedScroll(), + verticalAlignment = Alignment.CenterVertically + ) { + Spacer(modifier = Modifier.width(10.dp)) + Row( + modifier = Modifier + .clip(RoundedCornerShape(36.dp)) + .then( + if (theme.enableBlur) { + Modifier + .hazeEffect( + state = hazeState, + style = HazeMaterials.ultraThin() + ) + .border( + 1.dp, MaterialTheme.colorScheme.outlineVariant, + RoundedCornerShape(36.dp) + ) + } else Modifier + ) + .animateContentSize() + .weight(1f) + .background( + if (theme.enableBlur) Color.Transparent + else MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp) + ) + .onGloballyPositioned { + with(density) { + onSetMessageBarHeight(it.size.height.toDp()) + } + }, + verticalAlignment = Alignment.Bottom + ) { + Spacer(modifier = Modifier.width(6.dp)) + + if (showEmojiButton) { + val scope = rememberCoroutineScope() + val rotation = remember { Animatable(0f) } + + Column(verticalArrangement = Arrangement.Bottom) { + IconButton( + onClick = { + if (AppSettings.General.enableHaptic) { + view.performHapticFeedback( + HapticFeedbackConstantsCompat.REJECT + ) + } + scope.launch { + for (i in 20 downTo 0 step 4) { + rotation.animateTo( + targetValue = i.toFloat(), + animationSpec = tween(50) + ) + if (i > 0) { + rotation.animateTo( + targetValue = -i.toFloat(), + animationSpec = tween(50) + ) + } + } + } + }, + onLongClick = { + if (AppSettings.General.enableHaptic) { + view.performHapticFeedback( + HapticFeedbackConstantsCompat.LONG_PRESS + ) + } + onEmojiButtonLongClicked() + }, + modifier = Modifier.rotate(rotation.value) + ) { + Icon( + painter = painterResource(id = UiR.drawable.ic_outline_emoji_emotions_24), + contentDescription = "Emoji button", + tint = MaterialTheme.colorScheme.primary + ) + } + + Spacer(modifier = Modifier.height(4.dp)) + } + } + + TextField( + modifier = modifier + .weight(1f) + .addTextContextMenuComponents { + separator() + + item( + key = "Bold", + label = context.getString(UiR.string.bold) + ) { + onBoldRequested() + close() + } + item( + key = "Italic", + label = context.getString(UiR.string.italic) + ) { + onItalicRequested() + close() + } + item( + key = "Underline", + label = context.getString(UiR.string.underline) + ) { + onUnderlineRequested() + close() + } + item( + key = "Link", + label = context.getString(UiR.string.link) + ) { + onLinkRequested() + close() + } + item( + key = "Regular", + label = context.getString(UiR.string.regular) + ) { + onRegularRequested() + close() + } + + separator() + }, + value = message, + onValueChange = onMessageInputChanged, + colors = TextFieldDefaults.colors( + unfocusedContainerColor = Color.Transparent, + focusedContainerColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + ), + placeholder = { + Text( + text = stringResource(id = UiR.string.message_input_hint), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + ) + + + val scope = rememberCoroutineScope() + val attachmentRotation = remember { Animatable(0f) } + + Column(verticalArrangement = Arrangement.Bottom) { + IconButton( + onClick = { + onAttachmentButtonClicked() + if (AppSettings.General.enableHaptic) { + view.performHapticFeedback( + HapticFeedbackConstantsCompat.REJECT + ) + } + scope.launch { + for (i in 20 downTo 0 step 4) { + attachmentRotation.animateTo( + targetValue = i.toFloat(), + animationSpec = tween(50) + ) + if (i > 0) { + attachmentRotation.animateTo( + targetValue = -i.toFloat(), + animationSpec = tween(50) + ) + } + } + } + } + ) { + Icon( + painter = painterResource(id = UiR.drawable.round_attach_file_24), + contentDescription = "Add attachment button", + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.rotate(30f + attachmentRotation.value) + ) + } + + Spacer(modifier = Modifier.height(4.dp)) + } + + val micRotation = remember { Animatable(0f) } + + Column(verticalArrangement = Arrangement.Bottom) { + IconButton( + onClick = { + if (actionMode == ActionMode.Record) { + if (AppSettings.General.enableHaptic) { + view.performHapticFeedback( + HapticFeedbackConstantsCompat.REJECT + ) + } + scope.launch { + for (i in 20 downTo 0 step 4) { + micRotation.animateTo( + targetValue = i.toFloat(), + animationSpec = tween(50) + ) + if (i > 0) { + micRotation.animateTo( + targetValue = -i.toFloat(), + animationSpec = tween(50) + ) + } + } + } + } else { + onActionButtonClicked() + } + }, + modifier = Modifier.rotate(micRotation.value) + ) { + Icon( + painter = painterResource( + id = when (actionMode) { + ActionMode.Delete -> UiR.drawable.round_delete_outline_24 + ActionMode.Edit -> UiR.drawable.ic_round_done_24 + ActionMode.Record -> UiR.drawable.ic_round_mic_none_24 + ActionMode.Send -> UiR.drawable.round_send_24 + } + ), + contentDescription = when (actionMode) { + ActionMode.Delete -> "Delete message button" + ActionMode.Edit -> "Edit message button" + ActionMode.Record -> "Record audio message button" + ActionMode.Send -> "Send message button" + }, + tint = MaterialTheme.colorScheme.primary + ) + } + + Spacer(modifier = Modifier.height(4.dp)) + } + + Spacer(modifier = Modifier.width(6.dp)) + } + + Spacer(modifier = Modifier.width(10.dp)) + } + } +} diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryRoute.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryRoute.kt index 93bf589d..be6c8e78 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryRoute.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryRoute.kt @@ -73,7 +73,9 @@ fun MessagesHistoryRoute( onDeleteSelectedButtonClicked = viewModel::onDeleteSelectedMessagesClicked, onBoldRequested = viewModel::onBoldClicked, onItalicRequested = viewModel::onItalicClicked, - onUnderlineRequested = viewModel::onUnderlineClicked + onUnderlineRequested = viewModel::onUnderlineClicked, + onLinkRequested = viewModel::onLinkClicked, + onRegularRequested = viewModel::onRegularClicked ) HandleDialogs( diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt index 8b168200..0f10bce2 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt @@ -1,62 +1,23 @@ package dev.meloda.fast.messageshistory.presentation import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.Crossfade import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.animateContentSize -import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding -import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imeNestedScroll -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.contextmenu.builder.item -import androidx.compose.foundation.text.contextmenu.modifier.addTextContextMenuComponents -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.rounded.ArrowBack -import androidx.compose.material.icons.outlined.MoreVert -import androidx.compose.material.icons.rounded.Close -import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -68,42 +29,25 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.rotate -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.core.view.HapticFeedbackConstantsCompat -import coil.compose.AsyncImage import dev.chrisbanes.haze.HazeState -import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi -import dev.chrisbanes.haze.materials.HazeMaterials -import dev.meloda.fast.common.extensions.orDots import dev.meloda.fast.data.UserConfig import dev.meloda.fast.datastore.AppSettings -import dev.meloda.fast.messageshistory.model.ActionMode import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState import dev.meloda.fast.messageshistory.model.UiItem import dev.meloda.fast.messageshistory.util.indexOfMessageByCmId import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.api.domain.VkMessage -import dev.meloda.fast.ui.components.IconButton import dev.meloda.fast.ui.components.VkErrorView import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.util.ImmutableList import dev.meloda.fast.ui.util.emptyImmutableList -import dev.meloda.fast.ui.util.getImage import kotlinx.coroutines.launch import dev.meloda.fast.ui.R as UiR @@ -140,7 +84,9 @@ fun MessagesHistoryScreen( onDeleteSelectedButtonClicked: () -> Unit = {}, onBoldRequested: () -> Unit = {}, onItalicRequested: () -> Unit = {}, + onLinkRequested: () -> Unit = {}, onUnderlineRequested: () -> Unit = {}, + onRegularRequested: () -> Unit = {} ) { val context = LocalContext.current val view = LocalView.current @@ -179,10 +125,6 @@ fun MessagesHistoryScreen( } } - var dropDownMenuExpanded by remember { - mutableStateOf(false) - } - val topBarContainerColorAlpha by animateFloatAsState( targetValue = if (!theme.enableBlur || !listState.canScrollBackward) 1f else 0f, label = "toolbarColorAlpha", @@ -207,8 +149,6 @@ fun MessagesHistoryScreen( mutableStateOf(0.dp) } - val density = LocalDensity.current - val showReplyAction by remember(selectedMessages) { derivedStateOf { selectedMessages.size == 1 } } @@ -217,223 +157,40 @@ fun MessagesHistoryScreen( modifier = Modifier.fillMaxSize(), contentWindowInsets = WindowInsets.statusBars, topBar = { - Column( - modifier = Modifier - .fillMaxWidth() - .background(topBarContainerColor.copy(alpha = topBarContainerColorAlpha)) - .then( - if (theme.enableBlur) { - Modifier.hazeEffect( - state = hazeState, - style = HazeMaterials.thick() - ) - } else Modifier - ) - ) { - TopAppBar( - modifier = Modifier - .then( - if (theme.enableBlur) { - Modifier.hazeEffect( - state = hazeState, - style = HazeMaterials.thick() - ) - } else Modifier - ) - .fillMaxWidth() - .then( - if (screenState.isLoading && messages.isEmpty()) Modifier - else Modifier.clickable { - onTopBarClicked() - } - ), - title = { - Row( - modifier = Modifier.weight(1f), - verticalAlignment = Alignment.CenterVertically - ) { - if (selectedMessages.isEmpty()) { - val avatar = screenState.avatar.getImage() - if (screenState.conversationId == UserConfig.userId) { - Box( - modifier = Modifier - .size(36.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.primary) - ) { - Icon( - modifier = Modifier - .align(Alignment.Center) - .size(24.dp), - painter = painterResource(id = UiR.drawable.ic_round_bookmark_border_24), - contentDescription = "Favorites icon", - tint = MaterialTheme.colorScheme.onPrimary - ) - } - } else { - if (avatar is Painter) { - Image( - painter = avatar, - contentDescription = null, - modifier = Modifier - .size(36.dp) - .clip(CircleShape) - ) - } else { - AsyncImage( - model = screenState.avatar.getImage(), - contentDescription = "Profile Image", - modifier = Modifier - .size(36.dp) - .clip(CircleShape), - placeholder = painterResource(id = UiR.drawable.ic_account_circle_cut), - ) - } - } - - Spacer(modifier = Modifier.width(12.dp)) - } - - Text( - text = when { - screenState.isLoading -> stringResource(id = UiR.string.title_loading) - selectedMessages.isNotEmpty() -> "(${selectedMessages.size})" - else -> screenState.title - }, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.headlineSmall - ) - } - }, - navigationIcon = { - IconButton( - onClick = { - if (selectedMessages.isEmpty()) onBack() - else onClose() - } - ) { - Crossfade(targetState = selectedMessages.isEmpty()) { state -> - Icon( - imageVector = if (state) { - Icons.AutoMirrored.Rounded.ArrowBack - } else { - Icons.Rounded.Close - }, - contentDescription = if (state) "Close button" - else "Back button" - ) - } - } - }, - colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), - actions = { - if (selectedMessages.isNotEmpty()) { - AnimatedVisibility(showReplyAction) { - IconButton( - onClick = { - if (AppSettings.General.enableHaptic) { - view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT) - } - } - ) { - Icon( - painter = painterResource(UiR.drawable.round_reply_24), - contentDescription = null - ) - } - } - IconButton( - onClick = { - if (AppSettings.General.enableHaptic) { - view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT) - } - } - ) { - Icon( - painter = painterResource(UiR.drawable.round_reply_all_24), - contentDescription = null - ) - } - IconButton( - onClick = { - if (AppSettings.General.enableHaptic) { - view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT) - } - } - ) { - Icon( - painter = painterResource(UiR.drawable.round_forward_24), - contentDescription = null - ) - } - IconButton(onClick = onDeleteSelectedButtonClicked) { - Icon( - painter = painterResource(UiR.drawable.round_delete_outline_24), - contentDescription = null - ) - } - } else { - IconButton( - onClick = { dropDownMenuExpanded = true } - ) { - Icon( - imageVector = Icons.Outlined.MoreVert, - contentDescription = "Options" - ) - } - - DropdownMenu( - modifier = Modifier.defaultMinSize(minWidth = 140.dp), - expanded = dropDownMenuExpanded, - onDismissRequest = { - dropDownMenuExpanded = false - }, - offset = DpOffset(x = (-4).dp, y = (-60).dp) - ) { - DropdownMenuItem( - onClick = { - onRefresh() - dropDownMenuExpanded = false - }, - text = { - Text(text = stringResource(UiR.string.action_refresh)) - }, - leadingIcon = { - Icon( - imageVector = Icons.Rounded.Refresh, - contentDescription = null - ) - } - ) - } - } + val topBarTitle by remember(screenState, selectedMessages) { + derivedStateOf { + when { + screenState.isLoading -> context.getString(UiR.string.title_loading) + selectedMessages.isNotEmpty() -> "(${selectedMessages.size})" + else -> screenState.title } - ) - - val showHorizontalProgressBar by remember(screenState) { - derivedStateOf { screenState.isLoading && messages.isNotEmpty() } - } - if (showHorizontalProgressBar) { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) - } - AnimatedVisibility(!showHorizontalProgressBar) { - HorizontalDivider() - } - - if (!screenState.isLoading && pinnedMessage != null) { - PinnedMessageContainer( - modifier = Modifier, - pinnedMessage = requireNotNull(pinnedMessage), - title = screenState.pinnedTitle.orDots(), - summary = screenState.pinnedSummary, - canChangePin = screenState.conversation.canChangePin, - onPinnedMessageClicked = onPinnedMessageClicked, - onUnpinMessageButtonClicked = onUnpinMessageButtonClicked - ) - HorizontalDivider() } } + + MessagesHistoryTopBarContainer( + hazeState = hazeState, + showReplyAction = showReplyAction, + topBarContainerColor = topBarContainerColor, + topBarContainerColorAlpha = topBarContainerColorAlpha, + isClickable = !(screenState.isLoading && messages.isEmpty()), + isMessagesSelecting = selectedMessages.isNotEmpty(), + isPeerAccount = screenState.conversationId == UserConfig.userId, + avatar = screenState.avatar, + title = topBarTitle, + showHorizontalProgressBar = screenState.isLoading && messages.isNotEmpty(), + showPinnedContainer = !screenState.isLoading && pinnedMessage != null, + pinnedMessage = pinnedMessage, + pinnedTitle = screenState.pinnedTitle, + pinnedSummary = screenState.pinnedSummary, + showUnpinButton = screenState.conversation.canChangePin, + onTopBarClicked = onTopBarClicked, + onBack = onBack, + onClose = onClose, + onDeleteSelectedButtonClicked = onDeleteSelectedButtonClicked, + onRefresh = onRefresh, + onPinnedMessageClicked = onPinnedMessageClicked, + onUnpinMessageButtonClicked = onUnpinMessageButtonClicked + ) } ) { padding -> Box( @@ -472,254 +229,23 @@ fun MessagesHistoryScreen( onMessageLongClicked = onMessageLongClicked ) - Column( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomStart) - .background(Color.Transparent) - .padding(bottom = 8.dp) - .navigationBarsPadding() - .imePadding() - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .defaultMinSize(minHeight = 60.dp) - .imeNestedScroll(), - verticalAlignment = Alignment.CenterVertically - ) { - Spacer(modifier = Modifier.width(10.dp)) - Row( - modifier = Modifier - .clip(RoundedCornerShape(36.dp)) - .then( - if (theme.enableBlur) { - Modifier - .hazeEffect( - state = hazeState, - style = HazeMaterials.ultraThin() - ) - .border( - 1.dp, MaterialTheme.colorScheme.outlineVariant, - RoundedCornerShape(36.dp) - ) - } else Modifier - ) - .animateContentSize() - .weight(1f) - .background( - if (theme.enableBlur) Color.Transparent - else MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp) - ) - .onGloballyPositioned { - messageBarHeight = with(density) { - it.size.height.toDp() - } - }, - verticalAlignment = Alignment.Bottom - ) { - Spacer(modifier = Modifier.width(6.dp)) - - if (showEmojiButton) { - val scope = rememberCoroutineScope() - val rotation = remember { Animatable(0f) } - - Column(verticalArrangement = Arrangement.Bottom) { - IconButton( - onClick = { - if (AppSettings.General.enableHaptic) { - view.performHapticFeedback( - HapticFeedbackConstantsCompat.REJECT - ) - } - scope.launch { - for (i in 20 downTo 0 step 4) { - rotation.animateTo( - targetValue = i.toFloat(), - animationSpec = tween(50) - ) - if (i > 0) { - rotation.animateTo( - targetValue = -i.toFloat(), - animationSpec = tween(50) - ) - } - } - } - }, - onLongClick = { - if (AppSettings.General.enableHaptic) { - view.performHapticFeedback( - HapticFeedbackConstantsCompat.LONG_PRESS - ) - } - onEmojiButtonLongClicked() - }, - modifier = Modifier.rotate(rotation.value) - ) { - Icon( - painter = painterResource(id = UiR.drawable.ic_outline_emoji_emotions_24), - contentDescription = "Emoji button", - tint = MaterialTheme.colorScheme.primary - ) - } - - Spacer(modifier = Modifier.height(4.dp)) - } - } - - TextField( - modifier = Modifier - .weight(1f) - .addTextContextMenuComponents { - separator() - - item( - key = "Bold", - label = context.getString(UiR.string.bold) - ) { - onBoldRequested() - close() - } - item( - key = "Italic", - label = context.getString(UiR.string.italic) - ) { - onItalicRequested() - close() - } - item( - key = "Underline", - label = context.getString(UiR.string.underline) - ) { - onUnderlineRequested() - close() - } - item( - key = "Link", - label = context.getString(UiR.string.link) - ) { - close() - } - - separator() - }, - value = screenState.message, - onValueChange = onMessageInputChanged, - colors = TextFieldDefaults.colors( - unfocusedContainerColor = Color.Transparent, - focusedContainerColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - ), - placeholder = { - Text( - text = stringResource(id = UiR.string.message_input_hint), - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - ) - - - val scope = rememberCoroutineScope() - val attachmentRotation = remember { Animatable(0f) } - - Column(verticalArrangement = Arrangement.Bottom) { - IconButton( - onClick = { - onAttachmentButtonClicked() - if (AppSettings.General.enableHaptic) { - view.performHapticFeedback( - HapticFeedbackConstantsCompat.REJECT - ) - } - scope.launch { - for (i in 20 downTo 0 step 4) { - attachmentRotation.animateTo( - targetValue = i.toFloat(), - animationSpec = tween(50) - ) - if (i > 0) { - attachmentRotation.animateTo( - targetValue = -i.toFloat(), - animationSpec = tween(50) - ) - } - } - } - } - ) { - Icon( - painter = painterResource(id = UiR.drawable.round_attach_file_24), - contentDescription = "Add attachment button", - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.rotate(30f + attachmentRotation.value) - ) - } - - Spacer(modifier = Modifier.height(4.dp)) - } - - val micRotation = remember { Animatable(0f) } - - Column(verticalArrangement = Arrangement.Bottom) { - IconButton( - onClick = { - if (screenState.actionMode == ActionMode.Record) { - if (AppSettings.General.enableHaptic) { - view.performHapticFeedback( - HapticFeedbackConstantsCompat.REJECT - ) - } - scope.launch { - for (i in 20 downTo 0 step 4) { - micRotation.animateTo( - targetValue = i.toFloat(), - animationSpec = tween(50) - ) - if (i > 0) { - micRotation.animateTo( - targetValue = -i.toFloat(), - animationSpec = tween(50) - ) - } - } - } - } else { - onActionButtonClicked() - } - }, - modifier = Modifier.rotate(micRotation.value) - ) { - Icon( - painter = painterResource( - id = when (screenState.actionMode) { - ActionMode.Delete -> UiR.drawable.round_delete_outline_24 - ActionMode.Edit -> UiR.drawable.ic_round_done_24 - ActionMode.Record -> UiR.drawable.ic_round_mic_none_24 - ActionMode.Send -> UiR.drawable.round_send_24 - } - ), - contentDescription = when (screenState.actionMode) { - ActionMode.Delete -> "Delete message button" - ActionMode.Edit -> "Edit message button" - ActionMode.Record -> "Record audio message button" - ActionMode.Send -> "Send message button" - }, - tint = MaterialTheme.colorScheme.primary - ) - } - - Spacer(modifier = Modifier.height(4.dp)) - } - - Spacer(modifier = Modifier.width(6.dp)) - } - - Spacer(modifier = Modifier.width(10.dp)) - } - } + MessagesHistoryInputBar( + modifier = Modifier.align(Alignment.BottomStart), + message = screenState.message, + onMessageInputChanged = onMessageInputChanged, + onBoldRequested = onBoldRequested, + onItalicRequested = onItalicRequested, + onUnderlineRequested = onUnderlineRequested, + onLinkRequested = onLinkRequested, + onRegularRequested = onRegularRequested, + hazeState = hazeState, + showEmojiButton = showEmojiButton, + actionMode = screenState.actionMode, + onSetMessageBarHeight = { messageBarHeight = it }, + onEmojiButtonLongClicked = onEmojiButtonLongClicked, + onAttachmentButtonClicked = onAttachmentButtonClicked, + onActionButtonClicked = onActionButtonClicked + ) when { screenState.isLoading && messages.values.isEmpty() -> { diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryTopBar.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryTopBar.kt new file mode 100644 index 00000000..47e84dd6 --- /dev/null +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryTopBar.kt @@ -0,0 +1,260 @@ +package dev.meloda.fast.messageshistory.presentation + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.outlined.MoreVert +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.Refresh +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import androidx.core.view.HapticFeedbackConstantsCompat +import coil.compose.AsyncImage +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi +import dev.chrisbanes.haze.materials.HazeMaterials +import dev.meloda.fast.common.model.UiImage +import dev.meloda.fast.datastore.AppSettings +import dev.meloda.fast.ui.theme.LocalThemeConfig +import dev.meloda.fast.ui.util.getImage + +import dev.meloda.fast.ui.R as UiR + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class) +@Composable +fun MessagesHistoryTopBar( + modifier: Modifier = Modifier, + hazeState: HazeState, + showReplyAction: Boolean, + isClickable: Boolean, + isMessagesSelecting: Boolean, + isPeerAccount: Boolean, + avatar: UiImage, + title: String, + onTopBarClicked: () -> Unit = {}, + onBack: () -> Unit = {}, + onClose: () -> Unit = {}, + onDeleteSelectedButtonClicked: () -> Unit = {}, + onRefresh: () -> Unit = {} +) { + val view = LocalView.current + val theme = LocalThemeConfig.current + + var dropDownMenuExpanded by remember { + mutableStateOf(false) + } + + TopAppBar( + modifier = modifier + .then( + if (theme.enableBlur) { + Modifier.hazeEffect( + state = hazeState, + style = HazeMaterials.thick() + ) + } else Modifier + ) + .fillMaxWidth() + .then( + if (!isClickable) Modifier + else Modifier.clickable { + onTopBarClicked() + } + ), + title = { + Row( +// modifier = Modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically + ) { + if (!isMessagesSelecting) { + if (isPeerAccount) { + Box( + modifier = Modifier + .size(36.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary) + ) { + Icon( + modifier = Modifier + .align(Alignment.Center) + .size(24.dp), + painter = painterResource(id = UiR.drawable.ic_round_bookmark_border_24), + contentDescription = "Favorites icon", + tint = MaterialTheme.colorScheme.onPrimary + ) + } + } else { + val actualAvatar = avatar.getImage() + + if (actualAvatar is Painter) { + Image( + painter = actualAvatar, + contentDescription = null, + modifier = Modifier + .size(36.dp) + .clip(CircleShape) + ) + } else { + AsyncImage( + model = actualAvatar, + contentDescription = "Profile Image", + modifier = Modifier + .size(36.dp) + .clip(CircleShape), + placeholder = painterResource(id = UiR.drawable.ic_account_circle_cut), + ) + } + } + + Spacer(modifier = Modifier.width(12.dp)) + } + + Text( + text = title, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.headlineSmall + ) + } + }, + navigationIcon = { + IconButton( + onClick = { + if (!isMessagesSelecting) onBack() + else onClose() + } + ) { + Crossfade(targetState = !isMessagesSelecting) { state -> + Icon( + imageVector = if (state) { + Icons.AutoMirrored.Rounded.ArrowBack + } else { + Icons.Rounded.Close + }, + contentDescription = if (state) "Close button" + else "Back button" + ) + } + } + }, + colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), + actions = { + if (isMessagesSelecting) { + AnimatedVisibility(showReplyAction) { + IconButton( + onClick = { + if (AppSettings.General.enableHaptic) { + view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT) + } + } + ) { + Icon( + painter = painterResource(UiR.drawable.round_reply_24), + contentDescription = null + ) + } + } + IconButton( + onClick = { + if (AppSettings.General.enableHaptic) { + view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT) + } + } + ) { + Icon( + painter = painterResource(UiR.drawable.round_reply_all_24), + contentDescription = null + ) + } + IconButton( + onClick = { + if (AppSettings.General.enableHaptic) { + view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT) + } + } + ) { + Icon( + painter = painterResource(UiR.drawable.round_forward_24), + contentDescription = null + ) + } + IconButton(onClick = onDeleteSelectedButtonClicked) { + Icon( + painter = painterResource(UiR.drawable.round_delete_outline_24), + contentDescription = null + ) + } + } else { + IconButton( + onClick = { dropDownMenuExpanded = true } + ) { + Icon( + imageVector = Icons.Outlined.MoreVert, + contentDescription = "Options" + ) + } + + DropdownMenu( + modifier = Modifier.defaultMinSize(minWidth = 140.dp), + expanded = dropDownMenuExpanded, + onDismissRequest = { + dropDownMenuExpanded = false + }, + offset = DpOffset(x = (-4).dp, y = (-60).dp) + ) { + DropdownMenuItem( + onClick = { + onRefresh() + dropDownMenuExpanded = false + }, + text = { + Text(text = stringResource(UiR.string.action_refresh)) + }, + leadingIcon = { + Icon( + imageVector = Icons.Rounded.Refresh, + contentDescription = null + ) + } + ) + } + } + } + ) +} diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryTopBarContainer.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryTopBarContainer.kt new file mode 100644 index 00000000..75a51d18 --- /dev/null +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryTopBarContainer.kt @@ -0,0 +1,101 @@ +package dev.meloda.fast.messageshistory.presentation + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.hazeEffect +import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi +import dev.chrisbanes.haze.materials.HazeMaterials +import dev.meloda.fast.common.extensions.orDots +import dev.meloda.fast.common.model.UiImage +import dev.meloda.fast.model.api.domain.VkMessage +import dev.meloda.fast.ui.theme.LocalThemeConfig + +@OptIn(ExperimentalHazeMaterialsApi::class, ExperimentalMaterial3Api::class) +@Composable +fun MessagesHistoryTopBarContainer( + modifier: Modifier = Modifier, + hazeState: HazeState, + showReplyAction: Boolean, + topBarContainerColor: Color, + topBarContainerColorAlpha: Float, + isClickable: Boolean, + isMessagesSelecting: Boolean, + isPeerAccount: Boolean, + avatar: UiImage, + title: String, + showHorizontalProgressBar: Boolean, + showPinnedContainer: Boolean, + pinnedMessage: VkMessage?, + pinnedTitle: String?, + pinnedSummary: AnnotatedString?, + showUnpinButton: Boolean, + onTopBarClicked: () -> Unit = {}, + onBack: () -> Unit = {}, + onClose: () -> Unit = {}, + onDeleteSelectedButtonClicked: () -> Unit = {}, + onRefresh: () -> Unit = {}, + onPinnedMessageClicked: (Long) -> Unit = {}, + onUnpinMessageButtonClicked: () -> Unit = {} +) { + val theme = LocalThemeConfig.current + + Column( + modifier = modifier + .fillMaxWidth() + .background(topBarContainerColor.copy(alpha = topBarContainerColorAlpha)) + .then( + if (theme.enableBlur) { + Modifier.hazeEffect( + state = hazeState, + style = HazeMaterials.thick() + ) + } else Modifier + ) + ) { + MessagesHistoryTopBar( + modifier = modifier, + hazeState = hazeState, + showReplyAction = showReplyAction, + isClickable = isClickable, + isMessagesSelecting = isMessagesSelecting, + isPeerAccount = isPeerAccount, + avatar = avatar, + title = title, + onTopBarClicked = onTopBarClicked, + onBack = onBack, + onClose = onClose, + onDeleteSelectedButtonClicked = onDeleteSelectedButtonClicked, + onRefresh = onRefresh + ) + + if (showHorizontalProgressBar) { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } + AnimatedVisibility(!showHorizontalProgressBar) { + HorizontalDivider() + } + + if (showPinnedContainer) { + PinnedMessageContainer( + modifier = Modifier, + pinnedMessage = requireNotNull(pinnedMessage), + title = pinnedTitle.orDots(), + summary = pinnedSummary, + canChangePin = showUnpinButton, + onPinnedMessageClicked = onPinnedMessageClicked, + onUnpinMessageButtonClicked = onUnpinMessageButtonClicked + ) + HorizontalDivider() + } + } +}