From 35c8eb8b155683d84a8aada2ce652389289de30d Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 11 Jul 2023 12:50:50 +0200 Subject: [PATCH 01/59] Timeline: improve "jump to bottom" button --- .../messages/impl/timeline/TimelineView.kt | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) 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 bb33981a64..a8da9d5543 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 @@ -21,6 +21,7 @@ package io.element.android.features.messages.impl.timeline import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.animateContentSize +import androidx.compose.animation.core.tween import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.foundation.layout.Box @@ -278,8 +279,8 @@ internal fun BoxScope.TimelineScrollHelper( .align(Alignment.BottomEnd) .padding(end = 24.dp, bottom = 12.dp), visible = showScrollToBottomButton || LocalInspectionMode.current, - enter = scaleIn(), - exit = scaleOut(), + enter = scaleIn(animationSpec = tween(100)), + exit = scaleOut(animationSpec = tween(100)), ) { FloatingActionButton( onClick = { @@ -293,14 +294,7 @@ internal fun BoxScope.TimelineScrollHelper( }, elevation = FloatingActionButtonDefaults.elevation(4.dp, 4.dp, 4.dp, 4.dp), shape = CircleShape, - modifier = Modifier - .shadow( - elevation = 4.dp, - shape = CircleShape, - ambientColor = ElementTheme.materialColors.primary, - spotColor = ElementTheme.materialColors.primary, - ) - .size(36.dp), + modifier = Modifier.size(36.dp), containerColor = ElementTheme.colors.bgSubtleSecondary, contentColor = ElementTheme.colors.iconSecondary ) { From 51a7b8edd349f5453e7ea9efe9233d28172bdb1d Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 11 Jul 2023 12:51:01 +0200 Subject: [PATCH 02/59] Timeline: remove duplicated code --- .../features/messages/impl/timeline/TimelineView.kt | 7 ------- 1 file changed, 7 deletions(-) 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 a8da9d5543..4e0ca6124b 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 @@ -101,13 +101,6 @@ fun TimelineView( // TODO implement this logic once we have support to 'jump to event X' in sliding sync } - // Send an event to the presenter when the scrolling is finished, with the first visible index at the bottom. - val firstVisibleIndex by remember { derivedStateOf { lazyListState.firstVisibleItemIndex } } - val isScrollFinished by remember { derivedStateOf { !lazyListState.isScrollInProgress } } - LaunchedEffect(firstVisibleIndex, isScrollFinished) { - if (!isScrollFinished) return@LaunchedEffect - state.eventSink(TimelineEvents.OnScrollFinished(firstVisibleIndex)) - } Box(modifier = modifier) { LazyColumn( From dc4e36147d49af1ac619c3232752876b051a47da Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 13 Jul 2023 13:05:56 +0200 Subject: [PATCH 03/59] Timeline: introduce origin on timeline items --- .../impl/timeline/TimelineStateProvider.kt | 1 + .../event/TimelineItemEventFactory.kt | 1 + .../impl/timeline/model/TimelineItem.kt | 2 ++ .../timeline/item/event/EventTimelineItem.kt | 1 + .../item/event/TimelineItemEventOrigin.kt | 21 +++++++++++++++++++ .../item/event/EventTimelineItemMapper.kt | 11 ++++++++++ 6 files changed, 37 insertions(+) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/TimelineItemEventOrigin.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index c92658f7e8..b3c6f01303 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -126,6 +126,7 @@ internal fun aTimelineItemEvent( localSendState = sendState, inReplyTo = inReplyTo, debugInfo = debugInfo, + origin = null ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt index 9a09c77a34..6bc5df1e79 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt @@ -85,6 +85,7 @@ class TimelineItemEventFactory @Inject constructor( localSendState = currentTimelineItem.event.localSendState, inReplyTo = currentTimelineItem.event.inReplyTo(), debugInfo = currentTimelineItem.event.debugInfo, + origin = currentTimelineItem.event.origin, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt index 21b7e8607f..7f95b30409 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt @@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState +import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin import kotlinx.collections.immutable.ImmutableList @Immutable @@ -65,6 +66,7 @@ sealed interface TimelineItem { val localSendState: LocalEventSendState?, val inReplyTo: InReplyTo?, val debugInfo: TimelineItemDebugInfo, + val origin: TimelineItemEventOrigin?, ) : TimelineItem { val showSenderInformation = groupPosition.isNew() && !isMine diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt index 854d38b7dd..67dd580426 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt @@ -34,6 +34,7 @@ data class EventTimelineItem( val timestamp: Long, val content: EventContent, val debugInfo: TimelineItemDebugInfo, + val origin: TimelineItemEventOrigin?, ) { fun inReplyTo(): InReplyTo? { return (content as? MessageContent)?.inReplyTo diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/TimelineItemEventOrigin.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/TimelineItemEventOrigin.kt new file mode 100644 index 0000000000..0f906e6719 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/TimelineItemEventOrigin.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.matrix.api.timeline.item.event + +enum class TimelineItemEventOrigin { + LOCAL, SYNC, PAGINATION; +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt index b4887faf68..ec6ca0897b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt @@ -19,11 +19,13 @@ package io.element.android.libraries.matrix.impl.timeline.item.event import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo +import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails import org.matrix.rustcomponents.sdk.Reaction +import org.matrix.rustcomponents.sdk.EventItemOrigin as RustEventItemOrigin import org.matrix.rustcomponents.sdk.EventSendState as RustEventSendState import org.matrix.rustcomponents.sdk.EventTimelineItem as RustEventTimelineItem import org.matrix.rustcomponents.sdk.EventTimelineItemDebugInfo as RustEventTimelineItemDebugInfo @@ -46,6 +48,7 @@ class EventTimelineItemMapper(private val contentMapper: TimelineEventContentMap timestamp = it.timestamp().toLong(), content = contentMapper.map(it.content()), debugInfo = it.debugInfo().map(), + origin = it.origin()?.map() ) } } @@ -90,3 +93,11 @@ private fun RustEventTimelineItemDebugInfo.map(): TimelineItemDebugInfo { latestEditedJson = latestEditJson, ) } + +private fun RustEventItemOrigin.map(): TimelineItemEventOrigin { + return when (this) { + RustEventItemOrigin.LOCAL -> TimelineItemEventOrigin.LOCAL + RustEventItemOrigin.SYNC -> TimelineItemEventOrigin.SYNC + RustEventItemOrigin.PAGINATION -> TimelineItemEventOrigin.PAGINATION + } +} From b9676c1ec0cf7ad85c5707e1b9f697e3bb5b7449 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 13 Jul 2023 13:08:25 +0200 Subject: [PATCH 04/59] Timeline : improve auto-scroll --- .../impl/timeline/TimelinePresenter.kt | 61 ++++++++++++---- .../messages/impl/timeline/TimelineState.kt | 1 + .../impl/timeline/TimelineStateProvider.kt | 3 +- .../messages/impl/timeline/TimelineView.kt | 69 ++++++++++--------- 4 files changed, 87 insertions(+), 47 deletions(-) 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 0833ff2205..353d94e390 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 @@ -22,21 +22,25 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MessageEventType +import io.element.android.libraries.matrix.api.timeline.item.event.TimelineItemEventOrigin import io.element.android.libraries.matrix.ui.room.canSendEventAsState import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject private const val backPaginationEventLimit = 20 @@ -45,42 +49,57 @@ private const val backPaginationPageSize = 50 class TimelinePresenter @Inject constructor( private val timelineItemsFactory: TimelineItemsFactory, private val room: MatrixRoom, + private val dispatchers: CoroutineDispatchers, + private val appScope: CoroutineScope, ) : Presenter { private val timeline = room.timeline @Composable override fun present(): TimelineState { - val localCoroutineScope = rememberCoroutineScope() + val localScope = rememberCoroutineScope() val highlightedEventId: MutableState = rememberSaveable { mutableStateOf(null) } - var lastReadMarkerIndex by rememberSaveable { mutableStateOf(Int.MAX_VALUE) } - var lastReadMarkerId by rememberSaveable { mutableStateOf(null) } + var lastReadReceiptIndex by rememberSaveable { mutableStateOf(Int.MAX_VALUE) } + var lastReadReceiptId by rememberSaveable { mutableStateOf(null) } val timelineItems by timelineItemsFactory.collectItemsAsState() val paginationState by timeline.paginationState.collectAsState() - val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val userHasPermissionToSendMessage by room.canSendEventAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value) + val prevMostRecentItemId = rememberSaveable { mutableStateOf(null) } + val hasNewItems = remember { mutableStateOf(false) } + + fun CoroutineScope.sendReadReceiptIfNeeded(firstVisibleIndex: Int) = launch(dispatchers.computation) { + // Get last valid EventId seen by the user, as the first index might refer to a Virtual item + val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems) + if (eventId != null && firstVisibleIndex <= lastReadReceiptIndex && eventId != lastReadReceiptId) { + lastReadReceiptIndex = firstVisibleIndex + lastReadReceiptId = eventId + timeline.sendReadReceipt(eventId) + } + } + fun handleEvents(event: TimelineEvents) { when (event) { - TimelineEvents.LoadMore -> localCoroutineScope.paginateBackwards() + TimelineEvents.LoadMore -> localScope.paginateBackwards() is TimelineEvents.SetHighlightedEvent -> highlightedEventId.value = event.eventId is TimelineEvents.OnScrollFinished -> { - // Get last valid EventId seen by the user, as the first index might refer to a Virtual item - val eventId = getLastEventIdBeforeOrAt(event.firstIndex, timelineItems) ?: return - if (event.firstIndex <= lastReadMarkerIndex && eventId != lastReadMarkerId) { - lastReadMarkerIndex = event.firstIndex - lastReadMarkerId = eventId - localCoroutineScope.sendReadReceipt(eventId) + if (event.firstIndex == 0) { + hasNewItems.value = false } + appScope.sendReadReceiptIfNeeded(event.firstIndex) } } } + LaunchedEffect(timelineItems.size) { + computeHasNewItems(timelineItems, prevMostRecentItemId, hasNewItems) + } + LaunchedEffect(Unit) { timeline .timelineItems @@ -98,10 +117,26 @@ class TimelinePresenter @Inject constructor( canReply = userHasPermissionToSendMessage, paginationState = paginationState, timelineItems = timelineItems, + hasNewItems = hasNewItems.value, eventSink = ::handleEvents ) } + private suspend fun computeHasNewItems( + timelineItems: ImmutableList, + prevMostRecentItemId: MutableState, + hasNewItemsState: MutableState + ) = withContext(dispatchers.computation) { + val newMostRecentItem = timelineItems.firstOrNull() + val prevMostRecentItemIdValue = prevMostRecentItemId.value + val newMostRecentItemId = newMostRecentItem?.identifier() + hasNewItemsState.value = prevMostRecentItemIdValue != null && + newMostRecentItem is TimelineItem.Event && + newMostRecentItem.origin != TimelineItemEventOrigin.PAGINATION && + newMostRecentItemId != prevMostRecentItemIdValue + prevMostRecentItemId.value = newMostRecentItemId + } + private fun getLastEventIdBeforeOrAt(index: Int, items: ImmutableList): EventId? { for (item in items.subList(index, items.count())) { if (item is TimelineItem.Event) { @@ -114,8 +149,4 @@ class TimelinePresenter @Inject constructor( private fun CoroutineScope.paginateBackwards() = launch { timeline.paginateBackwards(backPaginationEventLimit, backPaginationPageSize) } - - private fun CoroutineScope.sendReadReceipt(eventId: EventId) = launch { - timeline.sendReadReceipt(eventId) - } } 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 0aa1bd0160..ab5874d39c 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 @@ -28,5 +28,6 @@ data class TimelineState( val highlightedEventId: EventId?, val canReply: Boolean, val paginationState: MatrixTimeline.PaginationState, + val hasNewItems: Boolean, val eventSink: (TimelineEvents) -> Unit ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index b3c6f01303..4e62b8649f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -44,7 +44,8 @@ fun aTimelineState(timelineItems: ImmutableList = persistentListOf paginationState = MatrixTimeline.PaginationState(isBackPaginating = false, hasMoreToLoadBackwards = true), highlightedEventId = null, canReply = true, - eventSink = {} + hasNewItems = false, + eventSink = {}, ) internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList { 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 1e00bea1a9..3bbaffab9f 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 @@ -49,7 +49,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.tooling.preview.Preview @@ -72,7 +71,6 @@ import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.theme.ElementTheme -import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.launch @Composable @@ -141,8 +139,8 @@ fun TimelineView( TimelineScrollHelper( lazyListState = lazyListState, - timelineItems = state.timelineItems, - onScrollFinishedAt = ::onScrollFinishedAt, + hasNewItems = state.hasNewItems, + onScrollFinishedAt = ::onScrollFinishedAt ) } } @@ -238,53 +236,62 @@ fun TimelineItemRow( } @Composable -internal fun BoxScope.TimelineScrollHelper( +private fun BoxScope.TimelineScrollHelper( lazyListState: LazyListState, - timelineItems: ImmutableList, - onScrollFinishedAt: (Int) -> Unit = {}, + hasNewItems: Boolean, + onScrollFinishedAt: (Int) -> Unit, ) { val coroutineScope = rememberCoroutineScope() - val firstVisibleItemIndex by remember { derivedStateOf { lazyListState.firstVisibleItemIndex } } val isScrollFinished by remember { derivedStateOf { !lazyListState.isScrollInProgress } } - val shouldAutoScrollToBottom by remember { derivedStateOf { lazyListState.firstVisibleItemIndex < 2 } } - val showScrollToBottomButton by remember { derivedStateOf { lazyListState.firstVisibleItemIndex > 0 } } + val canAutoScroll by remember { derivedStateOf { lazyListState.firstVisibleItemIndex < 3 } } - LaunchedEffect(timelineItems, firstVisibleItemIndex) { - if (!isScrollFinished) return@LaunchedEffect - - // Auto-scroll when new timeline items appear - if (shouldAutoScrollToBottom) { + LaunchedEffect(canAutoScroll, hasNewItems) { + val shouldAutoScroll = isScrollFinished && canAutoScroll && hasNewItems + if (shouldAutoScroll) { coroutineScope.launch { lazyListState.animateScrollToItem(0) } } } - LaunchedEffect(isScrollFinished) { - if (!isScrollFinished) return@LaunchedEffect - // Notify the parent composable about the first visible item index when scrolling finishes - onScrollFinishedAt(firstVisibleItemIndex) + LaunchedEffect(isScrollFinished) { + if (isScrollFinished) { + // Notify the parent composable about the first visible item index when scrolling finishes + onScrollFinishedAt(lazyListState.firstVisibleItemIndex) + } } - // Jump to bottom button (display also in previews) - AnimatedVisibility( + JumpToBottomButton( + isVisible = !canAutoScroll, modifier = Modifier .align(Alignment.BottomEnd) .padding(end = 24.dp, bottom = 12.dp), - visible = showScrollToBottomButton || LocalInspectionMode.current, + onClick = { + coroutineScope.launch { + if (lazyListState.firstVisibleItemIndex > 10) { + lazyListState.scrollToItem(0) + } else { + lazyListState.animateScrollToItem(0) + } + } + } + ) +} + +@Composable +private fun JumpToBottomButton( + isVisible: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + AnimatedVisibility( + modifier = modifier, + visible = isVisible || LocalInspectionMode.current, enter = scaleIn(animationSpec = tween(100)), exit = scaleOut(animationSpec = tween(100)), ) { FloatingActionButton( - onClick = { - coroutineScope.launch { - if (firstVisibleItemIndex > 10) { - lazyListState.scrollToItem(0) - } else { - lazyListState.animateScrollToItem(0) - } - } - }, + onClick = onClick, elevation = FloatingActionButtonDefaults.elevation(4.dp, 4.dp, 4.dp, 4.dp), shape = CircleShape, modifier = Modifier.size(36.dp), From f80f6f5bd9cee9c78c64cf76ac4746d3e3290fad Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 13 Jul 2023 17:09:20 +0200 Subject: [PATCH 05/59] Timeline: fix some tests and a one more --- .../impl/timeline/TimelineStateProvider.kt | 5 +- .../messages/impl/timeline/TimelineView.kt | 1 + .../messages/MessagesPresenterTest.kt | 2 + .../messages/fixtures/aMessageEvent.kt | 1 + .../timeline/TimelinePresenterTest.kt | 123 +++++++++++------- .../groups/TimelineItemGrouperTest.kt | 1 + .../matrix/test/room/RoomSummaryFixture.kt | 24 +++- .../test/timeline/FakeMatrixTimeline.kt | 14 +- .../android/tests/testutils/LongTask.kt | 20 +++ 9 files changed, 137 insertions(+), 54 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index 4e62b8649f..5b5319f952 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -30,8 +30,8 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo -import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo +import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList @@ -154,13 +154,14 @@ internal fun aTimelineItemDebugInfo( model, originalJson, latestEditedJson ) -fun aGroupedEvents(): TimelineItem.GroupedEvents { +fun aGroupedEvents(id: Long = 0): TimelineItem.GroupedEvents { val event = aTimelineItemEvent( isMine = true, content = aTimelineItemStateEventContent(), groupPosition = TimelineItemGroupPosition.None ) return TimelineItem.GroupedEvents( + id = id.toString(), events = listOf( event, event, 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 3bbaffab9f..0404edf7db 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 @@ -262,6 +262,7 @@ private fun BoxScope.TimelineScrollHelper( } JumpToBottomButton( + // Use inverse of canAutoScroll otherwise we might briefly see the before the scroll animation is triggered isVisible = !canAutoScroll, modifier = Modifier .align(Alignment.BottomEnd) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index 53034131dc..abbbeccf37 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -572,6 +572,8 @@ class MessagesPresenterTest { val timelinePresenter = TimelinePresenter( timelineItemsFactory = aTimelineItemsFactory(), room = matrixRoom, + dispatchers = coroutineDispatchers, + appScope = this ) val buildMeta = aBuildMeta() val actionListPresenter = ActionListPresenter(buildMeta = buildMeta) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/aMessageEvent.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/aMessageEvent.kt index 088df6060f..4f1edcb64f 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/aMessageEvent.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/aMessageEvent.kt @@ -52,4 +52,5 @@ internal fun aMessageEvent( localSendState = sendState, inReplyTo = inReplyTo, debugInfo = debugInfo, + origin = null ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt index 2835155b14..ad6e41e483 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt @@ -23,22 +23,25 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.fixtures.aTimelineItemsFactory import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelinePresenter +import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory +import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.aMessageContent import io.element.android.libraries.matrix.test.room.anEventTimelineItem import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline +import io.element.android.tests.testutils.awaitWithLatch +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test class TimelinePresenterTest { @Test fun `present - initial state`() = runTest { - val presenter = TimelinePresenter( - timelineItemsFactory = aTimelineItemsFactory(), - room = FakeMatrixRoom(), - ) + val presenter = createTimelinePresenter() moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -51,10 +54,7 @@ class TimelinePresenterTest { @Test fun `present - load more`() = runTest { - val presenter = TimelinePresenter( - timelineItemsFactory = aTimelineItemsFactory(), - room = FakeMatrixRoom(), - ) + val presenter = createTimelinePresenter() moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -73,10 +73,7 @@ class TimelinePresenterTest { @Test fun `present - set highlighted event`() = runTest { - val presenter = TimelinePresenter( - timelineItemsFactory = aTimelineItemsFactory(), - room = FakeMatrixRoom(), - ) + val presenter = createTimelinePresenter() moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -94,70 +91,106 @@ class TimelinePresenterTest { @Test fun `present - on scroll finished send read receipt if an event is before the index`() = runTest { - val timeline = FakeMatrixTimeline() - val timelineItemsFactory = aTimelineItemsFactory().apply { - replaceWith(listOf(MatrixTimelineItem.Event(0, anEventTimelineItem()))) - } - val room = FakeMatrixRoom(matrixTimeline = timeline) - val presenter = TimelinePresenter( - timelineItemsFactory = timelineItemsFactory, - room = room, + val timeline = FakeMatrixTimeline( + initialTimelineItems = listOf( + MatrixTimelineItem.Event(0, anEventTimelineItem()) + ) ) + val presenter = createTimelinePresenter(timeline) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { assertThat(timeline.sendReadReceiptCount).isEqualTo(0) val initialState = awaitItem() - - initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0)) - + awaitWithLatch { latch -> + timeline.sendReadReceiptLatch = latch + initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0)) + } assertThat(timeline.sendReadReceiptCount).isEqualTo(1) + cancelAndIgnoreRemainingEvents() } } @Test fun `present - on scroll finished will not send read receipt no event is before the index`() = runTest { - val timeline = FakeMatrixTimeline() - val timelineItemsFactory = aTimelineItemsFactory().apply { - replaceWith(listOf(MatrixTimelineItem.Event(0, anEventTimelineItem()))) - } - val room = FakeMatrixRoom(matrixTimeline = timeline) - val presenter = TimelinePresenter( - timelineItemsFactory = timelineItemsFactory, - room = room, + val timeline = FakeMatrixTimeline( + initialTimelineItems = listOf( + MatrixTimelineItem.Event(0, anEventTimelineItem()) + ) ) + val presenter = createTimelinePresenter(timeline) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { assertThat(timeline.sendReadReceiptCount).isEqualTo(0) val initialState = awaitItem() - - initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1)) - + awaitWithLatch { latch -> + timeline.sendReadReceiptLatch = latch + initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1)) + } assertThat(timeline.sendReadReceiptCount).isEqualTo(0) + cancelAndIgnoreRemainingEvents() } } @Test fun `present - on scroll finished will not send read receipt only virtual events exist before the index`() = runTest { - val timeline = FakeMatrixTimeline() - val timelineItemsFactory = aTimelineItemsFactory().apply { - replaceWith(listOf(MatrixTimelineItem.Virtual(0, VirtualTimelineItem.ReadMarker))) - } - val room = FakeMatrixRoom(matrixTimeline = timeline) - val presenter = TimelinePresenter( - timelineItemsFactory = timelineItemsFactory, - room = room, + val timeline = FakeMatrixTimeline( + initialTimelineItems = listOf( + MatrixTimelineItem.Virtual(0, VirtualTimelineItem.ReadMarker) + ) ) + val presenter = createTimelinePresenter(timeline) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { assertThat(timeline.sendReadReceiptCount).isEqualTo(0) val initialState = awaitItem() - - initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1)) - + awaitWithLatch { latch -> + timeline.sendReadReceiptLatch = latch + initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0)) + } assertThat(timeline.sendReadReceiptCount).isEqualTo(0) + cancelAndIgnoreRemainingEvents() } } + + @Test + fun `present - covers hasNewItems scenarios`() = runTest { + val timeline = FakeMatrixTimeline() + val presenter = createTimelinePresenter(timeline) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.hasNewItems).isFalse() + assertThat(initialState.timelineItems.size).isEqualTo(0) + timeline.updateTimelineItems { + listOf(MatrixTimelineItem.Event(0, anEventTimelineItem(content = aMessageContent()))) + } + skipItems(1) + assertThat(awaitItem().timelineItems.size).isEqualTo(1) + timeline.updateTimelineItems { items -> + items + listOf(MatrixTimelineItem.Event(1, anEventTimelineItem(content = aMessageContent()))) + } + skipItems(1) + assertThat(awaitItem().timelineItems.size).isEqualTo(2) + assertThat(awaitItem().hasNewItems).isTrue() + initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0)) + assertThat(awaitItem().hasNewItems).isFalse() + cancelAndIgnoreRemainingEvents() + } + } + + private fun TestScope.createTimelinePresenter( + timeline: MatrixTimeline = FakeMatrixTimeline(), + timelineItemsFactory: TimelineItemsFactory = aTimelineItemsFactory() + ): TimelinePresenter { + return TimelinePresenter( + timelineItemsFactory = timelineItemsFactory, + room = FakeMatrixRoom(matrixTimeline = timeline), + dispatchers = testCoroutineDispatchers(), + appScope = this + ) + } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt index 3cab1fe44c..71836e9f5e 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt @@ -45,6 +45,7 @@ class TimelineItemGrouperTest { localSendState = LocalEventSendState.Sent(AN_EVENT_ID), inReplyTo = null, debugInfo = aTimelineItemDebugInfo(), + origin = null ) private val aNonGroupableItem = aMessageEvent() private val aNonGroupableItemNoEvent = TimelineItem.Virtual("virtual", aTimelineItemDaySeparatorModel("Today")) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt index 0a956577ca..d9cd6dc867 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt @@ -25,14 +25,17 @@ import io.element.android.libraries.matrix.api.room.message.RoomMessage import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.event.EventContent import io.element.android.libraries.matrix.api.timeline.item.event.EventReaction -import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem +import io.element.android.libraries.matrix.api.timeline.item.event.InReplyTo +import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState +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.ProfileChangeContent import io.element.android.libraries.matrix.api.timeline.item.event.ProfileTimelineDetails +import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME -import io.element.android.libraries.matrix.test.A_UNIQUE_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_NAME @@ -114,6 +117,7 @@ fun anEventTimelineItem( timestamp = timestamp, content = content, debugInfo = debugInfo, + origin = null, ) fun aProfileTimelineDetails( @@ -138,6 +142,21 @@ fun aProfileChangeMessageContent( prevAvatarUrl = prevAvatarUrl, ) +fun aMessageContent( + body: String = "body", + inReplyTo: InReplyTo? = null, + isEdited: Boolean = false, + messageType: MessageType = TextMessageType( + body = body, + formatted = null + ) +) = MessageContent( + body = body, + inReplyTo = inReplyTo, + isEdited = isEdited, + type = messageType +) + fun aTimelineItemDebugInfo( model: String = "Rust(Model())", originalJson: String? = null, @@ -145,3 +164,4 @@ fun aTimelineItemDebugInfo( ) = TimelineItemDebugInfo( model, originalJson, latestEditedJson ) + diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt index b49a5490ce..73bc5fb597 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt @@ -19,6 +19,8 @@ package io.element.android.libraries.matrix.test.timeline import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.tests.testutils.simulateLongTask +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -36,6 +38,8 @@ class FakeMatrixTimeline( var sendReadReceiptCount = 0 private set + var sendReadReceiptLatch: CompletableDeferred? = null + fun updatePaginationState(update: (MatrixTimeline.PaginationState.() -> MatrixTimeline.PaginationState)) { _paginationState.getAndUpdate(update) } @@ -62,13 +66,13 @@ class FakeMatrixTimeline( return Result.success(Unit) } - - override suspend fun fetchDetailsForEvent(eventId: EventId): Result { - return Result.success(Unit) + override suspend fun fetchDetailsForEvent(eventId: EventId): Result = simulateLongTask { + Result.success(Unit) } - override suspend fun sendReadReceipt(eventId: EventId): Result { + override suspend fun sendReadReceipt(eventId: EventId): Result = simulateLongTask { sendReadReceiptCount++ - return Result.success(Unit) + sendReadReceiptLatch?.complete(Unit) + Result.success(Unit) } } diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/LongTask.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/LongTask.kt index 154877efe9..8a5158dbf8 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/LongTask.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/LongTask.kt @@ -16,7 +16,12 @@ package io.element.android.tests.testutils +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.delay +import kotlinx.coroutines.withTimeout +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds /** * Workaround for https://github.com/cashapp/molecule/issues/249. @@ -26,3 +31,18 @@ suspend inline fun simulateLongTask(lambda: () -> T): T { delay(1) return lambda() } + +/** + * Can be used for testing events in Presenter, where the event does not emit new state. + * If the (virtual) timeout is passed, we release the latch manually. + */ +suspend fun awaitWithLatch(timeout: Duration = 300.milliseconds, block: (CompletableDeferred) -> Unit) { + val latch = CompletableDeferred() + try { + withTimeout(timeout) { + latch.also(block).await() + } + } catch (exception: TimeoutCancellationException) { + latch.complete(Unit) + } +} From 32ab1f40e28cbce91a2950909da098c1611112d0 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 13 Jul 2023 17:21:56 +0200 Subject: [PATCH 06/59] Timeline: make group id really stable --- .../timeline/groups/TimelineItemGrouper.kt | 39 ++++++++++++++++--- .../impl/timeline/model/TimelineItem.kt | 7 ++-- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt index b04509ba22..bee2b055f3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt @@ -17,10 +17,20 @@ package io.element.android.features.messages.impl.timeline.groups import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.di.SingleIn import kotlinx.collections.immutable.toImmutableList import javax.inject.Inject +@SingleIn(RoomScope::class) class TimelineItemGrouper @Inject constructor() { + + /** + * Keys are identifier of items in a group, only one by group will be kept. + * Values are the actual groupIds. + */ + private val groupIds = HashMap() + /** * Create a new list of [TimelineItem] by grouping some of them into [TimelineItem.GroupedEvents]. */ @@ -34,14 +44,14 @@ class TimelineItemGrouper @Inject constructor() { // timelineItem cannot be grouped if (currentGroup.isNotEmpty()) { // There is a pending group, create a TimelineItem.GroupedEvents if there is more than 1 Event in the pending group. - result.addGroup(currentGroup) + result.addGroup(groupIds, currentGroup) currentGroup.clear() } result.add(timelineItem) } } if (currentGroup.isNotEmpty()) { - result.addGroup(currentGroup) + result.addGroup(groupIds, currentGroup) } return result } @@ -51,16 +61,33 @@ class TimelineItemGrouper @Inject constructor() { * Will add a group if there is more than 1 item, else add the item to the list. */ private fun MutableList.addGroup( - group: MutableList + groupIds: MutableMap, + groupOfItems: MutableList ) { - if (group.size == 1) { + if (groupOfItems.size == 1) { // Do not create a group with just 1 item, just add the item to the result - add(group.first()) + add(groupOfItems.first()) } else { + val groupId = groupIds.getOrPutGroupId(groupOfItems) add( TimelineItem.GroupedEvents( - events = group.toImmutableList() + id = groupId, + events = groupOfItems.toImmutableList() ) ) } } + +private fun MutableMap.getOrPutGroupId(timelineItems: List): String { + assert(timelineItems.isNotEmpty()) + for (item in timelineItems) { + val itemIdentifier = item.identifier() + if (this.contains(itemIdentifier)) { + return this[itemIdentifier]!! + } + } + val itemIdentifier = timelineItems.first().identifier() + return "${itemIdentifier}_group".also { groupId -> + this[itemIdentifier] = groupId + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt index 7f95b30409..b415d128a2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt @@ -82,9 +82,8 @@ sealed interface TimelineItem { @Immutable data class GroupedEvents( + val id: String, val events: ImmutableList, - ) : TimelineItem { - // use last id with a suffix. Last will not change in cas of new event from backpagination. - val id = "${events.last().id}_group" - } + ) : TimelineItem + } From edb025a549ca211338b90da0f4599a440e337ddd Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 13 Jul 2023 15:38:07 +0000 Subject: [PATCH 07/59] Update screenshots --- ...Group_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewDarkPreview_0_null_11,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewDarkPreview_0_null_12,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...oup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png | 4 ++-- ...oup_TimelineViewLightPreview_0_null_11,NEXUS_5,1.0,en].png | 4 ++-- ...oup_TimelineViewLightPreview_0_null_12,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png | 4 ++-- ...Group_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...Group_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...Group_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...Group_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...Group_MessagesViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...roup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...roup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...roup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...roup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...roup_MessagesViewLightPreview_0_null_4,NEXUS_5,1.0,en].png | 4 ++-- 36 files changed, 72 insertions(+), 72 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 1d34a00941..9f5b7d48c6 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:651319df939573207244eb84d9cea2bfac1c216e704f58c27533d3b2d98c4c64 -size 51933 +oid sha256:1a3b5bbcdd1593e81384b335045bdcb0b3e01782868993e9f6437a15ca39dbca +size 51380 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index 4fab21b0af..ba9f8a8d63 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b0e751aee2e0f3fa735bbbb6b96d561fa485ebb3244dd51e3842c1f685cb1772 -size 63335 +oid sha256:7e26db0a0a8d767d6e732c1d2742cba3c3475d761b0c3017ff01cee7e3d362a5 +size 62773 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png index dce1f75bc4..7d12d81b18 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3182e3cd2971af26007edfbee2d0ad058eb1bae0fb79d110800f0980c982035d -size 50045 +oid sha256:3d5cae7d73178aecc12fbf5f1f27c9e66608b90d462deec862a881b403469e93 +size 49475 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_11,NEXUS_5,1.0,en].png index 3fb99d2f69..44fd92b664 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_11,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_11,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e836190c744d6a65232c870544d4f1ec1020074c752585966488cd56e7bf6709 -size 66264 +oid sha256:fe9456f4446104142221e28167a578a2b5cac772dc35aeb14352c0d422dd6fb0 +size 65726 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_12,NEXUS_5,1.0,en].png index 00ccd6eb51..ee3a9ae3fe 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_12,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_12,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ba5b9161bd13d3185c4233b1c991c377f86ab7d33e61d549f1be0c89879db8f3 -size 56672 +oid sha256:cad3b5ce023d890fd4a5ff0f5bbabb678bc6d5c76d3476e8053673c06f360c8b +size 56102 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png index abdb75b960..8e578cb1f9 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9417f2631c429f047e799eba52183dc4a8be202f0110280332f837dd7b60c3d2 -size 229653 +oid sha256:71b3c712fb8e4bca178afed2de6f4184bb717e3415622e97f14bb4fe36aaf9d5 +size 229097 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png index 16994e6b8a..13d92ee329 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:51bfd93f2bd0bf313e675d63a89fddacfb007fbe207ca15871d3f94df14c9d24 -size 230633 +oid sha256:f44d772e5b9fe65a79e05aaaa82536b19382f9d821c0412c47a7d330d539ffd2 +size 230080 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png index 09e1fb4c35..4a58253b47 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:085091b7cb44ada72a16af6e3c652dd85589455648949db3bf37ef4160a8ebe9 -size 71401 +oid sha256:41a6c2dd81698696708802276b615a78ff22cc7cb8ae2d4b8d8cc862bfe44a24 +size 70856 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png index 2066060843..92bbf1dd70 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1eed02746fd9373275f6d66a283f3c9b0719e19d44194a1ba0cbb701c7c5d729 -size 85494 +oid sha256:f729f89dce978bb9e061826389e0db1af0e18835e4cd1ab6cbfaa31f4fd3152f +size 84942 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png index f01d3832e6..ace73de65e 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eddf42039c5629333db7d7b56abbd7dba9af8c70bb4787e4101a768e264c3199 -size 174401 +oid sha256:03a4b62920af61098e850cfca6a6f43acd7b310094fb70e1c3e4b7e29465e244 +size 173851 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png index 94b7b6f75c..afb2440466 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:147052b5017396f02c0381ba8084b6c22d893e6ccc9e3551ce34ade7c05c61bc -size 165153 +oid sha256:ebcbb40acf19fb489f6a8068a22af0b79c304be46863e77fa12b3b333065a1b2 +size 164622 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png index e32ec387f4..08c13f8f2a 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7655aa9de18743923bbe410b7ea05fe2d00ce716dc43f9673c8007951ec924ca -size 53316 +oid sha256:789ee5ccad8356198cdc0634b4e9a65ed44be2d26e7ce83a8662598c1bd8d4c2 +size 52765 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png index cc48e8a3fd..300bcf7167 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f81af9d9badaa6141390f83675ebeac7b3e9ec7412bb7ce2506e7123a5ec958 -size 65345 +oid sha256:7b40a2e5d60a906d7c35c3ece1854671f5b319b00ad077322d094cbf906c07f7 +size 64803 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index 518e317011..7c99f5e856 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9546f65d0347f7c7c07f07e730ec8a344549a43e47a21585b7d9375ac0a1c838 -size 53692 +oid sha256:54c12970e3563de958f88e4c538dd368f9810266060627393256a91741f7c6cf +size 53340 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index bc40cfe66b..def9cbe0d1 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a8a62fb60461f2f699ea010663f617ad12c949365b3f9125804cc9c674bb1f03 -size 65959 +oid sha256:3b7ae3084cb9d1ecee2e4db49c228516bdd7352683e797edf737c8d216922dec +size 65601 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png index b9a3dd7e2c..258ce3f3de 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c34993f6832f141eeee8bbae9068828d58ebc900a21a658d5b0627096fe563c4 -size 51582 +oid sha256:dd034d439c08793e0dfd59f6bd5dcd88c06ded6cbe98172bf3cf296888e6d575 +size 51244 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_11,NEXUS_5,1.0,en].png index 9f4542c3f8..b2f16a9d23 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_11,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_11,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d11d593be6d5ade5417aacef2aff79bc8940c971451669f01de4efa74fd7673 -size 69100 +oid sha256:d9d835cb1a420117b4d967181e2ca0fad71ba243d6a17bea08b82cae41f6b8e2 +size 68760 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_12,NEXUS_5,1.0,en].png index c2c6fb7af6..3bbd94a1c8 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_12,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_12,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3abc3d49852c70fe1d1c50e87cf86e91eb3cd91ff1cecca5ef0f1a30ab8dec9a -size 58883 +oid sha256:4ad09278ae2ebb8171adba96d9f6e91d0cc4f120b0b2368087796dadb37eeb87 +size 58539 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png index 5149d82b20..1c32b14777 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac58823ba2ae3c457f4199e71167693c6ea42839d912eb9b8a8d079aa26ec303 -size 230198 +oid sha256:145856c3a7ff43702403ee5b86a7119b0475f03fdbc0f2e7f84e10350a64b150 +size 229842 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png index 19a4f805d5..53f57d95d2 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5af0e9fa17bdf8562c0f4e2f31c0ff0e36e243ec3ae51f23eaca6fe67da9f6a6 -size 231168 +oid sha256:c1f0939b0c22ab89466953889e5bb63e11c45f02af79eeba3f377409af61d356 +size 230809 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png index fa988a7752..4c991e3c30 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a84fdf49767a07d801a745ffaf659f15ead717f8edd6aeea31acf0cf82f1e1e5 -size 73991 +oid sha256:082206122d4e6d9e6171b3b2444c576ff7bd47fb3946d8d98e2812654f39cd40 +size 73641 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png index bbb417ede8..96270c69ae 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f494dc4f4b95e50fec21ee5c33a467711ce96dae8d2e68c091efc7874d682d8d -size 90101 +oid sha256:14ecdfd226a25743e3fac8850318216d6ec193755fa7559fd5523236b7835bc6 +size 89754 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png index 6c41c7f016..b62d934c0c 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7359910c6bf938e2bb226b1ef7072de3441bd514f1c62286dfa5a1a9b2797d33 -size 364102 +oid sha256:d08de923f29ec6c3c2bf735ca0d015ca1097c95484119d1d39bb8c3f93f31100 +size 363776 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png index 92bddc4e40..3e79fb521a 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59a85793b104485cf64dc090ce295d21248502d98696c1d41c3906936a7990a6 -size 323105 +oid sha256:3e8832da336aea6c7ebb3621668569ff16238042fb9650afcd938594a59fd3ae +size 322771 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png index 06e1aab216..ef081b76b8 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1841a5b097309e8809b4a0ce8e798a19794e77bdb29d0037a98ff507fd8ba64a -size 55252 +oid sha256:9a243d53d10ca2eaa249b22af8a9fddf1a8ccf60db4f3ad9374e31cf494fe878 +size 54909 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png index 1028848722..0b13a93b0c 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:373ab2d1c52b7fd818be21a3b8ef167b7ec27a0bb9147d040c680316f30ac346 -size 67816 +oid sha256:3fdd478be89b47fcaf9725ca14c1e775087f1ccf2a266f3476a537bbc2b29922 +size 67476 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index a20978d372..101f913aa8 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a6659d49217bb75dd62310ab1e5012466402203f2b9d11597da037f21e1273df -size 52885 +oid sha256:7c724bc77185a9ceec2cf092cb1d7865b13718d5320bd7ac4850bb85590f05b2 +size 52294 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index 4b7eb025f1..cb28314caa 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c9bc30e1ff19b87b1ee95e78f9b5c000cda8760a6e84c6364b02e8b725092586 -size 54301 +oid sha256:a303894134ed06348b609e41cf109dcafcd994c3dffabc6d9ab436fe92605245 +size 53710 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png index 4ec03a627d..2ba437739f 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:20689b49bd2ef39ea8a9b2d492f1e6ebb7841bacca74a80fdbde33964407e635 -size 53116 +oid sha256:7523ec0d6defd7074af0c804fdde64fb8d421f6cfa729bc1c4f9858bd87c42d0 +size 52554 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png index afc72af080..73715a33a2 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c3dd1a28aa9ef018e58435efe308822bfb49a99dc19366328edc6131039614f -size 55948 +oid sha256:1f48089147f7e089abfa64254143a80857ccc9f840aa60403e1aabc67e2b6d51 +size 55458 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png index c682612eee..9ce6f92309 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:565e8b74a4e88fdd1533c982a7210a0d79a01efb2d1059bc03b40b05555a23bb -size 51661 +oid sha256:7ff682ee8363d450bb76db72ea06deea87fa47692ce319b7dff315d2a10dfb6a +size 51033 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index 0de93cadc1..57eeca6786 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0d449f35be765f1c5f38ff7bc026b34ebeeb46dc2e17ddd11d153b916ac8aa3c -size 54626 +oid sha256:e84edf8adf1a89153dd45272d36e04561d66f2ea765ad9edecfd8d750ba99f97 +size 54237 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index 2f3be581f2..8baf81c8b1 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a7c8f63609634975edd98298264de50da56cd0a805a71e0a10cce846fcb077de -size 56077 +oid sha256:0bc9521bd1576d47ca6f643adf43ce3638d40328207d908ec863c72503d34f24 +size 55682 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png index 7ccf9a8669..40900d7e92 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2e9579c9292407a7e837871a98785e135559c6584cca54b6ab997974f0ce24ae -size 54986 +oid sha256:c2209c3cc4e7de32ed92b3b0da4616b54d19091d43d21c0e4013728450d0d3f7 +size 54595 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png index a3de6acb2e..5777c7f77e 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf176efd967d4e00bae6589cc39cd24849f64f32d4d90e1d6461e29aa9f480f5 -size 57961 +oid sha256:d7bdd0ca39534b31c9d421e28337712b1cf2aaf841a766fcc6bdaf996c756bfa +size 57524 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_4,NEXUS_5,1.0,en].png index 9908572779..2e2bfa89cb 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92a4c16a14f645b3db415bad8a69db1d8c3097f30087be45702f6a9c45ee6cd6 -size 53055 +oid sha256:ba349f81d5c417c612cae1263504ea5b4e83dc606ec20c3942368e8992b87ad4 +size 52886 From 3d0e6a4130d41f0e102b3dd09cecf222a4e89835 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 13 Jul 2023 18:09:58 +0200 Subject: [PATCH 08/59] Media: render audio content --- .../messages/impl/MessagesPresenter.kt | 19 +-- .../impl/actionlist/ActionListView.kt | 21 +++- .../components/TimelineItemEventRow.kt | 15 +-- .../components/event/TimelineItemAudioView.kt | 108 ++++++++++++++++++ .../event/TimelineItemContentView.kt | 6 + .../TimelineItemContentMessageFactory.kt | 10 ++ .../impl/timeline/groups/Groupability.kt | 2 + .../model/event/TimelineItemAudioContent.kt | 33 ++++++ .../event/TimelineItemAudioContentProvider.kt | 39 +++++++ .../MessageSummaryFormatterImpl.kt | 2 + .../libraries/matrix/api/media/AudioInfo.kt | 2 +- .../libraries/matrix/impl/media/AudioInfo.kt | 4 +- .../ui/components/AttachmentThumbnail.kt | 21 ++-- .../mediaupload/AndroidMediaPreProcessor.kt | 2 +- .../libraries/textcomposer/TextComposer.kt | 8 +- 15 files changed, 257 insertions(+), 35 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContent.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContentProvider.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 841ccb8faa..ddc9f17262 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -43,6 +43,7 @@ import io.element.android.features.messages.impl.timeline.TimelinePresenter import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent @@ -108,10 +109,10 @@ class MessagesPresenter @AssistedInject constructor( val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val userHasPermissionToSendMessage by room.canSendEventAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value) - val roomName by produceState(initialValue = room.displayName, key1 = syncUpdateFlow.value){ + val roomName by produceState(initialValue = room.displayName, key1 = syncUpdateFlow.value) { value = room.displayName } - val roomAvatar by produceState(initialValue = room.avatarData(), key1 = syncUpdateFlow.value){ + val roomAvatar by produceState(initialValue = room.avatarData(), key1 = syncUpdateFlow.value) { value = room.avatarData() } var hasDismissedInviteDialog by rememberSaveable { @@ -250,28 +251,28 @@ class MessagesPresenter @AssistedInject constructor( val textContent = messageSummaryFormatter.format(targetEvent) val attachmentThumbnailInfo = when (targetEvent.content) { is TimelineItemImageContent -> AttachmentThumbnailInfo( - mediaSource = targetEvent.content.mediaSource, + thumbnailSource = targetEvent.content.thumbnailSource, textContent = targetEvent.content.body, type = AttachmentThumbnailType.Image, blurHash = targetEvent.content.blurhash, ) is TimelineItemVideoContent -> AttachmentThumbnailInfo( - mediaSource = targetEvent.content.thumbnailSource, + thumbnailSource = targetEvent.content.thumbnailSource, textContent = targetEvent.content.body, type = AttachmentThumbnailType.Video, blurHash = targetEvent.content.blurHash, ) is TimelineItemFileContent -> AttachmentThumbnailInfo( - mediaSource = targetEvent.content.thumbnailSource, + thumbnailSource = targetEvent.content.thumbnailSource, textContent = targetEvent.content.body, type = AttachmentThumbnailType.File, - blurHash = null, + ) + is TimelineItemAudioContent -> AttachmentThumbnailInfo( + textContent = targetEvent.content.body, + type = AttachmentThumbnailType.Audio, ) is TimelineItemLocationContent -> AttachmentThumbnailInfo( - mediaSource = null, - textContent = null, type = AttachmentThumbnailType.Location, - blurHash = null, ) is TimelineItemTextBasedContent, is TimelineItemRedactedContent, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt index bb1f2a7ac8..c41f1006c8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt @@ -56,6 +56,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent @@ -246,7 +247,7 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif info = AttachmentThumbnailInfo( type = AttachmentThumbnailType.Location, textContent = stringResource(CommonStrings.common_shared_location), - mediaSource = null, + thumbnailSource = null, blurHash = null, ) ) @@ -258,7 +259,7 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif AttachmentThumbnail( modifier = imageModifier, info = AttachmentThumbnailInfo( - mediaSource = event.content.mediaSource, + thumbnailSource = event.content.mediaSource, textContent = textContent, type = AttachmentThumbnailType.File, blurHash = event.content.blurhash, @@ -272,7 +273,7 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif AttachmentThumbnail( modifier = imageModifier, info = AttachmentThumbnailInfo( - mediaSource = event.content.thumbnailSource, + thumbnailSource = event.content.thumbnailSource, textContent = textContent, type = AttachmentThumbnailType.Video, blurHash = event.content.blurHash, @@ -286,7 +287,7 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif AttachmentThumbnail( modifier = imageModifier, info = AttachmentThumbnailInfo( - mediaSource = null, + thumbnailSource = event.content.thumbnailSource, textContent = textContent, type = AttachmentThumbnailType.File, blurHash = null @@ -295,6 +296,18 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif } content = { ContentForBody(event.content.body) } } + is TimelineItemAudioContent -> { + icon = { + AttachmentThumbnail( + modifier = imageModifier, + info = AttachmentThumbnailInfo( + textContent = textContent, + type = AttachmentThumbnailType.Audio, + ) + ) + } + content = { ContentForBody(event.content.body) } + } } Row(modifier = modifier) { icon() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index 8d7ff263e8..fbb745c34e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -56,7 +56,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex import androidx.constraintlayout.compose.ConstrainScope import androidx.constraintlayout.compose.ConstraintLayout @@ -85,6 +84,7 @@ import io.element.android.libraries.designsystem.text.toPx import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.timeline.item.event.AudioMessageType 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.InReplyTo @@ -521,28 +521,29 @@ private fun ReplyToContent( private fun attachmentThumbnailInfoForInReplyTo(inReplyTo: InReplyTo.Ready) = when (val type = inReplyTo.content.type) { is ImageMessageType -> AttachmentThumbnailInfo( - mediaSource = type.info?.thumbnailSource, + thumbnailSource = type.info?.thumbnailSource, textContent = inReplyTo.content.body, type = AttachmentThumbnailType.Image, blurHash = type.info?.blurhash, ) is VideoMessageType -> AttachmentThumbnailInfo( - mediaSource = type.info?.thumbnailSource, + thumbnailSource = type.info?.thumbnailSource, textContent = inReplyTo.content.body, type = AttachmentThumbnailType.Video, blurHash = type.info?.blurhash, ) is FileMessageType -> AttachmentThumbnailInfo( - mediaSource = type.info?.thumbnailSource, + thumbnailSource = type.info?.thumbnailSource, textContent = inReplyTo.content.body, type = AttachmentThumbnailType.File, - blurHash = null, ) is LocationMessageType -> AttachmentThumbnailInfo( - mediaSource = null, textContent = inReplyTo.content.body, type = AttachmentThumbnailType.Location, - blurHash = null, + ) + is AudioMessageType -> AttachmentThumbnailInfo( + textContent = inReplyTo.content.body, + type = AttachmentThumbnailType.Audio, ) else -> null } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt new file mode 100644 index 0000000000..3cae19e237 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.timeline.components.event + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Attachment +import androidx.compose.material.icons.outlined.GraphicEq +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContentProvider +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.theme.ElementTheme + +@Composable +fun TimelineItemAudioView( + content: TimelineItemAudioContent, + extraPadding: ExtraPadding, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier, + ) { + Box( + modifier = Modifier + .size(32.dp) + .clip(CircleShape) + .background(ElementTheme.materialColors.background), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = Icons.Outlined.GraphicEq, + contentDescription = null, + tint = ElementTheme.materialColors.primary, + modifier = Modifier + .size(16.dp), + ) + } + Spacer(Modifier.width(8.dp)) + Column { + Text( + text = content.body, + color = ElementTheme.materialColors.primary, + maxLines = 2, + style = ElementTheme.typography.fontBodyLgRegular, + overflow = TextOverflow.Ellipsis + ) + Text( + text = content.fileExtensionAndSize + extraPadding.getStr(12.sp), + color = ElementTheme.materialColors.secondary, + style = ElementTheme.typography.fontBodySmRegular, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@Preview +@Composable +internal fun TimelineItemAudioViewLightPreview(@PreviewParameter(TimelineItemAudioContentProvider::class) content: TimelineItemAudioContent) = + ElementPreviewLight { ContentToPreview(content) } + +@Preview +@Composable +internal fun TimelineItemAudioViewDarkPreview(@PreviewParameter(TimelineItemAudioContentProvider::class) content: TimelineItemAudioContent) = + ElementPreviewDark { ContentToPreview(content) } + +@Composable +private fun ContentToPreview(content: TimelineItemAudioContent) { + TimelineItemAudioView( + content, + extraPadding = noExtraPadding, + ) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt index 7d3f8835ca..3df45eb760 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemContentView.kt @@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.timeline.components.event import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent @@ -80,6 +81,11 @@ fun TimelineItemEventContentView( extraPadding = extraPadding, modifier = modifier ) + is TimelineItemAudioContent -> TimelineItemAudioView( + content = content, + extraPadding = extraPadding, + modifier = modifier + ) is TimelineItemStateContent -> TimelineItemStateView( content = content, modifier = modifier diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt index 9d4304cec0..9da31ee6a7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt @@ -17,6 +17,7 @@ package io.element.android.features.messages.impl.timeline.factories.event import io.element.android.features.location.api.Location +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEmoteContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent @@ -30,6 +31,7 @@ import io.element.android.features.messages.impl.timeline.util.FileExtensionExtr import io.element.android.features.messages.impl.timeline.util.toHtmlDocument import io.element.android.libraries.androidutils.filesize.FileSizeFormatter import io.element.android.libraries.core.mimetype.MimeTypes +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.FileMessageType import io.element.android.libraries.matrix.api.timeline.item.event.ImageMessageType @@ -99,6 +101,14 @@ class TimelineItemContentMessageFactory @Inject constructor( fileExtension = fileExtensionExtractor.extractFromName(messageType.body) ) } + is AudioMessageType -> TimelineItemAudioContent( + body = messageType.body, + audioSource = messageType.source, + duration = messageType.info?.duration?.toMillis() ?: 0L, + mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, + formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0), + fileExtension = fileExtensionExtractor.extractFromName(messageType.body) + ) is FileMessageType -> TimelineItemFileContent( body = messageType.body, thumbnailSource = messageType.info?.thumbnailSource, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt index bf827a2b3d..0b8baf692a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/Groupability.kt @@ -17,6 +17,7 @@ package io.element.android.features.messages.impl.timeline.groups import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent @@ -52,6 +53,7 @@ internal fun TimelineItem.Event.canBeGrouped(): Boolean { is TimelineItemImageContent, is TimelineItemFileContent, is TimelineItemVideoContent, + is TimelineItemAudioContent, is TimelineItemLocationContent, TimelineItemRedactedContent, TimelineItemUnknownContent -> false diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContent.kt new file mode 100644 index 0000000000..485b863170 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContent.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022 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 + * + * http://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.timeline.model.event + +import io.element.android.features.messages.impl.media.helper.formatFileExtensionAndSize +import io.element.android.libraries.matrix.api.media.MediaSource + +data class TimelineItemAudioContent( + val body: String, + val duration: Long, + val audioSource: MediaSource, + val mimeType: String, + val formattedFileSize: String, + val fileExtension: String, +) : TimelineItemEventContent { + + val fileExtensionAndSize = formatFileExtensionAndSize(fileExtension, formattedFileSize) + override val type: String = "TimelineItemAudioContent" +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContentProvider.kt new file mode 100644 index 0000000000..ed424781f8 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemAudioContentProvider.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.timeline.model.event + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.core.mimetype.MimeTypes +import io.element.android.libraries.matrix.api.media.MediaSource + +open class TimelineItemAudioContentProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aTimelineItemAudioContent("A sound.mp3"), + aTimelineItemAudioContent("A bigger name sound.mp3"), + aTimelineItemAudioContent("An even bigger bigger bigger bigger bigger bigger bigger sound name which doesn't fit .mp3"), + ) +} + +fun aTimelineItemAudioContent(fileName: String = "A sound.mp3") = TimelineItemAudioContent( + body = fileName, + mimeType = MimeTypes.Pdf, + formattedFileSize = "100kB", + fileExtension = "mp3", + duration = 100, + audioSource = MediaSource(""), +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/MessageSummaryFormatterImpl.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/MessageSummaryFormatterImpl.kt index e22aed6af9..42c50bbd9d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/MessageSummaryFormatterImpl.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/utils/messagesummary/MessageSummaryFormatterImpl.kt @@ -19,6 +19,7 @@ package io.element.android.features.messages.impl.utils.messagesummary import android.content.Context import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent @@ -50,6 +51,7 @@ class MessageSummaryFormatterImpl @Inject constructor( is TimelineItemImageContent -> context.getString(CommonStrings.common_image) is TimelineItemVideoContent -> context.getString(CommonStrings.common_video) is TimelineItemFileContent -> context.getString(CommonStrings.common_file) + is TimelineItemAudioContent -> context.getString(CommonStrings.common_audio) } } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/AudioInfo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/AudioInfo.kt index e9708a6926..bd4539bced 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/AudioInfo.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/AudioInfo.kt @@ -21,5 +21,5 @@ import java.time.Duration data class AudioInfo( val duration: Duration?, val size: Long?, - val mimeType: String?, + val mimetype: String?, ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/AudioInfo.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/AudioInfo.kt index 2f0d6879a4..70c3bac6ed 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/AudioInfo.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/AudioInfo.kt @@ -22,11 +22,11 @@ import org.matrix.rustcomponents.sdk.AudioInfo as RustAudioInfo fun RustAudioInfo.map(): AudioInfo = AudioInfo( duration = duration, size = size?.toLong(), - mimeType = mimetype + mimetype = mimetype ) fun AudioInfo.map(): RustAudioInfo = RustAudioInfo( duration = duration, size = size?.toULong(), - mimetype = mimeType, + mimetype = mimetype, ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnail.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnail.kt index cac211bcb5..71883ebc20 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnail.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/AttachmentThumbnail.kt @@ -22,6 +22,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Attachment +import androidx.compose.material.icons.outlined.GraphicEq import androidx.compose.material.icons.outlined.VideoCameraBack import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable @@ -44,9 +45,9 @@ fun AttachmentThumbnail( thumbnailSize: Long = 32L, backgroundColor: Color = MaterialTheme.colorScheme.surface, ) { - if (info.mediaSource != null) { + if (info.thumbnailSource != null) { val mediaRequestData = MediaRequestData( - source = info.mediaSource, + source = info.thumbnailSource, kind = MediaRequestData.Kind.Thumbnail(thumbnailSize), ) BlurHashAsyncImage( @@ -68,6 +69,12 @@ fun AttachmentThumbnail( contentDescription = info.textContent, ) } + AttachmentThumbnailType.Audio -> { + Icon( + imageVector = Icons.Outlined.GraphicEq, + contentDescription = info.textContent, + ) + } AttachmentThumbnailType.File -> { Icon( imageVector = Icons.Outlined.Attachment, @@ -88,13 +95,13 @@ fun AttachmentThumbnail( @Parcelize enum class AttachmentThumbnailType: Parcelable { - Image, Video, File, Location + Image, Video, File, Audio, Location } @Parcelize data class AttachmentThumbnailInfo( - val mediaSource: MediaSource?, - val textContent: String?, - val type: AttachmentThumbnailType?, - val blurHash: String?, + val type: AttachmentThumbnailType, + val thumbnailSource: MediaSource? = null, + val textContent: String? = null, + val blurHash: String? = null, ): Parcelable diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt index 9482aecc13..f0cf6b9dcb 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt @@ -196,7 +196,7 @@ class AndroidMediaPreProcessor @Inject constructor( val info = AudioInfo( duration = extractDuration(), size = file.length(), - mimeType = mimeType, + mimetype = mimeType, ) MediaUploadInfo.Audio(file, info) diff --git a/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index b757f69f03..3994d8f55c 100644 --- a/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -482,7 +482,7 @@ fun TextComposerReplyPreview() = ElementPreview { senderName = "Alice", eventId = EventId("$1234"), attachmentThumbnailInfo = AttachmentThumbnailInfo( - mediaSource = MediaSource("https://domain.com/image.jpg"), + thumbnailSource = MediaSource("https://domain.com/image.jpg"), textContent = "image.jpg", type = AttachmentThumbnailType.Image, blurHash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr", @@ -500,7 +500,7 @@ fun TextComposerReplyPreview() = ElementPreview { senderName = "Alice", eventId = EventId("$1234"), attachmentThumbnailInfo = AttachmentThumbnailInfo( - mediaSource = MediaSource("https://domain.com/video.mp4"), + thumbnailSource = MediaSource("https://domain.com/video.mp4"), textContent = "video.mp4", type = AttachmentThumbnailType.Video, blurHash = "TQF5:I_NtRE4kXt7Z#MwkCIARPjr", @@ -518,7 +518,7 @@ fun TextComposerReplyPreview() = ElementPreview { senderName = "Alice", eventId = EventId("$1234"), attachmentThumbnailInfo = AttachmentThumbnailInfo( - mediaSource = null, + thumbnailSource = null, textContent = "logs.txt", type = AttachmentThumbnailType.File, blurHash = null, @@ -536,7 +536,7 @@ fun TextComposerReplyPreview() = ElementPreview { senderName = "Alice", eventId = EventId("$1234"), attachmentThumbnailInfo = AttachmentThumbnailInfo( - mediaSource = null, + thumbnailSource = null, textContent = null, type = AttachmentThumbnailType.Location, blurHash = null, From e9a34eb46d12eff23bcfe468056312fbdbe00921 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 13 Jul 2023 20:34:07 +0200 Subject: [PATCH 09/59] Media: upload audio --- .../messages/impl/media/local/MediaInfo.kt | 2 +- .../MessageComposerPresenterTest.kt | 4 +-- .../edit/RoomDetailsEditPresenterTest.kt | 2 +- .../libraries/mediaupload/api/MediaSender.kt | 29 ++++++++++++------- .../mediaupload/api/MediaUploadInfo.kt | 8 ++--- .../mediaupload/AndroidMediaPreProcessor.kt | 6 ++-- 6 files changed, 29 insertions(+), 22 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/MediaInfo.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/MediaInfo.kt index 2fc47e0d2c..3dede8e929 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/MediaInfo.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/MediaInfo.kt @@ -29,7 +29,7 @@ data class MediaInfo( ) : Parcelable fun anImageInfo(): MediaInfo = MediaInfo( - "an image file.jpg", MimeTypes.Jpeg, "4MB","jpg" + "an image file.jpg", MimeTypes.Jpeg, "4MB", "jpg" ) fun aVideoInfo(): MediaInfo = MediaInfo( diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt index 97bbf925bd..8391704701 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt @@ -324,7 +324,7 @@ class MessageComposerPresenterTest { Result.success( MediaUploadInfo.Image( file = File("/some/path"), - info = ImageInfo( + imageInfo = ImageInfo( width = null, height = null, mimetype = null, @@ -357,7 +357,7 @@ class MessageComposerPresenterTest { Result.success( MediaUploadInfo.Video( file = File("/some/path"), - info = VideoInfo( + videoInfo = VideoInfo( width = null, height = null, mimetype = null, diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt index df80f40e9b..20d253f3fb 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt @@ -605,7 +605,7 @@ class RoomDetailsEditPresenterTest { Result.success( MediaUploadInfo.AnyFile( file = processedFile, - info = mockk(), + fileInfo = mockk(), ) ) ) diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt index 6dab564b6e..cfa59d65d3 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt @@ -46,36 +46,43 @@ class MediaSender @Inject constructor( } private suspend fun MatrixRoom.sendMedia( - info: MediaUploadInfo, + uploadInfo: MediaUploadInfo, progressCallback: ProgressCallback? ): Result { - return when (info) { + return when (uploadInfo) { is MediaUploadInfo.Image -> { sendImage( - file = info.file, - thumbnailFile = info.thumbnailFile, - imageInfo = info.info, + file = uploadInfo.file, + thumbnailFile = uploadInfo.thumbnailFile, + imageInfo = uploadInfo.imageInfo, progressCallback = progressCallback ) } is MediaUploadInfo.Video -> { sendVideo( - file = info.file, - thumbnailFile = info.thumbnailFile, - videoInfo = info.info, + file = uploadInfo.file, + thumbnailFile = uploadInfo.thumbnailFile, + videoInfo = uploadInfo.videoInfo, + progressCallback = progressCallback + ) + } + is MediaUploadInfo.Audio -> { + sendAudio( + file = uploadInfo.file, + audioInfo = uploadInfo.audioInfo, progressCallback = progressCallback ) } is MediaUploadInfo.AnyFile -> { sendFile( - file = info.file, - fileInfo = info.info, + file = uploadInfo.file, + fileInfo = uploadInfo.fileInfo, progressCallback = progressCallback ) } - else -> Result.failure(IllegalStateException("Unexpected MediaUploadInfo format: $info")) + else -> Result.failure(IllegalStateException("Unexpected MediaUploadInfo format: $uploadInfo")) } } } diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaUploadInfo.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaUploadInfo.kt index 5da3d36c44..51f6372b23 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaUploadInfo.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaUploadInfo.kt @@ -26,8 +26,8 @@ sealed interface MediaUploadInfo { val file: File - data class Image(override val file: File, val info: ImageInfo, val thumbnailFile: File) : MediaUploadInfo - data class Video(override val file: File, val info: VideoInfo, val thumbnailFile: File) : MediaUploadInfo - data class Audio(override val file: File, val info: AudioInfo) : MediaUploadInfo - data class AnyFile(override val file: File, val info: FileInfo) : MediaUploadInfo + data class Image(override val file: File, val imageInfo: ImageInfo, val thumbnailFile: File) : MediaUploadInfo + data class Video(override val file: File, val videoInfo: VideoInfo, val thumbnailFile: File) : MediaUploadInfo + data class Audio(override val file: File, val audioInfo: AudioInfo) : MediaUploadInfo + data class AnyFile(override val file: File, val fileInfo: FileInfo) : MediaUploadInfo } diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt index f0cf6b9dcb..8ff40fae39 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt @@ -133,7 +133,7 @@ class AndroidMediaPreProcessor @Inject constructor( removeSensitiveImageMetadata(compressionResult.file) return MediaUploadInfo.Image( file = compressionResult.file, - info = imageInfo, + imageInfo = imageInfo, thumbnailFile = thumbnailResult.file ) } @@ -156,7 +156,7 @@ class AndroidMediaPreProcessor @Inject constructor( removeSensitiveImageMetadata(file) return MediaUploadInfo.Image( file = file, - info = imageInfo, + imageInfo = imageInfo, thumbnailFile = thumbnailResult.file ) } @@ -184,7 +184,7 @@ class AndroidMediaPreProcessor @Inject constructor( val videoInfo = extractVideoMetadata(resultFile, mimeType, thumbnailInfo) return MediaUploadInfo.Video( file = resultFile, - info = videoInfo, + videoInfo = videoInfo, thumbnailFile = thumbnailInfo.file ) } From d7101f5170effffd720675ef27942331eb4d940d Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 13 Jul 2023 21:29:01 +0200 Subject: [PATCH 10/59] Timeline: fix tests --- .../timeline/groups/TimelineItemGrouper.kt | 10 ++++-- .../groups/TimelineItemGrouperTest.kt | 31 +++++++++++++++---- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt index bee2b055f3..e9e9af6445 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/groups/TimelineItemGrouper.kt @@ -16,6 +16,7 @@ package io.element.android.features.messages.impl.timeline.groups +import androidx.annotation.VisibleForTesting import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.di.RoomScope import io.element.android.libraries.di.SingleIn @@ -86,8 +87,11 @@ private fun MutableMap.getOrPutGroupId(timelineItems: List - this[itemIdentifier] = groupId + val timelineItem = timelineItems.first() + return computeGroupIdWith(timelineItem).also { groupId -> + this[timelineItem.identifier()] = groupId } } + +@VisibleForTesting +internal fun computeGroupIdWith(timelineItem: TimelineItem): String = "${timelineItem.identifier()}_group" diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt index 71836e9f5e..d5ce31f87a 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/groups/TimelineItemGrouperTest.kt @@ -20,13 +20,13 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.messages.fixtures.aMessageEvent import io.element.android.features.messages.impl.timeline.aTimelineItemReactions import io.element.android.features.messages.impl.timeline.groups.TimelineItemGrouper +import io.element.android.features.messages.impl.timeline.groups.computeGroupIdWith import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateEventContent import io.element.android.features.messages.impl.timeline.model.virtual.aTimelineItemDaySeparatorModel import io.element.android.libraries.designsystem.components.avatar.anAvatarData import io.element.android.libraries.matrix.api.timeline.item.event.LocalEventSendState 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.A_USER_ID import io.element.android.libraries.matrix.test.room.aTimelineItemDebugInfo import kotlinx.collections.immutable.toImmutableList @@ -36,7 +36,7 @@ class TimelineItemGrouperTest { private val sut = TimelineItemGrouper() private val aGroupableItem = TimelineItem.Event( - id = AN_EVENT_ID.value, + id = "0", senderId = A_USER_ID, senderAvatar = anAvatarData(), senderDisplayName = "", @@ -76,16 +76,17 @@ class TimelineItemGrouperTest { fun `test groupables and ensure reordering`() { val result = sut.group( listOf( - aGroupableItem.copy(id = AN_EVENT_ID_2.value), - aGroupableItem, + aGroupableItem.copy(id = "1"), + aGroupableItem.copy(id = "0"), ), ) assertThat(result).isEqualTo( listOf( TimelineItem.GroupedEvents( + computeGroupIdWith(aGroupableItem), events = listOf( - aGroupableItem, - aGroupableItem.copy(id = AN_EVENT_ID_2.value), + aGroupableItem.copy("0"), + aGroupableItem.copy(id = "1"), ).toImmutableList() ), ) @@ -128,6 +129,7 @@ class TimelineItemGrouperTest { assertThat(result).isEqualTo( listOf( TimelineItem.GroupedEvents( + computeGroupIdWith(aGroupableItem), events = listOf( aGroupableItem, aGroupableItem, @@ -135,6 +137,7 @@ class TimelineItemGrouperTest { ), aNonGroupableItem, TimelineItem.GroupedEvents( + computeGroupIdWith(aGroupableItem), events = listOf( aGroupableItem, aGroupableItem, @@ -144,4 +147,20 @@ class TimelineItemGrouperTest { ) ) } + + @Test + fun `when calling multiple time the method group over a growing list of groupable items, then groupId is stable`() { + // When + val groupableItems = mutableListOf( + aGroupableItem.copy(id = "1"), + aGroupableItem.copy(id = "2") + ) + val expectedGroupId = sut.group(groupableItems).first().identifier() + groupableItems.add(0, aGroupableItem.copy("3")) + groupableItems.add(2, aGroupableItem.copy("4")) + groupableItems.add(aGroupableItem.copy("5")) + val actualGroupId = sut.group(groupableItems).first().identifier() + // Then + assertThat(actualGroupId).isEqualTo(expectedGroupId) + } } From 0e379c59f3e5f9219259459d17f3289628f4cac7 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 13 Jul 2023 21:55:49 +0200 Subject: [PATCH 11/59] Media: show audio in LocalMediaView (as a file for now...) --- .../features/messages/impl/MessagesFlowNode.kt | 15 +++++++++++++++ .../messages/impl/media/local/LocalMediaView.kt | 10 +++++++--- 2 files changed, 22 insertions(+), 3 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 1593c19afc..da10171d0a 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 @@ -41,6 +41,7 @@ import io.element.android.features.messages.impl.media.viewer.MediaViewerNode import io.element.android.features.messages.impl.report.ReportMessageNode import io.element.android.features.messages.impl.timeline.debug.EventDebugInfoNode import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent @@ -224,6 +225,20 @@ class MessagesFlowNode @AssistedInject constructor( ) backstack.push(navTarget) } + is TimelineItemAudioContent -> { + val mediaSource = event.content.audioSource + val navTarget = NavTarget.MediaViewer( + mediaInfo = MediaInfo( + name = event.content.body, + mimeType = event.content.mimeType, + formattedFileSize = event.content.formattedFileSize, + fileExtension = event.content.fileExtension + ), + mediaSource = mediaSource, + thumbnailSource = null, + ) + backstack.push(navTarget) + } is TimelineItemLocationContent -> { val navTarget = NavTarget.LocationViewer( location = event.content.location, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt index 3d2b1da222..ff17029497 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/LocalMediaView.kt @@ -32,6 +32,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Attachment +import androidx.compose.material.icons.outlined.GraphicEq import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -47,7 +48,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle import androidx.media3.common.MediaItem @@ -59,7 +59,9 @@ import io.element.android.features.messages.impl.media.helper.formatFileExtensio import io.element.android.features.messages.impl.media.local.exoplayer.ExoPlayerWrapper import io.element.android.features.messages.impl.media.local.pdf.PdfViewer import io.element.android.features.messages.impl.media.local.pdf.rememberPdfViewerState +import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.mimetype.MimeTypes +import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeAudio import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo import io.element.android.libraries.designsystem.R @@ -103,6 +105,7 @@ fun LocalMediaView( zoomableState = zoomableState, modifier = modifier ) + //TODO handle audio with exoplayer else -> MediaFileView( localMediaViewState = localMediaViewState, uri = localMedia?.uri, @@ -215,6 +218,7 @@ fun MediaFileView( info: MediaInfo?, modifier: Modifier = Modifier, ) { + val isAudio = info?.mimeType.isMimeTypeAudio().orFalse() localMediaViewState.isReady = uri != null Box(modifier = modifier.padding(horizontal = 8.dp), contentAlignment = Alignment.Center) { Column(horizontalAlignment = Alignment.CenterHorizontally) { @@ -226,12 +230,12 @@ fun MediaFileView( contentAlignment = Alignment.Center, ) { Icon( - imageVector = Icons.Outlined.Attachment, + imageVector = if (isAudio) Icons.Outlined.GraphicEq else Icons.Outlined.Attachment, contentDescription = null, tint = MaterialTheme.colorScheme.background, modifier = Modifier .size(32.dp) - .rotate(-45f), + .rotate(if (isAudio) 0f else -45f), ) } if (info != null) { From f61a81e7f40aa8ea5af0f0e0ec343553f651ae6b Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 13 Jul 2023 22:00:29 +0200 Subject: [PATCH 12/59] Media: add audio fixture for tests --- .../features/messages/impl/media/local/MediaInfo.kt | 4 ++++ .../impl/media/viewer/MediaViewerStateProvider.kt | 13 ++++++++++++- .../android/libraries/core/mimetype/MimeTypes.kt | 1 + 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/MediaInfo.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/MediaInfo.kt index 3dede8e929..af0f142bd8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/MediaInfo.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/local/MediaInfo.kt @@ -43,3 +43,7 @@ fun aPdfInfo(): MediaInfo = MediaInfo( fun aFileInfo(): MediaInfo = MediaInfo( "an apk file.apk", MimeTypes.Apk, "50MB", "apk" ) + +fun anAudioInfo(): MediaInfo = MediaInfo( + "an audio file.mp3", MimeTypes.Mp3, "7MB", "mp3" +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt index 786ec984b7..820a34d8d4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerStateProvider.kt @@ -23,6 +23,7 @@ import io.element.android.features.messages.impl.media.local.MediaInfo import io.element.android.features.messages.impl.media.local.aFileInfo import io.element.android.features.messages.impl.media.local.aPdfInfo import io.element.android.features.messages.impl.media.local.aVideoInfo +import io.element.android.features.messages.impl.media.local.anAudioInfo import io.element.android.features.messages.impl.media.local.anImageInfo import io.element.android.libraries.architecture.Async @@ -59,7 +60,17 @@ open class MediaViewerStateProvider : PreviewParameterProvider LocalMedia(Uri.EMPTY, aFileInfo()) ), aFileInfo(), - ) + ), + aMediaViewerState( + Async.Loading(), + anAudioInfo(), + ), + aMediaViewerState( + Async.Success( + LocalMedia(Uri.EMPTY, anAudioInfo()) + ), + anAudioInfo(), + ), ) } diff --git a/libraries/core/src/main/kotlin/io/element/android/libraries/core/mimetype/MimeTypes.kt b/libraries/core/src/main/kotlin/io/element/android/libraries/core/mimetype/MimeTypes.kt index 637d2c056c..c6373fbbf6 100644 --- a/libraries/core/src/main/kotlin/io/element/android/libraries/core/mimetype/MimeTypes.kt +++ b/libraries/core/src/main/kotlin/io/element/android/libraries/core/mimetype/MimeTypes.kt @@ -38,6 +38,7 @@ object MimeTypes { const val Audio = "audio/*" const val Ogg = "audio/ogg" + const val Mp3 = "audio/mp3" const val PlainText = "text/plain" From e2bdeed2fbc4c287c165834c07ad8e750c3112e5 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 13 Jul 2023 23:49:55 +0200 Subject: [PATCH 13/59] ProgressDialog: add cancelable... --- .../designsystem/components/ProgressDialog.kt | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ProgressDialog.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ProgressDialog.kt index 140ab131ef..0154fb9252 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ProgressDialog.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/ProgressDialog.kt @@ -20,14 +20,17 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.Immutable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -37,21 +40,32 @@ import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.PreviewGroup import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.ui.strings.CommonStrings +import timber.log.Timber @Composable fun ProgressDialog( modifier: Modifier = Modifier, text: String? = null, type: ProgressDialogType = ProgressDialogType.Indeterminate, - onDismiss: () -> Unit = {}, + isCancellable: Boolean = false, + onDismissRequest: () -> Unit = {}, ) { + DisposableEffect(Unit) { + onDispose { + Timber.v("OnDispose progressDialog") + } + } Dialog( - onDismissRequest = onDismiss, + onDismissRequest = onDismissRequest, properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false) ) { ProgressDialogContent( modifier = modifier, text = text, + isCancellable = isCancellable, + onCancelClicked = onDismissRequest, progressIndicator = { when (type) { is ProgressDialogType.Indeterminate -> { @@ -81,6 +95,8 @@ sealed interface ProgressDialogType { private fun ProgressDialogContent( modifier: Modifier = Modifier, text: String? = null, + isCancellable: Boolean = true, + onCancelClicked: () -> Unit = {}, progressIndicator: @Composable () -> Unit = { CircularProgressIndicator( color = MaterialTheme.colorScheme.primary @@ -107,6 +123,17 @@ private fun ProgressDialogContent( color = MaterialTheme.colorScheme.primary, ) } + if (isCancellable) { + Spacer(modifier = Modifier.height(24.dp)) + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.BottomEnd + ) { + TextButton(onClick = onCancelClicked) { + Text(stringResource(id = CommonStrings.action_cancel)) + } + } + } } } } From 1db519ced654cc6c5cff51df05952705e3cfdd29 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 13 Jul 2023 22:07:07 +0000 Subject: [PATCH 14/59] Update dependency com.google.firebase:firebase-bom to v32.2.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ccfb5efdf7..5ff8dd4239 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -65,7 +65,7 @@ android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref android_desugar = "com.android.tools:desugar_jdk_libs:2.0.3" kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } # https://firebase.google.com/docs/android/setup#available-libraries -google_firebase_bom = "com.google.firebase:firebase-bom:32.1.1" +google_firebase_bom = "com.google.firebase:firebase-bom:32.2.0" # AndroidX androidx_material = { module = "com.google.android.material:material", version.ref = "material" } From bff5c9874fcc20c66aa55a1a271596de62c3bc3a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 16 Jul 2023 21:03:31 +0000 Subject: [PATCH 15/59] Update rnkdsh/action-upload-diawi action to v1.5.1 --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6c0bc7f85f..59719a6de4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,7 +45,7 @@ jobs: name: elementx-debug path: | app/build/outputs/apk/debug/*.apk - - uses: rnkdsh/action-upload-diawi@v1.5.0 + - uses: rnkdsh/action-upload-diawi@v1.5.1 id: diawi # Do not fail the whole build if Diawi upload fails continue-on-error: true From 0632d01d866624f17442c6cdbcea0926f207bbb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 17 Jul 2023 08:05:12 +0200 Subject: [PATCH 16/59] Fix sliding sync loop restarts due to expirations Both `NotifiableEventResolver` and `DefaultNotificationDrawerManager` were creating new Rust SDK Clients while processing notifications instead of reusing the already existing one. --- .../io/element/android/appnav/RootFlowNode.kt | 2 +- .../android/appnav/di/MatrixClientsHolder.kt | 66 -------------- changelog.d/880.bugfix | 1 + .../matrix/ui/di/MatrixClientsHolder.kt | 87 +++++++++++++++++++ .../DefaultNotificationDrawerManager.kt | 4 +- .../notifications/NotifiableEventResolver.kt | 6 +- 6 files changed, 96 insertions(+), 70 deletions(-) create mode 100644 changelog.d/880.bugfix create mode 100644 libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/di/MatrixClientsHolder.kt diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index 4150bcaebc..71f80dae3e 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -37,7 +37,7 @@ import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.appnav.di.MatrixClientsHolder +import io.element.android.libraries.matrix.ui.di.MatrixClientsHolder import io.element.android.appnav.intent.IntentResolver import io.element.android.appnav.intent.ResolvedIntent import io.element.android.appnav.root.RootPresenter diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt b/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt index bbb14d4d29..53a49aef9f 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt @@ -16,69 +16,3 @@ package io.element.android.appnav.di -import com.bumble.appyx.core.state.MutableSavedStateMap -import com.bumble.appyx.core.state.SavedStateMap -import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import io.element.android.libraries.matrix.api.core.SessionId -import kotlinx.coroutines.runBlocking -import timber.log.Timber -import java.util.concurrent.ConcurrentHashMap -import javax.inject.Inject - -private const val SAVE_INSTANCE_KEY = "io.element.android.x.di.MatrixClientsHolder.SaveInstanceKey" - -class MatrixClientsHolder @Inject constructor(private val authenticationService: MatrixAuthenticationService) { - - private val sessionIdsToMatrixClient = ConcurrentHashMap() - - fun add(matrixClient: MatrixClient) { - sessionIdsToMatrixClient[matrixClient.sessionId] = matrixClient - } - - fun removeAll() { - sessionIdsToMatrixClient.clear() - } - - fun remove(sessionId: SessionId) { - sessionIdsToMatrixClient.remove(sessionId) - } - - fun isEmpty(): Boolean = sessionIdsToMatrixClient.isEmpty() - - fun knowSession(sessionId: SessionId): Boolean = sessionIdsToMatrixClient.containsKey(sessionId) - - fun getOrNull(sessionId: SessionId): MatrixClient? { - return sessionIdsToMatrixClient[sessionId] - } - - @Suppress("UNCHECKED_CAST") - fun restore(state: SavedStateMap?) { - Timber.d("Restore state") - if (state == null || sessionIdsToMatrixClient.isNotEmpty()) return Unit.also { - Timber.w("Restore with non-empty map") - } - val sessionIds = state[SAVE_INSTANCE_KEY] as? Array - Timber.d("Restore matrix session keys = ${sessionIds?.map { it.value }}") - if (sessionIds.isNullOrEmpty()) return - // Not ideal but should only happens in case of process recreation. This ensure we restore all the active sessions before restoring the node graphs. - runBlocking { - sessionIds.forEach { sessionId -> - Timber.d("Restore matrix session: $sessionId") - authenticationService.restoreSession(sessionId) - .onSuccess { matrixClient -> - add(matrixClient) - } - .onFailure { - Timber.e("Fail to restore session") - } - } - } - } - - fun save(state: MutableSavedStateMap) { - val sessionKeys = sessionIdsToMatrixClient.keys.toTypedArray() - Timber.d("Save matrix session keys = ${sessionKeys.map { it.value }}") - state[SAVE_INSTANCE_KEY] = sessionKeys - } -} diff --git a/changelog.d/880.bugfix b/changelog.d/880.bugfix new file mode 100644 index 0000000000..b6d46820a3 --- /dev/null +++ b/changelog.d/880.bugfix @@ -0,0 +1 @@ +Fix sliding sync loop restarts due to expirations. diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/di/MatrixClientsHolder.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/di/MatrixClientsHolder.kt new file mode 100644 index 0000000000..904cbeaed2 --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/di/MatrixClientsHolder.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.matrix.ui.di + +import com.bumble.appyx.core.state.MutableSavedStateMap +import com.bumble.appyx.core.state.SavedStateMap +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.api.core.SessionId +import kotlinx.coroutines.runBlocking +import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject + +private const val SAVE_INSTANCE_KEY = "io.element.android.x.di.MatrixClientsHolder.SaveInstanceKey" + +@SingleIn(AppScope::class) +class MatrixClientsHolder @Inject constructor(private val authenticationService: MatrixAuthenticationService) { + + private val sessionIdsToMatrixClient = ConcurrentHashMap() + + fun add(matrixClient: MatrixClient) { + sessionIdsToMatrixClient[matrixClient.sessionId] = matrixClient + } + + fun removeAll() { + sessionIdsToMatrixClient.clear() + } + + fun remove(sessionId: SessionId) { + sessionIdsToMatrixClient.remove(sessionId) + } + + fun isEmpty(): Boolean = sessionIdsToMatrixClient.isEmpty() + + fun knowSession(sessionId: SessionId): Boolean = sessionIdsToMatrixClient.containsKey(sessionId) + + fun getOrNull(sessionId: SessionId): MatrixClient? { + return sessionIdsToMatrixClient[sessionId] + } + + @Suppress("UNCHECKED_CAST") + fun restore(state: SavedStateMap?) { + Timber.d("Restore state") + if (state == null || sessionIdsToMatrixClient.isNotEmpty()) return Unit.also { + Timber.w("Restore with non-empty map") + } + val sessionIds = state[SAVE_INSTANCE_KEY] as? Array + Timber.d("Restore matrix session keys = ${sessionIds?.map { it.value }}") + if (sessionIds.isNullOrEmpty()) return + // Not ideal but should only happens in case of process recreation. This ensure we restore all the active sessions before restoring the node graphs. + runBlocking { + sessionIds.forEach { sessionId -> + Timber.d("Restore matrix session: $sessionId") + authenticationService.restoreSession(sessionId) + .onSuccess { matrixClient -> + add(matrixClient) + } + .onFailure { + Timber.e("Fail to restore session") + } + } + } + } + + fun save(state: MutableSavedStateMap) { + val sessionKeys = sessionIdsToMatrixClient.keys.toTypedArray() + Timber.d("Save matrix session keys = ${sessionKeys.map { it.value }}") + state[SAVE_INSTANCE_KEY] = sessionKeys + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt index cd0274016b..c001c4ab6e 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.push.impl.notifications +import io.element.android.libraries.matrix.ui.di.MatrixClientsHolder import io.element.android.libraries.androidutils.throttler.FirstThrottler import io.element.android.libraries.core.cache.CircularCache import io.element.android.libraries.core.coroutine.CoroutineDispatchers @@ -60,6 +61,7 @@ class DefaultNotificationDrawerManager @Inject constructor( private val dispatchers: CoroutineDispatchers, private val buildMeta: BuildMeta, private val matrixAuthenticationService: MatrixAuthenticationService, + private val matrixClientsHolder: MatrixClientsHolder, ) : NotificationDrawerManager { /** * Lazily initializes the NotificationState as we rely on having a current session in order to fetch the persisted queue of events. @@ -255,7 +257,7 @@ class DefaultNotificationDrawerManager @Inject constructor( val currentUser = tryOrNull( onError = { Timber.e(it, "Unable to retrieve info for user ${sessionId.value}") }, operation = { - val client = matrixAuthenticationService.restoreSession(sessionId).getOrNull() + val client = matrixClientsHolder.getOrNull(sessionId) // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash val myUserDisplayName = client?.loadUserDisplayName()?.getOrNull() ?: sessionId.value diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt index f0b70d8fac..74189276a1 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.push.impl.notifications +import io.element.android.libraries.matrix.ui.di.MatrixClientsHolder import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService @@ -62,12 +63,13 @@ class NotifiableEventResolver @Inject constructor( private val matrixAuthenticationService: MatrixAuthenticationService, private val buildMeta: BuildMeta, private val clock: SystemClock, + private val matrixClientsHolder: MatrixClientsHolder, ) { suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? { // Restore session - val session = matrixAuthenticationService.restoreSession(sessionId).getOrNull() ?: return null - val notificationService = session.notificationService() + val client = matrixClientsHolder.getOrNull(sessionId) ?: return null + val notificationService = client.notificationService() val notificationData = notificationService.getNotification( userId = sessionId, roomId = roomId, From e2549a8308cc478a645962f8a220a40281f4b5ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 17 Jul 2023 15:05:13 +0200 Subject: [PATCH 17/59] Try to centralise session restoration through `MatrixClientsHolder` --- .../io/element/android/appnav/RootFlowNode.kt | 10 ++--- .../impl/auth/AuthenticationException.kt | 2 +- .../matrix/ui/di/MatrixClientsHolder.kt | 37 ++++++++++++++----- .../DefaultNotificationDrawerManager.kt | 6 +-- .../notifications/NotifiableEventResolver.kt | 2 +- 5 files changed, 36 insertions(+), 21 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index 71f80dae3e..adc1c3384d 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -145,14 +145,10 @@ class RootFlowNode @AssistedInject constructor( onFailure: () -> Unit = {}, onSuccess: (SessionId) -> Unit = {}, ) { - // If the session is already known it'll be restored by the node hierarchy - if (matrixClientsHolder.knowSession(sessionId)) { - Timber.v("Session $sessionId already alive, no need to restore.") - return + runCatching { + matrixClientsHolder.requireSession(sessionId) } - authenticationService.restoreSession(sessionId) - .onSuccess { matrixClient -> - matrixClientsHolder.add(matrixClient) + .onSuccess { Timber.v("Succeed to restore session $sessionId") onSuccess(sessionId) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt index c264a95f67..26a9fba38f 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt @@ -35,6 +35,6 @@ fun Throwable.mapAuthenticationException(): Throwable { is RustAuthenticationException.OidcNotSupported -> AuthenticationException.OidcError("OidcNotSupported", message!!) */ - else -> this + else -> AuthenticationException.Generic(this.message ?: "Unknown error") } } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/di/MatrixClientsHolder.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/di/MatrixClientsHolder.kt index 904cbeaed2..a6db7a1c9e 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/di/MatrixClientsHolder.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/di/MatrixClientsHolder.kt @@ -21,12 +21,16 @@ import com.bumble.appyx.core.state.SavedStateMap import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.auth.AuthenticationException import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.core.SessionId import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import timber.log.Timber import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject +import kotlin.jvm.Throws private const val SAVE_INSTANCE_KEY = "io.element.android.x.di.MatrixClientsHolder.SaveInstanceKey" @@ -34,8 +38,9 @@ private const val SAVE_INSTANCE_KEY = "io.element.android.x.di.MatrixClientsHold class MatrixClientsHolder @Inject constructor(private val authenticationService: MatrixAuthenticationService) { private val sessionIdsToMatrixClient = ConcurrentHashMap() + private val restoreMutex = Mutex() - fun add(matrixClient: MatrixClient) { + private fun add(matrixClient: MatrixClient) { sessionIdsToMatrixClient[matrixClient.sessionId] = matrixClient } @@ -55,6 +60,27 @@ class MatrixClientsHolder @Inject constructor(private val authenticationService: return sessionIdsToMatrixClient[sessionId] } + @Throws(AuthenticationException::class) + suspend fun requireSession(sessionId: SessionId): MatrixClient { + return restoreMutex.withLock { + when (val matrixClient = sessionIdsToMatrixClient[sessionId]) { + null -> restore(sessionId).getOrThrow() + else -> matrixClient + } + } + } + + private suspend fun restore(sessionId: SessionId): Result { + Timber.d("Restore matrix session: $sessionId") + return authenticationService.restoreSession(sessionId) + .onSuccess { matrixClient -> + add(matrixClient) + } + .onFailure { + Timber.e("Fail to restore session") + } + } + @Suppress("UNCHECKED_CAST") fun restore(state: SavedStateMap?) { Timber.d("Restore state") @@ -67,14 +93,7 @@ class MatrixClientsHolder @Inject constructor(private val authenticationService: // Not ideal but should only happens in case of process recreation. This ensure we restore all the active sessions before restoring the node graphs. runBlocking { sessionIds.forEach { sessionId -> - Timber.d("Restore matrix session: $sessionId") - authenticationService.restoreSession(sessionId) - .onSuccess { matrixClient -> - add(matrixClient) - } - .onFailure { - Timber.e("Fail to restore session") - } + restore(sessionId) } } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt index c001c4ab6e..dc484c29a3 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt @@ -257,11 +257,11 @@ class DefaultNotificationDrawerManager @Inject constructor( val currentUser = tryOrNull( onError = { Timber.e(it, "Unable to retrieve info for user ${sessionId.value}") }, operation = { - val client = matrixClientsHolder.getOrNull(sessionId) + val client = matrixClientsHolder.requireSession(sessionId) // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash - val myUserDisplayName = client?.loadUserDisplayName()?.getOrNull() ?: sessionId.value - val userAvatarUrl = client?.loadUserAvatarURLString()?.getOrNull() + val myUserDisplayName = client.loadUserDisplayName().getOrNull() ?: sessionId.value + val userAvatarUrl = client.loadUserAvatarURLString().getOrNull() MatrixUser( userId = sessionId, displayName = myUserDisplayName, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt index 74189276a1..ad55bddb54 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt @@ -68,7 +68,7 @@ class NotifiableEventResolver @Inject constructor( suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? { // Restore session - val client = matrixClientsHolder.getOrNull(sessionId) ?: return null + val client = matrixClientsHolder.requireSession(sessionId) val notificationService = client.notificationService() val notificationData = notificationService.getNotification( userId = sessionId, From abe7e952a3d8ae94f986151a239b39302086fbbd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 17 Jul 2023 15:50:08 +0200 Subject: [PATCH 18/59] Map ClientException. --- .../matrix/api/exception/ClientException.kt | 22 +++++++++++++++ .../auth/RustMatrixAuthenticationService.kt | 3 ++- .../matrix/impl/exception/ClientException.kt | 27 +++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ClientException.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ClientException.kt diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ClientException.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ClientException.kt new file mode 100644 index 0000000000..52dbd2eb12 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/exception/ClientException.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.matrix.api.exception + +sealed class ClientException(message: String) : Exception(message) { + class Generic(message: String) : ClientException(message) + class Other(message: String) : ClientException(message) +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt index bdb87b298c..5b8d175d40 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt @@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.impl.RustMatrixClient +import io.element.android.libraries.matrix.impl.exception.mapClientException import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.sessionstorage.api.SessionStore @@ -94,7 +95,7 @@ class RustMatrixAuthenticationService @Inject constructor( throw IllegalStateException("No session to restore with id $sessionId") } }.mapFailure { failure -> - failure.mapAuthenticationException() + failure.mapClientException() } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ClientException.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ClientException.kt new file mode 100644 index 0000000000..a72755d129 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ClientException.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.matrix.impl.exception + +import io.element.android.libraries.matrix.api.exception.ClientException +import org.matrix.rustcomponents.sdk.ClientException as RustClientException + +fun Throwable.mapClientException(): Throwable { + return when (this) { + is RustClientException.Generic -> ClientException.Generic(msg) + else -> ClientException.Other(message ?: "Unknown error") + } +} From 004b86b05d4a852aaac76044813d00cd1f06cc22 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Mon, 17 Jul 2023 16:22:29 +0200 Subject: [PATCH 19/59] MapLibre compose wrapper library (#877) Heavily inspired from https://github.com/googlemaps/android-maps-compose It doesn't aim to be a full featured library like android-maps-compose, it's been stripped down to only handle our use cases. Related to: https://github.com/vector-im/element-meta/issues/1674 https://github.com/vector-im/element-meta/issues/1682 --- build.gradle.kts | 3 + gradle/libs.versions.toml | 1 + libraries/maplibre-compose/build.gradle.kts | 34 +++ .../libraries/maplibre/compose/CameraMode.kt | 57 ++++ .../compose/CameraMoveStartedReason.kt | 57 ++++ .../maplibre/compose/CameraPositionState.kt | 189 +++++++++++++ .../libraries/maplibre/compose/IconAnchor.kt | 48 ++++ .../libraries/maplibre/compose/MapApplier.kt | 67 +++++ .../maplibre/compose/MapLocationSettings.kt | 31 +++ .../compose/MapSymbolManagerSettings.kt | 31 +++ .../maplibre/compose/MapUiSettings.kt | 41 +++ .../libraries/maplibre/compose/MapUpdater.kt | 154 +++++++++++ .../libraries/maplibre/compose/MapboxMap.kt | 251 ++++++++++++++++++ .../maplibre/compose/MapboxMapComposable.kt | 39 +++ .../libraries/maplibre/compose/Symbol.kt | 124 +++++++++ tools/detekt/detekt.yml | 2 +- tools/detekt/license.template | 10 +- 17 files changed, 1133 insertions(+), 6 deletions(-) create mode 100644 libraries/maplibre-compose/build.gradle.kts create mode 100644 libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraMode.kt create mode 100644 libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraMoveStartedReason.kt create mode 100644 libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraPositionState.kt create mode 100644 libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/IconAnchor.kt create mode 100644 libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapApplier.kt create mode 100644 libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapLocationSettings.kt create mode 100644 libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapSymbolManagerSettings.kt create mode 100644 libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapUiSettings.kt create mode 100644 libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapUpdater.kt create mode 100644 libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapboxMap.kt create mode 100644 libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapboxMapComposable.kt create mode 100644 libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/Symbol.kt diff --git a/build.gradle.kts b/build.gradle.kts index e9adcdee63..02c3ca3043 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -259,6 +259,9 @@ koverMerged { excludes += "io.element.android.libraries.designsystem.swipe.SwipeableActionsState*" excludes += "io.element.android.features.messages.impl.timeline.components.ExpandableState*" excludes += "io.element.android.features.messages.impl.timeline.model.bubble.BubbleState*" + excludes += "io.element.android.libraries.maplibre.compose.CameraPositionState*" + excludes += "io.element.android.libraries.maplibre.compose.SaveableCameraPositionState" + excludes += "io.element.android.libraries.maplibre.compose.SymbolState*" } bound { minValue = 90 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5eeb7a3fa5..d7658a9173 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -158,6 +158,7 @@ vanniktech_emoji = "com.vanniktech:emoji-google:0.16.0" telephoto_zoomableimage = { module = "me.saket.telephoto:zoomable-image-coil", version.ref = "telephoto" } statemachine = "com.freeletics.flowredux:compose:1.1.0" maplibre = "org.maplibre.gl:android-sdk:10.2.0" +maplibre_ktx = "org.maplibre.gl:android-sdk-ktx-v7:2.0.0" maplibre_annotation = "org.maplibre.gl:android-plugin-annotation-v9:2.0.0" # Analytics diff --git a/libraries/maplibre-compose/build.gradle.kts b/libraries/maplibre-compose/build.gradle.kts new file mode 100644 index 0000000000..e2a9b821ba --- /dev/null +++ b/libraries/maplibre-compose/build.gradle.kts @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2022 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 + * + * http://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. + */ + +plugins { + id("io.element.android-compose-library") + id("kotlin-parcelize") +} + +android { + namespace = "io.element.android.libraries.maplibre.compose" + + kotlinOptions { + freeCompilerArgs += "-Xexplicit-api=strict" + } +} + +dependencies { + api(libs.maplibre) + api(libs.maplibre.ktx) + api(libs.maplibre.annotation) +} diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraMode.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraMode.kt new file mode 100644 index 0000000000..0c85d3dfb3 --- /dev/null +++ b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraMode.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * Copyright 2021 Google LLC + * Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose) + * + * 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 + * + * http://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.maplibre.compose + +import androidx.compose.runtime.Immutable +import com.mapbox.mapboxsdk.location.modes.CameraMode as InternalCameraMode + +@Immutable +public enum class CameraMode { + NONE, + NONE_COMPASS, + NONE_GPS, + TRACKING, + TRACKING_COMPASS, + TRACKING_GPS, + TRACKING_GPS_NORTH; + + @InternalCameraMode.Mode + internal fun toInternal(): Int = when (this) { + NONE -> InternalCameraMode.NONE + NONE_COMPASS -> InternalCameraMode.NONE_COMPASS + NONE_GPS -> InternalCameraMode.NONE_GPS + TRACKING -> InternalCameraMode.TRACKING + TRACKING_COMPASS -> InternalCameraMode.TRACKING_COMPASS + TRACKING_GPS -> InternalCameraMode.TRACKING_GPS + TRACKING_GPS_NORTH -> InternalCameraMode.TRACKING_GPS_NORTH + } + + internal companion object { + fun fromInternal(@InternalCameraMode.Mode mode: Int): CameraMode = when (mode) { + InternalCameraMode.NONE -> NONE + InternalCameraMode.NONE_COMPASS -> NONE_COMPASS + InternalCameraMode.NONE_GPS -> NONE_GPS + InternalCameraMode.TRACKING -> TRACKING + InternalCameraMode.TRACKING_COMPASS -> TRACKING_COMPASS + InternalCameraMode.TRACKING_GPS -> TRACKING_GPS + InternalCameraMode.TRACKING_GPS_NORTH -> TRACKING_GPS_NORTH + else -> error("Unknown camera mode: $mode") + } + } +} diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraMoveStartedReason.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraMoveStartedReason.kt new file mode 100644 index 0000000000..10c9d8b69a --- /dev/null +++ b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraMoveStartedReason.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * Copyright 2021 Google LLC + * Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose) + * + * 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 + * + * http://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.maplibre.compose + +import androidx.compose.runtime.Immutable +import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener.REASON_API_ANIMATION +import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener.REASON_API_GESTURE +import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener.REASON_DEVELOPER_ANIMATION + +/** + * Enumerates the different reasons why the map camera started to move. + * + * Based on enum values from https://docs.maptiler.com/maplibre-gl-native-android/com.mapbox.mapboxsdk.maps/#oncameramovestartedlistener. + * + * [NO_MOVEMENT_YET] is used as the initial state before any map movement has been observed. + * + * [UNKNOWN] is used to represent when an unsupported integer value is provided to [fromInt] - this + * may be a new constant value from the Maps SDK that isn't supported by maps-compose yet, in which + * case this library should be updated to include a new enum value for that constant. + */ +@Immutable +public enum class CameraMoveStartedReason(public val value: Int) { + UNKNOWN(-2), + NO_MOVEMENT_YET(-1), + GESTURE(REASON_API_GESTURE), + API_ANIMATION(REASON_API_ANIMATION), + DEVELOPER_ANIMATION(REASON_DEVELOPER_ANIMATION); + + public companion object { + /** + * Converts from the Maps SDK [com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener] + * constants to [CameraMoveStartedReason], or returns [UNKNOWN] if there is no such + * [CameraMoveStartedReason] for the given [value]. + * + * See https://docs.maptiler.com/maplibre-gl-native-android/com.mapbox.mapboxsdk.maps/#oncameramovestartedlistener. + */ + public fun fromInt(value: Int): CameraMoveStartedReason { + return values().firstOrNull { it.value == value } ?: return UNKNOWN + } + } +} diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraPositionState.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraPositionState.kt new file mode 100644 index 0000000000..114e6acc02 --- /dev/null +++ b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/CameraPositionState.kt @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * Copyright 2021 Google LLC + * Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose) + * + * 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 + * + * http://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.maplibre.compose + +import android.location.Location +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.runtime.staticCompositionLocalOf +import com.mapbox.mapboxsdk.camera.CameraPosition +import com.mapbox.mapboxsdk.camera.CameraUpdateFactory +import com.mapbox.mapboxsdk.maps.MapboxMap +import com.mapbox.mapboxsdk.maps.Projection +import kotlinx.parcelize.Parcelize + +/** + * Create and [rememberSaveable] a [CameraPositionState] using [CameraPositionState.Saver]. + * [init] will be called when the [CameraPositionState] is first created to configure its + * initial state. + */ +@Composable +public inline fun rememberCameraPositionState( + key: String? = null, + crossinline init: CameraPositionState.() -> Unit = {} +): CameraPositionState = rememberSaveable(key = key, saver = CameraPositionState.Saver) { + CameraPositionState().apply(init) +} + +/** + * A state object that can be hoisted to control and observe the map's camera state. + * A [CameraPositionState] may only be used by a single [MapboxMap] composable at a time + * as it reflects instance state for a single view of a map. + * + * @param position the initial camera position + * @param cameraMode the initial camera mode + */ +public class CameraPositionState( + position: CameraPosition = CameraPosition.Builder().build(), + cameraMode: CameraMode = CameraMode.NONE, +) { + /** + * Whether the camera is currently moving or not. This includes any kind of movement: + * panning, zooming, or rotation. + */ + public var isMoving: Boolean by mutableStateOf(false) + internal set + + /** + * The reason for the start of the most recent camera moment, or + * [CameraMoveStartedReason.NO_MOVEMENT_YET] if the camera hasn't moved yet or + * [CameraMoveStartedReason.UNKNOWN] if an unknown constant is received from the Maps SDK. + */ + public var cameraMoveStartedReason: CameraMoveStartedReason by mutableStateOf( + CameraMoveStartedReason.NO_MOVEMENT_YET + ) + internal set + + /** + * Returns the current [Projection] to be used for converting between screen + * coordinates and lat/lng. + */ + public val projection: Projection? + get() = map?.projection + + /** + * Local source of truth for the current camera position. + * While [map] is non-null this reflects the current position of [map] as it changes. + * While [map] is null it reflects the last known map position, or the last value set by + * explicitly setting [position]. + */ + internal var rawPosition by mutableStateOf(position) + + /** + * Current position of the camera on the map. + */ + public var position: CameraPosition + get() = rawPosition + set(value) { + synchronized(lock) { + val map = map + if (map == null) { + rawPosition = value + } else { + map.moveCamera(CameraUpdateFactory.newCameraPosition(value)) + } + } + } + + /** + * Local source of truth for the current camera mode. + * While [map] is non-null this reflects the current camera mode as it changes. + * While [map] is null it reflects the last known camera mode, or the last value set by + * explicitly setting [cameraMode]. + */ + internal var rawCameraMode by mutableStateOf(cameraMode) + + /** + * Current tracking mode of the camera. + */ + public var cameraMode: CameraMode + get() = rawCameraMode + set(value) { + synchronized(lock) { + val map = map + if (map == null) { + rawCameraMode = value + } else { + map.locationComponent.cameraMode = value.toInternal() + } + } + } + + /** + * The user's last available location. + */ + public var location: Location? by mutableStateOf(null) + internal set + + // Used to perform side effects thread-safely. + // Guards all mutable properties that are not `by mutableStateOf`. + private val lock = Unit + + // The map currently associated with this CameraPositionState. + // Guarded by `lock`. + private var map: MapboxMap? by mutableStateOf(null) + + // The current map is set and cleared by side effect. + // There can be only one associated at a time. + internal fun setMap(map: MapboxMap?) { + synchronized(lock) { + if (this.map == null && map == null) return + if (this.map != null && map != null) { + error("CameraPositionState may only be associated with one MapboxMap at a time") + } + this.map = map + if (map == null) { + isMoving = false + } else { + map.moveCamera(CameraUpdateFactory.newCameraPosition(position)) + map.locationComponent.cameraMode = cameraMode.toInternal() + } + } + } + + public companion object { + /** + * The default saver implementation for [CameraPositionState]. + */ + public val Saver: Saver = Saver( + save = { SaveableCameraPositionState(it.position, it.cameraMode.toInternal()) }, + restore = { CameraPositionState(it.position, CameraMode.fromInternal(it.cameraMode)) } + ) + } +} + +/** Provides the [CameraPositionState] used by the map. */ +internal val LocalCameraPositionState = staticCompositionLocalOf { CameraPositionState() } + +/** The current [CameraPositionState] used by the map. */ +public val currentCameraPositionState: CameraPositionState + @[MapboxMapComposable ReadOnlyComposable Composable] + get() = LocalCameraPositionState.current + +@Parcelize +public data class SaveableCameraPositionState( + val position: CameraPosition, + val cameraMode: Int +) : Parcelable diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/IconAnchor.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/IconAnchor.kt new file mode 100644 index 0000000000..25f6f38c66 --- /dev/null +++ b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/IconAnchor.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * Copyright 2021 Google LLC + * Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose) + * + * 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 + * + * http://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.maplibre.compose + +import androidx.compose.runtime.Immutable +import com.mapbox.mapboxsdk.style.layers.Property + +@Immutable +public enum class IconAnchor { + CENTER, + LEFT, + RIGHT, + TOP, + BOTTOM, + TOP_LEFT, + TOP_RIGHT, + BOTTOM_LEFT, + BOTTOM_RIGHT; + + @Property.ICON_ANCHOR + internal fun toInternal(): String = when (this) { + CENTER -> Property.ICON_ANCHOR_CENTER + LEFT -> Property.ICON_ANCHOR_LEFT + RIGHT -> Property.ICON_ANCHOR_RIGHT + TOP -> Property.ICON_ANCHOR_TOP + BOTTOM -> Property.ICON_ANCHOR_BOTTOM + TOP_LEFT -> Property.ICON_ANCHOR_TOP_LEFT + TOP_RIGHT -> Property.ICON_ANCHOR_TOP_RIGHT + BOTTOM_LEFT -> Property.ICON_ANCHOR_BOTTOM_LEFT + BOTTOM_RIGHT -> Property.ICON_ANCHOR_BOTTOM_RIGHT + } +} diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapApplier.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapApplier.kt new file mode 100644 index 0000000000..b6cfff034a --- /dev/null +++ b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapApplier.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * Copyright 2021 Google LLC + * Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose) + * + * 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 + * + * http://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.maplibre.compose + +import androidx.compose.runtime.AbstractApplier +import com.mapbox.mapboxsdk.maps.MapboxMap +import com.mapbox.mapboxsdk.maps.Style +import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager + +internal interface MapNode { + fun onAttached() {} + fun onRemoved() {} + fun onCleared() {} +} + +private object MapNodeRoot : MapNode + +internal class MapApplier( + val map: MapboxMap, + val style: Style, + val symbolManager: SymbolManager, +) : AbstractApplier(MapNodeRoot) { + + private val decorations = mutableListOf() + + override fun onClear() { + symbolManager.deleteAll() + decorations.forEach { it.onCleared() } + decorations.clear() + } + + override fun insertBottomUp(index: Int, instance: MapNode) { + decorations.add(index, instance) + instance.onAttached() + } + + override fun insertTopDown(index: Int, instance: MapNode) { + // insertBottomUp is preferred + } + + override fun move(from: Int, to: Int, count: Int) { + decorations.move(from, to, count) + } + + override fun remove(index: Int, count: Int) { + repeat(count) { + decorations[index + it].onRemoved() + } + decorations.remove(index, count) + } +} diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapLocationSettings.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapLocationSettings.kt new file mode 100644 index 0000000000..4b7b7005f2 --- /dev/null +++ b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapLocationSettings.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * Copyright 2021 Google LLC + * Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose) + * + * 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 + * + * http://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.maplibre.compose + +internal val DefaultMapLocationSettings = MapLocationSettings() + +/** + * Data class for UI-related settings on the map. + * + * Note: Should not be a data class if in need of maintaining binary compatibility + * on future changes. See: https://jakewharton.com/public-api-challenges-in-kotlin/ + */ +public data class MapLocationSettings( + public val locationEnabled: Boolean = false, +) diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapSymbolManagerSettings.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapSymbolManagerSettings.kt new file mode 100644 index 0000000000..4bd2ff9e1e --- /dev/null +++ b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapSymbolManagerSettings.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * Copyright 2021 Google LLC + * Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose) + * + * 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 + * + * http://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.maplibre.compose + +internal val DefaultMapSymbolManagerSettings = MapSymbolManagerSettings() + +/** + * Data class for UI-related settings on the map. + * + * Note: Should not be a data class if in need of maintaining binary compatibility + * on future changes. See: https://jakewharton.com/public-api-challenges-in-kotlin/ + */ +public data class MapSymbolManagerSettings( + public val iconAllowOverlap: Boolean = false, +) diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapUiSettings.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapUiSettings.kt new file mode 100644 index 0000000000..a18c05a8f9 --- /dev/null +++ b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapUiSettings.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * Copyright 2021 Google LLC + * Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose) + * + * 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 + * + * http://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.maplibre.compose + +import android.view.Gravity +import androidx.compose.ui.graphics.Color + +internal val DefaultMapUiSettings = MapUiSettings() + +/** + * Data class for UI-related settings on the map. + * + * Note: Should not be a data class if in need of maintaining binary compatibility + * on future changes. See: https://jakewharton.com/public-api-challenges-in-kotlin/ + */ +public data class MapUiSettings( + public val compassEnabled: Boolean = true, + public val rotationGesturesEnabled: Boolean = true, + public val scrollGesturesEnabled: Boolean = true, + public val tiltGesturesEnabled: Boolean = true, + public val zoomGesturesEnabled: Boolean = true, + public val logoGravity: Int = Gravity.BOTTOM, + public val attributionGravity: Int = Gravity.BOTTOM, + public val attributionTintColor: Color = Color.Unspecified, +) diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapUpdater.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapUpdater.kt new file mode 100644 index 0000000000..d7d5f9ca11 --- /dev/null +++ b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapUpdater.kt @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * Copyright 2021 Google LLC + * Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose) + * + * 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 + * + * http://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. + */ +@file:Suppress("MatchingDeclarationName") +package io.element.android.libraries.maplibre.compose + +import android.annotation.SuppressLint +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ComposeNode +import androidx.compose.runtime.currentComposer +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import com.mapbox.mapboxsdk.location.LocationComponentActivationOptions +import com.mapbox.mapboxsdk.location.LocationComponentOptions +import com.mapbox.mapboxsdk.location.OnCameraTrackingChangedListener +import com.mapbox.mapboxsdk.location.engine.LocationEngineRequest +import com.mapbox.mapboxsdk.maps.MapboxMap +import com.mapbox.mapboxsdk.maps.Style + +private const val LOCATION_REQUEST_INTERVAL = 750L + +internal class MapPropertiesNode( + val map: MapboxMap, + style: Style, + context: Context, + cameraPositionState: CameraPositionState, +) : MapNode { + + init { + map.locationComponent.activateLocationComponent( + LocationComponentActivationOptions.Builder(context, style) + .locationComponentOptions( + LocationComponentOptions.builder(context) + .pulseEnabled(true) + .build() + ) + .locationEngineRequest( + LocationEngineRequest.Builder(LOCATION_REQUEST_INTERVAL) + .setPriority(LocationEngineRequest.PRIORITY_HIGH_ACCURACY) + .setFastestInterval(LOCATION_REQUEST_INTERVAL) + .build() + ) + .build() + ) + cameraPositionState.setMap(map) + } + + var cameraPositionState = cameraPositionState + set(value) { + if (value == field) return + field.setMap(null) + field = value + value.setMap(map) + } + + override fun onAttached() { + map.addOnCameraIdleListener { + cameraPositionState.isMoving = false + // addOnCameraIdleListener is only invoked when the camera position + // is changed via .animate(). To handle updating state when .move() + // is used, it's necessary to set the camera's position here as well + cameraPositionState.rawPosition = map.cameraPosition + // Updating user location on every camera move due to lack of a better location updates API. + cameraPositionState.location = map.locationComponent.lastKnownLocation + } + map.addOnCameraMoveCancelListener { + cameraPositionState.isMoving = false + } + map.addOnCameraMoveStartedListener { + cameraPositionState.cameraMoveStartedReason = CameraMoveStartedReason.fromInt(it) + cameraPositionState.isMoving = true + } + map.addOnCameraMoveListener { + cameraPositionState.rawPosition = map.cameraPosition + // Updating user location on every camera move due to lack of a better location updates API. + cameraPositionState.location = map.locationComponent.lastKnownLocation + } + map.locationComponent.addOnCameraTrackingChangedListener(object : OnCameraTrackingChangedListener { + override fun onCameraTrackingDismissed() {} + + override fun onCameraTrackingChanged(currentMode: Int) { + cameraPositionState.rawCameraMode = CameraMode.fromInternal(currentMode) + } + }) + } + + override fun onRemoved() { + cameraPositionState.setMap(null) + } + + override fun onCleared() { + cameraPositionState.setMap(null) + } +} + +/** + * Used to keep the primary map properties up to date. This should never leave the map composition. + */ +@SuppressLint("MissingPermission") +@Suppress("NOTHING_TO_INLINE") +@Composable +internal inline fun MapUpdater( + cameraPositionState: CameraPositionState, + mapLocationSettings: MapLocationSettings, + mapUiSettings: MapUiSettings, + mapSymbolManagerSettings: MapSymbolManagerSettings, +) { + val mapApplier = currentComposer.applier as MapApplier + val map = mapApplier.map + val style = mapApplier.style + val symbolManager = mapApplier.symbolManager + val context = LocalContext.current + ComposeNode( + factory = { + MapPropertiesNode( + map = map, + style = style, + context = context, + cameraPositionState = cameraPositionState, + ) + }, + update = { + set(mapLocationSettings.locationEnabled) { map.locationComponent.isLocationComponentEnabled = it } + + set(mapUiSettings.compassEnabled) { map.uiSettings.isCompassEnabled = it } + set(mapUiSettings.rotationGesturesEnabled) { map.uiSettings.isRotateGesturesEnabled = it } + set(mapUiSettings.scrollGesturesEnabled) { map.uiSettings.isScrollGesturesEnabled = it } + set(mapUiSettings.tiltGesturesEnabled) { map.uiSettings.isTiltGesturesEnabled = it } + set(mapUiSettings.zoomGesturesEnabled) { map.uiSettings.isZoomGesturesEnabled = it } + set(mapUiSettings.logoGravity) { map.uiSettings.logoGravity = it } + set(mapUiSettings.attributionGravity) { map.uiSettings.attributionGravity = it } + set(mapUiSettings.attributionTintColor) { map.uiSettings.setAttributionTintColor(it.toArgb()) } + + set(mapSymbolManagerSettings.iconAllowOverlap) { symbolManager.iconAllowOverlap = it } + + update(cameraPositionState) { this.cameraPositionState = it } + } + ) +} diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapboxMap.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapboxMap.kt new file mode 100644 index 0000000000..3c3cf3e44f --- /dev/null +++ b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapboxMap.kt @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * Copyright 2021 Google LLC + * Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose) + * + * 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 + * + * http://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.maplibre.compose + +import android.content.ComponentCallbacks +import android.content.Context +import android.content.res.Configuration +import android.os.Bundle +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Composition +import androidx.compose.runtime.CompositionContext +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCompositionContext +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import com.mapbox.mapboxsdk.Mapbox +import com.mapbox.mapboxsdk.maps.MapView +import com.mapbox.mapboxsdk.maps.MapboxMap +import com.mapbox.mapboxsdk.maps.Style +import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentMapOf +import kotlinx.coroutines.awaitCancellation +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +/** + * A compose container for a MapLibre [MapView]. + * + * Heavily inspired by https://github.com/googlemaps/android-maps-compose + * + * @param styleUri a URI where to asynchronously fetch a style for the map + * @param modifier Modifier to be applied to the MapboxMap + * @param images images added to the map's style to be later used with [Symbol] + * @param cameraPositionState the [CameraPositionState] to be used to control or observe the map's + * camera state + * @param uiSettings the [MapUiSettings] to be used for UI-specific settings on the map + * @param symbolManagerSettings the [MapSymbolManagerSettings] to be used for symbol manager settings + * @param locationSettings the [MapLocationSettings] to be used for location settings + * @param content the content of the map + */ +@Composable +public fun MapboxMap( + styleUri: String, + modifier: Modifier = Modifier, + images: ImmutableMap = persistentMapOf(), + cameraPositionState: CameraPositionState = rememberCameraPositionState(), + uiSettings: MapUiSettings = DefaultMapUiSettings, + symbolManagerSettings: MapSymbolManagerSettings = DefaultMapSymbolManagerSettings, + locationSettings: MapLocationSettings = DefaultMapLocationSettings, + content: (@Composable @MapboxMapComposable () -> Unit)? = null, +) { + // When in preview, early return a Box with the received modifier preserving layout + if (LocalInspectionMode.current) { + @Suppress("ModifierReused") // False positive, the modifier is not reused due to the early return. + Box( + modifier = modifier.background(Color.DarkGray) + ) { + Text("[Map]", modifier = Modifier.align(Alignment.Center)) + } + return + } + + val context = LocalContext.current + val mapView = remember { + Mapbox.getInstance(context) + MapView(context) + } + + @Suppress("ModifierReused") + AndroidView(modifier = modifier, factory = { mapView }) + MapLifecycle(mapView) + + // rememberUpdatedState and friends are used here to make these values observable to + // the subcomposition without providing a new content function each recomposition + val currentCameraPositionState by rememberUpdatedState(cameraPositionState) + val currentUiSettings by rememberUpdatedState(uiSettings) + val currentMapLocationSettings by rememberUpdatedState(locationSettings) + val currentSymbolManagerSettings by rememberUpdatedState(symbolManagerSettings) + + val parentComposition = rememberCompositionContext() + val currentContent by rememberUpdatedState(content) + + LaunchedEffect(styleUri, images) { + disposingComposition { + parentComposition.newComposition( + context = context, + mapView = mapView, + styleUri = styleUri, + images = images, + ) { + MapUpdater( + cameraPositionState = currentCameraPositionState, + mapUiSettings = currentUiSettings, + mapLocationSettings = currentMapLocationSettings, + mapSymbolManagerSettings = currentSymbolManagerSettings, + ) + CompositionLocalProvider( + LocalCameraPositionState provides cameraPositionState, + ) { + currentContent?.invoke() + } + } + } + } +} + +private suspend inline fun disposingComposition(factory: () -> Composition) { + val composition = factory() + try { + awaitCancellation() + } finally { + composition.dispose() + } +} + +private suspend inline fun CompositionContext.newComposition( + context: Context, + mapView: MapView, + styleUri: String, + images: ImmutableMap, + noinline content: @Composable () -> Unit +): Composition { + val map = mapView.awaitMap() + val style = map.awaitStyle(context, styleUri, images) + val symbolManager = SymbolManager(mapView, map, style) + return Composition( + MapApplier(map, style, symbolManager), this + ).apply { + setContent(content) + } +} + +private suspend inline fun MapView.awaitMap(): MapboxMap = suspendCoroutine { continuation -> + getMapAsync { map -> + continuation.resume(map) + } +} + +private suspend inline fun MapboxMap.awaitStyle( + context: Context, + styleUri: String, + images: ImmutableMap, +): Style = suspendCoroutine { continuation -> + setStyle( + Style.Builder().apply { + fromUri(styleUri) + images.forEach { (id, drawableRes) -> + withImage(id, checkNotNull(context.getDrawable(drawableRes)) { + "Drawable resource $drawableRes with id $id not found" + }) + } + } + ) { style -> + continuation.resume(style) + } +} + +/** + * Registers lifecycle observers to the local [MapView]. + */ +@Composable +private fun MapLifecycle(mapView: MapView) { + val context = LocalContext.current + val lifecycle = LocalLifecycleOwner.current.lifecycle + val previousState = remember { mutableStateOf(Lifecycle.Event.ON_CREATE) } + DisposableEffect(context, lifecycle, mapView) { + val mapLifecycleObserver = mapView.lifecycleObserver(previousState) + val callbacks = mapView.componentCallbacks() + + lifecycle.addObserver(mapLifecycleObserver) + context.registerComponentCallbacks(callbacks) + + onDispose { + lifecycle.removeObserver(mapLifecycleObserver) + context.unregisterComponentCallbacks(callbacks) + } + } + DisposableEffect(mapView) { + onDispose { + mapView.onDestroy() + mapView.removeAllViews() + } + } +} + +private fun MapView.lifecycleObserver(previousState: MutableState): LifecycleEventObserver = + LifecycleEventObserver { _, event -> + event.targetState + when (event) { + Lifecycle.Event.ON_CREATE -> { + // Skip calling mapView.onCreate if the lifecycle did not go through onDestroy - in + // this case the MapboxMap composable also doesn't leave the composition. So, + // recreating the map does not restore state properly which must be avoided. + if (previousState.value != Lifecycle.Event.ON_STOP) { + this.onCreate(Bundle()) + } + } + Lifecycle.Event.ON_START -> this.onStart() + Lifecycle.Event.ON_RESUME -> this.onResume() + Lifecycle.Event.ON_PAUSE -> this.onPause() + Lifecycle.Event.ON_STOP -> this.onStop() + Lifecycle.Event.ON_DESTROY -> { + //handled in onDispose + } + else -> throw IllegalStateException() + } + previousState.value = event + } + +private fun MapView.componentCallbacks(): ComponentCallbacks = + object : ComponentCallbacks { + override fun onConfigurationChanged(config: Configuration) {} + + override fun onLowMemory() { + this@componentCallbacks.onLowMemory() + } + } diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapboxMapComposable.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapboxMapComposable.kt new file mode 100644 index 0000000000..15876b0033 --- /dev/null +++ b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/MapboxMapComposable.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * Copyright 2021 Google LLC + * Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose) + * + * 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 + * + * http://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.maplibre.compose + +import androidx.compose.runtime.ComposableTargetMarker + +/** + * An annotation that can be used to mark a composable function as being expected to be use in a + * composable function that is also marked or inferred to be marked as a [MapboxMapComposable]. + * + * This will produce build warnings when [MapboxMapComposable] composable functions are used outside + * of a [MapboxMapComposable] content lambda, and vice versa. + */ +@Retention(AnnotationRetention.BINARY) +@ComposableTargetMarker(description = "MapLibre Map Composable") +@Target( + AnnotationTarget.FILE, + AnnotationTarget.FUNCTION, + AnnotationTarget.PROPERTY_GETTER, + AnnotationTarget.TYPE, + AnnotationTarget.TYPE_PARAMETER, +) +public annotation class MapboxMapComposable diff --git a/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/Symbol.kt b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/Symbol.kt new file mode 100644 index 0000000000..36e8cdc34e --- /dev/null +++ b/libraries/maplibre-compose/src/main/kotlin/io/element/android/libraries/maplibre/compose/Symbol.kt @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * Copyright 2021 Google LLC + * Copied and adapted from android-maps-compose (https://github.com/googlemaps/android-maps-compose) + * + * 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 + * + * http://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.maplibre.compose + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ComposeNode +import androidx.compose.runtime.currentComposer +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import com.mapbox.mapboxsdk.geometry.LatLng +import com.mapbox.mapboxsdk.plugins.annotation.Symbol +import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager +import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions + +internal class SymbolNode( + val symbolManager: SymbolManager, + val symbol: Symbol, +) : MapNode { + override fun onRemoved() { + symbolManager.delete(symbol) + } + + override fun onCleared() { + symbolManager.delete(symbol) + } +} + +/** + * A state object that can be hoisted to control and observe the symbol state. + * + * @param position the initial symbol position + */ +public class SymbolState( + position: LatLng = LatLng(0.0, 0.0) +) { + /** + * Current position of the symbol. + */ + public var position: LatLng by mutableStateOf(position) + + public companion object { + /** + * The default saver implementation for [SymbolState]. + */ + public val Saver: Saver = Saver( + save = { it.position }, + restore = { SymbolState(it) } + ) + } +} + +@Composable +public fun rememberSymbolState( + key: String? = null, + position: LatLng = LatLng(0.0, 0.0) +): SymbolState = rememberSaveable(key = key, saver = SymbolState.Saver) { + SymbolState(position) +} + +/** + * A composable for a symbol on the map. + * + * @param iconId an id of an image from the current [Style] + * @param state the [SymbolState] to be used to control or observe the symbol + * state such as its position and info window + * @param iconAnchor the anchor for the symbol image + */ +@Composable +@MapboxMapComposable +public fun Symbol( + iconId: String, + state: SymbolState = rememberSymbolState(), + iconAnchor: IconAnchor? = null, +) { + val mapApplier = currentComposer.applier as MapApplier + val symbolManager = mapApplier.symbolManager + ComposeNode( + factory = { + SymbolNode( + symbolManager = symbolManager, + symbol = symbolManager.create( + SymbolOptions().apply { + withLatLng(state.position) + withIconImage(iconId) + iconAnchor?.let { withIconAnchor(it.toInternal()) } + } + ), + ) + }, + update = { + update(state.position) { + this.symbol.latLng = it + symbolManager.update(this.symbol) + } + update(iconId) { + this.symbol.iconImage = it + symbolManager.update(this.symbol) + } + update(iconAnchor) { + this.symbol.iconAnchor = it?.toInternal() + symbolManager.update(this.symbol) + } + } + ) +} diff --git a/tools/detekt/detekt.yml b/tools/detekt/detekt.yml index f18f49a358..a3bad54ab3 100644 --- a/tools/detekt/detekt.yml +++ b/tools/detekt/detekt.yml @@ -113,7 +113,7 @@ Compose: CompositionLocalAllowlist: active: true # You can optionally define a list of CompositionLocals that are allowed here - allowedCompositionLocals: LocalCompoundColors, LocalSnackbarDispatcher + allowedCompositionLocals: LocalCompoundColors, LocalSnackbarDispatcher, LocalCameraPositionState CompositionLocalNaming: active: true ContentEmitterReturningValues: diff --git a/tools/detekt/license.template b/tools/detekt/license.template index 63b899da9b..08cadc82f9 100644 --- a/tools/detekt/license.template +++ b/tools/detekt/license.template @@ -1,15 +1,15 @@ -/\* -(.*\n)* \* Copyright \(c\) 20\d\d New Vector Ltd(.*\n)* - \* +\/\* +(?:.*\n)* \* Copyright \(c\) 20\d\d New Vector Ltd +(?:.*\n)* \* \* 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 \* - \* http(s)?://www\.apache\.org/licenses/LICENSE-2\.0 + \* http(?:s)?:\/\/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\. - \*/ + \*\/ From 6681f6f806bf543410368d37a354d3055005db55 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 17 Jul 2023 16:18:54 +0200 Subject: [PATCH 20/59] Remove StableCharSequence, it was useful when we were using the Epoxy library. --- .../messages/impl/MessagesStateProvider.kt | 3 +- .../messagecomposer/MessageComposerEvents.kt | 2 +- .../MessageComposerPresenter.kt | 16 +++---- .../messagecomposer/MessageComposerState.kt | 5 +-- .../MessageComposerStateProvider.kt | 3 +- .../messagecomposer/MessageComposerView.kt | 4 +- .../MessageComposerPresenterTest.kt | 43 +++++++++---------- .../libraries/core/data/StableCharSequence.kt | 31 ------------- .../textcomposer/MessageComposerMode.kt | 8 ++-- .../libraries/textcomposer/TextComposer.kt | 2 +- 10 files changed, 40 insertions(+), 77 deletions(-) delete mode 100644 libraries/core/src/main/kotlin/io/element/android/libraries/core/data/StableCharSequence.kt 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 69cb0fa493..d0ddcf68f4 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 @@ -25,7 +25,6 @@ import io.element.android.features.messages.impl.timeline.components.customreact import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.libraries.architecture.Async -import io.element.android.libraries.core.data.StableCharSequence import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.matrix.api.core.RoomId @@ -48,7 +47,7 @@ fun aMessagesState() = MessagesState( roomAvatar = AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom), userHasPermissionToSendMessage = true, composerState = aMessageComposerState().copy( - text = StableCharSequence("Hello"), + text = "Hello", isFullScreen = false, mode = MessageComposerMode.Normal("Hello"), ), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt index 82fb0982f4..46e57e92de 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt @@ -26,7 +26,7 @@ sealed interface MessageComposerEvents { data class SendMessage(val message: String) : MessageComposerEvents object CloseSpecialMode : MessageComposerEvents data class SetMode(val composerMode: MessageComposerMode) : MessageComposerEvents - data class UpdateText(val text: CharSequence) : MessageComposerEvents + data class UpdateText(val text: String) : MessageComposerEvents object AddAttachment : MessageComposerEvents object DismissAttachmentMenu : MessageComposerEvents sealed interface PickAttachmentSource : MessageComposerEvents { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index f7c80b4320..4d749b465e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -34,8 +34,6 @@ import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.preview.error.sendAttachmentError import io.element.android.features.messages.impl.media.local.LocalMediaFactory import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.core.data.StableCharSequence -import io.element.android.libraries.core.data.toStableCharSequence import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.SnackbarMessage import io.element.android.libraries.di.RoomScope @@ -94,15 +92,15 @@ class MessageComposerPresenter @Inject constructor( val hasFocus = remember { mutableStateOf(false) } - val text: MutableState = remember { - mutableStateOf(StableCharSequence("")) + val text: MutableState = remember { + mutableStateOf("") } var showAttachmentSourcePicker: Boolean by remember { mutableStateOf(false) } LaunchedEffect(messageComposerContext.composerMode) { when (val modeValue = messageComposerContext.composerMode) { - is MessageComposerMode.Edit -> text.value = modeValue.defaultContent.toStableCharSequence() + is MessageComposerMode.Edit -> text.value = modeValue.defaultContent else -> Unit } } @@ -120,9 +118,9 @@ class MessageComposerPresenter @Inject constructor( is MessageComposerEvents.FocusChanged -> hasFocus.value = event.hasFocus - is MessageComposerEvents.UpdateText -> text.value = event.text.toStableCharSequence() + is MessageComposerEvents.UpdateText -> text.value = event.text MessageComposerEvents.CloseSpecialMode -> { - text.value = "".toStableCharSequence() + text.value = "" messageComposerContext.composerMode = MessageComposerMode.Normal("") } @@ -189,11 +187,11 @@ class MessageComposerPresenter @Inject constructor( private fun CoroutineScope.sendMessage( text: String, updateComposerMode: (newComposerMode: MessageComposerMode) -> Unit, - textState: MutableState + textState: MutableState ) = launch { val capturedMode = messageComposerContext.composerMode // Reset composer right away - textState.value = "".toStableCharSequence() + textState.value = "" updateComposerMode(MessageComposerMode.Normal("")) when (capturedMode) { is MessageComposerMode.Normal -> room.sendMessage(text) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt index 2dc6042fb5..28ec14ffeb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt @@ -18,13 +18,12 @@ package io.element.android.features.messages.impl.messagecomposer import androidx.compose.runtime.Immutable import io.element.android.features.messages.impl.attachments.Attachment -import io.element.android.libraries.core.data.StableCharSequence import io.element.android.libraries.textcomposer.MessageComposerMode import kotlinx.collections.immutable.ImmutableList @Immutable data class MessageComposerState( - val text: StableCharSequence?, + val text: String?, val isFullScreen: Boolean, val hasFocus: Boolean, val mode: MessageComposerMode, @@ -32,7 +31,7 @@ data class MessageComposerState( val attachmentsState: AttachmentsState, val eventSink: (MessageComposerEvents) -> Unit ) { - val isSendButtonVisible: Boolean = text?.charSequence.isNullOrEmpty().not() + val isSendButtonVisible: Boolean = text.isNullOrEmpty().not() } @Immutable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt index 0504d3625a..1934154824 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt @@ -17,7 +17,6 @@ package io.element.android.features.messages.impl.messagecomposer import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.core.data.StableCharSequence import io.element.android.libraries.textcomposer.MessageComposerMode open class MessageComposerStateProvider : PreviewParameterProvider { @@ -28,7 +27,7 @@ open class MessageComposerStateProvider : PreviewParameterProvider Unit = {}, onResetComposerMode: () -> Unit = {}, - onComposerTextChange: (CharSequence) -> Unit = {}, + onComposerTextChange: (String) -> Unit = {}, onAddAttachment: () -> Unit = {}, onFocusChanged: (Boolean) -> Unit = {}, ) { From 9247cd765a8247c87766a176654877fe13ed3a82 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Mon, 17 Jul 2023 17:02:06 +0200 Subject: [PATCH 21/59] Fix: make sure we ignore notifications for open rooms (#867) * Make sure we ignore notifications for open rooms - Listen to process lifecycle changes in `AppForegroundStateService`. Use initializers to reliable create it. - Merge `AppNavigationState` with `AppForegroundState`. Renamed the previous `AppNavigationState` to `NavigationState`, created a new `AppNavigationState` which contains both the navigation state and the foreground state. --- app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 12 ++ .../android/appnav/RoomFlowNodeTest.kt | 4 +- libraries/push/impl/build.gradle.kts | 1 - .../DefaultNotificationDrawerManager.kt | 38 ++--- .../notifications/NotifiableEventProcessor.kt | 9 +- .../model/NotifiableMessageEvent.kt | 17 +- .../NotifiableEventProcessorTest.kt | 54 +++++-- services/appnavstate/api/build.gradle.kts | 3 + .../api/AppForegroundStateService.kt | 34 ++++ .../appnavstate/api/AppNavigationState.kt | 43 +---- .../api/AppNavigationStateExtension.kt | 62 -------- .../api/AppNavigationStateService.kt | 6 +- .../appnavstate/api/NavigationState.kt | 58 +++++++ .../api/NavigationStateExtension.kt | 62 ++++++++ services/appnavstate/impl/build.gradle.kts | 2 + .../impl/DefaultAppForegroundStateService.kt | 40 +++++ .../impl/DefaultAppNavigationStateService.kt | 149 ++++++++++-------- .../appnavstate/impl/di/AppNavStateModule.kt | 39 +++++ .../AppForegroundStateServiceInitializer.kt | 33 ++++ ...t => DefaultNavigationStateServiceTest.kt} | 30 ++-- .../impl/FakeAppForegroundStateService.kt | 37 +++++ services/appnavstate/test/build.gradle.kts | 1 + .../appnavstate/test/AppNavStateFixture.kt | 16 +- ...ce.kt => FakeAppNavigationStateService.kt} | 16 +- .../tests/testutils/RunCancellableTest.kt | 31 ++++ 26 files changed, 552 insertions(+), 246 deletions(-) create mode 100644 services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppForegroundStateService.kt delete mode 100644 services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationStateExtension.kt create mode 100644 services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/NavigationState.kt create mode 100644 services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/NavigationStateExtension.kt create mode 100644 services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppForegroundStateService.kt create mode 100644 services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/di/AppNavStateModule.kt create mode 100644 services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/initializer/AppForegroundStateServiceInitializer.kt rename services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/{DefaultAppNavigationStateServiceTest.kt => DefaultNavigationStateServiceTest.kt} (70%) create mode 100644 services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/FakeAppForegroundStateService.kt rename services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/{NoopAppNavigationStateService.kt => FakeAppNavigationStateService.kt} (78%) create mode 100644 tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RunCancellableTest.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index dd198cd5aa..32bfd40629 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -209,6 +209,7 @@ dependencies { implementation(libs.androidx.core) implementation(libs.androidx.corektx) implementation(libs.androidx.lifecycle.runtime) + implementation(libs.androidx.lifecycle.process) implementation(libs.androidx.activity.compose) implementation(libs.androidx.startup) implementation(libs.androidx.preference) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c77fba93e1..2917c5199b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -32,6 +32,18 @@ android:theme="@style/Theme.ElementX" tools:targetApi="33"> + + + + + + {} - is AppNavigationState.Session -> {} - is AppNavigationState.Space -> {} - is AppNavigationState.Room -> { + private fun onAppNavigationStateChange(navigationState: NavigationState) { + when (navigationState) { + NavigationState.Root -> {} + is NavigationState.Session -> {} + is NavigationState.Space -> {} + is NavigationState.Room -> { // Cleanup notification for current room - clearMessagesForRoom(appNavigationState.parentSpace.parentSession.sessionId, appNavigationState.roomId) + clearMessagesForRoom(navigationState.parentSpace.parentSession.sessionId, navigationState.roomId) } - is AppNavigationState.Thread -> { + is NavigationState.Thread -> { onEnteringThread( - appNavigationState.parentRoom.parentSpace.parentSession.sessionId, - appNavigationState.parentRoom.roomId, - appNavigationState.threadId + navigationState.parentRoom.parentSpace.parentSession.sessionId, + navigationState.parentRoom.roomId, + navigationState.threadId ) } } @@ -225,7 +221,7 @@ class DefaultNotificationDrawerManager @Inject constructor( private suspend fun refreshNotificationDrawerBg() { Timber.v("refreshNotificationDrawerBg()") val eventsToRender = notificationState.updateQueuedEvents(this) { queuedEvents, renderedEvents -> - notifiableEventProcessor.process(queuedEvents.rawEvents(), currentAppNavigationState, renderedEvents).also { + notifiableEventProcessor.process(queuedEvents.rawEvents(), renderedEvents).also { queuedEvents.clearAndAdd(it.onlyKeptEvents()) } } @@ -275,8 +271,4 @@ class DefaultNotificationDrawerManager @Inject constructor( notificationRenderer.render(currentUser, useCompleteNotificationFormat, notifiableEvents) } } - - fun shouldIgnoreMessageEventInRoom(resolvedEvent: NotifiableMessageEvent): Boolean { - return resolvedEvent.shouldIgnoreEventInRoom(currentAppNavigationState) - } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt index 4202ef78d4..50f1b88783 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessor.kt @@ -23,7 +23,7 @@ import io.element.android.libraries.push.impl.notifications.model.NotifiableEven import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.SimpleNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreEventInRoom -import io.element.android.services.appnavstate.api.AppNavigationState +import io.element.android.services.appnavstate.api.AppNavigationStateService import timber.log.Timber import javax.inject.Inject @@ -31,18 +31,19 @@ private typealias ProcessedEvents = List> class NotifiableEventProcessor @Inject constructor( private val outdatedDetector: OutdatedEventDetector, + private val appNavigationStateService: AppNavigationStateService, ) { fun process( queuedEvents: List, - appNavigationState: AppNavigationState?, renderedEvents: ProcessedEvents, ): ProcessedEvents { + val appState = appNavigationStateService.appNavigationState.value val processedEvents = queuedEvents.map { val type = when (it) { is InviteNotifiableEvent -> ProcessedEvent.Type.KEEP is NotifiableMessageEvent -> when { - it.shouldIgnoreEventInRoom(appNavigationState) -> { + it.shouldIgnoreEventInRoom(appState) -> { ProcessedEvent.Type.REMOVE .also { Timber.d("notification message removed due to currently viewing the same room or thread") } } @@ -55,7 +56,7 @@ class NotifiableEventProcessor @Inject constructor( else -> ProcessedEvent.Type.KEEP } is FallbackNotifiableEvent -> when { - it.shouldIgnoreEventInRoom(appNavigationState) -> { + it.shouldIgnoreEventInRoom(appState) -> { ProcessedEvent.Type.REMOVE .also { Timber.d("notification fallback removed due to currently viewing the same room or thread") } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt index 7730066d31..57a3eb45aa 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/model/NotifiableMessageEvent.kt @@ -16,8 +16,6 @@ package io.element.android.libraries.push.impl.notifications.model import android.net.Uri -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.ProcessLifecycleOwner import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId @@ -69,18 +67,13 @@ data class NotifiableMessageEvent( /** * Used to check if a notification should be ignored based on the current app and navigation state. */ -fun NotifiableEvent.shouldIgnoreEventInRoom( - appNavigationState: AppNavigationState? -): Boolean { - val currentSessionId = appNavigationState?.currentSessionId() ?: return false - return when (val currentRoomId = appNavigationState.currentRoomId()) { +fun NotifiableEvent.shouldIgnoreEventInRoom(appNavigationState: AppNavigationState): Boolean { + val currentSessionId = appNavigationState.navigationState.currentSessionId() ?: return false + return when (val currentRoomId = appNavigationState.navigationState.currentRoomId()) { null -> false - else -> isAppInForeground + else -> appNavigationState.isInForeground && sessionId == currentSessionId && roomId == currentRoomId - && (this as? NotifiableMessageEvent)?.threadId == appNavigationState.currentThreadId() + && (this as? NotifiableMessageEvent)?.threadId == appNavigationState.navigationState.currentThreadId() } } - -private val isAppInForeground: Boolean - get() = ProcessLifecycleOwner.get().lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessorTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessorTest.kt index a1398ef429..28b001ca28 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessorTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventProcessorTest.kt @@ -30,17 +30,20 @@ import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiable import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent -import io.element.android.services.appnavstate.test.anAppNavigationState +import io.element.android.services.appnavstate.api.NavigationState +import io.element.android.services.appnavstate.api.AppNavigationState +import io.element.android.services.appnavstate.test.FakeAppNavigationStateService +import io.element.android.services.appnavstate.test.aNavigationState +import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Test -private val NOT_VIEWING_A_ROOM = anAppNavigationState() -private val VIEWING_A_ROOM = anAppNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID) -private val VIEWING_A_THREAD = anAppNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID, A_THREAD_ID) +private val NOT_VIEWING_A_ROOM = aNavigationState() +private val VIEWING_A_ROOM = aNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID) +private val VIEWING_A_THREAD = aNavigationState(A_SESSION_ID, A_SPACE_ID, A_ROOM_ID, A_THREAD_ID) class NotifiableEventProcessorTest { private val outdatedDetector = FakeOutdatedEventDetector() - private val eventProcessor = NotifiableEventProcessor(outdatedDetector.instance) @Test fun `given simple events when processing then keep simple events`() { @@ -48,8 +51,9 @@ class NotifiableEventProcessorTest { aSimpleNotifiableEvent(eventId = AN_EVENT_ID), aSimpleNotifiableEvent(eventId = AN_EVENT_ID_2) ) + val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM) - val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = emptyList()) + val result = eventProcessor.process(events, renderedEvents = emptyList()) assertThat(result).isEqualTo( listOfProcessedEvents( @@ -62,8 +66,9 @@ class NotifiableEventProcessorTest { @Test fun `given redacted simple event when processing then remove redaction event`() { val events = listOf(aSimpleNotifiableEvent(eventId = AN_EVENT_ID, type = EventType.REDACTION)) + val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM) - val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = emptyList()) + val result = eventProcessor.process(events, renderedEvents = emptyList()) assertThat(result).isEqualTo( listOfProcessedEvents( @@ -78,8 +83,9 @@ class NotifiableEventProcessorTest { anInviteNotifiableEvent(roomId = A_ROOM_ID), anInviteNotifiableEvent(roomId = A_ROOM_ID_2) ) + val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM) - val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = emptyList()) + val result = eventProcessor.process(events, renderedEvents = emptyList()) assertThat(result).isEqualTo( listOfProcessedEvents( @@ -94,7 +100,9 @@ class NotifiableEventProcessorTest { val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID)) outdatedDetector.givenEventIsOutOfDate(events[0]) - val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = emptyList()) + val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM) + + val result = eventProcessor.process(events, renderedEvents = emptyList()) assertThat(result).isEqualTo( listOfProcessedEvents( @@ -107,8 +115,9 @@ class NotifiableEventProcessorTest { fun `given in date message event when processing then keep message event`() { val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID)) outdatedDetector.givenEventIsInDate(events[0]) + val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM) - val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = emptyList()) + val result = eventProcessor.process(events, renderedEvents = emptyList()) assertThat(result).isEqualTo( listOfProcessedEvents( @@ -121,8 +130,9 @@ class NotifiableEventProcessorTest { fun `given viewing the same room main timeline when processing main timeline message event then removes message`() { val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID, threadId = null)) events.forEach { outdatedDetector.givenEventIsOutOfDate(it) } + val eventProcessor = createProcessor(isInForeground = true, navigationState = VIEWING_A_ROOM) - val result = eventProcessor.process(events, VIEWING_A_ROOM, renderedEvents = emptyList()) + val result = eventProcessor.process(events, renderedEvents = emptyList()) assertThat(result).isEqualTo( listOfProcessedEvents( @@ -135,8 +145,9 @@ class NotifiableEventProcessorTest { fun `given viewing the same thread timeline when processing thread message event then removes message`() { val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID, threadId = A_THREAD_ID)) events.forEach { outdatedDetector.givenEventIsOutOfDate(it) } + val eventProcessor = createProcessor(isInForeground = true, navigationState = VIEWING_A_THREAD) - val result = eventProcessor.process(events, VIEWING_A_THREAD, renderedEvents = emptyList()) + val result = eventProcessor.process(events, renderedEvents = emptyList()) assertThat(result).isEqualTo( listOfProcessedEvents( @@ -149,8 +160,9 @@ class NotifiableEventProcessorTest { fun `given viewing main timeline of the same room when processing thread timeline message event then keep message`() { val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID, threadId = A_THREAD_ID)) outdatedDetector.givenEventIsInDate(events[0]) + val eventProcessor = createProcessor(isInForeground = true, navigationState = VIEWING_A_ROOM) - val result = eventProcessor.process(events, VIEWING_A_ROOM, renderedEvents = emptyList()) + val result = eventProcessor.process(events, renderedEvents = emptyList()) assertThat(result).isEqualTo( listOfProcessedEvents( @@ -163,8 +175,9 @@ class NotifiableEventProcessorTest { fun `given viewing thread timeline of the same room when processing main timeline message event then keep message`() { val events = listOf(aNotifiableMessageEvent(eventId = AN_EVENT_ID, roomId = A_ROOM_ID)) outdatedDetector.givenEventIsInDate(events[0]) + val eventProcessor = createProcessor(isInForeground = true, navigationState = VIEWING_A_THREAD) - val result = eventProcessor.process(events, VIEWING_A_THREAD, renderedEvents = emptyList()) + val result = eventProcessor.process(events, renderedEvents = emptyList()) assertThat(result).isEqualTo( listOfProcessedEvents( @@ -180,8 +193,9 @@ class NotifiableEventProcessorTest { ProcessedEvent(ProcessedEvent.Type.KEEP, events[0]), ProcessedEvent(ProcessedEvent.Type.KEEP, anInviteNotifiableEvent(eventId = AN_EVENT_ID_2)) ) + val eventProcessor = createProcessor(navigationState = NOT_VIEWING_A_ROOM) - val result = eventProcessor.process(events, appNavigationState = NOT_VIEWING_A_ROOM, renderedEvents = renderedEvents) + val result = eventProcessor.process(events, renderedEvents = renderedEvents) assertThat(result).isEqualTo( listOfProcessedEvents( @@ -194,4 +208,14 @@ class NotifiableEventProcessorTest { private fun listOfProcessedEvents(vararg event: Pair) = event.map { ProcessedEvent(it.first, it.second) } + + private fun createProcessor( + isInForeground: Boolean = false, + navigationState: NavigationState + ): NotifiableEventProcessor { + return NotifiableEventProcessor( + outdatedDetector.instance, + FakeAppNavigationStateService(MutableStateFlow(AppNavigationState(navigationState, isInForeground))), + ) + } } diff --git a/services/appnavstate/api/build.gradle.kts b/services/appnavstate/api/build.gradle.kts index b7ce6161fb..9ae81e15aa 100644 --- a/services/appnavstate/api/build.gradle.kts +++ b/services/appnavstate/api/build.gradle.kts @@ -24,5 +24,8 @@ android { dependencies { implementation(libs.coroutines.core) + implementation(libs.androidx.lifecycle.runtime) + implementation(libs.androidx.lifecycle.process) + implementation(libs.androidx.startup) implementation(projects.libraries.matrix.api) } diff --git a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppForegroundStateService.kt b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppForegroundStateService.kt new file mode 100644 index 0000000000..098769c370 --- /dev/null +++ b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppForegroundStateService.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.services.appnavstate.api + +import kotlinx.coroutines.flow.StateFlow + +/** + * A service that tracks the foreground state of the app. + */ +interface AppForegroundStateService { + /** + * Any updates to the foreground state of the app will be emitted here. + */ + val isInForeground: StateFlow + + /** + * Start observing the foreground state. + */ + fun start() +} diff --git a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationState.kt b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationState.kt index 5ead00c976..0a6ab692d2 100644 --- a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationState.kt +++ b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationState.kt @@ -16,43 +16,10 @@ package io.element.android.services.appnavstate.api -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.core.SessionId -import io.element.android.libraries.matrix.api.core.SpaceId -import io.element.android.libraries.matrix.api.core.ThreadId - /** - * Can represent the current global app navigation state. - * @param owner mostly a Node identifier associated with the state. - * We are using the owner parameter to check when calling onLeaving methods is still using the same owner than his companion onNavigate. - * Why this is needed : for now we rely on lifecycle methods of the node, which are async. - * If you navigate quickly between nodes, onCreate of the new node is called before onDestroy of the previous node. - * So we assume if we don't get the same owner, we can skip the onLeaving action as we already replaced it. + * A wrapper for the current navigation state of the app, along with its foreground/background state. */ -sealed class AppNavigationState(open val owner: String) { - object Root : AppNavigationState("ROOT") - - data class Session( - override val owner: String, - val sessionId: SessionId, - ) : AppNavigationState(owner) - - data class Space( - override val owner: String, - // Can be fake value, if no space is selected - val spaceId: SpaceId, - val parentSession: Session, - ) : AppNavigationState(owner) - - data class Room( - override val owner: String, - val roomId: RoomId, - val parentSpace: Space, - ) : AppNavigationState(owner) - - data class Thread( - override val owner: String, - val threadId: ThreadId, - val parentRoom: Room, - ) : AppNavigationState(owner) -} +data class AppNavigationState( + val navigationState: NavigationState, + val isInForeground: Boolean, +) diff --git a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationStateExtension.kt b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationStateExtension.kt deleted file mode 100644 index 00fe638a47..0000000000 --- a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationStateExtension.kt +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (c) 2023 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 - * - * http://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.services.appnavstate.api - -import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.core.SessionId -import io.element.android.libraries.matrix.api.core.SpaceId -import io.element.android.libraries.matrix.api.core.ThreadId - -fun AppNavigationState.currentSessionId(): SessionId? { - return when (this) { - AppNavigationState.Root -> null - is AppNavigationState.Session -> sessionId - is AppNavigationState.Space -> parentSession.sessionId - is AppNavigationState.Room -> parentSpace.parentSession.sessionId - is AppNavigationState.Thread -> parentRoom.parentSpace.parentSession.sessionId - } -} - -fun AppNavigationState.currentSpaceId(): SpaceId? { - return when (this) { - AppNavigationState.Root -> null - is AppNavigationState.Session -> null - is AppNavigationState.Space -> spaceId - is AppNavigationState.Room -> parentSpace.spaceId - is AppNavigationState.Thread -> parentRoom.parentSpace.spaceId - } -} - -fun AppNavigationState.currentRoomId(): RoomId? { - return when (this) { - AppNavigationState.Root -> null - is AppNavigationState.Session -> null - is AppNavigationState.Space -> null - is AppNavigationState.Room -> roomId - is AppNavigationState.Thread -> parentRoom.roomId - } -} - -fun AppNavigationState.currentThreadId(): ThreadId? { - return when (this) { - AppNavigationState.Root -> null - is AppNavigationState.Session -> null - is AppNavigationState.Space -> null - is AppNavigationState.Room -> null - is AppNavigationState.Thread -> threadId - } -} diff --git a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationStateService.kt b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationStateService.kt index 4bb40b7b75..50e6b3434e 100644 --- a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationStateService.kt +++ b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/AppNavigationStateService.kt @@ -22,8 +22,11 @@ import io.element.android.libraries.matrix.api.core.SpaceId import io.element.android.libraries.matrix.api.core.ThreadId import kotlinx.coroutines.flow.StateFlow +/** + * A service that tracks the navigation and foreground states of the app. + */ interface AppNavigationStateService { - val appNavigationStateFlow: StateFlow + val appNavigationState: StateFlow fun onNavigateToSession(owner: String, sessionId: SessionId) fun onLeavingSession(owner: String) @@ -37,3 +40,4 @@ interface AppNavigationStateService { fun onNavigateToThread(owner: String, threadId: ThreadId) fun onLeavingThread(owner: String) } + diff --git a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/NavigationState.kt b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/NavigationState.kt new file mode 100644 index 0000000000..12cd07f05e --- /dev/null +++ b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/NavigationState.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.services.appnavstate.api + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.SpaceId +import io.element.android.libraries.matrix.api.core.ThreadId + +/** + * Can represent the current global app navigation state. + * @param owner mostly a Node identifier associated with the state. + * We are using the owner parameter to check when calling onLeaving methods is still using the same owner than his companion onNavigate. + * Why this is needed : for now we rely on lifecycle methods of the node, which are async. + * If you navigate quickly between nodes, onCreate of the new node is called before onDestroy of the previous node. + * So we assume if we don't get the same owner, we can skip the onLeaving action as we already replaced it. + */ +sealed class NavigationState(open val owner: String) { + object Root : NavigationState("ROOT") + + data class Session( + override val owner: String, + val sessionId: SessionId, + ) : NavigationState(owner) + + data class Space( + override val owner: String, + // Can be fake value, if no space is selected + val spaceId: SpaceId, + val parentSession: Session, + ) : NavigationState(owner) + + data class Room( + override val owner: String, + val roomId: RoomId, + val parentSpace: Space, + ) : NavigationState(owner) + + data class Thread( + override val owner: String, + val threadId: ThreadId, + val parentRoom: Room, + ) : NavigationState(owner) +} diff --git a/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/NavigationStateExtension.kt b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/NavigationStateExtension.kt new file mode 100644 index 0000000000..b399934cac --- /dev/null +++ b/services/appnavstate/api/src/main/kotlin/io/element/android/services/appnavstate/api/NavigationStateExtension.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.services.appnavstate.api + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.SpaceId +import io.element.android.libraries.matrix.api.core.ThreadId + +fun NavigationState.currentSessionId(): SessionId? { + return when (this) { + NavigationState.Root -> null + is NavigationState.Session -> sessionId + is NavigationState.Space -> parentSession.sessionId + is NavigationState.Room -> parentSpace.parentSession.sessionId + is NavigationState.Thread -> parentRoom.parentSpace.parentSession.sessionId + } +} + +fun NavigationState.currentSpaceId(): SpaceId? { + return when (this) { + NavigationState.Root -> null + is NavigationState.Session -> null + is NavigationState.Space -> spaceId + is NavigationState.Room -> parentSpace.spaceId + is NavigationState.Thread -> parentRoom.parentSpace.spaceId + } +} + +fun NavigationState.currentRoomId(): RoomId? { + return when (this) { + NavigationState.Root -> null + is NavigationState.Session -> null + is NavigationState.Space -> null + is NavigationState.Room -> roomId + is NavigationState.Thread -> parentRoom.roomId + } +} + +fun NavigationState.currentThreadId(): ThreadId? { + return when (this) { + NavigationState.Root -> null + is NavigationState.Session -> null + is NavigationState.Space -> null + is NavigationState.Room -> null + is NavigationState.Thread -> threadId + } +} diff --git a/services/appnavstate/impl/build.gradle.kts b/services/appnavstate/impl/build.gradle.kts index 4cd39a4c42..4c6973b8da 100644 --- a/services/appnavstate/impl/build.gradle.kts +++ b/services/appnavstate/impl/build.gradle.kts @@ -38,6 +38,7 @@ dependencies { implementation(libs.coroutines.core) implementation(libs.androidx.corektx) + implementation(libs.androidx.lifecycle.process) api(projects.services.appnavstate.api) @@ -45,5 +46,6 @@ dependencies { testImplementation(libs.coroutines.test) testImplementation(libs.test.truth) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) testImplementation(projects.services.appnavstate.test) } diff --git a/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppForegroundStateService.kt b/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppForegroundStateService.kt new file mode 100644 index 0000000000..27c3f12a6a --- /dev/null +++ b/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppForegroundStateService.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.services.appnavstate.impl + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.ProcessLifecycleOwner +import io.element.android.services.appnavstate.api.AppForegroundStateService +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class DefaultAppForegroundStateService : AppForegroundStateService { + + private val state = MutableStateFlow(false) + override val isInForeground: StateFlow = state + + private val appLifecycle: Lifecycle by lazy { ProcessLifecycleOwner.get().lifecycle } + + override fun start() { + appLifecycle.addObserver(lifecycleObserver) + } + + private val lifecycleObserver = LifecycleEventObserver { _, _ -> state.value = getCurrentState() } + + private fun getCurrentState(): Boolean = appLifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) +} diff --git a/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateService.kt b/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateService.kt index bf20a04b11..9360ce93ec 100644 --- a/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateService.kt +++ b/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateService.kt @@ -24,10 +24,15 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SpaceId import io.element.android.libraries.matrix.api.core.ThreadId -import io.element.android.services.appnavstate.api.AppNavigationState +import io.element.android.services.appnavstate.api.AppForegroundStateService +import io.element.android.services.appnavstate.api.NavigationState import io.element.android.services.appnavstate.api.AppNavigationStateService +import io.element.android.services.appnavstate.api.AppNavigationState +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.getAndUpdate +import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject @@ -38,113 +43,131 @@ private val loggerTag = LoggerTag("Navigation") */ @ContributesBinding(AppScope::class) @SingleIn(AppScope::class) -class DefaultAppNavigationStateService @Inject constructor() : AppNavigationStateService { +class DefaultAppNavigationStateService @Inject constructor( + private val appForegroundStateService: AppForegroundStateService, + private val coroutineScope: CoroutineScope, +) : AppNavigationStateService { - private val currentAppNavigationState: MutableStateFlow = MutableStateFlow(AppNavigationState.Root) - override val appNavigationStateFlow: StateFlow = currentAppNavigationState + private val state = MutableStateFlow( + AppNavigationState( + navigationState = NavigationState.Root, + isInForeground = true, + ) + ) + override val appNavigationState: StateFlow = state + + init { + coroutineScope.launch { + appForegroundStateService.start() + + appForegroundStateService.isInForeground.collect { isInForeground -> + state.getAndUpdate { it.copy(isInForeground = isInForeground) } + } + } + } override fun onNavigateToSession(owner: String, sessionId: SessionId) { - val currentValue = currentAppNavigationState.value + val currentValue = state.value.navigationState Timber.tag(loggerTag.value).d("Navigating to session $sessionId. Current state: $currentValue") - val newValue: AppNavigationState.Session = when (currentValue) { - is AppNavigationState.Session, - is AppNavigationState.Space, - is AppNavigationState.Room, - is AppNavigationState.Thread, - is AppNavigationState.Root -> AppNavigationState.Session(owner, sessionId) + val newValue: NavigationState.Session = when (currentValue) { + is NavigationState.Session, + is NavigationState.Space, + is NavigationState.Room, + is NavigationState.Thread, + is NavigationState.Root -> NavigationState.Session(owner, sessionId) } - currentAppNavigationState.value = newValue + state.getAndUpdate { it.copy(navigationState = newValue) } } override fun onNavigateToSpace(owner: String, spaceId: SpaceId) { - val currentValue = currentAppNavigationState.value + val currentValue = state.value.navigationState Timber.tag(loggerTag.value).d("Navigating to space $spaceId. Current state: $currentValue") - val newValue: AppNavigationState.Space = when (currentValue) { - AppNavigationState.Root -> error("onNavigateToSession() must be called first") - is AppNavigationState.Session -> AppNavigationState.Space(owner, spaceId, currentValue) - is AppNavigationState.Space -> AppNavigationState.Space(owner, spaceId, currentValue.parentSession) - is AppNavigationState.Room -> AppNavigationState.Space(owner, spaceId, currentValue.parentSpace.parentSession) - is AppNavigationState.Thread -> AppNavigationState.Space(owner, spaceId, currentValue.parentRoom.parentSpace.parentSession) + val newValue: NavigationState.Space = when (currentValue) { + NavigationState.Root -> error("onNavigateToSession() must be called first") + is NavigationState.Session -> NavigationState.Space(owner, spaceId, currentValue) + is NavigationState.Space -> NavigationState.Space(owner, spaceId, currentValue.parentSession) + is NavigationState.Room -> NavigationState.Space(owner, spaceId, currentValue.parentSpace.parentSession) + is NavigationState.Thread -> NavigationState.Space(owner, spaceId, currentValue.parentRoom.parentSpace.parentSession) } - currentAppNavigationState.value = newValue + state.getAndUpdate { it.copy(navigationState = newValue) } } override fun onNavigateToRoom(owner: String, roomId: RoomId) { - val currentValue = currentAppNavigationState.value + val currentValue = state.value.navigationState Timber.tag(loggerTag.value).d("Navigating to room $roomId. Current state: $currentValue") - val newValue: AppNavigationState.Room = when (currentValue) { - AppNavigationState.Root -> error("onNavigateToSession() must be called first") - is AppNavigationState.Session -> error("onNavigateToSpace() must be called first") - is AppNavigationState.Space -> AppNavigationState.Room(owner, roomId, currentValue) - is AppNavigationState.Room -> AppNavigationState.Room(owner, roomId, currentValue.parentSpace) - is AppNavigationState.Thread -> AppNavigationState.Room(owner, roomId, currentValue.parentRoom.parentSpace) + val newValue: NavigationState.Room = when (currentValue) { + NavigationState.Root -> error("onNavigateToSession() must be called first") + is NavigationState.Session -> error("onNavigateToSpace() must be called first") + is NavigationState.Space -> NavigationState.Room(owner, roomId, currentValue) + is NavigationState.Room -> NavigationState.Room(owner, roomId, currentValue.parentSpace) + is NavigationState.Thread -> NavigationState.Room(owner, roomId, currentValue.parentRoom.parentSpace) } - currentAppNavigationState.value = newValue + state.getAndUpdate { it.copy(navigationState = newValue) } } override fun onNavigateToThread(owner: String, threadId: ThreadId) { - val currentValue = currentAppNavigationState.value + val currentValue = state.value.navigationState Timber.tag(loggerTag.value).d("Navigating to thread $threadId. Current state: $currentValue") - val newValue: AppNavigationState.Thread = when (currentValue) { - AppNavigationState.Root -> error("onNavigateToSession() must be called first") - is AppNavigationState.Session -> error("onNavigateToSpace() must be called first") - is AppNavigationState.Space -> error("onNavigateToRoom() must be called first") - is AppNavigationState.Room -> AppNavigationState.Thread(owner, threadId, currentValue) - is AppNavigationState.Thread -> AppNavigationState.Thread(owner, threadId, currentValue.parentRoom) + val newValue: NavigationState.Thread = when (currentValue) { + NavigationState.Root -> error("onNavigateToSession() must be called first") + is NavigationState.Session -> error("onNavigateToSpace() must be called first") + is NavigationState.Space -> error("onNavigateToRoom() must be called first") + is NavigationState.Room -> NavigationState.Thread(owner, threadId, currentValue) + is NavigationState.Thread -> NavigationState.Thread(owner, threadId, currentValue.parentRoom) } - currentAppNavigationState.value = newValue + state.getAndUpdate { it.copy(navigationState = newValue) } } override fun onLeavingThread(owner: String) { - val currentValue = currentAppNavigationState.value + val currentValue = state.value.navigationState Timber.tag(loggerTag.value).d("Leaving thread. Current state: $currentValue") if (!currentValue.assertOwner(owner)) return - val newValue: AppNavigationState.Room = when (currentValue) { - AppNavigationState.Root -> error("onNavigateToSession() must be called first") - is AppNavigationState.Session -> error("onNavigateToSpace() must be called first") - is AppNavigationState.Space -> error("onNavigateToRoom() must be called first") - is AppNavigationState.Room -> error("onNavigateToThread() must be called first") - is AppNavigationState.Thread -> currentValue.parentRoom + val newValue: NavigationState.Room = when (currentValue) { + NavigationState.Root -> error("onNavigateToSession() must be called first") + is NavigationState.Session -> error("onNavigateToSpace() must be called first") + is NavigationState.Space -> error("onNavigateToRoom() must be called first") + is NavigationState.Room -> error("onNavigateToThread() must be called first") + is NavigationState.Thread -> currentValue.parentRoom } - currentAppNavigationState.value = newValue + state.getAndUpdate { it.copy(navigationState = newValue) } } override fun onLeavingRoom(owner: String) { - val currentValue = currentAppNavigationState.value + val currentValue = state.value.navigationState Timber.tag(loggerTag.value).d("Leaving room. Current state: $currentValue") if (!currentValue.assertOwner(owner)) return - val newValue: AppNavigationState.Space = when (currentValue) { - AppNavigationState.Root -> error("onNavigateToSession() must be called first") - is AppNavigationState.Session -> error("onNavigateToSpace() must be called first") - is AppNavigationState.Space -> error("onNavigateToRoom() must be called first") - is AppNavigationState.Room -> currentValue.parentSpace - is AppNavigationState.Thread -> currentValue.parentRoom.parentSpace + val newValue: NavigationState.Space = when (currentValue) { + NavigationState.Root -> error("onNavigateToSession() must be called first") + is NavigationState.Session -> error("onNavigateToSpace() must be called first") + is NavigationState.Space -> error("onNavigateToRoom() must be called first") + is NavigationState.Room -> currentValue.parentSpace + is NavigationState.Thread -> currentValue.parentRoom.parentSpace } - currentAppNavigationState.value = newValue + state.getAndUpdate { it.copy(navigationState = newValue) } } override fun onLeavingSpace(owner: String) { - val currentValue = currentAppNavigationState.value + val currentValue = state.value.navigationState Timber.tag(loggerTag.value).d("Leaving space. Current state: $currentValue") if (!currentValue.assertOwner(owner)) return - val newValue: AppNavigationState.Session = when (currentValue) { - AppNavigationState.Root -> error("onNavigateToSession() must be called first") - is AppNavigationState.Session -> error("onNavigateToSpace() must be called first") - is AppNavigationState.Space -> currentValue.parentSession - is AppNavigationState.Room -> currentValue.parentSpace.parentSession - is AppNavigationState.Thread -> currentValue.parentRoom.parentSpace.parentSession + val newValue: NavigationState.Session = when (currentValue) { + NavigationState.Root -> error("onNavigateToSession() must be called first") + is NavigationState.Session -> error("onNavigateToSpace() must be called first") + is NavigationState.Space -> currentValue.parentSession + is NavigationState.Room -> currentValue.parentSpace.parentSession + is NavigationState.Thread -> currentValue.parentRoom.parentSpace.parentSession } - currentAppNavigationState.value = newValue + state.getAndUpdate { it.copy(navigationState = newValue) } } override fun onLeavingSession(owner: String) { - val currentValue = currentAppNavigationState.value + val currentValue = state.value.navigationState Timber.tag(loggerTag.value).d("Leaving session. Current state: $currentValue") if (!currentValue.assertOwner(owner)) return - currentAppNavigationState.value = AppNavigationState.Root + state.getAndUpdate { it.copy(navigationState = NavigationState.Root) } } - private fun AppNavigationState.assertOwner(owner: String): Boolean { + private fun NavigationState.assertOwner(owner: String): Boolean { if (this.owner != owner) { Timber.tag(loggerTag.value).d("Can't leave current state as the owner is not the same (current = ${this.owner}, new = $owner)") return false diff --git a/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/di/AppNavStateModule.kt b/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/di/AppNavStateModule.kt new file mode 100644 index 0000000000..4537c9f902 --- /dev/null +++ b/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/di/AppNavStateModule.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.services.appnavstate.impl.di + +import android.content.Context +import androidx.startup.AppInitializer +import com.squareup.anvil.annotations.ContributesTo +import dagger.Module +import dagger.Provides +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.ApplicationContext +import io.element.android.services.appnavstate.api.AppForegroundStateService +import io.element.android.services.appnavstate.impl.initializer.AppForegroundStateServiceInitializer + +@Module +@ContributesTo(AppScope::class) +object AppNavStateModule { + + @Provides + fun provideAppForegroundStateService( + @ApplicationContext context: Context + ): AppForegroundStateService = + AppInitializer.getInstance(context).initializeComponent(AppForegroundStateServiceInitializer::class.java) + +} diff --git a/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/initializer/AppForegroundStateServiceInitializer.kt b/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/initializer/AppForegroundStateServiceInitializer.kt new file mode 100644 index 0000000000..cfd382a57b --- /dev/null +++ b/services/appnavstate/impl/src/main/kotlin/io/element/android/services/appnavstate/impl/initializer/AppForegroundStateServiceInitializer.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.services.appnavstate.impl.initializer + +import android.content.Context +import androidx.lifecycle.ProcessLifecycleInitializer +import androidx.startup.Initializer +import io.element.android.services.appnavstate.api.AppForegroundStateService +import io.element.android.services.appnavstate.impl.DefaultAppForegroundStateService + +class AppForegroundStateServiceInitializer : Initializer { + override fun create(context: Context): AppForegroundStateService { + return DefaultAppForegroundStateService() + } + + override fun dependencies(): MutableList>> = mutableListOf( + ProcessLifecycleInitializer::class.java + ) +} diff --git a/services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateServiceTest.kt b/services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/DefaultNavigationStateServiceTest.kt similarity index 70% rename from services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateServiceTest.kt rename to services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/DefaultNavigationStateServiceTest.kt index d6000dc1d8..dd0e576c79 100644 --- a/services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/DefaultAppNavigationStateServiceTest.kt +++ b/services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/DefaultNavigationStateServiceTest.kt @@ -21,35 +21,36 @@ import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SPACE_ID import io.element.android.libraries.matrix.test.A_THREAD_ID -import io.element.android.services.appnavstate.api.AppNavigationState +import io.element.android.services.appnavstate.api.NavigationState import io.element.android.services.appnavstate.test.A_ROOM_OWNER import io.element.android.services.appnavstate.test.A_SESSION_OWNER import io.element.android.services.appnavstate.test.A_SPACE_OWNER import io.element.android.services.appnavstate.test.A_THREAD_OWNER +import io.element.android.tests.testutils.runCancellableScopeTest +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first -import kotlinx.coroutines.test.runTest import org.junit.Assert.assertThrows import org.junit.Test -class DefaultAppNavigationStateServiceTest { +class DefaultNavigationStateServiceTest { @Test - fun testNavigation() = runTest { - val service = DefaultAppNavigationStateService() + fun testNavigation() = runCancellableScopeTest { scope -> + val service = createStateService(scope) service.onNavigateToSession(A_SESSION_OWNER, A_SESSION_ID) service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID) service.onNavigateToRoom(A_ROOM_OWNER, A_ROOM_ID) service.onNavigateToThread(A_THREAD_OWNER, A_THREAD_ID) - assertThat(service.appNavigationStateFlow.first()).isEqualTo( - AppNavigationState.Thread( + assertThat(service.appNavigationState.first().navigationState).isEqualTo( + NavigationState.Thread( A_THREAD_OWNER, A_THREAD_ID, - AppNavigationState.Room( + NavigationState.Room( A_ROOM_OWNER, A_ROOM_ID, - AppNavigationState.Space( + NavigationState.Space( A_SPACE_OWNER, A_SPACE_ID, - AppNavigationState.Session( + NavigationState.Session( A_SESSION_OWNER, A_SESSION_ID ) @@ -60,8 +61,13 @@ class DefaultAppNavigationStateServiceTest { } @Test - fun testFailure() = runTest { - val service = DefaultAppNavigationStateService() + fun testFailure() = runCancellableScopeTest { scope -> + val service = createStateService(scope) + assertThrows(IllegalStateException::class.java) { service.onNavigateToSpace(A_SPACE_OWNER, A_SPACE_ID) } } + + private fun createStateService( + coroutineScope: CoroutineScope + ) = DefaultAppNavigationStateService(FakeAppForegroundStateService(), coroutineScope) } diff --git a/services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/FakeAppForegroundStateService.kt b/services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/FakeAppForegroundStateService.kt new file mode 100644 index 0000000000..e243523bd0 --- /dev/null +++ b/services/appnavstate/impl/src/test/kotlin/io/element/android/services/appnavstate/impl/FakeAppForegroundStateService.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.services.appnavstate.impl + +import io.element.android.services.appnavstate.api.AppForegroundStateService +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class FakeAppForegroundStateService( + initialValue: Boolean = true, +) : AppForegroundStateService { + + private val state = MutableStateFlow(initialValue) + override val isInForeground: StateFlow = state + + override fun start() { + // No-op + } + + fun givenIsInForeground(isInForeground: Boolean) { + state.value = isInForeground + } +} diff --git a/services/appnavstate/test/build.gradle.kts b/services/appnavstate/test/build.gradle.kts index 93e9294304..656777dac1 100644 --- a/services/appnavstate/test/build.gradle.kts +++ b/services/appnavstate/test/build.gradle.kts @@ -26,4 +26,5 @@ dependencies { api(projects.libraries.matrix.api) api(projects.services.appnavstate.api) implementation(libs.coroutines.core) + implementation(libs.androidx.lifecycle.runtime) } diff --git a/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/AppNavStateFixture.kt b/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/AppNavStateFixture.kt index aa0b351220..63c3d4e967 100644 --- a/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/AppNavStateFixture.kt +++ b/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/AppNavStateFixture.kt @@ -21,33 +21,33 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SpaceId import io.element.android.libraries.matrix.api.core.ThreadId -import io.element.android.services.appnavstate.api.AppNavigationState +import io.element.android.services.appnavstate.api.NavigationState const val A_SESSION_OWNER = "aSessionOwner" const val A_SPACE_OWNER = "aSpaceOwner" const val A_ROOM_OWNER = "aRoomOwner" const val A_THREAD_OWNER = "aThreadOwner" -fun anAppNavigationState( +fun aNavigationState( sessionId: SessionId? = null, spaceId: SpaceId? = MAIN_SPACE, roomId: RoomId? = null, threadId: ThreadId? = null, -): AppNavigationState { +): NavigationState { if (sessionId == null) { - return AppNavigationState.Root + return NavigationState.Root } - val session = AppNavigationState.Session(A_SESSION_OWNER, sessionId) + val session = NavigationState.Session(A_SESSION_OWNER, sessionId) if (spaceId == null) { return session } - val space = AppNavigationState.Space(A_SPACE_OWNER, spaceId, session) + val space = NavigationState.Space(A_SPACE_OWNER, spaceId, session) if (roomId == null) { return space } - val room = AppNavigationState.Room(A_ROOM_OWNER, roomId, space) + val room = NavigationState.Room(A_ROOM_OWNER, roomId, space) if (threadId == null) { return room } - return AppNavigationState.Thread(A_THREAD_OWNER, threadId, room) + return NavigationState.Thread(A_THREAD_OWNER, threadId, room) } diff --git a/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/NoopAppNavigationStateService.kt b/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/FakeAppNavigationStateService.kt similarity index 78% rename from services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/NoopAppNavigationStateService.kt rename to services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/FakeAppNavigationStateService.kt index c31d74ec18..a09e2a9c5e 100644 --- a/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/NoopAppNavigationStateService.kt +++ b/services/appnavstate/test/src/main/kotlin/io/element/android/services/appnavstate/test/FakeAppNavigationStateService.kt @@ -20,16 +20,22 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SpaceId import io.element.android.libraries.matrix.api.core.ThreadId -import io.element.android.services.appnavstate.api.AppNavigationState +import io.element.android.services.appnavstate.api.NavigationState import io.element.android.services.appnavstate.api.AppNavigationStateService +import io.element.android.services.appnavstate.api.AppNavigationState import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -class NoopAppNavigationStateService : AppNavigationStateService { +class FakeAppNavigationStateService( + private val fakeAppNavigationState: MutableStateFlow = MutableStateFlow( + AppNavigationState( + navigationState = NavigationState.Root, + isInForeground = true, + ) + ), +) : AppNavigationStateService { - private val currentAppNavigationState: MutableStateFlow = - MutableStateFlow(AppNavigationState.Root) - override val appNavigationStateFlow: StateFlow = currentAppNavigationState + override val appNavigationState: StateFlow = fakeAppNavigationState override fun onNavigateToSession(owner: String, sessionId: SessionId) = Unit override fun onLeavingSession(owner: String) = Unit diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RunCancellableTest.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RunCancellableTest.kt new file mode 100644 index 0000000000..aea33b6798 --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/RunCancellableTest.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.tests.testutils + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.test.runTest + +/** + * Run a test with a [CoroutineScope] that will be cancelled automatically and avoiding failing the test. + */ +fun runCancellableScopeTest(block: suspend (CoroutineScope) -> Unit) = runTest { + val scope = CoroutineScope(coroutineContext + SupervisorJob()) + block(scope) + scope.cancel() +} From 9ef8b36f51be90316a61991a18cf8e716e9ebb54 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Mon, 10 Jul 2023 14:43:59 +0100 Subject: [PATCH 22/59] Location sharing: don't hardcode API key In an effort to make it easier for forks to (a) use their own API keys (b) change map styles or maybe even providers, move the MapTiler key out of the source code and pass it in via env var or property. Also refactor the utility classes slightly to keep all the URL related functions together, to reduce the chance of collisions when maintaining such forks. --- .github/workflows/build.yml | 2 + .github/workflows/nightly.yml | 1 + docs/maps.md | 42 +++++++ features/location/api/build.gradle.kts | 19 +++ .../features/location/api/StaticMapView.kt | 15 ++- .../features/location/api/internal/MapUrls.kt | 55 ++++++++ .../location/api/internal/MapsUtils.kt | 91 -------------- .../api/internal/BuildStaticMapsApiUrlTest.kt | 117 ------------------ .../features/location/impl/map/MapView.kt | 4 +- 9 files changed, 128 insertions(+), 218 deletions(-) create mode 100644 docs/maps.md create mode 100644 features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapUrls.kt delete mode 100644 features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapsUtils.kt delete mode 100644 features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/BuildStaticMapsApiUrlTest.kt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6c0bc7f85f..831f2765b1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,6 +38,8 @@ jobs: with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Assemble debug APK + env: + ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} run: ./gradlew assembleDebug $CI_GRADLE_ARG_PROPERTIES - name: Upload debug APKs uses: actions/upload-artifact@v3 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 7d240080b0..95c2deb8eb 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -35,6 +35,7 @@ jobs: run: | ./gradlew assembleNightly appDistributionUploadNightly $CI_GRADLE_ARG_PROPERTIES env: + ELEMENT_ANDROID_MAPTILER_API_KEY: ${{ secrets.MAPTILER_KEY }} ELEMENT_ANDROID_NIGHTLY_KEYID: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYID }} ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_KEYPASSWORD }} ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD: ${{ secrets.ELEMENT_ANDROID_NIGHTLY_STOREPASSWORD }} diff --git a/docs/maps.md b/docs/maps.md new file mode 100644 index 0000000000..cc00905986 --- /dev/null +++ b/docs/maps.md @@ -0,0 +1,42 @@ +# Use of maps + + + +* [Overview](#overview) +* [Local development with MapTiler](#local-development-with-maptiler) +* [Making releasable builds with MapTiler](#making-releasable-builds-with-maptiler) +* [Using other map sources or MapTiler styles](#using-other-map-sources-or-maptiler-styles) + + + +## Overview + +Element Android uses [MapTiler](https://www.maptiler.com/) to provide map +imagery where required. MapTiler requires an API key, which we bake in to +the app at release time. + +## Local development with MapTiler + +If you're developing the application and want maps to render properly you can +sign up for the [MapTiler free tier](https://www.maptiler.com/cloud/pricing/). + +Place your API key in `local.properties` with the key +`services.maptiler.apikey`, e.g.: + +```properties +services.maptiler.apikey=abCd3fGhijK1mN0pQr5t +``` + +## Making releasable builds with MapTiler + +To insert the MapTiler API key when building an APK, set the +`ELEMENT_ANDROID_MAPTILER_API_KEY` environment variable in your build +environment. + +## Using other map sources or MapTiler styles + +If you wish to use an alternative map provider, or custom MapTiler styles, +you can customise the functions in +`features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapUrls.kt`. +We've kept this file small and self contained to minimise the chances of merge +collisions in forks. diff --git a/features/location/api/build.gradle.kts b/features/location/api/build.gradle.kts index 0e517fd3e6..6de297fe77 100644 --- a/features/location/api/build.gradle.kts +++ b/features/location/api/build.gradle.kts @@ -14,14 +14,33 @@ * limitations under the License. */ +import java.util.Properties + plugins { id("io.element.android-compose-library") alias(libs.plugins.ksp) id("kotlin-parcelize") } +fun readLocalProperty(name: String) = Properties().apply { + try { + load(rootProject.file("local.properties").reader()) + } catch (ignored: java.io.IOException) { + } +}[name] + android { namespace = "io.element.android.features.location.api" + + defaultConfig { + resValue( + type = "string", + name = "maptiler_api_key", + value = System.getenv("ELEMENT_ANDROID_MAPTILER_API_KEY") + ?: readLocalProperty("services.maptiler.apikey") as? String + ?: "" + ) + } } dependencies { diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt index 3d09c36604..c8762b3989 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt @@ -34,9 +34,8 @@ import androidx.compose.ui.unit.dp import coil.compose.AsyncImagePainter import coil.compose.rememberAsyncImagePainter import coil.request.ImageRequest -import io.element.android.features.location.api.internal.AttributionPlacement import io.element.android.features.location.api.internal.StaticMapPlaceholder -import io.element.android.features.location.api.internal.buildStaticMapsApiUrl +import io.element.android.features.location.api.internal.staticMapUrl import io.element.android.libraries.designsystem.preview.DayNightPreviews import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.text.toDp @@ -64,6 +63,7 @@ fun StaticMapView( modifier = modifier, contentAlignment = Alignment.Center ) { + val context = LocalContext.current var retryHash by remember { mutableStateOf(0) } val painter = rememberAsyncImagePainter( model = if (constraints.isZero) { @@ -72,17 +72,16 @@ fun StaticMapView( } else { ImageRequest.Builder(LocalContext.current) .data( - buildStaticMapsApiUrl( + staticMapUrl( + context = context, lat = lat, lon = lon, - desiredZoom = zoom, + zoom = zoom, darkMode = darkMode, - attributionPlacement = AttributionPlacement.BottomLeft, // Size the map based on DP rather than pixels, as otherwise the features and attribution // end up being illegibly tiny on high density displays. - desiredWidth = constraints.maxWidth.toDp().value.toInt(), - desiredHeight = constraints.maxHeight.toDp().value.toInt(), - doubleScale = true, + width = constraints.maxWidth.toDp().value.toInt(), + height = constraints.maxHeight.toDp().value.toInt(), ) ) .size(width = constraints.maxWidth, height = constraints.maxHeight) diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapUrls.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapUrls.kt new file mode 100644 index 0000000000..355741dbaa --- /dev/null +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapUrls.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.location.api.internal + +import android.content.Context +import io.element.android.features.location.api.R + +/** + * Provides the URL to an image that contains a statically-generated map of the given location. + */ +fun staticMapUrl( + context: Context, + lat: Double, + lon: Double, + zoom: Double, + width: Int, + height: Int, + darkMode: Boolean, +): String { + return "${baseUrl(darkMode)}/static/${lon},${lat},${zoom}/${width}x${height}@2x.webp?key=${context.apiKey}&attribution=bottomleft" +} + +/** + * Provides the URL to a MapLibre style document, used for rendering dynamic maps. + */ +fun tileStyleUrl( + context: Context, + darkMode: Boolean, +): String { + return "${baseUrl(darkMode)}/style.json?key=${context.apiKey}" +} + +private fun baseUrl(darkMode: Boolean) = + "https://api.maptiler.com/maps/" + + if (darkMode) + "dea61faf-292b-4774-9660-58fcef89a7f3" + else + "9bc819c8-e627-474a-a348-ec144fe3d810" + +private val Context.apiKey: String + get() = getString(R.string.maptiler_api_key) diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapsUtils.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapsUtils.kt deleted file mode 100644 index 66bbb906fc..0000000000 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/MapsUtils.kt +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright (c) 2023 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 - * - * http://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.location.api.internal - -import kotlin.math.roundToInt - -private const val API_KEY = "fU3vlMsMn4Jb6dnEIFsx" -private const val BASE_URL = "https://api.maptiler.com" -private const val LIGHT_MAP_ID = "9bc819c8-e627-474a-a348-ec144fe3d810" -private const val DARK_MAP_ID = "dea61faf-292b-4774-9660-58fcef89a7f3" -private const val STATIC_MAP_FORMAT = "webp" -private const val STATIC_MAP_SCALE_2X = "@2x" -private const val STATIC_MAP_MAX_WIDTH_HEIGHT = 2048 -private const val STATIC_MAP_MAX_ZOOM = 22.0 - -fun buildTileServerUrl( - darkMode: Boolean -): String = if (!darkMode) { - "$BASE_URL/maps/$LIGHT_MAP_ID/style.json?key=$API_KEY" -} else { - "$BASE_URL/maps/$DARK_MAP_ID/style.json?key=$API_KEY" -} - -internal enum class AttributionPlacement(val value: String) { - BottomRight("bottomright"), - BottomLeft("bottomleft"), - TopLeft("topleft"), - TopRight("topright"), - Hidden("false"), -} - -/** - * Builds a valid URL for maptiler.com static map api based on the given params. - * - * Coerces width and height to the API maximum of 2048 keeping the requested aspect ratio. - * Coerces zoom to the API maximum of 22. - * - * NB: This will throw if either width or height are <= 0. You need to handle this case upstream - * (hint: views can't have negative width or height but can have 0 width or height sometimes). - */ -internal fun buildStaticMapsApiUrl( - lat: Double, - lon: Double, - desiredZoom: Double, - desiredWidth: Int, - desiredHeight: Int, - darkMode: Boolean, - doubleScale: Boolean, - attributionPlacement: AttributionPlacement, -): String { - require(desiredWidth > 0 && desiredHeight > 0) { - "Width ($desiredHeight) and height ($desiredHeight) must be > 0" - } - require(desiredZoom >= 0) { "Zoom ($desiredZoom) must be >= 0" } - val zoom = desiredZoom.coerceAtMost(STATIC_MAP_MAX_ZOOM) // API will error if outside 0-22 range. - val width: Int - val height: Int - if (desiredWidth <= STATIC_MAP_MAX_WIDTH_HEIGHT && desiredHeight <= STATIC_MAP_MAX_WIDTH_HEIGHT) { - width = desiredWidth - height = desiredHeight - } else { - val aspectRatio = desiredWidth.toDouble() / desiredHeight.toDouble() - if (desiredWidth >= desiredHeight) { - width = desiredWidth.coerceAtMost(STATIC_MAP_MAX_WIDTH_HEIGHT) - height = (width / aspectRatio).roundToInt() - } else { - height = desiredHeight.coerceAtMost(STATIC_MAP_MAX_WIDTH_HEIGHT) - width = (height * aspectRatio).roundToInt() - } - } - - val mapId = if (darkMode) DARK_MAP_ID else LIGHT_MAP_ID - val scaleSuffix = if (doubleScale) STATIC_MAP_SCALE_2X else "" - - return "$BASE_URL/maps/$mapId/static/${lon},${lat},${zoom}/${width}x${height}${scaleSuffix}.$STATIC_MAP_FORMAT" + - "?key=$API_KEY&attribution=${attributionPlacement.value}" -} diff --git a/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/BuildStaticMapsApiUrlTest.kt b/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/BuildStaticMapsApiUrlTest.kt deleted file mode 100644 index 71e5988185..0000000000 --- a/features/location/api/src/test/kotlin/io/element/android/features/location/api/internal/BuildStaticMapsApiUrlTest.kt +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (c) 2023 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 - * - * http://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.location.api.internal - -import com.google.common.truth.Truth.assertThat -import org.junit.Test - -class BuildStaticMapsApiUrlTest { - @Test - fun `buildStaticMapsApiUrl builds light mode url`() { - assertThat( - buildStaticMapsApiUrl( - lat = 1.234, - lon = 5.678, - desiredZoom = 1.2, - desiredWidth = 100, - desiredHeight = 200, - darkMode = false, - doubleScale = false, - attributionPlacement = AttributionPlacement.BottomLeft, - ) - ).isEqualTo( - "https://api.maptiler.com/maps/9bc819c8-e627-474a-a348-ec144fe3d810/static/5.678,1.234,1.2/100x200.webp" + - "?key=fU3vlMsMn4Jb6dnEIFsx&attribution=bottomleft" - ) - } - - @Test - fun `buildStaticMapsApiUrl builds dark mode url`() { - assertThat( - buildStaticMapsApiUrl( - lat = 1.234, - lon = 5.678, - desiredZoom = 1.2, - desiredWidth = 100, - desiredHeight = 200, - darkMode = true, - doubleScale = false, - attributionPlacement = AttributionPlacement.BottomLeft, - ) - ).isEqualTo( - "https://api.maptiler.com/maps/dea61faf-292b-4774-9660-58fcef89a7f3/static/5.678,1.234,1.2/100x200.webp" + - "?key=fU3vlMsMn4Jb6dnEIFsx&attribution=bottomleft" - ) - } - - @Test - fun `buildStaticMapsApiUrl builds double scale mode url`() { - assertThat( - buildStaticMapsApiUrl( - lat = 1.234, - lon = 5.678, - desiredZoom = 1.2, - desiredWidth = 100, - desiredHeight = 200, - darkMode = false, - doubleScale = true, - attributionPlacement = AttributionPlacement.BottomLeft, - ) - ).isEqualTo( - "https://api.maptiler.com/maps/9bc819c8-e627-474a-a348-ec144fe3d810/static/5.678,1.234,1.2/100x200@2x.webp" + - "?key=fU3vlMsMn4Jb6dnEIFsx&attribution=bottomleft" - ) - } - - @Test - fun `buildStaticMapsApiUrl builds no attribution url`() { - assertThat( - buildStaticMapsApiUrl( - lat = 1.234, - lon = 5.678, - desiredZoom = 1.2, - desiredWidth = 100, - desiredHeight = 200, - darkMode = false, - doubleScale = false, - attributionPlacement = AttributionPlacement.Hidden, - ) - ).isEqualTo( - "https://api.maptiler.com/maps/9bc819c8-e627-474a-a348-ec144fe3d810/static/5.678,1.234,1.2/100x200.webp" + - "?key=fU3vlMsMn4Jb6dnEIFsx&attribution=false" - ) - } - - @Test - fun `buildStaticMapsApiUrl coerces zoom at 22 and width and height at max 2048 keeping aspect ratio`() { - assertThat( - buildStaticMapsApiUrl( - lat = 1.234, - lon = 5.678, - desiredZoom = 100.0, - desiredWidth = 8192, - desiredHeight = 4096, - darkMode = false, - doubleScale = false, - attributionPlacement = AttributionPlacement.BottomLeft, - ) - ).isEqualTo( - "https://api.maptiler.com/maps/9bc819c8-e627-474a-a348-ec144fe3d810/static/5.678,1.234,22.0/2048x1024.webp" + - "?key=fU3vlMsMn4Jb6dnEIFsx&attribution=bottomleft" - ) - } -} diff --git a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/map/MapView.kt b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/map/MapView.kt index 18d568d4a4..a344d8571e 100644 --- a/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/map/MapView.kt +++ b/features/location/impl/src/main/kotlin/io/element/android/features/location/impl/map/MapView.kt @@ -50,7 +50,7 @@ import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions import com.mapbox.mapboxsdk.style.layers.Property.ICON_ANCHOR_BOTTOM import io.element.android.features.location.api.Location -import io.element.android.features.location.api.internal.buildTileServerUrl +import io.element.android.features.location.api.internal.tileStyleUrl import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.components.Text @@ -102,7 +102,7 @@ fun MapView( isCompassEnabled = false isRotateGesturesEnabled = false } - map.setStyle(buildTileServerUrl(darkMode = darkMode)) { style -> + map.setStyle(tileStyleUrl(context, darkMode)) { style -> mapRefs = MapRefs( map = map, symbolManager = SymbolManager(mapView, map, style).apply { From 8b73abe089ba509839834728948b35b97bdc72ab Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 17 Jul 2023 17:14:41 +0200 Subject: [PATCH 23/59] Save text in composer when navigating to a sub node (opening an image from the timeline for instance). Fixes #870. --- .../messages/impl/messagecomposer/MessageComposerPresenter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt index 4d749b465e..020236e890 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt @@ -92,7 +92,7 @@ class MessageComposerPresenter @Inject constructor( val hasFocus = remember { mutableStateOf(false) } - val text: MutableState = remember { + val text: MutableState = rememberSaveable { mutableStateOf("") } From 24884328055f2d03cf7e8c0948d9b55fd07e15ea Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Mon, 17 Jul 2023 18:34:36 +0200 Subject: [PATCH 24/59] Hide encryption history + FTUE flow (#839) * First attempt at implementing encrypted history banner and removing old UTDs * Get the right behavior in the timeline * Implement the designs * Extract post-processing logic, add tests * Add encryption banner to timeline screenshots * Create FTUE feature to handle welcome screen and analytics * Move classes to their own packages, add tests for `DefaultFtueState`. * Remove unnecessary private MutableStateFlow * Move some FTUE related methods and classes back to the `impl` module * Handle back press at each FTUE step * Remove unneeded `TestScope` receiver for `createState` in tests. * Use light & dark previews for the banner view. * Move color customization from `TextStyle` to `Text` component. * Rename `InfoList` design components, use them in `AnalyticsOptInView` too. * Cleanup MatrixClient. * Fix copy&paste error Co-authored-by: Benoit Marty * Fix typo * Fix Maestro tests --------- Co-authored-by: ElementBot Co-authored-by: Benoit Marty --- .maestro/tests/account/login.yaml | 2 + .../assertWelcomeScreenDisplayed.yaml | 6 + appnav/build.gradle.kts | 2 + .../android/appnav/LoggedInFlowNode.kt | 48 ++--- build.gradle.kts | 4 +- .../analytics/impl/AnalyticsOptInView.kt | 116 ++++++------ features/ftue/api/build.gradle.kts | 27 +++ .../features/ftue/api/FtueEntryPoint.kt | 36 ++++ .../features/ftue/api/state/FtueState.kt | 23 +++ features/ftue/impl/build.gradle.kts | 55 ++++++ .../ftue/impl/DefaultFtueEntryPoint.kt | 46 +++++ .../features/ftue/impl/FtueFlowNode.kt | 154 ++++++++++++++++ .../ftue/impl/state/DefaultFtueState.kt | 89 +++++++++ .../features/ftue/impl/welcome/WelcomeNode.kt | 54 ++++++ .../features/ftue/impl/welcome/WelcomeView.kt | 128 +++++++++++++ .../state/AndroidWelcomeScreenState.kt | 43 +++++ .../impl/welcome/state/WelcomeScreenState.kt | 22 +++ .../impl/src/main/res/values/localazy.xml | 9 + .../ftue/impl/DefaultFtueStateTests.kt | 115 ++++++++++++ .../impl/welcome/state/FakeWelcomeState.kt | 30 ++++ .../res/drawable/onboarding_icon_light.png | Bin 0 -> 44244 bytes .../messages/impl/timeline/TimelineView.kt | 3 + .../components/TimelineItemVirtualRow.kt | 5 +- .../TimelineEncryptedHistoryBannerView.kt | 69 +++++++ .../factories/TimelineItemsFactory.kt | 1 - .../virtual/TimelineItemVirtualFactory.kt | 9 +- ...eItemEncryptedHistoryBannerVirtualModel.kt | 21 +++ .../atomic/atoms/ElementLogoAtom.kt | 170 ++++++++++++++++++ .../atomic/atoms/InfoListItemMolecule.kt | 113 ++++++++++++ .../atomic/molecules/InfoListOrganism.kt | 79 ++++++++ .../atomic/pages/OnBoardingPage.kt | 3 + .../src/main/res/drawable/element_logo.xml | 26 +++ .../libraries/matrix/api/MatrixClient.kt | 2 - .../item/virtual/VirtualTimelineItem.kt | 1 + libraries/matrix/impl/build.gradle.kts | 4 + .../libraries/matrix/impl/RustMatrixClient.kt | 1 + .../auth/RustMatrixAuthenticationService.kt | 2 + .../matrix/impl/room/RustMatrixRoom.kt | 6 +- .../impl/timeline/RustMatrixTimeline.kt | 29 ++- .../TimelineEncryptedHistoryPostProcessor.kt | 74 ++++++++ ...melineEncryptedHistoryPostProcessorTest.kt | 115 ++++++++++++ .../sessionstorage/api/SessionData.kt | 5 +- .../session-storage/impl/build.gradle.kts | 4 +- .../sessionstorage/impl/SessionDataMapper.kt | 10 +- .../libraries/matrix/session/SessionData.sq | 6 +- .../impl/src/main/sqldelight/migrations/0.sqm | 8 + .../impl/src/main/sqldelight/migrations/1.sqm | 1 + .../impl/DatabaseSessionStoreTests.kt | 3 +- .../android/libraries/testtags/TestTags.kt | 5 + .../src/main/res/values-de/translations.xml | 6 - .../src/main/res/values-fr/translations.xml | 5 - .../src/main/res/values-sk/translations.xml | 6 - .../src/main/res/values/localazy.xml | 6 - ...ViewPreview-D-0_1_null,NEXUS_5,1.0,en].png | 3 + ...ViewPreview-N-0_2_null,NEXUS_5,1.0,en].png | 3 + ...ViewPreview-D-7_8_null,NEXUS_5,1.0,en].png | 3 + ...ViewPreview-N-7_9_null,NEXUS_5,1.0,en].png | 3 + ...goAtomPreview-D_0_null,NEXUS_5,1.0,en].png | 3 + ...goAtomPreview-N_1_null,NEXUS_5,1.0,en].png | 3 + ...leculePreview-D_0_null,NEXUS_5,1.0,en].png | 3 + ...leculePreview-N_1_null,NEXUS_5,1.0,en].png | 3 + tools/localazy/config.json | 6 + 62 files changed, 1714 insertions(+), 123 deletions(-) create mode 100644 .maestro/tests/assertions/assertWelcomeScreenDisplayed.yaml create mode 100644 features/ftue/api/build.gradle.kts create mode 100644 features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/FtueEntryPoint.kt create mode 100644 features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/state/FtueState.kt create mode 100644 features/ftue/impl/build.gradle.kts create mode 100644 features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/DefaultFtueEntryPoint.kt create mode 100644 features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt create mode 100644 features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt create mode 100644 features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeNode.kt create mode 100644 features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeView.kt create mode 100644 features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/state/AndroidWelcomeScreenState.kt create mode 100644 features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/state/WelcomeScreenState.kt create mode 100644 features/ftue/impl/src/main/res/values/localazy.xml create mode 100644 features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueStateTests.kt create mode 100644 features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/welcome/state/FakeWelcomeState.kt create mode 100644 features/login/impl/src/main/res/drawable/onboarding_icon_light.png create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineEncryptedHistoryBannerView.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemEncryptedHistoryBannerVirtualModel.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/InfoListItemMolecule.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InfoListOrganism.kt create mode 100644 libraries/designsystem/src/main/res/drawable/element_logo.xml create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TimelineEncryptedHistoryPostProcessor.kt create mode 100644 libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TimelineEncryptedHistoryPostProcessorTest.kt create mode 100644 libraries/session-storage/impl/src/main/sqldelight/migrations/0.sqm create mode 100644 libraries/session-storage/impl/src/main/sqldelight/migrations/1.sqm create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.ftue.impl.welcome_null_DefaultGroup_WelcomeViewPreview-D-0_1_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.ftue.impl.welcome_null_DefaultGroup_WelcomeViewPreview-N-0_2_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-7_8_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-7_9_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomPreview-D_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomPreview-N_1_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_InfoListItemMoleculePreview-D_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_InfoListItemMoleculePreview-N_1_null,NEXUS_5,1.0,en].png diff --git a/.maestro/tests/account/login.yaml b/.maestro/tests/account/login.yaml index 70a9b956ef..6126e34459 100644 --- a/.maestro/tests/account/login.yaml +++ b/.maestro/tests/account/login.yaml @@ -23,6 +23,8 @@ appId: ${APP_ID} - inputText: ${PASSWORD} - pressKey: Enter - tapOn: "Continue" +- runFlow: ../assertions/assertWelcomeScreenDisplayed.yaml +- tapOn: "Continue" - runFlow: ../assertions/assertAnalyticsDisplayed.yaml - tapOn: "Not now" - runFlow: ../assertions/assertHomeDisplayed.yaml diff --git a/.maestro/tests/assertions/assertWelcomeScreenDisplayed.yaml b/.maestro/tests/assertions/assertWelcomeScreenDisplayed.yaml new file mode 100644 index 0000000000..73e8e78ef5 --- /dev/null +++ b/.maestro/tests/assertions/assertWelcomeScreenDisplayed.yaml @@ -0,0 +1,6 @@ +appId: ${APP_ID} +--- +- extendedWaitUntil: + visible: + id: "welcome_screen-title" + timeout: 10_000 diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index aa21c1f6d1..459acdeac6 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -54,6 +54,8 @@ dependencies { implementation(projects.tests.uitests) implementation(libs.coil) + implementation(projects.features.ftue.api) + implementation(projects.services.apperror.impl) implementation(projects.services.appnavstate.api) implementation(projects.services.analytics.api) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 3380dd91db..48a0743447 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -19,6 +19,8 @@ package io.element.android.appnav import android.os.Parcelable import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope @@ -41,7 +43,6 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.appnav.loggedin.LoggedInNode import io.element.android.appnav.room.RoomFlowNode import io.element.android.appnav.room.RoomLoadedFlowNode -import io.element.android.features.analytics.api.AnalyticsEntryPoint import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.invitelist.api.InviteListEntryPoint import io.element.android.features.networkmonitor.api.NetworkMonitor @@ -49,6 +50,8 @@ import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.features.preferences.api.PreferencesEntryPoint import io.element.android.features.roomlist.api.RoomListEntryPoint import io.element.android.features.verifysession.api.VerifySessionEntryPoint +import io.element.android.features.ftue.api.FtueEntryPoint +import io.element.android.features.ftue.api.state.FtueState import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler @@ -64,13 +67,10 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.sync.SyncState import io.element.android.libraries.matrix.ui.di.MatrixUIBindings import io.element.android.libraries.push.api.notifications.NotificationDrawerManager -import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.appnavstate.api.AppNavigationStateService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize @@ -81,14 +81,14 @@ class LoggedInFlowNode @AssistedInject constructor( private val roomListEntryPoint: RoomListEntryPoint, private val preferencesEntryPoint: PreferencesEntryPoint, private val createRoomEntryPoint: CreateRoomEntryPoint, - private val analyticsOptInEntryPoint: AnalyticsEntryPoint, private val appNavigationStateService: AppNavigationStateService, private val verifySessionEntryPoint: VerifySessionEntryPoint, private val inviteListEntryPoint: InviteListEntryPoint, - private val analyticsService: AnalyticsService, + private val ftueEntryPoint: FtueEntryPoint, private val coroutineScope: CoroutineScope, private val networkMonitor: NetworkMonitor, private val notificationDrawerManager: NotificationDrawerManager, + private val ftueState: FtueState, snackbarDispatcher: SnackbarDispatcher, ) : BackstackNode( backstack = BackStack( @@ -99,19 +99,6 @@ class LoggedInFlowNode @AssistedInject constructor( plugins = plugins ) { - private fun observeAnalyticsState() { - analyticsService.didAskUserConsent() - .distinctUntilChanged() - .onEach { isConsentAsked -> - if (isConsentAsked) { - backstack.removeLast(NavTarget.AnalyticsOptIn) - } else { - backstack.push(NavTarget.AnalyticsOptIn) - } - } - .launchIn(lifecycleScope) - } - interface Callback : Plugin { fun onOpenBugReport() = Unit } @@ -136,7 +123,7 @@ class LoggedInFlowNode @AssistedInject constructor( override fun onBuilt() { super.onBuilt() - observeAnalyticsState() + lifecycle.subscribe( onCreate = { plugins().forEach { it.onFlowCreated(id, inputs.matrixClient) } @@ -146,6 +133,10 @@ class LoggedInFlowNode @AssistedInject constructor( // TODO We do not support Space yet, so directly navigate to main space appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE) loggedInFlowProcessor.observeEvents(coroutineScope) + + if (ftueState.shouldDisplayFlow.value) { + backstack.push(NavTarget.Ftue) + } }, onResume = { syncService.startSync() @@ -209,7 +200,7 @@ class LoggedInFlowNode @AssistedInject constructor( object InviteList : NavTarget @Parcelize - object AnalyticsOptIn : NavTarget + object Ftue : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -306,8 +297,13 @@ class LoggedInFlowNode @AssistedInject constructor( .callback(callback) .build() } - NavTarget.AnalyticsOptIn -> { - analyticsOptInEntryPoint.createNode(this, buildContext) + NavTarget.Ftue -> { + ftueEntryPoint.nodeBuilder(this, buildContext) + .callback(object : FtueEntryPoint.Callback { + override fun onFtueFlowFinished() { + backstack.pop() + } + }).build() } } } @@ -335,7 +331,11 @@ class LoggedInFlowNode @AssistedInject constructor( transitionHandler = rememberDefaultTransitionHandler(), ) - PermanentChild(navTarget = NavTarget.Permanent) + val isFtueDisplayed by ftueState.shouldDisplayFlow.collectAsState() + + if (!isFtueDisplayed) { + PermanentChild(navTarget = NavTarget.Permanent) + } } } diff --git a/build.gradle.kts b/build.gradle.kts index 02c3ca3043..722e83fe87 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -246,7 +246,7 @@ koverMerged { name = "Check code coverage of states" target = kotlinx.kover.api.VerificationTarget.CLASS overrideClassFilter { - includes += "*State" + includes += "^*State$" excludes += "io.element.android.libraries.matrix.api.timeline.item.event.OtherState$*" excludes += "io.element.android.libraries.matrix.api.timeline.item.event.EventSendState$*" excludes += "io.element.android.libraries.matrix.api.room.RoomMembershipState*" @@ -262,6 +262,8 @@ koverMerged { excludes += "io.element.android.libraries.maplibre.compose.CameraPositionState*" excludes += "io.element.android.libraries.maplibre.compose.SaveableCameraPositionState" excludes += "io.element.android.libraries.maplibre.compose.SymbolState*" + excludes += "io.element.android.features.ftue.api.state.*" + excludes += "io.element.android.features.ftue.impl.welcome.state.*" } bound { minValue = 90 diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt index b9fe17d237..ba6d84ae74 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt @@ -16,12 +16,11 @@ package io.element.android.features.analytics.impl +import androidx.activity.compose.BackHandler import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding @@ -48,6 +47,8 @@ import androidx.compose.ui.unit.dp import io.element.android.features.analytics.api.AnalyticsOptInEvents import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule +import io.element.android.libraries.designsystem.atomic.molecules.InfoListItem +import io.element.android.libraries.designsystem.atomic.molecules.InfoListOrganism import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight @@ -60,6 +61,7 @@ import io.element.android.libraries.designsystem.theme.temporaryColorBgSpecial import io.element.android.libraries.designsystem.utils.LogCompositions import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.persistentListOf @Composable fun AnalyticsOptInView( @@ -69,6 +71,16 @@ fun AnalyticsOptInView( ) { LogCompositions(tag = "Analytics", msg = "Root") val eventSink = state.eventSink + + fun onTermsAccepted() { + eventSink(AnalyticsOptInEvents.EnableAnalytics(true)) + } + + fun onTermsDeclined() { + eventSink(AnalyticsOptInEvents.EnableAnalytics(false)) + } + + BackHandler(onBack = ::onTermsDeclined) HeaderFooterPage( modifier = modifier .fillMaxSize() @@ -76,7 +88,13 @@ fun AnalyticsOptInView( .imePadding(), header = { AnalyticsOptInHeader(state, onClickTerms) }, content = { AnalyticsOptInContent() }, - footer = { AnalyticsOptInFooter(eventSink) }) + footer = { + AnalyticsOptInFooter( + onTermsAccepted = ::onTermsAccepted, + onTermsDeclined = ::onTermsDeclined, + ) + } + ) } @Composable @@ -114,6 +132,19 @@ private fun AnalyticsOptInHeader( } } +@Composable +private fun CheckIcon(modifier: Modifier = Modifier) { + Icon( + modifier = Modifier + .size(20.dp) + .background(color = MaterialTheme.colorScheme.background, shape = CircleShape) + .padding(2.dp), + imageVector = Icons.Rounded.Check, + contentDescription = null, + tint = ElementTheme.colors.textActionAccent, + ) +} + @Composable private fun AnalyticsOptInContent( modifier: Modifier = Modifier, @@ -125,80 +156,45 @@ private fun AnalyticsOptInContent( verticalBias = -0.4f ) ) { - Column( - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - AnalyticsOptInContentRow( - text = stringResource(id = R.string.screen_analytics_prompt_data_usage), - idx = 0 - ) - AnalyticsOptInContentRow( - text = stringResource(id = R.string.screen_analytics_prompt_third_party_sharing), - idx = 1 - ) - AnalyticsOptInContentRow( - text = stringResource(id = R.string.screen_analytics_prompt_settings), - idx = 2 - ) - } - } -} - -@Composable -private fun AnalyticsOptInContentRow( - text: String, - idx: Int, - modifier: Modifier = Modifier, -) { - val radius = 14.dp - val bgShape = when (idx) { - 0 -> RoundedCornerShape(topStart = radius, topEnd = radius) - 2 -> RoundedCornerShape(bottomStart = radius, bottomEnd = radius) - else -> RoundedCornerShape(0.dp) - } - Row( - modifier = modifier - .fillMaxWidth() - .background( - color = ElementTheme.colors.temporaryColorBgSpecial, - shape = bgShape, - ) - .padding(vertical = 12.dp, horizontal = 20.dp), - ) { - Icon( - modifier = Modifier - .size(20.dp) - .background(color = MaterialTheme.colorScheme.background, shape = CircleShape) - .padding(2.dp), - imageVector = Icons.Rounded.Check, - contentDescription = null, - tint = ElementTheme.colors.textActionAccent, - ) - Text( - modifier = Modifier.padding(start = 16.dp), - text = text, - style = ElementTheme.typography.fontBodyMdMedium, - color = MaterialTheme.colorScheme.primary, + InfoListOrganism( + items = persistentListOf( + InfoListItem( + message = stringResource(id = R.string.screen_analytics_prompt_data_usage), + iconComposable = { CheckIcon() }, + ), + InfoListItem( + message = stringResource(id = R.string.screen_analytics_prompt_third_party_sharing), + iconComposable = { CheckIcon() }, + ), + InfoListItem( + message = stringResource(id = R.string.screen_analytics_prompt_settings), + iconComposable = { CheckIcon() }, + ), + ), + textStyle = ElementTheme.typography.fontBodyMdMedium, + iconTint = ElementTheme.colors.textPrimary, + backgroundColor = ElementTheme.colors.temporaryColorBgSpecial ) } } @Composable private fun AnalyticsOptInFooter( - eventSink: (AnalyticsOptInEvents) -> Unit, + onTermsAccepted: () -> Unit, + onTermsDeclined: () -> Unit, modifier: Modifier = Modifier, ) { ButtonColumnMolecule( modifier = modifier, ) { Button( - onClick = { eventSink(AnalyticsOptInEvents.EnableAnalytics(true)) }, + onClick = onTermsAccepted, modifier = Modifier.fillMaxWidth(), ) { Text(text = stringResource(id = CommonStrings.action_ok)) } TextButton( - onClick = { eventSink(AnalyticsOptInEvents.EnableAnalytics(false)) }, + onClick = onTermsDeclined, modifier = Modifier.fillMaxWidth(), ) { Text(text = stringResource(id = CommonStrings.action_not_now)) diff --git a/features/ftue/api/build.gradle.kts b/features/ftue/api/build.gradle.kts new file mode 100644 index 0000000000..9fd36026b9 --- /dev/null +++ b/features/ftue/api/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 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 + * + * http://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. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.features.ftue.api" +} + +dependencies { + implementation(projects.libraries.architecture) +} diff --git a/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/FtueEntryPoint.kt b/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/FtueEntryPoint.kt new file mode 100644 index 0000000000..649a327f6e --- /dev/null +++ b/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/FtueEntryPoint.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.ftue.api + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import io.element.android.libraries.architecture.FeatureEntryPoint + +interface FtueEntryPoint : FeatureEntryPoint { + + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + + interface NodeBuilder { + fun callback(callback: Callback): NodeBuilder + fun build(): Node + } + + interface Callback : Plugin { + fun onFtueFlowFinished() + } +} diff --git a/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/state/FtueState.kt b/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/state/FtueState.kt new file mode 100644 index 0000000000..2c19d4e3a7 --- /dev/null +++ b/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/state/FtueState.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.ftue.api.state + +import kotlinx.coroutines.flow.StateFlow + +interface FtueState { + val shouldDisplayFlow: StateFlow +} diff --git a/features/ftue/impl/build.gradle.kts b/features/ftue/impl/build.gradle.kts new file mode 100644 index 0000000000..0dee792464 --- /dev/null +++ b/features/ftue/impl/build.gradle.kts @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 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 + * + * http://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. + */ + +plugins { + id("io.element.android-compose-library") + alias(libs.plugins.anvil) + alias(libs.plugins.ksp) + id("kotlin-parcelize") +} + +android { + namespace = "io.element.android.features.ftue.impl" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(projects.anvilannotations) + anvil(projects.anvilcodegen) + api(projects.features.ftue.api) + implementation(projects.libraries.core) + implementation(projects.libraries.architecture) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) + implementation(projects.libraries.designsystem) + implementation(projects.libraries.uiStrings) + implementation(projects.libraries.testtags) + implementation(projects.features.analytics.api) + implementation(projects.services.analytics.api) + + testImplementation(libs.test.junit) + testImplementation(libs.coroutines.test) + testImplementation(libs.molecule.runtime) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.features.analytics.test) + + ksp(libs.showkase.processor) +} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/DefaultFtueEntryPoint.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/DefaultFtueEntryPoint.kt new file mode 100644 index 0000000000..9c2f74f072 --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/DefaultFtueEntryPoint.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.ftue.impl + +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.ftue.api.FtueEntryPoint +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultFtueEntryPoint @Inject constructor() : FtueEntryPoint { + + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): FtueEntryPoint.NodeBuilder { + val plugins = ArrayList() + + return object : FtueEntryPoint.NodeBuilder { + + override fun callback(callback: FtueEntryPoint.Callback): FtueEntryPoint.NodeBuilder { + plugins += callback + return this + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins) + } + } + } +} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt new file mode 100644 index 0000000000..0ff9c80d46 --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/FtueFlowNode.kt @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.ftue.impl + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.lifecycle.lifecycleScope +import com.bumble.appyx.core.composable.Children +import com.bumble.appyx.core.lifecycle.subscribe +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.navigation.backpresshandlerstrategies.BaseBackPressHandlerStrategy +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.newRoot +import com.bumble.appyx.navmodel.backstack.operation.replace +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.analytics.api.AnalyticsEntryPoint +import io.element.android.features.ftue.api.FtueEntryPoint +import io.element.android.features.ftue.impl.state.DefaultFtueState +import io.element.android.features.ftue.impl.state.FtueStep +import io.element.android.features.ftue.impl.welcome.WelcomeNode +import io.element.android.libraries.architecture.BackstackNode +import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.AppScope +import io.element.android.services.analytics.api.AnalyticsService +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch +import kotlinx.parcelize.Parcelize + +@ContributesNode(AppScope::class) +class FtueFlowNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val ftueState: DefaultFtueState, + private val analyticsEntryPoint: AnalyticsEntryPoint, + private val analyticsService: AnalyticsService, +) : BackstackNode( + backstack = BackStack( + initialElement = NavTarget.Placeholder, + savedStateMap = buildContext.savedStateMap, + backPressHandler = NoOpBackstackHandlerStrategy(), + ), + buildContext = buildContext, + plugins = plugins, +) { + + sealed interface NavTarget : Parcelable { + @Parcelize + object Placeholder : NavTarget + + @Parcelize + object WelcomeScreen : NavTarget + + @Parcelize + object AnalyticsOptIn : NavTarget + } + + private val callback = plugins.filterIsInstance().firstOrNull() + + override fun onBuilt() { + super.onBuilt() + + lifecycle.subscribe(onCreate = { + lifecycleScope.launch { moveToNextStep() } + }) + + analyticsService.didAskUserConsent() + .drop(1) // We only care about consent passing from not asked to asked state + .onEach { didAskUserConsent -> + if (didAskUserConsent) { + lifecycleScope.launch { moveToNextStep() } + } + } + .launchIn(lifecycleScope) + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + NavTarget.Placeholder -> { + createNode(buildContext) + } + NavTarget.WelcomeScreen -> { + val callback = object : WelcomeNode.Callback { + override fun onContinueClicked() { + ftueState.setWelcomeScreenShown() + lifecycleScope.launch { moveToNextStep() } + } + } + createNode(buildContext, listOf(callback)) + } + NavTarget.AnalyticsOptIn -> { + analyticsEntryPoint.createNode(this, buildContext) + } + } + } + + private suspend fun moveToNextStep() { + when (ftueState.getNextStep()) { + is FtueStep.WelcomeScreen -> { + backstack.newRoot(NavTarget.WelcomeScreen) + } + is FtueStep.AnalyticsOptIn -> { + backstack.replace(NavTarget.AnalyticsOptIn) + } + null -> callback?.onFtueFlowFinished() + } + } + + @Composable + override fun View(modifier: Modifier) { + Children( + navModel = backstack, + modifier = modifier, + transitionHandler = rememberDefaultTransitionHandler(), + ) + } + + @ContributesNode(AppScope::class) + class PlaceholderNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + ) : Node(buildContext, plugins = plugins) +} + +private class NoOpBackstackHandlerStrategy : BaseBackPressHandlerStrategy() { + override val canHandleBackPressFlow: StateFlow = MutableStateFlow(true) + + override fun onBackPressed() { + // No-op + } +} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt new file mode 100644 index 0000000000..39b100808f --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.ftue.impl.state + +import androidx.annotation.VisibleForTesting +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.ftue.api.state.FtueState +import io.element.android.features.ftue.impl.welcome.state.WelcomeScreenState +import io.element.android.libraries.di.AppScope +import io.element.android.services.analytics.api.AnalyticsService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultFtueState @Inject constructor( + private val coroutineScope: CoroutineScope, + private val analyticsService: AnalyticsService, + private val welcomeScreenState: WelcomeScreenState, +) : FtueState { + + override val shouldDisplayFlow = MutableStateFlow(isAnyStepIncomplete()) + + init { + analyticsService.didAskUserConsent() + .onEach { updateState() } + .launchIn(coroutineScope) + } + + fun getNextStep(currentStep: FtueStep? = null): FtueStep? = + when (currentStep) { + null -> if (shouldDisplayWelcomeScreen()) FtueStep.WelcomeScreen else getNextStep( + FtueStep.WelcomeScreen + ) + FtueStep.WelcomeScreen -> if (needsAnalyticsOptIn()) FtueStep.AnalyticsOptIn else getNextStep( + FtueStep.AnalyticsOptIn + ) + FtueStep.AnalyticsOptIn -> null + } + + private fun isAnyStepIncomplete(): Boolean { + return listOf( + shouldDisplayWelcomeScreen(), + needsAnalyticsOptIn() + ).any { it } + } + + private fun needsAnalyticsOptIn(): Boolean { + // We need this function to not be suspend, so we need to load the value through runBlocking + return runBlocking { analyticsService.didAskUserConsent().first().not() } + } + + private fun shouldDisplayWelcomeScreen(): Boolean { + return welcomeScreenState.isWelcomeScreenNeeded() + } + + fun setWelcomeScreenShown() { + welcomeScreenState.setWelcomeScreenShown() + updateState() + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal fun updateState() { + shouldDisplayFlow.value = isAnyStepIncomplete() + } +} + +sealed interface FtueStep { + object WelcomeScreen : FtueStep + object AnalyticsOptIn : FtueStep +} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeNode.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeNode.kt new file mode 100644 index 0000000000..f4e0d9f640 --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeNode.kt @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.ftue.impl.welcome + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.di.AppScope + +@ContributesNode(AppScope::class) +class WelcomeNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val buildMeta: BuildMeta, +) : Node(buildContext, plugins = plugins) { + + interface Callback : Plugin { + fun onContinueClicked() + } + + private fun onContinueClicked() { + plugins.filterIsInstance().forEach { it.onContinueClicked() } + } + + @Composable + override fun View(modifier: Modifier) { + WelcomeView( + applicationName = buildMeta.applicationName, + onContinueClicked = ::onContinueClicked, + modifier = modifier + ) + } + +} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeView.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeView.kt new file mode 100644 index 0000000000..ccb55494b8 --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeView.kt @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.ftue.impl.welcome + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AddComment +import androidx.compose.material.icons.outlined.Lock +import androidx.compose.material.icons.outlined.NewReleases +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import io.element.android.features.ftue.impl.R +import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtom +import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtomSize +import io.element.android.libraries.designsystem.atomic.molecules.InfoListItem +import io.element.android.libraries.designsystem.atomic.molecules.InfoListOrganism +import io.element.android.libraries.designsystem.atomic.pages.OnBoardingPage +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.testtags.testTag +import io.element.android.libraries.theme.ElementTheme +import io.element.android.libraries.ui.strings.CommonStrings +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun WelcomeView( + applicationName: String, + modifier: Modifier = Modifier, + onContinueClicked: () -> Unit, +) { + BackHandler(onBack = onContinueClicked) + OnBoardingPage( + modifier = modifier + .systemBarsPadding() + .fillMaxSize(), + content = { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Spacer(modifier = Modifier.height(78.dp)) + ElementLogoAtom(size = ElementLogoAtomSize.Medium) + Spacer(modifier = Modifier.height(32.dp)) + Text( + modifier = Modifier.testTag(TestTags.welcomeScreenTitle), + text = stringResource(R.string.screen_welcome_title, applicationName), + style = ElementTheme.typography.fontHeadingLgBold, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = stringResource(R.string.screen_welcome_subtitle), + style = ElementTheme.typography.fontBodyMdRegular, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(40.dp)) + InfoListOrganism( + items = listItems(), + textStyle = ElementTheme.typography.fontBodyMdMedium, + iconTint = ElementTheme.colors.iconSecondary, + backgroundColor = ElementTheme.colors.bgCanvasDefault.copy(alpha = 0.7f), + ) + Spacer(modifier = Modifier.height(32.dp)) + } + }, + footer = { + Button(modifier = Modifier.fillMaxWidth(), onClick = onContinueClicked) { + Text(text = stringResource(CommonStrings.action_continue)) + } + Spacer(modifier = Modifier.height(32.dp)) + } + ) +} + +@Composable +private fun listItems() = persistentListOf( + InfoListItem( + message = stringResource(R.string.screen_welcome_bullet_1), + iconVector = Icons.Outlined.NewReleases, + ), + InfoListItem( + message = stringResource(R.string.screen_welcome_bullet_2), + iconVector = Icons.Outlined.Lock, + ), + InfoListItem( + message = stringResource(R.string.screen_welcome_bullet_3), + iconVector = Icons.Outlined.AddComment, + ), +) + +@DayNightPreviews +@Composable +internal fun WelcomeViewPreview() { + ElementPreview { + WelcomeView(applicationName = "Element X", onContinueClicked = {}) + } +} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/state/AndroidWelcomeScreenState.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/state/AndroidWelcomeScreenState.kt new file mode 100644 index 0000000000..c482b4e744 --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/state/AndroidWelcomeScreenState.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.ftue.impl.welcome.state + +import android.content.SharedPreferences +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.DefaultPreferences +import io.element.android.libraries.di.SingleIn +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +@SingleIn(AppScope::class) +class AndroidWelcomeScreenState @Inject constructor( + @DefaultPreferences private val sharedPreferences: SharedPreferences, +): WelcomeScreenState { + + companion object { + private const val IS_WELCOME_SCREEN_SHOWN = "is_welcome_screen_shown" + } + + override fun isWelcomeScreenNeeded(): Boolean { + return sharedPreferences.getBoolean(IS_WELCOME_SCREEN_SHOWN, false).not() + } + + override fun setWelcomeScreenShown() { + sharedPreferences.edit().putBoolean(IS_WELCOME_SCREEN_SHOWN, true).apply() + } +} diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/state/WelcomeScreenState.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/state/WelcomeScreenState.kt new file mode 100644 index 0000000000..0e5f79d7c1 --- /dev/null +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/state/WelcomeScreenState.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.ftue.impl.welcome.state + +interface WelcomeScreenState { + fun isWelcomeScreenNeeded(): Boolean + fun setWelcomeScreenShown() +} diff --git a/features/ftue/impl/src/main/res/values/localazy.xml b/features/ftue/impl/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..17999e7158 --- /dev/null +++ b/features/ftue/impl/src/main/res/values/localazy.xml @@ -0,0 +1,9 @@ + + + "Calls, location sharing, search and more will be added later this year." + "Message history for encrypted rooms won’t be available in this update." + "We’d love to hear from you, let us know what you think via the settings page." + "Let\'s go!" + "Here’s what you need to know:" + "Welcome to %1$s!" + diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueStateTests.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueStateTests.kt new file mode 100644 index 0000000000..ce1683e8e5 --- /dev/null +++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueStateTests.kt @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.ftue.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.features.analytics.test.FakeAnalyticsService +import io.element.android.features.ftue.impl.state.DefaultFtueState +import io.element.android.features.ftue.impl.state.FtueStep +import io.element.android.features.ftue.impl.welcome.state.FakeWelcomeState +import io.element.android.services.analytics.api.AnalyticsService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class DefaultFtueStateTests { + + @Test + fun `given any check being false, should display flow is true`() = runTest { + val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) + val state = createState(coroutineScope) + + assertThat(state.shouldDisplayFlow.value).isTrue() + + // Cleanup + coroutineScope.cancel() + } + + @Test + fun `given all checks being true, should display flow is false`() = runTest { + val welcomeState = FakeWelcomeState() + val analyticsService = FakeAnalyticsService() + val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) + + val state = createState(coroutineScope, welcomeState, analyticsService) + + welcomeState.setWelcomeScreenShown() + analyticsService.setDidAskUserConsent() + state.updateState() + + assertThat(state.shouldDisplayFlow.value).isFalse() + + // Cleanup + coroutineScope.cancel() + } + + @Test + fun `traverse flow`() = runTest { + val welcomeState = FakeWelcomeState() + val analyticsService = FakeAnalyticsService() + val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) + + val state = createState(coroutineScope, welcomeState, analyticsService) + val steps = mutableListOf() + + // First step, welcome screen + steps.add(state.getNextStep(steps.lastOrNull())) + welcomeState.setWelcomeScreenShown() + + // Second step, analytics opt in + steps.add(state.getNextStep(steps.lastOrNull())) + analyticsService.setDidAskUserConsent() + + // Final step (null) + steps.add(state.getNextStep(steps.lastOrNull())) + + assertThat(steps).containsExactly( + FtueStep.WelcomeScreen, + FtueStep.AnalyticsOptIn, + null, // Final state + ) + + // Cleanup + coroutineScope.cancel() + } + + @Test + fun `if a check for a step is true, start from the next one`() = runTest { + val coroutineScope = CoroutineScope(coroutineContext + SupervisorJob()) + val analyticsService = FakeAnalyticsService() + val state = createState(coroutineScope = coroutineScope, analyticsService = analyticsService) + + state.setWelcomeScreenShown() + assertThat(state.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn) + + analyticsService.setDidAskUserConsent() + assertThat(state.getNextStep(FtueStep.WelcomeScreen)).isNull() + + // Cleanup + coroutineScope.cancel() + } + + private fun createState( + coroutineScope: CoroutineScope, + welcomeState: FakeWelcomeState = FakeWelcomeState(), + analyticsService: AnalyticsService = FakeAnalyticsService() + ) = DefaultFtueState(coroutineScope, analyticsService, welcomeState) + +} diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/welcome/state/FakeWelcomeState.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/welcome/state/FakeWelcomeState.kt new file mode 100644 index 0000000000..198d79115a --- /dev/null +++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/welcome/state/FakeWelcomeState.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.ftue.impl.welcome.state + +class FakeWelcomeState : WelcomeScreenState { + + private var isWelcomeScreenNeeded = true + + override fun isWelcomeScreenNeeded(): Boolean { + return isWelcomeScreenNeeded + } + + override fun setWelcomeScreenShown() { + isWelcomeScreenNeeded = false + } +} diff --git a/features/login/impl/src/main/res/drawable/onboarding_icon_light.png b/features/login/impl/src/main/res/drawable/onboarding_icon_light.png new file mode 100644 index 0000000000000000000000000000000000000000..ffd8631c4777248a3eff841ae579fa91a724bc07 GIT binary patch literal 44244 zcmV)VK(D`vP)%0DYo!WJ}dXl=U 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 >|Jg77D<$&zDPx9@Nya}3+L4)zccC46W5uz#nx z54Kf`2p_H~gIGWFK5T!sb6)<~)_(r^g!|OL!tT=hmit*GxB;6>p10E)q+6nyyxtj0 zQ5Es4p}Gt7uj;!=Gz(M~X-)kSQS}x6*Y}Oh#_s{Mk(i2^fHbolIqnnt4W`>Cb|l+w zEi%Q4IwAN1wGzg%j;w`dr0Z52r97_ODnLqkT)H)+HJ5l$zDhThvdf`Z8uVNC`wjOg zY(rVzb{_K@nCG+-u&AaF_)$p+G1D>-tP&bF1+%dw!a|~{ zD#dYCX6==34BLlXLce``zAc=e_R=}UTDBD5kpT@Ovozmcx4!)4 zFAvX0D3-j_$o8}L#2k-3W_DTm{TSSD>Y~1Ha={7)ySnt;mfoLCQdOtSd0g-|q z5_>S%g@86Yy~;uU*%dO7?OZxI+_H@bAsx#DiB@FH*4B~?CPX4It3#Gu0RVxtK#DRb zZNKt1_T?C6emmYIge`4bR%Bq{KrSPFCShPqinIgYkcKhwO55op;xdo;R*R~<;v zCc`FFN z05e)|W0N}O85pQE_@%HduoD?L*d`1rvpbllga7cl92=5BhYiVqyXtLkQ-31gi$qmB z%YXullxM#;z3ENPLY&x!eZwF7S~NX&5XZ5N!4D%8Hc^IKh4Zlw+c+1W`Qv^#FUN6C zTQ6#h=zZ~b%{A8yBlp>lZS2E(@!Yzk5YTf)usX}HxuUN?F$s;0y>S@O&O70r&Buy% z0n^DnW-uKnE}%yqZRk8sN^m)LKWA=OYNvD>lTg75n#bu;k0qE6axZR4a+FIS}Q zjE*694l)bQ2CWOm8>NvKl$Za?y1Y?gNUsP3lG#!HH_VbnR7@Uv`OPRW^W#6hQ-4&- z&GW-kiV&u>%E6BmYAGsdP&MmvuBVC!i8qT%dX;-}>R?9u=_hG!A&lBI2_&i%j0||g zxmm&-s9T&%&Yhyt@@K7}_+wvNXFr_yY{OScyykSl;52_BJc=AMwsC&`bP|vOZx070-tC$2P=n>v*m9ZuF8md!^$;*IV6B*hmC;IJhid1Kpq9h* zv^-xoe0ChIHyiInR1WPKz2_6!f%;`hEiHpI=Ay6w*SL>9gJC>e&Vqn2NIWWLz!J>G^#Ed z-C1OOmrRJV9d}&4lM*WF-5SJJo^4u}5^oh6>gdHRR+ezX8kobKLs**m3w_bCvuu

);pai>8T`>ci+P%#st&GLD%)U)$L0H7d-tX=E6{#Xjo3Pg zW@h)4w-@Ksb&f0VGrAwD7}vE34cnI`$J%~!&!#%+{i zx;&nD%^*V7oPseYs@Qht2h(DMGHd0wN^cGYW+uA57x1QM7%yIRRa zcV{W5ah14NU)ttJ9Ll3WgsSP5662Qbr4yEHET6AzW96kd+`N=tGwQf5-_wSwxIycY zR+D_TeN`#Cp_W}3^oo^u7YLOBp&>)%BauDDwhe~cpbgrf4LTY9-$EUC<}w`m4FCWD M07*qoM6N<$g3+ZiZ2$lO literal 0 HcmV?d00001 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 ee7a57c70e..949be6ed85 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 @@ -64,6 +64,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent +import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemEncryptedHistoryBannerVirtualModel import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.components.FloatingActionButton @@ -72,6 +73,8 @@ import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.theme.ElementTheme import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.launch @Composable diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemVirtualRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemVirtualRow.kt index 820560c2a2..d6b1c06f54 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemVirtualRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemVirtualRow.kt @@ -18,10 +18,11 @@ package io.element.android.features.messages.impl.timeline.components import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import io.element.android.features.messages.impl.timeline.components.virtual.TimelineEncryptedHistoryBannerView import io.element.android.features.messages.impl.timeline.components.virtual.TimelineItemDaySeparatorView -import io.element.android.features.messages.impl.timeline.components.virtual.TimelineLoadingMoreIndicator import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemDaySeparatorModel +import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemEncryptedHistoryBannerVirtualModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel @Composable @@ -32,5 +33,7 @@ fun TimelineItemVirtualRow( when (virtual.model) { is TimelineItemDaySeparatorModel -> TimelineItemDaySeparatorView(virtual.model, modifier) TimelineItemReadMarkerModel -> return + is TimelineItemEncryptedHistoryBannerVirtualModel -> TimelineEncryptedHistoryBannerView(modifier) } } + diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineEncryptedHistoryBannerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineEncryptedHistoryBannerView.kt new file mode 100644 index 0000000000..055e4bb876 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/virtual/TimelineEncryptedHistoryBannerView.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.timeline.components.virtual + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.element.android.features.messages.impl.R +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.theme.ElementTheme + +@Composable +fun TimelineEncryptedHistoryBannerView(modifier: Modifier = Modifier) { + Row( + modifier = modifier + .padding(start = 16.dp, end = 16.dp, top = 24.dp, bottom = 32.dp) + .clip(MaterialTheme.shapes.small) + .border(1.dp, ElementTheme.colors.borderInfoSubtle, MaterialTheme.shapes.small) + .background(ElementTheme.colors.bgInfoSubtle) + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = "Info", + tint = ElementTheme.colors.iconInfoPrimary + ) + Text( + text = stringResource(R.string.screen_room_encrypted_history_banner), + style = ElementTheme.typography.fontBodyMdMedium, + color = ElementTheme.colors.textInfoPrimary + ) + } +} + +@DayNightPreviews +@Composable +internal fun TimelineEncryptedHistoryBannerViewPreview() { + ElementTheme { + TimelineEncryptedHistoryBannerView() + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt index f607a0e034..aa9786c945 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt @@ -45,7 +45,6 @@ class TimelineItemsFactory @Inject constructor( private val virtualItemFactory: TimelineItemVirtualFactory, private val timelineItemGrouper: TimelineItemGrouper, ) { - private val timelineItems = MutableStateFlow(persistentListOf()) private val timelineItemsCache = arrayListOf() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemVirtualFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemVirtualFactory.kt index cca1786bf8..6178b1dee7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemVirtualFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/virtual/TimelineItemVirtualFactory.kt @@ -17,6 +17,7 @@ package io.element.android.features.messages.impl.timeline.factories.virtual import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemEncryptedHistoryBannerVirtualModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemReadMarkerModel import io.element.android.features.messages.impl.timeline.model.virtual.TimelineItemVirtualModel import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem @@ -30,8 +31,13 @@ class TimelineItemVirtualFactory @Inject constructor( fun create( virtualTimelineItem: MatrixTimelineItem.Virtual, ): TimelineItem.Virtual { + val id = if (virtualTimelineItem.virtual is VirtualTimelineItem.EncryptedHistoryBanner) { + "encrypted_history_banner" + } else { + virtualTimelineItem.uniqueId.toString() + } return TimelineItem.Virtual( - id = virtualTimelineItem.uniqueId.toString(), + id = id, model = virtualTimelineItem.computeModel() ) } @@ -40,6 +46,7 @@ class TimelineItemVirtualFactory @Inject constructor( return when (val inner = virtual) { is VirtualTimelineItem.DayDivider -> daySeparatorFactory.create(inner) is VirtualTimelineItem.ReadMarker -> TimelineItemReadMarkerModel + is VirtualTimelineItem.EncryptedHistoryBanner -> TimelineItemEncryptedHistoryBannerVirtualModel } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemEncryptedHistoryBannerVirtualModel.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemEncryptedHistoryBannerVirtualModel.kt new file mode 100644 index 0000000000..442aed5734 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/virtual/TimelineItemEncryptedHistoryBannerVirtualModel.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.timeline.model.virtual + +object TimelineItemEncryptedHistoryBannerVirtualModel : TimelineItemVirtualModel { + override val type: String = "TimelineItemEncryptedHistoryBannerVirtualModel" +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt new file mode 100644 index 0000000000..19a1798f74 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2023 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.atomic.atoms + +import android.graphics.BlurMaskFilter +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.blur +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.geometry.RoundRect +import androidx.compose.ui.graphics.ClipOp +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.clipPath +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.R +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.theme.ElementTheme + +@Composable +fun ElementLogoAtom( + size: ElementLogoAtomSize, + modifier: Modifier = Modifier, +) { + val outerSize = when (size) { + ElementLogoAtomSize.Large -> 158.dp + ElementLogoAtomSize.Medium -> 120.dp + } + val logoSize = when (size) { + ElementLogoAtomSize.Large -> 110.dp + ElementLogoAtomSize.Medium -> 83.5.dp + } + val cornerRadius = when(size) { + ElementLogoAtomSize.Large -> 44.dp + ElementLogoAtomSize.Medium -> 33.dp + } + val borderWidth = when (size) { + ElementLogoAtomSize.Large -> 1.dp + ElementLogoAtomSize.Medium -> 0.38.dp + } + val blur = if (isSystemInDarkTheme()) { + 160.dp + } else { + 24.dp + } + //box-shadow: 0px 6.075949668884277px 24.30379867553711px 0px #1B1D2280; + val shadowColor = if (isSystemInDarkTheme()) { + Color.Black.copy(alpha = 0.4f) + } else { + Color(0x401B1D22) + } + val backgroundColor = if (isSystemInDarkTheme()) Color.White.copy(alpha = 0.2f) else Color.White.copy(alpha = 0.4f) + val borderColor = if (isSystemInDarkTheme()) Color.White.copy(alpha = 0.8f) else Color.White.copy(alpha = 0.4f) + Box( + modifier = modifier + .size(outerSize) + .border(borderWidth, borderColor, RoundedCornerShape(cornerRadius)), + contentAlignment = Alignment.Center, + ) { + Box( + Modifier + .size(outerSize) + .shapeShadow( + color = shadowColor, + cornerRadius = cornerRadius, + blurRadius = 32.dp, + offsetY = 8.dp, + ) + ) + Box( + Modifier + .clip(RoundedCornerShape(cornerRadius)) + .size(outerSize) + .background(backgroundColor) + .blur(blur) + ) + Image( + modifier = Modifier.size(logoSize), + painter = painterResource(id = R.drawable.element_logo), + contentDescription = null + ) + } +} + +enum class ElementLogoAtomSize { + Medium, + Large +} + +@Composable +@DayNightPreviews +internal fun ElementLogoAtomPreview() { + ElementPreview { + Box( + Modifier + .size(170.dp) + .background(ElementTheme.colors.bgSubtlePrimary)) + ElementLogoAtom(ElementLogoAtomSize.Large) + } +} + +fun Modifier.shapeShadow( + color: Color = Color.Black, + cornerRadius: Dp = 0.dp, + offsetX: Dp = 0.dp, + offsetY: Dp = 0.dp, + blurRadius: Dp = 0.dp, +) = then( + drawBehind { + drawIntoCanvas { canvas -> + val path = Path().apply { + addRoundRect(RoundRect(Rect(Offset.Zero, size), CornerRadius(cornerRadius.toPx()))) + } + + clipPath(path, ClipOp.Difference) { + val paint = Paint() + val frameworkPaint = paint.asFrameworkPaint() + if (blurRadius != 0.dp) { + frameworkPaint.maskFilter = (BlurMaskFilter(blurRadius.toPx(), BlurMaskFilter.Blur.NORMAL)) + } + frameworkPaint.color = color.toArgb() + + val leftPixel = offsetX.toPx() + val topPixel = offsetY.toPx() + val rightPixel = size.width + topPixel + val bottomPixel = size.height + leftPixel + + canvas.drawRect( + left = leftPixel, + top = topPixel, + right = rightPixel, + bottom = bottomPixel, + paint = paint, + ) + } + } + } +) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/InfoListItemMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/InfoListItemMolecule.kt new file mode 100644 index 0000000000..6b20c96880 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/InfoListItemMolecule.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2023 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.atomic.atoms + +import androidx.compose.foundation.background +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Text + +@Composable +fun InfoListItemMolecule( + message: @Composable () -> Unit, + position: InfoListItemPosition, + backgroundColor: Color, + modifier: Modifier = Modifier, + icon: @Composable () -> Unit = {}, +) { + val radius = 14.dp + val backgroundShape = remember(position) { + when (position) { + InfoListItemPosition.Single -> RoundedCornerShape(radius) + InfoListItemPosition.Top -> RoundedCornerShape(topStart = radius, topEnd = radius) + InfoListItemPosition.Middle -> RoundedCornerShape(0.dp) + InfoListItemPosition.Bottom -> RoundedCornerShape(bottomStart = radius, bottomEnd = radius) + } + } + Row( + modifier = modifier + .fillMaxWidth() + .background( + color = backgroundColor, + shape = backgroundShape, + ) + .padding(vertical = 12.dp, horizontal = 20.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + icon() + message() + } +} + +@DayNightPreviews +@Composable +fun InfoListItemMoleculePreview() { + ElementPreview { + val color = if (isSystemInDarkTheme()) Color.DarkGray else Color.LightGray + Column( + modifier = Modifier.padding(10.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + InfoListItemMolecule( + message = { Text("A single item") }, + icon = { Icon(imageVector = Icons.Default.Info, contentDescription = null) }, + position = InfoListItemPosition.Single, + backgroundColor = color, + ) + InfoListItemMolecule( + message = { Text("A top item") }, + icon = { Icon(imageVector = Icons.Default.Info, contentDescription = null) }, + position = InfoListItemPosition.Top, + backgroundColor = color, + ) + InfoListItemMolecule( + message = { Text("A middle item") }, + icon = { Icon(imageVector = Icons.Default.Info, contentDescription = null) }, + position = InfoListItemPosition.Middle, + backgroundColor = color, + ) + InfoListItemMolecule( + message = { Text("A bottom item") }, + icon = { Icon(imageVector = Icons.Default.Info, contentDescription = null) }, + position = InfoListItemPosition.Bottom, + backgroundColor = color, + ) + } + } +} + +enum class InfoListItemPosition { + Top, + Middle, + Bottom, + Single, +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InfoListOrganism.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InfoListOrganism.kt new file mode 100644 index 0000000000..60c61d99fc --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InfoListOrganism.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2023 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.atomic.molecules + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalTextStyle +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.atomic.atoms.InfoListItemMolecule +import io.element.android.libraries.designsystem.atomic.atoms.InfoListItemPosition +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Text +import kotlinx.collections.immutable.ImmutableList + +@Composable +fun InfoListOrganism( + items: ImmutableList, + backgroundColor: Color, + modifier: Modifier = Modifier, + iconTint: Color = LocalContentColor.current, + textStyle: TextStyle = LocalTextStyle.current, + verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(4.dp), +) { + Column( + modifier = modifier, + verticalArrangement = verticalArrangement, + ) { + for ((index, item) in items.withIndex()) { + val position = when { + items.size == 1 -> InfoListItemPosition.Single + index == 0 -> InfoListItemPosition.Top + index == items.size - 1 -> InfoListItemPosition.Bottom + else -> InfoListItemPosition.Middle + } + InfoListItemMolecule( + message = { Text(item.message, style = textStyle) }, + icon = { + if (item.iconId != null) { + Icon(resourceId = item.iconId, contentDescription = null, tint = iconTint) + } else if (item.iconVector != null) { + Icon(imageVector = item.iconVector, contentDescription = null, tint = iconTint) + } else { + item.iconComposable() + } + }, + position = position, + backgroundColor = backgroundColor, + ) + } + } +} + +data class InfoListItem( + val message: String, + @DrawableRes val iconId: Int? = null, + val iconVector: ImageVector? = null, + val iconComposable: @Composable () -> Unit = {}, +) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/OnBoardingPage.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/OnBoardingPage.kt index 511bed24b5..ec3ee92be8 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/OnBoardingPage.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/OnBoardingPage.kt @@ -41,12 +41,14 @@ import io.element.android.libraries.theme.ElementTheme * * Ref: https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=133-5427&t=5SHVppfYzjvkEywR-0 * @param modifier Classical modifier. + * @param contentAlignment horizontal alignment of the contents. * @param footer optional footer. * @param content main content. */ @Composable fun OnBoardingPage( modifier: Modifier = Modifier, + contentAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, footer: @Composable () -> Unit = {}, content: @Composable () -> Unit = {}, ) { @@ -78,6 +80,7 @@ fun OnBoardingPage( .weight(1f) .padding(horizontal = 24.dp) .fillMaxWidth(), + horizontalAlignment = contentAlignment, ) { content() } diff --git a/libraries/designsystem/src/main/res/drawable/element_logo.xml b/libraries/designsystem/src/main/res/drawable/element_logo.xml new file mode 100644 index 0000000000..0101c0d541 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/element_logo.xml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 67e9e622b7..747de5f554 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -31,9 +31,7 @@ import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationService -import kotlinx.coroutines.TimeoutCancellationException import java.io.Closeable -import kotlin.time.Duration interface MatrixClient : Closeable { val sessionId: SessionId diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/virtual/VirtualTimelineItem.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/virtual/VirtualTimelineItem.kt index ed761a3d43..11fd8b9c63 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/virtual/VirtualTimelineItem.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/virtual/VirtualTimelineItem.kt @@ -24,4 +24,5 @@ sealed interface VirtualTimelineItem { object ReadMarker : VirtualTimelineItem + object EncryptedHistoryBanner : VirtualTimelineItem } diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index e18d13c2b2..7786a3ee3f 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -41,4 +41,8 @@ dependencies { implementation("net.java.dev.jna:jna:5.13.0@aar") implementation(libs.androidx.datastore.preferences) implementation(libs.serialization.json) + + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) + testImplementation(projects.libraries.matrix.test) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 8798e01f98..d7195ebf11 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -157,6 +157,7 @@ class RustMatrixClient constructor( coroutineDispatchers = dispatchers, systemClock = clock, roomContentForwarder = roomContentForwarder, + sessionData = sessionStore.getSession(sessionId.value)!!, ) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt index bdb87b298c..0600e96f3b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt @@ -45,6 +45,7 @@ import org.matrix.rustcomponents.sdk.ClientBuilder import org.matrix.rustcomponents.sdk.Session import org.matrix.rustcomponents.sdk.use import java.io.File +import java.util.Date import javax.inject.Inject import org.matrix.rustcomponents.sdk.AuthenticationService as RustAuthenticationService @@ -208,4 +209,5 @@ private fun Session.toSessionData() = SessionData( refreshToken = refreshToken, homeserverUrl = homeserverUrl, slidingSyncProxy = slidingSyncProxy, + loginTimestamp = Date(), ) 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 9eced2d5cb..0b886af8ed 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 @@ -42,6 +42,8 @@ import io.element.android.libraries.matrix.impl.room.location.toInner import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import io.element.android.libraries.matrix.impl.timeline.backPaginationStatusFlow import io.element.android.libraries.matrix.impl.timeline.timelineDiffFlow +import io.element.android.libraries.sessionstorage.api.SessionData +import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -73,6 +75,7 @@ class RustMatrixRoom( private val coroutineDispatchers: CoroutineDispatchers, private val systemClock: SystemClock, private val roomContentForwarder: RoomContentForwarder, + private val sessionData: SessionData, ) : MatrixRoom { override val roomId = RoomId(innerRoom.id()) @@ -91,7 +94,8 @@ class RustMatrixRoom( matrixRoom = this, innerRoom = innerRoom, roomCoroutineScope = roomCoroutineScope, - dispatcher = roomDispatcher + dispatcher = roomDispatcher, + lastLoginTimestamp = sessionData.loginTimestamp, ) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt index 2c245c7164..e213fb623c 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt @@ -21,19 +21,23 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import io.element.android.libraries.matrix.api.timeline.TimelineException +import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessageMapper import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelineItemMapper import io.element.android.libraries.matrix.impl.timeline.item.event.TimelineEventContentMapper import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper import kotlinx.coroutines.CompletableDeferred +import io.element.android.libraries.matrix.impl.timeline.postprocessor.TimelineEncryptedHistoryPostProcessor import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.getAndUpdate +import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.sample import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.BackPaginationStatus @@ -43,6 +47,7 @@ import org.matrix.rustcomponents.sdk.TimelineDiff import org.matrix.rustcomponents.sdk.TimelineItem import timber.log.Timber import java.util.concurrent.atomic.AtomicBoolean +import java.util.Date private const val INITIAL_MAX_SIZE = 50 @@ -51,6 +56,7 @@ class RustMatrixTimeline( private val matrixRoom: MatrixRoom, private val innerRoom: Room, private val dispatcher: CoroutineDispatcher, + private val lastLoginTimestamp: Date?, ) : MatrixTimeline { private val initLatch = CompletableDeferred() @@ -63,6 +69,12 @@ class RustMatrixTimeline( MatrixTimeline.PaginationState(hasMoreToLoadBackwards = true, isBackPaginating = false) ) + private val encryptedHistoryPostProcessor = TimelineEncryptedHistoryPostProcessor( + lastLoginTimestamp = lastLoginTimestamp, + isRoomEncrypted = matrixRoom.isEncrypted, + paginationStateFlow = _paginationState, + ) + private val timelineItemFactory = MatrixTimelineItemMapper( fetchDetailsForEvent = this::fetchDetailsForEvent, roomCoroutineScope = roomCoroutineScope, @@ -81,8 +93,11 @@ class RustMatrixTimeline( override val paginationState: StateFlow = _paginationState.asStateFlow() - @OptIn(FlowPreview::class) + @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) override val timelineItems: Flow> = _timelineItems.sample(50) + .mapLatest { items -> + encryptedHistoryPostProcessor.process(items) + } internal suspend fun postItems(items: List) { // Split the initial items in multiple list as there is no pagination in the cached data, so we can post timelineItems asap. @@ -100,6 +115,12 @@ class RustMatrixTimeline( internal fun postPaginationStatus(status: BackPaginationStatus) { _paginationState.getAndUpdate { currentPaginationState -> + if (hasEncryptionHistoryBanner()) { + return@getAndUpdate currentPaginationState.copy( + isBackPaginating = false, + hasMoreToLoadBackwards = false, + ) + } when (status) { BackPaginationStatus.IDLE -> { currentPaginationState.copy( @@ -159,4 +180,10 @@ class RustMatrixTimeline( fun getItemById(eventId: EventId): MatrixTimelineItem.Event? { return _timelineItems.value.firstOrNull { (it as? MatrixTimelineItem.Event)?.eventId == eventId } as? MatrixTimelineItem.Event } + + private fun hasEncryptionHistoryBanner(): Boolean { + val firstItem = _timelineItems.value.firstOrNull() + return firstItem is MatrixTimelineItem.Virtual + && firstItem.virtual is VirtualTimelineItem.EncryptedHistoryBanner + } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TimelineEncryptedHistoryPostProcessor.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TimelineEncryptedHistoryPostProcessor.kt new file mode 100644 index 0000000000..ca5c7342f8 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TimelineEncryptedHistoryPostProcessor.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.matrix.impl.timeline.postprocessor + +import io.element.android.libraries.matrix.api.timeline.MatrixTimeline +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.getAndUpdate +import java.util.Date +import java.util.UUID + +class TimelineEncryptedHistoryPostProcessor( + private val lastLoginTimestamp: Date?, + private val isRoomEncrypted: Boolean, + private val paginationStateFlow: MutableStateFlow, +) { + + fun process(items: List): List { + if (!isRoomEncrypted || lastLoginTimestamp == null) return items + + val filteredItems = replaceWithEncryptionHistoryBannerIfNeeded(items) + // Disable back pagination + val wasFiltered = filteredItems !== items + if (wasFiltered) { + paginationStateFlow.getAndUpdate { + it.copy( + isBackPaginating = false, + hasMoreToLoadBackwards = false + ) + } + } + return filteredItems + } + + private fun replaceWithEncryptionHistoryBannerIfNeeded(list: List): List { + var lastEncryptedHistoryBannerIndex = -1 + for ((i, item) in list.withIndex()) { + if (isItemEncryptionHistory(item)) { + lastEncryptedHistoryBannerIndex = i + } + } + return if (lastEncryptedHistoryBannerIndex >= 0) { + val sublist = list.drop(lastEncryptedHistoryBannerIndex + 1).toMutableList() + sublist.add(0, MatrixTimelineItem.Virtual(0L, VirtualTimelineItem.EncryptedHistoryBanner)) + sublist + } else { + list + } + } + + private fun isItemEncryptionHistory(item: MatrixTimelineItem): Boolean { + if ((item as? MatrixTimelineItem.Virtual)?.virtual is VirtualTimelineItem.EncryptedHistoryBanner) { + return true + } + val timestamp = (item as? MatrixTimelineItem.Event)?.event?.timestamp ?: return false + return timestamp <= lastLoginTimestamp!!.time + } + +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TimelineEncryptedHistoryPostProcessorTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TimelineEncryptedHistoryPostProcessorTest.kt new file mode 100644 index 0000000000..91f0bc1883 --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/timeline/postprocessor/TimelineEncryptedHistoryPostProcessorTest.kt @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.matrix.impl.timeline.postprocessor + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.api.timeline.MatrixTimeline +import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem +import io.element.android.libraries.matrix.api.timeline.item.virtual.VirtualTimelineItem +import io.element.android.libraries.matrix.test.room.anEventTimelineItem +import kotlinx.coroutines.flow.MutableStateFlow +import org.junit.Test +import java.util.Date + +class TimelineEncryptedHistoryPostProcessorTest { + + private val defaultLastLoginTimestamp = Date(1689061264L) + + @Test + fun `given an unencrypted room, nothing is done`() { + val processor = createPostProcessor(isRoomEncrypted = false) + val items = listOf( + MatrixTimelineItem.Event(0L, anEventTimelineItem()) + ) + assertThat(processor.process(items)).isSameInstanceAs(items) + } + + @Test + fun `given a null lastLoginTimestamp, nothing is done`() { + val processor = createPostProcessor(lastLoginTimestamp = null) + val items = listOf( + MatrixTimelineItem.Event(0L, anEventTimelineItem()) + ) + assertThat(processor.process(items)).isSameInstanceAs(items) + } + + @Test + fun `given an empty list, nothing is done`() { + val processor = createPostProcessor() + val items = emptyList() + assertThat(processor.process(items)).isSameInstanceAs(items) + } + + @Test + fun `given a list with no items before lastLoginTimestamp, nothing is done`() { + val processor = createPostProcessor() + val items = listOf( + MatrixTimelineItem.Event(0L, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1)) + ) + assertThat(processor.process(items)).isSameInstanceAs(items) + } + + @Test + fun `given a list with an item with equal timestamp as lastLoginTimestamp, it's replaced`() { + val processor = createPostProcessor() + val items = listOf( + MatrixTimelineItem.Event(0L, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time)) + ) + assertThat(processor.process(items)) + .isEqualTo(listOf(MatrixTimelineItem.Virtual(0L, VirtualTimelineItem.EncryptedHistoryBanner))) + } + + @Test + fun `given a list with an item with a lower timestamp than lastLoginTimestamp, it's replaced`() { + val processor = createPostProcessor() + val items = listOf( + MatrixTimelineItem.Event(0L, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time - 1)) + ) + assertThat(processor.process(items)).isEqualTo( + listOf(MatrixTimelineItem.Virtual(0L, VirtualTimelineItem.EncryptedHistoryBanner)) + ) + } + + @Test + fun `given a list with several with lower or equal timestamps than lastLoginTimestamp, they're replaced and the user can't back paginate`() { + val paginationStateFlow = MutableStateFlow(MatrixTimeline.PaginationState(hasMoreToLoadBackwards = true, isBackPaginating = false)) + val processor = createPostProcessor(paginationStateFlow = paginationStateFlow) + val items = listOf( + MatrixTimelineItem.Event(0L, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time - 1)), + MatrixTimelineItem.Event(0L, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time)), + MatrixTimelineItem.Event(0L, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1)), + ) + assertThat(processor.process(items)).isEqualTo( + listOf( + MatrixTimelineItem.Virtual(0L, VirtualTimelineItem.EncryptedHistoryBanner), + MatrixTimelineItem.Event(0L, anEventTimelineItem(timestamp = defaultLastLoginTimestamp.time + 1)) + ) + ) + assertThat(paginationStateFlow.value).isEqualTo(MatrixTimeline.PaginationState(hasMoreToLoadBackwards = false, isBackPaginating = false)) + } + + private fun createPostProcessor( + lastLoginTimestamp: Date? = defaultLastLoginTimestamp, + isRoomEncrypted: Boolean = true, + paginationStateFlow: MutableStateFlow = + MutableStateFlow(MatrixTimeline.PaginationState(hasMoreToLoadBackwards = true, isBackPaginating = false)) + ) = TimelineEncryptedHistoryPostProcessor( + lastLoginTimestamp = lastLoginTimestamp, + isRoomEncrypted = isRoomEncrypted, + paginationStateFlow = paginationStateFlow, + ) +} diff --git a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt index f2eb5847f7..cc106f960a 100644 --- a/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt +++ b/libraries/session-storage/api/src/main/kotlin/io/element/android/libraries/sessionstorage/api/SessionData.kt @@ -16,11 +16,14 @@ package io.element.android.libraries.sessionstorage.api +import java.util.Date + data class SessionData( val userId: String, val deviceId: String, val accessToken: String, val refreshToken: String?, val homeserverUrl: String, - val slidingSyncProxy: String? + val slidingSyncProxy: String?, + val loginTimestamp: Date?, ) diff --git a/libraries/session-storage/impl/build.gradle.kts b/libraries/session-storage/impl/build.gradle.kts index cd42a18402..698bfcf230 100644 --- a/libraries/session-storage/impl/build.gradle.kts +++ b/libraries/session-storage/impl/build.gradle.kts @@ -48,5 +48,7 @@ dependencies { } sqldelight { - database("SessionDatabase") {} + database("SessionDatabase") { + verifyMigrations = true + } } diff --git a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt index fd8a42ad6f..dbb42a8451 100644 --- a/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt +++ b/libraries/session-storage/impl/src/main/kotlin/io/element/android/libraries/sessionstorage/impl/SessionDataMapper.kt @@ -17,19 +17,22 @@ package io.element.android.libraries.sessionstorage.impl import io.element.android.libraries.sessionstorage.api.SessionData +import java.util.Date +import io.element.android.libraries.matrix.session.SessionData as DbSessionData -internal fun SessionData.toDbModel(): io.element.android.libraries.matrix.session.SessionData { - return io.element.android.libraries.matrix.session.SessionData( +internal fun SessionData.toDbModel(): DbSessionData { + return DbSessionData( userId = userId, deviceId = deviceId, accessToken = accessToken, refreshToken = refreshToken, homeserverUrl = homeserverUrl, slidingSyncProxy = slidingSyncProxy, + loginTimestamp = loginTimestamp?.time, ) } -internal fun io.element.android.libraries.matrix.session.SessionData.toApiModel(): SessionData { +internal fun DbSessionData.toApiModel(): SessionData { return SessionData( userId = userId, deviceId = deviceId, @@ -37,5 +40,6 @@ internal fun io.element.android.libraries.matrix.session.SessionData.toApiModel( refreshToken = refreshToken, homeserverUrl = homeserverUrl, slidingSyncProxy = slidingSyncProxy, + loginTimestamp = loginTimestamp?.let { Date(it) } ) } diff --git a/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq b/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq index ea8471a36a..c3123f2ffb 100644 --- a/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq +++ b/libraries/session-storage/impl/src/main/sqldelight/io/element/android/libraries/matrix/session/SessionData.sq @@ -4,9 +4,11 @@ CREATE TABLE SessionData ( accessToken TEXT NOT NULL, refreshToken TEXT, homeserverUrl TEXT NOT NULL, - slidingSyncProxy TEXT + slidingSyncProxy TEXT, + loginTimestamp INTEGER ); + selectFirst: SELECT * FROM SessionData LIMIT 1; @@ -17,7 +19,7 @@ selectByUserId: SELECT * FROM SessionData WHERE userId = ?; insertSessionData: -INSERT INTO SessionData(userId, deviceId, accessToken, refreshToken, homeserverUrl, slidingSyncProxy) VALUES ?; +INSERT INTO SessionData VALUES ?; removeSession: DELETE FROM SessionData WHERE userId = ?; diff --git a/libraries/session-storage/impl/src/main/sqldelight/migrations/0.sqm b/libraries/session-storage/impl/src/main/sqldelight/migrations/0.sqm new file mode 100644 index 0000000000..396a8f28dd --- /dev/null +++ b/libraries/session-storage/impl/src/main/sqldelight/migrations/0.sqm @@ -0,0 +1,8 @@ +CREATE TABLE SessionData ( + userId TEXT NOT NULL PRIMARY KEY, + deviceId TEXT NOT NULL, + accessToken TEXT NOT NULL, + refreshToken TEXT, + homeserverUrl TEXT NOT NULL, + slidingSyncProxy TEXT +); diff --git a/libraries/session-storage/impl/src/main/sqldelight/migrations/1.sqm b/libraries/session-storage/impl/src/main/sqldelight/migrations/1.sqm new file mode 100644 index 0000000000..3ee7762585 --- /dev/null +++ b/libraries/session-storage/impl/src/main/sqldelight/migrations/1.sqm @@ -0,0 +1 @@ +ALTER TABLE SessionData ADD COLUMN loginTimestamp INTEGER; diff --git a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt index 28b9dfba50..fc24c5a011 100644 --- a/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt +++ b/libraries/session-storage/impl/src/test/kotlin/io/element/android/libraries/sessionstorage/impl/DatabaseSessionStoreTests.kt @@ -35,7 +35,8 @@ class DatabaseSessionStoreTests { accessToken = "accessToken", refreshToken = "refreshToken", homeserverUrl = "homeserverUrl", - slidingSyncProxy = null + slidingSyncProxy = null, + loginTimestamp = null, ) @Before diff --git a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt index d702c797a8..d832a6168d 100644 --- a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt +++ b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt @@ -42,6 +42,11 @@ object TestTags { * Room list / Home screen. */ val homeScreenSettings = TestTag("home_screen-settings") + + /** + * Welcome screen. + */ + val welcomeScreenTitle = TestTag("welcome_screen-title") } diff --git a/libraries/ui-strings/src/main/res/values-de/translations.xml b/libraries/ui-strings/src/main/res/values-de/translations.xml index 768097f37c..10694181da 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -176,12 +176,6 @@ "In OpenStreetMap öffnen" "Diesen Ort teilen" "Standort" - "Anrufe, Standortfreigabe, Suche und mehr werden später in diesem Jahr hinzugefügt." - "Der Nachrichtenverlauf für verschlüsselte Räume wird in diesem Update nicht verfügbar sein." - "Wir würden uns freuen, wenn du uns über die Einstellungsseite deine Meinung mitteilst." - "Los geht\'s!" - "Folgendes musst du wissen:" - "Willkommen bei %1$s!" "Rageshake" "Erkennungsschwelle" "Allgemein" diff --git a/libraries/ui-strings/src/main/res/values-fr/translations.xml b/libraries/ui-strings/src/main/res/values-fr/translations.xml index 2ff0ae3914..e3fe11dfdc 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -178,11 +178,6 @@ "Ouvrir dans Google Maps" "Ouvrir dans OpenStreetMap" "Partager cette position" - "L’historique des messages pour les salons chiffrés ne sera pas disponible dans cette mise à jour." - "Nous serions ravis d’avoir votre avis, n’hésitez pas à nous le partager via la page des paramètres." - "C’est parti !" - "Voici ce qu’il faut savoir :" - "Bienvenue sur %1$s !" "Rageshake" "Seuil de détection" "Général" diff --git a/libraries/ui-strings/src/main/res/values-sk/translations.xml b/libraries/ui-strings/src/main/res/values-sk/translations.xml index d75afca6a4..336ae9144c 100644 --- a/libraries/ui-strings/src/main/res/values-sk/translations.xml +++ b/libraries/ui-strings/src/main/res/values-sk/translations.xml @@ -183,12 +183,6 @@ "Otvoriť v OpenStreetMap" "Zdieľajte túto polohu" "Poloha" - "Hovory, zdieľanie polohy, vyhľadávanie a ďalšie funkcie pribudnú neskôr v tomto roku." - "História správ pre zašifrované miestnosti nebude v tejto aktualizácii k dispozícii." - "Radi by sme od vás počuli, dajte nám vedieť, čo si myslíte, prostredníctvom stránky nastavení." - "Poďme na to!" - "Tu je to, čo potrebujete vedieť:" - "Vitajte v %1$s!" "Zúrivé potrasenie" "Prahová hodnota detekcie" "Všeobecné" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 94dceaaa7e..c73284e700 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -182,12 +182,6 @@ "Open in OpenStreetMap" "Share this location" "Location" - "Calls, location sharing, search and more will be added later this year." - "Message history for encrypted rooms won’t be available in this update." - "We’d love to hear from you, let us know what you think via the settings page." - "Let\'s go!" - "Here’s what you need to know:" - "Welcome to %1$s!" "Rageshake" "Detection threshold" "General" diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.ftue.impl.welcome_null_DefaultGroup_WelcomeViewPreview-D-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.ftue.impl.welcome_null_DefaultGroup_WelcomeViewPreview-D-0_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d0e4757049 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.ftue.impl.welcome_null_DefaultGroup_WelcomeViewPreview-D-0_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:76a68f2fc93894d6f9d9caea02546766c55664c0e53ba9506c6c32df058f5823 +size 303608 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.ftue.impl.welcome_null_DefaultGroup_WelcomeViewPreview-N-0_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.ftue.impl.welcome_null_DefaultGroup_WelcomeViewPreview-N-0_2_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..68ee3f3801 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.ftue.impl.welcome_null_DefaultGroup_WelcomeViewPreview-N-0_2_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f24bb3e40dd8c02037bd9d4523726ec0a0b1a283d23a8ca143973b0e9ee673c6 +size 408318 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-7_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-7_8_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..45593d6af2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-7_8_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c806ee0293d94ab1c882b0f0ceaac2c7ac69d587f47cc22b4d16bc331351a2e +size 14711 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-7_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-7_9_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c74bbe95f8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-7_9_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77c97c6c99943858530219234356459a7f0a88774332a4f11cd738b92ee15c91 +size 14200 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomPreview-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomPreview-D_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d903b752b0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomPreview-D_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2947531c19a0ac9a7e35c3f2a394f6eb805427e1ad296d22b7d8b5cbb2428e07 +size 20947 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomPreview-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomPreview-N_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..bd9fca6c0f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomPreview-N_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6f88eb992060d5b41ce3200bdc48d4fe6accaeda857d1ca08cb65ed8235798f7 +size 20266 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_InfoListItemMoleculePreview-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_InfoListItemMoleculePreview-D_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..15308b30bc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_InfoListItemMoleculePreview-D_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51616fee6314d06981ce18d654c166d8e941be3264578c89e479a2a1267caa65 +size 19226 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_InfoListItemMoleculePreview-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_InfoListItemMoleculePreview-N_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b3af060ee1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_InfoListItemMoleculePreview-N_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a7f455414ed06ec16785049bc3e99fa312a89599d24bcda0dc611c390e10c73 +size 18734 diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 3feadf7a7a..fce6b317b5 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -113,6 +113,12 @@ "includeRegex": [ "screen_analytics_prompt.*" ] + }, + { + "name": ":features:ftue:impl", + "includeRegex": [ + "screen_welcome_.*" + ] } ] } From 2b679710d21c893f9ba3225e7a32b5cdaeb32ba0 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 17 Jul 2023 18:34:50 +0200 Subject: [PATCH 25/59] Rework a bit MatrixClientHolder and reintroduce cacheIndex... --- .../io/element/android/appnav/RootFlowNode.kt | 66 ++++------- .../android/appnav/di/MatrixClientsHolder.kt | 83 ++++++++++++++ .../android/appnav/root/RootNavState.kt | 22 ++++ .../appnav/root/RootNavStateFlowFactory.kt | 90 +++++++++++++++ .../impl/tasks/ClearCacheUseCase.kt | 1 - .../matrix/api/MatrixClientProvider.kt | 28 +++++ .../matrix/ui/di/MatrixClientsHolder.kt | 106 ------------------ .../DefaultNotificationDrawerManager.kt | 9 +- .../notifications/NotifiableEventResolver.kt | 22 ++-- 9 files changed, 255 insertions(+), 172 deletions(-) create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/root/RootNavState.kt create mode 100644 appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClientProvider.kt delete mode 100644 libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/di/MatrixClientsHolder.kt diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index adc1c3384d..089e956c61 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -37,15 +37,14 @@ import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.libraries.matrix.ui.di.MatrixClientsHolder +import io.element.android.appnav.di.MatrixClientsHolder import io.element.android.appnav.intent.IntentResolver import io.element.android.appnav.intent.ResolvedIntent +import io.element.android.appnav.root.RootNavStateFlowFactory import io.element.android.appnav.root.RootPresenter import io.element.android.appnav.root.RootView -import io.element.android.features.login.api.LoginUserStory import io.element.android.features.login.api.oidc.OidcAction import io.element.android.features.login.api.oidc.OidcActionFlow -import io.element.android.features.preferences.api.CacheService import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler @@ -57,29 +56,22 @@ import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.core.SessionId import kotlinx.coroutines.flow.distinctUntilChanged - -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.onStart import kotlinx.parcelize.Parcelize import timber.log.Timber -import java.util.UUID @ContributesNode(AppScope::class) class RootFlowNode @AssistedInject constructor( @Assisted val buildContext: BuildContext, @Assisted plugins: List, private val authenticationService: MatrixAuthenticationService, - private val cacheService: CacheService, + private val navStateFlowFactory: RootNavStateFlowFactory, private val matrixClientsHolder: MatrixClientsHolder, private val presenter: RootPresenter, private val bugReportEntryPoint: BugReportEntryPoint, private val intentResolver: IntentResolver, private val oidcActionFlow: OidcActionFlow, - private val loginUserStory: LoginUserStory, ) : BackstackNode( backstack = BackStack( @@ -91,26 +83,25 @@ class RootFlowNode @AssistedInject constructor( ) { override fun onBuilt() { - matrixClientsHolder.restore(buildContext.savedStateMap) + matrixClientsHolder.restoreWithSavedState(buildContext.savedStateMap) super.onBuilt() - observeLoggedInState() + observeNavState() } override fun onSaveInstanceState(state: MutableSavedStateMap) { super.onSaveInstanceState(state) - matrixClientsHolder.save(state) + matrixClientsHolder.saveIntoSavedState(state) + navStateFlowFactory.saveIntoSavedState(state) } - private fun observeLoggedInState() { - combine( - cacheService.onClearedCacheEventFlow(), - isUserLoggedInFlow(), - ) { _, isLoggedIn -> isLoggedIn } - .onEach { isLoggedIn -> - Timber.v("isLoggedIn=$isLoggedIn") - if (isLoggedIn) { + private fun observeNavState() { + navStateFlowFactory.create(buildContext.savedStateMap) + .distinctUntilChanged() + .onEach { navState -> + Timber.v("navState=$navState") + if (navState.isLoggedIn) { tryToRestoreLatestSession( - onSuccess = { switchToLoggedInFlow(it) }, + onSuccess = { sessionId -> switchToLoggedInFlow(sessionId, navState.cacheIndex) }, onFailure = { switchToNotLoggedInFlow() } ) } else { @@ -120,19 +111,8 @@ class RootFlowNode @AssistedInject constructor( .launchIn(lifecycleScope) } - - private fun switchToLoggedInFlow(sessionId: SessionId) { - backstack.safeRoot(NavTarget.LoggedInFlow(sessionId)) - } - - private fun isUserLoggedInFlow(): Flow { - return combine( - authenticationService.isLoggedIn(), - loginUserStory.loginFlowIsDone - ) { isLoggedIn, loginFlowIsDone -> - isLoggedIn && loginFlowIsDone - } - .distinctUntilChanged() + private fun switchToLoggedInFlow(sessionId: SessionId, navId: Int) { + backstack.safeRoot(NavTarget.LoggedInFlow(sessionId, navId)) } private fun switchToNotLoggedInFlow() { @@ -145,9 +125,7 @@ class RootFlowNode @AssistedInject constructor( onFailure: () -> Unit = {}, onSuccess: (SessionId) -> Unit = {}, ) { - runCatching { - matrixClientsHolder.requireSession(sessionId) - } + matrixClientsHolder.getOrRestore(sessionId) .onSuccess { Timber.v("Succeed to restore session $sessionId") onSuccess(sessionId) @@ -200,7 +178,7 @@ class RootFlowNode @AssistedInject constructor( @Parcelize data class LoggedInFlow( val sessionId: SessionId, - val navId: UUID = UUID.randomUUID(), + val navId: Int ) : NavTarget @Parcelize @@ -274,11 +252,5 @@ class RootFlowNode @AssistedInject constructor( navTarget is NavTarget.LoggedInFlow && navTarget.sessionId == sessionId } } - - private fun CacheService.onClearedCacheEventFlow(): Flow { - return clearedCacheEventFlow - .onEach { sessionId -> matrixClientsHolder.remove(sessionId) } - .map { } - .onStart { emit((Unit)) } - } } + diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt b/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt index 53a49aef9f..ac0eb60763 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt @@ -16,3 +16,86 @@ package io.element.android.appnav.di +import com.bumble.appyx.core.state.MutableSavedStateMap +import com.bumble.appyx.core.state.SavedStateMap +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.MatrixClientProvider +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject + +private const val SAVE_INSTANCE_KEY = "io.element.android.x.di.MatrixClientsHolder.SaveInstanceKey" + +@SingleIn(AppScope::class) +@ContributesBinding(AppScope::class) +class MatrixClientsHolder @Inject constructor(private val authenticationService: MatrixAuthenticationService): MatrixClientProvider { + + private val sessionIdsToMatrixClient = ConcurrentHashMap() + private val restoreMutex = Mutex() + + fun removeAll() { + sessionIdsToMatrixClient.clear() + } + + fun remove(sessionId: SessionId) { + sessionIdsToMatrixClient.remove(sessionId) + } + + fun isEmpty(): Boolean = sessionIdsToMatrixClient.isEmpty() + + fun getOrNull(sessionId: SessionId): MatrixClient? { + return sessionIdsToMatrixClient[sessionId] + } + + override suspend fun getOrRestore(sessionId: SessionId): Result { + return restoreMutex.withLock { + when (val matrixClient = sessionIdsToMatrixClient[sessionId]) { + null -> restore(sessionId) + else -> Result.success(matrixClient) + } + } + } + + @Suppress("UNCHECKED_CAST") + fun restoreWithSavedState(state: SavedStateMap?) { + Timber.d("Restore state") + if (state == null || sessionIdsToMatrixClient.isNotEmpty()) return Unit.also { + Timber.w("Restore with non-empty map") + } + val sessionIds = state[SAVE_INSTANCE_KEY] as? Array + Timber.d("Restore matrix session keys = ${sessionIds?.map { it.value }}") + if (sessionIds.isNullOrEmpty()) return + // Not ideal but should only happens in case of process recreation. This ensure we restore all the active sessions before restoring the node graphs. + runBlocking { + sessionIds.forEach { sessionId -> + restore(sessionId) + } + } + } + + fun saveIntoSavedState(state: MutableSavedStateMap) { + val sessionKeys = sessionIdsToMatrixClient.keys.toTypedArray() + Timber.d("Save matrix session keys = ${sessionKeys.map { it.value }}") + state[SAVE_INSTANCE_KEY] = sessionKeys + } + + private suspend fun restore(sessionId: SessionId): Result { + Timber.d("Restore matrix session: $sessionId") + return authenticationService.restoreSession(sessionId) + .onSuccess { matrixClient -> + sessionIdsToMatrixClient[matrixClient.sessionId] = matrixClient + } + .onFailure { + Timber.e("Fail to restore session") + } + } + +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavState.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavState.kt new file mode 100644 index 0000000000..55edf96aee --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavState.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.appnav.root + +data class RootNavState( + val cacheIndex: Int, + val isLoggedIn: Boolean +) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt new file mode 100644 index 0000000000..84f50a835e --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.appnav.root + +import com.bumble.appyx.core.state.MutableSavedStateMap +import com.bumble.appyx.core.state.SavedStateMap +import io.element.android.appnav.di.MatrixClientsHolder +import io.element.android.features.login.api.LoginUserStory +import io.element.android.features.preferences.api.CacheService +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onEach +import javax.inject.Inject + +private const val SAVE_INSTANCE_KEY = "io.element.android.x.RootNavStateFlowFactory.SAVE_INSTANCE_KEY" + +class RootNavStateFlowFactory @Inject constructor( + private val authenticationService: MatrixAuthenticationService, + private val cacheService: CacheService, + private val matrixClientsHolder: MatrixClientsHolder, + private val loginUserStory: LoginUserStory, +) { + + private var currentCacheIndex = 0 + + fun create(savedStateMap: SavedStateMap?): Flow { + /** + * A flow of integer, where each time a clear cache is done, we have a new incremented value. + */ + val initialCacheIndex = savedStateMap.getCacheIndexOrDefault() + val cacheIndexFlow = cacheService.clearedCacheEventFlow + .onEach { sessionId -> + matrixClientsHolder.remove(sessionId) + } + .toIndexFlow(initialCacheIndex) + .onEach { cacheIndex -> + currentCacheIndex = cacheIndex + } + + return combine( + cacheIndexFlow, + isUserLoggedInFlow(), + ) { navId, isLoggedIn -> + RootNavState(navId, isLoggedIn) + } + } + + fun saveIntoSavedState(stateMap: MutableSavedStateMap) { + stateMap[SAVE_INSTANCE_KEY] = currentCacheIndex + } + + private fun isUserLoggedInFlow(): Flow { + return combine( + authenticationService.isLoggedIn(), + loginUserStory.loginFlowIsDone + ) { isLoggedIn, loginFlowIsDone -> + isLoggedIn && loginFlowIsDone + } + .distinctUntilChanged() + } + + private fun Flow.toIndexFlow(initialValue: Int): Flow = flow { + var index = initialValue + emit(initialValue) + collect { + emit(++index) + } + } + + private fun SavedStateMap?.getCacheIndexOrDefault(): Int { + return this?.get(SAVE_INSTANCE_KEY) as? Int ?: 0 + } +} diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt index 0ef0e02558..24c67a3824 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt @@ -27,7 +27,6 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.MatrixClient -import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import javax.inject.Inject diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClientProvider.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClientProvider.kt new file mode 100644 index 0000000000..44d1a1d1a6 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClientProvider.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 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 + * + * http://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.matrix.api + +import io.element.android.libraries.matrix.api.core.SessionId + +interface MatrixClientProvider { + /** + * Can be used to get or restore a MatrixClient with the given [SessionId]. + * If a [MatrixClient] is already in memory, it'll return it. Otherwise it'll try to restore one. + * Most of the time you want to use injected constructor instead of retrieving a MatrixClient with this provider. + */ + suspend fun getOrRestore(sessionId: SessionId): Result +} diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/di/MatrixClientsHolder.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/di/MatrixClientsHolder.kt deleted file mode 100644 index a6db7a1c9e..0000000000 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/di/MatrixClientsHolder.kt +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (c) 2023 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 - * - * http://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.matrix.ui.di - -import com.bumble.appyx.core.state.MutableSavedStateMap -import com.bumble.appyx.core.state.SavedStateMap -import io.element.android.libraries.di.AppScope -import io.element.android.libraries.di.SingleIn -import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.auth.AuthenticationException -import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import io.element.android.libraries.matrix.api.core.SessionId -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import timber.log.Timber -import java.util.concurrent.ConcurrentHashMap -import javax.inject.Inject -import kotlin.jvm.Throws - -private const val SAVE_INSTANCE_KEY = "io.element.android.x.di.MatrixClientsHolder.SaveInstanceKey" - -@SingleIn(AppScope::class) -class MatrixClientsHolder @Inject constructor(private val authenticationService: MatrixAuthenticationService) { - - private val sessionIdsToMatrixClient = ConcurrentHashMap() - private val restoreMutex = Mutex() - - private fun add(matrixClient: MatrixClient) { - sessionIdsToMatrixClient[matrixClient.sessionId] = matrixClient - } - - fun removeAll() { - sessionIdsToMatrixClient.clear() - } - - fun remove(sessionId: SessionId) { - sessionIdsToMatrixClient.remove(sessionId) - } - - fun isEmpty(): Boolean = sessionIdsToMatrixClient.isEmpty() - - fun knowSession(sessionId: SessionId): Boolean = sessionIdsToMatrixClient.containsKey(sessionId) - - fun getOrNull(sessionId: SessionId): MatrixClient? { - return sessionIdsToMatrixClient[sessionId] - } - - @Throws(AuthenticationException::class) - suspend fun requireSession(sessionId: SessionId): MatrixClient { - return restoreMutex.withLock { - when (val matrixClient = sessionIdsToMatrixClient[sessionId]) { - null -> restore(sessionId).getOrThrow() - else -> matrixClient - } - } - } - - private suspend fun restore(sessionId: SessionId): Result { - Timber.d("Restore matrix session: $sessionId") - return authenticationService.restoreSession(sessionId) - .onSuccess { matrixClient -> - add(matrixClient) - } - .onFailure { - Timber.e("Fail to restore session") - } - } - - @Suppress("UNCHECKED_CAST") - fun restore(state: SavedStateMap?) { - Timber.d("Restore state") - if (state == null || sessionIdsToMatrixClient.isNotEmpty()) return Unit.also { - Timber.w("Restore with non-empty map") - } - val sessionIds = state[SAVE_INSTANCE_KEY] as? Array - Timber.d("Restore matrix session keys = ${sessionIds?.map { it.value }}") - if (sessionIds.isNullOrEmpty()) return - // Not ideal but should only happens in case of process recreation. This ensure we restore all the active sessions before restoring the node graphs. - runBlocking { - sessionIds.forEach { sessionId -> - restore(sessionId) - } - } - } - - fun save(state: MutableSavedStateMap) { - val sessionKeys = sessionIdsToMatrixClient.keys.toTypedArray() - Timber.d("Save matrix session keys = ${sessionKeys.map { it.value }}") - state[SAVE_INSTANCE_KEY] = sessionKeys - } -} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt index dc484c29a3..8851b8eea8 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/DefaultNotificationDrawerManager.kt @@ -16,7 +16,6 @@ package io.element.android.libraries.push.impl.notifications -import io.element.android.libraries.matrix.ui.di.MatrixClientsHolder import io.element.android.libraries.androidutils.throttler.FirstThrottler import io.element.android.libraries.core.cache.CircularCache import io.element.android.libraries.core.coroutine.CoroutineDispatchers @@ -24,12 +23,12 @@ import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn -import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.ThreadId import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.push.api.notifications.NotificationDrawerManager import io.element.android.libraries.push.api.store.PushDataStore import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent @@ -60,8 +59,7 @@ class DefaultNotificationDrawerManager @Inject constructor( private val coroutineScope: CoroutineScope, private val dispatchers: CoroutineDispatchers, private val buildMeta: BuildMeta, - private val matrixAuthenticationService: MatrixAuthenticationService, - private val matrixClientsHolder: MatrixClientsHolder, + private val matrixClientProvider: MatrixClientProvider, ) : NotificationDrawerManager { /** * Lazily initializes the NotificationState as we rely on having a current session in order to fetch the persisted queue of events. @@ -257,8 +255,7 @@ class DefaultNotificationDrawerManager @Inject constructor( val currentUser = tryOrNull( onError = { Timber.e(it, "Unable to retrieve info for user ${sessionId.value}") }, operation = { - val client = matrixClientsHolder.requireSession(sessionId) - + val client = matrixClientProvider.getOrRestore(sessionId).getOrThrow() // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash val myUserDisplayName = client.loadUserDisplayName().getOrNull() ?: sessionId.value val userAvatarUrl = client.loadUserAvatarURLString().getOrNull() diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt index ad55bddb54..dbdddf1ac9 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotifiableEventResolver.kt @@ -16,10 +16,8 @@ package io.element.android.libraries.push.impl.notifications -import io.element.android.libraries.matrix.ui.di.MatrixClientsHolder import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.meta.BuildMeta -import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId @@ -36,6 +34,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.NoticeMessage import io.element.android.libraries.matrix.api.timeline.item.event.TextMessageType import io.element.android.libraries.matrix.api.timeline.item.event.UnknownMessageType import io.element.android.libraries.matrix.api.timeline.item.event.VideoMessageType +import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.log.pushLoggerTag import io.element.android.libraries.push.impl.notifications.model.FallbackNotifiableEvent @@ -60,26 +59,25 @@ class NotifiableEventResolver @Inject constructor( private val stringProvider: StringProvider, // private val noticeEventFormatter: NoticeEventFormatter, // private val displayableEventFormatter: DisplayableEventFormatter, - private val matrixAuthenticationService: MatrixAuthenticationService, private val buildMeta: BuildMeta, private val clock: SystemClock, - private val matrixClientsHolder: MatrixClientsHolder, + private val matrixClientProvider: MatrixClientProvider, ) { suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? { // Restore session - val client = matrixClientsHolder.requireSession(sessionId) + val client = matrixClientProvider.getOrRestore(sessionId).getOrNull() ?: return null val notificationService = client.notificationService() val notificationData = notificationService.getNotification( - userId = sessionId, - roomId = roomId, - eventId = eventId, + userId = sessionId, + roomId = roomId, + eventId = eventId, // FIXME should be true in the future, but right now it's broken // (https://github.com/vector-im/element-x-android/issues/640#issuecomment-1612913658) - filterByPushRules = false, - ).onFailure { - Timber.tag(loggerTag.value).e(it, "Unable to resolve event: $eventId.") - }.getOrNull() + filterByPushRules = false, + ).onFailure { + Timber.tag(loggerTag.value).e(it, "Unable to resolve event: $eventId.") + }.getOrNull() // TODO this notificationData is not always valid at the moment, sometimes the Rust SDK can't fetch the matching event return notificationData?.asNotifiableEvent(sessionId) From 07ab919367849a0fccaacf3bb33e8c9321cd59e3 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 17 Jul 2023 21:32:07 +0200 Subject: [PATCH 26/59] MatrixClientHolders: some more cleanup --- .../android/appnav/di/MatrixClientsHolder.kt | 9 ++--- .../android/appnav/root/RootNavState.kt | 10 +++++ .../appnav/root/RootNavStateFlowFactory.kt | 39 ++++++++++++------- 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt b/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt index ac0eb60763..3e36e7d692 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/di/MatrixClientsHolder.kt @@ -22,9 +22,9 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.MatrixClientProvider import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.core.SessionId -import io.element.android.libraries.matrix.api.MatrixClientProvider import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -36,7 +36,7 @@ private const val SAVE_INSTANCE_KEY = "io.element.android.x.di.MatrixClientsHold @SingleIn(AppScope::class) @ContributesBinding(AppScope::class) -class MatrixClientsHolder @Inject constructor(private val authenticationService: MatrixAuthenticationService): MatrixClientProvider { +class MatrixClientsHolder @Inject constructor(private val authenticationService: MatrixAuthenticationService) : MatrixClientProvider { private val sessionIdsToMatrixClient = ConcurrentHashMap() private val restoreMutex = Mutex() @@ -49,15 +49,13 @@ class MatrixClientsHolder @Inject constructor(private val authenticationService: sessionIdsToMatrixClient.remove(sessionId) } - fun isEmpty(): Boolean = sessionIdsToMatrixClient.isEmpty() - fun getOrNull(sessionId: SessionId): MatrixClient? { return sessionIdsToMatrixClient[sessionId] } override suspend fun getOrRestore(sessionId: SessionId): Result { return restoreMutex.withLock { - when (val matrixClient = sessionIdsToMatrixClient[sessionId]) { + when (val matrixClient = getOrNull(sessionId)) { null -> restore(sessionId) else -> Result.success(matrixClient) } @@ -97,5 +95,4 @@ class MatrixClientsHolder @Inject constructor(private val authenticationService: Timber.e("Fail to restore session") } } - } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavState.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavState.kt index 55edf96aee..ed3ac15972 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavState.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavState.kt @@ -16,7 +16,17 @@ package io.element.android.appnav.root +/** + * [RootNavState] produced by [RootNavStateFlowFactory]. + */ data class RootNavState( + /** + * This value is incremented when a clear cache is done. + * Can be useful to track to force ui state to re-render + */ val cacheIndex: Int, + /** + * true if we are currently loggedIn. + */ val isLoggedIn: Boolean ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt b/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt index 84f50a835e..0e8d93b0c9 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/root/RootNavStateFlowFactory.kt @@ -31,6 +31,10 @@ import javax.inject.Inject private const val SAVE_INSTANCE_KEY = "io.element.android.x.RootNavStateFlowFactory.SAVE_INSTANCE_KEY" +/** + * This class is responsible for creating a flow of [RootNavState]. + * It gathers data from multiple datasource and creates a unique one. + */ class RootNavStateFlowFactory @Inject constructor( private val authenticationService: MatrixAuthenticationService, private val cacheService: CacheService, @@ -41,11 +45,24 @@ class RootNavStateFlowFactory @Inject constructor( private var currentCacheIndex = 0 fun create(savedStateMap: SavedStateMap?): Flow { - /** - * A flow of integer, where each time a clear cache is done, we have a new incremented value. - */ + return combine( + cacheIndexFlow(savedStateMap), + isUserLoggedInFlow(), + ) { cacheIndex, isLoggedIn -> + RootNavState(cacheIndex = cacheIndex, isLoggedIn = isLoggedIn) + } + } + + fun saveIntoSavedState(stateMap: MutableSavedStateMap) { + stateMap[SAVE_INSTANCE_KEY] = currentCacheIndex + } + + /** + * @return a flow of integer, where each time a clear cache is done, we have a new incremented value. + */ + private fun cacheIndexFlow(savedStateMap: SavedStateMap?): Flow { val initialCacheIndex = savedStateMap.getCacheIndexOrDefault() - val cacheIndexFlow = cacheService.clearedCacheEventFlow + return cacheService.clearedCacheEventFlow .onEach { sessionId -> matrixClientsHolder.remove(sessionId) } @@ -53,17 +70,6 @@ class RootNavStateFlowFactory @Inject constructor( .onEach { cacheIndex -> currentCacheIndex = cacheIndex } - - return combine( - cacheIndexFlow, - isUserLoggedInFlow(), - ) { navId, isLoggedIn -> - RootNavState(navId, isLoggedIn) - } - } - - fun saveIntoSavedState(stateMap: MutableSavedStateMap) { - stateMap[SAVE_INSTANCE_KEY] = currentCacheIndex } private fun isUserLoggedInFlow(): Flow { @@ -76,6 +82,9 @@ class RootNavStateFlowFactory @Inject constructor( .distinctUntilChanged() } + /** + * @return a flow of integer that increments the value by one each time a new element is emitted upstream. + */ private fun Flow.toIndexFlow(initialValue: Int): Flow = flow { var index = initialValue emit(initialValue) From 48277d095ab432d5d828bdfeaa0d0ece7b7c28a2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 17 Jul 2023 22:03:21 +0200 Subject: [PATCH 27/59] Change return type (mostly for clarity) --- .../libraries/matrix/impl/auth/AuthenticationException.kt | 2 +- .../android/libraries/matrix/impl/exception/ClientException.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt index 26a9fba38f..f0feb2857d 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/AuthenticationException.kt @@ -19,7 +19,7 @@ package io.element.android.libraries.matrix.impl.auth import io.element.android.libraries.matrix.api.auth.AuthenticationException import org.matrix.rustcomponents.sdk.AuthenticationException as RustAuthenticationException -fun Throwable.mapAuthenticationException(): Throwable { +fun Throwable.mapAuthenticationException(): AuthenticationException { return when (this) { is RustAuthenticationException.ClientMissing -> AuthenticationException.ClientMissing(this.message!!) is RustAuthenticationException.Generic -> AuthenticationException.Generic(this.message!!) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ClientException.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ClientException.kt index a72755d129..6efca88f9b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ClientException.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/exception/ClientException.kt @@ -19,7 +19,7 @@ package io.element.android.libraries.matrix.impl.exception import io.element.android.libraries.matrix.api.exception.ClientException import org.matrix.rustcomponents.sdk.ClientException as RustClientException -fun Throwable.mapClientException(): Throwable { +fun Throwable.mapClientException(): ClientException { return when (this) { is RustClientException.Generic -> ClientException.Generic(msg) else -> ClientException.Other(message ?: "Unknown error") From af17a5646ce6f61adbf06f036ebbd00efb2e57a4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 17 Jul 2023 22:17:04 +0200 Subject: [PATCH 28/59] Ignore RootNavState regarding koverage. --- build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle.kts b/build.gradle.kts index f8eda34bd8..3348a47c55 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -247,6 +247,7 @@ koverMerged { target = kotlinx.kover.api.VerificationTarget.CLASS overrideClassFilter { includes += "*State" + excludes += "io.element.android.appnav.root.RootNavState*" excludes += "io.element.android.libraries.matrix.api.timeline.item.event.OtherState$*" excludes += "io.element.android.libraries.matrix.api.timeline.item.event.EventSendState$*" excludes += "io.element.android.libraries.matrix.api.room.RoomMembershipState*" From 207a20b67c4e5ea7f932391bfd9fcbf2265241c3 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 17 Jul 2023 23:27:18 +0200 Subject: [PATCH 29/59] RoomFlowNode: use newRoot instead of safeRoot as in this case it can create a race condition where we end up not switching node --- .../kotlin/io/element/android/appnav/room/RoomFlowNode.kt | 6 +++--- .../android/libraries/matrix/impl/RustMatrixClient.kt | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt index a03b6d61a2..20ec9f48b4 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/RoomFlowNode.kt @@ -32,11 +32,11 @@ import com.bumble.appyx.core.node.node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.newRoot import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.appnav.NodeLifecycleCallback -import io.element.android.appnav.safeRoot import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus import io.element.android.libraries.architecture.BackstackNode @@ -92,9 +92,9 @@ class RoomFlowNode @AssistedInject constructor( .distinctUntilChanged() .onEach { isLoaded -> if (isLoaded) { - backstack.safeRoot(NavTarget.Loaded) + backstack.newRoot(NavTarget.Loaded) } else { - backstack.safeRoot(NavTarget.Loading) + backstack.newRoot(NavTarget.Loading) } }.launchIn(lifecycleScope) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index d7195ebf11..a467a161e0 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -165,8 +165,10 @@ class RustMatrixClient constructor( val cachedRoomListItem = roomListService.roomOrNull(roomId.value) val fullRoom = cachedRoomListItem?.fullRoom() if (cachedRoomListItem == null || fullRoom == null) { + Timber.d("No room cached for $roomId") null } else { + Timber.d("Found room cached for $roomId") Pair(cachedRoomListItem, fullRoom) } } From 3d1bd343316fc20779f7aeb08a27c2fe90c773b9 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 17 Jul 2023 23:47:00 +0200 Subject: [PATCH 30/59] Timeline: changes after pr review --- .../impl/timeline/TimelinePresenter.kt | 47 +++++++++++++------ 1 file changed, 32 insertions(+), 15 deletions(-) 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 353d94e390..488b6093cd 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 @@ -25,7 +25,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue import io.element.android.features.messages.impl.timeline.factories.TimelineItemsFactory import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.architecture.Presenter @@ -62,8 +61,8 @@ class TimelinePresenter @Inject constructor( mutableStateOf(null) } - var lastReadReceiptIndex by rememberSaveable { mutableStateOf(Int.MAX_VALUE) } - var lastReadReceiptId by rememberSaveable { mutableStateOf(null) } + val lastReadReceiptIndex = rememberSaveable { mutableStateOf(Int.MAX_VALUE) } + val lastReadReceiptId = rememberSaveable { mutableStateOf(null) } val timelineItems by timelineItemsFactory.collectItemsAsState() val paginationState by timeline.paginationState.collectAsState() @@ -73,16 +72,6 @@ class TimelinePresenter @Inject constructor( val prevMostRecentItemId = rememberSaveable { mutableStateOf(null) } val hasNewItems = remember { mutableStateOf(false) } - fun CoroutineScope.sendReadReceiptIfNeeded(firstVisibleIndex: Int) = launch(dispatchers.computation) { - // Get last valid EventId seen by the user, as the first index might refer to a Virtual item - val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems) - if (eventId != null && firstVisibleIndex <= lastReadReceiptIndex && eventId != lastReadReceiptId) { - lastReadReceiptIndex = firstVisibleIndex - lastReadReceiptId = eventId - timeline.sendReadReceipt(eventId) - } - } - fun handleEvents(event: TimelineEvents) { when (event) { TimelineEvents.LoadMore -> localScope.paginateBackwards() @@ -91,7 +80,12 @@ class TimelinePresenter @Inject constructor( if (event.firstIndex == 0) { hasNewItems.value = false } - appScope.sendReadReceiptIfNeeded(event.firstIndex) + appScope.sendReadReceiptIfNeeded( + firstVisibleIndex = event.firstIndex, + timelineItems = timelineItems, + lastReadReceiptIndex = lastReadReceiptIndex, + lastReadReceiptId = lastReadReceiptId + ) } } } @@ -122,6 +116,11 @@ class TimelinePresenter @Inject constructor( ) } + /** + * This method compute the hasNewItem state passed as a [MutableState] each time the timeline items size changes. + * Basically, if we got new timeline event from sync or local, either from us or another user, we update the state so we tell we have new items. + * The state never goes back to false from this method, but need to be reset from somewhere else. + */ private suspend fun computeHasNewItems( timelineItems: ImmutableList, prevMostRecentItemId: MutableState, @@ -130,13 +129,31 @@ class TimelinePresenter @Inject constructor( val newMostRecentItem = timelineItems.firstOrNull() val prevMostRecentItemIdValue = prevMostRecentItemId.value val newMostRecentItemId = newMostRecentItem?.identifier() - hasNewItemsState.value = prevMostRecentItemIdValue != null && + val hasNewItems = prevMostRecentItemIdValue != null && newMostRecentItem is TimelineItem.Event && newMostRecentItem.origin != TimelineItemEventOrigin.PAGINATION && newMostRecentItemId != prevMostRecentItemIdValue + if (hasNewItems) { + hasNewItemsState.value = true + } prevMostRecentItemId.value = newMostRecentItemId } + private fun CoroutineScope.sendReadReceiptIfNeeded( + firstVisibleIndex: Int, + timelineItems: ImmutableList, + lastReadReceiptIndex: MutableState, + lastReadReceiptId: MutableState, + ) = launch(dispatchers.computation) { + // Get last valid EventId seen by the user, as the first index might refer to a Virtual item + val eventId = getLastEventIdBeforeOrAt(firstVisibleIndex, timelineItems) + if (eventId != null && firstVisibleIndex <= lastReadReceiptIndex.value && eventId != lastReadReceiptId.value) { + lastReadReceiptIndex.value = firstVisibleIndex + lastReadReceiptId.value = eventId + timeline.sendReadReceipt(eventId) + } + } + private fun getLastEventIdBeforeOrAt(index: Int, items: ImmutableList): EventId? { for (item in items.subList(index, items.count())) { if (item is TimelineItem.Event) { From e566bab75db5b0358f1b8ff5e3ab9fcf60a1a43e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jul 2023 00:19:12 +0200 Subject: [PATCH 31/59] Add `ftue` to the dict. --- .idea/dictionaries/shared.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/.idea/dictionaries/shared.xml b/.idea/dictionaries/shared.xml index abe4b190df..aafe02a2c8 100644 --- a/.idea/dictionaries/shared.xml +++ b/.idea/dictionaries/shared.xml @@ -2,6 +2,7 @@ backstack + ftue homeserver kover measurables From 7207afebccb86e700b13119937e86f7cd56aa2cd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jul 2023 00:18:48 +0200 Subject: [PATCH 32/59] When clearing cache, also reset some data store in prefs/datastore. --- .../features/analytics/test/FakeAnalyticsService.kt | 4 ++++ .../element/android/features/ftue/api/state/FtueState.kt | 2 ++ .../android/features/ftue/impl/state/DefaultFtueState.kt | 5 +++++ .../ftue/impl/welcome/state/AndroidWelcomeScreenState.kt | 9 ++++++++- .../ftue/impl/welcome/state/WelcomeScreenState.kt | 1 + .../features/ftue/impl/welcome/state/FakeWelcomeState.kt | 4 ++++ features/preferences/impl/build.gradle.kts | 1 + .../features/preferences/impl/tasks/ClearCacheUseCase.kt | 4 ++++ .../android/services/analytics/api/AnalyticsService.kt | 5 +++++ .../services/analytics/impl/DefaultAnalyticsService.kt | 4 ++++ 10 files changed, 38 insertions(+), 1 deletion(-) diff --git a/features/analytics/test/src/main/kotlin/io/element/android/features/analytics/test/FakeAnalyticsService.kt b/features/analytics/test/src/main/kotlin/io/element/android/features/analytics/test/FakeAnalyticsService.kt index 7ff0f50d9c..6e84c58d2a 100644 --- a/features/analytics/test/src/main/kotlin/io/element/android/features/analytics/test/FakeAnalyticsService.kt +++ b/features/analytics/test/src/main/kotlin/io/element/android/features/analytics/test/FakeAnalyticsService.kt @@ -67,4 +67,8 @@ class FakeAnalyticsService( override fun trackError(throwable: Throwable) { } + + override suspend fun reset() { + didAskUserConsentFlow.value = false + } } diff --git a/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/state/FtueState.kt b/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/state/FtueState.kt index 2c19d4e3a7..cd172669cc 100644 --- a/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/state/FtueState.kt +++ b/features/ftue/api/src/main/kotlin/io/element/android/features/ftue/api/state/FtueState.kt @@ -20,4 +20,6 @@ import kotlinx.coroutines.flow.StateFlow interface FtueState { val shouldDisplayFlow: StateFlow + + suspend fun reset() } diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt index 39b100808f..52c8d90254 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/state/DefaultFtueState.kt @@ -39,6 +39,11 @@ class DefaultFtueState @Inject constructor( override val shouldDisplayFlow = MutableStateFlow(isAnyStepIncomplete()) + override suspend fun reset() { + welcomeScreenState.reset() + analyticsService.reset() + } + init { analyticsService.didAskUserConsent() .onEach { updateState() } diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/state/AndroidWelcomeScreenState.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/state/AndroidWelcomeScreenState.kt index c482b4e744..6dbef47285 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/state/AndroidWelcomeScreenState.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/state/AndroidWelcomeScreenState.kt @@ -17,6 +17,7 @@ package io.element.android.features.ftue.impl.welcome.state import android.content.SharedPreferences +import androidx.core.content.edit import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.DefaultPreferences @@ -27,7 +28,7 @@ import javax.inject.Inject @SingleIn(AppScope::class) class AndroidWelcomeScreenState @Inject constructor( @DefaultPreferences private val sharedPreferences: SharedPreferences, -): WelcomeScreenState { +) : WelcomeScreenState { companion object { private const val IS_WELCOME_SCREEN_SHOWN = "is_welcome_screen_shown" @@ -40,4 +41,10 @@ class AndroidWelcomeScreenState @Inject constructor( override fun setWelcomeScreenShown() { sharedPreferences.edit().putBoolean(IS_WELCOME_SCREEN_SHOWN, true).apply() } + + override fun reset() { + sharedPreferences.edit { + remove(IS_WELCOME_SCREEN_SHOWN) + } + } } diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/state/WelcomeScreenState.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/state/WelcomeScreenState.kt index 0e5f79d7c1..d2be17fcbb 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/state/WelcomeScreenState.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/state/WelcomeScreenState.kt @@ -19,4 +19,5 @@ package io.element.android.features.ftue.impl.welcome.state interface WelcomeScreenState { fun isWelcomeScreenNeeded(): Boolean fun setWelcomeScreenShown() + fun reset() } diff --git a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/welcome/state/FakeWelcomeState.kt b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/welcome/state/FakeWelcomeState.kt index 198d79115a..e38d49db1c 100644 --- a/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/welcome/state/FakeWelcomeState.kt +++ b/features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/welcome/state/FakeWelcomeState.kt @@ -27,4 +27,8 @@ class FakeWelcomeState : WelcomeScreenState { override fun setWelcomeScreenShown() { isWelcomeScreenNeeded = false } + + override fun reset() { + isWelcomeScreenNeeded = true + } } diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index 682757d802..f183f7f1fa 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { implementation(projects.libraries.uiStrings) implementation(projects.features.rageshake.api) implementation(projects.features.analytics.api) + implementation(projects.features.ftue.api) implementation(projects.libraries.matrixui) implementation(projects.features.logout.api) implementation(projects.services.toolbox.api) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt index 24c67a3824..07ca0716e9 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/tasks/ClearCacheUseCase.kt @@ -22,6 +22,7 @@ import android.content.Context import coil.Coil import coil.annotation.ExperimentalCoilApi import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.ftue.api.state.FtueState import io.element.android.features.preferences.impl.DefaultCacheService import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.ApplicationContext @@ -43,6 +44,7 @@ class DefaultClearCacheUseCase @Inject constructor( private val coroutineDispatchers: CoroutineDispatchers, private val defaultCacheIndexProvider: DefaultCacheService, private val okHttpClient: Provider, + private val ftueState: FtueState, ) : ClearCacheUseCase { override suspend fun invoke() = withContext(coroutineDispatchers.io) { // Clear Matrix cache @@ -56,6 +58,8 @@ class DefaultClearCacheUseCase @Inject constructor( okHttpClient.get().cache?.delete() // Clear app cache context.cacheDir.deleteRecursively() + // Clear some settings + ftueState.reset() // Ensure the app is restarted defaultCacheIndexProvider.onClearedCache(matrixClient.sessionId) } diff --git a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/AnalyticsService.kt b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/AnalyticsService.kt index bf06542adb..9c6fb2d522 100644 --- a/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/AnalyticsService.kt +++ b/services/analytics/api/src/main/kotlin/io/element/android/services/analytics/api/AnalyticsService.kt @@ -58,4 +58,9 @@ interface AnalyticsService: AnalyticsTracker, ErrorTracker { * To be called when a session is destroyed. */ suspend fun onSignOut() + + /** + * Reset the analytics service (will ask for user consent again). + */ + suspend fun reset() } diff --git a/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt b/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt index 3fe45e53b7..5639f954ac 100644 --- a/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt +++ b/services/analytics/impl/src/main/kotlin/io/element/android/services/analytics/impl/DefaultAnalyticsService.kt @@ -78,6 +78,10 @@ class DefaultAnalyticsService @Inject constructor( analyticsStore.setDidAskUserConsent() } + override suspend fun reset() { + analyticsStore.setDidAskUserConsent(false) + } + override fun getAnalyticsId(): Flow { return analyticsStore.analyticsIdFlow } From a74278c69034269403db0c1282b5a56515c60b1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Tue, 18 Jul 2023 08:29:06 +0200 Subject: [PATCH 33/59] Fix `TimelinePresenterTests` --- .../features/messages/timeline/TimelinePresenterTest.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt index ad6e41e483..c1d414c633 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/TimelinePresenterTest.kt @@ -102,6 +102,8 @@ class TimelinePresenterTest { }.test { assertThat(timeline.sendReadReceiptCount).isEqualTo(0) val initialState = awaitItem() + // Wait for timeline items to be populated + skipItems(1) awaitWithLatch { latch -> timeline.sendReadReceiptLatch = latch initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0)) @@ -124,6 +126,8 @@ class TimelinePresenterTest { }.test { assertThat(timeline.sendReadReceiptCount).isEqualTo(0) val initialState = awaitItem() + // Wait for timeline items to be populated + skipItems(1) awaitWithLatch { latch -> timeline.sendReadReceiptLatch = latch initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(1)) @@ -146,6 +150,8 @@ class TimelinePresenterTest { }.test { assertThat(timeline.sendReadReceiptCount).isEqualTo(0) val initialState = awaitItem() + // Wait for timeline items to be populated + skipItems(1) awaitWithLatch { latch -> timeline.sendReadReceiptLatch = latch initialState.eventSink.invoke(TimelineEvents.OnScrollFinished(0)) From 5aa4cbdac7704014b69530dd4351ea91d2992833 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jul 2023 02:00:31 +0200 Subject: [PATCH 34/59] Fix text color --- .../android/features/ftue/impl/welcome/WelcomeView.kt | 2 ++ .../designsystem/atomic/molecules/InfoListOrganism.kt | 9 ++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeView.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeView.kt index ccb55494b8..ad040a25e8 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeView.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeView.kt @@ -76,12 +76,14 @@ fun WelcomeView( modifier = Modifier.testTag(TestTags.welcomeScreenTitle), text = stringResource(R.string.screen_welcome_title, applicationName), style = ElementTheme.typography.fontHeadingLgBold, + color = ElementTheme.colors.textPrimary, textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.height(8.dp)) Text( text = stringResource(R.string.screen_welcome_subtitle), style = ElementTheme.typography.fontBodyMdRegular, + color = ElementTheme.colors.textPrimary, textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.height(40.dp)) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InfoListOrganism.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InfoListOrganism.kt index 60c61d99fc..1bc82eb3b2 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InfoListOrganism.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/InfoListOrganism.kt @@ -31,6 +31,7 @@ import io.element.android.libraries.designsystem.atomic.atoms.InfoListItemMolecu import io.element.android.libraries.designsystem.atomic.atoms.InfoListItemPosition import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.theme.ElementTheme import kotlinx.collections.immutable.ImmutableList @Composable @@ -54,7 +55,13 @@ fun InfoListOrganism( else -> InfoListItemPosition.Middle } InfoListItemMolecule( - message = { Text(item.message, style = textStyle) }, + message = { + Text( + text = item.message, + style = textStyle, + color = ElementTheme.colors.textPrimary, + ) + }, icon = { if (item.iconId != null) { Icon(resourceId = item.iconId, contentDescription = null, tint = iconTint) From fc6ddf28762d4ff8d80db6e91280652c69ccee1e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jul 2023 02:05:12 +0200 Subject: [PATCH 35/59] Use correct font (from Figma) --- .../element/android/features/ftue/impl/welcome/WelcomeView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeView.kt b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeView.kt index ad040a25e8..7397e5ecc5 100644 --- a/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeView.kt +++ b/features/ftue/impl/src/main/kotlin/io/element/android/features/ftue/impl/welcome/WelcomeView.kt @@ -75,7 +75,7 @@ fun WelcomeView( Text( modifier = Modifier.testTag(TestTags.welcomeScreenTitle), text = stringResource(R.string.screen_welcome_title, applicationName), - style = ElementTheme.typography.fontHeadingLgBold, + style = ElementTheme.typography.fontHeadingMdBold, color = ElementTheme.colors.textPrimary, textAlign = TextAlign.Center, ) From 616d933fb6f48ce5f68e5585fad62770a7a1d0bb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jul 2023 02:11:15 +0200 Subject: [PATCH 36/59] Format file --- .../libraries/designsystem/atomic/atoms/ElementLogoAtom.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt index 19a1798f74..1ee4826fc0 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt @@ -62,7 +62,7 @@ fun ElementLogoAtom( ElementLogoAtomSize.Large -> 110.dp ElementLogoAtomSize.Medium -> 83.5.dp } - val cornerRadius = when(size) { + val cornerRadius = when (size) { ElementLogoAtomSize.Large -> 44.dp ElementLogoAtomSize.Medium -> 33.dp } @@ -126,7 +126,8 @@ internal fun ElementLogoAtomPreview() { Box( Modifier .size(170.dp) - .background(ElementTheme.colors.bgSubtlePrimary)) + .background(ElementTheme.colors.bgSubtlePrimary) + ) ElementLogoAtom(ElementLogoAtomSize.Large) } } From 7ca5bcf74ea65d2ea90cacfee52dd28fc31caccf Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jul 2023 02:13:23 +0200 Subject: [PATCH 37/59] Fix preview. --- .../designsystem/atomic/atoms/ElementLogoAtom.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt index 1ee4826fc0..93b489e17d 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt @@ -125,10 +125,12 @@ internal fun ElementLogoAtomPreview() { ElementPreview { Box( Modifier - .size(170.dp) - .background(ElementTheme.colors.bgSubtlePrimary) - ) - ElementLogoAtom(ElementLogoAtomSize.Large) + .size(180.dp) + .background(ElementTheme.colors.bgSubtlePrimary), + contentAlignment = Alignment.Center + ) { + ElementLogoAtom(ElementLogoAtomSize.Large) + } } } From 197ac6167059d3d21f5c7df29fd2bf9820695255 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jul 2023 02:18:07 +0200 Subject: [PATCH 38/59] Use the modifier parameter. --- .../android/features/analytics/impl/AnalyticsOptInView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt index ba6d84ae74..a27e6e7399 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt @@ -135,7 +135,7 @@ private fun AnalyticsOptInHeader( @Composable private fun CheckIcon(modifier: Modifier = Modifier) { Icon( - modifier = Modifier + modifier = modifier .size(20.dp) .background(color = MaterialTheme.colorScheme.background, shape = CircleShape) .padding(2.dp), From b8d9655e8e0cb2bd57efad81d8502f9ff36a20a3 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Tue, 18 Jul 2023 00:30:51 +0000 Subject: [PATCH 39/59] Update screenshots --- ...ultGroup_WelcomeViewPreview-D-0_1_null,NEXUS_5,1.0,en].png | 4 ++-- ...ultGroup_WelcomeViewPreview-N-0_2_null,NEXUS_5,1.0,en].png | 4 ++-- ...tGroup_ElementLogoAtomPreview-D_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...tGroup_ElementLogoAtomPreview-N_1_null,NEXUS_5,1.0,en].png | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.ftue.impl.welcome_null_DefaultGroup_WelcomeViewPreview-D-0_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.ftue.impl.welcome_null_DefaultGroup_WelcomeViewPreview-D-0_1_null,NEXUS_5,1.0,en].png index d0e4757049..f300f92921 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.ftue.impl.welcome_null_DefaultGroup_WelcomeViewPreview-D-0_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.ftue.impl.welcome_null_DefaultGroup_WelcomeViewPreview-D-0_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76a68f2fc93894d6f9d9caea02546766c55664c0e53ba9506c6c32df058f5823 -size 303608 +oid sha256:16de62092834bf803c8165e974f45e14ccfc0128a3e74295a58eef965abc10c5 +size 301336 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.ftue.impl.welcome_null_DefaultGroup_WelcomeViewPreview-N-0_2_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.ftue.impl.welcome_null_DefaultGroup_WelcomeViewPreview-N-0_2_null,NEXUS_5,1.0,en].png index 68ee3f3801..7465768560 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.ftue.impl.welcome_null_DefaultGroup_WelcomeViewPreview-N-0_2_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.ftue.impl.welcome_null_DefaultGroup_WelcomeViewPreview-N-0_2_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f24bb3e40dd8c02037bd9d4523726ec0a0b1a283d23a8ca143973b0e9ee673c6 -size 408318 +oid sha256:6838e81cc5f2755ff76de7254e2c8bb445b76662d7ba9b4c83443b2c2ed03029 +size 406044 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomPreview-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomPreview-D_0_null,NEXUS_5,1.0,en].png index d903b752b0..4a98068e90 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomPreview-D_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomPreview-D_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2947531c19a0ac9a7e35c3f2a394f6eb805427e1ad296d22b7d8b5cbb2428e07 -size 20947 +oid sha256:2b19b24fc94f200579827f66557a183842d5234881ed84fe2b8b74d935b90666 +size 22697 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomPreview-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomPreview-N_1_null,NEXUS_5,1.0,en].png index bd9fca6c0f..02c167a01b 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomPreview-N_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomPreview-N_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f88eb992060d5b41ce3200bdc48d4fe6accaeda857d1ca08cb65ed8235798f7 -size 20266 +oid sha256:e35ea20cabe37c05a594bce1b6b4a3c2175470408c18db25874ad5db088f733f +size 21219 From d80f2de112d806e2bb3e978e2262b1643b7d5ecb Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jul 2023 10:58:37 +0200 Subject: [PATCH 40/59] No need to use trick for night resource when using DayNightPreviews annotation. --- .../atomic/pages/OnBoardingPage.kt | 24 ++++-------------- .../onboarding_bg.png} | Bin ...oarding_bg_light.png => onboarding_bg.png} | Bin 3 files changed, 5 insertions(+), 19 deletions(-) rename libraries/designsystem/src/main/res/{drawable/onboarding_bg_dark.png => drawable-night/onboarding_bg.png} (100%) rename libraries/designsystem/src/main/res/drawable/{onboarding_bg_light.png => onboarding_bg.png} (100%) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/OnBoardingPage.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/OnBoardingPage.kt index ec3ee92be8..c411802d44 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/OnBoardingPage.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/OnBoardingPage.kt @@ -28,11 +28,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import io.element.android.libraries.designsystem.R -import io.element.android.libraries.designsystem.preview.ElementPreviewDark -import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.theme.ElementTheme @@ -52,10 +51,6 @@ fun OnBoardingPage( footer: @Composable () -> Unit = {}, content: @Composable () -> Unit = {}, ) { - // Note: having a night variant of R.drawable.onboarding_bg in the folder `drawable-night` is working - // at runtime, but is not in Android Studio Preview. So I prefer to handle this manually. - val isLight = ElementTheme.colors.isLight - val bgDrawableRes = if (isLight) R.drawable.onboarding_bg_light else R.drawable.onboarding_bg_dark Box( modifier = modifier .fillMaxSize() @@ -64,7 +59,7 @@ fun OnBoardingPage( Image( modifier = Modifier .fillMaxSize(), - painter = painterResource(id = bgDrawableRes), + painter = painterResource(id = R.drawable.onboarding_bg), contentScale = ContentScale.Crop, contentDescription = null, ) @@ -92,18 +87,9 @@ fun OnBoardingPage( } } -@Preview +@DayNightPreviews @Composable -internal fun OnBoardingPageLightPreview() = - ElementPreviewLight { ContentToPreview() } - -@Preview -@Composable -internal fun OnBoardingPageDarkPreview() = - ElementPreviewDark { ContentToPreview() } - -@Composable -private fun ContentToPreview() { +internal fun OnBoardingPagePreview() = ElementPreview { OnBoardingPage( content = { Box( diff --git a/libraries/designsystem/src/main/res/drawable/onboarding_bg_dark.png b/libraries/designsystem/src/main/res/drawable-night/onboarding_bg.png similarity index 100% rename from libraries/designsystem/src/main/res/drawable/onboarding_bg_dark.png rename to libraries/designsystem/src/main/res/drawable-night/onboarding_bg.png diff --git a/libraries/designsystem/src/main/res/drawable/onboarding_bg_light.png b/libraries/designsystem/src/main/res/drawable/onboarding_bg.png similarity index 100% rename from libraries/designsystem/src/main/res/drawable/onboarding_bg_light.png rename to libraries/designsystem/src/main/res/drawable/onboarding_bg.png From d6e811ee263101b4f6000ffaf4b99c2f6f0a63fc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jul 2023 11:08:19 +0200 Subject: [PATCH 41/59] No need to use trick for night resource when using DayNightPreviews annotation. --- .../features/location/api/StaticMapView.kt | 1 - .../api/internal/StaticMapPlaceholder.kt | 7 +----- .../blurred_map.png} | Bin ...{blurred_map_light.png => blurred_map.png} | Bin .../event/TimelineItemLocationView.kt | 22 ++++++------------ 5 files changed, 8 insertions(+), 22 deletions(-) rename features/location/api/src/main/res/{drawable/blurred_map_dark.png => drawable-night/blurred_map.png} (100%) rename features/location/api/src/main/res/drawable/{blurred_map_light.png => blurred_map.png} (100%) diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt index 3d09c36604..ad5e29e473 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/StaticMapView.kt @@ -119,7 +119,6 @@ fun StaticMapView( showProgress = painter.state is AsyncImagePainter.State.Loading, contentDescription = contentDescription, modifier = Modifier.size(width = maxWidth, height = maxHeight), - darkMode = darkMode, onLoadMapClick = { retryHash++ } ) } diff --git a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt index 7ef31c326f..d36ead5b28 100644 --- a/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt +++ b/features/location/api/src/main/kotlin/io/element/android/features/location/api/internal/StaticMapPlaceholder.kt @@ -38,7 +38,6 @@ import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings @Composable @@ -46,17 +45,13 @@ internal fun StaticMapPlaceholder( showProgress: Boolean, contentDescription: String?, modifier: Modifier = Modifier, - darkMode: Boolean = !ElementTheme.isLightTheme, onLoadMapClick: () -> Unit, ) { Box( contentAlignment = Alignment.Center, ) { Image( - painter = painterResource( - id = if (darkMode) R.drawable.blurred_map_dark - else R.drawable.blurred_map_light - ), + painter = painterResource(id = R.drawable.blurred_map), contentDescription = contentDescription, modifier = modifier, contentScale = ContentScale.FillBounds, diff --git a/features/location/api/src/main/res/drawable/blurred_map_dark.png b/features/location/api/src/main/res/drawable-night/blurred_map.png similarity index 100% rename from features/location/api/src/main/res/drawable/blurred_map_dark.png rename to features/location/api/src/main/res/drawable-night/blurred_map.png diff --git a/features/location/api/src/main/res/drawable/blurred_map_light.png b/features/location/api/src/main/res/drawable/blurred_map.png similarity index 100% rename from features/location/api/src/main/res/drawable/blurred_map_light.png rename to features/location/api/src/main/res/drawable/blurred_map.png diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemLocationView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemLocationView.kt index 4fbc3e995e..ebc9636773 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemLocationView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemLocationView.kt @@ -22,14 +22,13 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.location.api.StaticMapView import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemLocationContentProvider -import io.element.android.libraries.designsystem.preview.ElementPreviewDark -import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.theme.components.Text @Composable @@ -57,17 +56,10 @@ fun TimelineItemLocationView( } } -@Preview +@DayNightPreviews @Composable -internal fun TimelineItemLocationViewLightPreview(@PreviewParameter(TimelineItemLocationContentProvider::class) content: TimelineItemLocationContent) = - ElementPreviewLight { ContentToPreview(content) } +internal fun TimelineItemLocationViewPreview(@PreviewParameter(TimelineItemLocationContentProvider::class) content: TimelineItemLocationContent) = + ElementPreview { + TimelineItemLocationView(content) + } -@Preview -@Composable -internal fun TimelineItemLocationViewDarkPreview(@PreviewParameter(TimelineItemLocationContentProvider::class) content: TimelineItemLocationContent) = - ElementPreviewDark { ContentToPreview(content) } - -@Composable -private fun ContentToPreview(content: TimelineItemLocationContent) { - TimelineItemLocationView(content) -} From 615022b978ac5d2562ab5e66ac1b10c59c0dfa7c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jul 2023 11:13:50 +0200 Subject: [PATCH 42/59] Use DayNightPreviews for correct rendering in AndroidStudio. --- .../onboarding/impl/OnBoardingView.kt | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt index 0628f15a80..39ace0d425 100644 --- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt +++ b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt @@ -36,14 +36,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.pages.OnBoardingPage -import io.element.android.libraries.designsystem.preview.ElementPreviewDark -import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.theme.aliasButtonText import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Icon @@ -200,17 +199,10 @@ private fun OnBoardingButtons( } } -@Preview +@DayNightPreviews @Composable -internal fun OnBoardingScreenLightPreview(@PreviewParameter(OnBoardingStateProvider::class) state: OnBoardingState) = - ElementPreviewLight { ContentToPreview(state) } - -@Preview -@Composable -internal fun OnBoardingScreenDarkPreview(@PreviewParameter(OnBoardingStateProvider::class) state: OnBoardingState) = - ElementPreviewDark { ContentToPreview(state) } - -@Composable -private fun ContentToPreview(state: OnBoardingState) { +internal fun OnBoardingScreenPreview( + @PreviewParameter(OnBoardingStateProvider::class) state: OnBoardingState +) = ElementPreview { OnBoardingView(state) } From 5824281f0be3ef69fedcd03c273d44fbc5cd721b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jul 2023 11:22:51 +0200 Subject: [PATCH 43/59] Small refacto. --- .../designsystem/atomic/atoms/ElementLogoAtom.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt index 93b489e17d..b5998084c7 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt @@ -53,6 +53,7 @@ import io.element.android.libraries.theme.ElementTheme fun ElementLogoAtom( size: ElementLogoAtomSize, modifier: Modifier = Modifier, + darkTheme: Boolean = isSystemInDarkTheme(), ) { val outerSize = when (size) { ElementLogoAtomSize.Large -> 158.dp @@ -70,19 +71,19 @@ fun ElementLogoAtom( ElementLogoAtomSize.Large -> 1.dp ElementLogoAtomSize.Medium -> 0.38.dp } - val blur = if (isSystemInDarkTheme()) { + val blur = if (darkTheme) { 160.dp } else { 24.dp } //box-shadow: 0px 6.075949668884277px 24.30379867553711px 0px #1B1D2280; - val shadowColor = if (isSystemInDarkTheme()) { + val shadowColor = if (darkTheme) { Color.Black.copy(alpha = 0.4f) } else { Color(0x401B1D22) } - val backgroundColor = if (isSystemInDarkTheme()) Color.White.copy(alpha = 0.2f) else Color.White.copy(alpha = 0.4f) - val borderColor = if (isSystemInDarkTheme()) Color.White.copy(alpha = 0.8f) else Color.White.copy(alpha = 0.4f) + val backgroundColor = if (darkTheme) Color.White.copy(alpha = 0.2f) else Color.White.copy(alpha = 0.4f) + val borderColor = if (darkTheme) Color.White.copy(alpha = 0.8f) else Color.White.copy(alpha = 0.4f) Box( modifier = modifier .size(outerSize) From 6fe59cb5f8302a199317c30e3b5b2815744d0f4f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jul 2023 11:32:52 +0200 Subject: [PATCH 44/59] Small refacto to have more Preview. --- .../atomic/atoms/ElementLogoAtom.kt | 103 +++++++++--------- 1 file changed, 53 insertions(+), 50 deletions(-) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt index b5998084c7..9b1ac26f2c 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt @@ -55,84 +55,61 @@ fun ElementLogoAtom( modifier: Modifier = Modifier, darkTheme: Boolean = isSystemInDarkTheme(), ) { - val outerSize = when (size) { - ElementLogoAtomSize.Large -> 158.dp - ElementLogoAtomSize.Medium -> 120.dp - } - val logoSize = when (size) { - ElementLogoAtomSize.Large -> 110.dp - ElementLogoAtomSize.Medium -> 83.5.dp - } - val cornerRadius = when (size) { - ElementLogoAtomSize.Large -> 44.dp - ElementLogoAtomSize.Medium -> 33.dp - } - val borderWidth = when (size) { - ElementLogoAtomSize.Large -> 1.dp - ElementLogoAtomSize.Medium -> 0.38.dp - } - val blur = if (darkTheme) { - 160.dp - } else { - 24.dp - } + val blur = if (darkTheme) 160.dp else 24.dp //box-shadow: 0px 6.075949668884277px 24.30379867553711px 0px #1B1D2280; - val shadowColor = if (darkTheme) { - Color.Black.copy(alpha = 0.4f) - } else { - Color(0x401B1D22) - } + val shadowColor = if (darkTheme) Color.Black.copy(alpha = 0.4f) else Color(0x401B1D22) val backgroundColor = if (darkTheme) Color.White.copy(alpha = 0.2f) else Color.White.copy(alpha = 0.4f) val borderColor = if (darkTheme) Color.White.copy(alpha = 0.8f) else Color.White.copy(alpha = 0.4f) Box( modifier = modifier - .size(outerSize) - .border(borderWidth, borderColor, RoundedCornerShape(cornerRadius)), + .size(size.outerSize) + .border(size.borderWidth, borderColor, RoundedCornerShape(size.cornerRadius)), contentAlignment = Alignment.Center, ) { Box( Modifier - .size(outerSize) + .size(size.outerSize) .shapeShadow( color = shadowColor, - cornerRadius = cornerRadius, + cornerRadius = size.cornerRadius, blurRadius = 32.dp, offsetY = 8.dp, ) ) Box( Modifier - .clip(RoundedCornerShape(cornerRadius)) - .size(outerSize) + .clip(RoundedCornerShape(size.cornerRadius)) + .size(size.outerSize) .background(backgroundColor) .blur(blur) ) Image( - modifier = Modifier.size(logoSize), + modifier = Modifier.size(size.logoSize), painter = painterResource(id = R.drawable.element_logo), contentDescription = null ) } } -enum class ElementLogoAtomSize { - Medium, - Large -} +sealed class ElementLogoAtomSize( + val outerSize: Dp, + val logoSize: Dp, + val cornerRadius: Dp, + val borderWidth: Dp, +) { + object Medium : ElementLogoAtomSize( + outerSize = 120.dp, + logoSize = 83.5.dp, + cornerRadius = 33.dp, + borderWidth = 0.38.dp, + ) -@Composable -@DayNightPreviews -internal fun ElementLogoAtomPreview() { - ElementPreview { - Box( - Modifier - .size(180.dp) - .background(ElementTheme.colors.bgSubtlePrimary), - contentAlignment = Alignment.Center - ) { - ElementLogoAtom(ElementLogoAtomSize.Large) - } - } + object Large : ElementLogoAtomSize( + outerSize = 158.dp, + logoSize = 110.dp, + cornerRadius = 44.dp, + borderWidth = 1.dp, + ) } fun Modifier.shapeShadow( @@ -172,3 +149,29 @@ fun Modifier.shapeShadow( } } ) + +@Composable +@DayNightPreviews +internal fun ElementLogoAtomMediumPreview() { + ContentToPreview(ElementLogoAtomSize.Medium) +} + +@Composable +@DayNightPreviews +internal fun ElementLogoAtomLargePreview() { + ContentToPreview(ElementLogoAtomSize.Large) +} + +@Composable +private fun ContentToPreview(elementLogoAtomSize: ElementLogoAtomSize) { + ElementPreview { + Box( + Modifier + .size(elementLogoAtomSize.outerSize + 64.dp) + .background(ElementTheme.colors.bgSubtlePrimary), + contentAlignment = Alignment.Center + ) { + ElementLogoAtom(elementLogoAtomSize) + } + } +} From f12dc56ff8691dcd768694e80bebd0bb367f35f9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jul 2023 11:53:46 +0200 Subject: [PATCH 45/59] Create Huge logo size --- .../atomic/atoms/ElementLogoAtom.kt | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt index 9b1ac26f2c..a76aabc5ad 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt @@ -57,7 +57,7 @@ fun ElementLogoAtom( ) { val blur = if (darkTheme) 160.dp else 24.dp //box-shadow: 0px 6.075949668884277px 24.30379867553711px 0px #1B1D2280; - val shadowColor = if (darkTheme) Color.Black.copy(alpha = 0.4f) else Color(0x401B1D22) + val shadowColor = if (darkTheme) size.shadowColorDark else size.shadowColorLight val backgroundColor = if (darkTheme) Color.White.copy(alpha = 0.2f) else Color.White.copy(alpha = 0.4f) val borderColor = if (darkTheme) Color.White.copy(alpha = 0.8f) else Color.White.copy(alpha = 0.4f) Box( @@ -72,7 +72,7 @@ fun ElementLogoAtom( .shapeShadow( color = shadowColor, cornerRadius = size.cornerRadius, - blurRadius = 32.dp, + blurRadius = size.shadowRadius, offsetY = 8.dp, ) ) @@ -96,12 +96,18 @@ sealed class ElementLogoAtomSize( val logoSize: Dp, val cornerRadius: Dp, val borderWidth: Dp, + val shadowColorDark: Color, + val shadowColorLight: Color, + val shadowRadius: Dp, ) { object Medium : ElementLogoAtomSize( outerSize = 120.dp, logoSize = 83.5.dp, cornerRadius = 33.dp, borderWidth = 0.38.dp, + shadowColorDark = Color.Black.copy(alpha = 0.4f), + shadowColorLight = Color(0x401B1D22), + shadowRadius = 32.dp, ) object Large : ElementLogoAtomSize( @@ -109,6 +115,19 @@ sealed class ElementLogoAtomSize( logoSize = 110.dp, cornerRadius = 44.dp, borderWidth = 1.dp, + shadowColorDark = Color.Black.copy(alpha = 0.4f), + shadowColorLight = Color(0x401B1D22), + shadowRadius = 32.dp, + ) + + object Huge : ElementLogoAtomSize( + outerSize = 158.dp, + logoSize = 110.dp, + cornerRadius = 44.dp, + borderWidth = 0.5.dp, + shadowColorDark = Color.Black, + shadowColorLight = Color(0x801B1D22), + shadowRadius = 60.dp, ) } @@ -162,12 +181,18 @@ internal fun ElementLogoAtomLargePreview() { ContentToPreview(ElementLogoAtomSize.Large) } +@Composable +@DayNightPreviews +internal fun ElementLogoAtomHugePreview() { + ContentToPreview(ElementLogoAtomSize.Huge) +} + @Composable private fun ContentToPreview(elementLogoAtomSize: ElementLogoAtomSize) { ElementPreview { Box( Modifier - .size(elementLogoAtomSize.outerSize + 64.dp) + .size(elementLogoAtomSize.outerSize + elementLogoAtomSize.shadowRadius * 2) .background(ElementTheme.colors.bgSubtlePrimary), contentAlignment = Alignment.Center ) { From 041b655db12458e72b34778a5a460c30e71b5059 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jul 2023 12:13:48 +0200 Subject: [PATCH 46/59] Use ElementLogoAtom instead of png. Fixes #894 --- .../onboarding/impl/OnBoardingView.kt | 20 +++++------------- .../res/drawable/onboarding_icon_dark.png | Bin 85609 -> 0 bytes .../res/drawable/onboarding_icon_light.png | Bin 44244 -> 0 bytes 3 files changed, 5 insertions(+), 15 deletions(-) delete mode 100644 features/onboarding/impl/src/main/res/drawable/onboarding_icon_dark.png delete mode 100644 features/onboarding/impl/src/main/res/drawable/onboarding_icon_light.png diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt index 39ace0d425..d8400d94b5 100644 --- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt +++ b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt @@ -16,7 +16,6 @@ package io.element.android.features.onboarding.impl -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -24,7 +23,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.QrCode @@ -33,12 +31,13 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.BiasAlignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtom +import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtomSize import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule import io.element.android.libraries.designsystem.atomic.pages.OnBoardingPage import io.element.android.libraries.designsystem.preview.DayNightPreviews @@ -84,10 +83,6 @@ fun OnBoardingView( @Composable private fun OnBoardingContent(modifier: Modifier = Modifier) { - // Note: having a night variant of R.drawable.onboarding_icon in the folder `drawable-night` is working - // at runtime, but is not in Android Studio Preview. So I prefer to handle this manually. - val isLight = ElementTheme.colors.isLight - val iconDrawableRes = if (isLight) R.drawable.onboarding_icon_light else R.drawable.onboarding_icon_dark Box( modifier = modifier.fillMaxSize(), ) { @@ -98,14 +93,9 @@ private fun OnBoardingContent(modifier: Modifier = Modifier) { verticalBias = -0.4f ) ) { - // Dark and light icon does not have the same size, add padding to the smaller one - val imagePadding = if (isLight) 28.dp else 0.dp - Image( - modifier = Modifier - .size(278.dp) - .padding(imagePadding), - painter = painterResource(id = iconDrawableRes), - contentDescription = null, + ElementLogoAtom( + size = ElementLogoAtomSize.Huge, + modifier = Modifier.padding(top = ElementLogoAtomSize.Huge.shadowRadius / 2) ) } Box( diff --git a/features/onboarding/impl/src/main/res/drawable/onboarding_icon_dark.png b/features/onboarding/impl/src/main/res/drawable/onboarding_icon_dark.png deleted file mode 100644 index 3f93368ff162c37eef3853397766ce5ee00c6f8f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85609 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 A&Hw-a diff --git a/features/onboarding/impl/src/main/res/drawable/onboarding_icon_light.png b/features/onboarding/impl/src/main/res/drawable/onboarding_icon_light.png deleted file mode 100644 index ffd8631c4777248a3eff841ae579fa91a724bc07..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44244 zcmV)VK(D`vP)%0DYo!WJ}dXl=U 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 >|Jg77D<$&zDPx9@Nya}3+L4)zccC46W5uz#nx z54Kf`2p_H~gIGWFK5T!sb6)<~)_(r^g!|OL!tT=hmit*GxB;6>p10E)q+6nyyxtj0 zQ5Es4p}Gt7uj;!=Gz(M~X-)kSQS}x6*Y}Oh#_s{Mk(i2^fHbolIqnnt4W`>Cb|l+w zEi%Q4IwAN1wGzg%j;w`dr0Z52r97_ODnLqkT)H)+HJ5l$zDhThvdf`Z8uVNC`wjOg zY(rVzb{_K@nCG+-u&AaF_)$p+G1D>-tP&bF1+%dw!a|~{ zD#dYCX6==34BLlXLce``zAc=e_R=}UTDBD5kpT@Ovozmcx4!)4 zFAvX0D3-j_$o8}L#2k-3W_DTm{TSSD>Y~1Ha={7)ySnt;mfoLCQdOtSd0g-|q z5_>S%g@86Yy~;uU*%dO7?OZxI+_H@bAsx#DiB@FH*4B~?CPX4It3#Gu0RVxtK#DRb zZNKt1_T?C6emmYIge`4bR%Bq{KrSPFCShPqinIgYkcKhwO55op;xdo;R*R~<;v zCc`FFN z05e)|W0N}O85pQE_@%HduoD?L*d`1rvpbllga7cl92=5BhYiVqyXtLkQ-31gi$qmB z%YXullxM#;z3ENPLY&x!eZwF7S~NX&5XZ5N!4D%8Hc^IKh4Zlw+c+1W`Qv^#FUN6C zTQ6#h=zZ~b%{A8yBlp>lZS2E(@!Yzk5YTf)usX}HxuUN?F$s;0y>S@O&O70r&Buy% z0n^DnW-uKnE}%yqZRk8sN^m)LKWA=OYNvD>lTg75n#bu;k0qE6axZR4a+FIS}Q zjE*694l)bQ2CWOm8>NvKl$Za?y1Y?gNUsP3lG#!HH_VbnR7@Uv`OPRW^W#6hQ-4&- z&GW-kiV&u>%E6BmYAGsdP&MmvuBVC!i8qT%dX;-}>R?9u=_hG!A&lBI2_&i%j0||g zxmm&-s9T&%&Yhyt@@K7}_+wvNXFr_yY{OScyykSl;52_BJc=AMwsC&`bP|vOZx070-tC$2P=n>v*m9ZuF8md!^$;*IV6B*hmC;IJhid1Kpq9h* zv^-xoe0ChIHyiInR1WPKz2_6!f%;`hEiHpI=Ay6w*SL>9gJC>e&Vqn2NIWWLz!J>G^#Ed z-C1OOmrRJV9d}&4lM*WF-5SJJo^4u}5^oh6>gdHRR+ezX8kobKLs**m3w_bCvuu

);pai>8T`>ci+P%#st&GLD%)U)$L0H7d-tX=E6{#Xjo3Pg zW@h)4w-@Ksb&f0VGrAwD7}vE34cnI`$J%~!&!#%+{i zx;&nD%^*V7oPseYs@Qht2h(DMGHd0wN^cGYW+uA57x1QM7%yIRRa zcV{W5ah14NU)ttJ9Ll3WgsSP5662Qbr4yEHET6AzW96kd+`N=tGwQf5-_wSwxIycY zR+D_TeN`#Cp_W}3^oo^u7YLOBp&>)%BauDDwhe~cpbgrf4LTY9-$EUC<}w`m4FCWD M07*qoM6N<$g3+ZiZ2$lO From 12a30ed914ea96bd241af5d474322a79a237c094 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Tue, 18 Jul 2023 10:23:43 +0000 Subject: [PATCH 47/59] Update screenshots --- ...neItemLocationViewPreview-D-7_8_null_0,NEXUS_5,1.0,en].png | 3 +++ ...neItemLocationViewPreview-D-7_8_null_1,NEXUS_5,1.0,en].png | 3 +++ ...neItemLocationViewPreview-N-7_9_null_0,NEXUS_5,1.0,en].png | 3 +++ ...neItemLocationViewPreview-N-7_9_null_1,NEXUS_5,1.0,en].png | 3 +++ ...tedHistoryBannerViewPreview-D-8_9_null,NEXUS_5,1.0,en].png | 3 +++ ...edHistoryBannerViewPreview-N-8_10_null,NEXUS_5,1.0,en].png | 3 +++ ...Group_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png | 4 ++-- ...p_OnBoardingScreenPreview-D-0_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...p_OnBoardingScreenPreview-D-0_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...p_OnBoardingScreenPreview-D-0_0_null_2,NEXUS_5,1.0,en].png | 3 +++ ...p_OnBoardingScreenPreview-D-0_0_null_3,NEXUS_5,1.0,en].png | 3 +++ ...p_OnBoardingScreenPreview-N-0_1_null_0,NEXUS_5,1.0,en].png | 3 +++ ...p_OnBoardingScreenPreview-N-0_1_null_1,NEXUS_5,1.0,en].png | 3 +++ ...p_OnBoardingScreenPreview-N-0_1_null_2,NEXUS_5,1.0,en].png | 3 +++ ...p_OnBoardingScreenPreview-N-0_1_null_3,NEXUS_5,1.0,en].png | 3 +++ ...up_ElementLogoAtomHugePreview-D_0_null,NEXUS_5,1.0,en].png | 3 +++ ...up_ElementLogoAtomHugePreview-N_1_null,NEXUS_5,1.0,en].png | 3 +++ ...p_ElementLogoAtomLargePreview-D_0_null,NEXUS_5,1.0,en].png | 3 +++ ...p_ElementLogoAtomLargePreview-N_1_null,NEXUS_5,1.0,en].png | 3 +++ ..._ElementLogoAtomMediumPreview-D_0_null,NEXUS_5,1.0,en].png | 3 +++ ..._ElementLogoAtomMediumPreview-N_1_null,NEXUS_5,1.0,en].png | 3 +++ ...ltGroup_OnBoardingPagePreview-D_0_null,NEXUS_5,1.0,en].png | 3 +++ ...ltGroup_OnBoardingPagePreview-N_1_null,NEXUS_5,1.0,en].png | 3 +++ 24 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-7_8_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-7_8_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-7_9_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-7_9_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-8_9_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-8_10_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-D-0_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-D-0_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-D-0_0_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-D-0_0_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-N-0_1_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-N-0_1_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-N-0_1_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-N-0_1_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomHugePreview-D_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomHugePreview-N_1_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomLargePreview-D_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomLargePreview-N_1_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomMediumPreview-D_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomMediumPreview-N_1_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.pages_null_DefaultGroup_OnBoardingPagePreview-D_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.pages_null_DefaultGroup_OnBoardingPagePreview-N_1_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-7_8_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-7_8_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6bdd02b8f4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-7_8_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cf8ad6c8f8da98fdfc7a9d5c47e959aaa612dfc5192dd93b589c783d4e94fd6 +size 155690 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-7_8_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-7_8_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fbfd3b1b30 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-7_8_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ae5e86ad1728027f7189041745592d4ed24df07e008b5798b29eadba5449569 +size 159493 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-7_9_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-7_9_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8b8209ffcc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-7_9_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4c033b42d6e7fcd4fece8b8023ee07b1d0477e5bdff338c353903c83da62211 +size 76073 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-7_9_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-7_9_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4794defdd9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-7_9_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56875cfd798c1f5993c6630bc7183dab367913f7c58753434a7d4a08763eac48 +size 80041 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-8_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-8_9_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..45593d6af2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-8_9_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c806ee0293d94ab1c882b0f0ceaac2c7ac69d587f47cc22b4d16bc331351a2e +size 14711 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-8_10_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-8_10_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c74bbe95f8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-8_10_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77c97c6c99943858530219234356459a7f0a88774332a4f11cd738b92ee15c91 +size 14200 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png index ace73de65e..8aa6438474 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:03a4b62920af61098e850cfca6a6f43acd7b310094fb70e1c3e4b7e29465e244 -size 173851 +oid sha256:bcf0ef48396d304cda7d60e443cdce50cafab6d730a175855c365f91dbd9e60b +size 360459 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png index afb2440466..cabe553dc4 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ebcbb40acf19fb489f6a8068a22af0b79c304be46863e77fa12b3b333065a1b2 -size 164622 +oid sha256:c2b0df4b9dbc7a81fd10b24d0cac200fdd17404e34ca0cf0dd6baac87390415b +size 320043 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-D-0_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-D-0_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..636c99bead --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-D-0_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75bea68dfb5165f0f8c755237b627f5c61411fb2ff4d55a18d1daedf054d15ff +size 338382 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-D-0_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-D-0_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..14d6ab78da --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-D-0_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:66e66c919bcc118c87c1d1095d03a105d42f3d630466f0a08d2cef011e67e9e4 +size 327542 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-D-0_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-D-0_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5cadf29e4c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-D-0_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c3c01fb242bd0be1dd894542b3980d359e9276a55afe019ba95423eb7eec7a2 +size 340958 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-D-0_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-D-0_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..68cacc0096 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-D-0_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:595309f942c0ddc37965b8c1e2ef7afd2dc896929e71efd0696ad9214178c9ab +size 322751 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-N-0_1_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-N-0_1_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f32ba6cbaf --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-N-0_1_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:69c933682865e48212cad79bb2194f2bb20a8133e7c76ac1f53f8691445e840d +size 421976 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-N-0_1_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-N-0_1_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ebb9513813 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-N-0_1_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:afcb322f07f594e26b051146b22919daf35317c844669e6204c1f4d29f9fb7ad +size 404245 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-N-0_1_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-N-0_1_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2c884eeef0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-N-0_1_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:88d2194ffc00644a2666175e86ca49dfbad8988fbb98346128d0add77c79b8eb +size 421289 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-N-0_1_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-N-0_1_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..5ce6c750e1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenPreview-N-0_1_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c3c17831149c3a6a5b058e8b557b357e84157cb6a0bc56b945c9e2dd3bdc0de +size 393706 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomHugePreview-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomHugePreview-D_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..820a5f9889 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomHugePreview-D_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c834401ae22ca7ceecd0d92cd0aadaed9e3375b384b295c1e0c4f59d3184a642 +size 51603 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomHugePreview-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomHugePreview-N_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..de08bb030c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomHugePreview-N_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad7fd75f0b2bf8bb9c3c3b38e6a30822cb8732a80228040d3e5046851662ac9a +size 44271 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomLargePreview-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomLargePreview-D_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1f19827181 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomLargePreview-D_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1961539c86a8428964185b2ff42753379270ae1ed34e5139e37866b0545aab8 +size 28823 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomLargePreview-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomLargePreview-N_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6bd0d2f75e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomLargePreview-N_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a807630080b6bdc978997ea9678f7e1ff4df69b20e409b19f4b0c3a6d061de8 +size 25175 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomMediumPreview-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomMediumPreview-D_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b89a9a7443 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomMediumPreview-D_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc17f444d7141faa35cab1446cd02978916e80e6a82405cd96ddbd9f91ed24f8 +size 25327 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomMediumPreview-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomMediumPreview-N_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..cc87d1064a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomMediumPreview-N_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d21f91a5f4ea3f441a9f26da601f65da8054f2b7a330b983b784e811d13ae589 +size 21692 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.pages_null_DefaultGroup_OnBoardingPagePreview-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.pages_null_DefaultGroup_OnBoardingPagePreview-D_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9e191589cc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.pages_null_DefaultGroup_OnBoardingPagePreview-D_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65b33cc70bdfd18f94ea77fe39e6bb6e0595b7fe68583ed88c891baf0b907743 +size 295653 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.pages_null_DefaultGroup_OnBoardingPagePreview-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.pages_null_DefaultGroup_OnBoardingPagePreview-N_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b25a0b30a6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.pages_null_DefaultGroup_OnBoardingPagePreview-N_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:709d08d911e60ef3ec443a347a2824ef42fccc15749903ed3492fa980869c505 +size 430087 From e7a615ea71f624735b4c4b0de94687083f2c9067 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Jul 2023 12:25:18 +0200 Subject: [PATCH 48/59] Update gradle/gradle-build-action action to v2.6.1 (#893) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/nightlyReports.yml | 2 +- .github/workflows/quality.yml | 2 +- .github/workflows/recordScreenshots.yml | 2 +- .github/workflows/tests.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 59719a6de4..90335268a7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.6.0 + uses: gradle/gradle-build-action@v2.6.1 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Assemble debug APK diff --git a/.github/workflows/nightlyReports.yml b/.github/workflows/nightlyReports.yml index 89c992e2c9..ce7b763ef1 100644 --- a/.github/workflows/nightlyReports.yml +++ b/.github/workflows/nightlyReports.yml @@ -62,7 +62,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.6.0 + uses: gradle/gradle-build-action@v2.6.1 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Dependency analysis diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 9dfe7dc06b..94b8b7ff4e 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -39,7 +39,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.6.0 + uses: gradle/gradle-build-action@v2.6.1 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Run code quality check suite diff --git a/.github/workflows/recordScreenshots.yml b/.github/workflows/recordScreenshots.yml index a84ff0d9cf..d088b3ad94 100644 --- a/.github/workflows/recordScreenshots.yml +++ b/.github/workflows/recordScreenshots.yml @@ -24,7 +24,7 @@ jobs: java-version: '17' # Add gradle cache, this should speed up the process - name: Configure gradle - uses: gradle/gradle-build-action@v2.6.0 + uses: gradle/gradle-build-action@v2.6.1 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} - name: Record screenshots diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e5a35da4c9..04fd393e25 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -33,7 +33,7 @@ jobs: distribution: 'temurin' # See 'Supported distributions' for available options java-version: '17' - name: Configure gradle - uses: gradle/gradle-build-action@v2.6.0 + uses: gradle/gradle-build-action@v2.6.1 with: cache-read-only: ${{ github.ref != 'refs/heads/develop' }} From 02f40354cf91a2c9d2a734fceb1b74810873b037 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jul 2023 12:28:42 +0200 Subject: [PATCH 49/59] Use DayNightPreviews for correct rendering in AndroidStudio. --- .../messages/impl/timeline/TimelineView.kt | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) 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 0404edf7db..d7820e707b 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 @@ -51,7 +51,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.res.pluralStringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.features.messages.impl.R @@ -64,8 +63,8 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEventContentProvider import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent -import io.element.android.libraries.designsystem.preview.ElementPreviewDark -import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.theme.components.FloatingActionButton import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.matrix.api.core.EventId @@ -308,20 +307,11 @@ private fun JumpToBottomButton( } } -@Preview +@DayNightPreviews @Composable -fun TimelineViewLightPreview( +fun TimelineViewPreview( @PreviewParameter(TimelineItemEventContentProvider::class) content: TimelineItemEventContent -) = ElementPreviewLight { ContentToPreview(content) } - -@Preview -@Composable -fun TimelineViewDarkPreview( - @PreviewParameter(TimelineItemEventContentProvider::class) content: TimelineItemEventContent -) = ElementPreviewDark { ContentToPreview(content) } - -@Composable -private fun ContentToPreview(content: TimelineItemEventContent) { +) = ElementPreview { val timelineItems = aTimelineItemList(content) TimelineView( state = aTimelineState(timelineItems), From bfef2c0a6be335537804571d98c306b356d53cb2 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Tue, 18 Jul 2023 10:39:24 +0000 Subject: [PATCH 50/59] Update screenshots --- ...ineItemLocationViewPreview-D-8_9_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ineItemLocationViewPreview-D-8_9_null_1,NEXUS_5,1.0,en].png | 3 +++ ...neItemLocationViewPreview-N-8_10_null_0,NEXUS_5,1.0,en].png | 3 +++ ...neItemLocationViewPreview-N-8_10_null_1,NEXUS_5,1.0,en].png | 3 +++ ...tedHistoryBannerViewPreview-D-9_10_null,NEXUS_5,1.0,en].png | 3 +++ ...tedHistoryBannerViewPreview-N-9_11_null,NEXUS_5,1.0,en].png | 3 +++ ...sagesReactionButtonPreview-D-3_4_null_0,NEXUS_5,1.0,en].png | 3 +++ ...sagesReactionButtonPreview-D-3_4_null_1,NEXUS_5,1.0,en].png | 3 +++ ...sagesReactionButtonPreview-D-3_4_null_2,NEXUS_5,1.0,en].png | 3 +++ ...sagesReactionButtonPreview-D-3_4_null_3,NEXUS_5,1.0,en].png | 3 +++ ...sagesReactionButtonPreview-N-3_5_null_0,NEXUS_5,1.0,en].png | 3 +++ ...sagesReactionButtonPreview-N-3_5_null_1,NEXUS_5,1.0,en].png | 3 +++ ...sagesReactionButtonPreview-N-3_5_null_2,NEXUS_5,1.0,en].png | 3 +++ ...sagesReactionButtonPreview-N-3_5_null_3,NEXUS_5,1.0,en].png | 3 +++ ...sReactionExtraButtonsPreview-D-4_5_null,NEXUS_5,1.0,en].png | 3 +++ ...sReactionExtraButtonsPreview-N-4_6_null,NEXUS_5,1.0,en].png | 3 +++ ...eactionsViewCollapsedPreview-D-6_7_null,NEXUS_5,1.0,en].png | 3 +++ ...eactionsViewCollapsedPreview-N-6_8_null,NEXUS_5,1.0,en].png | 3 +++ ...ReactionsViewExpandedPreview-D-7_8_null,NEXUS_5,1.0,en].png | 3 +++ ...ReactionsViewExpandedPreview-N-7_9_null,NEXUS_5,1.0,en].png | 3 +++ ...lineItemReactionsViewPreview-D-5_6_null,NEXUS_5,1.0,en].png | 3 +++ ...lineItemReactionsViewPreview-N-5_7_null,NEXUS_5,1.0,en].png | 3 +++ ...tGroup_TimelineViewPreview-D-2_3_null_0,NEXUS_5,1.0,en].png | 3 +++ ...tGroup_TimelineViewPreview-D-2_3_null_1,NEXUS_5,1.0,en].png | 3 +++ ...Group_TimelineViewPreview-D-2_3_null_10,NEXUS_5,1.0,en].png | 3 +++ ...Group_TimelineViewPreview-D-2_3_null_11,NEXUS_5,1.0,en].png | 3 +++ ...Group_TimelineViewPreview-D-2_3_null_12,NEXUS_5,1.0,en].png | 3 +++ ...tGroup_TimelineViewPreview-D-2_3_null_2,NEXUS_5,1.0,en].png | 3 +++ ...tGroup_TimelineViewPreview-D-2_3_null_3,NEXUS_5,1.0,en].png | 3 +++ ...tGroup_TimelineViewPreview-D-2_3_null_4,NEXUS_5,1.0,en].png | 3 +++ ...tGroup_TimelineViewPreview-D-2_3_null_5,NEXUS_5,1.0,en].png | 3 +++ ...tGroup_TimelineViewPreview-D-2_3_null_6,NEXUS_5,1.0,en].png | 3 +++ ...tGroup_TimelineViewPreview-D-2_3_null_7,NEXUS_5,1.0,en].png | 3 +++ ...tGroup_TimelineViewPreview-D-2_3_null_8,NEXUS_5,1.0,en].png | 3 +++ ...tGroup_TimelineViewPreview-D-2_3_null_9,NEXUS_5,1.0,en].png | 3 +++ ...tGroup_TimelineViewPreview-N-2_4_null_0,NEXUS_5,1.0,en].png | 3 +++ ...tGroup_TimelineViewPreview-N-2_4_null_1,NEXUS_5,1.0,en].png | 3 +++ ...Group_TimelineViewPreview-N-2_4_null_10,NEXUS_5,1.0,en].png | 3 +++ ...Group_TimelineViewPreview-N-2_4_null_11,NEXUS_5,1.0,en].png | 3 +++ ...Group_TimelineViewPreview-N-2_4_null_12,NEXUS_5,1.0,en].png | 3 +++ ...tGroup_TimelineViewPreview-N-2_4_null_2,NEXUS_5,1.0,en].png | 3 +++ ...tGroup_TimelineViewPreview-N-2_4_null_3,NEXUS_5,1.0,en].png | 3 +++ ...tGroup_TimelineViewPreview-N-2_4_null_4,NEXUS_5,1.0,en].png | 3 +++ ...tGroup_TimelineViewPreview-N-2_4_null_5,NEXUS_5,1.0,en].png | 3 +++ ...tGroup_TimelineViewPreview-N-2_4_null_6,NEXUS_5,1.0,en].png | 3 +++ ...tGroup_TimelineViewPreview-N-2_4_null_7,NEXUS_5,1.0,en].png | 3 +++ ...tGroup_TimelineViewPreview-N-2_4_null_8,NEXUS_5,1.0,en].png | 3 +++ ...tGroup_TimelineViewPreview-N-2_4_null_9,NEXUS_5,1.0,en].png | 3 +++ 48 files changed, 144 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-8_9_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-8_9_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-8_10_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-8_10_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-9_10_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-9_11_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-3_4_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-3_4_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-3_4_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-3_4_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-3_5_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-3_5_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-3_5_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-3_5_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionExtraButtonsPreview-D-4_5_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionExtraButtonsPreview-N-4_6_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewCollapsedPreview-D-6_7_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewCollapsedPreview-N-6_8_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewExpandedPreview-D-7_8_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewExpandedPreview-N-7_9_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewPreview-D-5_6_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewPreview-N-5_7_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_10,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_11,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_12,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_6,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_7,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_8,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_9,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_10,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_11,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_12,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_6,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_7,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_8,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_9,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-8_9_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-8_9_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6bdd02b8f4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-8_9_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7cf8ad6c8f8da98fdfc7a9d5c47e959aaa612dfc5192dd93b589c783d4e94fd6 +size 155690 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-8_9_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-8_9_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fbfd3b1b30 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-8_9_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ae5e86ad1728027f7189041745592d4ed24df07e008b5798b29eadba5449569 +size 159493 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-8_10_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-8_10_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8b8209ffcc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-8_10_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4c033b42d6e7fcd4fece8b8023ee07b1d0477e5bdff338c353903c83da62211 +size 76073 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-8_10_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-8_10_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4794defdd9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-8_10_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56875cfd798c1f5993c6630bc7183dab367913f7c58753434a7d4a08763eac48 +size 80041 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-9_10_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-9_10_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..45593d6af2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-9_10_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c806ee0293d94ab1c882b0f0ceaac2c7ac69d587f47cc22b4d16bc331351a2e +size 14711 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-9_11_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-9_11_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c74bbe95f8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-9_11_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77c97c6c99943858530219234356459a7f0a88774332a4f11cd738b92ee15c91 +size 14200 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-3_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-3_4_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b8db218eba --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-3_4_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0e5cb3791012323f9ad6352537b4e579608ebb33f7f46742241b3cac737e617 +size 6349 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-3_4_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-3_4_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..02edd283e7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-3_4_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dfdd3edecb3cb2adb7686dec5043407e3a23e15c5a7911a28f5feba279532e9e +size 7261 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-3_4_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-3_4_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..23ed8a0354 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-3_4_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:995f7f218c005e18acd4b500a81d28784b3b3d81a12c2aadcdd9672ef18ebf28 +size 7191 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-3_4_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-3_4_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d6134420b0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-3_4_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e08bd85cee39e0827fa2cbbee3fd60dc83d860af553f52a609a0f4d691174d2e +size 8140 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-3_5_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-3_5_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4cbfc21652 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-3_5_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:211784ef45a8d357c2a9e9d8b4a16ea10be71d14613ab1e32119ed8fa584831a +size 6365 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-3_5_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-3_5_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..584b363b9e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-3_5_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8e2085deec0b6096437c82204e8da30e143f3c669c9759dbf34c26d63bbf4442 +size 7232 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-3_5_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-3_5_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..666ceab647 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-3_5_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c01fa8074194cf65443f2f321d3ce6b313b7ade8aa7f41155acbfd2a1728e210 +size 7147 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-3_5_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-3_5_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f3c7b5121b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-3_5_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:47ba3b562872e6078e48bd3168c3da54515ea7196fc5310e520a1876972bad16 +size 8020 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionExtraButtonsPreview-D-4_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionExtraButtonsPreview-D-4_5_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d732d1c37b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionExtraButtonsPreview-D-4_5_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:724bc9b0db8f581119201a8db3c405c9a7e7261cccea45a354fd37a5e33fcb41 +size 11141 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionExtraButtonsPreview-N-4_6_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionExtraButtonsPreview-N-4_6_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9b08510703 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionExtraButtonsPreview-N-4_6_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:109a1a9c6359a9fd7fe4258f022e845c9b8142f9e590a03e5c2efa9273cb572f +size 10742 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewCollapsedPreview-D-6_7_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewCollapsedPreview-D-6_7_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c838261a0d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewCollapsedPreview-D-6_7_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dfccebdef9523a984d9edc2414aa159ed025aec14f891945abed73472e462df8 +size 13641 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewCollapsedPreview-N-6_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewCollapsedPreview-N-6_8_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..001fbca040 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewCollapsedPreview-N-6_8_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c971ed744305a474f182bf3d6fde281b9034f99cee4f4207460a733ac8490a7 +size 13464 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewExpandedPreview-D-7_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewExpandedPreview-D-7_8_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..53c52d9a7d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewExpandedPreview-D-7_8_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:999475ddc51f74a590e5ef2810ab31222127e55cba01fadbeb8cb8e9058ad6f0 +size 27170 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewExpandedPreview-N-7_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewExpandedPreview-N-7_9_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..cbed0a0752 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewExpandedPreview-N-7_9_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6fb7202b53005274d98f0aee8014326f02bb62fcd323b38b4d8a447078499dd8 +size 26965 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewPreview-D-5_6_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewPreview-D-5_6_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..119678a386 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewPreview-D-5_6_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82550de45d39c52f34fca45f84b6e9eff53dfef6d2e9622dcaf49c707927d665 +size 7873 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewPreview-N-5_7_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewPreview-N-5_7_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..896fd1051e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewPreview-N-5_7_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc39041ac035aa372640ca7b2e9af254564aa10ce7a7f4c55fbc4bca9f6dc6d9 +size 7844 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7c99f5e856 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54c12970e3563de958f88e4c538dd368f9810266060627393256a91741f7c6cf +size 53340 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..def9cbe0d1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b7ae3084cb9d1ecee2e4db49c228516bdd7352683e797edf737c8d216922dec +size 65601 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_10,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..258ce3f3de --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_10,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd034d439c08793e0dfd59f6bd5dcd88c06ded6cbe98172bf3cf296888e6d575 +size 51244 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_11,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b2f16a9d23 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_11,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9d835cb1a420117b4d967181e2ca0fad71ba243d6a17bea08b82cae41f6b8e2 +size 68760 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_12,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3bbd94a1c8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_12,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ad09278ae2ebb8171adba96d9f6e91d0cc4f120b0b2368087796dadb37eeb87 +size 58539 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1c32b14777 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:145856c3a7ff43702403ee5b86a7119b0475f03fdbc0f2e7f84e10350a64b150 +size 229842 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..53f57d95d2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1f0939b0c22ab89466953889e5bb63e11c45f02af79eeba3f377409af61d356 +size 230809 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4c991e3c30 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:082206122d4e6d9e6171b3b2444c576ff7bd47fb3946d8d98e2812654f39cd40 +size 73641 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..96270c69ae --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14ecdfd226a25743e3fac8850318216d6ec193755fa7559fd5523236b7835bc6 +size 89754 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b62d934c0c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d08de923f29ec6c3c2bf735ca0d015ca1097c95484119d1d39bb8c3f93f31100 +size 363776 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3e79fb521a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e8832da336aea6c7ebb3621668569ff16238042fb9650afcd938594a59fd3ae +size 322771 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ef081b76b8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a243d53d10ca2eaa249b22af8a9fddf1a8ccf60db4f3ad9374e31cf494fe878 +size 54909 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_9,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0b13a93b0c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_9,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3fdd478be89b47fcaf9725ca14c1e775087f1ccf2a266f3476a537bbc2b29922 +size 67476 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9f5b7d48c6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a3b5bbcdd1593e81384b335045bdcb0b3e01782868993e9f6437a15ca39dbca +size 51380 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ba9f8a8d63 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e26db0a0a8d767d6e732c1d2742cba3c3475d761b0c3017ff01cee7e3d362a5 +size 62773 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_10,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7d12d81b18 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_10,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3d5cae7d73178aecc12fbf5f1f27c9e66608b90d462deec862a881b403469e93 +size 49475 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_11,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..44fd92b664 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_11,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe9456f4446104142221e28167a578a2b5cac772dc35aeb14352c0d422dd6fb0 +size 65726 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_12,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ee3a9ae3fe --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_12,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cad3b5ce023d890fd4a5ff0f5bbabb678bc6d5c76d3476e8053673c06f360c8b +size 56102 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8e578cb1f9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71b3c712fb8e4bca178afed2de6f4184bb717e3415622e97f14bb4fe36aaf9d5 +size 229097 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..13d92ee329 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f44d772e5b9fe65a79e05aaaa82536b19382f9d821c0412c47a7d330d539ffd2 +size 230080 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4a58253b47 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:41a6c2dd81698696708802276b615a78ff22cc7cb8ae2d4b8d8cc862bfe44a24 +size 70856 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..92bbf1dd70 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f729f89dce978bb9e061826389e0db1af0e18835e4cd1ab6cbfaa31f4fd3152f +size 84942 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ace73de65e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:03a4b62920af61098e850cfca6a6f43acd7b310094fb70e1c3e4b7e29465e244 +size 173851 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..afb2440466 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ebcbb40acf19fb489f6a8068a22af0b79c304be46863e77fa12b3b333065a1b2 +size 164622 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..08c13f8f2a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:789ee5ccad8356198cdc0634b4e9a65ed44be2d26e7ce83a8662598c1bd8d4c2 +size 52765 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_9,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..300bcf7167 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_9,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b40a2e5d60a906d7c35c3ece1854671f5b319b00ad077322d094cbf906c07f7 +size 64803 From bb6132c0fd084b5671211df516a7419e953b0256 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Tue, 18 Jul 2023 12:35:42 +0000 Subject: [PATCH 51/59] Update screenshots --- ...Group_SheetContentPreview-D-0_1_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...Group_SheetContentPreview-N-0_2_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...up_MediaViewerViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png | 3 +++ ...up_MediaViewerViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png | 3 +++ ...elineItemAudioViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...elineItemAudioViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...elineItemAudioViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 +++ ...lineItemAudioViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...lineItemAudioViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...lineItemAudioViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 3 +++ ...l_Dialogs_ProgressDialogPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- 11 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewLightPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewLightPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewLightPreview_0_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentPreview-D-0_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentPreview-D-0_1_null_5,NEXUS_5,1.0,en].png index b0937d3b09..0905a820cb 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentPreview-D-0_1_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentPreview-D-0_1_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e4bcce9d53c7c094698f07de6c6a25be2c7831707581861df4b0cdf0c3d6d1fd -size 40906 +oid sha256:16e955d418eadb90cf9952d18b87669e03c50d5b166e0fee1d193a733e48372d +size 39775 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentPreview-N-0_2_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentPreview-N-0_2_null_5,NEXUS_5,1.0,en].png index 6cc64a9ea3..579b7a76dd 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentPreview-N-0_2_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentPreview-N-0_2_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9bb3eab588d8cc20eb2bcbbcb8a7acdf6431299bf575a124ed7aff8a4fe6cd15 -size 39182 +oid sha256:5c9f0a9aa7cb0b1665eae95564a189c6752c707dce195979076b0504eac497a1 +size 38170 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8aea9ffda4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1805563eccf45f507152722511cacfa3f0411d1ccb90dd8d539f9a4a697b56f +size 14459 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..48767dad09 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.media.viewer_null_DefaultGroup_MediaViewerViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2824676afbff473eff8c8cfbcf8b3e58b2851e37324c6a8d9ba47d626b0f8bc +size 14234 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e0136f4c31 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b83378cdfb20b5ffc781fc514c5a4d3aaa0714101b12a572f032f92ec13180c +size 9776 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9f9e9a32fc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:40d1ea7620f7f830f52b87fbe32d39d4c01ddeeeb10535da17f7207acc80a1d6 +size 11821 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..179c04456a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11e2a8b0a79c56b379d387a3987362d5309e7f30903b940994ed43217de1380d +size 21441 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..53bfbe6d54 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e00510a5f35eb33aaef15c30e6caac62045d8e4c37c032a24922bbb749ad0375 +size 9817 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9ce9173802 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:229d83ed02804137ab1dad4114eecc6d52d6a0e3ccbad436cf226f6cfc628cf7 +size 12200 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewLightPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..77a65f3b40 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27fff9f0ea88cf06934298ea6155cbf4dd49c370cc5d0a14b4d387ac8a7e7c39 +size 23203 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components_null_Dialogs_ProgressDialogPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components_null_Dialogs_ProgressDialogPreview_0_null,NEXUS_5,1.0,en].png index af63d0ef73..9d7ab1352b 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components_null_Dialogs_ProgressDialogPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.components_null_Dialogs_ProgressDialogPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10b9570b3f00b7ad9fa473bd5de8568fc4e3305fc3ad5d6d3bec9cb40a1fbe46 -size 20350 +oid sha256:22747c979bf704a554dfa4ee3ace1551e78f1180bc594c55f1497ec1d6529aa2 +size 21211 From e831b621c99807e235cbb60e85d14d3c741c0068 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jul 2023 14:56:48 +0200 Subject: [PATCH 52/59] Ensure previous screenshots are deleted. Sometimes, the registered task is not trigger, I do not know why... --- .github/workflows/scripts/recordScreenshots.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/scripts/recordScreenshots.sh b/.github/workflows/scripts/recordScreenshots.sh index 028e480a5b..792be75931 100755 --- a/.github/workflows/scripts/recordScreenshots.sh +++ b/.github/workflows/scripts/recordScreenshots.sh @@ -58,6 +58,9 @@ if [[ -z ${REPO} ]]; then exit 1 fi +echo "Deleting previous screenshots" +./gradlew removeOldSnapshots --stacktrace -PpreDexEnable=false --max-workers 4 --warn + echo "Record screenshots" ./gradlew recordPaparazziDebug --stacktrace -PpreDexEnable=false --max-workers 4 --warn From 6b3f69accf381321751952b208db01469799e4ee Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jul 2023 14:58:09 +0200 Subject: [PATCH 53/59] Use Large and remove Huge. --- .../features/onboarding/impl/OnBoardingView.kt | 4 ++-- .../designsystem/atomic/atoms/ElementLogoAtom.kt | 16 ---------------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt index d8400d94b5..0651d9cc2e 100644 --- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt +++ b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt @@ -94,8 +94,8 @@ private fun OnBoardingContent(modifier: Modifier = Modifier) { ) ) { ElementLogoAtom( - size = ElementLogoAtomSize.Huge, - modifier = Modifier.padding(top = ElementLogoAtomSize.Huge.shadowRadius / 2) + size = ElementLogoAtomSize.Large, + modifier = Modifier.padding(top = ElementLogoAtomSize.Large.shadowRadius / 2) ) } Box( diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt index a76aabc5ad..94e50d5953 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/ElementLogoAtom.kt @@ -111,16 +111,6 @@ sealed class ElementLogoAtomSize( ) object Large : ElementLogoAtomSize( - outerSize = 158.dp, - logoSize = 110.dp, - cornerRadius = 44.dp, - borderWidth = 1.dp, - shadowColorDark = Color.Black.copy(alpha = 0.4f), - shadowColorLight = Color(0x401B1D22), - shadowRadius = 32.dp, - ) - - object Huge : ElementLogoAtomSize( outerSize = 158.dp, logoSize = 110.dp, cornerRadius = 44.dp, @@ -181,12 +171,6 @@ internal fun ElementLogoAtomLargePreview() { ContentToPreview(ElementLogoAtomSize.Large) } -@Composable -@DayNightPreviews -internal fun ElementLogoAtomHugePreview() { - ContentToPreview(ElementLogoAtomSize.Huge) -} - @Composable private fun ContentToPreview(elementLogoAtomSize: ElementLogoAtomSize) { ElementPreview { From 1a51b0410a78ccbe8ed5f62720657ba3a7572399 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Tue, 18 Jul 2023 13:07:52 +0000 Subject: [PATCH 54/59] Update screenshots --- ...neItemLocationViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 --- ...neItemLocationViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 --- ...eItemLocationViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 --- ...eItemLocationViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 --- ...neItemLocationViewPreview-D-7_8_null_0,NEXUS_5,1.0,en].png | 3 --- ...neItemLocationViewPreview-D-7_8_null_1,NEXUS_5,1.0,en].png | 3 --- ...neItemLocationViewPreview-N-7_9_null_0,NEXUS_5,1.0,en].png | 3 --- ...neItemLocationViewPreview-N-7_9_null_1,NEXUS_5,1.0,en].png | 3 --- ...tedHistoryBannerViewPreview-D-7_8_null,NEXUS_5,1.0,en].png | 3 --- ...tedHistoryBannerViewPreview-D-8_9_null,NEXUS_5,1.0,en].png | 3 --- ...tedHistoryBannerViewPreview-N-7_9_null,NEXUS_5,1.0,en].png | 3 --- ...edHistoryBannerViewPreview-N-8_10_null,NEXUS_5,1.0,en].png | 3 --- ...agesReactionButtonPreview-D-2_3_null_0,NEXUS_5,1.0,en].png | 3 --- ...agesReactionButtonPreview-D-2_3_null_1,NEXUS_5,1.0,en].png | 3 --- ...agesReactionButtonPreview-D-2_3_null_2,NEXUS_5,1.0,en].png | 3 --- ...agesReactionButtonPreview-D-2_3_null_3,NEXUS_5,1.0,en].png | 3 --- ...agesReactionButtonPreview-N-2_4_null_0,NEXUS_5,1.0,en].png | 3 --- ...agesReactionButtonPreview-N-2_4_null_1,NEXUS_5,1.0,en].png | 3 --- ...agesReactionButtonPreview-N-2_4_null_2,NEXUS_5,1.0,en].png | 3 --- ...agesReactionButtonPreview-N-2_4_null_3,NEXUS_5,1.0,en].png | 3 --- ...ReactionExtraButtonsPreview-D-3_4_null,NEXUS_5,1.0,en].png | 3 --- ...ReactionExtraButtonsPreview-N-3_5_null,NEXUS_5,1.0,en].png | 3 --- ...actionsViewCollapsedPreview-D-5_6_null,NEXUS_5,1.0,en].png | 3 --- ...actionsViewCollapsedPreview-N-5_7_null,NEXUS_5,1.0,en].png | 3 --- ...eactionsViewExpandedPreview-D-6_7_null,NEXUS_5,1.0,en].png | 3 --- ...eactionsViewExpandedPreview-N-6_8_null,NEXUS_5,1.0,en].png | 3 --- ...ineItemReactionsViewPreview-D-4_5_null,NEXUS_5,1.0,en].png | 3 --- ...ineItemReactionsViewPreview-N-4_6_null,NEXUS_5,1.0,en].png | 3 --- ...Group_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 --- ...Group_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 --- ...roup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png | 3 --- ...roup_TimelineViewDarkPreview_0_null_11,NEXUS_5,1.0,en].png | 3 --- ...roup_TimelineViewDarkPreview_0_null_12,NEXUS_5,1.0,en].png | 3 --- ...Group_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 --- ...Group_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 3 --- ...Group_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 3 --- ...Group_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png | 3 --- ...Group_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png | 3 --- ...Group_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png | 3 --- ...Group_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png | 3 --- ...Group_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png | 3 --- ...roup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 --- ...roup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 --- ...oup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png | 3 --- ...oup_TimelineViewLightPreview_0_null_11,NEXUS_5,1.0,en].png | 3 --- ...oup_TimelineViewLightPreview_0_null_12,NEXUS_5,1.0,en].png | 3 --- ...roup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 3 --- ...roup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png | 3 --- ...roup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png | 3 --- ...roup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png | 3 --- ...roup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png | 3 --- ...roup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png | 3 --- ...roup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png | 3 --- ...roup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png | 3 --- ...p_OnBoardingScreenDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 --- ...p_OnBoardingScreenDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 --- ...p_OnBoardingScreenDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 --- ...p_OnBoardingScreenDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 3 --- ..._OnBoardingScreenLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 --- ..._OnBoardingScreenLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 --- ..._OnBoardingScreenLightPreview_0_null_2,NEXUS_5,1.0,en].png | 3 --- ..._OnBoardingScreenLightPreview_0_null_3,NEXUS_5,1.0,en].png | 3 --- ...up_ElementLogoAtomHugePreview-D_0_null,NEXUS_5,1.0,en].png | 3 --- ...up_ElementLogoAtomHugePreview-N_1_null,NEXUS_5,1.0,en].png | 3 --- ...p_ElementLogoAtomLargePreview-D_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...p_ElementLogoAtomLargePreview-N_1_null,NEXUS_5,1.0,en].png | 4 ++-- ...tGroup_ElementLogoAtomPreview-D_0_null,NEXUS_5,1.0,en].png | 3 --- ...tGroup_ElementLogoAtomPreview-N_1_null,NEXUS_5,1.0,en].png | 3 --- ...Group_OnBoardingPageDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 --- ...roup_OnBoardingPageLightPreview_0_null,NEXUS_5,1.0,en].png | 3 --- 70 files changed, 4 insertions(+), 208 deletions(-) delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewLightPreview_0_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewLightPreview_0_null_1,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-7_8_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-7_8_null_1,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-7_9_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-7_9_null_1,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-7_8_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-8_9_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-7_9_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-8_10_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-2_3_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-2_3_null_1,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-2_3_null_2,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-2_3_null_3,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-2_4_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-2_4_null_1,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-2_4_null_2,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-2_4_null_3,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionExtraButtonsPreview-D-3_4_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionExtraButtonsPreview-N-3_5_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewCollapsedPreview-D-5_6_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewCollapsedPreview-N-5_7_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewExpandedPreview-D-6_7_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewExpandedPreview-N-6_8_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewPreview-D-4_5_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewPreview-N-4_6_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_11,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_12,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_11,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_12,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null_1,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null_2,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null_3,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null_1,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null_2,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null_3,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomHugePreview-D_0_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomHugePreview-N_1_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomPreview-D_0_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomPreview-N_1_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.pages_null_DefaultGroup_OnBoardingPageDarkPreview_0_null,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.pages_null_DefaultGroup_OnBoardingPageLightPreview_0_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index f1b15b6d73..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5b9dd2f15bb1a4d1ff8a1614f0a4c29f61b9277dcd04e921b8c164e7f18946e2 -size 76727 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index 3830329d1d..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8b14570268c5885105bb68d55e54afe83d1916e32a5a9cd8fa4084cba020b272 -size 80504 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewLightPreview_0_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index 6bdd02b8f4..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7cf8ad6c8f8da98fdfc7a9d5c47e959aaa612dfc5192dd93b589c783d4e94fd6 -size 155690 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewLightPreview_0_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index fbfd3b1b30..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2ae5e86ad1728027f7189041745592d4ed24df07e008b5798b29eadba5449569 -size 159493 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-7_8_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-7_8_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index 6bdd02b8f4..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-7_8_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7cf8ad6c8f8da98fdfc7a9d5c47e959aaa612dfc5192dd93b589c783d4e94fd6 -size 155690 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-7_8_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-7_8_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index fbfd3b1b30..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-7_8_null_1,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2ae5e86ad1728027f7189041745592d4ed24df07e008b5798b29eadba5449569 -size 159493 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-7_9_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-7_9_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index 8b8209ffcc..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-7_9_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b4c033b42d6e7fcd4fece8b8023ee07b1d0477e5bdff338c353903c83da62211 -size 76073 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-7_9_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-7_9_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index 4794defdd9..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-7_9_null_1,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:56875cfd798c1f5993c6630bc7183dab367913f7c58753434a7d4a08763eac48 -size 80041 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-7_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-7_8_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 45593d6af2..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-7_8_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2c806ee0293d94ab1c882b0f0ceaac2c7ac69d587f47cc22b4d16bc331351a2e -size 14711 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-8_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-8_9_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 45593d6af2..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-8_9_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2c806ee0293d94ab1c882b0f0ceaac2c7ac69d587f47cc22b4d16bc331351a2e -size 14711 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-7_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-7_9_null,NEXUS_5,1.0,en].png deleted file mode 100644 index c74bbe95f8..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-7_9_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:77c97c6c99943858530219234356459a7f0a88774332a4f11cd738b92ee15c91 -size 14200 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-8_10_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-8_10_null,NEXUS_5,1.0,en].png deleted file mode 100644 index c74bbe95f8..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-8_10_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:77c97c6c99943858530219234356459a7f0a88774332a4f11cd738b92ee15c91 -size 14200 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-2_3_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-2_3_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index b8db218eba..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-2_3_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a0e5cb3791012323f9ad6352537b4e579608ebb33f7f46742241b3cac737e617 -size 6349 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-2_3_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-2_3_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index 02edd283e7..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-2_3_null_1,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dfdd3edecb3cb2adb7686dec5043407e3a23e15c5a7911a28f5feba279532e9e -size 7261 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-2_3_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-2_3_null_2,NEXUS_5,1.0,en].png deleted file mode 100644 index 23ed8a0354..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-2_3_null_2,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:995f7f218c005e18acd4b500a81d28784b3b3d81a12c2aadcdd9672ef18ebf28 -size 7191 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-2_3_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-2_3_null_3,NEXUS_5,1.0,en].png deleted file mode 100644 index d6134420b0..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-D-2_3_null_3,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e08bd85cee39e0827fa2cbbee3fd60dc83d860af553f52a609a0f4d691174d2e -size 8140 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-2_4_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-2_4_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index 4cbfc21652..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-2_4_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:211784ef45a8d357c2a9e9d8b4a16ea10be71d14613ab1e32119ed8fa584831a -size 6365 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-2_4_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-2_4_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index 584b363b9e..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-2_4_null_1,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:8e2085deec0b6096437c82204e8da30e143f3c669c9759dbf34c26d63bbf4442 -size 7232 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-2_4_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-2_4_null_2,NEXUS_5,1.0,en].png deleted file mode 100644 index 666ceab647..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-2_4_null_2,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c01fa8074194cf65443f2f321d3ce6b313b7ade8aa7f41155acbfd2a1728e210 -size 7147 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-2_4_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-2_4_null_3,NEXUS_5,1.0,en].png deleted file mode 100644 index f3c7b5121b..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionButtonPreview-N-2_4_null_3,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:47ba3b562872e6078e48bd3168c3da54515ea7196fc5310e520a1876972bad16 -size 8020 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionExtraButtonsPreview-D-3_4_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionExtraButtonsPreview-D-3_4_null,NEXUS_5,1.0,en].png deleted file mode 100644 index d732d1c37b..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionExtraButtonsPreview-D-3_4_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:724bc9b0db8f581119201a8db3c405c9a7e7261cccea45a354fd37a5e33fcb41 -size 11141 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionExtraButtonsPreview-N-3_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionExtraButtonsPreview-N-3_5_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 9b08510703..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_MessagesReactionExtraButtonsPreview-N-3_5_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:109a1a9c6359a9fd7fe4258f022e845c9b8142f9e590a03e5c2efa9273cb572f -size 10742 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewCollapsedPreview-D-5_6_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewCollapsedPreview-D-5_6_null,NEXUS_5,1.0,en].png deleted file mode 100644 index c838261a0d..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewCollapsedPreview-D-5_6_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dfccebdef9523a984d9edc2414aa159ed025aec14f891945abed73472e462df8 -size 13641 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewCollapsedPreview-N-5_7_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewCollapsedPreview-N-5_7_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 001fbca040..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewCollapsedPreview-N-5_7_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9c971ed744305a474f182bf3d6fde281b9034f99cee4f4207460a733ac8490a7 -size 13464 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewExpandedPreview-D-6_7_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewExpandedPreview-D-6_7_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 53c52d9a7d..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewExpandedPreview-D-6_7_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:999475ddc51f74a590e5ef2810ab31222127e55cba01fadbeb8cb8e9058ad6f0 -size 27170 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewExpandedPreview-N-6_8_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewExpandedPreview-N-6_8_null,NEXUS_5,1.0,en].png deleted file mode 100644 index cbed0a0752..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewExpandedPreview-N-6_8_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6fb7202b53005274d98f0aee8014326f02bb62fcd323b38b4d8a447078499dd8 -size 26965 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewPreview-D-4_5_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewPreview-D-4_5_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 119678a386..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewPreview-D-4_5_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:82550de45d39c52f34fca45f84b6e9eff53dfef6d2e9622dcaf49c707927d665 -size 7873 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewPreview-N-4_6_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewPreview-N-4_6_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 896fd1051e..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemReactionsViewPreview-N-4_6_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fc39041ac035aa372640ca7b2e9af254564aa10ce7a7f4c55fbc4bca9f6dc6d9 -size 7844 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index 9f5b7d48c6..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1a3b5bbcdd1593e81384b335045bdcb0b3e01782868993e9f6437a15ca39dbca -size 51380 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index ba9f8a8d63..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7e26db0a0a8d767d6e732c1d2742cba3c3475d761b0c3017ff01cee7e3d362a5 -size 62773 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png deleted file mode 100644 index 7d12d81b18..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3d5cae7d73178aecc12fbf5f1f27c9e66608b90d462deec862a881b403469e93 -size 49475 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_11,NEXUS_5,1.0,en].png deleted file mode 100644 index 44fd92b664..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_11,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:fe9456f4446104142221e28167a578a2b5cac772dc35aeb14352c0d422dd6fb0 -size 65726 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_12,NEXUS_5,1.0,en].png deleted file mode 100644 index ee3a9ae3fe..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_12,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cad3b5ce023d890fd4a5ff0f5bbabb678bc6d5c76d3476e8053673c06f360c8b -size 56102 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png deleted file mode 100644 index 8e578cb1f9..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:71b3c712fb8e4bca178afed2de6f4184bb717e3415622e97f14bb4fe36aaf9d5 -size 229097 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png deleted file mode 100644 index 13d92ee329..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f44d772e5b9fe65a79e05aaaa82536b19382f9d821c0412c47a7d330d539ffd2 -size 230080 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png deleted file mode 100644 index 4a58253b47..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:41a6c2dd81698696708802276b615a78ff22cc7cb8ae2d4b8d8cc862bfe44a24 -size 70856 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png deleted file mode 100644 index 92bbf1dd70..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f729f89dce978bb9e061826389e0db1af0e18835e4cd1ab6cbfaa31f4fd3152f -size 84942 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png deleted file mode 100644 index 8aa6438474..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:bcf0ef48396d304cda7d60e443cdce50cafab6d730a175855c365f91dbd9e60b -size 360459 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png deleted file mode 100644 index cabe553dc4..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c2b0df4b9dbc7a81fd10b24d0cac200fdd17404e34ca0cf0dd6baac87390415b -size 320043 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png deleted file mode 100644 index 08c13f8f2a..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_8,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:789ee5ccad8356198cdc0634b4e9a65ed44be2d26e7ce83a8662598c1bd8d4c2 -size 52765 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png deleted file mode 100644 index 300bcf7167..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_9,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7b40a2e5d60a906d7c35c3ece1854671f5b319b00ad077322d094cbf906c07f7 -size 64803 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index 7c99f5e856..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:54c12970e3563de958f88e4c538dd368f9810266060627393256a91741f7c6cf -size 53340 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index def9cbe0d1..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3b7ae3084cb9d1ecee2e4db49c228516bdd7352683e797edf737c8d216922dec -size 65601 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png deleted file mode 100644 index 258ce3f3de..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dd034d439c08793e0dfd59f6bd5dcd88c06ded6cbe98172bf3cf296888e6d575 -size 51244 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_11,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_11,NEXUS_5,1.0,en].png deleted file mode 100644 index b2f16a9d23..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_11,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d9d835cb1a420117b4d967181e2ca0fad71ba243d6a17bea08b82cae41f6b8e2 -size 68760 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_12,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_12,NEXUS_5,1.0,en].png deleted file mode 100644 index 3bbd94a1c8..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_12,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:4ad09278ae2ebb8171adba96d9f6e91d0cc4f120b0b2368087796dadb37eeb87 -size 58539 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png deleted file mode 100644 index 1c32b14777..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:145856c3a7ff43702403ee5b86a7119b0475f03fdbc0f2e7f84e10350a64b150 -size 229842 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png deleted file mode 100644 index 53f57d95d2..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c1f0939b0c22ab89466953889e5bb63e11c45f02af79eeba3f377409af61d356 -size 230809 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png deleted file mode 100644 index 4c991e3c30..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_4,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:082206122d4e6d9e6171b3b2444c576ff7bd47fb3946d8d98e2812654f39cd40 -size 73641 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png deleted file mode 100644 index 96270c69ae..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_5,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:14ecdfd226a25743e3fac8850318216d6ec193755fa7559fd5523236b7835bc6 -size 89754 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png deleted file mode 100644 index b62d934c0c..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_6,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d08de923f29ec6c3c2bf735ca0d015ca1097c95484119d1d39bb8c3f93f31100 -size 363776 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png deleted file mode 100644 index 3e79fb521a..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_7,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3e8832da336aea6c7ebb3621668569ff16238042fb9650afcd938594a59fd3ae -size 322771 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png deleted file mode 100644 index ef081b76b8..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_8,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9a243d53d10ca2eaa249b22af8a9fddf1a8ccf60db4f3ad9374e31cf494fe878 -size 54909 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png deleted file mode 100644 index 0b13a93b0c..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_9,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3fdd478be89b47fcaf9725ca14c1e775087f1ccf2a266f3476a537bbc2b29922 -size 67476 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index 039f40e6ed..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1c001e4db1cee87d99b4faa8956d322dd5f70c1a3fe92ea36ccd24ba7f6ee94b -size 471797 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index f1d7ca94be..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:188c11dc1c5eca937a0e8aa20b71ea6ec0436a7d4139cb2294feb25f72037ab3 -size 454206 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null_2,NEXUS_5,1.0,en].png deleted file mode 100644 index 4e60fe09b0..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:887b8771ed039e5e4b8da51a567e73a886606497d0dd4906a89513ea5e3ce3e7 -size 470644 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null_3,NEXUS_5,1.0,en].png deleted file mode 100644 index b78df10dc4..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenDarkPreview_0_null_3,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7f7742fadef66ce01fffab3b8ba7ce2b7ac81977300b9bf541f7d10d3dd7650b -size 442213 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index df45da5c3e..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c809d004a829c2bc6b900e139c0c1e7379249403741646ad92bc1c2c9f248903 -size 354381 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index eece7a0c8c..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:15c75a82f4cd2e26108898622eaba99a2720fe9733d12c418b2eaacde009d9bb -size 344302 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null_2,NEXUS_5,1.0,en].png deleted file mode 100644 index 07c6c62b2e..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b9e4204700497f29a073ff238f5d42b1373e346a995e242861ec2ba1ebd6f606 -size 356483 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null_3,NEXUS_5,1.0,en].png deleted file mode 100644 index 3a9b54566e..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.onboarding.impl_null_DefaultGroup_OnBoardingScreenLightPreview_0_null_3,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:726a2c5fcf7d7e6b24799ad62bcd58872730493be0fd8282aab7884e2832d7ac -size 336263 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomHugePreview-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomHugePreview-D_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 820a5f9889..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomHugePreview-D_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c834401ae22ca7ceecd0d92cd0aadaed9e3375b384b295c1e0c4f59d3184a642 -size 51603 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomHugePreview-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomHugePreview-N_1_null,NEXUS_5,1.0,en].png deleted file mode 100644 index de08bb030c..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomHugePreview-N_1_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ad7fd75f0b2bf8bb9c3c3b38e6a30822cb8732a80228040d3e5046851662ac9a -size 44271 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomLargePreview-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomLargePreview-D_0_null,NEXUS_5,1.0,en].png index 1f19827181..820a5f9889 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomLargePreview-D_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomLargePreview-D_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1961539c86a8428964185b2ff42753379270ae1ed34e5139e37866b0545aab8 -size 28823 +oid sha256:c834401ae22ca7ceecd0d92cd0aadaed9e3375b384b295c1e0c4f59d3184a642 +size 51603 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomLargePreview-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomLargePreview-N_1_null,NEXUS_5,1.0,en].png index 6bd0d2f75e..de08bb030c 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomLargePreview-N_1_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomLargePreview-N_1_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a807630080b6bdc978997ea9678f7e1ff4df69b20e409b19f4b0c3a6d061de8 -size 25175 +oid sha256:ad7fd75f0b2bf8bb9c3c3b38e6a30822cb8732a80228040d3e5046851662ac9a +size 44271 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomPreview-D_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomPreview-D_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 4a98068e90..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomPreview-D_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2b19b24fc94f200579827f66557a183842d5234881ed84fe2b8b74d935b90666 -size 22697 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomPreview-N_1_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomPreview-N_1_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 02c167a01b..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_ElementLogoAtomPreview-N_1_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e35ea20cabe37c05a594bce1b6b4a3c2175470408c18db25874ad5db088f733f -size 21219 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.pages_null_DefaultGroup_OnBoardingPageDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.pages_null_DefaultGroup_OnBoardingPageDarkPreview_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index b25a0b30a6..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.pages_null_DefaultGroup_OnBoardingPageDarkPreview_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:709d08d911e60ef3ec443a347a2824ef42fccc15749903ed3492fa980869c505 -size 430087 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.pages_null_DefaultGroup_OnBoardingPageLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.pages_null_DefaultGroup_OnBoardingPageLightPreview_0_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 9e191589cc..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.pages_null_DefaultGroup_OnBoardingPageLightPreview_0_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:65b33cc70bdfd18f94ea77fe39e6bb6e0595b7fe68583ed88c891baf0b907743 -size 295653 From e7cab7ac1d298f25934a470cb447e60cd2ab916e Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Tue, 18 Jul 2023 15:11:11 +0200 Subject: [PATCH 55/59] Make the functions in SystemUtils extensions (#899) - They are now all extensions over `Context` or `Activity` (when `Context` is not enough) (some of them already were). - Allows for IDE completion. --- .../android/appnav/loggedin/LoggedInView.kt | 7 +- .../roomdetails/impl/RoomDetailsNode.kt | 6 +- .../members/details/RoomMemberDetailsNode.kt | 3 +- .../androidutils/system/SystemUtils.kt | 72 +++++++++---------- .../deeplink/usecase/InviteFriendsUseCase.kt | 3 +- .../channels/NotificationChannels.kt | 6 +- 6 files changed, 42 insertions(+), 55 deletions(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt index 3f415c3dc1..dc1faa0e2d 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt @@ -16,7 +16,6 @@ package io.element.android.appnav.loggedin -import android.app.Activity import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -32,14 +31,12 @@ fun LoggedInView( state: LoggedInState, modifier: Modifier = Modifier ) { - val activity = LocalContext.current as? Activity + val context = LocalContext.current PermissionsView( state = state.permissionsState, modifier = modifier, - openSystemSettings = { - activity?.let { openAppSettingsPage(it) } - } + openSystemSettings = context::openAppSettingsPage ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt index 4eb70d96e3..b74cf7aaf1 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt @@ -76,8 +76,7 @@ class RoomDetailsNode @AssistedInject constructor( val permalinkResult = alias?.let { PermalinkBuilder.permalinkForRoomAlias(it) } ?: PermalinkBuilder.permalinkForRoomId(room.roomId) permalinkResult.onSuccess { permalink -> - startSharePlainTextIntent( - context = context, + context.startSharePlainTextIntent( activityResultLauncher = null, chooserTitle = context.getString(R.string.screen_room_details_share_room_title), text = permalink, @@ -91,8 +90,7 @@ class RoomDetailsNode @AssistedInject constructor( private fun onShareMember(context: Context, member: RoomMember) { val permalinkResult = PermalinkBuilder.permalinkForUser(member.userId) permalinkResult.onSuccess { permalink -> - startSharePlainTextIntent( - context = context, + context.startSharePlainTextIntent( activityResultLauncher = null, chooserTitle = context.getString(R.string.screen_room_details_share_room_title), text = permalink, diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt index 5425fdf79a..54b86f973c 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt @@ -68,8 +68,7 @@ class RoomMemberDetailsNode @AssistedInject constructor( fun onShareUser() { val permalinkResult = PermalinkBuilder.permalinkForUser(inputs.roomMemberId) permalinkResult.onSuccess { permalink -> - startSharePlainTextIntent( - context = context, + context.startSharePlainTextIntent( activityResultLauncher = null, chooserTitle = context.getString(R.string.screen_room_details_share_room_title), text = permalink, diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt index c48d52a1fc..65a5dc9e0d 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt @@ -76,9 +76,9 @@ fun Context.getApplicationLabel(packageName: String): String { /** * Return true it the user has enabled the do not disturb mode. */ -fun isDoNotDisturbModeOn(context: Context): Boolean { +fun Context.isDoNotDisturbModeOn(): Boolean { // We cannot use NotificationManagerCompat here. - val setting = context.getSystemService()!!.currentInterruptionFilter + val setting = getSystemService()!!.currentInterruptionFilter return setting == NotificationManager.INTERRUPTION_FILTER_NONE || setting == NotificationManager.INTERRUPTION_FILTER_ALARMS @@ -92,10 +92,10 @@ fun isDoNotDisturbModeOn(context: Context): Boolean { * will return false and the notification privacy will fallback to "LOW_DETAIL". */ @SuppressLint("BatteryLife") -fun requestDisablingBatteryOptimization(activity: Activity, activityResultLauncher: ActivityResultLauncher) { +fun Context.requestDisablingBatteryOptimization(activityResultLauncher: ActivityResultLauncher) { val intent = Intent() intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS - intent.data = Uri.parse("package:" + activity.packageName) + intent.data = Uri.parse("package:$packageName") activityResultLauncher.launch(intent) } @@ -106,50 +106,48 @@ fun requestDisablingBatteryOptimization(activity: Activity, activityResultLaunch /** * Copy a text to the clipboard, and display a Toast when done. * - * @param context the context + * @receiver the context * @param text the text to copy * @param toastMessage content of the toast message as a String resource. Null for no toast */ -fun copyToClipboard( - context: Context, +fun Context.copyToClipboard( text: CharSequence, toastMessage: String? = null ) { - CopyToClipboardUseCase(context).execute(text) - toastMessage?.let { context.toast(it) } + CopyToClipboardUseCase(this).execute(text) + toastMessage?.let { toast(it) } } /** * Shows notification settings for the current app. * In android O will directly opens the notification settings, in lower version it will show the App settings */ -fun startNotificationSettingsIntent(context: Context, activityResultLauncher: ActivityResultLauncher) { +fun Context.startNotificationSettingsIntent(activityResultLauncher: ActivityResultLauncher) { val intent = Intent() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { intent.action = Settings.ACTION_APP_NOTIFICATION_SETTINGS - intent.putExtra(Settings.EXTRA_APP_PACKAGE, context.packageName) + intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName) } else { intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - intent.data = Uri.fromParts("package", context.packageName, null) + intent.data = Uri.fromParts("package", packageName, null) } activityResultLauncher.launch(intent) } -fun openAppSettingsPage( - activity: Activity, - noActivityFoundMessage: String = activity.getString(R.string.error_no_compatible_app_found), +fun Context.openAppSettingsPage( + noActivityFoundMessage: String = getString(R.string.error_no_compatible_app_found), ) { try { - activity.startActivity( + startActivity( Intent().apply { action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - data = Uri.fromParts("package", activity.packageName, null) + data = Uri.fromParts("package", packageName, null) } ) } catch (activityNotFoundException: ActivityNotFoundException) { - activity.toast(noActivityFoundMessage) + toast(noActivityFoundMessage) } } @@ -157,52 +155,49 @@ fun openAppSettingsPage( * Shows notification system settings for the given channel id. */ @TargetApi(Build.VERSION_CODES.O) -fun startNotificationChannelSettingsIntent(activity: Activity, channelID: String) { +fun Activity.startNotificationChannelSettingsIntent(channelID: String) { if (!supportNotificationChannels()) return val intent = Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS).apply { - putExtra(Settings.EXTRA_APP_PACKAGE, activity.packageName) + putExtra(Settings.EXTRA_APP_PACKAGE, packageName) putExtra(Settings.EXTRA_CHANNEL_ID, channelID) } - activity.startActivity(intent) + startActivity(intent) } -fun startAddGoogleAccountIntent( - context: Context, +fun Context.startAddGoogleAccountIntent( activityResultLauncher: ActivityResultLauncher, - noActivityFoundMessage: String = context.getString(R.string.error_no_compatible_app_found), + noActivityFoundMessage: String = getString(R.string.error_no_compatible_app_found), ) { val intent = Intent(Settings.ACTION_ADD_ACCOUNT) intent.putExtra(Settings.EXTRA_ACCOUNT_TYPES, arrayOf("com.google")) try { activityResultLauncher.launch(intent) } catch (activityNotFoundException: ActivityNotFoundException) { - context.toast(noActivityFoundMessage) + toast(noActivityFoundMessage) } } @RequiresApi(Build.VERSION_CODES.O) -fun startInstallFromSourceIntent( - context: Context, +fun Context.startInstallFromSourceIntent( activityResultLauncher: ActivityResultLauncher, - noActivityFoundMessage: String = context.getString(R.string.error_no_compatible_app_found), + noActivityFoundMessage: String = getString(R.string.error_no_compatible_app_found), ) { val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES) - .setData(Uri.parse(String.format("package:%s", context.packageName))) + .setData(Uri.parse(String.format("package:%s", packageName))) try { activityResultLauncher.launch(intent) } catch (activityNotFoundException: ActivityNotFoundException) { - context.toast(noActivityFoundMessage) + toast(noActivityFoundMessage) } } -fun startSharePlainTextIntent( - context: Context, +fun Context.startSharePlainTextIntent( activityResultLauncher: ActivityResultLauncher?, chooserTitle: String?, text: String, subject: String? = null, extraTitle: String? = null, - noActivityFoundMessage: String = context.getString(R.string.error_no_compatible_app_found), + noActivityFoundMessage: String = getString(R.string.error_no_compatible_app_found), ) { val share = Intent(Intent.ACTION_SEND) share.type = "text/plain" @@ -220,17 +215,16 @@ fun startSharePlainTextIntent( if (activityResultLauncher != null) { activityResultLauncher.launch(intent) } else { - context.startActivity(intent) + startActivity(intent) } } catch (activityNotFoundException: ActivityNotFoundException) { - context.toast(noActivityFoundMessage) + toast(noActivityFoundMessage) } } -fun startImportTextFromFileIntent( - context: Context, +fun Context.startImportTextFromFileIntent( activityResultLauncher: ActivityResultLauncher, - noActivityFoundMessage: String = context.getString(R.string.error_no_compatible_app_found), + noActivityFoundMessage: String = getString(R.string.error_no_compatible_app_found), ) { val intent = Intent(Intent.ACTION_GET_CONTENT).apply { type = "text/plain" @@ -238,7 +232,7 @@ fun startImportTextFromFileIntent( try { activityResultLauncher.launch(intent) } catch (activityNotFoundException: ActivityNotFoundException) { - context.toast(noActivityFoundMessage) + toast(noActivityFoundMessage) } } diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/usecase/InviteFriendsUseCase.kt b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/usecase/InviteFriendsUseCase.kt index 67f7330a0c..8ee94c31fe 100644 --- a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/usecase/InviteFriendsUseCase.kt +++ b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/usecase/InviteFriendsUseCase.kt @@ -37,8 +37,7 @@ class InviteFriendsUseCase @Inject constructor( permalinkResult.fold( onSuccess = { permalink -> val appName = buildMeta.applicationName - startSharePlainTextIntent( - context = activity, + activity.startSharePlainTextIntent( activityResultLauncher = null, chooserTitle = stringProvider.getString(CommonStrings.action_invite_friends), text = stringProvider.getString(CommonStrings.invite_friends_text, appName, permalink), diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt index fc4838e9a3..0624b863ed 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/channels/NotificationChannels.kt @@ -165,15 +165,15 @@ class NotificationChannels @Inject constructor( private fun supportNotificationChannels() = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) fun openSystemSettingsForSilentCategory(activity: Activity) { - startNotificationChannelSettingsIntent(activity, SILENT_NOTIFICATION_CHANNEL_ID) + activity.startNotificationChannelSettingsIntent(SILENT_NOTIFICATION_CHANNEL_ID) } fun openSystemSettingsForNoisyCategory(activity: Activity) { - startNotificationChannelSettingsIntent(activity, NOISY_NOTIFICATION_CHANNEL_ID) + activity.startNotificationChannelSettingsIntent(NOISY_NOTIFICATION_CHANNEL_ID) } fun openSystemSettingsForCallCategory(activity: Activity) { - startNotificationChannelSettingsIntent(activity, CALL_NOTIFICATION_CHANNEL_ID) + activity.startNotificationChannelSettingsIntent(CALL_NOTIFICATION_CHANNEL_ID) } } } From d273dd00ff6727c45ecd9c3e080a73bf7740aff0 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 18 Jul 2023 16:38:36 +0200 Subject: [PATCH 56/59] Media: Clean after pr reviews --- .../impl/actionlist/ActionListView.kt | 5 +-- .../components/event/TimelineItemAudioView.kt | 31 ++++++------------- .../event/TimelineItemEventContentProvider.kt | 2 +- .../event/TimelineItemFileContentProvider.kt | 4 +-- .../event/TimelineItemVideoContentProvider.kt | 2 +- .../designsystem/components/ProgressDialog.kt | 4 +-- 6 files changed, 17 insertions(+), 31 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt index c41f1006c8..fd2ad94345 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt @@ -247,8 +247,6 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif info = AttachmentThumbnailInfo( type = AttachmentThumbnailType.Location, textContent = stringResource(CommonStrings.common_shared_location), - thumbnailSource = null, - blurHash = null, ) ) } @@ -261,7 +259,7 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif info = AttachmentThumbnailInfo( thumbnailSource = event.content.mediaSource, textContent = textContent, - type = AttachmentThumbnailType.File, + type = AttachmentThumbnailType.Image, blurHash = event.content.blurhash, ) ) @@ -290,7 +288,6 @@ private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modif thumbnailSource = event.content.thumbnailSource, textContent = textContent, type = AttachmentThumbnailType.File, - blurHash = null ) ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt index 3cae19e237..f96290cb51 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAudioView.kt @@ -25,22 +25,19 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Attachment import androidx.compose.material.icons.outlined.GraphicEq import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.rotate import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemAudioContentProvider -import io.element.android.libraries.designsystem.preview.ElementPreviewDark -import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.theme.ElementTheme @@ -89,20 +86,12 @@ fun TimelineItemAudioView( } } -@Preview +@DayNightPreviews @Composable -internal fun TimelineItemAudioViewLightPreview(@PreviewParameter(TimelineItemAudioContentProvider::class) content: TimelineItemAudioContent) = - ElementPreviewLight { ContentToPreview(content) } - -@Preview -@Composable -internal fun TimelineItemAudioViewDarkPreview(@PreviewParameter(TimelineItemAudioContentProvider::class) content: TimelineItemAudioContent) = - ElementPreviewDark { ContentToPreview(content) } - -@Composable -private fun ContentToPreview(content: TimelineItemAudioContent) { - TimelineItemAudioView( - content, - extraPadding = noExtraPadding, - ) -} +internal fun TimelineItemAudioViewPreview(@PreviewParameter(TimelineItemAudioContentProvider::class) content: TimelineItemAudioContent) = + ElementPreview { + TimelineItemAudioView( + content, + extraPadding = noExtraPadding, + ) + } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt index 85b739cd80..4c25bdfb23 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemEventContentProvider.kt @@ -26,7 +26,7 @@ class TimelineItemEventContentProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aTimelineItemFileContent("A file.pdf"), + aTimelineItemFileContent(), aTimelineItemFileContent("A bigger name file.pdf"), aTimelineItemFileContent("An even bigger bigger bigger bigger bigger bigger bigger file name which doesn't fit .pdf"), ) @@ -31,7 +31,7 @@ open class TimelineItemFileContentProvider : PreviewParameterProvider Unit = {}, progressIndicator: @Composable () -> Unit = { CircularProgressIndicator( @@ -145,6 +145,6 @@ internal fun ProgressDialogPreview() = ElementThemedPreview { ContentToPreview() @Composable private fun ContentToPreview() { DialogPreview { - ProgressDialogContent(text = "test dialog content") + ProgressDialogContent(text = "test dialog content", isCancellable = true) } } From e98aee9f397a9260a204a424596cd75b59dbd056 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Tue, 18 Jul 2023 14:51:59 +0000 Subject: [PATCH 57/59] Update screenshots --- ...Group_SheetContentPreview-D-0_1_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...Group_SheetContentPreview-D-0_1_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...Group_SheetContentPreview-N-0_2_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...Group_SheetContentPreview-N-0_2_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...elineItemAudioViewPreview-D-7_8_null_0,NEXUS_5,1.0,en].png | 3 +++ ...elineItemAudioViewPreview-D-7_8_null_1,NEXUS_5,1.0,en].png | 3 +++ ...elineItemAudioViewPreview-D-7_8_null_2,NEXUS_5,1.0,en].png | 3 +++ ...elineItemAudioViewPreview-N-7_9_null_0,NEXUS_5,1.0,en].png | 3 +++ ...elineItemAudioViewPreview-N-7_9_null_1,NEXUS_5,1.0,en].png | 3 +++ ...elineItemAudioViewPreview-N-7_9_null_2,NEXUS_5,1.0,en].png | 3 +++ ...tedHistoryBannerViewPreview-D-8_9_null,NEXUS_5,1.0,en].png | 3 +++ ...edHistoryBannerViewPreview-N-8_10_null,NEXUS_5,1.0,en].png | 3 +++ 12 files changed, 32 insertions(+), 8 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-7_8_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-7_8_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-7_8_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-7_9_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-7_9_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-7_9_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-8_9_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-8_10_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentPreview-D-0_1_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentPreview-D-0_1_null_4,NEXUS_5,1.0,en].png index 17f4410180..f9c5109ceb 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentPreview-D-0_1_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentPreview-D-0_1_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a667744cd675695d67f5b402101f2ce72318fcc9311933300a83cf86bf3b2c19 -size 47473 +oid sha256:e456ca95ee33cf14cac839b2b57879d17ca47b156da65c8a870882a90ef2c84c +size 40664 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentPreview-D-0_1_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentPreview-D-0_1_null_5,NEXUS_5,1.0,en].png index 0905a820cb..b0937d3b09 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentPreview-D-0_1_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentPreview-D-0_1_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16e955d418eadb90cf9952d18b87669e03c50d5b166e0fee1d193a733e48372d -size 39775 +oid sha256:e4bcce9d53c7c094698f07de6c6a25be2c7831707581861df4b0cdf0c3d6d1fd +size 40906 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentPreview-N-0_2_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentPreview-N-0_2_null_4,NEXUS_5,1.0,en].png index 9b6329a89b..410fe4da24 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentPreview-N-0_2_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentPreview-N-0_2_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64a8da63cd27373a61e788cdf569e05f1608ab32bebffd2651755d5a3345c37c -size 45752 +oid sha256:66a7d55b40a9d5d7bc02f531bd3c80a1ba96e24a619c123f68549df355019558 +size 38989 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentPreview-N-0_2_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentPreview-N-0_2_null_5,NEXUS_5,1.0,en].png index 579b7a76dd..6cc64a9ea3 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentPreview-N-0_2_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.actionlist_null_DefaultGroup_SheetContentPreview-N-0_2_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c9f0a9aa7cb0b1665eae95564a189c6752c707dce195979076b0504eac497a1 -size 38170 +oid sha256:9bb3eab588d8cc20eb2bcbbcb8a7acdf6431299bf575a124ed7aff8a4fe6cd15 +size 39182 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-7_8_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-7_8_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..53bfbe6d54 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-7_8_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e00510a5f35eb33aaef15c30e6caac62045d8e4c37c032a24922bbb749ad0375 +size 9817 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-7_8_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-7_8_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9ce9173802 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-7_8_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:229d83ed02804137ab1dad4114eecc6d52d6a0e3ccbad436cf226f6cfc628cf7 +size 12200 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-7_8_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-7_8_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..77a65f3b40 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-7_8_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:27fff9f0ea88cf06934298ea6155cbf4dd49c370cc5d0a14b4d387ac8a7e7c39 +size 23203 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-7_9_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-7_9_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c02a4d8bf8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-7_9_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba5f5fefedaaf994fb2eae3df38724b245e27a1b03d48cb20d33f7ca49caa356 +size 9458 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-7_9_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-7_9_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d636a6b668 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-7_9_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19ce1d8ddd69760417e267cda7d4663b44d7a50f8ef1a5617497e1135f8aa586 +size 11500 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-7_9_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-7_9_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6db46605ae --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-7_9_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2e589a013be7a4b54e3f91816565dd38a05086c19d9832480713d895b1462e72 +size 20864 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-8_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-8_9_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..45593d6af2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-8_9_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2c806ee0293d94ab1c882b0f0ceaac2c7ac69d587f47cc22b4d16bc331351a2e +size 14711 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-8_10_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-8_10_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c74bbe95f8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-8_10_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77c97c6c99943858530219234356459a7f0a88774332a4f11cd738b92ee15c91 +size 14200 From 1b7aa4dd93f4c0a5ec623991971619c3b7fdb2fc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 18 Jul 2023 18:26:03 +0200 Subject: [PATCH 58/59] Change bubble width ration to 85% (#904) --- .../messages/impl/timeline/components/MessageEventBubble.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt index 339b6a3415..932dce913c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt @@ -51,8 +51,8 @@ import io.element.android.libraries.theme.ElementTheme private val BUBBLE_RADIUS = 12.dp private val BUBBLE_INCOMING_OFFSET = 16.dp -// Design says: The maximum width of a bubble is still 3/4 of the screen width -private const val BUBBLE_WIDTH_RATIO = 0.75f +// Design says: The maximum width of a bubble is still 3/4 of the screen width. But try with 85% now. +private const val BUBBLE_WIDTH_RATIO = 0.85f @OptIn(ExperimentalFoundationApi::class) @Composable From 913527670f8d23b24927f0d11c3cce1563722bdd Mon Sep 17 00:00:00 2001 From: ElementBot Date: Tue, 18 Jul 2023 16:38:41 +0000 Subject: [PATCH 59/59] Update screenshots --- ...elineItemAudioViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 --- ...elineItemAudioViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 --- ...elineItemAudioViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 --- ...elineItemAudioViewPreview-D-7_8_null_0,NEXUS_5,1.0,en].png | 3 --- ...elineItemAudioViewPreview-D-7_8_null_1,NEXUS_5,1.0,en].png | 3 --- ...elineItemAudioViewPreview-D-7_8_null_2,NEXUS_5,1.0,en].png | 3 --- ...lineItemAudioViewPreview-D-8_9_null_0,NEXUS_5,1.0,en].png} | 0 ...lineItemAudioViewPreview-D-8_9_null_1,NEXUS_5,1.0,en].png} | 0 ...lineItemAudioViewPreview-D-8_9_null_2,NEXUS_5,1.0,en].png} | 0 ...ineItemAudioViewPreview-N-8_10_null_0,NEXUS_5,1.0,en].png} | 0 ...ineItemAudioViewPreview-N-8_10_null_1,NEXUS_5,1.0,en].png} | 0 ...ineItemAudioViewPreview-N-8_10_null_2,NEXUS_5,1.0,en].png} | 0 ...ItemLocationViewPreview-D-9_10_null_0,NEXUS_5,1.0,en].png} | 0 ...ItemLocationViewPreview-D-9_10_null_1,NEXUS_5,1.0,en].png} | 0 ...ItemLocationViewPreview-N-9_11_null_0,NEXUS_5,1.0,en].png} | 0 ...ItemLocationViewPreview-N-9_11_null_1,NEXUS_5,1.0,en].png} | 0 ...HistoryBannerViewPreview-D-10_11_null,NEXUS_5,1.0,en].png} | 0 ...edHistoryBannerViewPreview-D-9_10_null,NEXUS_5,1.0,en].png | 3 --- ...HistoryBannerViewPreview-N-10_12_null,NEXUS_5,1.0,en].png} | 0 ...edHistoryBannerViewPreview-N-9_11_null,NEXUS_5,1.0,en].png | 3 --- ...TimelineItemEventRowDarkPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...imelineItemEventRowLightPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...mEventRowTimestampDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...mEventRowTimestampDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...mEventRowTimestampDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...mEventRowTimestampDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...EventRowTimestampLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...EventRowTimestampLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...EventRowTimestampLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...EventRowTimestampLightPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...RowWithManyReactionsDarkPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...owWithManyReactionsLightPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...temEventRowWithReplyDarkPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...emEventRowWithReplyLightPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewPreview-D-2_3_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewPreview-D-2_3_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewPreview-D-2_3_null_7,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewPreview-N-2_4_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewPreview-N-2_4_null_6,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewPreview-N-2_4_null_7,NEXUS_5,1.0,en].png | 4 ++-- 40 files changed, 40 insertions(+), 64 deletions(-) delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-7_8_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-7_8_null_1,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-7_8_null_2,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewLightPreview_0_null_0,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-8_9_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewLightPreview_0_null_1,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-8_9_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewLightPreview_0_null_2,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-8_9_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-7_9_null_0,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-8_10_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-7_9_null_1,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-8_10_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-7_9_null_2,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-8_10_null_2,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-8_9_null_0,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-9_10_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-8_9_null_1,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-9_10_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-8_10_null_0,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-9_11_null_0,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-8_10_null_1,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-9_11_null_1,NEXUS_5,1.0,en].png} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-8_9_null,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-10_11_null,NEXUS_5,1.0,en].png} (100%) delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-9_10_null,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-8_10_null,NEXUS_5,1.0,en].png => io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-10_12_null,NEXUS_5,1.0,en].png} (100%) delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-9_11_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index e0136f4c31..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6b83378cdfb20b5ffc781fc514c5a4d3aaa0714101b12a572f032f92ec13180c -size 9776 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index 9f9e9a32fc..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:40d1ea7620f7f830f52b87fbe32d39d4c01ddeeeb10535da17f7207acc80a1d6 -size 11821 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png deleted file mode 100644 index 179c04456a..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:11e2a8b0a79c56b379d387a3987362d5309e7f30903b940994ed43217de1380d -size 21441 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-7_8_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-7_8_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index 53bfbe6d54..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-7_8_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e00510a5f35eb33aaef15c30e6caac62045d8e4c37c032a24922bbb749ad0375 -size 9817 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-7_8_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-7_8_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index 9ce9173802..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-7_8_null_1,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:229d83ed02804137ab1dad4114eecc6d52d6a0e3ccbad436cf226f6cfc628cf7 -size 12200 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-7_8_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-7_8_null_2,NEXUS_5,1.0,en].png deleted file mode 100644 index 77a65f3b40..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-7_8_null_2,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:27fff9f0ea88cf06934298ea6155cbf4dd49c370cc5d0a14b4d387ac8a7e7c39 -size 23203 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-8_9_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewLightPreview_0_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-8_9_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-8_9_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewLightPreview_0_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-8_9_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-8_9_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewLightPreview_0_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-D-8_9_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-7_9_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-8_10_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-7_9_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-8_10_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-7_9_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-8_10_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-7_9_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-8_10_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-7_9_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-8_10_null_2,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-7_9_null_2,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemAudioViewPreview-N-8_10_null_2,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-8_9_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-9_10_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-8_9_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-9_10_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-8_9_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-9_10_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-8_9_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-D-9_10_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-8_10_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-9_11_null_0,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-8_10_null_0,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-9_11_null_0,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-8_10_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-9_11_null_1,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-8_10_null_1,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemLocationViewPreview-N-9_11_null_1,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-8_9_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-10_11_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-8_9_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-10_11_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-9_10_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-9_10_null,NEXUS_5,1.0,en].png deleted file mode 100644 index 45593d6af2..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-D-9_10_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2c806ee0293d94ab1c882b0f0ceaac2c7ac69d587f47cc22b4d16bc331351a2e -size 14711 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-8_10_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-10_12_null,NEXUS_5,1.0,en].png similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-8_10_null,NEXUS_5,1.0,en].png rename to tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-10_12_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-9_11_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-9_11_null,NEXUS_5,1.0,en].png deleted file mode 100644 index c74bbe95f8..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.virtual_null_DefaultGroup_TimelineEncryptedHistoryBannerViewPreview-N-9_11_null,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:77c97c6c99943858530219234356459a7f0a88774332a4f11cd738b92ee15c91 -size 14200 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowDarkPreview_0_null,NEXUS_5,1.0,en].png index b0cc7b0c99..3fc8c841f2 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowDarkPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:341bd6a72040547c9f8e30574dcf4cf416bca3f2581c8ecd460d4291d3eb1974 -size 138376 +oid sha256:38c6d3f4a47ed89c3deb4150024b49c0a533a859f21a1592a82309b8c7316ea4 +size 152242 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowLightPreview_0_null,NEXUS_5,1.0,en].png index ffb615dd5d..b204098bdc 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowLightPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowLightPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4e86c185d1b669c3c15614054f877fbf2219a2e700d36a765664ac132d148fe7 -size 142142 +oid sha256:f82840398e396e038ad68a5fe855394a0088c413e7aade339998b8ed63fb1c09 +size 157273 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampDarkPreview_0_null_0,NEXUS_5,1.0,en].png index ccab977550..65dc22a697 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2531a3845cd5136fd543a90ad05d497eda31a9bd8662a1e0ea7d503d6cbe76a9 -size 62525 +oid sha256:527dcf9001131276820a7853ae40a91e906ce9398609e439328991beb75bdcbc +size 62184 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampDarkPreview_0_null_1,NEXUS_5,1.0,en].png index 0cfee72e19..62f22bc84c 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aab12d61c532d72ce9b2cae609c4161976500032b2b432b961cda5652595e9fc -size 64624 +oid sha256:116d47547b64ac8f5403acfb79b1e1f0cdb6f8de6dafb3a5ca59351a6fa8cc4e +size 64207 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampDarkPreview_0_null_2,NEXUS_5,1.0,en].png index 369271eb0a..3b0e147157 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec8841fe56779bd507a130cb527a045ea2cad4da922f7828f5795dda4d10445d -size 68965 +oid sha256:ab3ce102aa2a3be65e52d26f261c5860f022bd0755c8069ff86c7e75bfe6952c +size 68744 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampDarkPreview_0_null_3,NEXUS_5,1.0,en].png index dcf519fafb..c5800c7bba 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampDarkPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef0bb4d881ed1113c8c0ad320cf3c8f220624514afae63ce82364e3b2c98c907 -size 70923 +oid sha256:08479dcbaaa215af9df4d8bf2d257b0eb3a33da55dcb449ea5b6441972765636 +size 70616 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampLightPreview_0_null_0,NEXUS_5,1.0,en].png index b4a6554906..4e647ccf76 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c8fbdf767801956f2211c6b3425dd48e0c891ceda7c32d8e932cc4e68bba188 -size 64393 +oid sha256:37fadb92faac0c5f85e1e198170d4e10b2b7d54bb3d97e3efd3a0c02e4e6f609 +size 63795 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampLightPreview_0_null_1,NEXUS_5,1.0,en].png index 12872a15de..0649ba7e14 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38d8b6b08174f0735c3e1ad0aa48ea4eed5d3c59d2ef58f1eb6016af454e51fa -size 66925 +oid sha256:90d0cb45a49afc59df03cd4caa6bab215560cbd13c259d25a7ec7990e551df91 +size 66397 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampLightPreview_0_null_2,NEXUS_5,1.0,en].png index 3a3751ad4e..c379b54986 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c48bd869248350abe61e639a9201a68905364aa9ac94c5205daa27e31500614e -size 71261 +oid sha256:86b912e31d126699fa2e7858d78e703676ed220f159994b2e8ea885c07054a43 +size 70816 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampLightPreview_0_null_3,NEXUS_5,1.0,en].png index dcd475e4e2..a4535fed6e 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampLightPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowTimestampLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:984a26ad164ef50fd356184662b9e25c8d9e1d7a277e47fdb6222877a52edc26 -size 74019 +oid sha256:e830ca620428646fc06ff1682354445bf4afc9566c55c7ec56fa87aca4cb4167 +size 73556 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowWithManyReactionsDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowWithManyReactionsDarkPreview_0_null,NEXUS_5,1.0,en].png index 1f693c8394..d389368d7e 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowWithManyReactionsDarkPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowWithManyReactionsDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:10ca9141ab5b383d5fdc73ffff878663706d56cf96bfc3f4dfb47a5c102e460b -size 84109 +oid sha256:7c5ef7519aef3badeda3983b2ec5474f31404bc06b4b46b9829848dc7884fe1d +size 81749 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowWithManyReactionsLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowWithManyReactionsLightPreview_0_null,NEXUS_5,1.0,en].png index cced93b05a..722a25f340 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowWithManyReactionsLightPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowWithManyReactionsLightPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ccd377cc8c709cc6dc963224ce1937b3d72a39281327ed917791441817df979b -size 87831 +oid sha256:c9e9b769422e4714e87ace97058e5a8f064e3b927e3a7c6042494de8381d6b09 +size 85856 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowWithReplyDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowWithReplyDarkPreview_0_null,NEXUS_5,1.0,en].png index 0b3fe672d0..f2add007e5 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowWithReplyDarkPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowWithReplyDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63fb78f8b81e7a621223b6fb9d133cd66edde2dd837e8213d0195d253abb5d93 -size 119401 +oid sha256:4970830b042e7b6588c03bf632ae3ade5b39de7339e27618ee341cbd9861c0e9 +size 129351 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowWithReplyLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowWithReplyLightPreview_0_null,NEXUS_5,1.0,en].png index 507bb5e3aa..a048edf7d8 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowWithReplyLightPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineItemEventRowWithReplyLightPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c04acca24e2da6eb9b04e63d9ec4db7f9620e85a6ceb65190a8ab96dd6fee51 -size 124793 +oid sha256:4aba48e03e8424e5ff131c22fd5a606280745dd318f5822e2167f3b39794ff41 +size 134569 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_5,NEXUS_5,1.0,en].png index 96270c69ae..80eeb03c19 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:14ecdfd226a25743e3fac8850318216d6ec193755fa7559fd5523236b7835bc6 -size 89754 +oid sha256:dc695bfc589e8e5a852eeb9b2828ce968d17519bb5be957cf2734a7d6d9cc356 +size 89482 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_6,NEXUS_5,1.0,en].png index b62d934c0c..727cfa4b3c 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d08de923f29ec6c3c2bf735ca0d015ca1097c95484119d1d39bb8c3f93f31100 -size 363776 +oid sha256:9efb4310c2eed085f8f72be66d725c628e07373cd18c262c18be510d7b042f69 +size 393373 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_7,NEXUS_5,1.0,en].png index 3e79fb521a..9c29642146 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-D-2_3_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e8832da336aea6c7ebb3621668569ff16238042fb9650afcd938594a59fd3ae -size 322771 +oid sha256:19982d091ffcb15a422e75461adb3d039775645eaf74b6bfd4a3ee617dd4555f +size 347932 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_5,NEXUS_5,1.0,en].png index 92bbf1dd70..fdb8345d8c 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f729f89dce978bb9e061826389e0db1af0e18835e4cd1ab6cbfaa31f4fd3152f -size 84942 +oid sha256:167f8368e0b94aa05e5d1cc30055e3b0c2c910752d719092ef2d34aae09dc832 +size 84649 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_6,NEXUS_5,1.0,en].png index ace73de65e..2a6187d9a3 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_6,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_6,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:03a4b62920af61098e850cfca6a6f43acd7b310094fb70e1c3e4b7e29465e244 -size 173851 +oid sha256:6ee754e789926bfd12212a21b6ff882ffca337d6903125ba5e1d7cd0c1c218ec +size 189364 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_7,NEXUS_5,1.0,en].png index afb2440466..a65dbca59c 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_7,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewPreview-N-2_4_null_7,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ebcbb40acf19fb489f6a8068a22af0b79c304be46863e77fa12b3b333065a1b2 -size 164622 +oid sha256:29f9ba8c19287b037f67aec5af4469174eb878b7ed5baf222f66e159faee6e16 +size 178410