diff --git a/.github/renovate.json b/.github/renovate.json index f9e1469496..3bc8c7e395 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -1,18 +1,28 @@ { - "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": [ + "$schema" : "https://docs.renovatebot.com/renovate-schema.json", + "extends" : [ "config:base" ], - "labels": ["dependencies"], - "ignoreDeps": ["string:app_name"], - "packageRules": [ + "labels" : [ + "dependencies" + ], + "ignoreDeps" : [ + "string:app_name" + ], + "packageRules" : [ { - "matchPackagePatterns": [ + "matchPackagePatterns" : [ "^org.jetbrains.kotlin", "^com.google.devtools.ksp", "^androidx.compose.compiler" ], - "groupName": "kotlin" + "groupName" : "kotlin" + }, + { + "matchPackageNames" : [ + "org.jetbrains.kotlinx.kover" + ], + "enabled" : false } ] } diff --git a/.github/workflows/recordScreenshots.yml b/.github/workflows/recordScreenshots.yml new file mode 100644 index 0000000000..52da484b9a --- /dev/null +++ b/.github/workflows/recordScreenshots.yml @@ -0,0 +1,29 @@ +name: Record screenshots + +on: + workflow_dispatch: + +# Enrich gradle.properties for CI/CD +env: + GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx3072m -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError" -Dkotlin.daemon.jvm.options="-Xmx2560m" -Dkotlin.incremental=false + +jobs: + record: + name: Record screenshots on branch ${{ inputs.param_branch }} + runs-on: ubuntu-latest + + steps: + - name: ⏬ Checkout with LFS + uses: actions/checkout@v3 + with: + lfs: 'true' + - name: ☕️ Use JDK 17 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' # See 'Supported distributions' for available options + java-version: '17' + - name: Record screenshots + run: "./.github/workflows/scripts/recordScreenshots.sh" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ secrets.GITHUB_REPOSITORY }} diff --git a/.github/workflows/scripts/recordScreenshots.sh b/.github/workflows/scripts/recordScreenshots.sh new file mode 100755 index 0000000000..d8cdbd0c6f --- /dev/null +++ b/.github/workflows/scripts/recordScreenshots.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +# +# 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. +# + +if [[ -z ${GITHUB_TOKEN} ]]; then + echo "Missing GITHUB_TOKEN variable" + exit 1 +fi + +if [[ -z ${GITHUB_REPOSITORY} ]]; then + echo "Missing GITHUB_REPOSITORY variable" + exit 1 +fi + +if [[ -z ${GITHUB_REF_NAME} ]]; then + echo "Missing GITHUB_REF_NAME variable" + exit 1 +fi + +git config user.name "ElementBot" +git config user.email "benoitm+elementbot@element.io" + +echo "Git status" +git status + +echo "Fetching..." +git fetch --all + +echo "Checkout origin/$GITHUB_REF_NAME" +git checkout "origin/$GITHUB_REF_NAME" + +echo "Record screenshots" +./gradlew recordPaparazziDebug --stacktrace -PpreDexEnable=false --max-workers 4 --warn + +echo "Committing changes" +git add -A +git commit -m "Update screenshots" + +echo "Pushing changes" +git push "https://$GITHUB_TOKEN@github.com/$GITHUB_REPOSITORY.git" +echo "Done!" diff --git a/.idea/dictionaries/shared.xml b/.idea/dictionaries/shared.xml index 216a8cbd20..7c04ccd5e7 100644 --- a/.idea/dictionaries/shared.xml +++ b/.idea/dictionaries/shared.xml @@ -2,6 +2,8 @@ backstack + kover + onboarding textfields diff --git a/.maestro/tests/account/login.yaml b/.maestro/tests/account/login.yaml index 3733b2b047..59cc0980e4 100644 --- a/.maestro/tests/account/login.yaml +++ b/.maestro/tests/account/login.yaml @@ -1,6 +1,6 @@ appId: ${APP_ID} --- -- tapOn: "Get started" +- tapOn: "Sign in manually" - runFlow: ../assertions/assertLoginDisplayed.yaml - takeScreenshot: build/maestro/100-SignIn - runFlow: changeServer.yaml diff --git a/.maestro/tests/assertions/assertInitDisplayed.yaml b/.maestro/tests/assertions/assertInitDisplayed.yaml index 0bcef846c6..b68412be84 100644 --- a/.maestro/tests/assertions/assertInitDisplayed.yaml +++ b/.maestro/tests/assertions/assertInitDisplayed.yaml @@ -1,5 +1,5 @@ appId: ${APP_ID} --- - extendedWaitUntil: - visible: "Own your conversations." + visible: "Communicate and collaborate securely" timeout: 10_000 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d4e889adc1..b355d840c4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -142,15 +142,6 @@ android { jvmTarget = "17" } - // Waiting for https://github.com/google/ksp/issues/37 - applicationVariants.all { - kotlin.sourceSets { - getByName(name) { - kotlin.srcDir("build/generated/ksp/$name/kotlin") - } - } - } - buildFeatures { buildConfig = true } 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 5447327152..78c39f93e6 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -93,10 +93,10 @@ class RootFlowNode @AssistedInject constructor( if (isLoggedIn) { tryToRestoreLatestSession( onSuccess = { switchToLoggedInFlow(it) }, - onFailure = { switchToLogoutFlow() } + onFailure = { switchToNotLoggedInFlow() } ) } else { - switchToLogoutFlow() + switchToNotLoggedInFlow() } } .launchIn(lifecycleScope) @@ -106,7 +106,7 @@ class RootFlowNode @AssistedInject constructor( backstack.safeRoot(NavTarget.LoggedInFlow(sessionId)) } - private fun switchToLogoutFlow() { + private fun switchToNotLoggedInFlow() { matrixClientsHolder.removeAll() backstack.safeRoot(NavTarget.NotLoggedInFlow) } diff --git a/build.gradle.kts b/build.gradle.kts index 8657aa7b4c..3fe16b498d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -203,6 +203,8 @@ koverMerged { includes += "*Presenter" excludes += "*Fake*Presenter" excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*" + // Too small presenter, cannot reach the threshold. + excludes += "io.element.android.features.onboarding.impl.OnBoardingPresenter" } bound { minValue = 90 diff --git a/changelog.d/483.feature b/changelog.d/483.feature new file mode 100644 index 0000000000..c1face5530 --- /dev/null +++ b/changelog.d/483.feature @@ -0,0 +1 @@ +Redesign the timeline item context menu using M3 bottom sheet 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 c075d4321e..cbaeb26c93 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 @@ -118,6 +118,8 @@ class MessagesPresenter @Inject constructor( TimelineItemAction.Redact -> handleActionRedact(targetEvent) TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState) TimelineItemAction.Reply -> handleActionReply(targetEvent, composerState) + TimelineItemAction.Developer -> notImplementedYet() + TimelineItemAction.ReportContent -> notImplementedYet() } } 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 3aca34ee2b..579dfeaf5a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -16,7 +16,6 @@ package io.element.android.features.messages.impl -import androidx.activity.compose.BackHandler import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi @@ -32,27 +31,22 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.ListItem -import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.icons.filled.AttachFile -import androidx.compose.material.icons.filled.Collections -import androidx.compose.material.icons.filled.PhotoCamera -import androidx.compose.material.icons.filled.Videocam -import androidx.compose.material.rememberModalBottomSheetState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.SideEffect +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 androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -66,9 +60,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListView import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.messagecomposer.AttachmentsState -import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents import io.element.android.features.messages.impl.messagecomposer.MessageComposerView -import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelineView import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView @@ -80,7 +72,6 @@ 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.IconButton -import io.element.android.libraries.designsystem.theme.components.ModalBottomSheetLayout 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 @@ -91,7 +82,7 @@ import kotlinx.coroutines.launch import timber.log.Timber import io.element.android.libraries.ui.strings.R as StringsR -@OptIn(ExperimentalMaterialApi::class, ExperimentalLayoutApi::class) +@OptIn(ExperimentalLayoutApi::class) @Composable fun MessagesView( state: MessagesState, @@ -103,26 +94,11 @@ fun MessagesView( modifier: Modifier = Modifier, ) { LogCompositions(tag = "MessagesScreen", msg = "Root") - val itemActionsBottomSheetState = rememberModalBottomSheetState( - initialValue = ModalBottomSheetValue.Hidden, - ) - val composerState = state.composerState - val initialBottomSheetState = if (LocalInspectionMode.current && composerState.showAttachmentSourcePicker) { - ModalBottomSheetValue.Expanded - } else { - ModalBottomSheetValue.Hidden - } - val bottomSheetState = rememberModalBottomSheetState(initialValue = initialBottomSheetState) val coroutineScope = rememberCoroutineScope() + var isMessageActionsBottomSheetVisible by rememberSaveable { mutableStateOf(false) } AttachmentStateView(state.composerState.attachmentsState, onPreviewAttachments) - BackHandler(enabled = bottomSheetState.isVisible) { - coroutineScope.launch { - bottomSheetState.hide() - } - } - val snackbarHostState = remember { SnackbarHostState() } val snackbarMessageText = state.snackbarMessage?.let { stringResource(it.messageResId) } if (snackbarMessageText != null) { @@ -150,78 +126,57 @@ fun MessagesView( Timber.v("OnMessageLongClicked= ${event.id}") localView.hideKeyboard() state.actionListState.eventSink(ActionListEvents.ComputeForMessage(event)) - coroutineScope.launch { - itemActionsBottomSheetState.show() - } + isMessageActionsBottomSheetVisible = true } fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) { + isMessageActionsBottomSheetVisible = false state.eventSink(MessagesEvents.HandleAction(action, event)) } - LaunchedEffect(composerState.showAttachmentSourcePicker) { - if (composerState.showAttachmentSourcePicker) { - // We need to use this instead of `LocalFocusManager.clearFocus()` to hide the keyboard when focus is on an Android View - localView.hideKeyboard() - bottomSheetState.show() - } else { - bottomSheetState.hide() - } + fun onDismissActionListBottomSheet() { + isMessageActionsBottomSheetVisible = false } - // Send 'DismissAttachmentMenu' event when the bottomsheet was just hidden - LaunchedEffect(bottomSheetState.isVisible) { - if (!bottomSheetState.isVisible) { - composerState.eventSink(MessageComposerEvents.DismissAttachmentMenu) - } - } - ModalBottomSheetLayout( - sheetState = bottomSheetState, - displayHandle = true, - sheetContent = { - AttachmentSourcePickerMenu( - eventSink = composerState.eventSink - ) - } - ) { - Scaffold( - modifier = modifier, - contentWindowInsets = WindowInsets.statusBars, - topBar = { - Column { - ConnectivityIndicatorView(isOnline = state.hasNetworkConnection) - MessagesViewTopBar( - roomTitle = state.roomName, - roomAvatar = state.roomAvatar, - onBackPressed = onBackPressed, - onRoomDetailsClicked = onRoomDetailsClicked, - ) - } - }, - content = { padding -> - MessagesViewContent( - state = state, - modifier = Modifier - .padding(padding) - .consumeWindowInsets(padding), - onMessageClicked = ::onMessageClicked, - onMessageLongClicked = ::onMessageLongClicked, - onUserDataClicked = onUserDataClicked, - ) - }, - snackbarHost = { - SnackbarHost( - snackbarHostState, - modifier = Modifier.navigationBarsPadding() - ) - }, - ) - ActionListView( - state = state.actionListState, - modalBottomSheetState = itemActionsBottomSheetState, - onActionSelected = ::onActionSelected - ) - } + Scaffold( + modifier = modifier, + contentWindowInsets = WindowInsets.statusBars, + topBar = { + Column { + ConnectivityIndicatorView(isOnline = state.hasNetworkConnection) + MessagesViewTopBar( + roomTitle = state.roomName, + roomAvatar = state.roomAvatar, + onBackPressed = onBackPressed, + onRoomDetailsClicked = onRoomDetailsClicked, + ) + } + }, + content = { padding -> + MessagesViewContent( + state = state, + modifier = Modifier + .padding(padding) + .consumeWindowInsets(padding), + onMessageClicked = ::onMessageClicked, + onMessageLongClicked = ::onMessageLongClicked, + onUserDataClicked = onUserDataClicked, + ) + }, + snackbarHost = { + SnackbarHost( + snackbarHostState, + modifier = Modifier.navigationBarsPadding() + ) + }, + ) + + ActionListView( + state = state.actionListState, + isVisible = isMessageActionsBottomSheetVisible, + onDismiss = ::onDismissActionListBottomSheet, + onActionSelected = ::onActionSelected + ) } @Composable @@ -312,36 +267,6 @@ fun MessagesViewTopBar( ) } -@OptIn(ExperimentalMaterialApi::class) -@Composable -internal fun AttachmentSourcePickerMenu( - eventSink: (MessageComposerEvents) -> Unit, - modifier: Modifier = Modifier, -) { - Column(modifier) { - ListItem( - modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery) }, - icon = { Icon(Icons.Default.Collections, null) }, - text = { Text(stringResource(R.string.screen_room_attachment_source_gallery)) }, - ) - ListItem( - modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) }, - icon = { Icon(Icons.Default.AttachFile, null) }, - text = { Text(stringResource(R.string.screen_room_attachment_source_files)) }, - ) - ListItem( - modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.PhotoFromCamera) }, - icon = { Icon(Icons.Default.PhotoCamera, null) }, - text = { Text(stringResource(R.string.screen_room_attachment_source_camera_photo)) }, - ) - ListItem( - modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.VideoFromCamera) }, - icon = { Icon(Icons.Default.Videocam, null) }, - text = { Text(stringResource(R.string.screen_room_attachment_source_camera_video)) }, - ) - } -} - @Preview @Composable internal fun MessagesViewLightPreview(@PreviewParameter(MessagesStateProvider::class) state: MessagesState) = diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt index 7eb3fbe433..56de0214ec 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListPresenter.kt @@ -26,12 +26,15 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.core.meta.BuildMeta import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import javax.inject.Inject -class ActionListPresenter @Inject constructor() : Presenter { +class ActionListPresenter @Inject constructor( + private val buildMeta: BuildMeta, +) : Presenter { @Composable override fun present(): ActionListState { @@ -60,21 +63,30 @@ class ActionListPresenter @Inject constructor() : Presenter { when (timelineItem.content) { is TimelineItemRedactedContent, is TimelineItemStateContent -> { - // TODO Add Share action (also) here, and developer options - emptyList() - } - else -> { - mutableListOf( - TimelineItemAction.Reply, - TimelineItemAction.Forward, - TimelineItemAction.Copy, - ).also { - if (timelineItem.isMine) { - it.add(TimelineItemAction.Edit) - it.add(TimelineItemAction.Redact) + buildList { + add(TimelineItemAction.Copy) + if (buildMeta.isDebuggable) { + add(TimelineItemAction.Developer) } } } + else -> buildList { + add(TimelineItemAction.Reply) + add(TimelineItemAction.Forward) + if (timelineItem.isMine) { + add(TimelineItemAction.Edit) + } + add(TimelineItemAction.Copy) + if (buildMeta.isDebuggable) { + add(TimelineItemAction.Developer) + } + if (!timelineItem.isMine) { + add(TimelineItemAction.ReportContent) + } + if (timelineItem.isMine) { + add(TimelineItemAction.Redact) + } + } } target.value = ActionListState.Target.Success(timelineItem, actions.toImmutableList()) } 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 9dcabd8af7..86d957e777 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 @@ -16,52 +16,81 @@ package io.element.android.features.messages.impl.actionlist +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.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.navigationBarsPadding +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.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ListItem -import androidx.compose.material.ModalBottomSheetState -import androidx.compose.material.ModalBottomSheetValue import androidx.compose.material.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AddReaction +import androidx.compose.material.icons.outlined.Attachment +import androidx.compose.material.icons.outlined.VideoCameraBack +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.snapshotFlow +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.layout.ContentScale +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +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.TextUnit import androidx.compose.ui.unit.dp import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction +import io.element.android.features.messages.impl.timeline.components.blurhash.BlurHashAsyncImage import io.element.android.features.messages.impl.timeline.model.TimelineItem +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 +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemProfileChangeContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemRedactedContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemStateContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemTextBasedContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemUnknownContent +import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent +import io.element.android.libraries.designsystem.ElementTextStyles +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Divider import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.ModalBottomSheetLayout -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.launch +import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet +import io.element.android.libraries.matrix.ui.media.MediaRequestData +import io.element.android.libraries.ui.strings.R as StringR -@OptIn(ExperimentalMaterialApi::class) +@OptIn(ExperimentalMaterial3Api::class) @Composable fun ActionListView( state: ActionListState, - modalBottomSheetState: ModalBottomSheetState, + isVisible: Boolean, onActionSelected: (action: TimelineItemAction, TimelineItem.Event) -> Unit, + onDismiss: () -> Unit, modifier: Modifier = Modifier ) { - val coroutineScope = rememberCoroutineScope() - LaunchedEffect(modalBottomSheetState) { - snapshotFlow { modalBottomSheetState.currentValue } - .filter { it == ModalBottomSheetValue.Hidden } - .collect { - state.eventSink(ActionListEvents.Clear) - } + LaunchedEffect(isVisible) { + if (!isVisible) { + state.eventSink(ActionListEvents.Clear) + } } fun onItemActionClicked( @@ -69,24 +98,22 @@ fun ActionListView( targetItem: TimelineItem.Event ) { onActionSelected(itemAction, targetItem) - coroutineScope.launch { - modalBottomSheetState.hide() - } } - ModalBottomSheetLayout( - modifier = modifier, - sheetState = modalBottomSheetState, - sheetContent = { + if (isVisible) { + ModalBottomSheet( + onDismissRequest = onDismiss + ) { SheetContent( state = state, onActionClicked = ::onItemActionClicked, - modifier = Modifier - .navigationBarsPadding() - .imePadding() + modifier = modifier + .padding(bottom = 32.dp) +// .navigationBarsPadding() - FIXME after https://issuetracker.google.com/issues/275849044 +// .imePadding() ) } - ) + } } @OptIn(ExperimentalMaterialApi::class) @@ -108,6 +135,19 @@ private fun SheetContent( LazyColumn( modifier = modifier.fillMaxWidth() ) { + item { + Column { + MessageSummary(event = target.event, modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp)) + Spacer(modifier = Modifier.height(14.dp)) + Divider() + } + } + item { + EmojiReactionsRow(Modifier.fillMaxWidth()) + Divider() + } items( items = actions, ) { action -> @@ -135,6 +175,141 @@ private fun SheetContent( } } +@Composable +private fun MessageSummary(event: TimelineItem.Event, modifier: Modifier = Modifier) { + val content: @Composable () -> Unit + var icon: @Composable () -> Unit = { Avatar(avatarData = event.senderAvatar.copy(size = AvatarSize.SMALL)) } + val contentStyle = ElementTextStyles.Regular.bodyMD.copy(color = MaterialTheme.colorScheme.secondary) + val imageModifier = Modifier + .size(36.dp) + .clip(RoundedCornerShape(9.dp)) + + @Composable + fun ContentForBody(body: String) { + Text(body, style = contentStyle, maxLines = 1, overflow = TextOverflow.Ellipsis) + } + + when (event.content) { + is TimelineItemTextBasedContent -> content = { ContentForBody(event.content.body) } + is TimelineItemStateContent -> content = { ContentForBody(event.content.body) } + is TimelineItemProfileChangeContent -> content = { ContentForBody(event.content.body) } + is TimelineItemEncryptedContent -> content = { ContentForBody(stringResource(StringR.string.common_unable_to_decrypt)) } + is TimelineItemRedactedContent -> content = { ContentForBody(stringResource(StringR.string.common_message_removed)) } + is TimelineItemUnknownContent -> content = { ContentForBody(stringResource(StringR.string.common_unsupported_event)) } + is TimelineItemImageContent -> { + icon = { + val mediaRequestData = MediaRequestData( + source = event.content.mediaSource, + kind = MediaRequestData.Kind.Thumbnail(32), + ) + BlurHashAsyncImage( + model = mediaRequestData, + blurHash = event.content.blurhash, + contentDescription = stringResource(StringR.string.common_image), + contentScale = ContentScale.Crop, + modifier = imageModifier, + ) + } + content = { ContentForBody(event.content.body) } + } + is TimelineItemVideoContent -> { + icon = { + val thumbnailSource = event.content.thumbnailSource + if (thumbnailSource != null) { + val mediaRequestData = MediaRequestData( + source = event.content.thumbnailSource, + kind = MediaRequestData.Kind.Thumbnail(32), + ) + BlurHashAsyncImage( + model = mediaRequestData, + blurHash = event.content.blurHash, + contentDescription = stringResource(StringR.string.common_video), + contentScale = ContentScale.Crop, + modifier = imageModifier, + ) + } else { + Box( + modifier = imageModifier.background(MaterialTheme.colorScheme.surface), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Outlined.VideoCameraBack, + contentDescription = stringResource(StringR.string.common_video), + ) + } + } + } + content = { ContentForBody(event.content.body) } + } + is TimelineItemFileContent -> { + icon = { + Box( + modifier = imageModifier.background(MaterialTheme.colorScheme.surface), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Outlined.Attachment, + contentDescription = stringResource(StringR.string.common_file), + modifier = Modifier.rotate(-45f) + ) + } + } + content = { ContentForBody(event.content.body) } + } + } + Row(modifier = modifier) { + icon() + Spacer(modifier = Modifier.width(8.dp)) + Column { + Row { + if (event.senderDisplayName != null) { + Text( + text = event.senderDisplayName, + style = ElementTextStyles.Bold.caption1, + color = MaterialTheme.colorScheme.primary + ) + } + Text( + event.sentTime, + style = ElementTextStyles.Regular.caption2, + color = MaterialTheme.colorScheme.secondary, + textAlign = TextAlign.End, + modifier = Modifier.weight(1f) + ) + } + content() + } + } +} + +@Composable +internal fun EmojiReactionsRow(modifier: Modifier = Modifier) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = modifier.padding(horizontal = 28.dp, vertical = 16.dp) + ) { + // TODO use real emojis, have real interaction + Text("\uD83D\uDC4D", fontSize = 28.dpToSp()) + Text("\uD83D\uDC4E", fontSize = 28.dpToSp()) + Text("\uD83D\uDD25", fontSize = 28.dpToSp()) + Text("❤\uFE0F", fontSize = 28.dpToSp()) + Text("\uD83D\uDC4F", fontSize = 28.dpToSp()) + Icon( + imageVector = Icons.Outlined.AddReaction, + contentDescription = "Emojis", + tint = MaterialTheme.colorScheme.secondary, + modifier = Modifier + .size(24.dp) + .align(Alignment.CenterVertically) + ) + } +} + +@Composable +private fun Int.dpToSp(): TextUnit = with(LocalDensity.current) { + return dp.toSp() +} + @Preview @Composable fun SheetContentLightPreview(@PreviewParameter(ActionListStateProvider::class) state: ActionListState) = @@ -145,14 +320,7 @@ fun SheetContentLightPreview(@PreviewParameter(ActionListStateProvider::class) s fun SheetContentDarkPreview(@PreviewParameter(ActionListStateProvider::class) state: ActionListState) = ElementPreviewDark { ContentToPreview(state) } -@OptIn(ExperimentalMaterialApi::class) @Composable private fun ContentToPreview(state: ActionListState) { - ActionListView( - state = state, - modalBottomSheetState = ModalBottomSheetState( - initialValue = ModalBottomSheetValue.Expanded - ), - onActionSelected = { _, _ -> } - ) + SheetContent(state = state) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt index fc3f114ac4..bc912e3d6e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/model/TimelineItemAction.kt @@ -26,9 +26,11 @@ sealed class TimelineItemAction( @DrawableRes val icon: Int, val destructive: Boolean = false ) { - object Forward : TimelineItemAction("Forward", VectorIcons.ArrowForward) + object Forward : TimelineItemAction("Forward", VectorIcons.Forward) object Copy : TimelineItemAction("Copy", VectorIcons.Copy) object Redact : TimelineItemAction("Redact", VectorIcons.Delete, destructive = true) object Reply : TimelineItemAction("Reply", VectorIcons.Reply) object Edit : TimelineItemAction("Edit", VectorIcons.Edit) + object Developer : TimelineItemAction("Developer", VectorIcons.DeveloperMode) + object ReportContent : TimelineItemAction("Report content", VectorIcons.ReportContent, destructive = true) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt new file mode 100644 index 0000000000..5dd055e711 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/AttachmentsBottomSheet.kt @@ -0,0 +1,117 @@ +/* + * 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.messagecomposer + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.ListItem +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AttachFile +import androidx.compose.material.icons.filled.Collections +import androidx.compose.material.icons.filled.PhotoCamera +import androidx.compose.material.icons.filled.Videocam +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import io.element.android.features.messages.impl.R +import io.element.android.libraries.androidutils.ui.hideKeyboard +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet +import io.element.android.libraries.designsystem.theme.components.Text + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun AttachmentsBottomSheet( + state: MessageComposerState, + modifier: Modifier = Modifier, +) { + val localView = LocalView.current + var isVisible by rememberSaveable { mutableStateOf(state.showAttachmentSourcePicker) } + + BackHandler(enabled = isVisible) { + isVisible = false + } + + LaunchedEffect(state.showAttachmentSourcePicker) { + if (state.showAttachmentSourcePicker) { + // We need to use this instead of `LocalFocusManager.clearFocus()` to hide the keyboard when focus is on an Android View + localView.hideKeyboard() + isVisible = true + } else { + isVisible = false + } + } + // Send 'DismissAttachmentMenu' event when the bottomsheet was just hidden + LaunchedEffect(isVisible) { + if (!isVisible) { + state.eventSink(MessageComposerEvents.DismissAttachmentMenu) + } + } + + if (isVisible) { + ModalBottomSheet( + modifier = modifier, + onDismissRequest = { isVisible = false } + ) { + AttachmentSourcePickerMenu(eventSink = state.eventSink) + } + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +internal fun AttachmentSourcePickerMenu( + eventSink: (MessageComposerEvents) -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier.padding(bottom = 32.dp) +// .navigationBarsPadding() - FIXME after https://issuetracker.google.com/issues/275849044 + ) { + ListItem( + modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.FromGallery) }, + icon = { Icon(Icons.Default.Collections, null) }, + text = { Text(stringResource(R.string.screen_room_attachment_source_gallery)) }, + ) + ListItem( + modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.FromFiles) }, + icon = { Icon(Icons.Default.AttachFile, null) }, + text = { Text(stringResource(R.string.screen_room_attachment_source_files)) }, + ) + ListItem( + modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.PhotoFromCamera) }, + icon = { Icon(Icons.Default.PhotoCamera, null) }, + text = { Text(stringResource(R.string.screen_room_attachment_source_camera_photo)) }, + ) + ListItem( + modifier = Modifier.clickable { eventSink(MessageComposerEvents.PickAttachmentSource.VideoFromCamera) }, + icon = { Icon(Icons.Default.Videocam, null) }, + text = { Text(stringResource(R.string.screen_room_attachment_source_camera_video)) }, + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt index 63fe656cd2..a66e7d06ab 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt @@ -16,6 +16,7 @@ package io.element.android.features.messages.impl.messagecomposer +import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview @@ -46,21 +47,25 @@ fun MessageComposerView( state.eventSink(MessageComposerEvents.UpdateText(text)) } - TextComposer( - onSendMessage = ::sendMessage, - fullscreen = state.isFullScreen, - onFullscreenToggle = ::onFullscreenToggle, - composerMode = state.mode, - onCloseSpecialMode = ::onCloseSpecialMode, - onComposerTextChange = ::onComposerTextChange, - onAddAttachment = { - state.eventSink(MessageComposerEvents.AddAttachment) - }, - composerCanSendMessage = state.isSendButtonVisible, - composerText = state.text?.charSequence?.toString(), - isInDarkMode = !ElementTheme.colors.isLight, - modifier = modifier - ) + Box { + AttachmentsBottomSheet(state = state) + + TextComposer( + onSendMessage = ::sendMessage, + fullscreen = state.isFullScreen, + onFullscreenToggle = ::onFullscreenToggle, + composerMode = state.mode, + onCloseSpecialMode = ::onCloseSpecialMode, + onComposerTextChange = ::onComposerTextChange, + onAddAttachment = { + state.eventSink(MessageComposerEvents.AddAttachment) + }, + composerCanSendMessage = state.isSendButtonVisible, + composerText = state.text?.charSequence?.toString(), + isInDarkMode = !ElementTheme.colors.isLight, + modifier = modifier + ) + } } @Preview diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt index 1df650e7bf..36aaa27be3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemFileView.kt @@ -24,12 +24,13 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.AttachFile +import androidx.compose.material.icons.outlined.Attachment import androidx.compose.material3.MaterialTheme 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 @@ -58,14 +59,14 @@ fun TimelineItemFileView( contentAlignment = Alignment.Center, ) { Icon( - modifier = Modifier.size(20.dp), - imageVector = Icons.Filled.AttachFile, - contentDescription = "OpenFile" + imageVector = Icons.Outlined.Attachment, + contentDescription = "OpenFile", + modifier = Modifier.size(16.dp).rotate(-45f), ) } Column(modifier = Modifier.padding(horizontal = 8.dp),) { Text( - text = content.name, + text = content.body, maxLines = 2, fontSize = 16.sp, overflow = TextOverflow.Ellipsis 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 3341d654e0..cc8082be62 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 @@ -73,7 +73,7 @@ class TimelineItemContentMessageFactory @Inject constructor( ) } is FileMessageType -> TimelineItemFileContent( - name = messageType.body, + body = messageType.body, thumbnailSource = messageType.info?.thumbnailSource, fileSource = messageType.source, mimeType = messageType.info?.mimetype, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContent.kt index 4557718561..9307cf8a67 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContent.kt @@ -19,7 +19,7 @@ package io.element.android.features.messages.impl.timeline.model.event import io.element.android.libraries.matrix.api.media.MediaSource data class TimelineItemFileContent( - val name: String, + val body: String, val fileSource: MediaSource, val thumbnailSource: MediaSource?, val formattedFileSize: String?, @@ -27,7 +27,7 @@ data class TimelineItemFileContent( ) : TimelineItemEventContent { override val type: String = "TimelineItemFileContent" - private val fileExtension = name.substringAfterLast('.', "").uppercase() + private val fileExtension = body.substringAfterLast('.', "").uppercase() val fileExtensionAndSize = buildString { append(fileExtension) if (formattedFileSize != null) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContentProvider.kt index 1eb0d6135c..08125ea777 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemFileContentProvider.kt @@ -30,7 +30,7 @@ open class TimelineItemFileContentProvider : PreviewParameterProvider -) { - data class Item( - @StringRes val title: Int, - @StringRes val body: Int, - @DrawableRes val image: Int, - @DrawableRes val pageBackground: Int - ) +object OnBoardingConfig { + const val canLoginWithQrCode = false + const val canCreateAccount = false } diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingNode.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingNode.kt index a6cb0a3b49..a081c0b7ab 100644 --- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingNode.kt +++ b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingNode.kt @@ -32,6 +32,7 @@ import io.element.android.libraries.di.AppScope class OnBoardingNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, + private val presenter: OnBoardingPresenter, ) : Node( buildContext = buildContext, plugins = plugins @@ -47,10 +48,11 @@ class OnBoardingNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { - OnBoardingScreen( + val state = presenter.present() + OnBoardingView( + state = state, modifier = modifier, - onSignIn = this::onSignIn, - onSignUp = this::onSignUp + onSignIn = ::onSignIn, ) } } diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenter.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenter.kt new file mode 100644 index 0000000000..48a360e6c9 --- /dev/null +++ b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenter.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.onboarding.impl + +import androidx.compose.runtime.Composable +import io.element.android.libraries.architecture.Presenter +import javax.inject.Inject + +/** + * Note: this Presenter is ignored regarding code coverage because it cannot reach the coverage threshold. + * When this presenter get more code in it, please remove the ignore rule in the kover configuration. + */ +class OnBoardingPresenter @Inject constructor( +) : Presenter { + @Composable + override fun present(): OnBoardingState { + return OnBoardingState( + canLoginWithQrCode = OnBoardingConfig.canLoginWithQrCode, + canCreateAccount = OnBoardingConfig.canCreateAccount, + ) + } +} diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingScreen.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingScreen.kt deleted file mode 100644 index 8694865938..0000000000 --- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingScreen.kt +++ /dev/null @@ -1,182 +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.onboarding.impl - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Alignment.Companion.CenterHorizontally -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.google.accompanist.pager.HorizontalPager -import com.google.accompanist.pager.HorizontalPagerIndicator -import com.google.accompanist.pager.rememberPagerState -import io.element.android.libraries.designsystem.preview.ElementPreviewDark -import io.element.android.libraries.designsystem.preview.ElementPreviewLight -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 kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -@Composable -fun OnBoardingScreen( - modifier: Modifier = Modifier, - onPageChanged: (Int) -> Unit = {}, - onSignUp: () -> Unit = {}, - onSignIn: () -> Unit = {}, -) { - val carrouselData = remember { SplashCarouselDataFactory().create() } - val nbOfPages = carrouselData.items.size - var key by remember { mutableStateOf(false) } - Box( - modifier = modifier - .fillMaxSize() - .systemBarsPadding() - .padding(vertical = 16.dp) - ) { - Column( - modifier = Modifier.fillMaxSize(), - ) { - val pagerState = rememberPagerState() - LaunchedEffect(key) { - launch { - delay(3_000) - pagerState.animateScrollToPage((pagerState.currentPage + 1) % nbOfPages) - // https://stackoverflow.com/questions/73714228/accompanist-pager-animatescrolltopage-doesnt-scroll-to-next-page-correctly - key = !key - } - } - LaunchedEffect(pagerState) { - // Collect from the pager state a snapshotFlow reading the currentPage - snapshotFlow { pagerState.currentPage }.collect { page -> - onPageChanged(page) - } - } - HorizontalPager( - modifier = Modifier.weight(1f), - count = nbOfPages, - state = pagerState, - ) { page -> - // Our page content - OnBoardingPage(carrouselData.items[page]) - } - HorizontalPagerIndicator( - pagerState = pagerState, - modifier = Modifier - .align(CenterHorizontally) - .padding(16.dp), - ) - Button( - onClick = { - onSignIn() - }, - enabled = true, - modifier = Modifier - .align(CenterHorizontally) - .testTag(TestTags.onBoardingSignIn) - .padding(top = 16.dp) - ) { - Text(text = stringResource(id = R.string.login_splash_submit)) - } - } - } -} - -@Composable -fun OnBoardingPage( - item: SplashCarouselData.Item, - modifier: Modifier = Modifier, -) { - Box( - modifier = modifier, - ) { - /* - Image( - painterResource(id = item.pageBackground), - contentDescription = null, - modifier = Modifier.fillMaxSize() - ) - */ - Column( - modifier = Modifier.padding(vertical = 16.dp, horizontal = 32.dp) - ) { - Image( - painterResource(id = item.image), - contentDescription = null, - modifier = Modifier - .align(CenterHorizontally) - .size(192.dp) - .padding(16.dp) - ) - Text( - text = stringResource(id = item.title), - modifier = Modifier - .fillMaxWidth() - .align(CenterHorizontally) - .padding(8.dp), - textAlign = TextAlign.Center, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.primary, - fontSize = 24.sp, - ) - Text( - text = stringResource(id = item.body), - modifier = Modifier - .fillMaxWidth() - .align(CenterHorizontally), - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.primary, - ) - } - } -} - -@Preview -@Composable -internal fun OnBoardingScreenLightPreview() = - ElementPreviewLight { ContentToPreview() } - -@Preview -@Composable -internal fun OnBoardingScreenDarkPreview() = - ElementPreviewDark { ContentToPreview() } - -@Composable -private fun ContentToPreview() { - OnBoardingScreen() -} diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingState.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingState.kt new file mode 100644 index 0000000000..88215c0c1e --- /dev/null +++ b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingState.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.onboarding.impl + +data class OnBoardingState( + val canLoginWithQrCode: Boolean, + val canCreateAccount: Boolean, +) diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingStateProvider.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingStateProvider.kt new file mode 100644 index 0000000000..1c60a56018 --- /dev/null +++ b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingStateProvider.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.features.onboarding.impl + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class OnBoardingStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + anOnBoardingState(), + anOnBoardingState(canLoginWithQrCode = true), + anOnBoardingState(canCreateAccount = true), + anOnBoardingState(canLoginWithQrCode = true, canCreateAccount = true), + ) +} + +fun anOnBoardingState( + canLoginWithQrCode: Boolean = false, + canCreateAccount: Boolean = false +) = OnBoardingState( + canLoginWithQrCode = canLoginWithQrCode, + canCreateAccount = canCreateAccount +) 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 new file mode 100644 index 0000000000..fd736dc0e5 --- /dev/null +++ b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt @@ -0,0 +1,182 @@ +/* + * 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.onboarding.impl + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.QrCode +import androidx.compose.material3.MaterialTheme +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.graphics.ColorFilter +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.pages.OnBoardingPage +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Button +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.OutlinedButton +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.testtags.testTag + +// Ref: https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=133-5427&t=5SHVppfYzjvkEywR-0 +@Composable +fun OnBoardingView( + state: OnBoardingState, + modifier: Modifier = Modifier, + onSignInWithQrCode: () -> Unit = {}, + onSignIn: () -> Unit = {}, + onCreateAccount: () -> Unit = {}, +) { + OnBoardingPage( + modifier = modifier, + footer = { + OnBoardingButtons( + state = state, + onSignInWithQrCode = onSignInWithQrCode, + onSignIn = onSignIn, + onCreateAccount = onCreateAccount, + ) + } + ) { + OnBoardingContent() + } +} + +@Composable +private fun OnBoardingContent(modifier: Modifier = Modifier) { + Box( + modifier = modifier.fillMaxSize(), + contentAlignment = BiasAlignment( + horizontalBias = 0f, + verticalBias = -0.2f + ) + ) { + Column( + modifier = Modifier + .fillMaxWidth(), + horizontalAlignment = CenterHorizontally, + ) { + Image( + painter = painterResource(id = R.drawable.element_logo), + contentDescription = null, + ) + Image( + modifier = Modifier.padding(top = 14.dp), + painter = painterResource(id = R.drawable.element), + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary), + contentDescription = null, + ) + Text( + modifier = Modifier.padding(top = 24.dp), + text = stringResource(id = R.string.screen_onboarding_subtitle), + color = MaterialTheme.colorScheme.secondary, + fontSize = 20.sp, + textAlign = TextAlign.Center + ) + } + } +} + +@Composable +private fun OnBoardingButtons( + state: OnBoardingState, + onSignInWithQrCode: () -> Unit, + onSignIn: () -> Unit, + onCreateAccount: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .fillMaxWidth(), + horizontalAlignment = CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + if (state.canLoginWithQrCode) { + Button( + onClick = { + onSignInWithQrCode() + }, + enabled = true, + modifier = Modifier + .fillMaxWidth() + ) { + Icon( + imageVector = Icons.Default.QrCode, contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimary + ) + Spacer(Modifier.width(14.dp)) + Text(text = stringResource(id = R.string.screen_onboarding_sign_in_with_qr_code)) + } + } + Button( + onClick = { + onSignIn() + }, + enabled = true, + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.onBoardingSignIn) + ) { + Text(text = stringResource(id = R.string.screen_onboarding_sign_in_manually)) + } + if (state.canCreateAccount) { + OutlinedButton( + onClick = { + onCreateAccount() + }, + enabled = true, + modifier = Modifier + .fillMaxWidth() + ) { + Text(text = stringResource(id = R.string.screen_onboarding_sign_up)) + } + } + } +} + +@Preview +@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) { + OnBoardingView(state) +} diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/SplashCarouselDataFactory.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/SplashCarouselDataFactory.kt deleted file mode 100644 index 5068bda82b..0000000000 --- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/SplashCarouselDataFactory.kt +++ /dev/null @@ -1,73 +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.onboarding.impl - -import androidx.annotation.DrawableRes - -class SplashCarouselDataFactory { - fun create(): SplashCarouselData { - val lightTheme = true - - fun background(@DrawableRes lightDrawable: Int) = - if (lightTheme) lightDrawable else R.drawable.bg_color_background - - fun hero(@DrawableRes lightDrawable: Int, @DrawableRes darkDrawable: Int) = - if (lightTheme) lightDrawable else darkDrawable - - return SplashCarouselData( - listOf( - SplashCarouselData.Item( - R.string.ftue_auth_carousel_secure_title, - R.string.ftue_auth_carousel_secure_body, - hero( - R.drawable.ic_splash_conversations, - R.drawable.ic_splash_conversations_dark - ), - background(R.drawable.bg_carousel_page_1) - ), - SplashCarouselData.Item( - R.string.ftue_auth_carousel_control_title, - R.string.ftue_auth_carousel_control_body, - hero(R.drawable.ic_splash_control, R.drawable.ic_splash_control_dark), - background(R.drawable.bg_carousel_page_2) - ), - SplashCarouselData.Item( - R.string.ftue_auth_carousel_encrypted_title, - R.string.ftue_auth_carousel_encrypted_body, - hero(R.drawable.ic_splash_secure, R.drawable.ic_splash_secure_dark), - background(R.drawable.bg_carousel_page_3) - ), - SplashCarouselData.Item( - collaborationTitle(), - R.string.ftue_auth_carousel_workplace_body, - hero( - R.drawable.ic_splash_collaboration, - R.drawable.ic_splash_collaboration_dark - ), - background(R.drawable.bg_carousel_page_4) - ) - ) - ) - } - - private fun collaborationTitle(): Int { - return when { - true -> R.string.cut_the_slack_from_teams - else -> R.string.ftue_auth_carousel_workplace_title - } - } -} diff --git a/features/onboarding/impl/src/main/res/drawable-hdpi/ic_splash_collaboration.webp b/features/onboarding/impl/src/main/res/drawable-hdpi/ic_splash_collaboration.webp deleted file mode 100644 index 7042e030d0..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-hdpi/ic_splash_collaboration.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-hdpi/ic_splash_collaboration_dark.webp b/features/onboarding/impl/src/main/res/drawable-hdpi/ic_splash_collaboration_dark.webp deleted file mode 100644 index 6e4297183a..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-hdpi/ic_splash_collaboration_dark.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-hdpi/ic_splash_control.webp b/features/onboarding/impl/src/main/res/drawable-hdpi/ic_splash_control.webp deleted file mode 100644 index 82c04e402b..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-hdpi/ic_splash_control.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-hdpi/ic_splash_control_dark.webp b/features/onboarding/impl/src/main/res/drawable-hdpi/ic_splash_control_dark.webp deleted file mode 100644 index 0d0c6ad78b..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-hdpi/ic_splash_control_dark.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-hdpi/ic_splash_conversations.webp b/features/onboarding/impl/src/main/res/drawable-hdpi/ic_splash_conversations.webp deleted file mode 100644 index ee9604c1f1..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-hdpi/ic_splash_conversations.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-hdpi/ic_splash_conversations_dark.webp b/features/onboarding/impl/src/main/res/drawable-hdpi/ic_splash_conversations_dark.webp deleted file mode 100644 index c5cdf4e6fe..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-hdpi/ic_splash_conversations_dark.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-hdpi/ic_splash_secure.webp b/features/onboarding/impl/src/main/res/drawable-hdpi/ic_splash_secure.webp deleted file mode 100644 index a880031ada..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-hdpi/ic_splash_secure.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-hdpi/ic_splash_secure_dark.webp b/features/onboarding/impl/src/main/res/drawable-hdpi/ic_splash_secure_dark.webp deleted file mode 100644 index 65ef9f35ff..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-hdpi/ic_splash_secure_dark.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-xhdpi/ic_splash_collaboration.webp b/features/onboarding/impl/src/main/res/drawable-xhdpi/ic_splash_collaboration.webp deleted file mode 100644 index d32d9f6026..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-xhdpi/ic_splash_collaboration.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-xhdpi/ic_splash_collaboration_dark.webp b/features/onboarding/impl/src/main/res/drawable-xhdpi/ic_splash_collaboration_dark.webp deleted file mode 100644 index 04af9e2db4..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-xhdpi/ic_splash_collaboration_dark.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-xhdpi/ic_splash_control.webp b/features/onboarding/impl/src/main/res/drawable-xhdpi/ic_splash_control.webp deleted file mode 100644 index 972d91d5d0..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-xhdpi/ic_splash_control.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-xhdpi/ic_splash_control_dark.webp b/features/onboarding/impl/src/main/res/drawable-xhdpi/ic_splash_control_dark.webp deleted file mode 100644 index cbbea1ae87..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-xhdpi/ic_splash_control_dark.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-xhdpi/ic_splash_conversations.webp b/features/onboarding/impl/src/main/res/drawable-xhdpi/ic_splash_conversations.webp deleted file mode 100644 index 4057edfc66..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-xhdpi/ic_splash_conversations.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-xhdpi/ic_splash_conversations_dark.webp b/features/onboarding/impl/src/main/res/drawable-xhdpi/ic_splash_conversations_dark.webp deleted file mode 100644 index e3b7f22c1a..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-xhdpi/ic_splash_conversations_dark.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-xhdpi/ic_splash_secure.webp b/features/onboarding/impl/src/main/res/drawable-xhdpi/ic_splash_secure.webp deleted file mode 100644 index b8c772bde2..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-xhdpi/ic_splash_secure.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-xhdpi/ic_splash_secure_dark.webp b/features/onboarding/impl/src/main/res/drawable-xhdpi/ic_splash_secure_dark.webp deleted file mode 100644 index d4c1f97652..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-xhdpi/ic_splash_secure_dark.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-xxhdpi/ic_splash_collaboration.webp b/features/onboarding/impl/src/main/res/drawable-xxhdpi/ic_splash_collaboration.webp deleted file mode 100644 index 8feed1f9f9..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-xxhdpi/ic_splash_collaboration.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-xxhdpi/ic_splash_collaboration_dark.webp b/features/onboarding/impl/src/main/res/drawable-xxhdpi/ic_splash_collaboration_dark.webp deleted file mode 100644 index 02e44fbf44..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-xxhdpi/ic_splash_collaboration_dark.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-xxhdpi/ic_splash_control.webp b/features/onboarding/impl/src/main/res/drawable-xxhdpi/ic_splash_control.webp deleted file mode 100644 index 99d4c4049d..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-xxhdpi/ic_splash_control.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-xxhdpi/ic_splash_control_dark.webp b/features/onboarding/impl/src/main/res/drawable-xxhdpi/ic_splash_control_dark.webp deleted file mode 100644 index 9afa384f27..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-xxhdpi/ic_splash_control_dark.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-xxhdpi/ic_splash_conversations.webp b/features/onboarding/impl/src/main/res/drawable-xxhdpi/ic_splash_conversations.webp deleted file mode 100644 index 99a4c0c6f5..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-xxhdpi/ic_splash_conversations.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-xxhdpi/ic_splash_conversations_dark.webp b/features/onboarding/impl/src/main/res/drawable-xxhdpi/ic_splash_conversations_dark.webp deleted file mode 100644 index 361981eec7..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-xxhdpi/ic_splash_conversations_dark.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-xxhdpi/ic_splash_secure.webp b/features/onboarding/impl/src/main/res/drawable-xxhdpi/ic_splash_secure.webp deleted file mode 100644 index 114421453e..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-xxhdpi/ic_splash_secure.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-xxhdpi/ic_splash_secure_dark.webp b/features/onboarding/impl/src/main/res/drawable-xxhdpi/ic_splash_secure_dark.webp deleted file mode 100644 index 737bcbdf17..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-xxhdpi/ic_splash_secure_dark.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-xxxhdpi/ic_splash_collaboration.webp b/features/onboarding/impl/src/main/res/drawable-xxxhdpi/ic_splash_collaboration.webp deleted file mode 100644 index 1dc31f6447..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-xxxhdpi/ic_splash_collaboration.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-xxxhdpi/ic_splash_collaboration_dark.webp b/features/onboarding/impl/src/main/res/drawable-xxxhdpi/ic_splash_collaboration_dark.webp deleted file mode 100644 index 943f2b9ba8..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-xxxhdpi/ic_splash_collaboration_dark.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-xxxhdpi/ic_splash_control.webp b/features/onboarding/impl/src/main/res/drawable-xxxhdpi/ic_splash_control.webp deleted file mode 100644 index 9375475513..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-xxxhdpi/ic_splash_control.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-xxxhdpi/ic_splash_control_dark.webp b/features/onboarding/impl/src/main/res/drawable-xxxhdpi/ic_splash_control_dark.webp deleted file mode 100644 index 905851dc26..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-xxxhdpi/ic_splash_control_dark.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-xxxhdpi/ic_splash_conversations.webp b/features/onboarding/impl/src/main/res/drawable-xxxhdpi/ic_splash_conversations.webp deleted file mode 100644 index 0d669312f5..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-xxxhdpi/ic_splash_conversations.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-xxxhdpi/ic_splash_conversations_dark.webp b/features/onboarding/impl/src/main/res/drawable-xxxhdpi/ic_splash_conversations_dark.webp deleted file mode 100644 index c5c4b2ccdd..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-xxxhdpi/ic_splash_conversations_dark.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-xxxhdpi/ic_splash_secure.webp b/features/onboarding/impl/src/main/res/drawable-xxxhdpi/ic_splash_secure.webp deleted file mode 100644 index 6a2a3fda56..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-xxxhdpi/ic_splash_secure.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable-xxxhdpi/ic_splash_secure_dark.webp b/features/onboarding/impl/src/main/res/drawable-xxxhdpi/ic_splash_secure_dark.webp deleted file mode 100644 index b792cb16ea..0000000000 Binary files a/features/onboarding/impl/src/main/res/drawable-xxxhdpi/ic_splash_secure_dark.webp and /dev/null differ diff --git a/features/onboarding/impl/src/main/res/drawable/bg_carousel_page_1.xml b/features/onboarding/impl/src/main/res/drawable/bg_carousel_page_1.xml deleted file mode 100644 index 03414760f5..0000000000 --- a/features/onboarding/impl/src/main/res/drawable/bg_carousel_page_1.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - diff --git a/features/onboarding/impl/src/main/res/drawable/bg_carousel_page_2.xml b/features/onboarding/impl/src/main/res/drawable/bg_carousel_page_2.xml deleted file mode 100644 index 216f37c056..0000000000 --- a/features/onboarding/impl/src/main/res/drawable/bg_carousel_page_2.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - diff --git a/features/onboarding/impl/src/main/res/drawable/bg_carousel_page_3.xml b/features/onboarding/impl/src/main/res/drawable/bg_carousel_page_3.xml deleted file mode 100644 index b206670820..0000000000 --- a/features/onboarding/impl/src/main/res/drawable/bg_carousel_page_3.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - diff --git a/features/onboarding/impl/src/main/res/drawable/bg_carousel_page_4.xml b/features/onboarding/impl/src/main/res/drawable/bg_carousel_page_4.xml deleted file mode 100644 index 8eca5f922f..0000000000 --- a/features/onboarding/impl/src/main/res/drawable/bg_carousel_page_4.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - diff --git a/features/onboarding/impl/src/main/res/drawable/bg_color_background.xml b/features/onboarding/impl/src/main/res/drawable/bg_color_background.xml deleted file mode 100644 index df950fd479..0000000000 --- a/features/onboarding/impl/src/main/res/drawable/bg_color_background.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - diff --git a/features/onboarding/impl/src/main/res/drawable/element.xml b/features/onboarding/impl/src/main/res/drawable/element.xml new file mode 100644 index 0000000000..96a86d0db5 --- /dev/null +++ b/features/onboarding/impl/src/main/res/drawable/element.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/features/onboarding/impl/src/main/res/drawable/element_logo.xml b/features/onboarding/impl/src/main/res/drawable/element_logo.xml new file mode 100644 index 0000000000..9601fe3d06 --- /dev/null +++ b/features/onboarding/impl/src/main/res/drawable/element_logo.xml @@ -0,0 +1,26 @@ + + + + + + + diff --git a/features/onboarding/impl/src/main/res/values/strings.xml b/features/onboarding/impl/src/main/res/values/strings.xml deleted file mode 100644 index d325d6ad1c..0000000000 --- a/features/onboarding/impl/src/main/res/values/strings.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - Cut the slack from teams. - - Get started - - Own your conversations. - You\'re in control. - Secure messaging. - Messaging for your team. - - Secure and independent communication that gives you the same level of privacy as a face-to-face conversation in your own home. - Choose where your conversations are kept, giving you control and independence. Connected via Matrix. - End-to-end encrypted and no phone number required. No ads or datamining. - - Element is also great for the workplace. It’s trusted by the world’s most secure organisations. - - - diff --git a/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenterTest.kt b/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenterTest.kt new file mode 100644 index 0000000000..f415cd795f --- /dev/null +++ b/features/onboarding/impl/src/test/kotlin/io/element/android/features/onboarding/impl/OnBoardingPresenterTest.kt @@ -0,0 +1,38 @@ +/* + * 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.onboarding.impl + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class OnBoardingPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = OnBoardingPresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.canLoginWithQrCode).isFalse() + assertThat(initialState.canCreateAccount).isFalse() + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2f84422fce..cce55faaca 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -96,8 +96,6 @@ accompanist_permission = { module = "com.google.accompanist:accompanist-permissi accompanist_material = { module = "com.google.accompanist:accompanist-navigation-material", version.ref = "accompanist" } accompanist_systemui = { module = "com.google.accompanist:accompanist-systemuicontroller", version.ref = "accompanist" } accompanist_placeholder = { module = "com.google.accompanist:accompanist-placeholder-material", version.ref = "accompanist" } -accompanist_pager = { module = "com.google.accompanist:accompanist-pager", version.ref = "accompanist" } -accompanist_pagerindicator = { module = "com.google.accompanist:accompanist-pager-indicators", version.ref = "accompanist" } accompanist_flowlayout = { module = "com.google.accompanist:accompanist-flowlayout", version.ref = "accompanist" } # Libraries diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/NodeFactories.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/NodeFactories.kt index 82ec21667b..6073b45351 100644 --- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/NodeFactories.kt +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/NodeFactories.kt @@ -34,6 +34,8 @@ inline fun Context.createNode(context: BuildContext, plugi inline fun NodeFactoriesBindings.createNode(context: BuildContext, plugins: List = emptyList()): NODE { val nodeClass = NODE::class.java val nodeFactoryMap = nodeFactories() + // Note to developers: If you got the error below, make sure to build again after + // clearing the cache (sometimes several times) to let Dagger generate the NodeFactory. val nodeFactory = nodeFactoryMap[nodeClass] ?: error("Cannot find NodeFactory for ${nodeClass.name}.") @Suppress("UNCHECKED_CAST") diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt index c2a82f0e21..0e33567129 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/VectorIcons.kt @@ -18,9 +18,11 @@ package io.element.android.libraries.designsystem object VectorIcons { val Copy = R.drawable.ic_content_copy - val ArrowForward = R.drawable.ic_content_arrow_forward - val Delete = R.drawable.ic_baseline_delete_outline_24 - val Reply = R.drawable.ic_baseline_reply_24 - val Edit = R.drawable.ic_baseline_edit_24 + val Forward = R.drawable.ic_forward + val Delete = R.drawable.ic_delete + val Reply = R.drawable.ic_reply + val Edit = R.drawable.ic_edit val DoorOpen = R.drawable.ic_door_open_24 + val DeveloperMode = R.drawable.ic_developer_mode + val ReportContent = R.drawable.ic_report_content } 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 new file mode 100644 index 0000000000..0e0292957b --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/pages/OnBoardingPage.kt @@ -0,0 +1,126 @@ +/* + * 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.pages + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBarsPadding +import androidx.compose.runtime.Composable +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 androidx.compose.ui.unit.sp +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.theme.components.Text + +/** + * Page for onboarding screens, with content and optional footer. + * + * Ref: https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=133-5427&t=5SHVppfYzjvkEywR-0 + * @param modifier Classical modifier. + * @param footer optional footer. + * @param content main content. + */ +@Composable +fun OnBoardingPage( + modifier: Modifier = Modifier, + footer: @Composable () -> Unit = {}, + content: @Composable () -> Unit = {}, +) { + Box( + modifier = modifier + .fillMaxSize() + ) { + // BG + Image( + modifier = Modifier + .fillMaxSize(), + painter = painterResource(id = R.drawable.onboarding_bg), + contentScale = ContentScale.Crop, + contentDescription = null, + ) + Column( + modifier = Modifier + .fillMaxSize() + .systemBarsPadding() + .padding(vertical = 16.dp), + ) { + // Content + Column( + modifier = Modifier + .weight(1f) + .padding(horizontal = 24.dp) + .fillMaxWidth(), + ) { + content() + } + // Footer + Box(modifier = Modifier.padding(horizontal = 16.dp)) { + footer() + } + } + } +} + +@Preview +@Composable +internal fun OnBoardingPageLightPreview() = + ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +internal fun OnBoardingPageDarkPreview() = + ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + OnBoardingPage( + content = { + Box( + Modifier + .fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = "Content", + fontSize = 40.sp + ) + } + }, + footer = { + Box( + Modifier + .fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Text( + text = "Footer", + fontSize = 40.sp + ) + } + } + ) +} diff --git a/libraries/designsystem/src/main/res/drawable/ic_baseline_delete_outline_24.xml b/libraries/designsystem/src/main/res/drawable/ic_baseline_delete_outline_24.xml deleted file mode 100644 index 479bafb78b..0000000000 --- a/libraries/designsystem/src/main/res/drawable/ic_baseline_delete_outline_24.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - diff --git a/libraries/designsystem/src/main/res/drawable/ic_baseline_edit_24.xml b/libraries/designsystem/src/main/res/drawable/ic_baseline_edit_24.xml deleted file mode 100644 index a91e41a6e9..0000000000 --- a/libraries/designsystem/src/main/res/drawable/ic_baseline_edit_24.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - diff --git a/libraries/designsystem/src/main/res/drawable/ic_delete.xml b/libraries/designsystem/src/main/res/drawable/ic_delete.xml new file mode 100644 index 0000000000..d724c2e05f --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_delete.xml @@ -0,0 +1,10 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_developer_mode.xml b/libraries/designsystem/src/main/res/drawable/ic_developer_mode.xml new file mode 100644 index 0000000000..282937850b --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_developer_mode.xml @@ -0,0 +1,10 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_edit.xml b/libraries/designsystem/src/main/res/drawable/ic_edit.xml new file mode 100644 index 0000000000..f64fa2f5fb --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,10 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_forward.xml b/libraries/designsystem/src/main/res/drawable/ic_forward.xml new file mode 100644 index 0000000000..9608767c8d --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_forward.xml @@ -0,0 +1,11 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_reply.xml b/libraries/designsystem/src/main/res/drawable/ic_reply.xml new file mode 100644 index 0000000000..ac41dfaa55 --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_reply.xml @@ -0,0 +1,11 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/ic_report_content.xml b/libraries/designsystem/src/main/res/drawable/ic_report_content.xml new file mode 100644 index 0000000000..18c9c2f95e --- /dev/null +++ b/libraries/designsystem/src/main/res/drawable/ic_report_content.xml @@ -0,0 +1,10 @@ + + + diff --git a/libraries/designsystem/src/main/res/drawable/onboarding_bg.png b/libraries/designsystem/src/main/res/drawable/onboarding_bg.png new file mode 100644 index 0000000000..61e2264ced Binary files /dev/null and b/libraries/designsystem/src/main/res/drawable/onboarding_bg.png differ diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt index 991f8dd117..eb6e9998ac 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/notification/NotificationData.kt @@ -20,15 +20,23 @@ 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.UserId -//TODO add content data class NotificationData( val senderId: UserId, val eventId: EventId, val roomId: RoomId, - val senderAvatarUrl: String? = null, - val senderDisplayName: String? = null, - val roomAvatarUrl: String? = null, + val senderAvatarUrl: String?, + val senderDisplayName: String?, + val roomAvatarUrl: String?, + val roomDisplayName: String?, val isDirect: Boolean, val isEncrypted: Boolean, val isNoisy: Boolean, + val event: NotificationEvent, +) + +data class NotificationEvent( + val timestamp: Long, + val content: String, + // For images for instance + val contentUrl: String? ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt index 4b121db9bf..e6125cf69b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/NotificationMapper.kt @@ -23,9 +23,9 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.notification.NotificationData import org.matrix.rustcomponents.sdk.NotificationItem import org.matrix.rustcomponents.sdk.use -import javax.inject.Inject -class NotificationMapper @Inject constructor() { +class NotificationMapper { + private val timelineEventMapper = TimelineEventMapper() fun map(notificationItem: NotificationItem): NotificationData { return notificationItem.use { @@ -36,9 +36,11 @@ class NotificationMapper @Inject constructor() { senderAvatarUrl = it.senderAvatarUrl, senderDisplayName = it.senderDisplayName, roomAvatarUrl = it.roomAvatarUrl, + roomDisplayName = it.roomDisplayName, isDirect = it.isDirect, isEncrypted = it.isEncrypted.orFalse(), - isNoisy = it.isNoisy + isNoisy = it.isNoisy, + event = it.event.use { event -> timelineEventMapper.map(event) } ) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt index bd94de21fc..8b630cd64a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/RustNotificationService.kt @@ -16,18 +16,13 @@ package io.element.android.libraries.matrix.impl.notification -import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.matrix.api.MatrixClient 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.UserId import io.element.android.libraries.matrix.api.notification.NotificationData import io.element.android.libraries.matrix.api.notification.NotificationService -import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.use -import java.io.File class RustNotificationService( private val client: Client, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventMapper.kt new file mode 100644 index 0000000000..adb9dcce72 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventMapper.kt @@ -0,0 +1,109 @@ +/* + * 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.notification + +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.notification.NotificationEvent +import org.matrix.rustcomponents.sdk.MessageLikeEventContent +import org.matrix.rustcomponents.sdk.MessageType +import org.matrix.rustcomponents.sdk.StateEventContent +import org.matrix.rustcomponents.sdk.TimelineEvent +import org.matrix.rustcomponents.sdk.TimelineEventType +import org.matrix.rustcomponents.sdk.use +import javax.inject.Inject + +class TimelineEventMapper @Inject constructor() { + + fun map(timelineEvent: TimelineEvent): NotificationEvent { + return timelineEvent.use { + NotificationEvent( + timestamp = it.timestamp().toLong(), + content = it.eventType().toContent(), + contentUrl = null // TODO it.eventType().toContentUrl(), + ) + } + } +} + +private fun TimelineEventType.toContent(): String { + return when (this) { + is TimelineEventType.MessageLike -> content.toContent() + is TimelineEventType.State -> content.toContent() + } +} + +private fun StateEventContent.toContent(): String { + return when (this) { + StateEventContent.PolicyRuleRoom -> "PolicyRuleRoom" + StateEventContent.PolicyRuleServer -> "PolicyRuleServer" + StateEventContent.PolicyRuleUser -> "PolicyRuleUser" + StateEventContent.RoomAliases -> "RoomAliases" + StateEventContent.RoomAvatar -> "RoomAvatar" + StateEventContent.RoomCanonicalAlias -> "RoomCanonicalAlias" + StateEventContent.RoomCreate -> "RoomCreate" + StateEventContent.RoomEncryption -> "RoomEncryption" + StateEventContent.RoomGuestAccess -> "RoomGuestAccess" + StateEventContent.RoomHistoryVisibility -> "RoomHistoryVisibility" + StateEventContent.RoomJoinRules -> "RoomJoinRules" + is StateEventContent.RoomMemberContent -> "$userId is now $membershipState" + StateEventContent.RoomName -> "RoomName" + StateEventContent.RoomPinnedEvents -> "RoomPinnedEvents" + StateEventContent.RoomPowerLevels -> "RoomPowerLevels" + StateEventContent.RoomServerAcl -> "RoomServerAcl" + StateEventContent.RoomThirdPartyInvite -> "RoomThirdPartyInvite" + StateEventContent.RoomTombstone -> "RoomTombstone" + StateEventContent.RoomTopic -> "RoomTopic" + StateEventContent.SpaceChild -> "SpaceChild" + StateEventContent.SpaceParent -> "SpaceParent" + } +} + +private fun MessageLikeEventContent.toContent(): String { + return use { + when (it) { + MessageLikeEventContent.CallAnswer -> "CallAnswer" + MessageLikeEventContent.CallCandidates -> "CallCandidates" + MessageLikeEventContent.CallHangup -> "CallHangup" + MessageLikeEventContent.CallInvite -> "CallInvite" + MessageLikeEventContent.KeyVerificationAccept -> "KeyVerificationAccept" + MessageLikeEventContent.KeyVerificationCancel -> "KeyVerificationCancel" + MessageLikeEventContent.KeyVerificationDone -> "KeyVerificationDone" + MessageLikeEventContent.KeyVerificationKey -> "KeyVerificationKey" + MessageLikeEventContent.KeyVerificationMac -> "KeyVerificationMac" + MessageLikeEventContent.KeyVerificationReady -> "KeyVerificationReady" + MessageLikeEventContent.KeyVerificationStart -> "KeyVerificationStart" + is MessageLikeEventContent.ReactionContent -> "Reacted to ${it.relatedEventId.take(8)}…" + MessageLikeEventContent.RoomEncrypted -> "RoomEncrypted" + is MessageLikeEventContent.RoomMessage -> it.messageType.toContent() + MessageLikeEventContent.RoomRedaction -> "RoomRedaction" + MessageLikeEventContent.Sticker -> "Sticker" + } + } +} + +private fun MessageType.toContent(): String { + return when (this) { + is MessageType.Audio -> content.use { it.body } + is MessageType.Emote -> content.body + is MessageType.File -> content.use { it.body } + is MessageType.Image -> content.use { it.body } + is MessageType.Notice -> content.body + is MessageType.Text -> content.body + is MessageType.Video -> content.use { it.body } + } +} diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index 2951ca0e25..725961a248 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -35,6 +35,7 @@ dependencies { implementation(libs.androidx.security.crypto) implementation(libs.network.retrofit) implementation(libs.serialization.json) + implementation(libs.coil) implementation(projects.libraries.architecture) implementation(projects.libraries.core) @@ -42,6 +43,7 @@ dependencies { implementation(projects.libraries.androidutils) implementation(projects.libraries.network) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrixui) api(projects.libraries.pushproviders.api) api(projects.libraries.pushstore.api) api(projects.libraries.push.api) 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 de168090f4..fb3fcfc61f 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 @@ -24,6 +24,7 @@ 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.UserId import io.element.android.libraries.matrix.api.notification.NotificationData +import io.element.android.libraries.matrix.api.notification.NotificationEvent import io.element.android.libraries.push.impl.log.pushLoggerTag import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent @@ -44,9 +45,9 @@ class NotifiableEventResolver @Inject constructor( private val stringProvider: StringProvider, // private val noticeEventFormatter: NoticeEventFormatter, // private val displayableEventFormatter: DisplayableEventFormatter, - private val clock: SystemClock, private val matrixAuthenticationService: MatrixAuthenticationService, private val buildMeta: BuildMeta, + private val clock: SystemClock, ) { suspend fun resolveEvent(sessionId: SessionId, roomId: RoomId, eventId: EventId): NotifiableEvent? { @@ -80,14 +81,14 @@ class NotifiableEventResolver @Inject constructor( editedEventId = null, canBeReplaced = true, noisy = isNoisy, - timestamp = clock.epochMillis(), + timestamp = event.timestamp, senderName = senderDisplayName, senderId = senderId.value, - body = "Message ${eventId.value.take(8)}… in room ${roomId.value.take(8)}…", - imageUriString = null, + body = event.content, + imageUriString = event.contentUrl, threadId = null, - roomName = null, - roomIsDirect = false, + roomName = roomDisplayName, + roomIsDirect = isDirect, roomAvatarPath = roomAvatarUrl, senderAvatarPath = senderAvatarUrl, soundName = null, @@ -97,18 +98,27 @@ class NotifiableEventResolver @Inject constructor( isUpdated = false ) } -} -/** - * TODO This is a temporary method for EAx. - */ -private fun NotificationData?.orDefault(roomId: RoomId, eventId: EventId): NotificationData { - return this ?: NotificationData( - eventId = eventId, - senderId = UserId("@user:domain"), - roomId = roomId, - isNoisy = false, - isEncrypted = false, - isDirect = false - ) + /** + * TODO This is a temporary method for EAx. + */ + private fun NotificationData?.orDefault(roomId: RoomId, eventId: EventId): NotificationData { + return this ?: NotificationData( + eventId = eventId, + senderId = UserId("@user:domain"), + roomId = roomId, + senderAvatarUrl = null, + senderDisplayName = null, + roomAvatarUrl = null, + roomDisplayName = null, + isNoisy = false, + isEncrypted = false, + isDirect = false, + event = NotificationEvent( + timestamp = clock.epochMillis(), + content = "Message ${eventId.value.take(8)}… in room ${roomId.value.take(8)}…", + contentUrl = null + ) + ) + } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt index 7bd76f9f42..c2cdfc5677 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationBitmapLoader.kt @@ -19,9 +19,14 @@ package io.element.android.libraries.push.impl.notifications import android.content.Context import android.graphics.Bitmap import android.os.Build -import androidx.annotation.WorkerThread import androidx.core.graphics.drawable.IconCompat +import androidx.core.graphics.drawable.toBitmap +import coil.imageLoader +import coil.request.ImageRequest +import coil.transform.CircleCropTransformation import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.matrix.ui.media.MediaRequestData import timber.log.Timber import javax.inject.Inject @@ -31,30 +36,24 @@ class NotificationBitmapLoader @Inject constructor( /** * Get icon of a room. + * @param path mxc url */ - @WorkerThread - fun getRoomBitmap(path: String?): Bitmap? { + suspend fun getRoomBitmap(path: String?): Bitmap? { if (path == null) { return null } return loadRoomBitmap(path) } - @WorkerThread - private fun loadRoomBitmap(path: String): Bitmap? { + private suspend fun loadRoomBitmap(path: String): Bitmap? { return try { - null - /* TODO Notification - Glide.with(context) - .asBitmap() - .load(path) - .format(DecodeFormat.PREFER_ARGB_8888) - .signature(ObjectKey("room-icon-notification")) - .submit() - .get() - */ - } catch (e: Exception) { - Timber.e(e, "decodeFile failed") + val imageRequest = ImageRequest.Builder(context) + .data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(1024))) + .build() + val result = context.imageLoader.execute(imageRequest) + result.drawable?.toBitmap() + } catch (e: Throwable) { + Timber.e(e, "Unable to load room bitmap") null } } @@ -62,9 +61,9 @@ class NotificationBitmapLoader @Inject constructor( /** * Get icon of a user. * Before Android P, this does nothing because the icon won't be used + * @param path mxc url */ - @WorkerThread - fun getUserIcon(path: String?): IconCompat? { + suspend fun getUserIcon(path: String?): IconCompat? { if (path == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { return null } @@ -72,23 +71,17 @@ class NotificationBitmapLoader @Inject constructor( return loadUserIcon(path) } - @WorkerThread - private fun loadUserIcon(path: String): IconCompat? { + private suspend fun loadUserIcon(path: String): IconCompat? { return try { - null - /* TODO Notification - val bitmap = Glide.with(context) - .asBitmap() - .load(path) - .transform(CircleCrop()) - .format(DecodeFormat.PREFER_ARGB_8888) - .signature(ObjectKey("user-icon-notification")) - .submit() - .get() - IconCompat.createWithBitmap(bitmap) - */ - } catch (e: Exception) { - Timber.e(e, "decodeFile failed") + val imageRequest = ImageRequest.Builder(context) + .data(MediaRequestData(MediaSource(path), MediaRequestData.Kind.Thumbnail(1024))) + .transformations(CircleCropTransformation()) + .build() + val result = context.imageLoader.execute(imageRequest) + val bitmap = result.drawable?.toBitmap() + return bitmap?.let { IconCompat.createWithBitmap(it) } + } catch (e: Throwable) { + Timber.e(e, "Unable to load user bitmap") null } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt index cf0307fbd9..87d37e7e33 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt @@ -16,28 +16,28 @@ package io.element.android.libraries.push.impl.notifications -import android.content.Context -import android.os.Handler -import android.os.HandlerThread -import androidx.annotation.WorkerThread import io.element.android.libraries.androidutils.throttler.FirstThrottler import io.element.android.libraries.core.cache.CircularCache +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +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.ApplicationContext import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService 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.push.api.store.PushDataStore -import io.element.android.libraries.push.impl.R import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.model.shouldIgnoreMessageEventInRoom import io.element.android.services.appnavstate.api.AppNavigationState import io.element.android.services.appnavstate.api.AppNavigationStateService import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import timber.log.Timber import javax.inject.Inject @@ -48,7 +48,6 @@ import javax.inject.Inject */ @SingleIn(AppScope::class) class NotificationDrawerManager @Inject constructor( - @ApplicationContext context: Context, private val pushDataStore: PushDataStore, private val notifiableEventProcessor: NotifiableEventProcessor, private val notificationRenderer: NotificationRenderer, @@ -56,17 +55,14 @@ class NotificationDrawerManager @Inject constructor( private val filteredEventDetector: FilteredEventDetector, private val appNavigationStateService: AppNavigationStateService, private val coroutineScope: CoroutineScope, + private val dispatchers: CoroutineDispatchers, private val buildMeta: BuildMeta, + private val matrixAuthenticationService: MatrixAuthenticationService, ) { - - private val handlerThread: HandlerThread = HandlerThread("NotificationDrawerManager", Thread.MIN_PRIORITY) - private var backgroundHandler: Handler - /** * Lazily initializes the NotificationState as we rely on having a current session in order to fetch the persisted queue of events. */ private val notificationState by lazy { createInitialNotificationState() } - private val avatarSize = context.resources.getDimensionPixelSize(R.dimen.profile_avatar_size) private var currentAppNavigationState: AppNavigationState? = null private val firstThrottler = FirstThrottler(200) @@ -74,8 +70,6 @@ class NotificationDrawerManager @Inject constructor( private var useCompleteNotificationFormat = true init { - handlerThread.start() - backgroundHandler = Handler(handlerThread.looper) // Observe application state coroutineScope.launch { appNavigationStateService.appNavigationStateFlow @@ -193,30 +187,25 @@ class NotificationDrawerManager @Inject constructor( notificationState.updateQueuedEvents(this) { queuedEvents, _ -> action(queuedEvents) } - refreshNotificationDrawer() + coroutineScope.refreshNotificationDrawer() } - private fun refreshNotificationDrawer() { + private fun CoroutineScope.refreshNotificationDrawer() = launch { // Implement last throttler val canHandle = firstThrottler.canHandle() Timber.v("refreshNotificationDrawer(), delay: ${canHandle.waitMillis()} ms") - backgroundHandler.removeCallbacksAndMessages(null) - - backgroundHandler.postDelayed( - { - try { - refreshNotificationDrawerBg() - } catch (throwable: Throwable) { - // It can happen if for instance session has been destroyed. It's a bit ugly to try catch like this, but it's safer - Timber.w(throwable, "refreshNotificationDrawerBg failure") - } - }, - canHandle.waitMillis() - ) + withContext(dispatchers.io) { + delay(canHandle.waitMillis()) + try { + refreshNotificationDrawerBg() + } catch (throwable: Throwable) { + // It can happen if for instance session has been destroyed. It's a bit ugly to try catch like this, but it's safer + Timber.w(throwable, "refreshNotificationDrawerBg failure") + } + } } - @WorkerThread - private fun refreshNotificationDrawerBg() { + private suspend fun refreshNotificationDrawerBg() { Timber.v("refreshNotificationDrawerBg()") val eventsToRender = notificationState.updateQueuedEvents(this) { queuedEvents, renderedEvents -> notifiableEventProcessor.process(queuedEvents.rawEvents(), currentAppNavigationState, renderedEvents).also { @@ -239,24 +228,34 @@ class NotificationDrawerManager @Inject constructor( } } - private fun renderEvents(eventsToRender: List>) { + private suspend fun renderEvents(eventsToRender: List>) { // Group by sessionId val eventsForSessions = eventsToRender.groupBy { it.event.sessionId } eventsForSessions.forEach { (sessionId, notifiableEvents) -> - // TODO EAx val user = session.getUserOrDefault(session.myUserId) - // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash - val myUserDisplayName = "Todo display name" // user.toMatrixItem().getBestName() - // TODO EAx avatar URL - val myUserAvatarUrl = null // session.contentUrlResolver().resolveThumbnail( - // contentUrl = user.avatarUrl, - // width = avatarSize, - // height = avatarSize, - // method = ContentUrlResolver.ThumbnailMethod.SCALE - //) - notificationRenderer.render(sessionId, myUserDisplayName, myUserAvatarUrl, useCompleteNotificationFormat, notifiableEvents) + val currentUser = tryOrNull( + onError = { Timber.e(it, "Unable to retrieve info for user ${sessionId.value}") }, + operation = { + val client = matrixAuthenticationService.restoreSession(sessionId).getOrNull() + + // myUserDisplayName cannot be empty else NotificationCompat.MessagingStyle() will crash + val myUserDisplayName = client?.loadUserDisplayName()?.getOrNull() ?: sessionId.value + val userAvatarUrl = client?.loadUserAvatarURLString()?.getOrNull() + MatrixUser( + userId = sessionId, + displayName = myUserDisplayName, + avatarUrl = userAvatarUrl + ) + } + ) ?: MatrixUser( + userId = sessionId, + displayName = sessionId.value, + avatarUrl = null + ) + + notificationRenderer.render(currentUser, useCompleteNotificationFormat, notifiableEvents) } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt index 4bb49e168f..79173611dc 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactory.kt @@ -18,7 +18,7 @@ package io.element.android.libraries.push.impl.notifications import android.app.Notification 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.user.MatrixUser import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent @@ -34,10 +34,8 @@ class NotificationFactory @Inject constructor( private val summaryGroupMessageCreator: SummaryGroupMessageCreator ) { - fun Map.toNotifications( - sessionId: SessionId, - myUserDisplayName: String, - myUserAvatarUrl: String? + suspend fun Map.toNotifications( + currentUser: MatrixUser, ): List { return map { (roomId, events) -> when { @@ -45,11 +43,9 @@ class NotificationFactory @Inject constructor( else -> { val messageEvents = events.onlyKeptEvents().filterNot { it.isRedacted } roomGroupMessageCreator.createRoomMessage( - sessionId = sessionId, + currentUser = currentUser, events = messageEvents, roomId = roomId, - userDisplayName = myUserDisplayName, - userAvatarUrl = myUserAvatarUrl ) } } @@ -99,7 +95,7 @@ class NotificationFactory @Inject constructor( } fun createSummaryNotification( - sessionId: SessionId, + currentUser: MatrixUser, roomNotifications: List, invitationNotifications: List, simpleNotifications: List, @@ -112,7 +108,7 @@ class NotificationFactory @Inject constructor( roomMeta.isEmpty() && invitationMeta.isEmpty() && simpleMeta.isEmpty() -> SummaryNotification.Removed else -> SummaryNotification.Update( summaryGroupMessageCreator.createSummaryNotification( - sessionId = sessionId, + currentUser = currentUser, roomNotifications = roomMeta, invitationNotifications = invitationMeta, simpleNotifications = simpleMeta, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt index 277dc3b822..428420211b 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRenderer.kt @@ -16,9 +16,8 @@ package io.element.android.libraries.push.impl.notifications -import androidx.annotation.WorkerThread 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.user.MatrixUser import io.element.android.libraries.push.impl.notifications.model.InviteNotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent @@ -32,21 +31,18 @@ class NotificationRenderer @Inject constructor( private val notificationFactory: NotificationFactory, ) { - @WorkerThread - fun render( - sessionId: SessionId, - myUserDisplayName: String, - myUserAvatarUrl: String?, + suspend fun render( + currentUser: MatrixUser, useCompleteNotificationFormat: Boolean, eventsToProcess: List> ) { val (roomEvents, simpleEvents, invitationEvents) = eventsToProcess.groupByType() with(notificationFactory) { - val roomNotifications = roomEvents.toNotifications(sessionId, myUserDisplayName, myUserAvatarUrl) + val roomNotifications = roomEvents.toNotifications(currentUser) val invitationNotifications = invitationEvents.toNotifications() val simpleNotifications = simpleEvents.toNotifications() val summaryNotification = createSummaryNotification( - sessionId = sessionId, + currentUser = currentUser, roomNotifications = roomNotifications, invitationNotifications = invitationNotifications, simpleNotifications = simpleNotifications, @@ -56,21 +52,27 @@ class NotificationRenderer @Inject constructor( // Remove summary first to avoid briefly displaying it after dismissing the last notification if (summaryNotification == SummaryNotification.Removed) { Timber.d("Removing summary notification") - notificationDisplayer.cancelNotificationMessage(null, notificationIdProvider.getSummaryNotificationId(sessionId)) + notificationDisplayer.cancelNotificationMessage( + tag = null, + id = notificationIdProvider.getSummaryNotificationId(currentUser.userId) + ) } roomNotifications.forEach { wrapper -> when (wrapper) { is RoomNotification.Removed -> { Timber.d("Removing room messages notification ${wrapper.roomId}") - notificationDisplayer.cancelNotificationMessage(wrapper.roomId.value, notificationIdProvider.getRoomMessagesNotificationId(sessionId)) + notificationDisplayer.cancelNotificationMessage( + tag = wrapper.roomId.value, + id = notificationIdProvider.getRoomMessagesNotificationId(currentUser.userId) + ) } is RoomNotification.Message -> if (useCompleteNotificationFormat) { Timber.d("Updating room messages notification ${wrapper.meta.roomId}") notificationDisplayer.showNotificationMessage( - wrapper.meta.roomId.value, - notificationIdProvider.getRoomMessagesNotificationId(sessionId), - wrapper.notification + tag = wrapper.meta.roomId.value, + id = notificationIdProvider.getRoomMessagesNotificationId(currentUser.userId), + notification = wrapper.notification ) } } @@ -80,14 +82,17 @@ class NotificationRenderer @Inject constructor( when (wrapper) { is OneShotNotification.Removed -> { Timber.d("Removing invitation notification ${wrapper.key}") - notificationDisplayer.cancelNotificationMessage(wrapper.key, notificationIdProvider.getRoomInvitationNotificationId(sessionId)) + notificationDisplayer.cancelNotificationMessage( + tag = wrapper.key, + id = notificationIdProvider.getRoomInvitationNotificationId(currentUser.userId) + ) } is OneShotNotification.Append -> if (useCompleteNotificationFormat) { Timber.d("Updating invitation notification ${wrapper.meta.key}") notificationDisplayer.showNotificationMessage( - wrapper.meta.key, - notificationIdProvider.getRoomInvitationNotificationId(sessionId), - wrapper.notification + tag = wrapper.meta.key, + id = notificationIdProvider.getRoomInvitationNotificationId(currentUser.userId), + notification = wrapper.notification ) } } @@ -97,14 +102,17 @@ class NotificationRenderer @Inject constructor( when (wrapper) { is OneShotNotification.Removed -> { Timber.d("Removing simple notification ${wrapper.key}") - notificationDisplayer.cancelNotificationMessage(wrapper.key, notificationIdProvider.getRoomEventNotificationId(sessionId)) + notificationDisplayer.cancelNotificationMessage( + tag = wrapper.key, + id = notificationIdProvider.getRoomEventNotificationId(currentUser.userId) + ) } is OneShotNotification.Append -> if (useCompleteNotificationFormat) { Timber.d("Updating simple notification ${wrapper.meta.key}") notificationDisplayer.showNotificationMessage( - wrapper.meta.key, - notificationIdProvider.getRoomEventNotificationId(sessionId), - wrapper.notification + tag = wrapper.meta.key, + id = notificationIdProvider.getRoomEventNotificationId(currentUser.userId), + notification = wrapper.notification ) } } @@ -114,9 +122,9 @@ class NotificationRenderer @Inject constructor( if (summaryNotification is SummaryNotification.Update) { Timber.d("Updating summary notification") notificationDisplayer.showNotificationMessage( - null, - notificationIdProvider.getSummaryNotificationId(sessionId), - summaryNotification.notification + tag = null, + id = notificationIdProvider.getSummaryNotificationId(currentUser.userId), + notification = summaryNotification.notification ) } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt index 00222728bf..5656b81dd9 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/RoomGroupMessageCreator.kt @@ -20,8 +20,9 @@ import android.graphics.Bitmap import androidx.core.app.NotificationCompat import androidx.core.app.Person 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.user.MatrixUser import io.element.android.libraries.push.impl.R +import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent import io.element.android.services.toolbox.api.strings.StringProvider @@ -36,24 +37,22 @@ class RoomGroupMessageCreator @Inject constructor( private val notificationFactory: NotificationFactory ) { - fun createRoomMessage( - sessionId: SessionId, + suspend fun createRoomMessage( + currentUser: MatrixUser, events: List, roomId: RoomId, - userDisplayName: String, - userAvatarUrl: String? ): RoomNotification.Message { val lastKnownRoomEvent = events.last() val roomName = lastKnownRoomEvent.roomName ?: lastKnownRoomEvent.senderName ?: "Room name (${roomId.value.take(8)}…)" val roomIsGroup = !lastKnownRoomEvent.roomIsDirect val style = NotificationCompat.MessagingStyle( Person.Builder() - .setName(userDisplayName) - .setIcon(bitmapLoader.getUserIcon(userAvatarUrl)) + .setName(currentUser.displayName?.annotateForDebug(50)) + .setIcon(bitmapLoader.getUserIcon(currentUser.avatarUrl)) .setKey(lastKnownRoomEvent.sessionId.value) .build() ).also { - it.conversationTitle = roomName.takeIf { roomIsGroup } + it.conversationTitle = roomName.takeIf { roomIsGroup }?.annotateForDebug(51) it.isGroupConversation = roomIsGroup it.addMessagesFromEvents(events) } @@ -80,7 +79,7 @@ class RoomGroupMessageCreator @Inject constructor( notificationFactory.createMessagesListNotification( style, RoomEventGroupInfo( - sessionId = sessionId, + sessionId = currentUser.userId, roomId = roomId, roomDisplayName = roomName, isDirect = !roomIsGroup, @@ -99,13 +98,13 @@ class RoomGroupMessageCreator @Inject constructor( ) } - private fun NotificationCompat.MessagingStyle.addMessagesFromEvents(events: List) { + private suspend fun NotificationCompat.MessagingStyle.addMessagesFromEvents(events: List) { events.forEach { event -> val senderPerson = if (event.outGoingMessage) { null } else { Person.Builder() - .setName(event.senderName) + .setName(event.senderName?.annotateForDebug(70)) .setIcon(bitmapLoader.getUserIcon(event.senderAvatarPath)) .setKey(event.senderId) .build() @@ -117,7 +116,11 @@ class RoomGroupMessageCreator @Inject constructor( senderPerson ) else -> { - val message = NotificationCompat.MessagingStyle.Message(event.body, event.timestamp, senderPerson).also { message -> + val message = NotificationCompat.MessagingStyle.Message( + event.body?.annotateForDebug(71), + event.timestamp, + senderPerson + ).also { message -> event.imageUri?.let { message.setData("image/", it) } @@ -168,7 +171,7 @@ class RoomGroupMessageCreator @Inject constructor( } } - private fun getRoomBitmap(events: List): Bitmap? { + private suspend fun getRoomBitmap(events: List): Bitmap? { // Use the last event (most recent?) return events.lastOrNull() ?.roomAvatarPath diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt index a400c2b7a3..5a7f3d36e8 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/SummaryGroupMessageCreator.kt @@ -18,8 +18,9 @@ package io.element.android.libraries.push.impl.notifications import android.app.Notification import androidx.core.app.NotificationCompat -import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.push.impl.R +import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug import io.element.android.libraries.push.impl.notifications.factories.NotificationFactory import io.element.android.services.toolbox.api.strings.StringProvider import javax.inject.Inject @@ -40,20 +41,20 @@ import javax.inject.Inject */ class SummaryGroupMessageCreator @Inject constructor( private val stringProvider: StringProvider, - private val notificationFactory: NotificationFactory + private val notificationFactory: NotificationFactory, ) { fun createSummaryNotification( - sessionId: SessionId, + currentUser: MatrixUser, roomNotifications: List, invitationNotifications: List, simpleNotifications: List, useCompleteNotificationFormat: Boolean ): Notification { val summaryInboxStyle = NotificationCompat.InboxStyle().also { style -> - roomNotifications.forEach { style.addLine(it.summaryLine) } - invitationNotifications.forEach { style.addLine(it.summaryLine) } - simpleNotifications.forEach { style.addLine(it.summaryLine) } + roomNotifications.forEach { style.addLine(it.summaryLine.annotateForDebug(40)) } + invitationNotifications.forEach { style.addLine(it.summaryLine.annotateForDebug(41)) } + simpleNotifications.forEach { style.addLine(it.summaryLine.annotateForDebug(42)) } } val summaryIsNoisy = roomNotifications.any { it.shouldBing } || @@ -69,12 +70,13 @@ class SummaryGroupMessageCreator @Inject constructor( // FIXME roomIdToEventMap.size is not correct, this is the number of rooms val nbEvents = roomNotifications.size + simpleNotifications.size val sumTitle = stringProvider.getQuantityString(R.plurals.notification_compat_summary_title, nbEvents, nbEvents) - summaryInboxStyle.setBigContentTitle(sumTitle) - // TODO get latest event? - .setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents)) + summaryInboxStyle.setBigContentTitle(sumTitle.annotateForDebug(43)) + //.setSummaryText(stringProvider.getQuantityString(R.plurals.notification_unread_notified_messages, nbEvents, nbEvents).annotateForDebug(44)) + // Use account name now, for multi-session + .setSummaryText(currentUser.userId.value.annotateForDebug(44)) return if (useCompleteNotificationFormat) { notificationFactory.createSummaryListNotification( - sessionId, + currentUser, summaryInboxStyle, sumTitle, noisy = summaryIsNoisy, @@ -82,7 +84,7 @@ class SummaryGroupMessageCreator @Inject constructor( ) } else { processSimpleGroupSummary( - sessionId, + currentUser, summaryIsNoisy, messageCount, simpleNotifications.size, @@ -94,7 +96,7 @@ class SummaryGroupMessageCreator @Inject constructor( } private fun processSimpleGroupSummary( - sessionId: SessionId, + currentUser: MatrixUser, summaryIsNoisy: Boolean, messageEventsCount: Int, simpleEventsCount: Int, @@ -167,7 +169,7 @@ class SummaryGroupMessageCreator @Inject constructor( } } return notificationFactory.createSummaryListNotification( - sessionId = sessionId, + currentUser = currentUser, style = null, compatSummary = privacyTitle, noisy = summaryIsNoisy, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/debug/DebugNotification.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/debug/DebugNotification.kt new file mode 100644 index 0000000000..37f33e1188 --- /dev/null +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/debug/DebugNotification.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.push.impl.notifications.debug + +fun CharSequence.annotateForDebug(@Suppress("UNUSED_PARAMETER") prefix: Int): CharSequence { + return this // "$prefix-$this" +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt index 5795ea5f5f..9da47a6569 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/factories/NotificationFactory.kt @@ -26,11 +26,12 @@ import androidx.core.content.ContextCompat import androidx.core.content.res.ResourcesCompat import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.ApplicationContext -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.push.impl.R import io.element.android.libraries.push.impl.notifications.RoomEventGroupInfo import io.element.android.libraries.push.impl.notifications.channels.NotificationChannels +import io.element.android.libraries.push.impl.notifications.debug.annotateForDebug import io.element.android.libraries.push.impl.notifications.factories.action.AcceptInvitationActionFactory import io.element.android.libraries.push.impl.notifications.factories.action.MarkAsReadActionFactory import io.element.android.libraries.push.impl.notifications.factories.action.QuickReplyActionFactory @@ -84,16 +85,16 @@ class NotificationFactory @Inject constructor( // ID of the corresponding shortcut, for conversation features under API 30+ .setShortcutId(roomInfo.roomId.value) // Title for API < 16 devices. - .setContentTitle(roomInfo.roomDisplayName) + .setContentTitle(roomInfo.roomDisplayName.annotateForDebug(1)) // Content for API < 16 devices. - .setContentText(stringProvider.getString(R.string.notification_new_messages)) + .setContentText(stringProvider.getString(R.string.notification_new_messages).annotateForDebug(2)) // Number of new notifications for API <24 (M and below) devices. .setSubText( stringProvider.getQuantityString( R.plurals.notification_new_messages_for_room, messageStyle.messages.size, messageStyle.messages.size - ) + ).annotateForDebug(3) ) // Auto-bundling is enabled for 4 or more notifications on API 24+ (N+) // devices and all Wear devices. But we want a custom grouping, so we specify the groupID @@ -135,7 +136,7 @@ class NotificationFactory @Inject constructor( } setDeleteIntent(pendingIntentFactory.createDismissRoomPendingIntent(roomInfo.sessionId, roomInfo.roomId)) } - .setTicker(tickerText) + .setTicker(tickerText.annotateForDebug(4)) .build() } @@ -147,8 +148,8 @@ class NotificationFactory @Inject constructor( val channelId = notificationChannels.getChannelIdForMessage(inviteNotifiableEvent.noisy) return NotificationCompat.Builder(context, channelId) .setOnlyAlertOnce(true) - .setContentTitle(inviteNotifiableEvent.roomName ?: buildMeta.applicationName) - .setContentText(inviteNotifiableEvent.description) + .setContentTitle((inviteNotifiableEvent.roomName ?: buildMeta.applicationName).annotateForDebug(5)) + .setContentText(inviteNotifiableEvent.description.annotateForDebug(6)) .setGroup(inviteNotifiableEvent.sessionId.value) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .setSmallIcon(smallIcon) @@ -196,8 +197,8 @@ class NotificationFactory @Inject constructor( val channelId = notificationChannels.getChannelIdForMessage(simpleNotifiableEvent.noisy) return NotificationCompat.Builder(context, channelId) .setOnlyAlertOnce(true) - .setContentTitle(buildMeta.applicationName) - .setContentText(simpleNotifiableEvent.description) + .setContentTitle(buildMeta.applicationName.annotateForDebug(7)) + .setContentText(simpleNotifiableEvent.description.annotateForDebug(8)) .setGroup(simpleNotifiableEvent.sessionId.value) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_ALL) .setSmallIcon(smallIcon) @@ -226,7 +227,7 @@ class NotificationFactory @Inject constructor( * Create the summary notification. */ fun createSummaryListNotification( - sessionId: SessionId, + currentUser: MatrixUser, style: NotificationCompat.InboxStyle?, compatSummary: String, noisy: Boolean, @@ -240,12 +241,12 @@ class NotificationFactory @Inject constructor( // used in compat < N, after summary is built based on child notifications .setWhen(lastMessageTimestamp) .setStyle(style) - .setContentTitle(sessionId.value) + .setContentTitle(currentUser.userId.value.annotateForDebug(9)) .setCategory(NotificationCompat.CATEGORY_MESSAGE) .setSmallIcon(smallIcon) // set content text to support devices running API level < 24 - .setContentText(compatSummary) - .setGroup(sessionId.value) + .setContentText(compatSummary.annotateForDebug(10)) + .setGroup(currentUser.userId.value) // set this notification as the summary for the group .setGroupSummary(true) .setColor(accentColor) @@ -264,8 +265,8 @@ class NotificationFactory @Inject constructor( priority = NotificationCompat.PRIORITY_LOW } } - .setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(sessionId)) - .setDeleteIntent(pendingIntentFactory.createDismissSummaryPendingIntent(sessionId)) + .setContentIntent(pendingIntentFactory.createOpenSessionPendingIntent(currentUser.userId)) + .setDeleteIntent(pendingIntentFactory.createDismissSummaryPendingIntent(currentUser.userId)) .build() } 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 d7af528ce3..1216e0fe12 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 @@ -57,6 +57,9 @@ data class NotifiableMessageEvent( val description: String = body ?: "" val title: String = senderName ?: "" + // TODO EAx The image has to be downloaded and expose using the file provider. + // Example of value from Element Android: + // content://im.vector.app.debug.mx-sdk.fileprovider/downloads/downloads/816abf76d806c768760568952b1862c8/F/72c33edd23dee3b95f4d5a18aa25fa54/image.png val imageUri: Uri? get() = imageUriString?.let { Uri.parse(it) } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt index 9fbd723071..18d8870ac3 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationFactoryTest.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.push.impl.notifications import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.user.MatrixUser 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_SESSION_ID @@ -27,6 +28,7 @@ import io.element.android.libraries.push.impl.notifications.fake.FakeSummaryGrou import io.element.android.libraries.push.impl.notifications.fixtures.aNotifiableMessageEvent import io.element.android.libraries.push.impl.notifications.fixtures.aSimpleNotifiableEvent import io.element.android.libraries.push.impl.notifications.fixtures.anInviteNotifiableEvent +import kotlinx.coroutines.test.runTest import org.junit.Test private val MY_AVATAR_URL: String? = null @@ -124,11 +126,13 @@ class NotificationFactoryTest { fun `given room with message when mapping to notification then delegates to room group message creator`() = testWith(notificationFactory) { val events = listOf(A_MESSAGE_EVENT) val expectedNotification = roomGroupMessageCreator.givenCreatesRoomMessageFor( - A_SESSION_ID, events, A_ROOM_ID, A_SESSION_ID.value, MY_AVATAR_URL + MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), events, A_ROOM_ID ) val roomWithMessage = mapOf(A_ROOM_ID to listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, A_MESSAGE_EVENT))) - val result = roomWithMessage.toNotifications(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL) + val result = roomWithMessage.toNotifications( + MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL) + ) assertThat(result).isEqualTo(listOf(expectedNotification)) } @@ -138,7 +142,9 @@ class NotificationFactoryTest { val events = listOf(ProcessedEvent(ProcessedEvent.Type.REMOVE, A_MESSAGE_EVENT)) val emptyRoom = mapOf(A_ROOM_ID to events) - val result = emptyRoom.toNotifications(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL) + val result = emptyRoom.toNotifications( + MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL) + ) assertThat(result).isEqualTo( listOf( @@ -153,7 +159,9 @@ class NotificationFactoryTest { fun `given a room with only redacted events when mapping to notification then is Empty`() = testWith(notificationFactory) { val redactedRoom = mapOf(A_ROOM_ID to listOf(ProcessedEvent(ProcessedEvent.Type.KEEP, A_MESSAGE_EVENT.copy(isRedacted = true)))) - val result = redactedRoom.toNotifications(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL) + val result = redactedRoom.toNotifications( + MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL) + ) assertThat(result).isEqualTo( listOf( @@ -176,19 +184,21 @@ class NotificationFactoryTest { ) val withRedactedRemoved = listOf(A_MESSAGE_EVENT.copy(eventId = EventId("\$not-redacted"))) val expectedNotification = roomGroupMessageCreator.givenCreatesRoomMessageFor( - A_SESSION_ID, + MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL), withRedactedRemoved, A_ROOM_ID, - A_SESSION_ID.value, - MY_AVATAR_URL ) - val result = roomWithRedactedMessage.toNotifications(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL) + val result = roomWithRedactedMessage.toNotifications( + MatrixUser(A_SESSION_ID, A_SESSION_ID.value, MY_AVATAR_URL) + ) assertThat(result).isEqualTo(listOf(expectedNotification)) } } -fun testWith(receiver: T, block: T.() -> Unit) { - receiver.block() +fun testWith(receiver: T, block: suspend T.() -> Unit) { + runTest { + receiver.block() + } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt index 79c6dfdb02..c109edb40a 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/NotificationRendererTest.kt @@ -17,6 +17,7 @@ package io.element.android.libraries.push.impl.notifications import android.app.Notification +import io.element.android.libraries.matrix.api.user.MatrixUser 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_SESSION_ID @@ -24,6 +25,7 @@ import io.element.android.libraries.push.impl.notifications.fake.FakeNotificatio import io.element.android.libraries.push.impl.notifications.fake.FakeNotificationFactory import io.element.android.libraries.push.impl.notifications.model.NotifiableEvent import io.mockk.mockk +import kotlinx.coroutines.test.runTest import org.junit.Test private const val MY_USER_DISPLAY_NAME = "display-name" @@ -53,7 +55,7 @@ class NotificationRendererTest { ) @Test - fun `given no notifications when rendering then cancels summary notification`() { + fun `given no notifications when rendering then cancels summary notification`() = runTest { givenNoNotifications() renderEventsAsNotifications() @@ -63,7 +65,7 @@ class NotificationRendererTest { } @Test - fun `given last room message group notification is removed when rendering then remove the summary and then remove message notification`() { + fun `given last room message group notification is removed when rendering then remove the summary and then remove message notification`() = runTest { givenNotifications(roomNotifications = listOf(RoomNotification.Removed(A_ROOM_ID)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION) renderEventsAsNotifications() @@ -75,7 +77,7 @@ class NotificationRendererTest { } @Test - fun `given a room message group notification is removed when rendering then remove the message notification and update summary`() { + fun `given a room message group notification is removed when rendering then remove the message notification and update summary`() = runTest { givenNotifications(roomNotifications = listOf(RoomNotification.Removed(A_ROOM_ID))) renderEventsAsNotifications() @@ -87,7 +89,7 @@ class NotificationRendererTest { } @Test - fun `given a room message group notification is added when rendering then show the message notification and update summary`() { + fun `given a room message group notification is added when rendering then show the message notification and update summary`() = runTest { givenNotifications( roomNotifications = listOf( RoomNotification.Message( @@ -106,7 +108,7 @@ class NotificationRendererTest { } @Test - fun `given last simple notification is removed when rendering then remove the summary and then remove simple notification`() { + fun `given last simple notification is removed when rendering then remove the summary and then remove simple notification`() = runTest { givenNotifications(simpleNotifications = listOf(OneShotNotification.Removed(AN_EVENT_ID.value)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION) renderEventsAsNotifications() @@ -118,7 +120,7 @@ class NotificationRendererTest { } @Test - fun `given a simple notification is removed when rendering then remove the simple notification and update summary`() { + fun `given a simple notification is removed when rendering then remove the simple notification and update summary`() = runTest { givenNotifications(simpleNotifications = listOf(OneShotNotification.Removed(AN_EVENT_ID.value))) renderEventsAsNotifications() @@ -130,7 +132,7 @@ class NotificationRendererTest { } @Test - fun `given a simple notification is added when rendering then show the simple notification and update summary`() { + fun `given a simple notification is added when rendering then show the simple notification and update summary`() = runTest { givenNotifications( simpleNotifications = listOf( OneShotNotification.Append( @@ -149,7 +151,7 @@ class NotificationRendererTest { } @Test - fun `given last invitation notification is removed when rendering then remove the summary and then remove invitation notification`() { + fun `given last invitation notification is removed when rendering then remove the summary and then remove invitation notification`() = runTest { givenNotifications(invitationNotifications = listOf(OneShotNotification.Removed(A_ROOM_ID.value)), summaryNotification = A_REMOVE_SUMMARY_NOTIFICATION) renderEventsAsNotifications() @@ -161,7 +163,7 @@ class NotificationRendererTest { } @Test - fun `given an invitation notification is removed when rendering then remove the invitation notification and update summary`() { + fun `given an invitation notification is removed when rendering then remove the invitation notification and update summary`() = runTest { givenNotifications(invitationNotifications = listOf(OneShotNotification.Removed(A_ROOM_ID.value))) renderEventsAsNotifications() @@ -173,7 +175,7 @@ class NotificationRendererTest { } @Test - fun `given an invitation notification is added when rendering then show the invitation notification and update summary`() { + fun `given an invitation notification is added when rendering then show the invitation notification and update summary`() = runTest { givenNotifications( simpleNotifications = listOf( OneShotNotification.Append( @@ -191,11 +193,9 @@ class NotificationRendererTest { } } - private fun renderEventsAsNotifications() { + private suspend fun renderEventsAsNotifications() { notificationRenderer.render( - sessionId = A_SESSION_ID, - myUserDisplayName = MY_USER_DISPLAY_NAME, - myUserAvatarUrl = MY_USER_AVATAR_URL, + MatrixUser(A_SESSION_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR_URL), useCompleteNotificationFormat = USE_COMPLETE_NOTIFICATION_FORMAT, eventsToProcess = AN_EVENT_LIST ) @@ -214,9 +214,7 @@ class NotificationRendererTest { ) { notificationFactory.givenNotificationsFor( groupedEvents = A_PROCESSED_EVENTS, - sessionId = A_SESSION_ID, - myUserDisplayName = MY_USER_DISPLAY_NAME, - myUserAvatarUrl = MY_USER_AVATAR_URL, + matrixUser = MatrixUser(A_SESSION_ID, MY_USER_DISPLAY_NAME, MY_USER_AVATAR_URL), useCompleteNotificationFormat = useCompleteNotificationFormat, roomNotifications = roomNotifications, invitationNotifications = invitationNotifications, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt index 7d7812e6cb..09957e2cf2 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeNotificationFactory.kt @@ -16,12 +16,13 @@ package io.element.android.libraries.push.impl.notifications.fake -import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.push.impl.notifications.GroupedNotificationEvents import io.element.android.libraries.push.impl.notifications.NotificationFactory import io.element.android.libraries.push.impl.notifications.OneShotNotification import io.element.android.libraries.push.impl.notifications.RoomNotification import io.element.android.libraries.push.impl.notifications.SummaryNotification +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk @@ -30,9 +31,7 @@ class FakeNotificationFactory { fun givenNotificationsFor( groupedEvents: GroupedNotificationEvents, - sessionId: SessionId, - myUserDisplayName: String, - myUserAvatarUrl: String?, + matrixUser: MatrixUser, useCompleteNotificationFormat: Boolean, roomNotifications: List, invitationNotifications: List, @@ -40,13 +39,13 @@ class FakeNotificationFactory { summaryNotification: SummaryNotification ) { with(instance) { - every { groupedEvents.roomEvents.toNotifications(sessionId, myUserDisplayName, myUserAvatarUrl) } returns roomNotifications + coEvery { groupedEvents.roomEvents.toNotifications(matrixUser) } returns roomNotifications every { groupedEvents.invitationEvents.toNotifications() } returns invitationNotifications every { groupedEvents.simpleEvents.toNotifications() } returns simpleNotifications every { createSummaryNotification( - sessionId, + matrixUser, roomNotifications, invitationNotifications, simpleNotifications, diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt index df0b5ad42b..b896737e6f 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/notifications/fake/FakeRoomGroupMessageCreator.kt @@ -17,11 +17,11 @@ package io.element.android.libraries.push.impl.notifications.fake 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.user.MatrixUser import io.element.android.libraries.push.impl.notifications.RoomGroupMessageCreator import io.element.android.libraries.push.impl.notifications.RoomNotification import io.element.android.libraries.push.impl.notifications.model.NotifiableMessageEvent -import io.mockk.every +import io.mockk.coEvery import io.mockk.mockk class FakeRoomGroupMessageCreator { @@ -29,14 +29,18 @@ class FakeRoomGroupMessageCreator { val instance = mockk() fun givenCreatesRoomMessageFor( - sessionId: SessionId, + matrixUser: MatrixUser, events: List, roomId: RoomId, - userDisplayName: String, - userAvatarUrl: String? ): RoomNotification.Message { val mockMessage = mockk() - every { instance.createRoomMessage(sessionId, events, roomId, userDisplayName, userAvatarUrl) } returns mockMessage + coEvery { + instance.createRoomMessage( + currentUser = matrixUser, + events = events, + roomId = roomId, + ) + } returns mockMessage return mockMessage } } diff --git a/plugins/src/main/kotlin/io.element.android-compose-library.gradle.kts b/plugins/src/main/kotlin/io.element.android-compose-library.gradle.kts index 44f1b6265a..e420ab3c8d 100644 --- a/plugins/src/main/kotlin/io.element.android-compose-library.gradle.kts +++ b/plugins/src/main/kotlin/io.element.android-compose-library.gradle.kts @@ -32,14 +32,6 @@ plugins { android { androidConfig(project) composeConfig(libs) - // Waiting for https://github.com/google/ksp/issues/37 - libraryVariants.all { - kotlin.sourceSets { - getByName(name) { - kotlin.srcDir("build/generated/ksp/$name/kotlin") - } - } - } } dependencies { diff --git a/plugins/src/main/kotlin/io.element.android-library.gradle.kts b/plugins/src/main/kotlin/io.element.android-library.gradle.kts index 561c20ffc1..6c3c77223c 100644 --- a/plugins/src/main/kotlin/io.element.android-library.gradle.kts +++ b/plugins/src/main/kotlin/io.element.android-library.gradle.kts @@ -29,14 +29,6 @@ plugins { android { androidConfig(project) - // Waiting for https://github.com/google/ksp/issues/37 - libraryVariants.all { - kotlin.sourceSets { - getByName(name) { - kotlin.srcDir("build/generated/ksp/$name/kotlin") - } - } - } } dependencies { 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_SheetContentDarkPreview_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.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 860002e3dd..78ce1fa217 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_SheetContentDarkPreview_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.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3cb476c16c2cae9f3230cc4030b66662b9f63cef22208fc5bf577d0b16bf946 -size 4484 +oid sha256:a5948ff8260dc73ff19232679312141dd4021a00cce2871f69870b291a237aa9 +size 4478 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_SheetContentDarkPreview_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.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_1,NEXUS_5,1.0,en].png index 860002e3dd..78ce1fa217 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_SheetContentDarkPreview_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.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3cb476c16c2cae9f3230cc4030b66662b9f63cef22208fc5bf577d0b16bf946 -size 4484 +oid sha256:a5948ff8260dc73ff19232679312141dd4021a00cce2871f69870b291a237aa9 +size 4478 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_SheetContentDarkPreview_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.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_2,NEXUS_5,1.0,en].png index 29104a87c0..e552bc8c5b 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_SheetContentDarkPreview_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.actionlist_null_DefaultGroup_SheetContentDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dea394d708a714603ea77543a7ab31550baaea72c75255c56ac9162589096128 -size 14453 +oid sha256:92fc84907bd6a779d10daa894f4c8ed3039ad8019c32a502f487086a8cbc535a +size 33707 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_SheetContentLightPreview_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.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_0,NEXUS_5,1.0,en].png index 4c04d9893f..665c8811ac 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_SheetContentLightPreview_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.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54b434198b8b6b534e0e82310e58eec162f18aba876f0dca9a1790c137230595 -size 4496 +oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b +size 4457 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_SheetContentLightPreview_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.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_1,NEXUS_5,1.0,en].png index 4c04d9893f..665c8811ac 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_SheetContentLightPreview_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.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54b434198b8b6b534e0e82310e58eec162f18aba876f0dca9a1790c137230595 -size 4496 +oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b +size 4457 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_SheetContentLightPreview_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.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_2,NEXUS_5,1.0,en].png index 9734dff6c3..999631b3f9 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_SheetContentLightPreview_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.actionlist_null_DefaultGroup_SheetContentLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f3080445c87d85fd5c51228e33ef7f91eb3a718f2f8288bdfa2a48d6769a25a1 -size 15480 +oid sha256:aaf0c08cc44a092f72e964f53982570ea277be4db8b30045b175abfe5a3eea50 +size 32930 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_TimelineItemFileViewDarkPreview_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_TimelineItemFileViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 04aa33ddc2..44a3d0d1cf 100644 --- 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_TimelineItemFileViewDarkPreview_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_TimelineItemFileViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f5791778cbc29439400d90a83428f672036dde4656b201ea29ecbf43d350e6f3 -size 10205 +oid sha256:ae38e963b9e39fd01c459756e4af7d48ec17269bc81fee64cac32d3d764fa4aa +size 253 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_TimelineItemFileViewDarkPreview_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_TimelineItemFileViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index 67a2bed5bf..abc78dd8a7 100644 --- 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_TimelineItemFileViewDarkPreview_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_TimelineItemFileViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c4684cdcf14f09eaadf4c92a6f3e388ba01d755e7cfd2e3de3cb293b7e96c46 -size 12727 +oid sha256:142983105571815b1d01401a11f4a43615576808932f86f32d12582dc64659fa +size 254 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_TimelineItemFileViewDarkPreview_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_TimelineItemFileViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png index 8a0d88c30f..a3d38efe59 100644 --- 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_TimelineItemFileViewDarkPreview_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_TimelineItemFileViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d0d9e345bf15b1fd136f1c986b0553cddfd137c7c9fa6d85031bdb068056997e -size 23384 +oid sha256:cd81a446ff191d4e67164ac0f0d102112bbb65751e799dd6f2f9f8fa7828a8d6 +size 254 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_TimelineItemFileViewLightPreview_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_TimelineItemFileViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index 9cf9dbe8bb..1cec63c97b 100644 --- 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_TimelineItemFileViewLightPreview_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_TimelineItemFileViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:11643e5ff81b5b35399c7453740349e08c716d282e822787dd87e4bb5fc48127 -size 9698 +oid sha256:c112803bd15d4dce012d15bc1d32ef3fe519d0f59e754e3d1e929d5ac4cbc56f +size 252 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_TimelineItemFileViewLightPreview_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_TimelineItemFileViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index 041bb0422d..cf9d281f8c 100644 --- 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_TimelineItemFileViewLightPreview_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_TimelineItemFileViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f61cb3462c59face987c271302fc7bf397f53a33a065ef8bb4dc7413b6edbc1 -size 11820 +oid sha256:3008c4816434cc89f29868ef88f6dc1663e972ef38d1dedaf70f2561b045341d +size 253 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_TimelineItemFileViewLightPreview_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_TimelineItemFileViewLightPreview_0_null_2,NEXUS_5,1.0,en].png index ce3f1a26ee..c19c8d4a21 100644 --- 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_TimelineItemFileViewLightPreview_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_TimelineItemFileViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e56a7fbd352e79f1fec994d86e6c31bfd62be4ed2ac81cc07fd9fa6beab815b3 -size 20972 +oid sha256:5b1189ba60a167ff0c273386f488e8fccf3c2579596c2ec51de9a3b74a6c8275 +size 254 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 618e708fa5..db345ddc5c 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:701bc1aa61f85da1c5750b7c0a55e5c7bc7940b59faa55bdbbfb9bf5e7a234fb -size 61452 +oid sha256:02677f338b9320bec5591066a0e400dade1bea3892c8cac295cba77535ae8aa3 +size 254 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 4d55805ea7..e5159a8646 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:df29f11b68678f437ee2dda4c96a44a943d2b7c960c9d95cdf9b2307c00afc42 -size 72684 +oid sha256:53447ea06f291b0bd1a7e4a1c1d7ba5db3a398ca078eee79db0748a5fbadb378 +size 254 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 5519a1ce4f..e52df506f5 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:1da2134e49960c0a092acdf37ff02d258b0beb520186c9bfedb41447f42121d9 -size 61721 +oid sha256:64a485f83f02524667588c5f0add1691d050f8dd852751c3d82e77a3031fe674 +size 254 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 9c88293110..9348e58303 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:1ac9ba0d9c94ff7c081816a0fb5cb9f97fe09ee2515ee0e82cdb9c78195bf72e -size 73584 +oid sha256:4c5d33ac21bf1759e4c12fc306d184038ae956f9a73a382b727a34822f96d094 +size 254 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 f1bc47d485..ccca384a12 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:49759fbb46a966d0e0f1c793545e320e7588ac482399a12ac45762474e6781f8 -size 45579 +oid sha256:ea20b55e31ad840d0eff1f1d607dbb4e1fe7d988a33ddc0c8ddb83e876cdb041 +size 254 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 c28a1d3ac7..308caca034 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:cdca9e94c071c20753aa2076f6c3a7ed931a2c1e90a78c29e00f27f7af99dfe8 -size 45758 +oid sha256:124cdb3c001741f9415443b61508bf4cd59ee6ebe262b7d9a0d5a1233952137e +size 254 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 6864f509ba..0d1bbeb804 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:4e47966820a6a77efece51aaac5adbe3dd1dad1880b1feb7da2e86e36b386f7e -size 43815 +oid sha256:1c48c1d5dbdd1a29ed84694dc70c1b1aa1eee4289da062b74fe58e1279175fb8 +size 254 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 7009f86be7..d76b1deb98 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:bc3fc4c97a43c5786a75328e35828ed0e0cfd74ffab673832053ecc11db9b5e0 -size 44852 +oid sha256:4f7fec36f8c49fcb1257adb92260d23c8b67f335deb078253a619e47cc586afd +size 254 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 823b19bf1c..1b69f8243f 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:7f82a51b6b6ed4ac030c4602da9da2629010169e998aaba16f9e05c652f6010d -size 45283 +oid sha256:3ff77d9c4459bc19e9f1f1b3dcfcdba99d8f04de00b2a141763be0c80679bd80 +size 254 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 49d46e6e23..680421dd05 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:5d18843f61267ae7385c23113be5c5c3d4376d95a843eef126ed670877f69b80 -size 42281 +oid sha256:212eda172bcd89e10e32b9f89f7fc0489c1d08bd0a2f78014435b67a67dacfeb +size 254 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 new file mode 100644 index 0000000000..d8bebb7201 --- /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_OnBoardingScreenDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e7ee7df10cd863d2ec989fa4dc6621d38464de9ce640947a392c2397f53eb8c +size 254906 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 new file mode 100644 index 0000000000..d930025396 --- /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_OnBoardingScreenDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5e22509b6b519792ec8927c543373abe3976335aab96c72d99d40c8f924b410d +size 244289 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 new file mode 100644 index 0000000000..4a4853308a --- /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_OnBoardingScreenDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3661424e8a5268b0606e5d70ea4cf6e42133c03d8ad88190669b887e9a3197d4 +size 254282 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 new file mode 100644 index 0000000000..12155debdf --- /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_OnBoardingScreenDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9efc0ec64518c059d10edbfa4b1136237330a5bd9a1000a444a18df9044e788 +size 241109 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 new file mode 100644 index 0000000000..16881ad056 --- /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_OnBoardingScreenLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:75045402335b2f02962c64cb0884b7e6132b8314bcfc225b6c71ebf303e13675 +size 241702 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 new file mode 100644 index 0000000000..62c0cafdec --- /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_OnBoardingScreenLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9fb6b0905faf223fd0505197060ff2838329435a2588a44c200277a5ed34083 +size 231701 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 new file mode 100644 index 0000000000..1fcf7d32aa --- /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_OnBoardingScreenLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96d3d1147d02b49eb206cd0f051882cdb7ce7f66088e1d01b62f30cf9469d5a1 +size 241589 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 new file mode 100644 index 0000000000..29d66a93e6 --- /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_OnBoardingScreenLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec6523d971baf66b6946334927069491c7203345aeda820cac35e168d95779bf +size 227789 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 new file mode 100644 index 0000000000..8ac535e6c0 --- /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_OnBoardingPageDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:237bf6506435a954985cccc4f40333a02f8d74a11b57671414442f09706aa6c1 +size 253095 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 new file mode 100644 index 0000000000..269a829676 --- /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_OnBoardingPageLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b9e801ae788e63a0737d4ae043cfdbeebbda9de07f925d15ceebb0ba1467795 +size 241513