From ac108c45823cb04303cddd14f3c2ab48c498878c Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 21 Jul 2023 10:20:37 +0200 Subject: [PATCH 01/14] Introduce Disposable extension to destroy all disposable in an Iterable --- .../libraries/matrix/impl/util/Disposables.kt | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/Disposables.kt diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/Disposables.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/Disposables.kt new file mode 100644 index 0000000000..ac92a2e026 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/util/Disposables.kt @@ -0,0 +1,24 @@ +/* + * 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.util + +import org.matrix.rustcomponents.sdk.Disposable + +/** + * Call destroy on all elements of the iterable. + */ +internal fun Iterable.destroyAll() = forEach { it.destroy() } From a87ae86398921441a28d5a3ee7a4bf2e9f7ed0e0 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 21 Jul 2023 10:24:57 +0200 Subject: [PATCH 02/14] Deadlock: makes sure timelineListener TaskHandle.cancel is called (and memory is released correctly) --- .../matrix/impl/room/RustMatrixRoom.kt | 8 +++--- .../impl/timeline/RoomTimelineExtensions.kt | 25 ++++++++++++++++--- .../impl/timeline/RustMatrixTimeline.kt | 9 ++++--- 3 files changed, 32 insertions(+), 10 deletions(-) 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 16434aa3c8..b185c34205 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 @@ -122,10 +122,12 @@ class RustMatrixRoom( innerRoom.timelineDiffFlow { initialList -> _timeline.postItems(initialList) }.onEach { diff -> - if (diff.eventOrigin() == EventItemOrigin.SYNC) { - _syncUpdateFlow.value = systemClock.epochMillis() + diff.use { + if (diff.eventOrigin() == EventItemOrigin.SYNC) { + _syncUpdateFlow.value = systemClock.epochMillis() + } + _timeline.postDiff(diff) } - _timeline.postDiff(diff) }.launchIn(this) innerRoom.backPaginationStatusFlow() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt index d6febb32dc..29b75a1dca 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RoomTimelineExtensions.kt @@ -16,28 +16,45 @@ package io.element.android.libraries.matrix.impl.timeline +import io.element.android.libraries.matrix.impl.util.destroyAll import io.element.android.libraries.matrix.impl.util.mxCallbackFlow import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.trySendBlocking import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.buffer +import kotlinx.coroutines.flow.callbackFlow import org.matrix.rustcomponents.sdk.BackPaginationStatus import org.matrix.rustcomponents.sdk.BackPaginationStatusListener import org.matrix.rustcomponents.sdk.Room +import org.matrix.rustcomponents.sdk.RoomTimelineListenerResult import org.matrix.rustcomponents.sdk.TimelineDiff import org.matrix.rustcomponents.sdk.TimelineItem import org.matrix.rustcomponents.sdk.TimelineListener +import timber.log.Timber internal fun Room.timelineDiffFlow(onInitialList: suspend (List) -> Unit): Flow = - mxCallbackFlow { + callbackFlow { + val roomId = id() + Timber.d("Open timelineDiffFlow for room $roomId") val listener = object : TimelineListener { override fun onUpdate(diff: TimelineDiff) { trySendBlocking(diff) } } - val result = addTimelineListener(listener) - onInitialList(result.items) - result.itemsStream + var result: RoomTimelineListenerResult? = null + try { + result = addTimelineListener(listener) + onInitialList(result.items) + } catch (exception: Exception) { + Timber.d(exception, "Catch failure in timelineDiffFlow of room $roomId") + } + awaitClose { + Timber.d("Close timelineDiffFlow for room $roomId") + result?.itemsStream?.cancel() + result?.itemsStream?.destroy() + result?.items?.destroyAll() + } }.buffer(Channel.UNLIMITED) internal fun Room.backPaginationStatusFlow(): Flow = 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 e213fb623c..5e7f4a2282 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 @@ -26,12 +26,14 @@ import io.element.android.libraries.matrix.impl.timeline.item.event.EventMessage 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.CompletableDeferred import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -46,8 +48,8 @@ import org.matrix.rustcomponents.sdk.Room 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 +import java.util.concurrent.atomic.AtomicBoolean private const val INITIAL_MAX_SIZE = 50 @@ -99,9 +101,10 @@ class RustMatrixTimeline( encryptedHistoryPostProcessor.process(items) } - internal suspend fun postItems(items: List) { + internal suspend fun postItems(items: List) = coroutineScope { // Split the initial items in multiple list as there is no pagination in the cached data, so we can post timelineItems asap. items.chunked(INITIAL_MAX_SIZE).reversed().forEach { + ensureActive() timelineDiffProcessor.postItems(it) } isInit.set(true) From c0b8388fadacbb7a251c53cf977a43151fdc0a01 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 21 Jul 2023 14:12:54 +0200 Subject: [PATCH 03/14] Session.getRoom : suspend the whole method --- .../libraries/matrix/impl/RustMatrixClient.kt | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) 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 640e0772a9..c0bdcbb77e 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 @@ -150,7 +150,7 @@ class RustMatrixClient constructor( }.launchIn(sessionCoroutineScope) } - override suspend fun getRoom(roomId: RoomId): MatrixRoom? { + override suspend fun getRoom(roomId: RoomId): MatrixRoom? = withContext(sessionDispatcher) { // Check if already in memory... var cachedPairOfRoom = pairOfRoom(roomId) if (cachedPairOfRoom == null) { @@ -158,24 +158,27 @@ class RustMatrixClient constructor( roomSummaryDataSource.awaitAllRoomsAreLoaded() cachedPairOfRoom = pairOfRoom(roomId) } - if (cachedPairOfRoom == null) return null - val (roomListItem, fullRoom) = cachedPairOfRoom - return RustMatrixRoom( - sessionId = sessionId, - roomListItem = roomListItem, - innerRoom = fullRoom, - sessionCoroutineScope = sessionCoroutineScope, - coroutineDispatchers = dispatchers, - systemClock = clock, - roomContentForwarder = roomContentForwarder, - sessionData = sessionStore.getSession(sessionId.value)!!, - ) + return@withContext if (cachedPairOfRoom == null) { + null + } else { + val (roomListItem, fullRoom) = cachedPairOfRoom + RustMatrixRoom( + sessionId = sessionId, + roomListItem = roomListItem, + innerRoom = fullRoom, + sessionCoroutineScope = sessionCoroutineScope, + coroutineDispatchers = dispatchers, + systemClock = clock, + roomContentForwarder = roomContentForwarder, + sessionData = sessionStore.getSession(sessionId.value)!!, + ) + } } - private suspend fun pairOfRoom(roomId: RoomId): Pair? = withContext(sessionDispatcher) { + private fun pairOfRoom(roomId: RoomId): Pair? { val cachedRoomListItem = roomListService.roomOrNull(roomId.value) val fullRoom = cachedRoomListItem?.fullRoom() - if (cachedRoomListItem == null || fullRoom == null) { + return if (cachedRoomListItem == null || fullRoom == null) { Timber.d("No room cached for $roomId") null } else { From 2f92203d85329876771fa6f75f4f6050aca6b3ba Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 21 Jul 2023 15:19:19 +0200 Subject: [PATCH 04/14] Room: avoid calling displayName/avatarData on each recomposition --- .../appnav/room/LoadingRoomNodeView.kt | 25 +-------- .../messages/impl/MessagesPresenter.kt | 14 +++-- .../features/messages/impl/MessagesState.kt | 4 +- .../messages/impl/MessagesStateProvider.kt | 8 ++- .../features/messages/impl/MessagesView.kt | 55 +++++++++++++------ .../IconTitlePlaceholdersRowMolecule.kt | 43 +++++++++++++++ 6 files changed, 99 insertions(+), 50 deletions(-) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitlePlaceholdersRowMolecule.kt diff --git a/appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomNodeView.kt b/appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomNodeView.kt index e8d68a3e94..aa01b259de 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomNodeView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/room/LoadingRoomNodeView.kt @@ -16,19 +16,13 @@ package io.element.android.appnav.room -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -37,9 +31,8 @@ import androidx.compose.ui.res.stringResource 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.networkmonitor.api.ui.ConnectivityIndicatorView -import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom +import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreviewDark @@ -48,7 +41,6 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TopAppBar -import io.element.android.libraries.designsystem.theme.placeholderBackground import io.element.android.libraries.theme.ElementTheme import io.element.android.libraries.ui.strings.CommonStrings @@ -103,20 +95,7 @@ private fun LoadingRoomTopBar( BackButton(onClick = onBackClicked) }, title = { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Box( - modifier = Modifier - .size(AvatarSize.TimelineRoom.dp) - .align(Alignment.CenterVertically) - .background(color = ElementTheme.colors.placeholderBackground, shape = CircleShape) - ) - Spacer(modifier = Modifier.width(8.dp)) - PlaceholderAtom(width = 20.dp, height = 7.dp) - Spacer(modifier = Modifier.width(7.dp)) - PlaceholderAtom(width = 45.dp, height = 7.dp) - } + IconTitlePlaceholdersRowMolecule(iconSize = AvatarSize.TimelineRoom.dp) }, windowInsets = WindowInsets(0.dp), ) 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 a0a3e3a286..acaaf54c9e 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 @@ -24,7 +24,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable @@ -76,6 +75,7 @@ import io.element.android.libraries.matrix.ui.room.canSendMessageAsState import io.element.android.libraries.textcomposer.MessageComposerMode import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber class MessagesPresenter @AssistedInject constructor( @@ -109,11 +109,13 @@ class MessagesPresenter @AssistedInject constructor( val syncUpdateFlow = room.syncUpdateFlow.collectAsState() val userHasPermissionToSendMessage by room.canSendMessageAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = 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) { - value = room.avatarData() + var roomName: Async by remember { mutableStateOf(Async.Uninitialized) } + var roomAvatar: Async by remember { mutableStateOf(Async.Uninitialized) } + LaunchedEffect(syncUpdateFlow.value) { + withContext(dispatchers.io) { + roomName = Async.Success(room.displayName) + roomAvatar = Async.Success(room.avatarData()) + } } var hasDismissedInviteDialog by rememberSaveable { mutableStateOf(false) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index 8a067a3a26..a042ec1ac4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -30,8 +30,8 @@ import io.element.android.libraries.matrix.api.core.RoomId @Immutable data class MessagesState( val roomId: RoomId, - val roomName: String, - val roomAvatar: AvatarData, + val roomName: Async, + val roomAvatar: Async, val userHasPermissionToSendMessage: Boolean, val composerState: MessageComposerState, val timelineState: TimelineState, 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 d0ddcf68f4..582cd2e7ab 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 @@ -34,6 +34,10 @@ open class MessagesStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aMessagesState(), + aMessagesState().copy( + roomName = Async.Uninitialized, + roomAvatar = Async.Uninitialized, + ), aMessagesState().copy(hasNetworkConnection = false), aMessagesState().copy(composerState = aMessageComposerState().copy(showAttachmentSourcePicker = true)), aMessagesState().copy(userHasPermissionToSendMessage = false), @@ -43,8 +47,8 @@ open class MessagesStateProvider : PreviewParameterProvider { fun aMessagesState() = MessagesState( roomId = RoomId("!id:domain"), - roomName = "Room name", - roomAvatar = AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom), + roomName = Async.Success("Room name"), + roomAvatar = Async.Success(AvatarData("!id:domain", "Room name", size = AvatarSize.TimelineRoom)), userHasPermissionToSendMessage = true, composerState = aMessageComposerState().copy( text = "Hello", diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 6d8f2792e0..d8a8446c8c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -43,13 +43,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign 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.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListView import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction @@ -64,10 +62,12 @@ import io.element.android.features.messages.impl.timeline.components.retrysendme import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView import io.element.android.libraries.androidutils.ui.hideKeyboard +import io.element.android.libraries.designsystem.atomic.molecules.IconTitlePlaceholdersRowMolecule import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.ProgressDialogType import io.element.android.libraries.designsystem.components.avatar.Avatar import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.preview.ElementPreviewDark @@ -137,8 +137,8 @@ fun MessagesView( Column { ConnectivityIndicatorView(isOnline = state.hasNetworkConnection) MessagesViewTopBar( - roomTitle = state.roomName, - roomAvatar = state.roomAvatar, + roomName = state.roomName.dataOrNull(), + roomAvatar = state.roomAvatar.dataOrNull(), onBackPressed = onBackPressed, onRoomDetailsClicked = onRoomDetailsClicked, ) @@ -289,29 +289,50 @@ fun MessagesViewContent( @OptIn(ExperimentalMaterial3Api::class) @Composable fun MessagesViewTopBar( - roomTitle: String, - roomAvatar: AvatarData, + roomName: String?, + roomAvatar: AvatarData?, modifier: Modifier = Modifier, onRoomDetailsClicked: () -> Unit = {}, onBackPressed: () -> Unit = {}, ) { + @Composable + fun RoomAvatarAndNameRow( + roomName: String, + roomAvatar: AvatarData, + modifier: Modifier = Modifier + ) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + Avatar(roomAvatar) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = roomName, + style = ElementTheme.typography.fontBodyLgMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + TopAppBar( modifier = modifier, navigationIcon = { BackButton(onClick = onBackPressed) }, title = { - Row( - modifier = Modifier.clickable { onRoomDetailsClicked() }, - verticalAlignment = Alignment.CenterVertically - ) { - Avatar(roomAvatar) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = roomTitle, - style = ElementTheme.typography.fontBodyLgMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis + val titleModifier = Modifier.clickable { onRoomDetailsClicked() } + if (roomName != null && roomAvatar != null) { + RoomAvatarAndNameRow( + roomName = roomName, + roomAvatar = roomAvatar, + modifier = titleModifier + ) + } else { + IconTitlePlaceholdersRowMolecule( + iconSize = AvatarSize.TimelineRoom.dp, + modifier = titleModifier ) } }, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitlePlaceholdersRowMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitlePlaceholdersRowMolecule.kt new file mode 100644 index 0000000000..ea98a0283d --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitlePlaceholdersRowMolecule.kt @@ -0,0 +1,43 @@ +package io.element.android.libraries.designsystem.atomic.molecules + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom +import io.element.android.libraries.designsystem.theme.placeholderBackground +import io.element.android.libraries.theme.ElementTheme + +@Composable +fun IconTitlePlaceholdersRowMolecule( + iconSize: Dp, + modifier: Modifier = Modifier, + horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, + verticalAlignment: Alignment.Vertical = Alignment.CenterVertically, +) { + Row( + modifier = modifier, + horizontalArrangement = horizontalArrangement, + verticalAlignment = verticalAlignment, + ) { + Box( + modifier = Modifier + .size(iconSize) + .align(Alignment.CenterVertically) + .background(color = ElementTheme.colors.placeholderBackground, shape = CircleShape) + ) + Spacer(modifier = Modifier.width(8.dp)) + PlaceholderAtom(width = 20.dp, height = 7.dp) + Spacer(modifier = Modifier.width(7.dp)) + PlaceholderAtom(width = 45.dp, height = 7.dp) + } +} From 9cf74eff63c267433069efc512fdd5f84c050877 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 25 Jul 2023 16:02:33 +0200 Subject: [PATCH 05/14] Clean PR --- .../messages/impl/MessagesStateProvider.kt | 8 ++++---- .../IconTitlePlaceholdersRowMolecule.kt | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index 582cd2e7ab..ce50cc138b 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 @@ -34,14 +34,14 @@ open class MessagesStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aMessagesState(), - aMessagesState().copy( - roomName = Async.Uninitialized, - roomAvatar = Async.Uninitialized, - ), aMessagesState().copy(hasNetworkConnection = false), aMessagesState().copy(composerState = aMessageComposerState().copy(showAttachmentSourcePicker = true)), aMessagesState().copy(userHasPermissionToSendMessage = false), aMessagesState().copy(showReinvitePrompt = true), + aMessagesState().copy( + roomName = Async.Uninitialized, + roomAvatar = Async.Uninitialized, + ), ) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitlePlaceholdersRowMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitlePlaceholdersRowMolecule.kt index ea98a0283d..ec8636c759 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitlePlaceholdersRowMolecule.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitlePlaceholdersRowMolecule.kt @@ -1,3 +1,19 @@ +/* + * 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.compose.foundation.background From fa963dcc1190592347083b719efafd49b4bb47ca Mon Sep 17 00:00:00 2001 From: ElementBot Date: Tue, 25 Jul 2023 14:20:05 +0000 Subject: [PATCH 06/14] Update screenshots --- ...tGroup_MessagesViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png | 3 +++ ...Group_MessagesViewLightPreview_0_null_5,NEXUS_5,1.0,en].png | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 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_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_null_DefaultGroup_MessagesViewLightPreview_0_null_5,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_null_DefaultGroup_MessagesViewDarkPreview_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_null_DefaultGroup_MessagesViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f5df0bc94e --- /dev/null +++ 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_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d5f6af4e49ea68ef43a7342910d5f6c72981044ad776ea484adc60fb5578a179 +size 49863 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_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_null_DefaultGroup_MessagesViewLightPreview_0_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b7a9808bc0 --- /dev/null +++ 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_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26aef777d4601de18948ace8c85d5935eb8e008d07b4f3a09f40a416164bbc44 +size 51602 From b96b0b10f525ab35667660e106bb5625556633c1 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 25 Jul 2023 18:37:32 +0200 Subject: [PATCH 07/14] Turbine: introduce consumeItemsUntilTimeout --- tests/testutils/build.gradle.kts | 1 + .../android/tests/testutils/ReceiveTurbine.kt | 44 +++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt diff --git a/tests/testutils/build.gradle.kts b/tests/testutils/build.gradle.kts index d7c17c7895..184bbc418a 100644 --- a/tests/testutils/build.gradle.kts +++ b/tests/testutils/build.gradle.kts @@ -30,4 +30,5 @@ dependencies { implementation(libs.test.junit) implementation(libs.coroutines.test) implementation(projects.libraries.core) + implementation(libs.test.turbine) } diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt new file mode 100644 index 0000000000..e3f7b7139c --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt @@ -0,0 +1,44 @@ +/* + * 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 app.cash.turbine.Event +import app.cash.turbine.ReceiveTurbine +import app.cash.turbine.withTurbineTimeout +import io.element.android.libraries.core.data.tryOrNull +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds + +/** + * Consume all items until timeout is reached waiting for an event. + * The timeout is applied for each event. + * @return the list of consumed items. + */ +suspend fun ReceiveTurbine.consumeItemsUntilTimeout(timeout: Duration = 100.milliseconds): List { + val items = ArrayList() + tryOrNull { + while (true) { + when (val event = withTurbineTimeout(timeout) { awaitEvent() }) { + is Event.Item -> { + items.add(event.value) + } + else -> break + } + } + } + return items +} From bbbee5a6d9fda7e9a6c5daa970679337baa23c63 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 25 Jul 2023 18:37:54 +0200 Subject: [PATCH 08/14] Fix tests --- .../features/messages/impl/MessagesView.kt | 42 +++++++++---------- .../messages/MessagesPresenterTest.kt | 26 ++++-------- 2 files changed, 29 insertions(+), 39 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index d8a8446c8c..c1187fdf7b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -295,27 +295,6 @@ fun MessagesViewTopBar( onRoomDetailsClicked: () -> Unit = {}, onBackPressed: () -> Unit = {}, ) { - @Composable - fun RoomAvatarAndNameRow( - roomName: String, - roomAvatar: AvatarData, - modifier: Modifier = Modifier - ) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically - ) { - Avatar(roomAvatar) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = roomName, - style = ElementTheme.typography.fontBodyLgMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } - TopAppBar( modifier = modifier, navigationIcon = { @@ -340,6 +319,27 @@ fun MessagesViewTopBar( ) } +@Composable +fun RoomAvatarAndNameRow( + roomName: String, + roomAvatar: AvatarData, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier, + verticalAlignment = Alignment.CenterVertically + ) { + Avatar(roomAvatar) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = roomName, + style = ElementTheme.typography.fontBodyLgMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} + @Composable fun CantSendMessageBanner( modifier: Modifier = Modifier, 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 2ff244621d..aeca219b68 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 @@ -65,6 +65,7 @@ import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.tests.testutils.consumeItemsUntilTimeout import io.element.android.tests.testutils.testCoroutineDispatchers import io.mockk.mockk import kotlinx.coroutines.test.TestScope @@ -132,7 +133,6 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitItem() initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Forward, aMessageEvent())) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) @@ -177,7 +177,6 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitItem() initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent(eventId = null))) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) @@ -314,7 +313,6 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitItem() initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.ReportContent, aMessageEvent())) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) @@ -328,10 +326,10 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitItem() initialState.eventSink.invoke(MessagesEvents.Dismiss) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) + } } @@ -342,7 +340,6 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) val initialState = awaitItem() initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Developer, aMessageEvent())) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) @@ -419,9 +416,7 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) - val initialState = awaitItem() - skipItems(1) + val initialState = consumeItemsUntilTimeout().last() initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite)) skipItems(1) val loadingState = awaitItem() @@ -448,9 +443,7 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) - val initialState = awaitItem() - skipItems(1) + val initialState = consumeItemsUntilTimeout().last() initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite)) skipItems(1) val loadingState = awaitItem() @@ -469,9 +462,7 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) - val initialState = awaitItem() - skipItems(1) + val initialState = consumeItemsUntilTimeout().last() initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite)) skipItems(1) val loadingState = awaitItem() @@ -497,9 +488,7 @@ class MessagesPresenterTest { moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test { - skipItems(1) - val initialState = awaitItem() - skipItems(1) + val initialState = consumeItemsUntilTimeout().last() initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite)) skipItems(1) val loadingState = awaitItem() @@ -532,8 +521,9 @@ class MessagesPresenterTest { }.test { // Default value assertThat(awaitItem().userHasPermissionToSendMessage).isTrue() - skipItems(2) + skipItems(1) assertThat(awaitItem().userHasPermissionToSendMessage).isFalse() + cancelAndIgnoreRemainingEvents() } } From ba12fbc9e3275dd1b0ac616cc4166c36c91f2c8a Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 25 Jul 2023 18:41:43 +0200 Subject: [PATCH 09/14] Small change after PR review --- .../android/libraries/matrix/impl/RustMatrixClient.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) 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 c0bdcbb77e..6bfe37045f 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 @@ -158,10 +158,7 @@ class RustMatrixClient constructor( roomSummaryDataSource.awaitAllRoomsAreLoaded() cachedPairOfRoom = pairOfRoom(roomId) } - return@withContext if (cachedPairOfRoom == null) { - null - } else { - val (roomListItem, fullRoom) = cachedPairOfRoom + cachedPairOfRoom?.let { (roomListItem, fullRoom) -> RustMatrixRoom( sessionId = sessionId, roomListItem = roomListItem, From 2c2c23b3a176ba74d63d14bdb2403cda3f14e483 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 26 Jul 2023 12:22:41 +0200 Subject: [PATCH 10/14] Push to understand test failure in CI --- .../android/features/messages/MessagesPresenterTest.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 aeca219b68..a24e5f9ff8 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 @@ -490,11 +490,11 @@ class MessagesPresenterTest { }.test { val initialState = consumeItemsUntilTimeout().last() initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite)) - skipItems(1) - val loadingState = awaitItem() - assertThat(loadingState.inviteProgress.isLoading()).isTrue() - val newState = awaitItem() - assertThat(newState.inviteProgress.isFailure()).isTrue() + val remainingStates = consumeItemsUntilTimeout() + assertThat(remainingStates.size).isEqualTo(3) + assertThat(remainingStates[0].inviteProgress.isLoading()).isFalse() + assertThat(remainingStates[1].inviteProgress.isLoading()).isTrue() + assertThat(remainingStates[2].inviteProgress.isFailure()).isTrue() } } From c6e023b053910a4eb9960f8f2d281b2e45241d72 Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 26 Jul 2023 13:07:11 +0200 Subject: [PATCH 11/14] Add consumeItemsUntilPredicate to check how it goes... --- .../features/messages/MessagesPresenterTest.kt | 14 +++++++++----- .../android/tests/testutils/ReceiveTurbine.kt | 17 ++++++++++++++++- 2 files changed, 25 insertions(+), 6 deletions(-) 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 a24e5f9ff8..b47ac55d35 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 @@ -65,6 +65,7 @@ import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor import io.element.android.libraries.textcomposer.MessageComposerMode +import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.consumeItemsUntilTimeout import io.element.android.tests.testutils.testCoroutineDispatchers import io.mockk.mockk @@ -490,11 +491,14 @@ class MessagesPresenterTest { }.test { val initialState = consumeItemsUntilTimeout().last() initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite)) - val remainingStates = consumeItemsUntilTimeout() - assertThat(remainingStates.size).isEqualTo(3) - assertThat(remainingStates[0].inviteProgress.isLoading()).isFalse() - assertThat(remainingStates[1].inviteProgress.isLoading()).isTrue() - assertThat(remainingStates[2].inviteProgress.isFailure()).isTrue() + val loadingState = consumeItemsUntilPredicate { state -> + state.inviteProgress.isLoading() + }.last() + assertThat(loadingState.inviteProgress.isLoading()).isTrue() + val failureState = consumeItemsUntilPredicate { state -> + state.inviteProgress.isFailure() + }.last() + assertThat(failureState.inviteProgress.isFailure()).isTrue() } } diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt index e3f7b7139c..57aaa7dbfc 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/ReceiveTurbine.kt @@ -24,17 +24,32 @@ import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds /** - * Consume all items until timeout is reached waiting for an event. + * Consume all items until timeout is reached waiting for an event or we receive terminal event. * The timeout is applied for each event. * @return the list of consumed items. */ suspend fun ReceiveTurbine.consumeItemsUntilTimeout(timeout: Duration = 100.milliseconds): List { + return consumeItemsUntilPredicate(timeout) { false } +} + +/** + * Consume items until predicate is true, or timeout is reached waiting for an event, or we receive terminal event. + * The timeout is applied for each event. + * @return the list of consumed items. + */ +suspend fun ReceiveTurbine.consumeItemsUntilPredicate( + timeout: Duration = 100.milliseconds, + predicate: (T) -> Boolean, +): List { val items = ArrayList() tryOrNull { while (true) { when (val event = withTurbineTimeout(timeout) { awaitEvent() }) { is Event.Item -> { items.add(event.value) + if (predicate(event.value)) { + break + } } else -> break } From 9c1f9f47f2326258748da3694f4b6ad3a3fe01c2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 26 Jul 2023 12:41:45 +0200 Subject: [PATCH 12/14] Make some composable private. --- .../android/features/messages/impl/MessagesView.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index c1187fdf7b..f909ac7f1a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -203,7 +203,7 @@ fun MessagesView( } @Composable -fun ReinviteDialog(state: MessagesState) { +private fun ReinviteDialog(state: MessagesState) { if (state.showReinvitePrompt) { ConfirmationDialog( title = stringResource(id = R.string.screen_room_invite_again_alert_title), @@ -240,7 +240,7 @@ private fun AttachmentStateView( } @Composable -fun MessagesViewContent( +private fun MessagesViewContent( state: MessagesState, onMessageClicked: (TimelineItem.Event) -> Unit, onUserDataClicked: (UserId) -> Unit, @@ -288,7 +288,7 @@ fun MessagesViewContent( @OptIn(ExperimentalMaterial3Api::class) @Composable -fun MessagesViewTopBar( +private fun MessagesViewTopBar( roomName: String?, roomAvatar: AvatarData?, modifier: Modifier = Modifier, @@ -320,7 +320,7 @@ fun MessagesViewTopBar( } @Composable -fun RoomAvatarAndNameRow( +private fun RoomAvatarAndNameRow( roomName: String, roomAvatar: AvatarData, modifier: Modifier = Modifier @@ -341,7 +341,7 @@ fun RoomAvatarAndNameRow( } @Composable -fun CantSendMessageBanner( +private fun CantSendMessageBanner( modifier: Modifier = Modifier, ) { Row( From e7f673c5bcc22d24b5b733e1651d394c009d5bd8 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 26 Jul 2023 13:58:04 +0200 Subject: [PATCH 13/14] Add missing preview. --- .../molecules/IconTitlePlaceholdersRowMolecule.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitlePlaceholdersRowMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitlePlaceholdersRowMolecule.kt index ec8636c759..1a10aa2886 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitlePlaceholdersRowMolecule.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitlePlaceholdersRowMolecule.kt @@ -30,6 +30,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import io.element.android.libraries.designsystem.atomic.atoms.PlaceholderAtom +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.preview.DayNightPreviews +import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.theme.placeholderBackground import io.element.android.libraries.theme.ElementTheme @@ -57,3 +60,11 @@ fun IconTitlePlaceholdersRowMolecule( PlaceholderAtom(width = 45.dp, height = 7.dp) } } + +@DayNightPreviews +@Composable +internal fun IconTitlePlaceholdersRowMoleculePreview() = ElementPreview { + IconTitlePlaceholdersRowMolecule( + iconSize = AvatarSize.TimelineRoom.dp, + ) +} From c7d5baa3561d586217db1846760b897438e836fc Mon Sep 17 00:00:00 2001 From: ElementBot Date: Wed, 26 Jul 2023 12:12:39 +0000 Subject: [PATCH 14/14] Update screenshots --- ...PlaceholdersRowMoleculePreview-D_0_null,NEXUS_5,1.0,en].png | 3 +++ ...PlaceholdersRowMoleculePreview-N_1_null,NEXUS_5,1.0,en].png | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.molecules_null_DefaultGroup_IconTitlePlaceholdersRowMoleculePreview-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.molecules_null_DefaultGroup_IconTitlePlaceholdersRowMoleculePreview-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.libraries.designsystem.atomic.molecules_null_DefaultGroup_IconTitlePlaceholdersRowMoleculePreview-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.molecules_null_DefaultGroup_IconTitlePlaceholdersRowMoleculePreview-D_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..08f5a24c49 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.molecules_null_DefaultGroup_IconTitlePlaceholdersRowMoleculePreview-D_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ebb2d33b0f8aa473b9deb44e57df4edec1346f164098eec87163ebd5c8db9ebd +size 5314 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.molecules_null_DefaultGroup_IconTitlePlaceholdersRowMoleculePreview-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.molecules_null_DefaultGroup_IconTitlePlaceholdersRowMoleculePreview-N_1_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..52309e0c28 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.molecules_null_DefaultGroup_IconTitlePlaceholdersRowMoleculePreview-N_1_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b71348d0a316569c389682f5904c2d5b67180bd6c4ae13112d81afdb6cdada6 +size 5273