From 039744c4be64e9c05dcd1ec6c90f1a42246b9ea4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 1 Aug 2024 18:14:53 +0200 Subject: [PATCH 01/17] Pinned events : introduce banner formatter --- .../api/PinnedMessagesBannerFormatter.kt | 23 ++++ .../DefaultPinnedMessagesBannerFormatter.kt | 123 ++++++++++++++++++ .../impl/DefaultRoomLastMessageFormatter.kt | 17 +-- .../eventformatter/impl/PrefixWith.kt | 33 +++++ 4 files changed, 182 insertions(+), 14 deletions(-) create mode 100644 libraries/eventformatter/api/src/main/kotlin/io/element/android/libraries/eventformatter/api/PinnedMessagesBannerFormatter.kt create mode 100644 libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatter.kt create mode 100644 libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/PrefixWith.kt 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..91a6de9a95 --- /dev/null +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/DefaultPinnedMessagesBannerFormatter.kt @@ -0,0 +1,123 @@ +/* + * 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..08a4045c43 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 @@ -79,7 +79,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 +90,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 +113,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 +167,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) + } +} From 34fd21f4404ce4abea7ec1993efe1e8700dafdfa Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 1 Aug 2024 18:22:22 +0200 Subject: [PATCH 02/17] Pinned events : introduce pinnedEventsTimeline method. --- .../libraries/matrix/api/room/MatrixRoom.kt | 5 +++++ .../libraries/matrix/impl/room/RustMatrixRoom.kt | 15 +++++++++++++++ .../libraries/matrix/test/room/FakeMatrixRoom.kt | 5 +++++ 3 files changed, 25 insertions(+) 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 e2be5bcb66..3904de05d3 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) }, @@ -181,6 +182,10 @@ class FakeMatrixRoom( timelineFocusedOnEventResult(eventId) } + override suspend fun pinnedEventsTimeline(): Result = simulateLongTask { + pinnedEventsTimelineResult() + } + override suspend fun subscribeToSync() = Unit override suspend fun powerLevels(): Result { From f63b59e118ddf50c7857bb78bae6edd9ee4f2a0c Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 1 Aug 2024 18:34:52 +0200 Subject: [PATCH 03/17] Pinned events : start branching sdk data in the banner. --- features/messages/impl/build.gradle.kts | 1 + .../features/messages/impl/MessagesView.kt | 4 + .../messages/impl/di/MessagesModule.kt | 4 +- .../pinned/banner/PinnedMessagesBannerItem.kt | 25 +++++ .../banner/PinnedMessagesBannerPresenter.kt | 69 ++++++++++++- .../banner/PinnedMessagesBannerState.kt | 3 +- .../PinnedMessagesBannerStateProvider.kt | 8 ++ .../pinned/banner/PinnedMessagesBannerView.kt | 98 ++++++++++--------- .../PinnedMessagesBannerPresenterTest.kt | 17 +++- .../test/FakePinnedMessagesBannerFormatter.kt | 28 ++++++ 10 files changed, 201 insertions(+), 56 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerItem.kt create mode 100644 libraries/eventformatter/test/src/main/kotlin/io/element/android/libraries/eventformatter/test/FakePinnedMessagesBannerFormatter.kt 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/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 3f606f9fb9..962ed0f8ff 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 @@ -73,6 +73,7 @@ import io.element.android.features.messages.impl.messagecomposer.AttachmentsStat import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerView import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerView +import io.element.android.features.messages.impl.timeline.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 @@ -401,6 +402,9 @@ private fun MessagesViewContent( ) { PinnedMessagesBannerView( state = state.pinnedMessagesBannerState, + onClick = { pinnedEventId -> + //state.timelineState.eventSink(TimelineEvents.FocusOnEvent(pinnedEventId)) + }, ) } } 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/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/PinnedMessagesBannerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt index e020581468..d5968b6a7b 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,28 +17,86 @@ package io.element.android.features.messages.impl.pinned.banner import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.text.AnnotatedString import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.eventformatter.api.PinnedMessagesBannerFormatter +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.flowOn +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() : Presenter { +class PinnedMessagesBannerPresenter @Inject constructor( + private val room: MatrixRoom, + private val pinnedMessagesBannerFormatter: PinnedMessagesBannerFormatter, +) : Presenter { + + @OptIn(FlowPreview::class) @Composable override fun present(): PinnedMessagesBannerState { - var pinnedMessageCount by remember { - mutableIntStateOf(0) + var pinnedMessages by remember { + mutableStateOf>(emptyList()) } var currentPinnedMessageIndex by rememberSaveable { mutableIntStateOf(0) } + LaunchedEffect(pinnedMessages) { + val pinnedMessageCount = pinnedMessages.size + if (currentPinnedMessageIndex >= pinnedMessageCount) { + currentPinnedMessageIndex = (pinnedMessageCount - 1).coerceAtLeast(0) + } + } + + LaunchedEffect(Unit) { + val pinnedEventsTimeline = room.pinnedEventsTimeline().getOrNull() ?: return@LaunchedEffect + pinnedEventsTimeline.timelineItems + .debounce(300.milliseconds) + .map { timelineItems -> + timelineItems.mapNotNull { timelineItem -> + when (timelineItem) { + is MatrixTimelineItem.Event -> { + val eventId = timelineItem.eventId ?: return@mapNotNull null + val formatted = pinnedMessagesBannerFormatter.format(timelineItem.event) + PinnedMessagesBannerItem( + eventId = eventId, + formatted = if (formatted is AnnotatedString) { + formatted + } else { + AnnotatedString(formatted.toString()) + }, + ) + } + else -> null + } + } + } + .flowOn(Dispatchers.Default) + .onEach { newPinnedMessages -> + pinnedMessages = newPinnedMessages + }.onCompletion { + pinnedEventsTimeline.close() + } + .launchIn(this) + } fun handleEvent(event: PinnedMessagesBannerEvents) { when (event) { is PinnedMessagesBannerEvents.MoveToNextPinned -> { - if (currentPinnedMessageIndex < pinnedMessageCount - 1) { + if (currentPinnedMessageIndex < pinnedMessages.size - 1) { currentPinnedMessageIndex++ } else { currentPinnedMessageIndex = 0 @@ -48,7 +106,8 @@ class PinnedMessagesBannerPresenter @Inject constructor() : Presenter Unit ) { - val displayBanner = pinnedMessagesCount > 0 && currentPinnedMessageIndex < pinnedMessagesCount + val displayBanner = pinnedMessagesCount > 0 && currentPinnedMessage != null } 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..dcd6a4984a 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,7 +16,10 @@ 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 @@ -33,9 +36,14 @@ internal class PinnedMessagesBannerStateProvider : PreviewParameterProvider Unit = {} ) = PinnedMessagesBannerState( pinnedMessagesCount = pinnedMessagesCount, currentPinnedMessageIndex = currentPinnedMessageIndex, + currentPinnedMessage = currentPinnedMessage, 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..7fe2fa49cd 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,8 +33,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -42,6 +41,7 @@ import androidx.compose.ui.draw.shadow import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -55,39 +55,44 @@ 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, modifier: Modifier = Modifier, ) { + if (state.currentPinnedMessage == null) return + val borderColor = ElementTheme.colors.pinnedMessageBannerBorder Row( modifier = modifier - .background(color = ElementTheme.colors.bgCanvasDefault) - .fillMaxWidth() - .drawBehind { - val strokeWidth = 0.5.dp.toPx() - val y = size.height - strokeWidth / 2 - drawLine( - borderColor, - Offset(0f, y), - Offset(size.width, y), - strokeWidth - ) - drawLine( - borderColor, - Offset(0f, 0f), - Offset(size.width, 0f), - strokeWidth - ) - } - .shadow(elevation = 5.dp, spotColor = Color.Transparent) - .heightIn(min = 64.dp) - .clickable { - state.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned) - }, + .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 { + onClick(state.currentPinnedMessage.eventId) + state.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned) + }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = spacedBy(10.dp) ) { @@ -106,7 +111,7 @@ fun PinnedMessagesBannerView( PinnedMessageItem( index = state.currentPinnedMessageIndex, totalCount = state.pinnedMessagesCount, - message = "This is a pinned message", + message = state.currentPinnedMessage.formatted, modifier = Modifier.weight(1f) ) TextButton(text = stringResource(id = CommonStrings.screen_room_pinned_banner_view_all_button_title), onClick = { /*TODO*/ }) @@ -119,14 +124,12 @@ private fun PinIndicators( 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 +144,21 @@ private fun PinIndicators( modifier = modifier, state = lazyListState, verticalArrangement = spacedBy(2.dp), - userScrollEnabled = false + userScrollEnabled = false, + reverseLayout = true ) { 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 +168,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,6 +183,7 @@ private fun PinnedMessageItem( style = ElementTheme.typography.fontBodySmMedium, color = ElementTheme.colors.textActionAccent, maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } Text( @@ -196,5 +201,6 @@ private fun PinnedMessageItem( internal fun PinnedMessagesBannerViewPreview(@PreviewParameter(PinnedMessagesBannerStateProvider::class) state: PinnedMessagesBannerState) = ElementPreview { PinnedMessagesBannerView( state = state, + onClick = {}, ) } 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..9ed181d9e1 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,6 +17,10 @@ package io.element.android.features.messages.impl.pinned.banner import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.eventformatter.api.PinnedMessagesBannerFormatter +import io.element.android.libraries.eventformatter.test.FakePinnedMessagesBannerFormatter +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.tests.testutils.test import kotlinx.coroutines.test.runTest import org.junit.Test @@ -29,6 +33,7 @@ class PinnedMessagesBannerPresenterTest { val initialState = awaitItem() assertThat(initialState.pinnedMessagesCount).isEqualTo(0) assertThat(initialState.currentPinnedMessageIndex).isEqualTo(0) + assertThat(initialState.currentPinnedMessage).isNull() } } @@ -43,7 +48,15 @@ class PinnedMessagesBannerPresenterTest { } } - private fun createPinnedMessagesBannerPresenter(): PinnedMessagesBannerPresenter { - return PinnedMessagesBannerPresenter() + private fun createPinnedMessagesBannerPresenter( + room: MatrixRoom = FakeMatrixRoom(), + formatter: PinnedMessagesBannerFormatter = FakePinnedMessagesBannerFormatter( + formatLambda = { event -> "Content ${event.content}" } + ) + ): PinnedMessagesBannerPresenter { + return PinnedMessagesBannerPresenter( + room = room, + pinnedMessagesBannerFormatter = formatter + ) } } 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) + } +} From 8e8c271bc20db7926cbc8b880f2aee17092c0fc8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 2 Aug 2024 11:14:24 +0200 Subject: [PATCH 04/17] Pinned events : only load data if feature is enabled --- .../banner/PinnedMessagesBannerItemFactory.kt | 47 ++++++++++ .../banner/PinnedMessagesBannerPresenter.kt | 91 ++++++++++--------- .../PinnedMessagesBannerPresenterTest.kt | 25 +++-- 3 files changed, 113 insertions(+), 50 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerItemFactory.kt 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 d5968b6a7b..bc594ff49c 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 @@ -18,21 +18,20 @@ 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 androidx.compose.ui.text.AnnotatedString import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.eventformatter.api.PinnedMessagesBannerFormatter +import io.element.android.libraries.featureflag.api.FeatureFlagService +import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion @@ -42,61 +41,37 @@ import kotlin.time.Duration.Companion.milliseconds class PinnedMessagesBannerPresenter @Inject constructor( private val room: MatrixRoom, - private val pinnedMessagesBannerFormatter: PinnedMessagesBannerFormatter, + private val itemFactory: PinnedMessagesBannerItemFactory, + private val featureFlagService: FeatureFlagService, ) : Presenter { - @OptIn(FlowPreview::class) @Composable override fun present(): PinnedMessagesBannerState { - var pinnedMessages by remember { + var pinnedItems by remember { mutableStateOf>(emptyList()) } + + fun onItemsChange(newItems: List) { + pinnedItems = newItems + } + var currentPinnedMessageIndex by rememberSaveable { mutableIntStateOf(0) } - LaunchedEffect(pinnedMessages) { - val pinnedMessageCount = pinnedMessages.size + + LaunchedEffect(pinnedItems) { + val pinnedMessageCount = pinnedItems.size if (currentPinnedMessageIndex >= pinnedMessageCount) { currentPinnedMessageIndex = (pinnedMessageCount - 1).coerceAtLeast(0) } } - LaunchedEffect(Unit) { - val pinnedEventsTimeline = room.pinnedEventsTimeline().getOrNull() ?: return@LaunchedEffect - pinnedEventsTimeline.timelineItems - .debounce(300.milliseconds) - .map { timelineItems -> - timelineItems.mapNotNull { timelineItem -> - when (timelineItem) { - is MatrixTimelineItem.Event -> { - val eventId = timelineItem.eventId ?: return@mapNotNull null - val formatted = pinnedMessagesBannerFormatter.format(timelineItem.event) - PinnedMessagesBannerItem( - eventId = eventId, - formatted = if (formatted is AnnotatedString) { - formatted - } else { - AnnotatedString(formatted.toString()) - }, - ) - } - else -> null - } - } - } - .flowOn(Dispatchers.Default) - .onEach { newPinnedMessages -> - pinnedMessages = newPinnedMessages - }.onCompletion { - pinnedEventsTimeline.close() - } - .launchIn(this) - } + PinnedMessagesBannerItemsEffect(::onItemsChange) fun handleEvent(event: PinnedMessagesBannerEvents) { when (event) { is PinnedMessagesBannerEvents.MoveToNextPinned -> { - if (currentPinnedMessageIndex < pinnedMessages.size - 1) { + if (currentPinnedMessageIndex < pinnedItems.size - 1) { currentPinnedMessageIndex++ } else { currentPinnedMessageIndex = 0 @@ -106,10 +81,38 @@ class PinnedMessagesBannerPresenter @Inject constructor( } return PinnedMessagesBannerState( - pinnedMessagesCount = pinnedMessages.size, - currentPinnedMessage = pinnedMessages.getOrNull(currentPinnedMessageIndex), + pinnedMessagesCount = pinnedItems.size, + currentPinnedMessage = pinnedItems.getOrNull(currentPinnedMessageIndex), currentPinnedMessageIndex = currentPinnedMessageIndex, eventSink = ::handleEvent ) } + + @OptIn(FlowPreview::class) + @Composable + private fun PinnedMessagesBannerItemsEffect( + onItemsChange: (List) -> Unit, + ) { + val isFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.PinnedEvents).collectAsState(initial = false) + val updatedOnItemsChange by rememberUpdatedState(onItemsChange) + + LaunchedEffect(isFeatureEnabled) { + if (!isFeatureEnabled) return@LaunchedEffect + + val pinnedEventsTimeline = room.pinnedEventsTimeline().getOrNull() ?: return@LaunchedEffect + pinnedEventsTimeline.timelineItems + .debounce(300.milliseconds) + .map { timelineItems -> + timelineItems.mapNotNull { timelineItem -> + itemFactory.create(timelineItem) + } + } + .onEach { newItems -> + updatedOnItemsChange(newItems) + }.onCompletion { + pinnedEventsTimeline.close() + } + .launchIn(this) + } + } } 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 9ed181d9e1..baab17577d 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,11 +17,14 @@ package io.element.android.features.messages.impl.pinned.banner import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.eventformatter.api.PinnedMessagesBannerFormatter import io.element.android.libraries.eventformatter.test.FakePinnedMessagesBannerFormatter +import io.element.android.libraries.featureflag.api.FeatureFlags +import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.tests.testutils.test +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test @@ -48,15 +51,25 @@ class PinnedMessagesBannerPresenterTest { } } - private fun createPinnedMessagesBannerPresenter( + private fun TestScope.createPinnedMessagesBannerPresenter( room: MatrixRoom = FakeMatrixRoom(), - formatter: PinnedMessagesBannerFormatter = FakePinnedMessagesBannerFormatter( - formatLambda = { event -> "Content ${event.content}" } - ) + itemFactory: PinnedMessagesBannerItemFactory = PinnedMessagesBannerItemFactory( + coroutineDispatchers = testCoroutineDispatchers(), + formatter = FakePinnedMessagesBannerFormatter( + formatLambda = { event -> "Content ${event.content}" } + ) + ), + isFeatureEnabled: Boolean = true, ): PinnedMessagesBannerPresenter { + val featureFlagService = FakeFeatureFlagService( + initialState = mapOf( + FeatureFlags.PinnedEvents.key to isFeatureEnabled + ) + ) return PinnedMessagesBannerPresenter( room = room, - pinnedMessagesBannerFormatter = formatter + itemFactory = itemFactory, + featureFlagService = featureFlagService ) } } From 9d2e35db0d6df5cf4cc404637665fc16813508af Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 2 Aug 2024 12:18:36 +0200 Subject: [PATCH 05/17] Pinned events : add the glue for the ViewAll click. --- .../messages/impl/MessagesFlowNode.kt | 6 ++ .../features/messages/impl/MessagesNode.kt | 6 ++ .../features/messages/impl/MessagesView.kt | 10 ++- .../pinned/banner/PinnedMessagesBannerView.kt | 72 ++++++++++--------- .../typing/MessagesViewWithTypingPreview.kt | 3 +- .../messages/impl/MessagesViewTest.kt | 2 + 6 files changed, 60 insertions(+), 39 deletions(-) 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..8523900981 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 @@ -17,6 +17,7 @@ package io.element.android.features.messages.impl import android.os.Parcelable +import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier @@ -81,6 +82,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 +219,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/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 962ed0f8ff..17141d6496 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 @@ -73,7 +73,6 @@ import io.element.android.features.messages.impl.messagecomposer.AttachmentsStat import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerView import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerView -import io.element.android.features.messages.impl.timeline.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 @@ -127,8 +126,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)) @@ -229,6 +229,7 @@ fun MessagesView( }, forceJumpToBottomVisibility = forceJumpToBottomVisibility, onJoinCallClick = onJoinCallClick, + onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick, ) }, snackbarHost = { @@ -320,6 +321,7 @@ private fun MessagesViewContent( onSendLocationClick: () -> Unit, onCreatePollClick: () -> Unit, onJoinCallClick: () -> Unit, + onViewAllPinnedMessagesClick: () -> Unit, forceJumpToBottomVisibility: Boolean, modifier: Modifier = Modifier, onSwipeToReply: (TimelineItem.Event) -> Unit, @@ -405,6 +407,7 @@ private fun MessagesViewContent( onClick = { pinnedEventId -> //state.timelineState.eventSink(TimelineEvents.FocusOnEvent(pinnedEventId)) }, + onViewAllClick = onViewAllPinnedMessagesClick, ) } } @@ -576,12 +579,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/pinned/banner/PinnedMessagesBannerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerView.kt index 7fe2fa49cd..9e7bec076d 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 @@ -62,6 +62,7 @@ import io.element.android.libraries.ui.strings.CommonStrings fun PinnedMessagesBannerView( state: PinnedMessagesBannerState, onClick: (EventId) -> Unit, + onViewAllClick: () -> Unit, modifier: Modifier = Modifier, ) { if (state.currentPinnedMessage == null) return @@ -69,30 +70,30 @@ fun PinnedMessagesBannerView( 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 { - onClick(state.currentPinnedMessage.eventId) - state.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned) - }, + .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 { + onClick(state.currentPinnedMessage.eventId) + state.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned) + }, verticalAlignment = Alignment.CenterVertically, horizontalArrangement = spacedBy(10.dp) ) { @@ -114,7 +115,7 @@ fun PinnedMessagesBannerView( message = state.currentPinnedMessage.formatted, modifier = Modifier.weight(1f) ) - TextButton(text = stringResource(id = CommonStrings.screen_room_pinned_banner_view_all_button_title), onClick = { /*TODO*/ }) + TextButton(text = stringResource(id = CommonStrings.screen_room_pinned_banner_view_all_button_title), onClick = onViewAllClick) } } @@ -150,15 +151,15 @@ private fun PinIndicators( 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 + } + ) ) } } @@ -174,7 +175,7 @@ private fun PinnedMessageItem( 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) { - AnimatedVisibility (totalCount > 1) { + AnimatedVisibility(totalCount > 1) { Text( text = annotatedTextWithBold( text = fullCountMessage, @@ -202,5 +203,6 @@ internal fun PinnedMessagesBannerViewPreview(@PreviewParameter(PinnedMessagesBan PinnedMessagesBannerView( state = state, onClick = {}, + onViewAllClick = {}, ) } 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/MessagesViewTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/MessagesViewTest.kt index e876b5ec60..c88d2ffa28 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 @@ -471,6 +471,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 +489,7 @@ private fun AndroidComposeTestRule.setMessa onSendLocationClick = onSendLocationClick, onCreatePollClick = onCreatePollClick, onJoinCallClick = onJoinCallClick, + onViewAllPinnedMessagesClick = onViewAllPinnedMessagesClick, ) } } From ca47a1c6d5b980bc95d4ffaf1b3439cdb8fbe66f Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 2 Aug 2024 15:35:53 +0200 Subject: [PATCH 06/17] Pinned events : handle focus on pinned event --- .../messages/impl/MessagesFlowNode.kt | 1 - .../features/messages/impl/MessagesView.kt | 21 +++++---- .../banner/PinnedMessagesBannerPresenter.kt | 4 +- .../pinned/banner/PinnedMessagesBannerView.kt | 34 ++++++++++++++ .../messages/impl/timeline/TimelineEvents.kt | 3 +- .../impl/timeline/TimelinePresenter.kt | 45 +++++++++++-------- .../messages/impl/timeline/TimelineState.kt | 3 ++ .../messages/impl/timeline/TimelineView.kt | 10 ++++- .../impl/timeline/TimelinePresenterTest.kt | 13 ++++++ .../DefaultPinnedMessagesBannerFormatter.kt | 2 - .../impl/DefaultRoomLastMessageFormatter.kt | 5 --- 11 files changed, 101 insertions(+), 40 deletions(-) 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 8523900981..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 @@ -17,7 +17,6 @@ package io.element.android.features.messages.impl import android.os.Parcelable -import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier 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 17141d6496..d983404f9b 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 @@ -73,6 +72,8 @@ import io.element.android.features.messages.impl.messagecomposer.AttachmentsStat import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerView import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerView +import io.element.android.features.messages.impl.pinned.banner.PinnedMessagesBannerViewDefaults +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 +106,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( @@ -380,7 +382,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, @@ -395,18 +397,21 @@ private fun MessagesViewContent( onReadReceiptClick = onReadReceiptClick, forceJumpToBottomVisibility = forceJumpToBottomVisibility, onJoinCallClick = onJoinCallClick, - lazyListState = timelineLazyListState, + nestedScrollConnection = scrollBehavior.nestedScrollConnection, ) AnimatedVisibility( - visible = state.pinnedMessagesBannerState.displayBanner && timelineLazyListState.isScrollingUp(), + visible = state.pinnedMessagesBannerState.displayBanner && scrollBehavior.isVisible, enter = expandVertically(), exit = shrinkVertically(), ) { + fun focusOnPinnedEvent(eventId: EventId) { + state.timelineState.eventSink( + TimelineEvents.FocusOnEvent(eventId = eventId, debounce = 200.milliseconds) + ) + } PinnedMessagesBannerView( state = state.pinnedMessagesBannerState, - onClick = { pinnedEventId -> - //state.timelineState.eventSink(TimelineEvents.FocusOnEvent(pinnedEventId)) - }, + onClick = ::focusOnPinnedEvent, onViewAllClick = onViewAllPinnedMessagesClick, ) } 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 bc594ff49c..16b8c97af7 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 @@ -44,7 +44,6 @@ class PinnedMessagesBannerPresenter @Inject constructor( private val itemFactory: PinnedMessagesBannerItemFactory, private val featureFlagService: FeatureFlagService, ) : Presenter { - @Composable override fun present(): PinnedMessagesBannerState { var pinnedItems by remember { @@ -109,7 +108,8 @@ class PinnedMessagesBannerPresenter @Inject constructor( } .onEach { newItems -> updatedOnItemsChange(newItems) - }.onCompletion { + } + .onCompletion { pinnedEventsTimeline.close() } .launchIn(this) 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 9e7bec076d..0ddbe1ed7b 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 @@ -33,13 +33,19 @@ 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.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 @@ -197,6 +203,34 @@ private fun PinnedMessageItem( } } +@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 + } + } +} + @PreviewsDayNight @Composable internal fun PinnedMessagesBannerViewPreview(@PreviewParameter(PinnedMessagesBannerStateProvider::class) state: PinnedMessagesBannerState) = ElementPreview { 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..93667f3987 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,6 +50,7 @@ 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 @@ -136,13 +137,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 +153,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/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/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 index 91a6de9a95..05403e5355 100644 --- 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 @@ -51,7 +51,6 @@ 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) @@ -77,7 +76,6 @@ class DefaultPinnedMessagesBannerFormatter @Inject constructor( event: EventTimelineItem, messageContent: MessageContent, ): CharSequence { - return when (val messageType: MessageType = messageContent.type) { is EmoteMessageType -> { val senderDisambiguatedDisplayName = event.senderProfile.getDisambiguatedDisplayName(event.sender) 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 08a4045c43..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 From 80f1574b6603e728a329431b5aeb0182a481f4d0 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 6 Aug 2024 15:08:02 +0200 Subject: [PATCH 07/17] Pinned events : update sdk --- gradle/libs.versions.toml | 2 +- .../impl/timeline/item/event/TimelineEventContentMapper.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a3211068d4..686f3b5b20 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -163,7 +163,7 @@ jsoup = "org.jsoup:jsoup:1.18.1" appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0" timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.36" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.37" matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" } matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" } sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt index 6b0da71eee..90b13e356d 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/TimelineEventContentMapper.kt @@ -176,7 +176,7 @@ private fun RustOtherState.map(): OtherState { RustOtherState.RoomHistoryVisibility -> OtherState.RoomHistoryVisibility RustOtherState.RoomJoinRules -> OtherState.RoomJoinRules is RustOtherState.RoomName -> OtherState.RoomName(name) - RustOtherState.RoomPinnedEvents -> OtherState.RoomPinnedEvents + is RustOtherState.RoomPinnedEvents -> OtherState.RoomPinnedEvents is RustOtherState.RoomPowerLevels -> OtherState.RoomUserPowerLevels(users) RustOtherState.RoomServerAcl -> OtherState.RoomServerAcl is RustOtherState.RoomThirdPartyInvite -> OtherState.RoomThirdPartyInvite(displayName) From 50fd2558dea06998381f3c5caa9b7ca8b7ac0d31 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 6 Aug 2024 15:08:45 +0200 Subject: [PATCH 08/17] Pinned events : handle loading/error on banner --- .../messages/impl/MessagesStateProvider.kt | 8 +- .../features/messages/impl/MessagesView.kt | 3 +- .../banner/PinnedMessagesBannerPresenter.kt | 91 ++++++++--- .../banner/PinnedMessagesBannerState.kt | 43 ++++- .../PinnedMessagesBannerStateProvider.kt | 36 +++-- .../pinned/banner/PinnedMessagesBannerView.kt | 149 ++++++++++++------ .../messages/impl/MessagesPresenterTest.kt | 4 +- .../src/main/res/values/localazy.xml | 1 + 8 files changed, 238 insertions(+), 97 deletions(-) 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 d983404f9b..f365d917f0 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 @@ -71,6 +71,7 @@ import io.element.android.features.messages.impl.messagecomposer.AttachmentsBott import io.element.android.features.messages.impl.messagecomposer.AttachmentsState import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerView +import io.element.android.features.messages.impl.pinned.banner.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.TimelineEvents @@ -400,7 +401,7 @@ private fun MessagesViewContent( nestedScrollConnection = scrollBehavior.nestedScrollConnection, ) AnimatedVisibility( - visible = state.pinnedMessagesBannerState.displayBanner && scrollBehavior.isVisible, + visible = state.pinnedMessagesBannerState != PinnedMessagesBannerState.Hidden && scrollBehavior.isVisible, enter = expandVertically(), exit = shrinkVertically(), ) { 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 16b8c97af7..59bf1c5a2f 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 @@ -26,10 +26,14 @@ 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.networkmonitor.api.NetworkMonitor import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags 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 @@ -43,29 +47,34 @@ class PinnedMessagesBannerPresenter @Inject constructor( private val room: MatrixRoom, private val itemFactory: PinnedMessagesBannerItemFactory, private val featureFlagService: FeatureFlagService, + private val networkMonitor: NetworkMonitor, ) : Presenter { @Composable override fun present(): PinnedMessagesBannerState { + val isFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.PinnedEvents).collectAsState(initial = false) + var timelineFailed by rememberSaveable { mutableStateOf(false) } var pinnedItems by remember { - mutableStateOf>(emptyList()) + mutableStateOf>(persistentListOf()) } + val knownPinnedMessagesCount by remember { + room.roomInfoFlow.map { roomInfo -> roomInfo.pinnedEventIds.size } + }.collectAsState(initial = null) - fun onItemsChange(newItems: List) { - pinnedItems = newItems - } + var currentPinnedMessageIndex by rememberSaveable { mutableIntStateOf(0) } - var currentPinnedMessageIndex by rememberSaveable { - mutableIntStateOf(0) - } - - LaunchedEffect(pinnedItems) { - val pinnedMessageCount = pinnedItems.size - if (currentPinnedMessageIndex >= pinnedMessageCount) { - currentPinnedMessageIndex = (pinnedMessageCount - 1).coerceAtLeast(0) + PinnedMessagesBannerItemsEffect( + isFeatureEnabled = isFeatureEnabled, + onItemsChange = { newItems -> + val pinnedMessageCount = newItems.size + if (currentPinnedMessageIndex >= pinnedMessageCount) { + currentPinnedMessageIndex = 0 + } + pinnedItems = newItems + }, + onTimelineFail = { hasTimelineFailed -> + timelineFailed = hasTimelineFailed } - } - - PinnedMessagesBannerItemsEffect(::onItemsChange) + ) fun handleEvent(event: PinnedMessagesBannerEvents) { when (event) { @@ -79,32 +88,68 @@ class PinnedMessagesBannerPresenter @Inject constructor( } } - return PinnedMessagesBannerState( - pinnedMessagesCount = pinnedItems.size, - currentPinnedMessage = pinnedItems.getOrNull(currentPinnedMessageIndex), + return pinnedMessagesBannerState( + isFeatureEnabled = isFeatureEnabled, + hasTimelineFailed = timelineFailed, + realPinnedMessagesCount = knownPinnedMessagesCount, + pinnedItems = pinnedItems, currentPinnedMessageIndex = currentPinnedMessageIndex, eventSink = ::handleEvent ) } + @Composable + private fun pinnedMessagesBannerState( + isFeatureEnabled: Boolean, + hasTimelineFailed: Boolean, + realPinnedMessagesCount: Int?, + pinnedItems: ImmutableList, + currentPinnedMessageIndex: Int, + eventSink: (PinnedMessagesBannerEvents) -> Unit + ): PinnedMessagesBannerState { + val currentPinnedMessage = pinnedItems.getOrNull(currentPinnedMessageIndex) + return when { + !isFeatureEnabled -> PinnedMessagesBannerState.Hidden + hasTimelineFailed -> PinnedMessagesBannerState.Hidden + realPinnedMessagesCount == null || realPinnedMessagesCount == 0 -> PinnedMessagesBannerState.Hidden + currentPinnedMessage == null -> PinnedMessagesBannerState.Loading(realPinnedMessagesCount = realPinnedMessagesCount) + else -> { + PinnedMessagesBannerState.Loaded( + currentPinnedMessage = currentPinnedMessage, + currentPinnedMessageIndex = currentPinnedMessageIndex, + knownPinnedMessagesCount = realPinnedMessagesCount, + eventSink = eventSink + ) + } + } + } + @OptIn(FlowPreview::class) @Composable private fun PinnedMessagesBannerItemsEffect( - onItemsChange: (List) -> Unit, + isFeatureEnabled: Boolean, + onItemsChange: (ImmutableList) -> Unit, + onTimelineFail: (Boolean) -> Unit, ) { - val isFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.PinnedEvents).collectAsState(initial = false) val updatedOnItemsChange by rememberUpdatedState(onItemsChange) + val updatedOnTimelineFail by rememberUpdatedState(onTimelineFail) + val networkStatus by networkMonitor.connectivity.collectAsState() - LaunchedEffect(isFeatureEnabled) { + LaunchedEffect(isFeatureEnabled, networkStatus) { if (!isFeatureEnabled) return@LaunchedEffect - val pinnedEventsTimeline = room.pinnedEventsTimeline().getOrNull() ?: 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) 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 b607855058..886cef81de 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,11 +16,40 @@ package io.element.android.features.messages.impl.pinned.banner -data class PinnedMessagesBannerState( - val pinnedMessagesCount: Int, - val currentPinnedMessageIndex: Int, - val currentPinnedMessage: PinnedMessagesBannerItem?, - val eventSink: (PinnedMessagesBannerEvents) -> Unit -) { - val displayBanner = pinnedMessagesCount > 0 && currentPinnedMessage != null +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 + data class Loading(val realPinnedMessagesCount: Int) : PinnedMessagesBannerState + data class Loaded( + val currentPinnedMessage: PinnedMessagesBannerItem, + val currentPinnedMessageIndex: Int, + val knownPinnedMessagesCount: Int, + val eventSink: (PinnedMessagesBannerEvents) -> Unit + ) : PinnedMessagesBannerState + + fun pinnedMessagesCount() = when (this) { + is Hidden -> 0 + is Loading -> realPinnedMessagesCount + is Loaded -> knownPinnedMessagesCount + } + + fun currentPinnedMessageIndex() = when (this) { + is Hidden -> 0 + is Loading -> 0 + 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 dcd6a4984a..bdcab879fe 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 @@ -24,26 +24,38 @@ 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( + realPinnedMessagesCount = 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, - currentPinnedMessageIndex = currentPinnedMessageIndex, +) = PinnedMessagesBannerState.Loaded( currentPinnedMessage = currentPinnedMessage, + currentPinnedMessageIndex = currentPinnedMessageIndex, + knownPinnedMessagesCount = 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 0ddbe1ed7b..b7524e43df 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 @@ -71,42 +71,52 @@ fun PinnedMessagesBannerView( onViewAllClick: () -> Unit, modifier: Modifier = Modifier, ) { - if (state.currentPinnedMessage == null) return + Box(modifier = modifier) { + when (state) { + PinnedMessagesBannerState.Hidden -> Unit + is PinnedMessagesBannerState.Loading -> { + PinnedMessagesBannerRow( + state = state, + onViewAllClick = onViewAllClick, + modifier = Modifier.clickable(onClick = { }), + ) + } + is PinnedMessagesBannerState.Loaded -> { + fun onClick() { + onClick(state.currentPinnedMessage.eventId) + state.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned) + } + PinnedMessagesBannerRow( + state = state, + onViewAllClick = onViewAllClick, + modifier = Modifier.clickable(onClick = ::onClick), + ) + } + } + } +} + +@Composable +fun PinnedMessagesBannerRow( + state: PinnedMessagesBannerState, + 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 { - onClick(state.currentPinnedMessage.eventId) - state.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned) - }, + .background(color = ElementTheme.colors.bgCanvasDefault) + .fillMaxWidth() + .drawBorder(borderColor) + .heightIn(min = 64.dp), 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( @@ -116,15 +126,56 @@ fun PinnedMessagesBannerView( modifier = Modifier.size(20.dp) ) PinnedMessageItem( - index = state.currentPinnedMessageIndex, - totalCount = state.pinnedMessagesCount, - message = state.currentPinnedMessage.formatted, + 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 = onViewAllClick) + 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, @@ -157,15 +208,15 @@ private fun PinIndicators( 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 + } + ) ) } } @@ -175,7 +226,7 @@ private fun PinIndicators( private fun PinnedMessageItem( index: Int, totalCount: Int, - message: AnnotatedString, + message: AnnotatedString?, modifier: Modifier = Modifier, ) { val countMessage = stringResource(id = CommonStrings.screen_room_pinned_banner_indicator, index + 1, totalCount) @@ -193,13 +244,15 @@ private fun PinnedMessageItem( 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, + ) + } } } 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/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" From 0390ede2ad9d16c392df0ae271d908c4aa1cd78d Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 6 Aug 2024 16:13:30 +0200 Subject: [PATCH 09/17] Pinned events: add tests on PinnedMessagesBannerPresenter --- .../banner/PinnedMessagesBannerPresenter.kt | 16 +- .../PinnedMessagesBannerPresenterTest.kt | 157 ++++++++++++++++-- 2 files changed, 154 insertions(+), 19 deletions(-) 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 59bf1c5a2f..6604114fc5 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 @@ -52,15 +52,15 @@ class PinnedMessagesBannerPresenter @Inject constructor( @Composable override fun present(): PinnedMessagesBannerState { val isFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.PinnedEvents).collectAsState(initial = false) - var timelineFailed by rememberSaveable { mutableStateOf(false) } - var pinnedItems by remember { - mutableStateOf>(persistentListOf()) - } + var hasTimelineFailedToLoad by rememberSaveable { mutableStateOf(false) } + var currentPinnedMessageIndex by rememberSaveable { mutableIntStateOf(0) } val knownPinnedMessagesCount by remember { room.roomInfoFlow.map { roomInfo -> roomInfo.pinnedEventIds.size } }.collectAsState(initial = null) - var currentPinnedMessageIndex by rememberSaveable { mutableIntStateOf(0) } + var pinnedItems by remember { + mutableStateOf>(persistentListOf()) + } PinnedMessagesBannerItemsEffect( isFeatureEnabled = isFeatureEnabled, @@ -72,7 +72,7 @@ class PinnedMessagesBannerPresenter @Inject constructor( pinnedItems = newItems }, onTimelineFail = { hasTimelineFailed -> - timelineFailed = hasTimelineFailed + hasTimelineFailedToLoad = hasTimelineFailed } ) @@ -90,7 +90,7 @@ class PinnedMessagesBannerPresenter @Inject constructor( return pinnedMessagesBannerState( isFeatureEnabled = isFeatureEnabled, - hasTimelineFailed = timelineFailed, + hasTimelineFailed = hasTimelineFailedToLoad, realPinnedMessagesCount = knownPinnedMessagesCount, pinnedItems = pinnedItems, currentPinnedMessageIndex = currentPinnedMessageIndex, @@ -117,7 +117,7 @@ class PinnedMessagesBannerPresenter @Inject constructor( PinnedMessagesBannerState.Loaded( currentPinnedMessage = currentPinnedMessage, currentPinnedMessageIndex = currentPinnedMessageIndex, - knownPinnedMessagesCount = realPinnedMessagesCount, + knownPinnedMessagesCount = pinnedItems.size, eventSink = eventSink ) } 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 baab17577d..33e1b74aba 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,13 +17,23 @@ 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.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService 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 @@ -31,23 +41,146 @@ 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.currentPinnedMessage).isNull() + 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) + } + } + + @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.knownPinnedMessagesCount).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(0) + assertThat(loadedState.knownPinnedMessagesCount).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.knownPinnedMessagesCount).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.knownPinnedMessagesCount).isEqualTo(2) + assertThat(loadedState.currentPinnedMessage.formatted.text).isEqualTo(messageContent1.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) + } } } @@ -56,9 +189,10 @@ class PinnedMessagesBannerPresenterTest { itemFactory: PinnedMessagesBannerItemFactory = PinnedMessagesBannerItemFactory( coroutineDispatchers = testCoroutineDispatchers(), formatter = FakePinnedMessagesBannerFormatter( - formatLambda = { event -> "Content ${event.content}" } + formatLambda = { event -> "${event.content}" } ) ), + networkMonitor: NetworkMonitor = FakeNetworkMonitor(), isFeatureEnabled: Boolean = true, ): PinnedMessagesBannerPresenter { val featureFlagService = FakeFeatureFlagService( @@ -69,7 +203,8 @@ class PinnedMessagesBannerPresenterTest { return PinnedMessagesBannerPresenter( room = room, itemFactory = itemFactory, - featureFlagService = featureFlagService + featureFlagService = featureFlagService, + networkMonitor = networkMonitor, ) } } From d7edc7e165be01cfcdd995c5c5d904d613197160 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 6 Aug 2024 16:51:53 +0200 Subject: [PATCH 10/17] Pinned events : fix click on banner --- .../impl/pinned/banner/PinnedMessagesBannerView.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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 b7524e43df..34b531ff55 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 @@ -82,15 +82,14 @@ fun PinnedMessagesBannerView( ) } is PinnedMessagesBannerState.Loaded -> { - fun onClick() { - onClick(state.currentPinnedMessage.eventId) - state.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned) - } - PinnedMessagesBannerRow( state = state, onViewAllClick = onViewAllClick, - modifier = Modifier.clickable(onClick = ::onClick), + modifier = Modifier.clickable( + onClick = { + onClick(state.currentPinnedMessage.eventId) + state.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned) + }), ) } } From c609a7d002681854f6224f76c9059d6f3c740d12 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 6 Aug 2024 19:05:08 +0200 Subject: [PATCH 11/17] Pinned events : use correct ordering logic --- .../banner/PinnedMessagesBannerPresenter.kt | 12 ++++----- .../banner/PinnedMessagesBannerState.kt | 7 +++--- .../pinned/banner/PinnedMessagesBannerView.kt | 25 ++++++++----------- .../PinnedMessagesBannerPresenterTest.kt | 16 ++++++------ 4 files changed, 28 insertions(+), 32 deletions(-) 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 6604114fc5..b04b6e656a 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 @@ -53,10 +53,10 @@ class PinnedMessagesBannerPresenter @Inject constructor( override fun present(): PinnedMessagesBannerState { val isFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.PinnedEvents).collectAsState(initial = false) var hasTimelineFailedToLoad by rememberSaveable { mutableStateOf(false) } - var currentPinnedMessageIndex by rememberSaveable { mutableIntStateOf(0) } + var currentPinnedMessageIndex by rememberSaveable { mutableIntStateOf(-1) } val knownPinnedMessagesCount by remember { room.roomInfoFlow.map { roomInfo -> roomInfo.pinnedEventIds.size } - }.collectAsState(initial = null) + }.collectAsState(initial = 0) var pinnedItems by remember { mutableStateOf>(persistentListOf()) @@ -66,8 +66,8 @@ class PinnedMessagesBannerPresenter @Inject constructor( isFeatureEnabled = isFeatureEnabled, onItemsChange = { newItems -> val pinnedMessageCount = newItems.size - if (currentPinnedMessageIndex >= pinnedMessageCount) { - currentPinnedMessageIndex = 0 + if (currentPinnedMessageIndex >= pinnedMessageCount || currentPinnedMessageIndex < 0) { + currentPinnedMessageIndex = pinnedMessageCount - 1 } pinnedItems = newItems }, @@ -102,7 +102,7 @@ class PinnedMessagesBannerPresenter @Inject constructor( private fun pinnedMessagesBannerState( isFeatureEnabled: Boolean, hasTimelineFailed: Boolean, - realPinnedMessagesCount: Int?, + realPinnedMessagesCount: Int, pinnedItems: ImmutableList, currentPinnedMessageIndex: Int, eventSink: (PinnedMessagesBannerEvents) -> Unit @@ -111,7 +111,7 @@ class PinnedMessagesBannerPresenter @Inject constructor( return when { !isFeatureEnabled -> PinnedMessagesBannerState.Hidden hasTimelineFailed -> PinnedMessagesBannerState.Hidden - realPinnedMessagesCount == null || realPinnedMessagesCount == 0 -> PinnedMessagesBannerState.Hidden + realPinnedMessagesCount == 0 -> PinnedMessagesBannerState.Hidden currentPinnedMessage == null -> PinnedMessagesBannerState.Loading(realPinnedMessagesCount = realPinnedMessagesCount) else -> { PinnedMessagesBannerState.Loaded( 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 886cef81de..c06003cc7c 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 @@ -26,13 +26,14 @@ import io.element.android.libraries.ui.strings.CommonStrings @Immutable sealed interface PinnedMessagesBannerState { data object Hidden : PinnedMessagesBannerState - data class Loading(val realPinnedMessagesCount: Int) : PinnedMessagesBannerState + sealed interface Visible : PinnedMessagesBannerState + data class Loading(val realPinnedMessagesCount: Int) : Visible data class Loaded( val currentPinnedMessage: PinnedMessagesBannerItem, val currentPinnedMessageIndex: Int, val knownPinnedMessagesCount: Int, val eventSink: (PinnedMessagesBannerEvents) -> Unit - ) : PinnedMessagesBannerState + ) : Visible fun pinnedMessagesCount() = when (this) { is Hidden -> 0 @@ -42,7 +43,7 @@ sealed interface PinnedMessagesBannerState { fun currentPinnedMessageIndex() = when (this) { is Hidden -> 0 - is Loading -> 0 + is Loading -> realPinnedMessagesCount - 1 is Loaded -> currentPinnedMessageIndex } 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 34b531ff55..3a8fa448ef 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 @@ -74,22 +74,11 @@ fun PinnedMessagesBannerView( Box(modifier = modifier) { when (state) { PinnedMessagesBannerState.Hidden -> Unit - is PinnedMessagesBannerState.Loading -> { + is PinnedMessagesBannerState.Visible -> { PinnedMessagesBannerRow( state = state, + onClick = onClick, onViewAllClick = onViewAllClick, - modifier = Modifier.clickable(onClick = { }), - ) - } - is PinnedMessagesBannerState.Loaded -> { - PinnedMessagesBannerRow( - state = state, - onViewAllClick = onViewAllClick, - modifier = Modifier.clickable( - onClick = { - onClick(state.currentPinnedMessage.eventId) - state.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned) - }), ) } } @@ -99,6 +88,7 @@ fun PinnedMessagesBannerView( @Composable fun PinnedMessagesBannerRow( state: PinnedMessagesBannerState, + onClick: (EventId) -> Unit, onViewAllClick: () -> Unit, modifier: Modifier = Modifier, ) { @@ -108,7 +98,13 @@ fun PinnedMessagesBannerRow( .background(color = ElementTheme.colors.bgCanvasDefault) .fillMaxWidth() .drawBorder(borderColor) - .heightIn(min = 64.dp), + .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) ) { @@ -202,7 +198,6 @@ private fun PinIndicators( state = lazyListState, verticalArrangement = spacedBy(2.dp), userScrollEnabled = false, - reverseLayout = true ) { items(pinsCount) { index -> Box( 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 33e1b74aba..6e78b2c446 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 @@ -138,14 +138,6 @@ class PinnedMessagesBannerPresenterTest { val presenter = createPinnedMessagesBannerPresenter(room = room) presenter.test { skipItems(2) - awaitItem().also { loadedState -> - loadedState as PinnedMessagesBannerState.Loaded - assertThat(loadedState.currentPinnedMessageIndex).isEqualTo(0) - assertThat(loadedState.knownPinnedMessagesCount).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) @@ -159,6 +151,14 @@ class PinnedMessagesBannerPresenterTest { assertThat(loadedState.currentPinnedMessageIndex).isEqualTo(0) assertThat(loadedState.knownPinnedMessagesCount).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.knownPinnedMessagesCount).isEqualTo(2) + assertThat(loadedState.currentPinnedMessage.formatted.text).isEqualTo(messageContent2.toString()) } } } From 04995cb26cfe8e4aa3733de9381548d1701c287b Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 7 Aug 2024 12:26:52 +0200 Subject: [PATCH 12/17] Pinned events : add some ui testing --- .../features/messages/impl/MessagesView.kt | 5 +- .../impl/timeline/TimelinePresenter.kt | 2 + .../messages/impl/MessagesViewTest.kt | 26 ++++++ .../banner/PinnedMessagesBannerViewTest.kt | 87 +++++++++++++++++++ 4 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerViewTest.kt 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 f365d917f0..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 @@ -74,6 +74,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer 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 @@ -401,13 +402,13 @@ private fun MessagesViewContent( nestedScrollConnection = scrollBehavior.nestedScrollConnection, ) AnimatedVisibility( - visible = state.pinnedMessagesBannerState != PinnedMessagesBannerState.Hidden && scrollBehavior.isVisible, + visible = state.pinnedMessagesBannerState is PinnedMessagesBannerState.Visible && scrollBehavior.isVisible, enter = expandVertically(), exit = shrinkVertically(), ) { fun focusOnPinnedEvent(eventId: EventId) { state.timelineState.eventSink( - TimelineEvents.FocusOnEvent(eventId = eventId, debounce = 200.milliseconds) + TimelineEvents.FocusOnEvent(eventId = eventId, debounce = FOCUS_ON_PINNED_EVENT_DEBOUNCE_DURATION_IN_MILLIS.milliseconds) ) } PinnedMessagesBannerView( 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 93667f3987..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 @@ -57,6 +57,8 @@ 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, 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 c88d2ffa28..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( 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 + ) + } +} From 49f4526338399aef361c4c53913c258d94e67956 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 7 Aug 2024 15:57:27 +0200 Subject: [PATCH 13/17] Pinned events : banner goes backward instead of forward --- .../impl/pinned/banner/PinnedMessagesBannerPresenter.kt | 6 +++--- gradle/libs.versions.toml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) 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 b04b6e656a..ba094dfffe 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 @@ -79,10 +79,10 @@ class PinnedMessagesBannerPresenter @Inject constructor( fun handleEvent(event: PinnedMessagesBannerEvents) { when (event) { is PinnedMessagesBannerEvents.MoveToNextPinned -> { - if (currentPinnedMessageIndex < pinnedItems.size - 1) { - currentPinnedMessageIndex++ + if (currentPinnedMessageIndex > 0) { + currentPinnedMessageIndex-- } else { - currentPinnedMessageIndex = 0 + currentPinnedMessageIndex = pinnedItems.size - 1 } } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 686f3b5b20..751bff685e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -163,7 +163,7 @@ jsoup = "org.jsoup:jsoup:1.18.1" appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0" timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.37" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.2.38" matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" } matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" } sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } From ac14845797be1ad42926c1299200b579f841afbd Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 7 Aug 2024 16:31:23 +0200 Subject: [PATCH 14/17] Pinned events: fix public compose method. --- .../messages/impl/pinned/banner/PinnedMessagesBannerView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 3a8fa448ef..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 @@ -86,7 +86,7 @@ fun PinnedMessagesBannerView( } @Composable -fun PinnedMessagesBannerRow( +private fun PinnedMessagesBannerRow( state: PinnedMessagesBannerState, onClick: (EventId) -> Unit, onViewAllClick: () -> Unit, From c96539163da0a8f91a9e3063a53b97045752f49d Mon Sep 17 00:00:00 2001 From: ElementBot Date: Wed, 7 Aug 2024 14:41:27 +0000 Subject: [PATCH 15/17] Update screenshots --- ...s.impl.pinned.banner_PinnedMessagesBannerView_Day_0_en.png | 4 ++-- ...s.impl.pinned.banner_PinnedMessagesBannerView_Day_1_en.png | 4 ++-- ...s.impl.pinned.banner_PinnedMessagesBannerView_Day_2_en.png | 4 ++-- ...s.impl.pinned.banner_PinnedMessagesBannerView_Day_3_en.png | 4 ++-- ...s.impl.pinned.banner_PinnedMessagesBannerView_Day_4_en.png | 4 ++-- ...s.impl.pinned.banner_PinnedMessagesBannerView_Day_5_en.png | 4 ++-- ...s.impl.pinned.banner_PinnedMessagesBannerView_Day_6_en.png | 3 +++ ...s.impl.pinned.banner_PinnedMessagesBannerView_Day_7_en.png | 3 +++ ...s.impl.pinned.banner_PinnedMessagesBannerView_Day_8_en.png | 3 +++ ...s.impl.pinned.banner_PinnedMessagesBannerView_Day_9_en.png | 3 +++ ...impl.pinned.banner_PinnedMessagesBannerView_Night_0_en.png | 4 ++-- ...impl.pinned.banner_PinnedMessagesBannerView_Night_1_en.png | 4 ++-- ...impl.pinned.banner_PinnedMessagesBannerView_Night_2_en.png | 4 ++-- ...impl.pinned.banner_PinnedMessagesBannerView_Night_3_en.png | 4 ++-- ...impl.pinned.banner_PinnedMessagesBannerView_Night_4_en.png | 4 ++-- ...impl.pinned.banner_PinnedMessagesBannerView_Night_5_en.png | 4 ++-- ...impl.pinned.banner_PinnedMessagesBannerView_Night_6_en.png | 3 +++ ...impl.pinned.banner_PinnedMessagesBannerView_Night_7_en.png | 3 +++ ...impl.pinned.banner_PinnedMessagesBannerView_Night_8_en.png | 3 +++ ...impl.pinned.banner_PinnedMessagesBannerView_Night_9_en.png | 3 +++ ...s.messages.impl.typing_MessagesViewWithTyping_Day_0_en.png | 4 ++-- ...s.messages.impl.typing_MessagesViewWithTyping_Day_1_en.png | 4 ++-- ...s.messages.impl.typing_MessagesViewWithTyping_Day_2_en.png | 4 ++-- ...messages.impl.typing_MessagesViewWithTyping_Night_0_en.png | 4 ++-- ...messages.impl.typing_MessagesViewWithTyping_Night_1_en.png | 4 ++-- ...messages.impl.typing_MessagesViewWithTyping_Night_2_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_0_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_10_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_11_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_12_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_1_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_3_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_4_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_5_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_6_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_7_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_8_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_9_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Night_0_en.png | 4 ++-- .../features.messages.impl_MessagesView_Night_10_en.png | 4 ++-- .../features.messages.impl_MessagesView_Night_11_en.png | 4 ++-- .../features.messages.impl_MessagesView_Night_12_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Night_1_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Night_3_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Night_4_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Night_5_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Night_6_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Night_7_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Night_8_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Night_9_en.png | 4 ++-- 50 files changed, 108 insertions(+), 84 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_6_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_7_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_8_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Day_9_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_6_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_7_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_8_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.pinned.banner_PinnedMessagesBannerView_Night_9_en.png 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 From 331413e8b4f856921b0c07f37e92e3e9981e2751 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 7 Aug 2024 19:00:41 +0200 Subject: [PATCH 16/17] Pinned events: make sure state is preserved --- .../pinned/IsPinnedMessagesFeatureEnabled.kt | 54 +++++++++++++++++++ .../banner/PinnedMessagesBannerPresenter.kt | 44 ++++++++------- .../PinnedMessagesBannerPresenterTest.kt | 9 +--- 3 files changed, 76 insertions(+), 31 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/IsPinnedMessagesFeatureEnabled.kt 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/PinnedMessagesBannerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/pinned/banner/PinnedMessagesBannerPresenter.kt index ba094dfffe..578e38eaa9 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 @@ -26,10 +26,9 @@ 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.featureflag.api.FeatureFlagService -import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.api.room.MatrixRoom import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf @@ -46,21 +45,20 @@ import kotlin.time.Duration.Companion.milliseconds class PinnedMessagesBannerPresenter @Inject constructor( private val room: MatrixRoom, private val itemFactory: PinnedMessagesBannerItemFactory, - private val featureFlagService: FeatureFlagService, + private val isFeatureEnabled: IsPinnedMessagesFeatureEnabled, private val networkMonitor: NetworkMonitor, ) : Presenter { + private val pinnedItems = mutableStateOf>(persistentListOf()) + @Composable override fun present(): PinnedMessagesBannerState { - val isFeatureEnabled by featureFlagService.isFeatureEnabledFlow(FeatureFlags.PinnedEvents).collectAsState(initial = false) - var hasTimelineFailedToLoad by rememberSaveable { mutableStateOf(false) } - var currentPinnedMessageIndex by rememberSaveable { mutableIntStateOf(-1) } + val isFeatureEnabled = isFeatureEnabled() val knownPinnedMessagesCount by remember { room.roomInfoFlow.map { roomInfo -> roomInfo.pinnedEventIds.size } }.collectAsState(initial = 0) - var pinnedItems by remember { - mutableStateOf>(persistentListOf()) - } + var hasTimelineFailedToLoad by rememberSaveable { mutableStateOf(false) } + var currentPinnedMessageIndex by rememberSaveable { mutableIntStateOf(-1) } PinnedMessagesBannerItemsEffect( isFeatureEnabled = isFeatureEnabled, @@ -69,7 +67,7 @@ class PinnedMessagesBannerPresenter @Inject constructor( if (currentPinnedMessageIndex >= pinnedMessageCount || currentPinnedMessageIndex < 0) { currentPinnedMessageIndex = pinnedMessageCount - 1 } - pinnedItems = newItems + pinnedItems.value = newItems }, onTimelineFail = { hasTimelineFailed -> hasTimelineFailedToLoad = hasTimelineFailed @@ -82,7 +80,7 @@ class PinnedMessagesBannerPresenter @Inject constructor( if (currentPinnedMessageIndex > 0) { currentPinnedMessageIndex-- } else { - currentPinnedMessageIndex = pinnedItems.size - 1 + currentPinnedMessageIndex = pinnedItems.value.size - 1 } } } @@ -92,7 +90,7 @@ class PinnedMessagesBannerPresenter @Inject constructor( isFeatureEnabled = isFeatureEnabled, hasTimelineFailed = hasTimelineFailedToLoad, realPinnedMessagesCount = knownPinnedMessagesCount, - pinnedItems = pinnedItems, + pinnedItems = pinnedItems.value, currentPinnedMessageIndex = currentPinnedMessageIndex, eventSink = ::handleEvent ) @@ -111,16 +109,14 @@ class PinnedMessagesBannerPresenter @Inject constructor( return when { !isFeatureEnabled -> PinnedMessagesBannerState.Hidden hasTimelineFailed -> PinnedMessagesBannerState.Hidden + currentPinnedMessage != null -> PinnedMessagesBannerState.Loaded( + currentPinnedMessage = currentPinnedMessage, + currentPinnedMessageIndex = currentPinnedMessageIndex, + knownPinnedMessagesCount = pinnedItems.size, + eventSink = eventSink + ) realPinnedMessagesCount == 0 -> PinnedMessagesBannerState.Hidden - currentPinnedMessage == null -> PinnedMessagesBannerState.Loading(realPinnedMessagesCount = realPinnedMessagesCount) - else -> { - PinnedMessagesBannerState.Loaded( - currentPinnedMessage = currentPinnedMessage, - currentPinnedMessageIndex = currentPinnedMessageIndex, - knownPinnedMessagesCount = pinnedItems.size, - eventSink = eventSink - ) - } + else -> PinnedMessagesBannerState.Loading(realPinnedMessagesCount = realPinnedMessagesCount) } } @@ -136,8 +132,10 @@ class PinnedMessagesBannerPresenter @Inject constructor( val networkStatus by networkMonitor.connectivity.collectAsState() LaunchedEffect(isFeatureEnabled, networkStatus) { - if (!isFeatureEnabled) return@LaunchedEffect - + if (!isFeatureEnabled) { + updatedOnItemsChange(persistentListOf()) + return@LaunchedEffect + } val pinnedEventsTimeline = room.pinnedEventsTimeline() .onFailure { updatedOnTimelineFail(true) } .onSuccess { updatedOnTimelineFail(false) } 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 6e78b2c446..fbbf40b7f0 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 @@ -20,8 +20,6 @@ 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.featureflag.api.FeatureFlags -import io.element.android.libraries.featureflag.test.FakeFeatureFlagService 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 @@ -195,15 +193,10 @@ class PinnedMessagesBannerPresenterTest { networkMonitor: NetworkMonitor = FakeNetworkMonitor(), isFeatureEnabled: Boolean = true, ): PinnedMessagesBannerPresenter { - val featureFlagService = FakeFeatureFlagService( - initialState = mapOf( - FeatureFlags.PinnedEvents.key to isFeatureEnabled - ) - ) return PinnedMessagesBannerPresenter( room = room, itemFactory = itemFactory, - featureFlagService = featureFlagService, + isFeatureEnabled = { isFeatureEnabled }, networkMonitor = networkMonitor, ) } From a5d633a02f33f3439089d2da05ecb88c6413f0e7 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 8 Aug 2024 14:51:49 +0200 Subject: [PATCH 17/17] Pinned events : minor code quality changes --- .../banner/PinnedMessagesBannerPresenter.kt | 18 +++++++----------- .../pinned/banner/PinnedMessagesBannerState.kt | 10 +++++----- .../PinnedMessagesBannerStateProvider.kt | 4 ++-- .../PinnedMessagesBannerPresenterTest.kt | 8 ++++---- 4 files changed, 18 insertions(+), 22 deletions(-) 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 578e38eaa9..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 @@ -53,7 +53,7 @@ class PinnedMessagesBannerPresenter @Inject constructor( @Composable override fun present(): PinnedMessagesBannerState { val isFeatureEnabled = isFeatureEnabled() - val knownPinnedMessagesCount by remember { + val expectedPinnedMessagesCount by remember { room.roomInfoFlow.map { roomInfo -> roomInfo.pinnedEventIds.size } }.collectAsState(initial = 0) @@ -77,11 +77,7 @@ class PinnedMessagesBannerPresenter @Inject constructor( fun handleEvent(event: PinnedMessagesBannerEvents) { when (event) { is PinnedMessagesBannerEvents.MoveToNextPinned -> { - if (currentPinnedMessageIndex > 0) { - currentPinnedMessageIndex-- - } else { - currentPinnedMessageIndex = pinnedItems.value.size - 1 - } + currentPinnedMessageIndex = (currentPinnedMessageIndex - 1).mod(pinnedItems.value.size) } } } @@ -89,7 +85,7 @@ class PinnedMessagesBannerPresenter @Inject constructor( return pinnedMessagesBannerState( isFeatureEnabled = isFeatureEnabled, hasTimelineFailed = hasTimelineFailedToLoad, - realPinnedMessagesCount = knownPinnedMessagesCount, + expectedPinnedMessagesCount = expectedPinnedMessagesCount, pinnedItems = pinnedItems.value, currentPinnedMessageIndex = currentPinnedMessageIndex, eventSink = ::handleEvent @@ -100,7 +96,7 @@ class PinnedMessagesBannerPresenter @Inject constructor( private fun pinnedMessagesBannerState( isFeatureEnabled: Boolean, hasTimelineFailed: Boolean, - realPinnedMessagesCount: Int, + expectedPinnedMessagesCount: Int, pinnedItems: ImmutableList, currentPinnedMessageIndex: Int, eventSink: (PinnedMessagesBannerEvents) -> Unit @@ -112,11 +108,11 @@ class PinnedMessagesBannerPresenter @Inject constructor( currentPinnedMessage != null -> PinnedMessagesBannerState.Loaded( currentPinnedMessage = currentPinnedMessage, currentPinnedMessageIndex = currentPinnedMessageIndex, - knownPinnedMessagesCount = pinnedItems.size, + loadedPinnedMessagesCount = pinnedItems.size, eventSink = eventSink ) - realPinnedMessagesCount == 0 -> PinnedMessagesBannerState.Hidden - else -> PinnedMessagesBannerState.Loading(realPinnedMessagesCount = realPinnedMessagesCount) + expectedPinnedMessagesCount == 0 -> PinnedMessagesBannerState.Hidden + else -> PinnedMessagesBannerState.Loading(expectedPinnedMessagesCount = expectedPinnedMessagesCount) } } 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 c06003cc7c..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 @@ -27,23 +27,23 @@ import io.element.android.libraries.ui.strings.CommonStrings sealed interface PinnedMessagesBannerState { data object Hidden : PinnedMessagesBannerState sealed interface Visible : PinnedMessagesBannerState - data class Loading(val realPinnedMessagesCount: Int) : Visible + data class Loading(val expectedPinnedMessagesCount: Int) : Visible data class Loaded( val currentPinnedMessage: PinnedMessagesBannerItem, val currentPinnedMessageIndex: Int, - val knownPinnedMessagesCount: Int, + val loadedPinnedMessagesCount: Int, val eventSink: (PinnedMessagesBannerEvents) -> Unit ) : Visible fun pinnedMessagesCount() = when (this) { is Hidden -> 0 - is Loading -> realPinnedMessagesCount - is Loaded -> knownPinnedMessagesCount + is Loading -> expectedPinnedMessagesCount + is Loaded -> loadedPinnedMessagesCount } fun currentPinnedMessageIndex() = when (this) { is Hidden -> 0 - is Loading -> realPinnedMessagesCount - 1 + is Loading -> expectedPinnedMessagesCount - 1 is Loaded -> currentPinnedMessageIndex } 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 bdcab879fe..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 @@ -42,7 +42,7 @@ internal fun aHiddenPinnedMessagesBannerState() = PinnedMessagesBannerState.Hidd internal fun aLoadingPinnedMessagesBannerState( knownPinnedMessagesCount: Int = 4 ) = PinnedMessagesBannerState.Loading( - realPinnedMessagesCount = knownPinnedMessagesCount + expectedPinnedMessagesCount = knownPinnedMessagesCount ) internal fun aLoadedPinnedMessagesBannerState( @@ -56,6 +56,6 @@ internal fun aLoadedPinnedMessagesBannerState( ) = PinnedMessagesBannerState.Loaded( currentPinnedMessage = currentPinnedMessage, currentPinnedMessageIndex = currentPinnedMessageIndex, - knownPinnedMessagesCount = knownPinnedMessagesCount, + loadedPinnedMessagesCount = knownPinnedMessagesCount, eventSink = eventSink ) 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 fbbf40b7f0..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 @@ -99,7 +99,7 @@ class PinnedMessagesBannerPresenterTest { skipItems(2) val loadedState = awaitItem() as PinnedMessagesBannerState.Loaded assertThat(loadedState.currentPinnedMessageIndex).isEqualTo(0) - assertThat(loadedState.knownPinnedMessagesCount).isEqualTo(1) + assertThat(loadedState.loadedPinnedMessagesCount).isEqualTo(1) assertThat(loadedState.currentPinnedMessage.formatted.text).isEqualTo(messageContent.toString()) } } @@ -139,7 +139,7 @@ class PinnedMessagesBannerPresenterTest { awaitItem().also { loadedState -> loadedState as PinnedMessagesBannerState.Loaded assertThat(loadedState.currentPinnedMessageIndex).isEqualTo(1) - assertThat(loadedState.knownPinnedMessagesCount).isEqualTo(2) + assertThat(loadedState.loadedPinnedMessagesCount).isEqualTo(2) assertThat(loadedState.currentPinnedMessage.formatted.text).isEqualTo(messageContent2.toString()) loadedState.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned) } @@ -147,7 +147,7 @@ class PinnedMessagesBannerPresenterTest { awaitItem().also { loadedState -> loadedState as PinnedMessagesBannerState.Loaded assertThat(loadedState.currentPinnedMessageIndex).isEqualTo(0) - assertThat(loadedState.knownPinnedMessagesCount).isEqualTo(2) + assertThat(loadedState.loadedPinnedMessagesCount).isEqualTo(2) assertThat(loadedState.currentPinnedMessage.formatted.text).isEqualTo(messageContent1.toString()) loadedState.eventSink(PinnedMessagesBannerEvents.MoveToNextPinned) } @@ -155,7 +155,7 @@ class PinnedMessagesBannerPresenterTest { awaitItem().also { loadedState -> loadedState as PinnedMessagesBannerState.Loaded assertThat(loadedState.currentPinnedMessageIndex).isEqualTo(1) - assertThat(loadedState.knownPinnedMessagesCount).isEqualTo(2) + assertThat(loadedState.loadedPinnedMessagesCount).isEqualTo(2) assertThat(loadedState.currentPinnedMessage.formatted.text).isEqualTo(messageContent2.toString()) } }