diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 329196a958..c17ac1035b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -41,6 +41,7 @@ import io.element.android.features.messages.impl.actionlist.model.TimelineItemAc import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter import io.element.android.features.messages.impl.messagecomposer.MessageComposerState +import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState import io.element.android.features.messages.impl.timeline.TimelineController import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelinePresenter @@ -101,6 +102,7 @@ class MessagesPresenter @AssistedInject constructor( private val customReactionPresenter: CustomReactionPresenter, private val reactionSummaryPresenter: ReactionSummaryPresenter, private val readReceiptBottomSheetPresenter: ReadReceiptBottomSheetPresenter, + private val pinnedMessagesBannerPresenter: Presenter, private val networkMonitor: NetworkMonitor, private val snackbarDispatcher: SnackbarDispatcher, private val dispatchers: CoroutineDispatchers, @@ -132,6 +134,7 @@ class MessagesPresenter @AssistedInject constructor( val customReactionState = customReactionPresenter.present() val reactionSummaryState = reactionSummaryPresenter.present() val readReceiptBottomSheetState = readReceiptBottomSheetPresenter.present() + val pinnedMessagesBannerState = pinnedMessagesBannerPresenter.present() val syncUpdateFlow = room.syncUpdateFlow.collectAsState() @@ -230,6 +233,7 @@ class MessagesPresenter @AssistedInject constructor( enableVoiceMessages = enableVoiceMessages, appName = buildMeta.applicationName, callState = callState, + pinnedMessagesBannerState = pinnedMessagesBannerState, eventSink = { handleEvents(it) } ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index 99e5b50de8..172111e862 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -19,6 +19,7 @@ package io.element.android.features.messages.impl import androidx.compose.runtime.Immutable import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.messagecomposer.MessageComposerState +import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState import io.element.android.features.messages.impl.timeline.TimelineState import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState import io.element.android.features.messages.impl.timeline.components.reactionsummary.ReactionSummaryState @@ -54,6 +55,7 @@ data class MessagesState( val enableVoiceMessages: Boolean, val callState: RoomCallState, val appName: String, + val pinnedMessagesBannerState: PinnedMessagesBannerState, val eventSink: (MessagesEvents) -> Unit ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index 3f3a691a98..a1b7bc6ad1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -22,6 +22,8 @@ import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.messagecomposer.AttachmentsState import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState +import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState +import io.element.android.features.messages.impl.pinned.banner.aPinnedMessagesBannerState import io.element.android.features.messages.impl.timeline.TimelineState import io.element.android.features.messages.impl.timeline.aTimelineItemList import io.element.android.features.messages.impl.timeline.aTimelineState @@ -87,6 +89,12 @@ open class MessagesStateProvider : PreviewParameterProvider { aMessagesState( callState = RoomCallState.DISABLED, ), + aMessagesState( + pinnedMessagesBannerState = aPinnedMessagesBannerState( + pinnedMessagesCount = 4, + currentPinnedMessageIndex = 0, + ), + ), ) } @@ -113,6 +121,7 @@ fun aMessagesState( showReinvitePrompt: Boolean = false, enableVoiceMessages: Boolean = true, callState: RoomCallState = RoomCallState.ENABLED, + pinnedMessagesBannerState: PinnedMessagesBannerState = aPinnedMessagesBannerState(), eventSink: (MessagesEvents) -> Unit = {}, ) = MessagesState( roomId = RoomId("!id:domain"), @@ -136,6 +145,7 @@ fun aMessagesState( enableVoiceMessages = enableVoiceMessages, callState = callState, appName = "Element", + pinnedMessagesBannerState = pinnedMessagesBannerState, eventSink = eventSink, ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 0a868fac12..3f606f9fb9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -16,6 +16,9 @@ package io.element.android.features.messages.impl +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -33,6 +36,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme @@ -68,6 +72,7 @@ import io.element.android.features.messages.impl.messagecomposer.AttachmentsBott import io.element.android.features.messages.impl.messagecomposer.AttachmentsState import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerView +import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerView import io.element.android.features.messages.impl.timeline.TimelineView import io.element.android.features.messages.impl.timeline.components.JoinCallMenuItem import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet @@ -100,6 +105,7 @@ import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.utils.KeepScreenOn import io.element.android.libraries.designsystem.utils.OnLifecycleEvent +import io.element.android.libraries.designsystem.utils.isScrollingUp import io.element.android.libraries.designsystem.utils.snackbar.SnackbarHost import io.element.android.libraries.designsystem.utils.snackbar.rememberSnackbarHostState import io.element.android.libraries.matrix.api.core.UserId @@ -370,22 +376,34 @@ private fun MessagesViewContent( RectangleShape }, content = { paddingValues -> - TimelineView( - state = state.timelineState, - typingNotificationState = state.typingNotificationState, - onUserDataClick = onUserDataClick, - onLinkClick = onLinkClick, - onMessageClick = onMessageClick, - onMessageLongClick = onMessageLongClick, - onSwipeToReply = onSwipeToReply, - onReactionClick = onReactionClick, - onReactionLongClick = onReactionLongClick, - onMoreReactionsClick = onMoreReactionsClick, - onReadReceiptClick = onReadReceiptClick, - modifier = Modifier.padding(paddingValues), - forceJumpToBottomVisibility = forceJumpToBottomVisibility, - onJoinCallClick = onJoinCallClick, - ) + Box(modifier = Modifier.padding(paddingValues)) { + val timelineLazyListState = rememberLazyListState() + TimelineView( + state = state.timelineState, + typingNotificationState = state.typingNotificationState, + onUserDataClick = onUserDataClick, + onLinkClick = onLinkClick, + onMessageClick = onMessageClick, + onMessageLongClick = onMessageLongClick, + onSwipeToReply = onSwipeToReply, + onReactionClick = onReactionClick, + onReactionLongClick = onReactionLongClick, + onMoreReactionsClick = onMoreReactionsClick, + onReadReceiptClick = onReadReceiptClick, + forceJumpToBottomVisibility = forceJumpToBottomVisibility, + onJoinCallClick = onJoinCallClick, + lazyListState = timelineLazyListState, + ) + AnimatedVisibility( + visible = state.pinnedMessagesBannerState.displayBanner && timelineLazyListState.isScrollingUp(), + enter = expandVertically(), + exit = shrinkVertically(), + ) { + PinnedMessagesBannerView( + state = state.pinnedMessagesBannerState, + ) + } + } }, sheetContent = { subcomposing: Boolean -> MessagesViewComposerBottomSheetContents( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt new file mode 100644 index 0000000000..77fafd0887 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/di/MessagesModule.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerPresenter +import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerState +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.di.SessionScope + +@ContributesTo(SessionScope::class) +@Module +interface MessagesModule { + @Binds + fun bindPinnedMessagesBannerPresenter(presenter: PinnedMessagesBannerPresenter): Presenter +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerEvents.kt new file mode 100644 index 0000000000..792db7e3da --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerEvents.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.pinned.banner + +sealed interface PinnedMessagesBannerEvents { + data object MoveToNextPinned : PinnedMessagesBannerEvents +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt new file mode 100644 index 0000000000..e020581468 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.pinned.banner + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import io.element.android.libraries.architecture.Presenter +import javax.inject.Inject + +class PinnedMessagesBannerPresenter @Inject constructor() : Presenter { + @Composable + override fun present(): PinnedMessagesBannerState { + var pinnedMessageCount by remember { + mutableIntStateOf(0) + } + var currentPinnedMessageIndex by rememberSaveable { + mutableIntStateOf(0) + } + + fun handleEvent(event: PinnedMessagesBannerEvents) { + when (event) { + is PinnedMessagesBannerEvents.MoveToNextPinned -> { + if (currentPinnedMessageIndex < pinnedMessageCount - 1) { + currentPinnedMessageIndex++ + } else { + currentPinnedMessageIndex = 0 + } + } + } + } + + return PinnedMessagesBannerState( + pinnedMessagesCount = pinnedMessageCount, + currentPinnedMessageIndex = currentPinnedMessageIndex, + eventSink = ::handleEvent + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerState.kt new file mode 100644 index 0000000000..6c285d3f68 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerState.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.pinned.banner + +data class PinnedMessagesBannerState( + val pinnedMessagesCount: Int, + val currentPinnedMessageIndex: Int, + val eventSink: (PinnedMessagesBannerEvents) -> Unit +) { + val displayBanner = pinnedMessagesCount > 0 && currentPinnedMessageIndex < pinnedMessagesCount +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerStateProvider.kt new file mode 100644 index 0000000000..54fa0f12c0 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerStateProvider.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.pinned.banner + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +internal class PinnedMessagesBannerStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aPinnedMessagesBannerState(pinnedMessagesCount = 1, currentPinnedMessageIndex = 0), + aPinnedMessagesBannerState(pinnedMessagesCount = 2, currentPinnedMessageIndex = 0), + aPinnedMessagesBannerState(pinnedMessagesCount = 4, currentPinnedMessageIndex = 0), + aPinnedMessagesBannerState(pinnedMessagesCount = 4, currentPinnedMessageIndex = 1), + aPinnedMessagesBannerState(pinnedMessagesCount = 4, currentPinnedMessageIndex = 2), + aPinnedMessagesBannerState(pinnedMessagesCount = 4, currentPinnedMessageIndex = 3), + ) +} + +internal fun aPinnedMessagesBannerState( + pinnedMessagesCount: Int = 0, + currentPinnedMessageIndex: Int = -1, + eventSink: (PinnedMessagesBannerEvents) -> Unit = {} +) = PinnedMessagesBannerState( + pinnedMessagesCount = pinnedMessagesCount, + currentPinnedMessageIndex = currentPinnedMessageIndex, + eventSink = eventSink +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerView.kt new file mode 100644 index 0000000000..d6139f5aba --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerView.kt @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.pinned.banner + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.compound.theme.ElementTheme +import io.element.android.compound.tokens.generated.CompoundIcons +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.designsystem.theme.pinnedMessageBannerBorder +import io.element.android.libraries.designsystem.theme.pinnedMessageBannerIndicator +import io.element.android.libraries.designsystem.utils.annotatedTextWithBold +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun PinnedMessagesBannerView( + state: PinnedMessagesBannerState, + modifier: Modifier = Modifier, +) { + val borderColor = ElementTheme.colors.pinnedMessageBannerBorder + Row( + modifier = modifier + .background(color = ElementTheme.colors.bgCanvasDefault) + .fillMaxWidth() + .drawBehind { + val strokeWidth = 0.5.dp.toPx() + val y = size.height - strokeWidth / 2 + drawLine( + borderColor, + Offset(0f, y), + Offset(size.width, y), + strokeWidth + ) + drawLine( + borderColor, + Offset(0f, 0f), + Offset(size.width, 0f), + strokeWidth + ) + } + .shadow(elevation = 5.dp, spotColor = Color.Transparent) + .heightIn(min = 64.dp) + .clickable { + state.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned) + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = spacedBy(10.dp) + ) { + Spacer(modifier = Modifier.width(16.dp)) + PinIndicators( + pinIndex = state.currentPinnedMessageIndex, + pinsCount = state.pinnedMessagesCount, + modifier = Modifier.heightIn(max = 40.dp) + ) + Icon( + imageVector = CompoundIcons.PinSolid(), + contentDescription = null, + tint = ElementTheme.materialColors.secondary, + modifier = Modifier.size(20.dp) + ) + PinnedMessageItem( + index = state.currentPinnedMessageIndex, + totalCount = state.pinnedMessagesCount, + message = "This is a pinned message", + modifier = Modifier.weight(1f) + ) + TextButton(text = stringResource(id = CommonStrings.screen_room_pinned_banner_view_all_button_title), onClick = { /*TODO*/ }) + } +} + +@Composable +private fun PinIndicators( + pinIndex: Int, + pinsCount: Int, + modifier: Modifier = Modifier, +) { + val indicatorHeight by remember { + derivedStateOf { + when (pinsCount) { + 0 -> 0 + 1 -> 32 + 2 -> 18 + else -> 11 + } + } + } + val lazyListState = rememberLazyListState() + LaunchedEffect(pinIndex) { + val viewportSize = lazyListState.layoutInfo.viewportSize + lazyListState.animateScrollToItem( + pinIndex, + indicatorHeight / 2 - viewportSize.height / 2 + ) + } + LazyColumn( + modifier = modifier, + state = lazyListState, + verticalArrangement = spacedBy(2.dp), + userScrollEnabled = false + ) { + items(pinsCount) { index -> + Box( + modifier = Modifier + .width(2.dp) + .height(indicatorHeight.dp) + .background( + color = if (index == pinIndex) { + ElementTheme.colors.iconAccentPrimary + } else { + ElementTheme.colors.pinnedMessageBannerIndicator + } + ) + ) + } + } +} + +@Composable +private fun PinnedMessageItem( + index: Int, + totalCount: Int, + message: String, + modifier: Modifier = Modifier, +) { + val countMessage = stringResource(id = CommonStrings.screen_room_pinned_banner_indicator, index + 1, totalCount) + val fullCountMessage = stringResource(id = CommonStrings.screen_room_pinned_banner_indicator_description, countMessage) + Column(modifier = modifier) { + if (totalCount > 1) { + Text( + text = annotatedTextWithBold( + text = fullCountMessage, + boldText = countMessage, + ), + style = ElementTheme.typography.fontBodySmMedium, + color = ElementTheme.colors.textActionAccent, + maxLines = 1, + ) + } + Text( + text = message, + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textPrimary, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + } +} + +@PreviewsDayNight +@Composable +internal fun PinnedMessagesBannerViewPreview(@PreviewParameter(PinnedMessagesBannerStateProvider::class) state: PinnedMessagesBannerState) = ElementPreview { + PinnedMessagesBannerView( + state = state, + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index eff2019a11..b640d9d450 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -90,6 +90,7 @@ fun TimelineView( onReadReceiptClick: (TimelineItem.Event) -> Unit, onJoinCallClick: () -> Unit, modifier: Modifier = Modifier, + lazyListState: LazyListState = rememberLazyListState(), forceJumpToBottomVisibility: Boolean = false ) { fun clearFocusRequestState() { @@ -109,7 +110,6 @@ fun TimelineView( } val context = LocalContext.current - val lazyListState = rememberLazyListState() // Disable reverse layout when TalkBack is enabled to avoid incorrect ordering issues seen in the current Compose UI version val useReverseLayout = remember { val accessibilityManager = context.getSystemService(AccessibilityManager::class.java) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt index db85546e42..d04c0974e0 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesPresenterTest.kt @@ -30,6 +30,7 @@ import io.element.android.features.messages.impl.fixtures.aMessageEvent import io.element.android.features.messages.impl.fixtures.aTimelineItemsFactory import io.element.android.features.messages.impl.messagecomposer.DefaultMessageComposerContext import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter +import io.element.android.features.messages.impl.pinned.banner.aPinnedMessagesBannerState import io.element.android.features.messages.impl.textcomposer.TestRichTextEditorStateFactory import io.element.android.features.messages.impl.timeline.TimelineController import io.element.android.features.messages.impl.timeline.TimelineItemIndexer @@ -158,7 +159,7 @@ class MessagesPresenterTest { canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, canUserPinUnpinResult = { Result.success(true) }, - ) + ) assertThat(room.markAsReadCalls).isEmpty() val presenter = createMessagesPresenter(matrixRoom = room) moleculeFlow(RecompositionMode.Immediate) { @@ -179,7 +180,7 @@ class MessagesPresenterTest { canRedactOtherResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, canUserPinUnpinResult = { Result.success(true) }, - ).apply { + ).apply { givenRoomInfo(aRoomInfo(hasRoomCall = true)) } val presenter = createMessagesPresenter(matrixRoom = room) @@ -208,7 +209,7 @@ class MessagesPresenterTest { canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, canUserPinUnpinResult = { Result.success(true) }, - ) + ) val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -246,7 +247,7 @@ class MessagesPresenterTest { canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, canUserPinUnpinResult = { Result.success(true) }, - ) + ) val presenter = createMessagesPresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -305,7 +306,7 @@ class MessagesPresenterTest { canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, canUserPinUnpinResult = { Result.success(true) }, - ) + ) val presenter = createMessagesPresenter( clipboardHelper = clipboardHelper, matrixRoom = matrixRoom, @@ -495,7 +496,7 @@ class MessagesPresenterTest { canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, canUserPinUnpinResult = { Result.success(true) }, - ) + ) val redactEventLambda = lambdaRecorder { _: EventId?, _: TransactionId?, _: String? -> Result.success(true) } liveTimeline.redactEventLambda = redactEventLambda @@ -570,7 +571,7 @@ class MessagesPresenterTest { canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, canUserPinUnpinResult = { Result.success(true) }, - ) + ) val presenter = createMessagesPresenter(matrixRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -606,7 +607,7 @@ class MessagesPresenterTest { canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, canUserPinUnpinResult = { Result.success(true) }, - ) + ) val presenter = createMessagesPresenter(matrixRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -631,7 +632,7 @@ class MessagesPresenterTest { canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, canUserPinUnpinResult = { Result.success(true) }, - ) + ) val presenter = createMessagesPresenter(matrixRoom = room) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -656,7 +657,7 @@ class MessagesPresenterTest { canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, canUserPinUnpinResult = { Result.success(true) }, - ) + ) room.givenRoomMembersState( MatrixRoomMembersState.Ready( persistentListOf( @@ -692,7 +693,7 @@ class MessagesPresenterTest { canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, canUserPinUnpinResult = { Result.success(true) }, - ) + ) room.givenRoomMembersState( MatrixRoomMembersState.Error( failure = Throwable(), @@ -729,7 +730,7 @@ class MessagesPresenterTest { canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, canUserPinUnpinResult = { Result.success(true) }, - ) + ) room.givenRoomMembersState(MatrixRoomMembersState.Unknown) val presenter = createMessagesPresenter(matrixRoom = room) moleculeFlow(RecompositionMode.Immediate) { @@ -756,7 +757,7 @@ class MessagesPresenterTest { canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, canUserPinUnpinResult = { Result.success(true) }, - ) + ) room.givenRoomMembersState( MatrixRoomMembersState.Ready( persistentListOf( @@ -797,7 +798,7 @@ class MessagesPresenterTest { canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, canUserPinUnpinResult = { Result.success(true) }, - ) + ) val presenter = createMessagesPresenter(matrixRoom = matrixRoom) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -822,7 +823,7 @@ class MessagesPresenterTest { canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, canUserPinUnpinResult = { Result.success(true) }, - ) + ) val presenter = createMessagesPresenter(matrixRoom = matrixRoom) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -844,7 +845,7 @@ class MessagesPresenterTest { canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, canUserPinUnpinResult = { Result.success(true) }, - ) + ) val presenter = createMessagesPresenter(matrixRoom = matrixRoom) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -865,7 +866,7 @@ class MessagesPresenterTest { canUserJoinCallResult = { Result.success(true) }, typingNoticeResult = { Result.success(Unit) }, canUserPinUnpinResult = { Result.success(true) }, - ) + ) val presenter = createMessagesPresenter(matrixRoom = matrixRoom) moleculeFlow(RecompositionMode.Immediate) { presenter.present() @@ -1060,6 +1061,7 @@ class MessagesPresenterTest { val readReceiptBottomSheetPresenter = ReadReceiptBottomSheetPresenter() val customReactionPresenter = CustomReactionPresenter(emojibaseProvider = FakeEmojibaseProvider()) val reactionSummaryPresenter = ReactionSummaryPresenter(room = matrixRoom) + return MessagesPresenter( room = matrixRoom, composerPresenter = messageComposerPresenter, @@ -1070,6 +1072,7 @@ class MessagesPresenterTest { customReactionPresenter = customReactionPresenter, reactionSummaryPresenter = reactionSummaryPresenter, readReceiptBottomSheetPresenter = readReceiptBottomSheetPresenter, + pinnedMessagesBannerPresenter = { aPinnedMessagesBannerState() }, networkMonitor = FakeNetworkMonitor(), snackbarDispatcher = SnackbarDispatcher(), navigator = navigator, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt new file mode 100644 index 0000000000..6cf1b8beb0 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenterTest.kt @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.messages.impl.pinned.banner + +import com.google.common.truth.Truth.assertThat +import io.element.android.tests.testutils.test +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class PinnedMessagesBannerPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = createPinnedMessagesBannerPresenter() + presenter.test { + val initialState = awaitItem() + assertThat(initialState.pinnedMessagesCount).isEqualTo(0) + assertThat(initialState.currentPinnedMessageIndex).isEqualTo(0) + } + } + + @Test + fun `present - move to next pinned message when there is no pinned events`() = runTest { + val presenter = createPinnedMessagesBannerPresenter() + presenter.test { + val initialState = awaitItem() + initialState.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned) + // Nothing is emitted + ensureAllEventsConsumed() + } + } + + private fun createPinnedMessagesBannerPresenter(): PinnedMessagesBannerPresenter { + return PinnedMessagesBannerPresenter() + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt index 5fc6fd2a23..4603541055 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ColorAliases.kt @@ -179,6 +179,14 @@ val SemanticColors.badgeNegativeBackgroundColor val SemanticColors.badgeNegativeContentColor get() = if (isLight) LightColorTokens.colorRed1100 else DarkColorTokens.colorRed1100 +@OptIn(CoreColorToken::class) +val SemanticColors.pinnedMessageBannerIndicator + get() = if (isLight) LightColorTokens.colorAlphaGray600 else DarkColorTokens.colorAlphaGray600 + +@OptIn(CoreColorToken::class) +val SemanticColors.pinnedMessageBannerBorder + get() = if (isLight) LightColorTokens.colorAlphaGray400 else DarkColorTokens.colorAlphaGray400 + @PreviewsDayNight @Composable internal fun ColorAliasesPreview() = ElementPreview { diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/LazyListState.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/LazyListState.kt new file mode 100644 index 0000000000..7e1eb53461 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/LazyListState.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.utils + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue + +/** + * Returns whether the lazy list is currently scrolling up. + */ +@Composable +fun LazyListState.isScrollingUp(): Boolean { + var previousIndex by remember(this) { mutableIntStateOf(firstVisibleItemIndex) } + var previousScrollOffset by remember(this) { mutableIntStateOf(firstVisibleItemScrollOffset) } + return remember(this) { + derivedStateOf { + if (previousIndex != firstVisibleItemIndex) { + previousIndex > firstVisibleItemIndex + } else { + previousScrollOffset >= firstVisibleItemScrollOffset + }.also { + previousIndex = firstVisibleItemIndex + previousScrollOffset = firstVisibleItemScrollOffset + } + } + }.value +} diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_0_en.png new file mode 100644 index 0000000000..558c81c8b7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7114202a1de9860547c525c0dadc110ce9e2e198465218ac2c33cf65f2f0eaa2 +size 9496 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_1_en.png new file mode 100644 index 0000000000..3d77277fec --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e6b5fd9ecc2b01cc8a83f3fe8e34352de1792a82db85c396377a18246adad1a +size 12953 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_2_en.png new file mode 100644 index 0000000000..cfd32bb09c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14ca5901134299e801e204e280d731e7de4072f1d522b076eb41c5f806897ed2 +size 12905 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_3_en.png new file mode 100644 index 0000000000..40ed099f5b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb206284c642dd665290d5d553491e622b8e15a64df7bb2dbd91ea5d3a13e19a +size 13041 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_4_en.png new file mode 100644 index 0000000000..15a58d4763 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1940c4ee1e07c6a0198682460af1a6558fcaf14cb69ff061831cb591eb7aec3 +size 13066 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_5_en.png new file mode 100644 index 0000000000..ac3fb40ad1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fee5c1dbcdf7929f4762b2915584fe45b7f39916a949663f03e8d7e85e991b4b +size 12988 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_0_en.png new file mode 100644 index 0000000000..625e940b9e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_0_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16658495b889654f152ba80a52111164f9682009b96abf1f3f20e660bd7c2407 +size 9297 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_1_en.png new file mode 100644 index 0000000000..3de9e45d12 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_1_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43651c4a7f10406f1a6a4b995467ba292d4a43bc198dcf352ae2e66694154de2 +size 12340 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_2_en.png new file mode 100644 index 0000000000..b320ff09e6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_2_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cca8ebb1ec12497de7e2efc1725a2e4427eecd1d340ae8176d10f914def0af25 +size 12297 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_3_en.png new file mode 100644 index 0000000000..8299ed3a1c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37bf00fbf548b7ba3d601af1ca489e07e25059c2f5a68abf9b85f4c656cf482c +size 12425 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_4_en.png new file mode 100644 index 0000000000..533f421f11 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed813a7003eb01a06667b10191990ed5bb3f75ee6a447cc4d52510b7e13b3724 +size 12448 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_5_en.png new file mode 100644 index 0000000000..a3b1d03bce --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6fa556ff7f6757c69c24e47520e08ccfd1b009d8e49a704c36d7fc4ca4186cbf +size 12378 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_13_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_13_en.png new file mode 100644 index 0000000000..c1200f26b4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_13_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:35761e520dcd7ad01d3913a98479314a1df645bdf141214e3a180fe150d2e8fd +size 59959 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_13_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_13_en.png new file mode 100644 index 0000000000..704ea5bb60 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_13_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acd2cf24e0a894936d6ed0896fb4ebcdba3529437e595ca54aa13a6f8ee79a5f +size 59327