From 0fff668b4b6b538eaae0a99e2c31b862edf7745e Mon Sep 17 00:00:00 2001 From: Vitor Hugo Schwaab Date: Tue, 7 May 2024 17:51:33 +0200 Subject: [PATCH 1/7] fix: disable swiping in some extra cases [WPB-9044] (#2983) --- .../home/conversations/ConversationScreen.kt | 16 ++++++++-- .../conversations/media/FileAssetsContent.kt | 3 +- .../messages/item/MessageContainerItem.kt | 4 +-- .../messages/item/RegularMessageItem.kt | 31 +++++++++++++------ .../ui/home/conversations/model/UIMessage.kt | 1 + ...SearchConversationMessagesResultsScreen.kt | 3 +- 6 files changed, 42 insertions(+), 16 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index 3bf56a5319a..92924b93233 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt @@ -86,8 +86,8 @@ import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.ui.LocalActivity -import com.wire.android.ui.calling.getOutgoingCallIntent import com.wire.android.ui.calling.getOngoingCallIntent +import com.wire.android.ui.calling.getOutgoingCallIntent import com.wire.android.ui.common.bottomsheet.MenuModalSheetHeader import com.wire.android.ui.common.bottomsheet.MenuModalSheetLayout import com.wire.android.ui.common.colorsScheme @@ -131,6 +131,7 @@ import com.wire.android.ui.home.conversations.messages.ConversationMessagesViewM import com.wire.android.ui.home.conversations.messages.ConversationMessagesViewState import com.wire.android.ui.home.conversations.messages.draft.MessageDraftViewModel import com.wire.android.ui.home.conversations.messages.item.MessageContainerItem +import com.wire.android.ui.home.conversations.messages.item.SwipableMessageConfiguration import com.wire.android.ui.home.conversations.migration.ConversationMigrationViewModel import com.wire.android.ui.home.conversations.model.ExpirationStatus import com.wire.android.ui.home.conversations.model.UIMessage @@ -1074,6 +1075,11 @@ fun MessageList( } } } + val swipableConfiguration = remember(message) { + SwipableMessageConfiguration.SwipableToReply { + onSwipedToReply(it) + } + } MessageContainerItem( message = message, @@ -1085,7 +1091,7 @@ fun MessageList( onAudioClick = onAudioItemClicked, onChangeAudioPosition = onChangeAudioPosition, onLongClicked = onShowEditingOption, - onSwipedToReply = onSwipedToReply, + swipableMessageConfiguration = swipableConfiguration, onAssetMessageClicked = onAssetItemClicked, onImageMessageClicked = onImageFullScreenMode, onOpenProfile = onOpenProfile, @@ -1126,6 +1132,7 @@ private fun MessageGroupDateTime( now, DateUtils.MINUTE_IN_MILLIS ).toString() + is MessageDateTimeGroup.Daily -> { when (messageDateTimeGroup.type) { MessageDateTimeGroup.Daily.Type.Today -> DateUtils.getRelativeDateTimeString( @@ -1135,6 +1142,7 @@ private fun MessageGroupDateTime( DateUtils.DAY_IN_MILLIS, 0 ).toString() + MessageDateTimeGroup.Daily.Type.Yesterday -> DateUtils.getRelativeDateTimeString( context, @@ -1143,16 +1151,19 @@ private fun MessageGroupDateTime( DateUtils.DAY_IN_MILLIS * 2, 0 ).toString() + MessageDateTimeGroup.Daily.Type.WithinWeek -> DateUtils.formatDateTime( context, messageDateTime.time, DateUtils.FORMAT_SHOW_WEEKDAY or DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME ) + MessageDateTimeGroup.Daily.Type.NotWithinWeekButSameYear -> DateUtils.formatDateTime( context, messageDateTime.time, DateUtils.FORMAT_SHOW_DATE or DateUtils.FORMAT_SHOW_TIME ) + MessageDateTimeGroup.Daily.Type.Other -> DateUtils.formatDateTime( context, messageDateTime.time, @@ -1160,6 +1171,7 @@ private fun MessageGroupDateTime( ) } } + null -> "" } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/FileAssetsContent.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/FileAssetsContent.kt index 17be9d60ed9..a1bcaa24f5c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/FileAssetsContent.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/media/FileAssetsContent.kt @@ -40,6 +40,7 @@ import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.progress.WireCircularProgressIndicator import com.wire.android.ui.home.conversations.info.ConversationDetailsData import com.wire.android.ui.home.conversations.messages.item.MessageContainerItem +import com.wire.android.ui.home.conversations.messages.item.SwipableMessageConfiguration import com.wire.android.ui.home.conversations.model.UIMessage import com.wire.android.ui.home.conversations.usecase.UIPagingItem import com.wire.android.ui.home.conversationslist.common.FolderHeader @@ -135,7 +136,7 @@ private fun AssetMessagesListContent( shouldDisplayMessageStatus = false, shouldDisplayFooter = false, onReplyClickable = null, - onSwipedToReply = { } + swipableMessageConfiguration = SwipableMessageConfiguration.NotSwipable ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageContainerItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageContainerItem.kt index c3c0f4e67d5..7283b881f01 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageContainerItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageContainerItem.kt @@ -58,7 +58,7 @@ fun MessageContainerItem( audioMessagesState: PersistentMap, assetStatus: AssetTransferStatus? = null, onLongClicked: (UIMessage.Regular) -> Unit, - onSwipedToReply: (UIMessage.Regular) -> Unit, + swipableMessageConfiguration: SwipableMessageConfiguration, onAssetMessageClicked: (String) -> Unit, onAudioClick: (String) -> Unit, onChangeAudioPosition: (String, Int) -> Unit, @@ -140,7 +140,7 @@ fun MessageContainerItem( onAudioClick = onAudioClick, onChangeAudioPosition = onChangeAudioPosition, onLongClicked = onLongClicked, - onSwipedToReply = onSwipedToReply, + swipableMessageConfiguration = swipableMessageConfiguration, onAssetMessageClicked = onAssetMessageClicked, onImageMessageClicked = onImageMessageClicked, onOpenProfile = onOpenProfile, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt index af0831e5bd6..c587826c69d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt @@ -40,6 +40,7 @@ import androidx.compose.material3.SwipeToDismissBoxState import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -114,7 +115,7 @@ fun RegularMessageItem( audioMessagesState: PersistentMap, assetStatus: AssetTransferStatus? = null, onLongClicked: (UIMessage.Regular) -> Unit, - onSwipedToReply: (UIMessage.Regular) -> Unit = {}, + swipableMessageConfiguration: SwipableMessageConfiguration = SwipableMessageConfiguration.NotSwipable, onAssetMessageClicked: (String) -> Unit, onAudioClick: (String) -> Unit, onChangeAudioPosition: (String, Int) -> Unit, @@ -133,11 +134,8 @@ fun RegularMessageItem( useSmallBottomPadding: Boolean = false, selfDeletionTimerState: SelfDeletionTimerHelper.SelfDeletionTimerState = SelfDeletionTimerHelper.SelfDeletionTimerState.NotExpirable ): Unit = with(message) { - val onSwipe = remember(message) { { onSwipedToReply(message) } } - SwipableToReplyBox( - isSwipable = isReplyable, - onSwipedToReply = onSwipe - ) { + @Composable + fun messageContent() { MessageItemTemplate( showAuthor, useSmallBottomPadding = useSmallBottomPadding, @@ -260,12 +258,25 @@ fun RegularMessageItem( } ) } + if (swipableMessageConfiguration is SwipableMessageConfiguration.SwipableToReply && isReplyable) { + val onSwipe = remember(message) { { swipableMessageConfiguration.onSwipedToReply(message) } } + SwipableToReplyBox(onSwipedToReply = onSwipe) { + messageContent() + } + } else { + messageContent() + } +} + +@Stable +sealed interface SwipableMessageConfiguration { + data object NotSwipable : SwipableMessageConfiguration + class SwipableToReply(val onSwipedToReply: (uiMessage: UIMessage.Regular) -> Unit) : SwipableMessageConfiguration } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun SwipableToReplyBox( - isSwipable: Boolean, modifier: Modifier = Modifier, onSwipedToReply: () -> Unit = {}, content: @Composable RowScope.() -> Unit @@ -275,7 +286,7 @@ private fun SwipableToReplyBox( var didVibrateOnCurrentDrag by remember { mutableStateOf(false) } // Finish the animation in the first 25% of the drag - val progressUntilAnimationCompletion = 0.25f + val progressUntilAnimationCompletion = 0.33f val dismissState = remember { SwipeToDismissBoxState( SwipeToDismissBoxValue.Settled, @@ -301,12 +312,12 @@ private fun SwipableToReplyBox( SwipeToDismissBox( state = dismissState, modifier = modifier, - enableDismissFromStartToEnd = isSwipable, content = content, enableDismissFromEndToStart = false, backgroundContent = { Row( - modifier = Modifier.fillMaxSize() + modifier = Modifier + .fillMaxSize() .drawBehind { // TODO(RTL): Might need adjusting once RTL is supported (also lacking in SwipeToDismissBox) drawRect( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt index 9c2643824f3..d568387f0f9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt @@ -99,6 +99,7 @@ sealed interface UIMessage { val isReplyable: Boolean get() = isReplyableContent && isTheMessageAvailableToOtherUsers && + !isDeleted && header.messageStatus.expirationStatus is ExpirationStatus.NotExpirable val isTextContentWithoutQuote = messageContent is UIMessageContent.TextMessage && messageContent.messageBody.quotedMessage == null diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesResultsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesResultsScreen.kt index 4851ef9db54..a6a6f6db68e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesResultsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/search/messages/SearchConversationMessagesResultsScreen.kt @@ -27,6 +27,7 @@ import androidx.paging.compose.itemKey import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.home.conversations.info.ConversationDetailsData import com.wire.android.ui.home.conversations.messages.item.MessageContainerItem +import com.wire.android.ui.home.conversations.messages.item.SwipableMessageConfiguration import com.wire.android.ui.home.conversations.mock.mockMessageWithText import com.wire.android.ui.home.conversations.model.UIMessage import com.wire.android.ui.theme.WireTheme @@ -71,7 +72,7 @@ fun SearchConversationMessagesResultsScreen( shouldDisplayMessageStatus = false, shouldDisplayFooter = false, onReplyClickable = null, - onSwipedToReply = {} + swipableMessageConfiguration = SwipableMessageConfiguration.NotSwipable ) } From f7e7f0edf58d33c1cc9f4234ad90674ad04da033 Mon Sep 17 00:00:00 2001 From: Vitor Hugo Schwaab Date: Wed, 8 May 2024 09:42:23 +0200 Subject: [PATCH 2/7] fix: improve swipe detection and animation [WPB-9046] (#2985) --- .../messages/item/MessageContainerItem.kt | 2 +- .../messages/item/RegularMessageItem.kt | 134 +++++++++++------- 2 files changed, 86 insertions(+), 50 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageContainerItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageContainerItem.kt index 7283b881f01..e68407246c3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageContainerItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/MessageContainerItem.kt @@ -77,7 +77,7 @@ fun MessageContainerItem( shouldDisplayFooter: Boolean = true, onReplyClickable: Clickable? = null, isSelectedMessage: Boolean = false, - isInteractionAvailable: Boolean = true + isInteractionAvailable: Boolean = true, ) { val selfDeletionTimerState = rememberSelfDeletionTimer(message.header.messageStatus.expirationStatus) if ( diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt index c587826c69d..88050562468 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt @@ -19,27 +19,29 @@ package com.wire.android.ui.home.conversations.messages.item import androidx.compose.animation.core.FastOutLinearInEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.AnchoredDraggableState +import androidx.compose.foundation.gestures.DraggableAnchors +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.anchoredDraggable 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.FlowRow import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SwipeToDismissBox -import androidx.compose.material3.SwipeToDismissBoxState -import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -51,13 +53,18 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.platform.LocalViewConfiguration +import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp import com.wire.android.R import com.wire.android.media.audiomessage.AudioState import com.wire.android.model.Clickable @@ -274,92 +281,121 @@ sealed interface SwipableMessageConfiguration { class SwipableToReply(val onSwipedToReply: (uiMessage: UIMessage.Regular) -> Unit) : SwipableMessageConfiguration } -@OptIn(ExperimentalMaterial3Api::class) +enum class SwipeAnchor { + CENTERED, + START_TO_END +} + +@OptIn(ExperimentalFoundationApi::class) @Composable private fun SwipableToReplyBox( modifier: Modifier = Modifier, onSwipedToReply: () -> Unit = {}, - content: @Composable RowScope.() -> Unit + content: @Composable () -> Unit ) { val density = LocalDensity.current val haptic = LocalHapticFeedback.current + val configuration = LocalConfiguration.current + val screenWidth = with(density) { configuration.screenWidthDp.dp.toPx() } var didVibrateOnCurrentDrag by remember { mutableStateOf(false) } // Finish the animation in the first 25% of the drag val progressUntilAnimationCompletion = 0.33f - val dismissState = remember { - SwipeToDismissBoxState( - SwipeToDismissBoxValue.Settled, - density, - positionalThreshold = { distance: Float -> distance * progressUntilAnimationCompletion }, + val dragWidth = screenWidth * progressUntilAnimationCompletion + val dragState = remember { + AnchoredDraggableState( + initialValue = SwipeAnchor.CENTERED, + positionalThreshold = { dragWidth }, + velocityThreshold = { screenWidth }, + animationSpec = tween(), confirmValueChange = { changedValue -> - if (changedValue == SwipeToDismissBoxValue.StartToEnd) { + if (changedValue == SwipeAnchor.START_TO_END) { // Attempt to finish dismiss, notify reply intention onSwipedToReply() } - if (changedValue == SwipeToDismissBoxValue.Settled) { + if (changedValue == SwipeAnchor.CENTERED) { // Reset the haptic feedback when drag is stopped didVibrateOnCurrentDrag = false } // Reject state change, only allow returning back to rest position - changedValue == SwipeToDismissBoxValue.Settled + changedValue == SwipeAnchor.CENTERED + }, + anchors = DraggableAnchors { + SwipeAnchor.CENTERED at 0f + SwipeAnchor.START_TO_END at screenWidth } ) } val primaryColor = colorsScheme().primary - // TODO: RTL is currently broken https://issuetracker.google.com/issues/321600474 - // Maybe addressed in compose3 1.3.0 (currently in alpha) - SwipeToDismissBox( - state = dismissState, - modifier = modifier, - content = content, - enableDismissFromEndToStart = false, - backgroundContent = { + + val currentViewConfiguration = LocalViewConfiguration.current + val scopedViewConfiguration = object : ViewConfiguration by currentViewConfiguration { + // Make it easier to scroll by giving the user a bit more length to identify the gesture as vertical + override val touchSlop: Float + get() = currentViewConfiguration.touchSlop * 3f + } + CompositionLocalProvider(LocalViewConfiguration provides scopedViewConfiguration) { + Box( + modifier = modifier.fillMaxSize(), + ) { + // Drag indication Row( modifier = Modifier - .fillMaxSize() + .matchParentSize() .drawBehind { - // TODO(RTL): Might need adjusting once RTL is supported (also lacking in SwipeToDismissBox) + // TODO(RTL): Might need adjusting once RTL is supported drawRect( color = primaryColor, topLeft = Offset(0f, 0f), - size = Size(dismissState.requireOffset().absoluteValue, size.height), + size = Size(dragState.requireOffset().absoluteValue, size.height), ) }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start ) { - if (dismissState.dismissDirection == SwipeToDismissBoxValue.StartToEnd - // Sometimes this is called with progress 1f when the user stops the interaction, causing a blink. - // Ignore these cases as it doesn't make any difference - && dismissState.progress < 1f - ) { - val adjustedProgress = min(1f, (dismissState.progress / progressUntilAnimationCompletion)) - val iconSize = dimensions().fabIconSize - val spacing = dimensions().spacing16x + if (dragState.offset > 0f) { + val dragProgress = dragState.offset / dragWidth + val adjustedProgress = min(1f, dragProgress) val progress = FastOutLinearInEasing.transform(adjustedProgress) - val xOffset = with(density) { - val offsetBeforeScreenStart = iconSize.toPx() - val offsetAfterScreenStart = spacing.toPx() - val totalTravelDistance = offsetBeforeScreenStart + offsetAfterScreenStart - -offsetBeforeScreenStart + (totalTravelDistance * progress) - } - // Got to the end, user can release to + // Got to the end, user can release to perform action, so we vibrate to show it if (progress == 1f && !didVibrateOnCurrentDrag) { haptic.performHapticFeedback(HapticFeedbackType.LongPress) didVibrateOnCurrentDrag = true } - Icon( - painter = painterResource(id = R.drawable.ic_reply), - contentDescription = "", - modifier = Modifier - .size(iconSize) - .offset { IntOffset(xOffset.toInt(), 0) }, - tint = colorsScheme().onPrimary - ) + + ReplySwipeIcon(dragWidth, density, progress) } } + // Message content, which is draggable + Box( + modifier = Modifier + .fillMaxSize() + .anchoredDraggable(dragState, Orientation.Horizontal, startDragImmediately = false) + .offset { + val x = dragState.requireOffset().toInt() + IntOffset(x, 0) + }, + ) { content() } } + } +} + +@Composable +private fun ReplySwipeIcon(dragWidth: Float, density: Density, progress: Float) { + val midPointBetweenStartAndGestureEnd = dragWidth / 2 + val iconSize = dimensions().fabIconSize + val targetIconAnchorPosition = midPointBetweenStartAndGestureEnd - with(density) { iconSize.toPx() / 2 } + val xOffset = with(density) { + val totalTravelDistance = iconSize.toPx() + targetIconAnchorPosition + -iconSize.toPx() + (totalTravelDistance * progress) + } + Icon( + painter = painterResource(id = R.drawable.ic_reply), + contentDescription = "", + modifier = Modifier + .size(iconSize) + .offset { IntOffset(xOffset.toInt(), 0) }, + tint = colorsScheme().onPrimary ) } From e33275b2fb92462ff7ced872c987e82b484fd99a Mon Sep 17 00:00:00 2001 From: Alexandre Ferris Date: Wed, 8 May 2024 05:12:47 -0300 Subject: [PATCH 3/7] fix: group messages date | design changes (WPB-1733) (#2980) Signed-off-by: alexandreferris Co-authored-by: Vitor Hugo Schwaab --- .../home/conversations/ConversationScreen.kt | 26 ++++++++++++--- .../ui/home/conversations/model/UIMessage.kt | 3 ++ .../com/wire/android/util/DateTimeUtil.kt | 33 ++++++++++++++----- 3 files changed, 49 insertions(+), 13 deletions(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt index 92924b93233..7dea288b51e 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/ConversationScreen.kt @@ -1061,11 +1061,12 @@ fun MessageList( if (index > 0) { val previousMessage = lazyPagingMessages[index - 1] ?: message + val shouldDisplayDateTimeDivider = message.header.messageTime.shouldDisplayDatesDifferenceDivider( + previousDate = previousMessage.header.messageTime.utcISO + ) - val currentGroup = message.header.messageTime.getFormattedDateGroup(now = currentTime) - val previousGroup = previousMessage.header.messageTime.getFormattedDateGroup(now = currentTime) - - if (currentGroup != previousGroup) { + if (shouldDisplayDateTimeDivider) { + val previousGroup = previousMessage.header.messageTime.getFormattedDateGroup(now = currentTime) previousMessage.header.messageTime.utcISO.serverDate()?.let { serverDate -> MessageGroupDateTime( messageDateTime = serverDate, @@ -1110,6 +1111,19 @@ fun MessageList( isSelectedMessage = (message.header.messageId == selectedMessageId), isInteractionAvailable = interactionAvailability == InteractionAvailability.ENABLED ) + + val isTheOnlyItem = index == 0 && lazyPagingMessages.itemCount == 1 + val isTheLastItem = (index + 1) == lazyPagingMessages.itemCount + if (isTheOnlyItem || isTheLastItem) { + val currentGroup = message.header.messageTime.getFormattedDateGroup(now = currentTime) + message.header.messageTime.utcISO.serverDate()?.let { serverDate -> + MessageGroupDateTime( + messageDateTime = serverDate, + messageDateTimeGroup = currentGroup, + now = currentTime + ) + } + } } } JumpToLastMessageButton(lazyListState = lazyListState) @@ -1178,6 +1192,10 @@ private fun MessageGroupDateTime( Row( Modifier .fillMaxWidth() + .padding( + top = dimensions().spacing4x, + bottom = dimensions().spacing8x + ) .background(color = colorsScheme().divider) .padding( top = dimensions().spacing6x, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt index d568387f0f9..ab0105f090b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/model/UIMessage.kt @@ -33,6 +33,7 @@ import com.wire.android.ui.theme.Accent import com.wire.android.util.Copyable import com.wire.android.util.MessageDateTimeGroup import com.wire.android.util.groupedUIMessageDateTime +import com.wire.android.util.shouldDisplayDatesDifferenceDivider import com.wire.android.util.ui.LocalizedStringResource import com.wire.android.util.ui.UIText import com.wire.android.util.uiMessageDateTime @@ -626,6 +627,8 @@ enum class MessageSource { data class MessageTime(val utcISO: String) { val formattedDate: String = utcISO.uiMessageDateTime() ?: "" fun getFormattedDateGroup(now: Long): MessageDateTimeGroup? = utcISO.groupedUIMessageDateTime(now = now) + fun shouldDisplayDatesDifferenceDivider(previousDate: String): Boolean = + utcISO.shouldDisplayDatesDifferenceDivider(previousDate = previousDate) } @Stable diff --git a/app/src/main/kotlin/com/wire/android/util/DateTimeUtil.kt b/app/src/main/kotlin/com/wire/android/util/DateTimeUtil.kt index a5086799790..0e712b80d2b 100644 --- a/app/src/main/kotlin/com/wire/android/util/DateTimeUtil.kt +++ b/app/src/main/kotlin/com/wire/android/util/DateTimeUtil.kt @@ -20,7 +20,6 @@ package com.wire.android.util -import android.text.format.DateUtils import com.wire.android.appLogger import kotlinx.datetime.Instant import java.text.DateFormat @@ -28,6 +27,7 @@ import java.text.ParseException import java.text.SimpleDateFormat import java.time.LocalDate import java.time.ZoneId +import java.time.temporal.ChronoUnit import java.util.Calendar import java.util.Date import java.util.Locale @@ -43,16 +43,15 @@ private val longDateShortTimeFormat = DateFormat .getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT) private val mediumOnlyDateTimeFormat = DateFormat .getDateInstance(DateFormat.MEDIUM) -val messageTimeFormatter = DateFormat +private val messageTimeFormatter = DateFormat .getTimeInstance(DateFormat.SHORT) .apply { timeZone = TimeZone.getDefault() } -private val messageDateTimeFormatter = DateFormat - .getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT) - .apply { timeZone = TimeZone.getDefault() } private const val ONE_MINUTE_FROM_MILLIS = 60 * 1000 private const val THIRTY_MINUTES = 30 private const val ONE_WEEK_IN_DAYS = 7 private const val ONE_DAY = 1 +private const val FORTY_FIVE_MINUTES_DIFFERENCE = 45 +private const val MINIMUM_DAYS_DIFFERENCE = 1 private val readReceiptDateTimeFormat = SimpleDateFormat( "MMM dd yyyy, hh:mm a", @@ -181,12 +180,28 @@ sealed interface MessageDateTimeGroup { fun String.uiMessageDateTime(): String? = this .serverDate()?.let { serverDate -> - when (DateUtils.isToday(serverDate.time)) { - true -> messageTimeFormatter.format(serverDate) - false -> messageDateTimeFormatter.format(serverDate) - } + messageTimeFormatter.format(serverDate) } +fun String.shouldDisplayDatesDifferenceDivider(previousDate: String): Boolean { + val currentDate = this@shouldDisplayDatesDifferenceDivider + + val currentLocalDateTime = currentDate.serverDate()?.toInstant()?.atZone(ZoneId.systemDefault())?.toLocalDateTime() + val previousLocalDateTime = previousDate.serverDate()?.toInstant()?.atZone(ZoneId.systemDefault())?.toLocalDateTime() + + val differenceInMinutes = ChronoUnit.MINUTES.between( + currentLocalDateTime, + previousLocalDateTime + ) + + val differenceInDays = ChronoUnit.DAYS.between( + currentLocalDateTime, + previousLocalDateTime + ) + + return differenceInMinutes > FORTY_FIVE_MINUTES_DIFFERENCE || differenceInDays >= MINIMUM_DAYS_DIFFERENCE +} + fun String.groupedUIMessageDateTime(now: Long): MessageDateTimeGroup? = this .serverDate()?.let { serverDate -> val localDate = serverDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate() From faad6235729ce15b8e389e57a56be746796d936b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= <30429749+saleniuk@users.noreply.github.com> Date: Fri, 10 May 2024 09:38:38 +0200 Subject: [PATCH 4/7] fix: heavy sketch file (#2992) --- .../com/wire/android/feature/sketch/DrawingCanvasViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingCanvasViewModel.kt b/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingCanvasViewModel.kt index f3183943b26..efd3889aefe 100644 --- a/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingCanvasViewModel.kt +++ b/features/sketch/src/main/java/com/wire/android/feature/sketch/DrawingCanvasViewModel.kt @@ -157,7 +157,7 @@ class DrawingCanvasViewModel : ViewModel() { Bitmap.Config.ARGB_8888 ) val canvas = Canvas(bitmap).apply { drawPaint(Paint().apply { color = Color.White.toArgb() }) } - context.contentResolver.openFileDescriptor(tempSketchFile, "rw")?.use { fileDescriptor -> + context.contentResolver.openFileDescriptor(tempSketchFile, "rwt")?.use { fileDescriptor -> FileOutputStream(fileDescriptor.fileDescriptor).use { fileOutputStream -> paths.forEach { path -> path.drawNative(canvas) } bitmap.compress(Bitmap.CompressFormat.JPEG, QUALITY, fileOutputStream) From ce608e927f744bb2d1774af370e13dcb399124ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=BBerko?= Date: Fri, 10 May 2024 12:15:12 +0200 Subject: [PATCH 5/7] chore: file access crashes [WPB-7368] (#2994) --- .../com/wire/android/ExternalLoggerManager.kt | 8 +- .../avatarpicker/AvatarPickerViewModel.kt | 37 +++++---- .../com/wire/android/util/DeviceUtil.kt | 77 +++++++++++++++++++ .../kotlin/com/wire/android/util/FileUtil.kt | 52 +++---------- app/src/main/res/values/strings.xml | 1 + 5 files changed, 120 insertions(+), 55 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/util/DeviceUtil.kt diff --git a/app/src/beta/kotlin/com/wire/android/ExternalLoggerManager.kt b/app/src/beta/kotlin/com/wire/android/ExternalLoggerManager.kt index 78cc0b1658e..0810621f529 100644 --- a/app/src/beta/kotlin/com/wire/android/ExternalLoggerManager.kt +++ b/app/src/beta/kotlin/com/wire/android/ExternalLoggerManager.kt @@ -29,6 +29,7 @@ import com.datadog.android.rum.tracking.ActivityViewTrackingStrategy import com.datadog.android.rum.tracking.ComponentPredicate import com.wire.android.datastore.GlobalDataStore import com.wire.android.ui.WireActivity +import com.wire.android.util.DeviceUtil import com.wire.android.util.getDeviceIdString import com.wire.android.util.getGitBuildId import com.wire.android.util.sha256 @@ -72,10 +73,15 @@ object ExternalLoggerManager { .useSite(DatadogSite.EU1) .build() + val availableMemorySize = DeviceUtil.getAvailableInternalMemorySize() + val totalMemorySize = DeviceUtil.getTotalInternalMemorySize() + val deviceParams = mapOf("available_memory_size" to availableMemorySize, "total_memory_size" to totalMemorySize) + val credentials = Credentials(clientToken, environmentName, appVariantName, applicationId) val extraInfo = mapOf( "encrypted_proteus_storage_enabled" to runBlocking { globalDataStore.isEncryptedProteusStorageEnabled().first() }, - "git_commit_hash" to context.getGitBuildId() + "git_commit_hash" to context.getGitBuildId(), + "device_params" to deviceParams ) Datadog.initialize(context, credentials, configuration, TrackingConsent.GRANTED) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPickerViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPickerViewModel.kt index 8bdc8879867..946457b3a97 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPickerViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/avatarpicker/AvatarPickerViewModel.kt @@ -49,6 +49,7 @@ import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import okio.Path +import java.io.FileNotFoundException import javax.inject.Inject @HiltViewModel @@ -113,24 +114,31 @@ class AvatarPickerViewModel @Inject constructor( pictureState = PictureState.Uploading(imgUri) val avatarPath = defaultAvatarPath - val imageDataSize = imgUri.toByteArray(appContext, dispatchers).size.toLong() - when (val result = uploadUserAvatar(avatarPath, imageDataSize)) { - is UploadAvatarResult.Success -> { - dataStore.updateUserAvatarAssetId(result.userAssetId.toString()) - onComplete(dataStore.avatarAssetId.first()) - } - is UploadAvatarResult.Failure -> { - when (result.coreFailure) { - is NetworkFailure.NoNetworkConnection -> showInfoMessage(InfoMessageType.NoNetworkError) - else -> showInfoMessage(InfoMessageType.UploadAvatarError) + try { + val imageDataSize = imgUri.toByteArray(appContext, dispatchers).size.toLong() + + when (val result = uploadUserAvatar(avatarPath, imageDataSize)) { + is UploadAvatarResult.Success -> { + dataStore.updateUserAvatarAssetId(result.userAssetId.toString()) + onComplete(dataStore.avatarAssetId.first()) } - with(initialPictureLoadingState) { - pictureState = when (this) { - is InitialPictureLoadingState.Loaded -> PictureState.Initial(avatarUri) - else -> PictureState.Empty + + is UploadAvatarResult.Failure -> { + when (result.coreFailure) { + is NetworkFailure.NoNetworkConnection -> showInfoMessage(InfoMessageType.NoNetworkError) + else -> showInfoMessage(InfoMessageType.UploadAvatarError) + } + with(initialPictureLoadingState) { + pictureState = when (this) { + is InitialPictureLoadingState.Loaded -> PictureState.Initial(avatarUri) + else -> PictureState.Empty + } } } } + } catch (e: FileNotFoundException) { + appLogger.e("[AvatarPickerViewModel] Could not find a file", e) + showInfoMessage(InfoMessageType.ImageProcessError) } } } @@ -157,5 +165,6 @@ class AvatarPickerViewModel @Inject constructor( sealed class InfoMessageType(override val uiText: UIText) : SnackBarMessage { data object UploadAvatarError : InfoMessageType(UIText.StringResource(R.string.error_uploading_user_avatar)) data object NoNetworkError : InfoMessageType(UIText.StringResource(R.string.error_no_network_message)) + data object ImageProcessError : InfoMessageType(UIText.StringResource(R.string.error_process_user_avatar)) } } diff --git a/app/src/main/kotlin/com/wire/android/util/DeviceUtil.kt b/app/src/main/kotlin/com/wire/android/util/DeviceUtil.kt new file mode 100644 index 00000000000..74ad6cbbffb --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/util/DeviceUtil.kt @@ -0,0 +1,77 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.util + +import android.os.Environment +import android.os.StatFs + +object DeviceUtil { + private const val BYTES_IN_KILOBYTE = 1024 + private const val BYTES_IN_MEGABYTE = BYTES_IN_KILOBYTE * 1024 + private const val BYTES_IN_GIGABYTE = BYTES_IN_MEGABYTE * 1024 + private const val DIGITS_GROUP_SIZE = 3 // Number of digits between commas in formatted size. + + fun getAvailableInternalMemorySize(): String = try { + val path = Environment.getDataDirectory() + val stat = StatFs(path.path) + val blockSize = stat.blockSizeLong + val availableBlocks = stat.availableBlocksLong + formatSize(availableBlocks * blockSize) + } catch (e: IllegalArgumentException) { + "" + } + + fun getTotalInternalMemorySize(): String = try { + val path = Environment.getDataDirectory() + val stat = StatFs(path.path) + val blockSize = stat.blockSizeLong + val totalBlocks = stat.blockCountLong + formatSize(totalBlocks * blockSize) + } catch (e: IllegalArgumentException) { + "" + } + + private fun formatSize(sizeInBytes: Long): String { + var size = sizeInBytes + var suffix: String? = null + when { + size >= BYTES_IN_GIGABYTE -> { + suffix = "GB" + size /= BYTES_IN_GIGABYTE + } + + size >= BYTES_IN_MEGABYTE -> { + suffix = "MB" + size /= BYTES_IN_MEGABYTE + } + + size >= BYTES_IN_KILOBYTE -> { + suffix = "KB" + size /= BYTES_IN_KILOBYTE + } + } + val resultBuffer = StringBuilder(size.toString()) + var commaOffset = resultBuffer.length - DIGITS_GROUP_SIZE + while (commaOffset > 0) { + resultBuffer.insert(commaOffset, ',') + commaOffset -= DIGITS_GROUP_SIZE + } + suffix?.let { resultBuffer.append(it) } + return resultBuffer.toString() + } +} diff --git a/app/src/main/kotlin/com/wire/android/util/FileUtil.kt b/app/src/main/kotlin/com/wire/android/util/FileUtil.kt index 4a66f7c3b2e..667b5765e5e 100644 --- a/app/src/main/kotlin/com/wire/android/util/FileUtil.kt +++ b/app/src/main/kotlin/com/wire/android/util/FileUtil.kt @@ -27,7 +27,6 @@ import android.content.ContentValues import android.content.Context import android.content.Intent import android.database.Cursor -import android.graphics.drawable.Drawable import android.media.MediaMetadataRetriever import android.net.Uri import android.os.Build @@ -42,10 +41,7 @@ import android.provider.MediaStore.MediaColumns.SIZE import android.provider.OpenableColumns import android.provider.Settings import android.webkit.MimeTypeMap -import androidx.annotation.AnyRes -import androidx.annotation.NonNull import androidx.annotation.VisibleForTesting -import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import com.wire.android.R import com.wire.android.appLogger @@ -62,28 +58,11 @@ import kotlinx.serialization.json.Json import okio.Path import java.io.File import java.io.FileNotFoundException +import java.io.IOException import java.io.InputStream import java.util.Locale import kotlin.time.Duration.Companion.milliseconds -/** - * Gets the uri of any drawable or given resource - * @param context - context - * @param drawableId - drawable res id - * @return - uri - */ -fun getUriFromDrawable( - @NonNull context: Context, - @AnyRes drawableId: Int -): Uri { - return Uri.parse( - ContentResolver.SCHEME_ANDROID_RESOURCE + - "://" + context.resources.getResourcePackageName(drawableId) + - '/' + context.resources.getResourceTypeName(drawableId) + - '/' + context.resources.getResourceEntryName(drawableId) - ) -} - @Suppress("MagicNumber") suspend fun Uri.toByteArray(context: Context, dispatcher: DispatcherProvider = DefaultDispatcherProvider()): ByteArray { return withContext(dispatcher.io()) { @@ -91,21 +70,6 @@ suspend fun Uri.toByteArray(context: Context, dispatcher: DispatcherProvider = D } } -suspend fun Uri.toDrawable(context: Context, dispatcher: DispatcherProvider = DefaultDispatcherProvider()): Drawable? { - val dataUri = this - return withContext(dispatcher.io()) { - try { - context.contentResolver.openInputStream(dataUri).use { inputStream -> - Drawable.createFromStream(inputStream, dataUri.toString()) - } - } catch (e: FileNotFoundException) { - defaultGalleryIcon(context) - } - } -} - -private fun defaultGalleryIcon(context: Context) = ContextCompat.getDrawable(context, R.drawable.ic_gallery) - fun getTempWritableAttachmentUri(context: Context, attachmentPath: Path): Uri { val file = attachmentPath.toFile() file.setWritable(true) @@ -243,9 +207,17 @@ suspend fun Uri.resampleImageAndCopyToTempPath( ImageUtil.resample(originalImage, sizeClass).let { processedImage -> val file = tempCachePath.toFile() - size = processedImage.size.toLong() - file.setWritable(true) - file.outputStream().use { it.write(processedImage) } + try { + size = processedImage.size.toLong() + file.setWritable(true) + file.outputStream().use { it.write(processedImage) } + } catch (e: FileNotFoundException) { + appLogger.e("[ResampleImage] Cannot find file ${file.path}", e) + throw e + } catch (e: IOException) { + appLogger.e("[ResampleImage] I/O error while writing the image", e) + throw e + } } size diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ac1b79197a6..4b66ac39640 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -774,6 +774,7 @@ Please check your Internet connection and try again There was an error downloading your profile picture. Please check your Internet connection Picture could not be uploaded + Picture could not be processed Image upload failed Image download failed Notifications could not be updated From 163d5ab1f17146a410e1adbeb5b0662872dc9fa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Saleniuk?= <30429749+saleniuk@users.noreply.github.com> Date: Fri, 10 May 2024 17:22:14 +0200 Subject: [PATCH 6/7] fix: crashing message composer input [WPB-8727] (#2988) --- app/build.gradle.kts | 1 + .../ui/calling/controlbuttons/CameraButton.kt | 4 +- .../controlbuttons/CameraFlipButton.kt | 4 +- .../calling/controlbuttons/DeclineButton.kt | 4 +- .../controlbuttons/MicrophoneButton.kt | 4 +- .../calling/controlbuttons/SpeakerButton.kt | 5 +- .../wire/android/ui/common/AppExtensions.kt | 4 +- .../common/textfield/StateSyncingModifier.kt | 105 ++++++++++ .../ui/common/textfield/WireTextField.kt | 14 +- .../ui/common/textfield/WireTextField2.kt | 195 ++++++++++++++++++ .../messages/item/RegularMessageItem.kt | 4 +- .../messagecomposer/MessageComposerInput.kt | 4 +- core/ui-common/build.gradle.kts | 1 + .../ui/common/snackbar/SwipeableSnackbar.kt | 4 +- features/sketch/build.gradle.kts | 1 + features/template/build.gradle.kts | 1 + gradle/libs.versions.toml | 5 +- 17 files changed, 336 insertions(+), 24 deletions(-) create mode 100644 app/src/main/kotlin/com/wire/android/ui/common/textfield/StateSyncingModifier.kt create mode 100644 app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField2.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ae26201ea83..e3f131194d9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -135,6 +135,7 @@ dependencies { implementation(libs.compose.ui) implementation(libs.compose.foundation) + implementation(libs.compose.material.android) // we still cannot get rid of material2 because swipeable is still missing - https://issuetracker.google.com/issues/229839039 // https://developer.android.com/jetpack/compose/designsystems/material2-material3#components-and implementation(libs.compose.material.core) diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/CameraButton.kt b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/CameraButton.kt index aec1317a0fa..472756b7e71 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/CameraButton.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/CameraButton.kt @@ -22,7 +22,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material.ripple import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -62,7 +62,7 @@ fun CameraButton( .wrapContentSize() .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple( + indication = ripple( bounded = false, radius = dimensions().defaultCallingControlsSize / 2 ), diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/CameraFlipButton.kt b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/CameraFlipButton.kt index 29c06df9314..5b8920776e1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/CameraFlipButton.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/CameraFlipButton.kt @@ -21,7 +21,7 @@ package com.wire.android.ui.calling.controlbuttons import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material.ripple import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -46,7 +46,7 @@ fun CameraFlipButton( .wrapContentSize() .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false, radius = dimensions().defaultCallingControlsSize / 2), + indication = ripple(bounded = false, radius = dimensions().defaultCallingControlsSize / 2), role = Role.Button, onClick = onCameraFlipButtonClicked diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/DeclineButton.kt b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/DeclineButton.kt index d3a34db51bd..696d3602633 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/DeclineButton.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/DeclineButton.kt @@ -21,7 +21,7 @@ package com.wire.android.ui.calling.controlbuttons import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.size -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material.ripple import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable @@ -45,7 +45,7 @@ fun DeclineButton(buttonClicked: () -> Unit) { modifier = Modifier .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple( + indication = ripple( bounded = false, radius = dimensions().outgoingCallHangUpButtonSize / 2 ), diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/MicrophoneButton.kt b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/MicrophoneButton.kt index 5eac7fbd901..a19c42d80b1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/MicrophoneButton.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/MicrophoneButton.kt @@ -22,7 +22,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material.ripple import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -49,7 +49,7 @@ fun MicrophoneButton( .wrapContentSize() .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false, radius = dimensions().defaultCallingControlsSize / 2), + indication = ripple(bounded = false, radius = dimensions().defaultCallingControlsSize / 2), role = Role.Button, onClick = { onMicrophoneButtonClicked() } ), diff --git a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/SpeakerButton.kt b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/SpeakerButton.kt index 91a0949c9ce..0771baf44cc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/SpeakerButton.kt +++ b/app/src/main/kotlin/com/wire/android/ui/calling/controlbuttons/SpeakerButton.kt @@ -22,12 +22,11 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material.ripple import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role @@ -50,7 +49,7 @@ fun SpeakerButton( .wrapContentSize() .clickable( interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = false, radius = dimensions().defaultCallingControlsSize / 2), + indication = ripple(bounded = false, radius = dimensions().defaultCallingControlsSize / 2), role = Role.Button, onClick = { onSpeakerButtonClicked() } ), diff --git a/app/src/main/kotlin/com/wire/android/ui/common/AppExtensions.kt b/app/src/main/kotlin/com/wire/android/ui/common/AppExtensions.kt index dab64da1e8e..6272bc25548 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/AppExtensions.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/AppExtensions.kt @@ -21,7 +21,7 @@ package com.wire.android.ui.common import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material.ripple import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.State @@ -62,7 +62,7 @@ fun Modifier.selectableBackground(isSelected: Boolean, onClick: () -> Unit): Mod selected = isSelected, onClick = { onClick() }, interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = true, color = MaterialTheme.colorScheme.onBackground.copy(0.5f)), + indication = ripple(bounded = true, color = MaterialTheme.colorScheme.onBackground.copy(0.5f)), role = Role.Tab ) diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/StateSyncingModifier.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/StateSyncingModifier.kt new file mode 100644 index 00000000000..afba166ade1 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/common/textfield/StateSyncingModifier.kt @@ -0,0 +1,105 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ +package com.wire.android.ui.common.textfield + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.text.input.TextFieldCharSequence +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.ui.Modifier +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.ObserverModifierNode +import androidx.compose.ui.node.observeReads +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.text.input.TextFieldValue +import io.github.esentsov.PackagePrivate + +/** + * Enables us to temporarily still use TextFieldValue and onValueChanged callback instead of TextFieldState directly, + * also allows us to get selection updates as by default BasicTextField2 callback only gives a String without selection. + * @sample androidx.compose.foundation.samples.BasicTextFieldWithValueOnValueChangeSample + * TODO: Remove this class once all WireTextField usages are migrated to use TextFieldState. + */ +@PackagePrivate +internal class StateSyncingModifier( + private val state: TextFieldState, + private val value: TextFieldValue, + private val onValueChanged: (TextFieldValue) -> Unit, +) : ModifierNodeElement() { + + override fun create(): StateSyncingModifierNode = StateSyncingModifierNode(state, onValueChanged) + + override fun update(node: StateSyncingModifierNode) { + node.update(value, onValueChanged) + } + + @Suppress("EqualsAlwaysReturnsTrueOrFalse") + override fun equals(other: Any?): Boolean = false + + override fun hashCode(): Int = state.hashCode() + + @Suppress("EmptyFunctionBlock") + override fun InspectorInfo.inspectableProperties() {} +} + +@OptIn(ExperimentalFoundationApi::class) +@PackagePrivate +internal class StateSyncingModifierNode( + private val state: TextFieldState, + private var onValueChanged: (TextFieldValue) -> Unit, +) : Modifier.Node(), ObserverModifierNode { + override val shouldAutoInvalidate: Boolean + get() = false + + fun update(value: TextFieldValue, onValueChanged: (TextFieldValue) -> Unit) { + this.onValueChanged = onValueChanged + if (value.text != state.text.toString() || value.selection != state.text.selection) { + state.edit { + if (value.text != state.text.toString()) { + replace(0, length, value.text) + } + if (value.selection != state.text.selection) { + selection = value.selection + } + } + onValueChanged(value) + } + } + + override fun onAttach() { + observeTextState(fireOnValueChanged = false) + } + + override fun onObservedReadsChanged() { + observeTextState() + } + + private fun observeTextState(fireOnValueChanged: Boolean = true) { + lateinit var text: TextFieldCharSequence + observeReads { + text = state.text + } + if (fireOnValueChanged) { + val newValue = TextFieldValue( + text = text.toString(), + selection = text.selection, + composition = text.composition + ) + onValueChanged(newValue) + } + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField.kt index 270ef97bffd..4a0d1bc28c6 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField.kt @@ -77,6 +77,7 @@ import com.wire.android.ui.common.Tint import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography import com.wire.android.util.EMPTY +import io.github.esentsov.PackagePrivate @Composable internal fun WireTextField( @@ -156,7 +157,7 @@ internal fun WireTextField( decorationBox = { innerTextField -> InnerText( innerTextField, - value, + value.text.isEmpty(), leadingIcon, trailingIcon, placeholderText, @@ -219,10 +220,11 @@ fun Label( } } +@PackagePrivate @Composable -private fun InnerText( +internal fun InnerText( innerTextField: @Composable () -> Unit, - value: TextFieldValue, + shouldShowPlaceholder: Boolean, leadingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, placeholderText: String? = null, @@ -232,12 +234,12 @@ private fun InnerText( inputMinHeight: Dp = 48.dp, colors: WireTextFieldColors = wireTextFieldColors(), shouldDetectTaps: Boolean = false, - onClick: (Offset) -> Unit = { } + onTap: (Offset) -> Unit = { } ) { var modifier: Modifier = Modifier if (shouldDetectTaps) { modifier = modifier.pointerInput(Unit) { - detectTapGestures(onTap = onClick) + detectTapGestures(onTap = onTap) } } @@ -266,7 +268,7 @@ private fun InnerText( top = 2.dp, bottom = 2.dp ) ) { - if (value.text.isEmpty() && placeholderText != null) { + if (shouldShowPlaceholder && placeholderText != null) { Text( text = placeholderText, style = placeholderTextStyle, diff --git a/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField2.kt b/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField2.kt new file mode 100644 index 00000000000..25c770f9ca0 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/common/textfield/WireTextField2.kt @@ -0,0 +1,195 @@ +/* + * Wire + * Copyright (C) 2024 Wire Swiss GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see http://www.gnu.org/licenses/. + */ + +package com.wire.android.ui.common.textfield + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.InputTransformation +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.maxLength +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDirection +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.theme.WireTheme +import com.wire.android.ui.theme.wireDimensions +import com.wire.android.ui.theme.wireTypography +import com.wire.android.util.EMPTY +import com.wire.android.util.ui.PreviewMultipleThemes + +/** + * Hybrid text field that uses new BasicTextField2 which resolves multiple issues that old ones had. It's been renamed to BasicTextField + * as well in the newest compose version. The difference is that this new text field takes TextFieldState, all other BasicTextFields + * which take TextFieldValue or String with onValueChange callback are the previous generation ones. + * This hybrid is created to allow us to still pass TextFieldValue and onValueChange callback but already use the new text input version. + */ +@OptIn(ExperimentalFoundationApi::class) +@Composable +internal fun WireTextField2( + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, + placeholderText: String? = null, + labelText: String? = null, + labelMandatoryIcon: Boolean = false, + descriptionText: String? = null, + leadingIcon: @Composable (() -> Unit)? = null, + trailingIcon: @Composable (() -> Unit)? = null, + readOnly: Boolean = false, + state: WireTextFieldState = WireTextFieldState.Default, + maxLines: Int = 1, + singleLine: Boolean = true, + maxTextLength: Int = 8000, + keyboardOptions: KeyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Sentences, + autoCorrect = true + ), + keyboardActions: KeyboardActions = KeyboardActions.Default, + scrollState: ScrollState = rememberScrollState(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + textStyle: TextStyle = MaterialTheme.wireTypography.body01, + placeholderTextStyle: TextStyle = MaterialTheme.wireTypography.body01, + placeholderAlignment: Alignment.Horizontal = Alignment.Start, + inputMinHeight: Dp = MaterialTheme.wireDimensions.textFieldMinHeight, + shape: Shape = RoundedCornerShape(MaterialTheme.wireDimensions.textFieldCornerSize), + colors: WireTextFieldColors = wireTextFieldColors(), + modifier: Modifier = Modifier, + onSelectedLineIndexChanged: (Int) -> Unit = { }, + onLineBottomYCoordinateChanged: (Float) -> Unit = { }, + shouldDetectTaps: Boolean = false, + testTag: String = String.EMPTY, + onTap: (Offset) -> Unit = { }, +) { + val textState = rememberTextFieldState(value.text, value.selection) + val lineLimits = if (singleLine) TextFieldLineLimits.SingleLine else TextFieldLineLimits.MultiLine(1, maxLines) + + Column(modifier = modifier) { + if (labelText != null) { + Label(labelText, labelMandatoryIcon, state, interactionSource, colors) + } + + BasicTextField( + state = textState, + textStyle = textStyle.copy(color = colors.textColor(state = state).value, textDirection = TextDirection.ContentOrLtr), + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + lineLimits = lineLimits, + inputTransformation = InputTransformation.maxLength(maxTextLength), + scrollState = scrollState, + readOnly = readOnly, + enabled = state !is WireTextFieldState.Disabled, + cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), + interactionSource = interactionSource, + modifier = Modifier + .fillMaxWidth() + .background(color = colors.backgroundColor(state).value, shape = shape) + .border(width = dimensions().spacing1x, color = colors.borderColor(state, interactionSource).value, shape = shape) + .semantics { + (labelText ?: placeholderText ?: descriptionText)?.let { + contentDescription = it + } + } + .testTag(testTag) + .then( + StateSyncingModifier( + state = textState, + value = value, + onValueChanged = onValueChange + ) + ), + decorator = { innerTextField -> + InnerText( + innerTextField = innerTextField, + shouldShowPlaceholder = textState.text.isEmpty(), + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + placeholderText = placeholderText, + state = state, + placeholderTextStyle = placeholderTextStyle, + placeholderAlignment = placeholderAlignment, + inputMinHeight = inputMinHeight, + colors = colors, + shouldDetectTaps = shouldDetectTaps, + onTap = onTap, + ) + }, + onTextLayout = { + it()?.let { + val lineOfText = it.getLineForOffset(textState.text.selection.end) + val bottomYCoordinate = it.getLineBottom(lineOfText) + onSelectedLineIndexChanged(lineOfText) + onLineBottomYCoordinateChanged(bottomYCoordinate) + } + } + ) + + val bottomText = when { + state is WireTextFieldState.Error && state.errorText != null -> state.errorText + !descriptionText.isNullOrEmpty() -> descriptionText + else -> String.EMPTY + } + AnimatedVisibility(visible = bottomText.isNotEmpty()) { + Text( + text = bottomText, + style = MaterialTheme.wireTypography.label04, + textAlign = TextAlign.Start, + color = colors.descriptionColor(state).value, + modifier = Modifier.padding(top = dimensions().spacing4x) + ) + } + } +} + +@PreviewMultipleThemes +@Composable +fun PreviewWireTextField2() = WireTheme { + WireTextField2( + value = TextFieldValue("text"), + onValueChange = {}, + modifier = Modifier.padding(16.dp) + ) +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt index 88050562468..f2c731f0e43 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/conversations/messages/item/RegularMessageItem.kt @@ -20,6 +20,7 @@ package com.wire.android.ui.home.conversations.messages.item import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.tween +import androidx.compose.animation.splineBasedDecay import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.AnchoredDraggableState import androidx.compose.foundation.gestures.DraggableAnchors @@ -307,7 +308,8 @@ private fun SwipableToReplyBox( initialValue = SwipeAnchor.CENTERED, positionalThreshold = { dragWidth }, velocityThreshold = { screenWidth }, - animationSpec = tween(), + snapAnimationSpec = tween(), + decayAnimationSpec = splineBasedDecay(density), confirmValueChange = { changedValue -> if (changedValue == SwipeAnchor.START_TO_END) { // Attempt to finish dismiss, notify reply intention diff --git a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt index 3a1bafc0baa..3b09f96122b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/messagecomposer/MessageComposerInput.kt @@ -59,7 +59,7 @@ import com.wire.android.R import com.wire.android.ui.common.colorsScheme import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.spacers.VerticalSpace -import com.wire.android.ui.common.textfield.WireTextField +import com.wire.android.ui.common.textfield.WireTextField2 import com.wire.android.ui.common.textfield.WireTextFieldColors import com.wire.android.ui.home.conversations.UsersTypingIndicatorForConversation import com.wire.android.ui.home.conversations.messages.QuotedMessagePreview @@ -301,7 +301,7 @@ private fun MessageComposerTextInput( } } - WireTextField( + WireTextField2( value = messageText, onValueChange = onMessageTextChanged, colors = colors, diff --git a/core/ui-common/build.gradle.kts b/core/ui-common/build.gradle.kts index 5262f93c7f0..b01cc651ee2 100644 --- a/core/ui-common/build.gradle.kts +++ b/core/ui-common/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { implementation(composeBom) implementation(libs.compose.ui) implementation(libs.compose.foundation) + implementation(libs.compose.material.android) implementation(libs.compose.ui.graphics) implementation(libs.compose.material.core) implementation(libs.compose.material3) diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/snackbar/SwipeableSnackbar.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/snackbar/SwipeableSnackbar.kt index 91e126d15b2..f9e5bbf8659 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/common/snackbar/SwipeableSnackbar.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/common/snackbar/SwipeableSnackbar.kt @@ -18,6 +18,7 @@ package com.wire.android.ui.common.snackbar import androidx.compose.animation.core.SpringSpec +import androidx.compose.animation.splineBasedDecay import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.AnchoredDraggableState import androidx.compose.foundation.gestures.DraggableAnchors @@ -89,7 +90,8 @@ fun SwipeableSnackbar( anchors = anchors, positionalThreshold = positionalThreshold, velocityThreshold = velocityThreshold, - animationSpec = SpringSpec(), + snapAnimationSpec = SpringSpec(), + decayAnimationSpec = splineBasedDecay(density), confirmValueChange = { true } ) } diff --git a/features/sketch/build.gradle.kts b/features/sketch/build.gradle.kts index 5f7e54c713a..c06d5df7849 100644 --- a/features/sketch/build.gradle.kts +++ b/features/sketch/build.gradle.kts @@ -14,6 +14,7 @@ dependencies { implementation(composeBom) implementation(libs.compose.ui) implementation(libs.compose.foundation) + implementation(libs.compose.material.android) implementation(libs.compose.ui.graphics) implementation(libs.compose.material.core) implementation(libs.compose.material3) diff --git a/features/template/build.gradle.kts b/features/template/build.gradle.kts index 8f553b3fa3b..89bb9604ba3 100644 --- a/features/template/build.gradle.kts +++ b/features/template/build.gradle.kts @@ -12,6 +12,7 @@ dependencies { implementation(composeBom) implementation(libs.compose.ui) implementation(libs.compose.foundation) + implementation(libs.compose.material.android) testImplementation(libs.junit4) androidTestImplementation(libs.androidx.test.extJunit) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7a4eb4a4699..9f1e52e832c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -45,6 +45,8 @@ androidx-startup = "1.1.1" # Compose composeBom = "2024.04.01" +compose-foundation = "1.7.0-alpha05" # remove when composeBom contains new stable version of BasicTextField2 +compose-material-android = "1.7.0-alpha05" # remove when composeBom contains new stable version of BasicTextField2 compose-activity = "1.8.2" compose-compiler = "1.5.11" compose-constraint = "1.0.1" @@ -181,7 +183,8 @@ hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "hilt-work" } # Compose BOM compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } -compose-foundation = { module = "androidx.compose.foundation:foundation" } +compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose-foundation" } +compose-material-android = { module = "androidx.compose.material:material-android", version.ref = "compose-material-android" } compose-material-core = { module = "androidx.compose.material:material" } compose-material-icons = { module = "androidx.compose.material:material-icons-extended" } compose-material-ripple = { module = "androidx.compose.material:material-ripple" } From 1c93e471cb4d5f796e380d616f8240658833f213 Mon Sep 17 00:00:00 2001 From: boris Date: Wed, 15 May 2024 11:59:36 +0300 Subject: [PATCH 7/7] fix: Websocket toggle always off [WBP-8669] (#2999) --- .../appsettings/networkSettings/NetworkSettingsScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsScreen.kt index d46ef8a6ea9..c13a5fa6f44 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/settings/appsettings/networkSettings/NetworkSettingsScreen.kt @@ -79,7 +79,7 @@ fun NetworkSettingsScreenContent( val isWebSocketEnforcedByDefault = remember { isWebsocketEnabledByDefault(appContext) } - val switchState = remember { + val switchState = remember(isWebSocketEnabled) { if (isWebSocketEnforcedByDefault) { SwitchState.TextOnly(true) } else {