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