diff --git a/features/messages/impl/build.gradle.kts b/features/messages/impl/build.gradle.kts index 781a42b2fb..c1d3067510 100644 --- a/features/messages/impl/build.gradle.kts +++ b/features/messages/impl/build.gradle.kts @@ -102,5 +102,6 @@ dependencies { testImplementation(projects.features.poll.test) testImplementation(projects.features.poll.impl) testImplementation(libs.androidx.compose.ui.test.junit) + testImplementation(projects.libraries.eventformatter.test) testReleaseImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index 4fd59be6cb..f022fd0caf 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -81,6 +81,7 @@ import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize +import timber.log.Timber @ContributesNode(RoomScope::class) class MessagesFlowNode @AssistedInject constructor( @@ -217,6 +218,10 @@ class MessagesFlowNode @AssistedInject constructor( analyticsService.captureInteraction(Interaction.Name.MobileRoomCallButton) elementCallEntryPoint.startCall(callType) } + + override fun onViewAllPinnedEvents() { + Timber.d("On View All Pinned Events not implemented yet.") + } } val inputs = MessagesNode.Inputs( focusedEventId = inputs.focusedEventId, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index b2ee1053a6..d722a5b7a0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -97,6 +97,7 @@ class MessagesNode @AssistedInject constructor( fun onCreatePollClick() fun onEditPollClick(eventId: EventId) fun onJoinCallClick(roomId: RoomId) + fun onViewAllPinnedEvents() } override fun onBuilt() { @@ -185,6 +186,10 @@ class MessagesNode @AssistedInject constructor( callbacks.forEach { it.onEditPollClick(eventId) } } + private fun onViewAllPinnedMessagesClick() { + callbacks.forEach { it.onViewAllPinnedEvents() } + } + private fun onSendLocationClick() { callbacks.forEach { it.onSendLocationClick() } } @@ -221,6 +226,7 @@ class MessagesNode @AssistedInject constructor( onSendLocationClick = this::onSendLocationClick, onCreatePollClick = this::onCreatePollClick, onJoinCallClick = this::onJoinCallClick, + onViewAllPinnedMessagesClick = this::onViewAllPinnedMessagesClick, modifier = modifier, ) 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 a1b7bc6ad1..97436c2bc0 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 @@ -23,7 +23,7 @@ import io.element.android.features.messages.impl.messagecomposer.AttachmentsStat 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.pinned.banner.aLoadedPinnedMessagesBannerState 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 @@ -90,8 +90,8 @@ open class MessagesStateProvider : PreviewParameterProvider { callState = RoomCallState.DISABLED, ), aMessagesState( - pinnedMessagesBannerState = aPinnedMessagesBannerState( - pinnedMessagesCount = 4, + pinnedMessagesBannerState = aLoadedPinnedMessagesBannerState( + knownPinnedMessagesCount = 4, currentPinnedMessageIndex = 0, ), ), @@ -121,7 +121,7 @@ fun aMessagesState( showReinvitePrompt: Boolean = false, enableVoiceMessages: Boolean = true, callState: RoomCallState = RoomCallState.ENABLED, - pinnedMessagesBannerState: PinnedMessagesBannerState = aPinnedMessagesBannerState(), + pinnedMessagesBannerState: PinnedMessagesBannerState = aLoadedPinnedMessagesBannerState(), eventSink: (MessagesEvents) -> Unit = {}, ) = MessagesState( roomId = RoomId("!id:domain"), 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 3f606f9fb9..df6aa26778 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 @@ -36,7 +36,6 @@ 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 @@ -72,7 +71,11 @@ 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.PinnedMessagesBannerState import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerView +import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerViewDefaults +import io.element.android.features.messages.impl.timeline.FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS +import io.element.android.features.messages.impl.timeline.TimelineEvents 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 @@ -105,14 +108,15 @@ 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.EventId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.ui.strings.CommonStrings import kotlinx.collections.immutable.ImmutableList import timber.log.Timber import kotlin.random.Random +import kotlin.time.Duration.Companion.milliseconds @Composable fun MessagesView( @@ -126,8 +130,9 @@ fun MessagesView( onSendLocationClick: () -> Unit, onCreatePollClick: () -> Unit, onJoinCallClick: () -> Unit, + onViewAllPinnedMessagesClick: () -> Unit, modifier: Modifier = Modifier, - forceJumpToBottomVisibility: Boolean = false + forceJumpToBottomVisibility: Boolean = false, ) { OnLifecycleEvent { _, event -> state.voiceMessageComposerState.eventSink(VoiceMessageComposerEvents.LifecycleEvent(event)) @@ -228,6 +233,7 @@ fun MessagesView( }, forceJumpToBottomVisibility = forceJumpToBottomVisibility, onJoinCallClick = onJoinCallClick, + onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick, ) }, snackbarHost = { @@ -319,6 +325,7 @@ private fun MessagesViewContent( onSendLocationClick: () -> Unit, onCreatePollClick: () -> Unit, onJoinCallClick: () -> Unit, + onViewAllPinnedMessagesClick: () -> Unit, forceJumpToBottomVisibility: Boolean, modifier: Modifier = Modifier, onSwipeToReply: (TimelineItem.Event) -> Unit, @@ -377,7 +384,7 @@ private fun MessagesViewContent( }, content = { paddingValues -> Box(modifier = Modifier.padding(paddingValues)) { - val timelineLazyListState = rememberLazyListState() + val scrollBehavior = PinnedMessagesBannerViewDefaults.rememberExitOnScrollBehavior() TimelineView( state = state.timelineState, typingNotificationState = state.typingNotificationState, @@ -392,15 +399,22 @@ private fun MessagesViewContent( onReadReceiptClick = onReadReceiptClick, forceJumpToBottomVisibility = forceJumpToBottomVisibility, onJoinCallClick = onJoinCallClick, - lazyListState = timelineLazyListState, + nestedScrollConnection = scrollBehavior.nestedScrollConnection, ) AnimatedVisibility( - visible = state.pinnedMessagesBannerState.displayBanner && timelineLazyListState.isScrollingUp(), + visible = state.pinnedMessagesBannerState is PinnedMessagesBannerState.Visible && scrollBehavior.isVisible, enter = expandVertically(), exit = shrinkVertically(), ) { + fun focusOnPinnedEvent(eventId: EventId) { + state.timelineState.eventSink( + TimelineEvents.FocusOnEvent(eventId = eventId, debounce = FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS.milliseconds) + ) + } PinnedMessagesBannerView( state = state.pinnedMessagesBannerState, + onClick = ::focusOnPinnedEvent, + onViewAllClick = onViewAllPinnedMessagesClick, ) } } @@ -572,12 +586,13 @@ internal fun MessagesViewPreview(@PreviewParameter(MessagesStateProvider::class) onBackClick = {}, onRoomDetailsClick = {}, onEventClick = { false }, - onPreviewAttachments = {}, onUserDataClick = {}, onLinkClick = {}, + onPreviewAttachments = {}, onSendLocationClick = {}, onCreatePollClick = {}, onJoinCallClick = {}, + onViewAllPinnedMessagesClick = { }, forceJumpToBottomVisibility = true, ) } 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 index 77fafd0887..dba98edf3d 100644 --- 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 @@ -22,9 +22,9 @@ 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 +import io.element.android.libraries.di.RoomScope -@ContributesTo(SessionScope::class) +@ContributesTo(RoomScope::class) @Module interface MessagesModule { @Binds diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/IsPinnedMessagesFeatureEnabled.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/IsPinnedMessagesFeatureEnabled.kt new file mode 100644 index 0000000000..5ef5e2c793 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/IsPinnedMessagesFeatureEnabled.kt @@ -0,0 +1,54 @@ +/* + * 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 + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +fun interface IsPinnedMessagesFeatureEnabled { + @Composable + operator fun invoke(): Boolean +} + +@ContributesBinding(AppScope::class) +class DefaultIsPinnedMessagesFeatureEnabled @Inject constructor( + private val featureFlagService: FeatureFlagService, +) : IsPinnedMessagesFeatureEnabled { + @Composable + override operator fun invoke(): Boolean { + var isFeatureEnabled by rememberSaveable { + mutableStateOf(false) + } + LaunchedEffect(Unit) { + featureFlagService.isFeatureEnabledFlow(FeatureFlags.PinnedEvents) + .onEach { isFeatureEnabled = it } + .launchIn(this) + } + return isFeatureEnabled + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerItem.kt new file mode 100644 index 0000000000..19dce8a4a0 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerItem.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 + +import androidx.compose.ui.text.AnnotatedString +import io.element.android.libraries.matrix.api.core.EventId + +data class PinnedMessagesBannerItem( + val eventId: EventId, + val formatted: AnnotatedString, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerItemFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerItemFactory.kt new file mode 100644 index 0000000000..95c13e1f64 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerItemFactory.kt @@ -0,0 +1,47 @@ +/* + * 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.text.AnnotatedString +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.eventformatter.api.PinnedMessagesBannerFormatter +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class PinnedMessagesBannerItemFactory @Inject constructor( + private val coroutineDispatchers: CoroutineDispatchers, + private val formatter: PinnedMessagesBannerFormatter, +) { + suspend fun create(timelineItem: MatrixTimelineItem): PinnedMessagesBannerItem? = withContext(coroutineDispatchers.computation) { + when (timelineItem) { + is MatrixTimelineItem.Event -> { + val eventId = timelineItem.eventId ?: return@withContext null + val formatted = formatter.format(timelineItem.event) + PinnedMessagesBannerItem( + eventId = eventId, + formatted = if (formatted is AnnotatedString) { + formatted + } else { + AnnotatedString(formatted.toString()) + }, + ) + } + else -> null + } + } +} 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 index e020581468..13c45e0092 100644 --- 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 @@ -17,40 +17,141 @@ package io.element.android.features.messages.impl.pinned.banner import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import io.element.android.features.messages.impl.pinned.IsPinnedMessagesFeatureEnabled +import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.room.MatrixRoom +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onCompletion +import kotlinx.coroutines.flow.onEach import javax.inject.Inject +import kotlin.time.Duration.Companion.milliseconds + +class PinnedMessagesBannerPresenter @Inject constructor( + private val room: MatrixRoom, + private val itemFactory: PinnedMessagesBannerItemFactory, + private val isFeatureEnabled: IsPinnedMessagesFeatureEnabled, + private val networkMonitor: NetworkMonitor, +) : Presenter { + private val pinnedItems = mutableStateOf>(persistentListOf()) -class PinnedMessagesBannerPresenter @Inject constructor() : Presenter { @Composable override fun present(): PinnedMessagesBannerState { - var pinnedMessageCount by remember { - mutableIntStateOf(0) - } - var currentPinnedMessageIndex by rememberSaveable { - mutableIntStateOf(0) - } + val isFeatureEnabled = isFeatureEnabled() + val expectedPinnedMessagesCount by remember { + room.roomInfoFlow.map { roomInfo -> roomInfo.pinnedEventIds.size } + }.collectAsState(initial = 0) + + var hasTimelineFailedToLoad by rememberSaveable { mutableStateOf(false) } + var currentPinnedMessageIndex by rememberSaveable { mutableIntStateOf(-1) } + + PinnedMessagesBannerItemsEffect( + isFeatureEnabled = isFeatureEnabled, + onItemsChange = { newItems -> + val pinnedMessageCount = newItems.size + if (currentPinnedMessageIndex >= pinnedMessageCount || currentPinnedMessageIndex < 0) { + currentPinnedMessageIndex = pinnedMessageCount - 1 + } + pinnedItems.value = newItems + }, + onTimelineFail = { hasTimelineFailed -> + hasTimelineFailedToLoad = hasTimelineFailed + } + ) fun handleEvent(event: PinnedMessagesBannerEvents) { when (event) { is PinnedMessagesBannerEvents.MoveToNextPinned -> { - if (currentPinnedMessageIndex < pinnedMessageCount - 1) { - currentPinnedMessageIndex++ - } else { - currentPinnedMessageIndex = 0 - } + currentPinnedMessageIndex = (currentPinnedMessageIndex - 1).mod(pinnedItems.value.size) } } } - return PinnedMessagesBannerState( - pinnedMessagesCount = pinnedMessageCount, + return pinnedMessagesBannerState( + isFeatureEnabled = isFeatureEnabled, + hasTimelineFailed = hasTimelineFailedToLoad, + expectedPinnedMessagesCount = expectedPinnedMessagesCount, + pinnedItems = pinnedItems.value, currentPinnedMessageIndex = currentPinnedMessageIndex, eventSink = ::handleEvent ) } + + @Composable + private fun pinnedMessagesBannerState( + isFeatureEnabled: Boolean, + hasTimelineFailed: Boolean, + expectedPinnedMessagesCount: Int, + pinnedItems: ImmutableList, + currentPinnedMessageIndex: Int, + eventSink: (PinnedMessagesBannerEvents) -> Unit + ): PinnedMessagesBannerState { + val currentPinnedMessage = pinnedItems.getOrNull(currentPinnedMessageIndex) + return when { + !isFeatureEnabled -> PinnedMessagesBannerState.Hidden + hasTimelineFailed -> PinnedMessagesBannerState.Hidden + currentPinnedMessage != null -> PinnedMessagesBannerState.Loaded( + currentPinnedMessage = currentPinnedMessage, + currentPinnedMessageIndex = currentPinnedMessageIndex, + loadedPinnedMessagesCount = pinnedItems.size, + eventSink = eventSink + ) + expectedPinnedMessagesCount == 0 -> PinnedMessagesBannerState.Hidden + else -> PinnedMessagesBannerState.Loading(expectedPinnedMessagesCount = expectedPinnedMessagesCount) + } + } + + @OptIn(FlowPreview::class) + @Composable + private fun PinnedMessagesBannerItemsEffect( + isFeatureEnabled: Boolean, + onItemsChange: (ImmutableList) -> Unit, + onTimelineFail: (Boolean) -> Unit, + ) { + val updatedOnItemsChange by rememberUpdatedState(onItemsChange) + val updatedOnTimelineFail by rememberUpdatedState(onTimelineFail) + val networkStatus by networkMonitor.connectivity.collectAsState() + + LaunchedEffect(isFeatureEnabled, networkStatus) { + if (!isFeatureEnabled) { + updatedOnItemsChange(persistentListOf()) + return@LaunchedEffect + } + val pinnedEventsTimeline = room.pinnedEventsTimeline() + .onFailure { updatedOnTimelineFail(true) } + .onSuccess { updatedOnTimelineFail(false) } + .getOrNull() + ?: return@LaunchedEffect + + pinnedEventsTimeline.timelineItems + .debounce(300.milliseconds) + .map { timelineItems -> + timelineItems.mapNotNull { timelineItem -> + itemFactory.create(timelineItem) + }.toImmutableList() + } + .onEach { newItems -> + updatedOnItemsChange(newItems) + } + .onCompletion { + pinnedEventsTimeline.close() + } + .launchIn(this) + } + } } 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 index 6c285d3f68..a686ada33c 100644 --- 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 @@ -16,10 +16,41 @@ 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 +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import io.element.android.libraries.designsystem.text.toAnnotatedString +import io.element.android.libraries.ui.strings.CommonStrings + +@Immutable +sealed interface PinnedMessagesBannerState { + data object Hidden : PinnedMessagesBannerState + sealed interface Visible : PinnedMessagesBannerState + data class Loading(val expectedPinnedMessagesCount: Int) : Visible + data class Loaded( + val currentPinnedMessage: PinnedMessagesBannerItem, + val currentPinnedMessageIndex: Int, + val loadedPinnedMessagesCount: Int, + val eventSink: (PinnedMessagesBannerEvents) -> Unit + ) : Visible + + fun pinnedMessagesCount() = when (this) { + is Hidden -> 0 + is Loading -> expectedPinnedMessagesCount + is Loaded -> loadedPinnedMessagesCount + } + + fun currentPinnedMessageIndex() = when (this) { + is Hidden -> 0 + is Loading -> expectedPinnedMessagesCount - 1 + is Loaded -> currentPinnedMessageIndex + } + + @Composable + fun formattedMessage() = when (this) { + is Hidden -> AnnotatedString("") + is Loading -> stringResource(id = CommonStrings.screen_room_pinned_banner_loading_description).toAnnotatedString() + is Loaded -> currentPinnedMessage.formatted + } } 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 index 54fa0f12c0..e9d0a27c16 100644 --- 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 @@ -16,26 +16,46 @@ package io.element.android.features.messages.impl.pinned.banner +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.matrix.api.core.EventId +import kotlin.random.Random 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), + aHiddenPinnedMessagesBannerState(), + aLoadingPinnedMessagesBannerState(knownPinnedMessagesCount = 1), + aLoadingPinnedMessagesBannerState(knownPinnedMessagesCount = 4), + aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 1, currentPinnedMessageIndex = 0), + aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 2, currentPinnedMessageIndex = 0), + aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 3, currentPinnedMessageIndex = 0), + aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 4, currentPinnedMessageIndex = 0), + aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 4, currentPinnedMessageIndex = 1), + aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 4, currentPinnedMessageIndex = 2), + aLoadedPinnedMessagesBannerState(knownPinnedMessagesCount = 4, currentPinnedMessageIndex = 3), ) } -internal fun aPinnedMessagesBannerState( - pinnedMessagesCount: Int = 0, - currentPinnedMessageIndex: Int = -1, +internal fun aHiddenPinnedMessagesBannerState() = PinnedMessagesBannerState.Hidden + +internal fun aLoadingPinnedMessagesBannerState( + knownPinnedMessagesCount: Int = 4 +) = PinnedMessagesBannerState.Loading( + expectedPinnedMessagesCount = knownPinnedMessagesCount +) + +internal fun aLoadedPinnedMessagesBannerState( + currentPinnedMessageIndex: Int = 0, + knownPinnedMessagesCount: Int = 1, + currentPinnedMessage: PinnedMessagesBannerItem = PinnedMessagesBannerItem( + eventId = EventId("\$" + Random.nextInt().toString()), + formatted = AnnotatedString("This is a pinned message") + ), eventSink: (PinnedMessagesBannerEvents) -> Unit = {} -) = PinnedMessagesBannerState( - pinnedMessagesCount = pinnedMessagesCount, +) = PinnedMessagesBannerState.Loaded( + currentPinnedMessage = currentPinnedMessage, currentPinnedMessageIndex = currentPinnedMessageIndex, + loadedPinnedMessagesCount = knownPinnedMessagesCount, 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 index d6139f5aba..e925220796 100644 --- 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 @@ -16,6 +16,7 @@ package io.element.android.features.messages.impl.pinned.banner +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement.spacedBy @@ -32,16 +33,21 @@ 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.Stable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.draw.shadow import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -55,46 +61,57 @@ 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.matrix.api.core.EventId import io.element.android.libraries.ui.strings.CommonStrings @Composable fun PinnedMessagesBannerView( state: PinnedMessagesBannerState, + onClick: (EventId) -> Unit, + onViewAllClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Box(modifier = modifier) { + when (state) { + PinnedMessagesBannerState.Hidden -> Unit + is PinnedMessagesBannerState.Visible -> { + PinnedMessagesBannerRow( + state = state, + onClick = onClick, + onViewAllClick = onViewAllClick, + ) + } + } + } +} + +@Composable +private fun PinnedMessagesBannerRow( + state: PinnedMessagesBannerState, + onClick: (EventId) -> Unit, + onViewAllClick: () -> Unit, 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 { + .background(color = ElementTheme.colors.bgCanvasDefault) + .fillMaxWidth() + .drawBorder(borderColor) + .heightIn(min = 64.dp) + .clickable { + if (state is PinnedMessagesBannerState.Loaded) { + onClick(state.currentPinnedMessage.eventId) state.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned) - }, + } + }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = spacedBy(10.dp) ) { Spacer(modifier = Modifier.width(16.dp)) PinIndicators( - pinIndex = state.currentPinnedMessageIndex, - pinsCount = state.pinnedMessagesCount, + pinIndex = state.currentPinnedMessageIndex(), + pinsCount = state.pinnedMessagesCount(), modifier = Modifier.heightIn(max = 40.dp) ) Icon( @@ -104,29 +121,68 @@ fun PinnedMessagesBannerView( modifier = Modifier.size(20.dp) ) PinnedMessageItem( - index = state.currentPinnedMessageIndex, - totalCount = state.pinnedMessagesCount, - message = "This is a pinned message", + index = state.currentPinnedMessageIndex(), + totalCount = state.pinnedMessagesCount(), + message = state.formattedMessage(), modifier = Modifier.weight(1f) ) - TextButton(text = stringResource(id = CommonStrings.screen_room_pinned_banner_view_all_button_title), onClick = { /*TODO*/ }) + ViewAllButton(state, onViewAllClick) } } +@Composable +private fun ViewAllButton( + state: PinnedMessagesBannerState, + onViewAllClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Box(modifier = modifier) { + val text = if (state is PinnedMessagesBannerState.Loaded) { + stringResource(id = CommonStrings.screen_room_pinned_banner_view_all_button_title) + } else { + "" + } + TextButton( + text = text, + showProgress = state is PinnedMessagesBannerState.Loading, + onClick = onViewAllClick + ) + } +} + +private fun Modifier.drawBorder(borderColor: Color): Modifier { + return this + .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) +} + @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 indicatorHeight = remember(pinsCount) { + when (pinsCount) { + 0 -> 0 + 1 -> 32 + 2 -> 18 + else -> 11 } } val lazyListState = rememberLazyListState() @@ -141,20 +197,20 @@ private fun PinIndicators( modifier = modifier, state = lazyListState, verticalArrangement = spacedBy(2.dp), - userScrollEnabled = false + 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 - } - ) + .width(2.dp) + .height(indicatorHeight.dp) + .background( + color = if (index == pinIndex) { + ElementTheme.colors.iconAccentPrimary + } else { + ElementTheme.colors.pinnedMessageBannerIndicator + } + ) ) } } @@ -164,13 +220,13 @@ private fun PinIndicators( private fun PinnedMessageItem( index: Int, totalCount: Int, - message: String, + message: AnnotatedString?, 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) { + AnimatedVisibility(totalCount > 1) { Text( text = annotatedTextWithBold( text = fullCountMessage, @@ -179,15 +235,46 @@ private fun PinnedMessageItem( style = ElementTheme.typography.fontBodySmMedium, color = ElementTheme.colors.textActionAccent, maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } - Text( - text = message, - style = ElementTheme.typography.fontBodyMdRegular, - color = ElementTheme.colors.textPrimary, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - ) + if (message != null) { + Text( + text = message, + style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textPrimary, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + ) + } + } +} + +@Stable +internal interface PinnedMessagesBannerViewScrollBehavior { + val isVisible: Boolean + val nestedScrollConnection: NestedScrollConnection +} + +internal object PinnedMessagesBannerViewDefaults { + @Composable + fun rememberExitOnScrollBehavior(): PinnedMessagesBannerViewScrollBehavior = remember { + ExitOnScrollBehavior() + } +} + +private class ExitOnScrollBehavior : PinnedMessagesBannerViewScrollBehavior { + override var isVisible by mutableStateOf(true) + override val nestedScrollConnection: NestedScrollConnection = object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + if (available.y < -1) { + isVisible = true + } + if (available.y > 1) { + isVisible = false + } + return Offset.Zero + } } } @@ -196,5 +283,7 @@ private fun PinnedMessageItem( internal fun PinnedMessagesBannerViewPreview(@PreviewParameter(PinnedMessagesBannerStateProvider::class) state: PinnedMessagesBannerState) = ElementPreview { PinnedMessagesBannerView( state = state, + onClick = {}, + onViewAllClick = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt index 24007530e6..25ed908cd0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineEvents.kt @@ -18,10 +18,11 @@ package io.element.android.features.messages.impl.timeline import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.Timeline +import kotlin.time.Duration sealed interface TimelineEvents { data class OnScrollFinished(val firstIndex: Int) : TimelineEvents - data class FocusOnEvent(val eventId: EventId) : TimelineEvents + data class FocusOnEvent(val eventId: EventId, val debounce: Duration = Duration.ZERO) : TimelineEvents data object ClearFocusRequestState : TimelineEvents data object OnFocusEventRender : TimelineEvents data object JumpToLive : TimelineEvents diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt index f5903f036b..3f7db9607a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenter.kt @@ -50,12 +50,15 @@ import io.element.android.libraries.matrix.ui.room.canSendMessageAsState import io.element.android.libraries.preferences.api.store.SessionPreferencesStore import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +const val FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS = 200L + class TimelinePresenter @AssistedInject constructor( private val timelineItemsFactory: TimelineItemsFactory, private val timelineItemIndexer: TimelineItemIndexer, @@ -136,13 +139,8 @@ class TimelinePresenter @AssistedInject constructor( is TimelineEvents.EditPoll -> { navigator.onEditPollClick(event.pollStartId) } - is TimelineEvents.FocusOnEvent -> localScope.launch { - if (timelineItemIndexer.isKnown(event.eventId)) { - val index = timelineItemIndexer.indexOf(event.eventId) - focusRequestState.value = FocusRequestState.Success(eventId = event.eventId, index = index) - } else { - focusRequestState.value = FocusRequestState.Loading(eventId = event.eventId) - } + is TimelineEvents.FocusOnEvent -> { + focusRequestState.value = FocusRequestState.Requested(event.eventId, event.debounce) } is TimelineEvents.OnFocusEventRender -> { focusRequestState.value = focusRequestState.value.onFocusEventRender() @@ -157,18 +155,29 @@ class TimelinePresenter @AssistedInject constructor( } LaunchedEffect(focusRequestState.value) { - val currentFocusRequestState = focusRequestState.value - if (currentFocusRequestState is FocusRequestState.Loading) { - val eventId = currentFocusRequestState.eventId - timelineController.focusOnEvent(eventId) - .fold( - onSuccess = { - focusRequestState.value = FocusRequestState.Success(eventId = eventId) - }, - onFailure = { - focusRequestState.value = FocusRequestState.Failure(throwable = it) - } - ) + when (val currentFocusRequestState = focusRequestState.value) { + is FocusRequestState.Requested -> { + delay(currentFocusRequestState.debounce) + if (timelineItemIndexer.isKnown(currentFocusRequestState.eventId)) { + val index = timelineItemIndexer.indexOf(currentFocusRequestState.eventId) + focusRequestState.value = FocusRequestState.Success(eventId = currentFocusRequestState.eventId, index = index) + } else { + focusRequestState.value = FocusRequestState.Loading(eventId = currentFocusRequestState.eventId) + } + } + is FocusRequestState.Loading -> { + val eventId = currentFocusRequestState.eventId + timelineController.focusOnEvent(eventId) + .fold( + onSuccess = { + focusRequestState.value = FocusRequestState.Success(eventId = eventId) + }, + onFailure = { + focusRequestState.value = FocusRequestState.Failure(throwable = it) + } + ) + } + else -> Unit } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt index eb8622d0bc..9339fcb620 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineState.kt @@ -21,6 +21,7 @@ import io.element.android.features.messages.impl.timeline.model.NewEventState import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.matrix.api.core.EventId import kotlinx.collections.immutable.ImmutableList +import kotlin.time.Duration @Immutable data class TimelineState( @@ -39,6 +40,7 @@ data class TimelineState( @Immutable sealed interface FocusRequestState { data object None : FocusRequestState + data class Requested(val eventId: EventId, val debounce: Duration) : FocusRequestState data class Loading(val eventId: EventId) : FocusRequestState data class Success( val eventId: EventId, @@ -54,6 +56,7 @@ sealed interface FocusRequestState { fun eventId(): EventId? { return when (this) { + is Requested -> eventId is Loading -> eventId is Success -> eventId else -> null 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 b640d9d450..2b62071c19 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 @@ -48,7 +48,10 @@ import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -91,7 +94,8 @@ fun TimelineView( onJoinCallClick: () -> Unit, modifier: Modifier = Modifier, lazyListState: LazyListState = rememberLazyListState(), - forceJumpToBottomVisibility: Boolean = false + forceJumpToBottomVisibility: Boolean = false, + nestedScrollConnection: NestedScrollConnection = rememberNestedScrollInteropConnection(), ) { fun clearFocusRequestState() { state.eventSink(TimelineEvents.ClearFocusRequestState) @@ -124,7 +128,9 @@ fun TimelineView( AnimatedVisibility(visible = true, enter = fadeIn()) { Box(modifier) { LazyColumn( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .nestedScroll(nestedScrollConnection), state = lazyListState, reverseLayout = useReverseLayout, contentPadding = PaddingValues(vertical = 8.dp), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/MessagesViewWithTypingPreview.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/MessagesViewWithTypingPreview.kt index c4a8a88153..9dadb7515d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/MessagesViewWithTypingPreview.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/typing/MessagesViewWithTypingPreview.kt @@ -33,11 +33,12 @@ internal fun MessagesViewWithTypingPreview( onBackClick = {}, onRoomDetailsClick = {}, onEventClick = { false }, - onPreviewAttachments = {}, onUserDataClick = {}, onLinkClick = {}, + onPreviewAttachments = {}, onSendLocationClick = {}, onCreatePollClick = {}, onJoinCallClick = {}, + onViewAllPinnedMessagesClick = {}, ) } 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 d04c0974e0..a6c123971b 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,7 +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.pinned.banner.aLoadedPinnedMessagesBannerState 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 @@ -1072,7 +1072,7 @@ class MessagesPresenterTest { customReactionPresenter = customReactionPresenter, reactionSummaryPresenter = reactionSummaryPresenter, readReceiptBottomSheetPresenter = readReceiptBottomSheetPresenter, - pinnedMessagesBannerPresenter = { aPinnedMessagesBannerState() }, + pinnedMessagesBannerPresenter = { aLoadedPinnedMessagesBannerState() }, networkMonitor = FakeNetworkMonitor(), snackbarDispatcher = SnackbarDispatcher(), navigator = navigator, diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt index e876b5ec60..b304ad9178 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performTouchInput import androidx.compose.ui.test.swipeRight +import androidx.compose.ui.text.AnnotatedString import androidx.test.ext.junit.runners.AndroidJUnit4 import io.element.android.emojibasebindings.Emoji import io.element.android.emojibasebindings.EmojibaseCategory @@ -43,6 +44,10 @@ import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState +import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerItem +import io.element.android.features.messages.impl.pinned.banner.aLoadedPinnedMessagesBannerState +import io.element.android.features.messages.impl.timeline.FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS +import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.aTimelineItemEvent import io.element.android.features.messages.impl.timeline.aTimelineItemReadReceipts import io.element.android.features.messages.impl.timeline.aTimelineRoomInfo @@ -54,6 +59,7 @@ import io.element.android.features.messages.impl.timeline.components.receipt.aRe import io.element.android.features.messages.impl.timeline.components.receipt.bottomsheet.ReadReceiptBottomSheetEvents import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.tests.testutils.EnsureCalledOnceWithParam @@ -72,6 +78,7 @@ import org.junit.Test import org.junit.rules.TestRule import org.junit.runner.RunWith import org.robolectric.annotation.Config +import kotlin.time.Duration.Companion.milliseconds @RunWith(AndroidJUnit4::class) class MessagesViewTest { @@ -458,6 +465,25 @@ class MessagesViewTest { customReactionStateEventsRecorder.assertSingle(CustomReactionEvents.DismissCustomReactionSheet) eventsRecorder.assertSingle(MessagesEvents.ToggleReaction(aUnicode, timelineItem.eventId!!)) } + + @Test + fun `clicking on pinned messages banner emits the expected Event`() { + val eventsRecorder = EventsRecorder() + val state = aMessagesState( + timelineState = aTimelineState(eventSink = eventsRecorder), + pinnedMessagesBannerState = aLoadedPinnedMessagesBannerState( + knownPinnedMessagesCount = 2, + currentPinnedMessageIndex = 0, + currentPinnedMessage = PinnedMessagesBannerItem( + eventId = AN_EVENT_ID, + formatted = AnnotatedString("This is a pinned message") + ), + ), + ) + rule.setMessagesView(state = state) + rule.onNodeWithText("This is a pinned message").performClick() + eventsRecorder.assertSingle(TimelineEvents.FocusOnEvent(AN_EVENT_ID, debounce = FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS.milliseconds)) + } } private fun AndroidComposeTestRule.setMessagesView( @@ -471,6 +497,7 @@ private fun AndroidComposeTestRule.setMessa onSendLocationClick: () -> Unit = EnsureNeverCalled(), onCreatePollClick: () -> Unit = EnsureNeverCalled(), onJoinCallClick: () -> Unit = EnsureNeverCalled(), + onViewAllPinnedMessagesClick: () -> Unit = EnsureNeverCalled(), ) { setContent { // Cannot use the RichTextEditor, so simulate a LocalInspectionMode @@ -488,6 +515,7 @@ private fun AndroidComposeTestRule.setMessa onSendLocationClick = onSendLocationClick, onCreatePollClick = onCreatePollClick, onJoinCallClick = onJoinCallClick, + onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick, ) } } 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 index 6cf1b8beb0..1ae4729a40 100644 --- 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 @@ -17,33 +17,187 @@ package io.element.android.features.messages.impl.pinned.banner import com.google.common.truth.Truth.assertThat +import io.element.android.features.networkmonitor.api.NetworkMonitor +import io.element.android.features.networkmonitor.test.FakeNetworkMonitor +import io.element.android.libraries.eventformatter.test.FakePinnedMessagesBannerFormatter +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.AN_EVENT_ID_2 +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.aRoomInfo +import io.element.android.libraries.matrix.test.timeline.FakeTimeline +import io.element.android.libraries.matrix.test.timeline.aMessageContent +import io.element.android.libraries.matrix.test.timeline.anEventTimelineItem import io.element.android.tests.testutils.test +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test class PinnedMessagesBannerPresenterTest { @Test fun `present - initial state`() = runTest { - val presenter = createPinnedMessagesBannerPresenter() + val presenter = createPinnedMessagesBannerPresenter(isFeatureEnabled = true) presenter.test { val initialState = awaitItem() - assertThat(initialState.pinnedMessagesCount).isEqualTo(0) - assertThat(initialState.currentPinnedMessageIndex).isEqualTo(0) + assertThat(initialState).isEqualTo(PinnedMessagesBannerState.Hidden) + cancelAndIgnoreRemainingEvents() } } @Test - fun `present - move to next pinned message when there is no pinned events`() = runTest { - val presenter = createPinnedMessagesBannerPresenter() + fun `present - feature disabled`() = runTest { + val presenter = createPinnedMessagesBannerPresenter(isFeatureEnabled = false) presenter.test { val initialState = awaitItem() - initialState.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned) - // Nothing is emitted - ensureAllEventsConsumed() + assertThat(initialState).isEqualTo(PinnedMessagesBannerState.Hidden) } } - private fun createPinnedMessagesBannerPresenter(): PinnedMessagesBannerPresenter { - return PinnedMessagesBannerPresenter() + @Test + fun `present - loading state`() = runTest { + val room = FakeMatrixRoom( + pinnedEventsTimelineResult = { Result.success(FakeTimeline()) } + ).apply { + givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) + } + val presenter = createPinnedMessagesBannerPresenter(room = room) + presenter.test { + skipItems(1) + val loadingState = awaitItem() + assertThat(loadingState).isEqualTo(PinnedMessagesBannerState.Loading(1)) + assertThat(loadingState.pinnedMessagesCount()).isEqualTo(1) + assertThat(loadingState.currentPinnedMessageIndex()).isEqualTo(0) + } + } + + @Test + fun `present - loaded state`() = runTest { + val messageContent = aMessageContent("A message") + val pinnedEventsTimeline = FakeTimeline( + timelineItems = flowOf( + listOf( + MatrixTimelineItem.Event( + uniqueId = "FAKE_UNIQUE_ID", + event = anEventTimelineItem( + eventId = AN_EVENT_ID, + content = messageContent, + ), + ) + ) + ) + ) + val room = FakeMatrixRoom( + pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) } + ).apply { + givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID, AN_EVENT_ID_2))) + } + val presenter = createPinnedMessagesBannerPresenter(room = room) + presenter.test { + skipItems(2) + val loadedState = awaitItem() as PinnedMessagesBannerState.Loaded + assertThat(loadedState.currentPinnedMessageIndex).isEqualTo(0) + assertThat(loadedState.loadedPinnedMessagesCount).isEqualTo(1) + assertThat(loadedState.currentPinnedMessage.formatted.text).isEqualTo(messageContent.toString()) + } + } + + @Test + fun `present - loaded state - multiple pinned messages`() = runTest { + val messageContent1 = aMessageContent("A message") + val messageContent2 = aMessageContent("Another message") + val pinnedEventsTimeline = FakeTimeline( + timelineItems = flowOf( + listOf( + MatrixTimelineItem.Event( + uniqueId = "FAKE_UNIQUE_ID", + event = anEventTimelineItem( + eventId = AN_EVENT_ID, + content = messageContent1, + ), + ), + MatrixTimelineItem.Event( + uniqueId = "FAKE_UNIQUE_ID_2", + event = anEventTimelineItem( + eventId = AN_EVENT_ID_2, + content = messageContent2, + ), + ) + ) + ) + ) + val room = FakeMatrixRoom( + pinnedEventsTimelineResult = { Result.success(pinnedEventsTimeline) } + ).apply { + givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID, AN_EVENT_ID_2))) + } + val presenter = createPinnedMessagesBannerPresenter(room = room) + presenter.test { + skipItems(2) + awaitItem().also { loadedState -> + loadedState as PinnedMessagesBannerState.Loaded + assertThat(loadedState.currentPinnedMessageIndex).isEqualTo(1) + assertThat(loadedState.loadedPinnedMessagesCount).isEqualTo(2) + assertThat(loadedState.currentPinnedMessage.formatted.text).isEqualTo(messageContent2.toString()) + loadedState.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned) + } + + awaitItem().also { loadedState -> + loadedState as PinnedMessagesBannerState.Loaded + assertThat(loadedState.currentPinnedMessageIndex).isEqualTo(0) + assertThat(loadedState.loadedPinnedMessagesCount).isEqualTo(2) + assertThat(loadedState.currentPinnedMessage.formatted.text).isEqualTo(messageContent1.toString()) + loadedState.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned) + } + + awaitItem().also { loadedState -> + loadedState as PinnedMessagesBannerState.Loaded + assertThat(loadedState.currentPinnedMessageIndex).isEqualTo(1) + assertThat(loadedState.loadedPinnedMessagesCount).isEqualTo(2) + assertThat(loadedState.currentPinnedMessage.formatted.text).isEqualTo(messageContent2.toString()) + } + } + } + + @Test + fun `present - timeline failed`() = runTest { + val room = FakeMatrixRoom( + pinnedEventsTimelineResult = { Result.failure(Exception()) } + ).apply { + givenRoomInfo(aRoomInfo(pinnedEventIds = listOf(AN_EVENT_ID))) + } + val presenter = createPinnedMessagesBannerPresenter(room = room) + presenter.test { + skipItems(1) + awaitItem().also { loadingState -> + assertThat(loadingState).isEqualTo(PinnedMessagesBannerState.Loading(1)) + assertThat(loadingState.pinnedMessagesCount()).isEqualTo(1) + assertThat(loadingState.currentPinnedMessageIndex()).isEqualTo(0) + } + awaitItem().also { failedState -> + assertThat(failedState).isEqualTo(PinnedMessagesBannerState.Hidden) + } + } + } + + private fun TestScope.createPinnedMessagesBannerPresenter( + room: MatrixRoom = FakeMatrixRoom(), + itemFactory: PinnedMessagesBannerItemFactory = PinnedMessagesBannerItemFactory( + coroutineDispatchers = testCoroutineDispatchers(), + formatter = FakePinnedMessagesBannerFormatter( + formatLambda = { event -> "${event.content}" } + ) + ), + networkMonitor: NetworkMonitor = FakeNetworkMonitor(), + isFeatureEnabled: Boolean = true, + ): PinnedMessagesBannerPresenter { + return PinnedMessagesBannerPresenter( + room = room, + itemFactory = itemFactory, + isFeatureEnabled = { isFeatureEnabled }, + networkMonitor = networkMonitor, + ) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerViewTest.kt new file mode 100644 index 0000000000..ed23bdfb84 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerViewTest.kt @@ -0,0 +1,87 @@ +/* + * 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.activity.ComponentActivity +import androidx.compose.ui.test.junit4.AndroidComposeTestRule +import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onRoot +import androidx.compose.ui.test.performClick +import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.tests.testutils.EnsureNeverCalled +import io.element.android.tests.testutils.EnsureNeverCalledWithParam +import io.element.android.tests.testutils.EventsRecorder +import io.element.android.tests.testutils.clickOn +import io.element.android.tests.testutils.ensureCalledOnce +import io.element.android.tests.testutils.ensureCalledOnceWithParam +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TestRule +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class PinnedMessagesBannerViewTest { + @get:Rule val rule = createAndroidComposeRule() + + @Test + fun `clicking on the banner invoke expected callback`() { + val eventsRecorder = EventsRecorder() + val state = aLoadedPinnedMessagesBannerState( + eventSink = eventsRecorder + ) + val pinnedEventId = state.currentPinnedMessage.eventId + ensureCalledOnceWithParam(pinnedEventId) { callback -> + rule.setPinnedMessagesBannerView( + state = state, + onClick = callback + ) + rule.onRoot().performClick() + eventsRecorder.assertSingle(PinnedMessagesBannerEvents.MoveToNextPinned) + } + } + + @Test + fun `clicking on view all emit the expected event`() { + val eventsRecorder = EventsRecorder(expectEvents = true) + val state = aLoadedPinnedMessagesBannerState( + eventSink = eventsRecorder + ) + ensureCalledOnce { callback -> + rule.setPinnedMessagesBannerView( + state = state, + onViewAllClick = callback + ) + rule.clickOn(CommonStrings.screen_room_pinned_banner_view_all_button_title) + } + } +} + +private fun AndroidComposeTestRule.setPinnedMessagesBannerView( + state: PinnedMessagesBannerState, + onClick: (EventId) -> Unit = EnsureNeverCalledWithParam(), + onViewAllClick: () -> Unit = EnsureNeverCalled(), +) { + setContent { + PinnedMessagesBannerView( + state = state, + onClick = onClick, + onViewAllClick = onViewAllClick + ) + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt index 5a171227e7..90c71a964a 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/timeline/TimelinePresenterTest.kt @@ -75,6 +75,7 @@ import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test import java.util.Date +import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds private const val FAKE_UNIQUE_ID = "FAKE_UNIQUE_ID" @@ -496,6 +497,10 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2" }.test { val initialState = awaitFirstItem() initialState.eventSink.invoke(TimelineEvents.FocusOnEvent(AN_EVENT_ID)) + awaitItem().also { state -> + assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID) + assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Requested(AN_EVENT_ID, Duration.ZERO)) + } awaitItem().also { state -> assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID) assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Loading(AN_EVENT_ID)) @@ -541,6 +546,10 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2" }.test { val initialState = awaitFirstItem() initialState.eventSink.invoke(TimelineEvents.FocusOnEvent(AN_EVENT_ID)) + awaitItem().also { state -> + assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID) + assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Requested(AN_EVENT_ID, Duration.ZERO)) + } awaitItem().also { state -> assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID) assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Success(AN_EVENT_ID, 0)) @@ -564,6 +573,10 @@ private const val FAKE_UNIQUE_ID_2 = "FAKE_UNIQUE_ID_2" }.test { val initialState = awaitFirstItem() initialState.eventSink(TimelineEvents.FocusOnEvent(AN_EVENT_ID)) + awaitItem().also { state -> + assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID) + assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Requested(AN_EVENT_ID, Duration.ZERO)) + } awaitItem().also { state -> assertThat(state.focusedEventId).isEqualTo(AN_EVENT_ID) assertThat(state.focusRequestState).isEqualTo(FocusRequestState.Loading(AN_EVENT_ID)) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 36888ad06b..d5c36cba37 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -44,7 +44,7 @@ serialization_json = "1.6.3" showkase = "1.0.3" appyx = "1.4.0" sqldelight = "2.0.2" -wysiwyg = "2.37.8" +wysiwyg = "2.37.7" telephoto = "0.12.1" # DI @@ -187,7 +187,7 @@ play_services_oss_licenses = "com.google.android.gms:play-services-oss-licenses: # Analytics posthog = "com.posthog:posthog-android:3.4.2" -sentry = "io.sentry:sentry-android:7.13.0" +sentry = "io.sentry:sentry-android:7.12.1" # main branch can be tested replacing the version with main-SNAPSHOT matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:0.23.1" diff --git a/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/PinnedMessagesBannerFormatter.kt b/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/PinnedMessagesBannerFormatter.kt new file mode 100644 index 0000000000..9db3a19bd7 --- /dev/null +++ b/libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/PinnedMessagesBannerFormatter.kt @@ -0,0 +1,23 @@ +/* + * 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.eventformatter.api + +import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem + +interface PinnedMessagesBannerFormatter { + fun format(event: EventTimelineItem): CharSequence +} diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatter.kt new file mode 100644 index 0000000000..05403e5355 --- /dev/null +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatter.kt @@ -0,0 +1,121 @@ +/* + * 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.eventformatter.impl + +import androidx.annotation.StringRes +import androidx.compose.ui.text.AnnotatedString +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.eventformatter.api.PinnedMessagesBannerFormatter +import io.element.android.libraries.matrix.api.permalink.PermalinkParser +import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.EmoteMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem +import io.element.android.libraries.matrix.api.timeline.item.event.FileMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.LocationMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent +import io.element.android.libraries.matrix.api.timeline.item.event.MessageType +import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.OtherMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.PollContent +import io.element.android.libraries.matrix.api.timeline.item.event.RedactedContent +import io.element.android.libraries.matrix.api.timeline.item.event.StickerContent +import io.element.android.libraries.matrix.api.timeline.item.event.StickerMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.UnableToDecryptContent +import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.VoiceMessageType +import io.element.android.libraries.matrix.api.timeline.item.event.getDisambiguatedDisplayName +import io.element.android.libraries.matrix.ui.messages.toPlainText +import io.element.android.libraries.ui.strings.CommonStrings +import io.element.android.services.toolbox.api.strings.StringProvider +import javax.inject.Inject + +@ContributesBinding(SessionScope::class) +class DefaultPinnedMessagesBannerFormatter @Inject constructor( + private val sp: StringProvider, + private val permalinkParser: PermalinkParser, +) : PinnedMessagesBannerFormatter { + override fun format(event: EventTimelineItem): CharSequence { + return when (val content = event.content) { + is MessageContent -> processMessageContents(event, content) + is StickerContent -> { + content.body.prefixWith(CommonStrings.common_sticker) + } + is UnableToDecryptContent -> { + sp.getString(CommonStrings.common_waiting_for_decryption_key) + } + is PollContent -> { + content.question.prefixWith(CommonStrings.a11y_poll) + } + RedactedContent -> { + sp.getString(CommonStrings.common_message_removed) + } + else -> { + sp.getString(CommonStrings.common_unsupported_event) + } + } + } + + private fun processMessageContents( + event: EventTimelineItem, + messageContent: MessageContent, + ): CharSequence { + return when (val messageType: MessageType = messageContent.type) { + is EmoteMessageType -> { + val senderDisambiguatedDisplayName = event.senderProfile.getDisambiguatedDisplayName(event.sender) + "* $senderDisambiguatedDisplayName ${messageType.body}" + } + is TextMessageType -> { + messageType.toPlainText(permalinkParser) + } + is VideoMessageType -> { + messageType.body.prefixWith(CommonStrings.common_video) + } + is ImageMessageType -> { + messageType.body.prefixWith(CommonStrings.common_image) + } + is StickerMessageType -> { + messageType.body.prefixWith(CommonStrings.common_sticker) + } + is LocationMessageType -> { + messageType.body.prefixWith(CommonStrings.common_shared_location) + } + is FileMessageType -> { + messageType.body.prefixWith(CommonStrings.common_file) + } + is AudioMessageType -> { + messageType.body.prefixWith(CommonStrings.common_audio) + } + is VoiceMessageType -> { + messageType.body.prefixWith(CommonStrings.common_voice_message) + } + is OtherMessageType -> { + messageType.body + } + is NoticeMessageType -> { + messageType.body + } + } + } + + private fun CharSequence.prefixWith(@StringRes res: Int): AnnotatedString { + val prefix = sp.getString(res) + return prefixWith(prefix) + } +} diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt index 13655c48f2..ac125b48e6 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultRoomLastMessageFormatter.kt @@ -16,11 +16,6 @@ package io.element.android.libraries.eventformatter.impl -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.withStyle import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.SessionScope import io.element.android.libraries.eventformatter.api.RoomLastMessageFormatter @@ -79,7 +74,7 @@ class DefaultRoomLastMessageFormatter @Inject constructor( RedactedContent -> { val message = sp.getString(CommonStrings.common_message_removed) if (!isDmRoom) { - prefix(message, senderDisambiguatedDisplayName) + message.prefixWith(senderDisambiguatedDisplayName) } else { message } @@ -90,7 +85,7 @@ class DefaultRoomLastMessageFormatter @Inject constructor( is UnableToDecryptContent -> { val message = sp.getString(CommonStrings.common_waiting_for_decryption_key) if (!isDmRoom) { - prefix(message, senderDisambiguatedDisplayName) + message.prefixWith(senderDisambiguatedDisplayName) } else { message } @@ -113,7 +108,6 @@ class DefaultRoomLastMessageFormatter @Inject constructor( } is LegacyCallInviteContent -> sp.getString(CommonStrings.common_call_invite) is CallNotifyContent -> sp.getString(CommonStrings.common_call_started) - else -> null }?.take(MAX_SAFE_LENGTH) } @@ -168,16 +162,6 @@ class DefaultRoomLastMessageFormatter @Inject constructor( ): CharSequence = if (isDmRoom) { message } else { - prefix(message, senderDisambiguatedDisplayName) - } - - private fun prefix(message: String, senderDisambiguatedDisplayName: String): AnnotatedString { - return buildAnnotatedString { - withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { - append(senderDisambiguatedDisplayName) - } - append(": ") - append(message) - } + message.prefixWith(senderDisambiguatedDisplayName) } } diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/PrefixWith.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/PrefixWith.kt new file mode 100644 index 0000000000..a5991e31df --- /dev/null +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/PrefixWith.kt @@ -0,0 +1,33 @@ +/* + * 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.eventformatter.impl + +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle + +internal fun CharSequence.prefixWith(prefix: String): AnnotatedString { + return buildAnnotatedString { + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append(prefix) + } + append(": ") + append(this@prefixWith) + } +} diff --git a/libraries/eventformatter/test/src/main/kotlin/io/element/android/libraries/eventformatter/test/FakePinnedMessagesBannerFormatter.kt b/libraries/eventformatter/test/src/main/kotlin/io/element/android/libraries/eventformatter/test/FakePinnedMessagesBannerFormatter.kt new file mode 100644 index 0000000000..668803169e --- /dev/null +++ b/libraries/eventformatter/test/src/main/kotlin/io/element/android/libraries/eventformatter/test/FakePinnedMessagesBannerFormatter.kt @@ -0,0 +1,28 @@ +/* + * 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.eventformatter.test + +import io.element.android.libraries.eventformatter.api.PinnedMessagesBannerFormatter +import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem + +class FakePinnedMessagesBannerFormatter( + val formatLambda: (event: EventTimelineItem) -> CharSequence +) : PinnedMessagesBannerFormatter { + override fun format(event: EventTimelineItem): CharSequence { + return formatLambda(event) + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 76c9f902c7..a469904aac 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -106,6 +106,11 @@ interface MatrixRoom : Closeable { */ suspend fun timelineFocusedOnEvent(eventId: EventId): Result + /** + * Create a new timeline for the pinned events of the room. + */ + suspend fun pinnedEventsTimeline(): Result + fun destroy() suspend fun subscribeToSync() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 96beb99d5a..111370dcc4 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -192,6 +192,21 @@ class RustMatrixRoom( } } + override suspend fun pinnedEventsTimeline(): Result { + return runCatching { + innerRoom.pinnedEventsTimeline( + internalIdPrefix = "pinned_events", + maxEventsToLoad = 100u, + ).let { inner -> + createTimeline(inner, isLive = false) + } + }.onFailure { + if (it is CancellationException) { + throw it + } + } + } + override fun destroy() { roomCoroutineScope.cancel() liveTimeline.close() diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index fef964c867..955e9b64c5 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -135,6 +135,7 @@ class FakeMatrixRoom( private val updateMembersResult: () -> Unit = { lambdaError() }, private val getMembersResult: (Int) -> Result> = { lambdaError() }, private val timelineFocusedOnEventResult: (EventId) -> Result = { lambdaError() }, + private val pinnedEventsTimelineResult: () -> Result = { lambdaError() }, private val setSendQueueEnabledLambda: (Boolean) -> Unit = { _: Boolean -> }, private val saveComposerDraftLambda: (ComposerDraft) -> Result = { _: ComposerDraft -> Result.success(Unit) }, private val loadComposerDraftLambda: () -> Result = { Result.success(null) }, @@ -182,6 +183,10 @@ class FakeMatrixRoom( timelineFocusedOnEventResult(eventId) } + override suspend fun pinnedEventsTimeline(): Result = simulateLongTask { + pinnedEventsTimelineResult() + } + override suspend fun subscribeToSync() { subscribeToSyncLambda() } diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 5766c900dd..c66d504ea8 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -293,6 +293,7 @@ Reason: %1$s." "Unblock user" "%1$s of %2$s" "%1$s Pinned messages" + "Loading message…" "View All" "Chat" "Share location" 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 index 558c81c8b7..1b6fb4bab8 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7114202a1de9860547c525c0dadc110ce9e2e198465218ac2c33cf65f2f0eaa2 -size 9496 +oid sha256:96a867cb12498cbdc97957bee07855dfaa13602baddaf933aff2b666ef4c7650 +size 3642 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 index 3d77277fec..98720535b6 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e6b5fd9ecc2b01cc8a83f3fe8e34352de1792a82db85c396377a18246adad1a -size 12953 +oid sha256:d5b71d416128b5b4791cec79ed7cdf963574c8fa66b32844ec798f55ddf62f42 +size 7960 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 index cfd32bb09c..336a9a5ad1 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:14ca5901134299e801e204e280d731e7de4072f1d522b076eb41c5f806897ed2 -size 12905 +oid sha256:720d657356e7fff0d0994a11fb95152b896b9e1de04dd50023b85c4d72cde6af +size 11414 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 index 40ed099f5b..558c81c8b7 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb206284c642dd665290d5d553491e622b8e15a64df7bb2dbd91ea5d3a13e19a -size 13041 +oid sha256:7114202a1de9860547c525c0dadc110ce9e2e198465218ac2c33cf65f2f0eaa2 +size 9496 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 index 15a58d4763..3d77277fec 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1940c4ee1e07c6a0198682460af1a6558fcaf14cb69ff061831cb591eb7aec3 -size 13066 +oid sha256:8e6b5fd9ecc2b01cc8a83f3fe8e34352de1792a82db85c396377a18246adad1a +size 12953 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 index ac3fb40ad1..575d07a9f1 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fee5c1dbcdf7929f4762b2915584fe45b7f39916a949663f03e8d7e85e991b4b -size 12988 +oid sha256:743c950b885f3831034dab99571e19660a608eb8cc93218c939b570c1925e8f7 +size 12926 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_6_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_6_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_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_7_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_7_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_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_8_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_8_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_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_9_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_9_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 index 625e940b9e..d6fd8eeb70 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16658495b889654f152ba80a52111164f9682009b96abf1f3f20e660bd7c2407 -size 9297 +oid sha256:5bb36ccd718f3fec5b04f1bc812dc7718b5ea7fa4619c8b031466297a8d016fd +size 3659 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 index 3de9e45d12..7c8404b2d7 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:43651c4a7f10406f1a6a4b995467ba292d4a43bc198dcf352ae2e66694154de2 -size 12340 +oid sha256:ff5e3562e981f2675a78e1cdf6cb7599471ad91030059fc8ff429165e1178131 +size 7873 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 index b320ff09e6..a45ab0dc23 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cca8ebb1ec12497de7e2efc1725a2e4427eecd1d340ae8176d10f914def0af25 -size 12297 +oid sha256:86caf295341ae9f5ae5cf99b39539a31afb0286c213e20b528078fc3fb3532e0 +size 10874 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 index 8299ed3a1c..625e940b9e 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37bf00fbf548b7ba3d601af1ca489e07e25059c2f5a68abf9b85f4c656cf482c -size 12425 +oid sha256:16658495b889654f152ba80a52111164f9682009b96abf1f3f20e660bd7c2407 +size 9297 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 index 533f421f11..3de9e45d12 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed813a7003eb01a06667b10191990ed5bb3f75ee6a447cc4d52510b7e13b3724 -size 12448 +oid sha256:43651c4a7f10406f1a6a4b995467ba292d4a43bc198dcf352ae2e66694154de2 +size 12340 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 index a3b1d03bce..743791eeaa 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6fa556ff7f6757c69c24e47520e08ccfd1b009d8e49a704c36d7fc4ca4186cbf -size 12378 +oid sha256:634006a41aee47e783bd8dcce295aa816f16e8d892f8967debeced863e4cc18b +size 12333 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_6_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_6_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_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_7_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_7_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_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_8_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_8_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_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_9_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_9_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.typing_MessagesViewWithTyping_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.typing_MessagesViewWithTyping_Day_0_en.png index 309c2bd605..0cb73c33e9 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.typing_MessagesViewWithTyping_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.typing_MessagesViewWithTyping_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:40960e8da93791449132dcc4c77a0ee4ca711358ea8f184c080bb4dddbe01724 -size 53669 +oid sha256:5c96744ce341811ece23e037fe15bdeeb48c7c6d9a0e931fec1bbd9993679aed +size 56230 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.typing_MessagesViewWithTyping_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.typing_MessagesViewWithTyping_Day_1_en.png index 97450096ad..9f9598a575 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.typing_MessagesViewWithTyping_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.typing_MessagesViewWithTyping_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3505a9a29ca2636dbf201b1bdfb0a52d05ec885b5b14709dca3f3d1184cf258 -size 54547 +oid sha256:bb81645a873e815d96ec14efe0f84a2ff354093e62fb4957878bfc6ae214b9b8 +size 57067 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.typing_MessagesViewWithTyping_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.typing_MessagesViewWithTyping_Day_2_en.png index 106395dcc2..a615304e7f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.typing_MessagesViewWithTyping_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.typing_MessagesViewWithTyping_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c495a8d5d9d4cef2f258d39decc7c841b2bee5196a330944e745d78af42b046c -size 50595 +oid sha256:4886e89da091d3869bfedb4e6f50009976fcab59f9617b8a4083c00e7027459a +size 53118 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.typing_MessagesViewWithTyping_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.typing_MessagesViewWithTyping_Night_0_en.png index fdd5741b35..fa8b564c04 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.typing_MessagesViewWithTyping_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.typing_MessagesViewWithTyping_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:43e55acf964c3bde105c3b00eaa9d67d9cdeb3334f0d641d18c5c2476a178655 -size 54416 +oid sha256:bc69aaab31d3f7e63b0481818b0c91c90aef44afc21ee06591ba69cdf45b978a +size 56712 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.typing_MessagesViewWithTyping_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.typing_MessagesViewWithTyping_Night_1_en.png index 0e7b362a1c..c0d8ccb194 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.typing_MessagesViewWithTyping_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.typing_MessagesViewWithTyping_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c749a0dff6fa96383268514cc330e02d13c1f50e5e6d238539c612cad7ba54d -size 55283 +oid sha256:0123a32464a8ca8ca3f85af8bfde18aca054c63e91ec1264a54e0177c58f0d4b +size 57539 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.typing_MessagesViewWithTyping_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.typing_MessagesViewWithTyping_Night_2_en.png index 9f47f82ec1..5d5d4daf56 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.typing_MessagesViewWithTyping_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.typing_MessagesViewWithTyping_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d741537e51e539a627aaee9a567a52a5a96207c8dfb7d69ce799438179ad6c29 -size 51388 +oid sha256:d5b8ff0e7e06516a29694c46cd22c5f09f029725cc3556c92a4491c00868151f +size 53662 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_0_en.png index fb3f6bcbad..315f5a6852 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff311eba5365bb2a726eb59b71ecc40295032003dd5e03587efed086bdca79ec -size 55228 +oid sha256:277463b454876b3bff870e27deafd2b1d7affcdb5b72efcbb33bc4bfeac0a961 +size 56838 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_10_en.png index 3b7df7988e..8707889672 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a24bfa8d881fac3d302898f7721c202c4cdedb37d4a75277cdc551a3a4e6ab80 -size 56882 +oid sha256:50517ad2782539a02ad2ac302e15da20f70e48f7b3ceab9438f41f4cb590d35a +size 58414 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png index 8fdd7ef2a3..c1ff22b537 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6bca01015488a95b5a7daf277fe2e8114012e26ddacad195ddffba6c30edb080 -size 47104 +oid sha256:d5f6843d5976f1b476f36a01e6db8cce729862a5d138f23b2053f4a2f085d6df +size 48527 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_12_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_12_en.png index 5f698df606..2d5a682ca0 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b2b13b08c823c492457daf9b54ae17616845154af22a58880cd1081ab38b304c -size 55264 +oid sha256:3f2e8218de84709a8f42a9b4594c92da51a0427651de3c85885e8f147527bad9 +size 56860 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_1_en.png index c50c0cac8b..aa177b86a8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8424c63cd12ce518c096d62c8aeeffc5d7f7308fed1c1612dca7ad8e832606c4 -size 54790 +oid sha256:26490a489101d7718b08a7a85982af6dc1458dd01a1b9491903278d66724d0b6 +size 55998 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_3_en.png index 6f7fa71e13..670b6e71d2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8292a8b339592e3caf7118a16584ca41754900f03a8fa89bbc2424d2c3a6c9f1 -size 57701 +oid sha256:b3d185905f72389bd4edbed4d2ec7b6f4547465004174a1c798e604e83f84363 +size 59845 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_4_en.png index ff5fb09de4..d8eb4d59a7 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21f6c7ac3951f1aca51d6a2eabca7fdbdb1e00b3b3a3c44c96dae180bbee3623 -size 53216 +oid sha256:60cb1e0ab614fd719991e6c27ffdec3f68b6d9c6a7e0c5a8294eab3e4e51fa89 +size 54618 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_5_en.png index 50322c59cf..3fca9380fb 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d6db28acda62e4343ba7b4f64f31070253fe968f6ed8464afb1bb54506353278 -size 53081 +oid sha256:28615c037c99b201dd987faa83b4d184b7c593d0f3871c8f731d681ee6795a1d +size 54688 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_6_en.png index 69801a6993..850098bf34 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:806695481b7fe30e0c315b2e46c6fe48c21a1cedbfaf21736ad2716a41212473 -size 53645 +oid sha256:b6c85d42e90ea4c60d2cd4e0ac96fc38cb45504e1d863a4283f157f23e790899 +size 54346 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_7_en.png index 45e3b40177..ac7ade1342 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c46f2842388c3d2d14cddae957ccc74c4a108faceff36846f5ca7daa00b9f593 -size 56398 +oid sha256:e5cbd166edeaec6ab06a5da87a0a023adfbb6f2e9727b4471c5586a2b897dbab +size 57729 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png index fe804c04af..176a8a9941 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7e85f80a115c90586f81127b87aa5874a2f811d15c6af443bc0f0d7078c85f4e -size 38280 +oid sha256:8170faa5461e703c09803966cb25be7da37cbad08d6c8e21584c75eccf8a7d29 +size 39873 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_9_en.png index c344a1583e..04baf108ba 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9ae1d25882c0c05723b68c61f97ac4ff2423750913f1f3186a6dad48e845e8e -size 37557 +oid sha256:1362fddcca30b31f92575d3e8020b4dc1c415ed0afefe29c56d5f2179d6a35f1 +size 39043 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_0_en.png index ae3271ee83..becc913996 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1e1974d0ec89208a55b0faeb230c89fc3ed10c1d4bead234dc39c06169ab24ca -size 55060 +oid sha256:16afa707859ef41a8420eec52f3e7e2e2c0d2dcf4b438e068468dcaa77fd4200 +size 56547 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_10_en.png index 439ce25005..2c0cfc57da 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:74a2c962831b6468dbff878d6f55460a5cdbc3d06c7566cecfb89b7bcf049b18 -size 56600 +oid sha256:ce3e8d221e1a4c9557fabd5902dd507d3c9a6774c4185612b708ef80499e13d3 +size 58015 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png index c74b552192..b5146ab82b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e468164817c0e6302c1f8cacecb24daed2fe65eb5453723186651dc7bc1851cf -size 43297 +oid sha256:4dbd737eaa65c834550ab421e4e3ee5c77701ab58ce06197048dbf8e044c548a +size 44487 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_12_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_12_en.png index 7c09af645a..a39a656f25 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_12_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_12_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad336e1b96ac2528cd1d7573fda649e5fe09235b72d555d79f32cca4eb8c1c05 -size 55084 +oid sha256:0bdec5d25d991452633e77e4d5c2468d4b48cd880fbfb713a6c3b897cbdde921 +size 56587 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_1_en.png index b935cc06e7..08e5acbbc8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d5b9fe533cfad8892708a609a79fa1f8bed95ad8fb20b979993da9b2d1ea380d -size 54511 +oid sha256:872853bf96c32e0f375d6ec772437e27e21d16506f7fa2f7853ef91cd274bb28 +size 55440 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_3_en.png index 0cc32b0dbf..0e7a7f783c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4d0b4ab8e2234e08ca7c67d4653729abcf2c4ed055b983fdde15589daaf0124 -size 57234 +oid sha256:86a3e684d4a794915344963c4dd26e1e68729e7e5d9c4091c7824cf0901684b3 +size 59342 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_4_en.png index b513f8b897..4776d19372 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a4a32531adceea6b03b25291435fb2df753868571b3edf3ea29f00353d07087e -size 49544 +oid sha256:c446a2b24ae180496649f518f53f15f939c1e166093b82e479d2d7f1a8a7829d +size 50742 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_5_en.png index bb7da5784e..70caedc0cf 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:534490ea60a6078200761864dd112f54be124647f818ccec77fb986ea71582a8 -size 52689 +oid sha256:3df1d346ea653cfafea9e2e13a7ff47017a480ce593d1c959553e7b8073f0250 +size 54217 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_6_en.png index 1eb51aad8a..6cc5a72d24 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2063163a5ac3c78398df388b9e471362a32244d5bdf4790409358640b5cfb15f -size 53399 +oid sha256:6dcf842a6bc3abfc9168b0e30354f3de2f72bddb04b203dcf9d211c77155bcfd +size 53901 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_7_en.png index a8c890d748..bcc10ece2a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d0d0b885ae157ff456d7113707bc72f3ced9ab74c895cdf7377adc1bc8c48db -size 52373 +oid sha256:8639bc9d5728e8bf5b1b3ab76aff16aacf89ff15397770f08c5211dc9342fa78 +size 53545 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png index ecdeb0c387..2d560199c1 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:828b9893e06b168b16b42950f42b89ecc51b73c9b425732d379a72d737f04491 -size 36126 +oid sha256:310862ba0323270901b3a554ff230931a75de56a77487c7b0e63137f90cc0214 +size 37398 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_9_en.png index 5920916813..df43160511 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f251ca0e5c4f98b7c5889b42161e4d52f3faf37850c242122aac2d124a1357b1 -size 35393 +oid sha256:5abde960f15c7d28e6fc538f5450f277e89ee17893e12daaba004ca7a0f09974 +size 36656